const http = require('node:http'); const path = require('node:path'); const { BRIDGE_PROTOCOL_VERSION, BRIDGE_SERVER_VERSION, DEFAULT_REMOTE_HOST, DEFAULT_REMOTE_PORT, DEV_SERVER_PORTS, PORT_SCAN_RANGE, REMOTE_ASSETS_PREFIX, REMOTE_BASE_PATH, REMOTE_HEALTH_PATH, REMOTE_WS_PATH, WS_OPCODE, } = require('./constants'); const { createBridgeLogger } = require('./logger'); const { normalizeRemoteCommandAction, normalizeRemoteCommandResult, } = require('./normalizers'); const { createBridgeState } = require('./bridge-state'); const { validateClientMessage, validateSetValueTarget } = require('./protocol-router'); const { getAdvertisedUrls, serveFile } = require('./server-files'); const { cloneValue, isValidPort, safeString } = require('./utils'); const { closeClient, createAcceptKey, FRAME_TOO_LARGE_ERROR, makeFrame, parseFrame, sendJson, } = require('./websocket-codec'); import type { BridgeOptions } from './types'; function createBridgeServer(options: BridgeOptions = {}) { const preferredPort = Number(options.port || process.env.WAND_REMOTE_PORT || DEFAULT_REMOTE_PORT); let port = isValidPort(preferredPort) ? preferredPort : DEFAULT_REMOTE_PORT; const maxPort = Number(options.maxPort || process.env.WAND_REMOTE_MAX_PORT || port + PORT_SCAN_RANGE); const host = options.host || process.env.WAND_REMOTE_HOST || DEFAULT_REMOTE_HOST; const panelRoot = options.panelRoot || path.dirname(__dirname); const clients = new Set(); const log = createBridgeLogger(options); let advertisedUrls: string[] = []; let setValueHandler: any = null; let commandHandler: any = null; let listening = false; const bridgeState = createBridgeState({ clients, log, getServerInfo: () => ({ advertisedUrls, listening, remoteUrl: globalThis.__wandRemoteBridgeUrl, }), }); function setAdvertisedPort(nextPort) { port = nextPort; advertisedUrls = getAdvertisedUrls(port); globalThis.__wandRemoteBridgeUrl = advertisedUrls.find((entry) => !entry.includes('localhost')) || advertisedUrls[0]; } function setHandler(handler) { setValueHandler = typeof handler === 'function' ? handler : null; } function setCommandHandler(handler) { commandHandler = typeof handler === 'function' ? handler : null; } function handleRequest(request, response) { const url = parseRequestUrl(request.url); if (!url) { response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' }); response.end('Bad Request'); return; } if (url.pathname === '/' || url.pathname === '') { response.writeHead(302, { Location: REMOTE_BASE_PATH }); response.end(); return; } if (url.pathname === REMOTE_BASE_PATH.slice(0, -1)) { response.writeHead(302, { Location: REMOTE_BASE_PATH }); response.end(); return; } if (url.pathname === REMOTE_BASE_PATH) { serveFile(response, path.join(panelRoot, 'index.html')); return; } if (url.pathname === REMOTE_HEALTH_PATH) { response.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); response.end(JSON.stringify(bridgeState.buildHealthPayload())); return; } if (url.pathname.startsWith(REMOTE_ASSETS_PREFIX)) { serveFile(response, path.join(panelRoot, url.pathname.replace(REMOTE_BASE_PATH, ''))); return; } response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); response.end('Not found'); } async function handleRemoteCommandMessage(client, message) { const action = normalizeRemoteCommandAction(message.payload?.action); const gameId = typeof message.payload?.gameId === 'string' || typeof message.payload?.gameId === 'number' ? String(message.payload.gameId) : null; const titleId = typeof message.payload?.titleId === 'string' || typeof message.payload?.titleId === 'number' ? String(message.payload.titleId) : null; if (!action) { sendJson(client, 'error', { code: 'invalid_command', message: 'Unknown remote command.', }, message.requestId ?? null); return; } const fallback = { action, gameId, titleId }; if (action === 'launch' && !gameId) { sendJson(client, 'remote_command_result', normalizeRemoteCommandResult({ ok: false, error: { code: 'invalid_game', message: 'A game id is required to launch a trainer.', }, }, fallback), message.requestId ?? null); return; } if (!commandHandler) { sendJson(client, 'remote_command_result', normalizeRemoteCommandResult({ ok: false, error: { code: 'bridge_not_ready', message: 'The local bridge is not ready to execute remote game commands yet.', }, }, fallback), message.requestId ?? null); return; } try { const result = await Promise.resolve(commandHandler({ action, gameId, titleId })); sendJson(client, 'remote_command_result', normalizeRemoteCommandResult(result, fallback), message.requestId ?? null); } catch (error) { log('warn', 'Remote command handler failed.', error); sendJson(client, 'remote_command_result', normalizeRemoteCommandResult({ ok: false, error: { code: 'command_failed', message: 'Failed to execute the remote command.', }, }, fallback), message.requestId ?? null); } } async function handleSetValueMessage(client, message) { const currentSnapshot = bridgeState.snapshot; const validation = validateSetValueTarget(message, currentSnapshot); if (!validation.ok) { sendJson(client, 'set_value_result', { ok: false, trainerId: currentSnapshot?.trainerMeta?.trainer?.trainerId || '', target: safeString(message.payload?.target), error: validation.error, }, message.requestId ?? null); return; } const { target } = validation; if (!setValueHandler) { sendJson(client, 'set_value_result', { ok: false, trainerId: currentSnapshot.trainerMeta.trainer.trainerId, target, error: { code: 'bridge_not_ready', message: 'The local bridge is not ready to write trainer values yet.', }, }, message.requestId ?? null); return; } let result = false; try { result = await Promise.resolve(setValueHandler({ trainerId: currentSnapshot.trainerMeta.trainer.trainerId, target, value: cloneValue(validation.value), cheatId: typeof message.payload?.cheatId === 'string' ? message.payload.cheatId : undefined, })); } catch (error) { log('warn', 'Set-value handler failed.', error); sendJson(client, 'set_value_result', { ok: false, trainerId: currentSnapshot.trainerMeta.trainer.trainerId, target, error: { code: 'set_failed', message: 'Failed to set trainer value.', }, }, message.requestId ?? null); return; } if (!result) { sendJson(client, 'set_value_result', { ok: false, trainerId: currentSnapshot.trainerMeta.trainer.trainerId, target, error: { code: 'set_rejected', message: 'The trainer rejected the requested value.', }, }, message.requestId ?? null); return; } sendJson(client, 'set_value_result', { ok: true, trainerId: currentSnapshot.trainerMeta.trainer.trainerId, target, }, message.requestId ?? null); } async function handleClientMessage(client, message) { const validation = validateClientMessage(message, client.handshaken); if (!validation.ok) { sendJson(client, 'error', validation.error, message?.requestId ?? null); return; } if (message?.type === 'hello') { client.handshaken = true; sendJson(client, 'hello_ack', { sessionId: `sess_${Date.now()}`, accepted: true, serverVersion: BRIDGE_SERVER_VERSION, protocolVersion: BRIDGE_PROTOCOL_VERSION, remoteUrl: globalThis.__wandRemoteBridgeUrl, advertisedUrls, }, message.requestId ?? null); bridgeState.sendSnapshot(client); return; } if (message?.type === 'remote_command') { await handleRemoteCommandMessage(client, message); return; } if (message?.type === 'set_value') { await handleSetValueMessage(client, message); } } function bindSocket(socket) { const client = { socket, buffer: Buffer.alloc(0), closed: false, handshaken: false, }; clients.add(client); socket.on('data', async (chunk) => { try { client.buffer = Buffer.concat([client.buffer, chunk]); while (client.buffer.length > 0) { const frame = parseFrame(client.buffer); if (!frame) { return; } client.buffer = client.buffer.subarray(frame.bytesConsumed); if (!frame.fin) { closeClient(client, 1003, 'Fragmented frames are not supported.'); return; } if (frame.opcode === WS_OPCODE.CLOSE) { closeClient(client, 1000, 'Closing'); return; } if (frame.opcode === WS_OPCODE.PING) { client.socket.write(makeFrame(WS_OPCODE.PONG, frame.payload)); continue; } if (frame.opcode !== WS_OPCODE.TEXT) { continue; } await handleClientMessage(client, JSON.parse(frame.payload.toString('utf8'))); } } catch (error) { if (error instanceof Error && 'code' in error && error.code === FRAME_TOO_LARGE_ERROR) { closeClient(client, 1009, error.message); return; } sendJson(client, 'error', { code: 'invalid_message', message: error instanceof Error ? error.message : 'Failed to process client message.', }); } }); socket.on('close', () => { client.closed = true; clients.delete(client); }); socket.on('end', () => { client.closed = true; clients.delete(client); }); socket.on('error', (error) => { client.closed = true; clients.delete(client); log('warn', 'WebSocket client error.', error); }); } function handleUpgrade(request, socket) { const url = parseRequestUrl(request.url); if (!url) { rejectUpgrade(socket, 400, 'Bad Request'); return; } if (url.pathname !== REMOTE_WS_PATH) { rejectUpgrade(socket, 404, 'Not Found'); return; } if (!isAllowedWebSocketOrigin(request.headers.origin, request.headers.host)) { rejectUpgrade(socket, 403, 'Forbidden'); return; } const key = request.headers['sec-websocket-key']; if (typeof key !== 'string' || !key) { rejectUpgrade(socket, 400, 'Bad Request'); return; } socket.write([ 'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${createAcceptKey(key)}`, '', '', ].join('\r\n')); bindSocket(socket); } function listen(nextPort) { setAdvertisedPort(nextPort); server.listen(port, host); } setAdvertisedPort(port); log('info', `Bridge starting (pid=${process.pid}, panelRoot=${panelRoot}, preferredPort=${port}, host=${host})`); globalThis.__wandRemoteBridgeLogFile = log.file; const server = http.createServer(handleRequest); server.on('upgrade', handleUpgrade); server.on('error', (error) => { if (!listening && error && error.code === 'EADDRINUSE' && port < maxPort) { const nextPort = port + 1; log('warn', `Port ${port} is busy, trying ${nextPort}.`); listen(nextPort); return; } log('warn', `Bridge server error on ${host}:${port}.`, error); }); server.on('listening', () => { listening = true; log('info', `Listening on ${host}:${port}.`); }); listen(port); return { get advertisedUrls() { return advertisedUrls.slice(); }, get listening() { return listening; }, get remoteUrl() { return globalThis.__wandRemoteBridgeUrl; }, close() { for (const client of clients) { closeClient(client); } clients.clear(); bridgeState.clear(); listening = false; server.close(); }, setCommandHandler, setHandler, sync: bridgeState.sync, syncTrainerMeta: bridgeState.syncTrainerMeta, syncGameStatus: bridgeState.syncGameStatus, syncInstalledApps: bridgeState.syncInstalledApps, valueChanged: bridgeState.valueChanged, }; } function parseRequestUrl(requestUrl) { try { return new URL(requestUrl || '/', 'http://localhost'); } catch { return null; } } function isAllowedWebSocketOrigin(origin, host) { if (origin === undefined) { return true; } if (typeof origin !== 'string' || typeof host !== 'string') { return false; } try { const parsed = new URL(origin); const requested = new URL(`http://${host}`); if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { return false; } const sameHostname = parsed.hostname.toLowerCase() === requested.hostname.toLowerCase(); const compatibleLoopback = isLoopback(parsed.hostname) && isLoopback(requested.hostname); return parsed.host.toLowerCase() === host.toLowerCase() || DEV_SERVER_PORTS.includes(parsed.port) && (sameHostname || compatibleLoopback); } catch { return false; } } function isLoopback(hostname) { return ['localhost', '127.0.0.1', '[::1]', '::1'].includes(hostname.toLowerCase()); } function rejectUpgrade(socket, statusCode, statusText) { socket.end([ `HTTP/1.1 ${statusCode} ${statusText}`, 'Connection: close', 'Content-Length: 0', '', '', ].join('\r\n')); } module.exports = { createBridgeServer, };