Spaces:
Runtime error
Runtime error
| 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 } |