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 } |