Fix desyncs when multiple clients connected

This commit is contained in:
Max Goodhart
2025-06-23 01:31:06 -07:00
parent 1031f6f8f8
commit cc8baead06
2 changed files with 33 additions and 14 deletions

View File

@@ -29,10 +29,19 @@ const base62 = baseX(
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
) )
function rand62(len: number) { export function rand62(len: number) {
return base62.encode(randomBytes(len)) return base62.encode(randomBytes(len))
} }
export function uniqueRand62(len: number, map: Map<string, unknown>) {
let val = rand62(len)
while (map.has(val)) {
// Regenerate in case of a collision
val = rand62(len)
}
return val
}
async function hashToken62(token: string, salt: string) { async function hashToken62(token: string, salt: string) {
const hashBuffer = await scrypt(token, salt, 24) const hashBuffer = await scrypt(token, salt, 24)
return base62.encode(hashBuffer as Buffer) return base62.encode(hashBuffer as Buffer)
@@ -162,12 +171,7 @@ export class Auth extends EventEmitter<AuthEvents> {
throw new Error(`invalid role: ${role}`) throw new Error(`invalid role: ${role}`)
} }
let tokenId = rand62(8) const tokenId = uniqueRand62(8, this.tokensById)
while (this.tokensById.has(tokenId)) {
// Regenerate in case of an id collision
tokenId = rand62(8)
}
const secret = rand62(24) const secret = rand62(24)
const tokenHash = await hashToken62(secret, this.salt) const tokenHash = await hashToken62(secret, this.salt)
const tokenData = { const tokenData = {

View File

@@ -16,12 +16,13 @@ import {
stateDiff, stateDiff,
type StreamwallRole, type StreamwallRole,
} from 'streamwall-shared' } from 'streamwall-shared'
import { Auth, StateWrapper } from './auth.ts' import { Auth, StateWrapper, uniqueRand62 } from './auth.ts'
import { loadStorage, type StorageDB } from './storage.ts' import { loadStorage, type StorageDB } from './storage.ts'
export const SESSION_COOKIE_NAME = 's' export const SESSION_COOKIE_NAME = 's'
interface Client { interface Client {
clientId: string
ws: WebSocket ws: WebSocket
lastStateSent: any lastStateSent: any
identity: AuthTokenInfo identity: AuthTokenInfo
@@ -248,7 +249,7 @@ async function initApp({ baseURL, clientStaticPath }: AppOptions) {
console.error('Failed to send Streamwall doc update') console.error('Failed to send Streamwall doc update')
} }
for (const client of clients.values()) { for (const client of clients.values()) {
if (client.identity.tokenId === origin) { if (client.clientId === origin) {
continue continue
} }
try { try {
@@ -299,25 +300,39 @@ async function initApp({ baseURL, clientStaticPath }: AppOptions) {
return return
} }
const clientId = uniqueRand62(8, clients)
const client: Client = { const client: Client = {
clientId,
ws, ws,
lastStateSent: null, lastStateSent: null,
identity, identity,
} }
clients.set(identity.tokenId, client) clients.set(clientId, client)
const pingInterval = setInterval(() => { const pingInterval = setInterval(() => {
ws.ping() ws.ping()
}, 20 * 1000) }, 20 * 1000)
ws.on('close', () => { ws.on('close', () => {
clients.delete(identity.tokenId) clients.delete(clientId)
clearInterval(pingInterval) clearInterval(pingInterval)
console.log('Client disconnected from', request.ip, client.identity) console.log(
'Client',
clientId,
'disconnected from',
request.ip,
client.identity,
)
}) })
console.log('Client connected from', request.ip, client.identity) console.log(
'Client',
clientId,
'connected from',
request.ip,
client.identity,
)
handleMessage(async (rawData) => { handleMessage(async (rawData) => {
let msg: ControlCommandMessage let msg: ControlCommandMessage
@@ -350,7 +365,7 @@ async function initApp({ baseURL, clientStaticPath }: AppOptions) {
Y.applyUpdate( Y.applyUpdate(
streamwallConn.stateDoc, streamwallConn.stateDoc,
new Uint8Array(rawData), new Uint8Array(rawData),
identity.tokenId, clientId,
) )
return return
} }