All checks were successful
ci/woodpecker/push/push Pipeline was successful
Co-authored-by: Madison Grubb <madison@elastiflow.com> Reviewed-on: #4
154 lines
4.1 KiB
JavaScript
154 lines
4.1 KiB
JavaScript
import { WebSocketServer } from 'ws'
|
|
import { getDb } from '../utils/db.js'
|
|
import { handleWebSocketMessage } from '../utils/webrtcSignaling.js'
|
|
|
|
function parseCookie(cookieHeader) {
|
|
const cookies = {}
|
|
if (!cookieHeader) return cookies
|
|
cookieHeader.split(';').forEach((cookie) => {
|
|
const [name, ...valueParts] = cookie.trim().split('=')
|
|
if (name && valueParts.length > 0) {
|
|
cookies[name] = decodeURIComponent(valueParts.join('='))
|
|
}
|
|
})
|
|
return cookies
|
|
}
|
|
|
|
let wss = null
|
|
const connections = new Map()
|
|
|
|
export function getWebSocketServer() {
|
|
return wss
|
|
}
|
|
|
|
export function getSessionConnections(sessionId) {
|
|
return connections.get(sessionId) || new Set()
|
|
}
|
|
|
|
export function addSessionConnection(sessionId, ws) {
|
|
if (!connections.has(sessionId)) {
|
|
connections.set(sessionId, new Set())
|
|
}
|
|
connections.get(sessionId).add(ws)
|
|
}
|
|
|
|
export function removeSessionConnection(sessionId, ws) {
|
|
const conns = connections.get(sessionId)
|
|
if (conns) {
|
|
conns.delete(ws)
|
|
if (conns.size === 0) {
|
|
connections.delete(sessionId)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function broadcastToSession(sessionId, message) {
|
|
const conns = getSessionConnections(sessionId)
|
|
const data = JSON.stringify(message)
|
|
for (const ws of conns) {
|
|
if (ws.readyState === 1) { // OPEN
|
|
ws.send(data)
|
|
}
|
|
}
|
|
}
|
|
|
|
export default defineNitroPlugin((nitroApp) => {
|
|
nitroApp.hooks.hook('ready', async () => {
|
|
const server = nitroApp.h3App.server || nitroApp.h3App.nodeServer
|
|
if (!server) {
|
|
console.warn('[websocket] Could not attach to HTTP server')
|
|
return
|
|
}
|
|
|
|
wss = new WebSocketServer({
|
|
server,
|
|
path: '/ws',
|
|
verifyClient: async (info, callback) => {
|
|
// Verify session cookie on upgrade request
|
|
const cookies = parseCookie(info.req.headers.cookie || '')
|
|
const sessionId = cookies.session_id
|
|
if (!sessionId) {
|
|
callback(false, 401, 'Unauthorized')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const { get } = await getDb()
|
|
const session = await get('SELECT user_id, expires_at FROM sessions WHERE id = ?', [sessionId])
|
|
if (!session || new Date(session.expires_at) < new Date()) {
|
|
callback(false, 401, 'Unauthorized')
|
|
return
|
|
}
|
|
// Store user_id in request for later use
|
|
info.req.userId = session.user_id
|
|
callback(true)
|
|
}
|
|
catch (err) {
|
|
console.error('[websocket] Auth error:', err)
|
|
callback(false, 500, 'Internal Server Error')
|
|
}
|
|
},
|
|
})
|
|
|
|
wss.on('connection', (ws, req) => {
|
|
const userId = req.userId
|
|
if (!userId) {
|
|
ws.close(1008, 'Unauthorized')
|
|
return
|
|
}
|
|
|
|
let currentSessionId = null
|
|
|
|
ws.on('message', async (data) => {
|
|
try {
|
|
const message = JSON.parse(data.toString())
|
|
const { sessionId, type } = message
|
|
|
|
if (!sessionId || !type) {
|
|
ws.send(JSON.stringify({ error: 'Invalid message format' }))
|
|
return
|
|
}
|
|
|
|
// Track session connection
|
|
if (currentSessionId !== sessionId) {
|
|
if (currentSessionId) {
|
|
removeSessionConnection(currentSessionId, ws)
|
|
}
|
|
currentSessionId = sessionId
|
|
addSessionConnection(sessionId, ws)
|
|
}
|
|
|
|
// Handle WebRTC signaling message
|
|
const response = await handleWebSocketMessage(userId, sessionId, type, message.data || {})
|
|
if (response) {
|
|
ws.send(JSON.stringify(response))
|
|
}
|
|
}
|
|
catch (err) {
|
|
console.error('[websocket] Message error:', err)
|
|
ws.send(JSON.stringify({ error: err.message || 'Internal error' }))
|
|
}
|
|
})
|
|
|
|
ws.on('close', () => {
|
|
if (currentSessionId) {
|
|
removeSessionConnection(currentSessionId, ws)
|
|
}
|
|
})
|
|
|
|
ws.on('error', (err) => {
|
|
console.error('[websocket] Connection error:', err)
|
|
})
|
|
})
|
|
|
|
console.log('[websocket] WebSocket server started on /ws')
|
|
})
|
|
|
|
nitroApp.hooks.hook('close', () => {
|
|
if (wss) {
|
|
wss.close()
|
|
wss = null
|
|
}
|
|
})
|
|
})
|