feat(server-sdk): authentication
This commit is contained in:
@@ -8,6 +8,7 @@ export default antfu(
|
||||
ignores: [
|
||||
'**/assets/js/**',
|
||||
'**/assets/live2d/models/**',
|
||||
'packages/stage-tamagotchi/out/**',
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
for (const p of peers) {
|
||||
if (p.id !== peer.id) {
|
||||
p.send(JSON.stringify(event))
|
||||
router.get('/ws', defineWebSocketHandler({
|
||||
open: (peer) => {
|
||||
const token = env.AUTHENTICATION_TOKEN || ''
|
||||
if (token) {
|
||||
peers.set(peer.id, { peer, authenticated: false })
|
||||
}
|
||||
}
|
||||
},
|
||||
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)
|
||||
},
|
||||
}))
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Peer } from 'crossws'
|
||||
|
||||
export interface AuthenticatedPeer {
|
||||
peer: Peer
|
||||
authenticated: boolean
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './conn'
|
||||
@@ -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 || '' } })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, [])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './concurrency'
|
||||
@@ -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)>
|
||||
|
||||
Reference in New Issue
Block a user