Skip to content

Commit

Permalink
feat(server-sdk): authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
nekomeowww committed Jan 16, 2025
1 parent be0dc65 commit 7b7fb2d
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 46 deletions.
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default antfu(
ignores: [
'**/assets/js/**',
'**/assets/live2d/models/**',
'packages/stage-tamagotchi/out/**',
],
},
)
100 changes: 66 additions & 34 deletions packages/server-runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,81 @@
import type { WebSocketEvent } from '@proj-airi/server-shared/types'
import type { Peer } from 'crossws'
import type { AuthenticatedPeer } from './types'
import { env } from 'node:process'
import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel, useLogg } from '@guiiai/logg'
import { createApp, createRouter, defineWebSocketHandler } from 'h3'

setGlobalFormat(Format.Pretty)
setGlobalLogLevel(LogLevel.Log)

const appLogger = useLogg('App').useGlobalConfig()
const websocketLogger = useLogg('WebSocket').useGlobalConfig()
function send(peer: Peer, event: WebSocketEvent) {
peer.send(JSON.stringify(event))
}

export const app = createApp({
onError: error => appLogger.withError(error).error('an error occurred'),
})
function main() {
const appLogger = useLogg('App').useGlobalConfig()
const websocketLogger = useLogg('WebSocket').useGlobalConfig()

const router = createRouter()
app.use(router)
const app = createApp({
onError: error => appLogger.withError(error).error('an error occurred'),
})

const peers = new Set<Peer>()
const router = createRouter()
app.use(router)

router.get('/ws', defineWebSocketHandler({
open: (peer) => {
peers.add(peer)
websocketLogger.withFields({ peer: peer.id, activePeers: peers.size }).log('connected')
},
message: (peer, message) => {
const event = message.json() as WebSocketEvent
const peers = new Map<string, AuthenticatedPeer>()

switch (event.type) {
case 'input:text':
break
case 'input:text:voice':
break
}
router.get('/ws', defineWebSocketHandler({
open: (peer) => {
const token = env.AUTHENTICATION_TOKEN || ''
if (token) {
peers.set(peer.id, { peer, authenticated: false })
}
else {
send(peer, { type: 'module:authenticated', data: { authenticated: true } })
peers.set(peer.id, { peer, authenticated: true })
}

websocketLogger.withFields({ peer: peer.id, activePeers: peers.size }).log('connected')
},
message: (peer, message) => {
const token = env.AUTHENTICATION_TOKEN || ''
const event = message.json() as WebSocketEvent

for (const p of peers) {
if (p.id !== peer.id) {
p.send(JSON.stringify(event))
switch (event.type) {
case 'module:authenticate':
if (!!token && event.data.token !== token) {
websocketLogger.withFields({ peer: peer.id }).debug('authentication failed')
send(peer, { type: 'error', data: { message: 'invalid token' } })
return
}

send(peer, { type: 'module:authenticated', data: { authenticated: true } })
peers.set(peer.id, { peer, authenticated: true })
return
}
if (!peers.get(peer.id)?.authenticated) {
websocketLogger.withFields({ peer: peer.id }).debug('not authenticated')
send(peer, { type: 'error', data: { message: 'not authenticated' } })
return
}
}
},
error: (peer, error) => {
websocketLogger.withFields({ peer: peer.id }).withError(error).error('an error occurred')
},
close: (peer, details) => {
websocketLogger.withFields({ peer: peer.id, details, activePeers: peers.size }).log('closed')
peers.delete(peer)
},
}))

for (const [id, p] of peers.entries()) {
if (id !== peer.id) {
p.peer.send(JSON.stringify(event))
}
}
},
error: (peer, error) => {
websocketLogger.withFields({ peer: peer.id }).withError(error).error('an error occurred')
},
close: (peer, details) => {
websocketLogger.withFields({ peer: peer.id, details, activePeers: peers.size }).log('closed')
peers.delete(peer.id)
},
}))

return app
}

export const app = main()
6 changes: 6 additions & 0 deletions packages/server-runtime/src/types/conn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Peer } from 'crossws'

