import EventEmitter from 'node:events' import crypto from 'node:crypto' import { Buffer } from 'node:buffer' const TLS_MAX_SEND_SIZE = 2 ** 14 const CONTINUE_HEADER_LENGTH = 2 function parseFrameHeader(buffer) { let startIndex = 2 const opcode = buffer[0] & 0b00001111 const fin = (buffer[0] & 0b10000000) === 0b10000000 const isMasked = (buffer[1] & 0x80) === 0x80 let payloadLength = buffer[1] & 0b01111111 if (payloadLength === 126) { startIndex += 2 payloadLength = buffer.readUInt16BE(2) } else if (payloadLength === 127) { const buf = buffer.subarray(startIndex, startIndex + 8) payloadLength = buf.readUInt32BE(0) * Math.pow(2, 32) + buf.readUInt32BE(4) startIndex += 8 } let mask = null if (isMasked) { mask = buffer.subarray(startIndex, startIndex + 4) startIndex += 4 buffer = buffer.subarray(startIndex, startIndex + payloadLength) for (let i = 0; i < buffer.length; i++) { buffer[i] ^= mask[i & 3] } } else { buffer = buffer.subarray(startIndex, startIndex + payloadLength) } return { opcode, fin, buffer, payloadLength } } class WebsocketConnection extends EventEmitter { constructor(req, socket, head, addHeaders) { super() this.req = req this.socket = socket socket.setNoDelay() socket.setKeepAlive(true) if (head.length !== 0) socket.unshift(head) const headers = [ 'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', 'Sec-WebSocket-Accept: ' + crypto.createHash('sha1').update(req.headers['sec-websocket-key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64'), 'Sec-WebSocket-Version: 13', ] if (addHeaders) { for (const [key, value] of Object.entries(addHeaders)) { headers.push(`${key}: ${value}`) } } socket.write(headers.join('\r\n') + '\r\n\r\n') socket.on('data', (data) => { const headers = parseFrameHeader(data) switch (headers.opcode) { case 0x0: { this.cachedData.push(headers.buffer) if (headers.fin) { this.emit('message', Buffer.concat(this.cachedData).toString()) this.cachedData = [] } break } case 0x1: { this.emit('message', headers.buffer.toString()) break } case 0x2: { throw new Error('Binary data is not supported.') break } case 0x8: { if (headers.buffer.length === 0) { this.emit('close', 1006, '') } else { const code = headers.buffer.readUInt16BE(0) const reason = headers.buffer.subarray(2).toString('utf-8') this.emit('close', code, reason) } socket.end() socket.removeAllListeners() break } case 0x9: { const pong = Buffer.allocUnsafe(2) pong[0] = 0x8a pong[1] = 0x00 this.socket.write(pong) break } case 0x10: { this.emit('pong') } } if (headers.buffer.length > headers.payloadLength) this.socket.unshift(headers.buffer) }) req.on('error', (err) => { socket.destroy() this.emit('close', 1006, `Error: ${err.message}`) socket.removeAllListeners() }) socket.on('error', (err) => { socket.destroy() this.emit('close', 1006, `Error: ${err.message}`) socket.removeAllListeners() }) socket.on('end', () => { socket.end() this.emit('close', 1006, '') socket.removeAllListeners() }) } send(data) { const payload = Buffer.from(data, 'utf-8') if (payload.length + CONTINUE_HEADER_LENGTH > TLS_MAX_SEND_SIZE) { for (let i = 0; i < payload.length; i += TLS_MAX_SEND_SIZE) { const buffer = payload.subarray(i, i + TLS_MAX_SEND_SIZE) const length = Buffer.byteLength(buffer) let header = null if (i === 0) { header = this.makeFHeader({ len: length, fin: false, opcode: 0x1 }) } else if (i + TLS_MAX_SEND_SIZE >= payload.length) { header = this.makeFHeader({ len: length, fin: true, opcode: 0x0 }) } else { header = this.makeFHeader({ len: length, fin: false, opcode: 0x0 }) } this.socket.write(Buffer.concat([ header, buffer ])) } return true } else { return this.sendFrame(payload, { len: payload.length, fin: true, opcode: 0x01 }) } } destroy() { this.socket.destroy() this.socket = null this.req = null } makeFHeader(options) { let payloadStartIndex = 2 let payloadLength = options.len if (options.len >= 65536) { payloadStartIndex += 8 payloadLength = 127 } else if (options.len > 125) { payloadStartIndex += 2 payloadLength = 126 } const header = Buffer.allocUnsafe(payloadStartIndex) header[0] = options.fin ? options.opcode | 0x80 : options.opcode header[1] = payloadLength if (payloadLength === 126) { header.writeUInt16BE(options.len, 2) } else if (payloadLength === 127) { header[2] = header[3] = 0 header.writeUIntBE(options.len, 4, 6) } return header } sendFrame(data, options) { const header = this.makeFHeader(options) if (this.socket) this.socket.write(Buffer.concat([ header, data ])) return true } close(code, reason) { const data = Buffer.allocUnsafe(2 + Buffer.byteLength(reason || 'normal close')) data.writeUInt16BE(code || 1000) data.write(reason || 'normal close', 2) this.sendFrame(data, { len: data.length, fin: true, opcode: 0x08 }) return true } } class WebSocketServer extends EventEmitter { constructor() { super() } handleUpgrade(req, socket, head, headers, callback) { const connection = new WebsocketConnection(req, socket, head, headers) if (!socket.readable || !socket.writable) return socket.destroy() callback(connection) } } export { WebSocketServer }