(server)
| 104 | } |
| 105 | |
| 106 | function setupWebSocketServer(server) { |
| 107 | const wss = new WebSocketServer({ server }); |
| 108 | state.wss = wss; |
| 109 | |
| 110 | wss.on('connection', (ws, req) => { |
| 111 | ws._bridgeId = ++state.nextWsId; |
| 112 | ws._clientInstanceId = ''; |
| 113 | ws._authenticated = state.AUTH_DISABLED; |
| 114 | ws._approvalMode = 'default'; |
| 115 | ws._authTimer = null; |
| 116 | log(`WS connected: ${wsLabel(ws)} remote=${req.socket.remoteAddress || '?'} ua=${JSON.stringify(req.headers['user-agent'] || '')} authRequired=${!state.AUTH_DISABLED}`); |
| 117 | |
| 118 | if (state.AUTH_DISABLED) { |
| 119 | sendAuthOk(ws); |
| 120 | sendInitialMessages(ws); |
| 121 | } else { |
| 122 | ws._authTimer = setTimeout(() => { |
| 123 | if (ws.readyState !== WebSocket.OPEN || ws._authenticated) return; |
| 124 | log(`Auth timeout for ${wsLabel(ws)}`); |
| 125 | ws.close(WS_CLOSE_AUTH_TIMEOUT, WS_CLOSE_REASON_AUTH_TIMEOUT); |
| 126 | }, AUTH_HELLO_TIMEOUT_MS); |
| 127 | } |
| 128 | |
| 129 | ws._resumeHandled = false; |
| 130 | ws._legacyReplayTimer = null; |
| 131 | if (state.AUTH_DISABLED) { |
| 132 | ws._legacyReplayTimer = setTimeout(() => { |
| 133 | if (ws.readyState !== WebSocket.OPEN || ws._resumeHandled) return; |
| 134 | ws._resumeHandled = true; |
| 135 | sendReplay(ws, null); |
| 136 | }, LEGACY_REPLAY_DELAY_MS); |
| 137 | } |
| 138 | |
| 139 | ws.on('message', async (raw) => { |
| 140 | let msg; |
| 141 | try { msg = JSON.parse(raw); } catch { return; } |
| 142 | |
| 143 | // --- Authentication gate --- |
| 144 | if (!ws._authenticated) { |
| 145 | if (msg.type !== 'hello') return; |
| 146 | ws._clientInstanceId = String(msg.clientInstanceId || ws._clientInstanceId || ''); |
| 147 | log(`WS hello from ${wsLabel(ws)} page=${JSON.stringify(msg.page || '')} ua=${JSON.stringify(msg.userAgent || '')}`); |
| 148 | |
| 149 | const clientToken = String(msg.token || ''); |
| 150 | if (!state.AUTH_TOKEN || !clientToken) { |
| 151 | log(`Auth failed for ${wsLabel(ws)}: missing token`); |
| 152 | ws.close(WS_CLOSE_AUTH_FAILED, WS_CLOSE_REASON_AUTH_FAILED); |
| 153 | return; |
| 154 | } |
| 155 | const a = Buffer.from(state.AUTH_TOKEN, 'utf8'); |
| 156 | const b = Buffer.from(clientToken, 'utf8'); |
| 157 | if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { |
| 158 | log(`Auth failed for ${wsLabel(ws)}: invalid token`); |
| 159 | ws.close(WS_CLOSE_AUTH_FAILED, WS_CLOSE_REASON_AUTH_FAILED); |
| 160 | return; |
| 161 | } |
| 162 | ws._authenticated = true; |
| 163 | if (ws._authTimer) { |
no test coverage detected