export interface AuthenticatedPeer {
peer: Peer
authenticated: boolean
}
1 change: 1 addition & 0 deletions packages/server-runtime/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './conn'
73 changes: 61 additions & 12 deletions packages/server-sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,77 @@ import type { WebSocketBaseEvent, WebSocketEvent, WebSocketEvents } from '@proj-
import type { Blob } from 'node:buffer'
import WebSocket from 'crossws/websocket'
import { defu } from 'defu'
import { sleep } from './utils'

export interface ClientOptions {
url?: string
name: string
possibleEvents?: Array<(keyof WebSocketEvents)>
token?: string
}

export class Client {
private opts: Required<ClientOptions>
private websocket: WebSocket
private eventListeners: Map<keyof WebSocketEvents, Array<(data: WebSocketBaseEvent<keyof WebSocketEvents, WebSocketEvents[keyof WebSocketEvents]>) => void | Promise<void>>> = new Map()
private eventListeners: Map<
keyof WebSocketEvents,
Array<(data: WebSocketBaseEvent<
keyof WebSocketEvents,
WebSocketEvents[keyof WebSocketEvents]
>) => void | Promise<void>>
> = new Map()

private authenticateAttempts = 0

constructor(options: ClientOptions) {
const opts = defu<Required<ClientOptions>, Required<Omit<ClientOptions, 'name'>>[]>(options, { url: 'ws://localhost:6121/ws', possibleEvents: [] })
const opts = defu<Required<ClientOptions>, Required<Omit<ClientOptions, 'name' | 'token'>>[]>(
options,
{ url: 'ws://localhost:6121/ws', possibleEvents: [] },
)

this.websocket = new WebSocket(opts.url)

this.onEvent('module:authenticated', async (event) => {
const auth = event.data.authenticated
if (!auth) {
this.authenticateAttempts++
await sleep(2 ** this.authenticateAttempts * 1000)
this.tryAuthenticate()
}
else {
this.tryAnnounce()
}
})

this.websocket.onmessage = this.handleMessage.bind(this)

this.websocket.onopen = () => {
this.send({
type: 'module:announce',
data: {
name: opts.name,
possibleEvents: opts.possibleEvents,
},
})
if (opts.token) {
this.tryAuthenticate()
}
else {
this.tryAnnounce()
}
}

this.websocket.onclose = () => {
this.authenticateAttempts = 0
}
}

private tryAnnounce() {
this.send({
type: 'module:announce',
data: {
name: this.opts.name,
possibleEvents: this.opts.possibleEvents,
},
})
}

private tryAuthenticate() {
if (this.opts.token) {
this.send({ type: 'module:authenticate', data: { token: this.opts.token || '' } })
}
}

Expand All @@ -35,12 +82,14 @@ export class Client {
if (!listeners)
return

for (const listener of listeners) {
for (const listener of listeners)
await listener(data)
}
}

onEvent<E extends keyof WebSocketEvents>(event: E, callback: (data: WebSocketBaseEvent<E, WebSocketEvents[E]>) => void | Promise<void>): void {
onEvent<E extends keyof WebSocketEvents>(
event: E,
callback: (data: WebSocketBaseEvent<E, WebSocketEvents[E]>) => void | Promise<void>,
): void {
if (!this.eventListeners.get(event)) {
this.eventListeners.set(event, [])
}
Expand Down
3 changes: 3 additions & 0 deletions packages/server-sdk/src/utils/concurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
1 change: 1 addition & 0 deletions packages/server-sdk/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './concurrency'
9 changes: 9 additions & 0 deletions packages/server-shared/src/types/websocket/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ export type WithInputSource<Source extends keyof InputSource> = {
// A little hack for creating extensible discriminated unions : r/typescript
// https://www.reddit.com/r/typescript/comments/1064ibt/a_little_hack_for_creating_extensible/
export interface WebSocketEvents {
'error': {
message: string
}
'module:authenticate': {
token: string
}
'module:authenticated': {
authenticated: boolean
}
'module:announce': {
name: string
possibleEvents: Array<(keyof WebSocketEvents)>
Expand Down

0 comments on commit 7b7fb2d

Please sign in to comment.