diff --git a/lib/websocket.js b/lib/websocket.js index 6e5c2e9a..4b5168a6 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -17,29 +17,109 @@ const OPCODE_SHORT = 0x81; const LEN_16_BIT = 126; const MAX_16_BIT = 65536; const LEN_64_BIT = 127; +const MAX_64_BIT_PAYLOAD = Number.MAX_SAFE_INTEGER; +const HEAD_LENGTH = 2; +const EXTENDED_PAYLOAD_16_LENGTH = 2; +const EXTENDED_PAYLOAD_64_LENGTH = 8; -const calcOffset = (frame, length) => { - if (length < LEN_16_BIT) return [2, 6]; - if (length === LEN_16_BIT) return [4, 8]; - return [10, 14]; -}; +class Frame { + #frame = null; + #masked = null; + #dataOffset = null; + #mask = null; + #length = null; -const parseFrame = (frame) => { - const length = frame[1] ^ 0x80; - const [maskOffset, dataOffset] = calcOffset(frame, length); - const mask = frame.subarray(maskOffset, maskOffset + MASK_LENGTH); - const data = frame.subarray(dataOffset); - return { mask, data }; -}; + constructor(frame) { + this.#frame = frame; + const length = this.#frame[1] & 0x7f; + this.#masked = (frame[1] & 0x80) === 0x80; + if (length < 126) { + this.#dataOffset = HEAD_LENGTH + MASK_LENGTH; + this.#mask = frame.subarray(HEAD_LENGTH, HEAD_LENGTH + MASK_LENGTH); + this.#length = length; + } else if (length === 126) { + this.#dataOffset = HEAD_LENGTH + EXTENDED_PAYLOAD_16_LENGTH + MASK_LENGTH; + this.#mask = frame.subarray( + HEAD_LENGTH + EXTENDED_PAYLOAD_16_LENGTH, + HEAD_LENGTH + EXTENDED_PAYLOAD_16_LENGTH + MASK_LENGTH, + ); + this.#length = this.#frame.readUInt16BE(2); + } else { + this.#dataOffset = HEAD_LENGTH + EXTENDED_PAYLOAD_64_LENGTH + MASK_LENGTH; + this.#mask = frame.subarray( + HEAD_LENGTH + EXTENDED_PAYLOAD_64_LENGTH, + HEAD_LENGTH + EXTENDED_PAYLOAD_64_LENGTH + MASK_LENGTH, + ); + this.#length = (frame.readUInt32BE(2) << 32) + frame.readUInt32BE(4); + } + } -const unmask = (buffer, mask) => { - const data = Buffer.allocUnsafe(buffer.length); - buffer.copy(data); - for (let i = 0; i < data.length; i++) { - data[i] ^= mask[i & 3]; + unmask() { + if (!this.#masked) return; + for (let i = 0; i < this.#length; ++i) { + this.#frame[this.#dataOffset + i] ^= this.#mask[i & 3]; + } + this.#masked = false; + } + + toString() { + return this.#frame.toString( + 'utf8', + this.#dataOffset, + this.#dataOffset + this.#length, + ); + } + + get frame() { + return this.#frame; } - return data; -}; + + static from(data) { + if (Buffer.isBuffer(data)) { + if (data.length === 0) throw new Error('Empty frame!'); + if ((data[1] & 0x80) !== 0x80) throw new Error('1002: protocol error'); + // + // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned + // if payload length is greater than this number. + // + if ((data[2] & 0x7f) === 127) { + const upperInt = data.readUInt32BE(2); + if (upperInt > MAX_64_BIT_PAYLOAD >> 32) + throw new Error( + '1009: Unsupported WebSocket frame: payload length > 2^53 - 1', + ); + } + return new Frame(data); + } + + if (typeof data === 'string') { + if (data.length === 0) throw new Error('Empty string!'); + const payload = Buffer.from(data); + const length = payload.length; + let meta = Buffer.alloc(2); + meta[0] = 0x81; // FIN = 1, RSV = 0b000, opcode = 0b0001 (text frame) + if (length < LEN_16_BIT) { + meta[1] = length; + } else if (length < MAX_16_BIT) { + const len = Buffer.alloc(2); + len.writeUint16BE(length, 0); + meta[1] = LEN_16_BIT; + meta = Buffer.concat([meta, len]); + } else if (length < MAX_64_BIT_PAYLOAD) { + const len = Buffer.alloc(8); + len.writeBigUInt64BE(BigInt(length), 0); + meta[1] = LEN_64_BIT; + this.meta = Buffer.concat([meta, len]); + } else { + throw new Error('string is too long to encode in one frame!'); + } + const frame = Buffer.concat([meta, payload]); + return new Frame(Buffer.from(frame)); + } + + throw new Error('Unsupported'); + } +} class Connection { constructor(socket) { @@ -56,34 +136,18 @@ class Connection { } send(text) { - const data = Buffer.from(text); - let meta = Buffer.alloc(2); - const length = data.length; - meta[0] = OPCODE_SHORT; - if (length < LEN_16_BIT) { - meta[1] = length; - } else if (length < MAX_16_BIT) { - const len = Buffer.from([(length & 0xff00) >> 8, length & 0x00ff]); - meta = Buffer.concat([meta, len]); - meta[1] = LEN_16_BIT; - } else { - const len = Buffer.alloc(8); - len.writeBigInt64BE(BigInt(length), 0); - meta = Buffer.concat([meta, len]); - meta[1] = LEN_64_BIT; - } - const frame = Buffer.concat([meta, data]); - this.socket.write(frame); + const frame = Frame.from(text); + this.socket.write(frame.frame); } receive(data) { console.log('data: ', data[0], data.length); if (data[0] !== OPCODE_SHORT) return; - const frame = parseFrame(data); - const msg = unmask(frame.data, frame.mask); - const text = msg.toString(); - this.send(`Echo "${text}"`); + const frame = Frame.from(data); + frame.unmask(); + const text = frame.toString(); console.log('Message:', text); + this.send(`Echo "${text}"`); } accept(key) {