// --------------------------------------------------------------- // Minimal RFC6455 WebSocket server — zero dependencies // --------------------------------------------------------------- 'use strict'; const crypto = require('crypto'); const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; function acceptKey(key) { return crypto.createHash('sha1').update(key + GUID).digest('base64'); } class WSConn { constructor(socket) { this.socket = socket; this.alive = true; this.fragOp = 0; this.fragBufs = []; this.onmessage = null; this.onclose = null; socket.setNoDelay(true); socket.on('data', (buf) => this._onData(buf)); const end = () => this._closed(); socket.on('close', end); socket.on('error', end); } get writable() { return this.socket && this.socket.writable && this.alive; } _closed() { if (!this.alive) return; this.alive = false; if (this.onclose) this.onclose(); try { this.socket.destroy(); } catch (_) {} } _onData(buf) { let off = 0; try { while (off + 2 <= buf.length) { const b0 = buf[off]; const b1 = buf[off + 1]; const fin = (b0 & 0x80) !== 0; const op = b0 & 0x0f; const masked = (b1 & 0x80) !== 0; let len = b1 & 0x7f; off += 2; if (len === 126) { if (off + 2 > buf.length) break; len = buf.readUInt16BE(off); off += 2; } else if (len === 127) { if (off + 8 > buf.length) break; const big = buf.readBigUInt64BE(off); off += 8; len = Number(big); } if (len > 10 * 1024 * 1024) { this.close(); return; } // sanity cap let maskKey = null; if (masked) { if (off + 4 > buf.length) break; maskKey = buf.subarray(off, off + 4); off += 4; } if (off + len > buf.length) break; // wait for more data (rare; messages are small) let payload = buf.subarray(off, off + len); off += len; if (maskKey) { payload = Buffer.from(payload); // copy so we can unmask for (let i = 0; i < payload.length; i++) payload[i] ^= maskKey[i & 3]; } switch (op) { case 0x0: // continuation this.fragBufs.push(payload); if (fin) { const full = Buffer.concat(this.fragBufs); this.fragBufs = []; this._emit(this.fragOp, full); } break; case 0x1: case 0x2: // text / binary if (fin) this._emit(op, payload); else { this.fragOp = op; this.fragBufs = [payload]; } break; case 0x8: this.close(); return; // close case 0x9: this._sendFrame(0xA, payload); break; // ping -> pong case 0xA: break; // pong default: break; } } } catch (_) { this.close(); } } _emit(op, payload) { if (op === 0x1) { const str = payload.toString('utf8'); if (this.onmessage) this.onmessage(str); } } _sendFrame(op, payload) { if (!this.writable) return; const len = payload.length; let header; if (len < 126) { header = Buffer.from([0x80 | op, len]); } else if (len < 65536) { header = Buffer.alloc(4); header[0] = 0x80 | op; header[1] = 126; header.writeUInt16BE(len, 2); } else { header = Buffer.alloc(10); header[0] = 0x80 | op; header[1] = 127; header.writeBigUInt64BE(BigInt(len), 2); } try { this.socket.write(Buffer.concat([header, payload])); } catch (_) {} } send(str) { this._sendFrame(0x1, Buffer.from(str, 'utf8')); } ping() { this._sendFrame(0x9, Buffer.alloc(0)); } close() { if (this.writable) { try { this._sendFrame(0x8, Buffer.alloc(0)); } catch (_) {} } this._closed(); } } function attach(server, path, onConn) { server.on('upgrade', (req, socket) => { try { const url = req.url.split('?')[0]; if (url !== path) { socket.destroy(); return; } const key = req.headers['sec-websocket-key']; if (!key) { socket.destroy(); return; } const headers = [ 'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${acceptKey(key)}`, '\r\n', ].join('\r\n'); socket.write(headers); onConn(new WSConn(socket)); } catch (_) { try { socket.destroy(); } catch (_) {} } }); } module.exports = { attach, WSConn };