Spaces:
Paused
Paused
| const { Writable } = require('node:stream') | |
| const assert = require('node:assert') | |
| const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants') | |
| const { | |
| isValidStatusCode, | |
| isValidOpcode, | |
| websocketMessageReceived, | |
| utf8Decode, | |
| isControlFrame, | |
| isTextBinaryFrame, | |
| isContinuationFrame | |
| } = require('./util') | |
| const { failWebsocketConnection } = require('./connection') | |
| const { WebsocketFrameSend } = require('./frame') | |
| const { PerMessageDeflate } = require('./permessage-deflate') | |
| const { MessageSizeExceededError } = require('../../core/errors') | |
| // This code was influenced by ws released under the MIT license. | |
| // Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com> | |
| // Copyright (c) 2013 Arnout Kazemier and contributors | |
| // Copyright (c) 2016 Luigi Pinca and contributors | |
| class ByteParser extends Writable { | |
| #buffers = [] | |
| #fragmentsBytes = 0 | |
| #byteOffset = 0 | |
| #loop = false | |
| #state = parserStates.INFO | |
| #info = {} | |
| #fragments = [] | |
| /** @type {Map<string, PerMessageDeflate>} */ | |
| #extensions | |
| /** @type {import('./websocket').Handler} */ | |
| #handler | |
| /** @type {number} */ | |
| #maxFragments | |
| /** @type {number} */ | |
| #maxPayloadSize | |
| /** | |
| * @param {import('./websocket').Handler} handler | |
| * @param {Map<string, string>|null} extensions | |
| * @param {{ maxFragments?: number, maxPayloadSize?: number }} [options] | |
| */ | |
| constructor (handler, extensions, options = {}) { | |
| super() | |
| this.#handler = handler | |
| this.#extensions = extensions == null ? new Map() : extensions | |
| this.#maxFragments = options.maxFragments ?? 0 | |
| this.#maxPayloadSize = options.maxPayloadSize ?? 0 | |
| if (this.#extensions.has('permessage-deflate')) { | |
| this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options)) | |
| } | |
| } | |
| /** | |
| * @param {Buffer} chunk | |
| * @param {() => void} callback | |
| */ | |
| _write (chunk, _, callback) { | |
| this.#buffers.push(chunk) | |
| this.#byteOffset += chunk.length | |
| this.#loop = true | |
| this.run(callback) | |
| } | |
| #validatePayloadLength () { | |
| if ( | |
| this.#maxPayloadSize > 0 && | |
| !isControlFrame(this.#info.opcode) && | |
| this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize | |
| ) { | |
| failWebsocketConnection(this.#handler, 1009, 'Payload size exceeds maximum allowed size') | |
| return false | |
| } | |
| return true | |
| } | |
| /** | |
| * Runs whenever a new chunk is received. | |
| * Callback is called whenever there are no more chunks buffering, | |
| * or not enough bytes are buffered to parse. | |
| */ | |
| run (callback) { | |
| while (this.#loop) { | |
| if (this.#state === parserStates.INFO) { | |
| // If there aren't enough bytes to parse the payload length, etc. | |
| if (this.#byteOffset < 2) { | |
| return callback() | |
| } | |
| const buffer = this.consume(2) | |
| const fin = (buffer[0] & 0x80) !== 0 | |
| const opcode = buffer[0] & 0x0F | |
| const masked = (buffer[1] & 0x80) === 0x80 | |
| const fragmented = !fin && opcode !== opcodes.CONTINUATION | |
| const payloadLength = buffer[1] & 0x7F | |
| const rsv1 = buffer[0] & 0x40 | |
| const rsv2 = buffer[0] & 0x20 | |
| const rsv3 = buffer[0] & 0x10 | |
| if (!isValidOpcode(opcode)) { | |
| failWebsocketConnection(this.#handler, 1002, 'Invalid opcode received') | |
| return callback() | |
| } | |
| if (masked) { | |
| failWebsocketConnection(this.#handler, 1002, 'Frame cannot be masked') | |
| return callback() | |
| } | |
| // MUST be 0 unless an extension is negotiated that defines meanings | |
| // for non-zero values. If a nonzero value is received and none of | |
| // the negotiated extensions defines the meaning of such a nonzero | |
| // value, the receiving endpoint MUST _Fail the WebSocket | |
| // Connection_. | |
| // This document allocates the RSV1 bit of the WebSocket header for | |
| // PMCEs and calls the bit the "Per-Message Compressed" bit. On a | |
| // WebSocket connection where a PMCE is in use, this bit indicates | |
| // whether a message is compressed or not. | |
| if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) { | |
| failWebsocketConnection(this.#handler, 1002, 'Expected RSV1 to be clear.') | |
| return | |
| } | |
| if (rsv2 !== 0 || rsv3 !== 0) { | |
| failWebsocketConnection(this.#handler, 1002, 'RSV1, RSV2, RSV3 must be clear') | |
| return | |
| } | |
| if (fragmented && !isTextBinaryFrame(opcode)) { | |
| // Only text and binary frames can be fragmented | |
| failWebsocketConnection(this.#handler, 1002, 'Invalid frame type was fragmented.') | |
| return | |
| } | |
| // If we are already parsing a text/binary frame and do not receive either | |
| // a continuation frame or close frame, fail the connection. | |
| if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) { | |
| failWebsocketConnection(this.#handler, 1002, 'Expected continuation frame') | |
| return | |
| } | |
| if (this.#info.fragmented && fragmented) { | |
| // A fragmented frame can't be fragmented itself | |
| failWebsocketConnection(this.#handler, 1002, 'Fragmented frame exceeded 125 bytes.') | |
| return | |
| } | |
| // "All control frames MUST have a payload length of 125 bytes or less | |
| // and MUST NOT be fragmented." | |
| if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) { | |
| failWebsocketConnection(this.#handler, 1002, 'Control frame either too large or fragmented') | |
| return | |
| } | |
| if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) { | |
| failWebsocketConnection(this.#handler, 1002, 'Unexpected continuation frame') | |
| return | |
| } | |
| if (payloadLength <= 125) { | |
| this.#info.payloadLength = payloadLength | |
| this.#state = parserStates.READ_DATA | |
| if (!this.#validatePayloadLength()) { | |
| return | |
| } | |
| } else if (payloadLength === 126) { | |
| this.#state = parserStates.PAYLOADLENGTH_16 | |
| } else if (payloadLength === 127) { | |
| this.#state = parserStates.PAYLOADLENGTH_64 | |
| } | |
| if (isTextBinaryFrame(opcode)) { | |
| this.#info.binaryType = opcode | |
| this.#info.compressed = rsv1 !== 0 | |
| } | |
| this.#info.opcode = opcode | |
| this.#info.masked = masked | |
| this.#info.fin = fin | |
| this.#info.fragmented = fragmented | |
| } else if (this.#state === parserStates.PAYLOADLENGTH_16) { | |
| if (this.#byteOffset < 2) { | |
| return callback() | |
| } | |
| const buffer = this.consume(2) | |
| this.#info.payloadLength = buffer.readUInt16BE(0) | |
| this.#state = parserStates.READ_DATA | |
| if (!this.#validatePayloadLength()) { | |
| return | |
| } | |
| } else if (this.#state === parserStates.PAYLOADLENGTH_64) { | |
| if (this.#byteOffset < 8) { | |
| return callback() | |
| } | |
| const buffer = this.consume(8) | |
| const upper = buffer.readUInt32BE(0) | |
| const lower = buffer.readUInt32BE(4) | |
| // 2^31 is the maximum bytes an arraybuffer can contain | |
| // on 32-bit systems. Although, on 64-bit systems, this is | |
| // 2^53-1 bytes. | |
| // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length | |
| // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275 | |
| // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e | |
| if (upper !== 0 || lower > 2 ** 31 - 1) { | |
| failWebsocketConnection(this.#handler, 1009, 'Received payload length > 2^31 bytes.') | |
| return | |
| } | |
| this.#info.payloadLength = lower | |
| this.#state = parserStates.READ_DATA | |
| if (!this.#validatePayloadLength()) { | |
| return | |
| } | |
| } else if (this.#state === parserStates.READ_DATA) { | |
| if (this.#byteOffset < this.#info.payloadLength) { | |
| return callback() | |
| } | |
| const body = this.consume(this.#info.payloadLength) | |
| if (isControlFrame(this.#info.opcode)) { | |
| this.#loop = this.parseControlFrame(body) | |
| this.#state = parserStates.INFO | |
| } else { | |
| if (!this.#info.compressed) { | |
| if (!this.writeFragments(body)) { | |
| return | |
| } | |
| // If the frame is not fragmented, a message has been received. | |
| // If the frame is fragmented, it will terminate with a fin bit set | |
| // and an opcode of 0 (continuation), therefore we handle that when | |
| // parsing continuation frames, not here. | |
| if (!this.#info.fragmented && this.#info.fin) { | |
| websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments()) | |
| } | |
| this.#state = parserStates.INFO | |
| } else { | |
| this.#extensions.get('permessage-deflate').decompress( | |
| body, | |
| this.#info.fin, | |
| (error, data) => { | |
| if (error) { | |
| const code = error instanceof MessageSizeExceededError ? 1009 : 1007 | |
| failWebsocketConnection(this.#handler, code, error.message) | |
| return | |
| } | |
| if (!this.writeFragments(data)) { | |
| return | |
| } | |
| // Check cumulative fragment size | |
| if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { | |
| failWebsocketConnection(this.#handler, 1009, new MessageSizeExceededError().message) | |
| return | |
| } | |
| if (!this.#info.fin) { | |
| this.#state = parserStates.INFO | |
| this.#loop = true | |
| this.run(callback) | |
| return | |
| } | |
| websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments()) | |
| this.#loop = true | |
| this.#state = parserStates.INFO | |
| this.run(callback) | |
| }, | |
| this.#fragmentsBytes | |
| ) | |
| this.#loop = false | |
| break | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Take n bytes from the buffered Buffers | |
| * @param {number} n | |
| * @returns {Buffer} | |
| */ | |
| consume (n) { | |
| if (n > this.#byteOffset) { | |
| throw new Error('Called consume() before buffers satiated.') | |
| } else if (n === 0) { | |
| return emptyBuffer | |
| } | |
| this.#byteOffset -= n | |
| const first = this.#buffers[0] | |
| if (first.length > n) { | |
| // replace with remaining buffer | |
| this.#buffers[0] = first.subarray(n, first.length) | |
| return first.subarray(0, n) | |
| } else if (first.length === n) { | |
| // prefect match | |
| return this.#buffers.shift() | |
| } else { | |
| let offset = 0 | |
| // If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero. | |
| const buffer = Buffer.allocUnsafeSlow(n) | |
| while (offset !== n) { | |
| const next = this.#buffers[0] | |
| const length = next.length | |
| if (length + offset === n) { | |
| buffer.set(this.#buffers.shift(), offset) | |
| break | |
| } else if (length + offset > n) { | |
| buffer.set(next.subarray(0, n - offset), offset) | |
| this.#buffers[0] = next.subarray(n - offset) | |
| break | |
| } else { | |
| buffer.set(this.#buffers.shift(), offset) | |
| offset += length | |
| } | |
| } | |
| return buffer | |
| } | |
| } | |
| writeFragments (fragment) { | |
| if ( | |
| this.#maxFragments > 0 && | |
| this.#fragments.length === this.#maxFragments | |
| ) { | |
| failWebsocketConnection(this.#handler, 1008, 'Too many message fragments') | |
| return false | |
| } | |
| this.#fragmentsBytes += fragment.length | |
| this.#fragments.push(fragment) | |
| return true | |
| } | |
| consumeFragments () { | |
| const fragments = this.#fragments | |
| if (fragments.length === 1) { | |
| // single fragment | |
| this.#fragmentsBytes = 0 | |
| return fragments.shift() | |
| } | |
| let offset = 0 | |
| // If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero. | |
| const output = Buffer.allocUnsafeSlow(this.#fragmentsBytes) | |
| for (let i = 0; i < fragments.length; ++i) { | |
| const buffer = fragments[i] | |
| output.set(buffer, offset) | |
| offset += buffer.length | |
| } | |
| this.#fragments = [] | |
| this.#fragmentsBytes = 0 | |
| return output | |
| } | |
| parseCloseBody (data) { | |
| assert(data.length !== 1) | |
| // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 | |
| /** @type {number|undefined} */ | |
| let code | |
| if (data.length >= 2) { | |
| // _The WebSocket Connection Close Code_ is | |
| // defined as the status code (Section 7.4) contained in the first Close | |
| // control frame received by the application | |
| code = data.readUInt16BE(0) | |
| } | |
| if (code !== undefined && !isValidStatusCode(code)) { | |
| return { code: 1002, reason: 'Invalid status code', error: true } | |
| } | |
| // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6 | |
| /** @type {Buffer} */ | |
| let reason = data.subarray(2) | |
| // Remove BOM | |
| if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) { | |
| reason = reason.subarray(3) | |
| } | |
| try { | |
| reason = utf8Decode(reason) | |
| } catch { | |
| return { code: 1007, reason: 'Invalid UTF-8', error: true } | |
| } | |
| return { code, reason, error: false } | |
| } | |
| /** | |
| * Parses control frames. | |
| * @param {Buffer} body | |
| */ | |
| parseControlFrame (body) { | |
| const { opcode, payloadLength } = this.#info | |
| if (opcode === opcodes.CLOSE) { | |
| if (payloadLength === 1) { | |
| failWebsocketConnection(this.#handler, 1002, 'Received close frame with a 1-byte body.') | |
| return false | |
| } | |
| this.#info.closeInfo = this.parseCloseBody(body) | |
| if (this.#info.closeInfo.error) { | |
| const { code, reason } = this.#info.closeInfo | |
| failWebsocketConnection(this.#handler, code, reason) | |
| return false | |
| } | |
| // Upon receiving such a frame, the other peer sends a | |
| // Close frame in response, if it hasn't already sent one. | |
| if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) { | |
| // If an endpoint receives a Close frame and did not previously send a | |
| // Close frame, the endpoint MUST send a Close frame in response. (When | |
| // sending a Close frame in response, the endpoint typically echos the | |
| // status code it received.) | |
| let body = emptyBuffer | |
| if (this.#info.closeInfo.code) { | |
| body = Buffer.allocUnsafe(2) | |
| body.writeUInt16BE(this.#info.closeInfo.code, 0) | |
| } | |
| const closeFrame = new WebsocketFrameSend(body) | |
| this.#handler.socket.write(closeFrame.createFrame(opcodes.CLOSE)) | |
| this.#handler.closeState.add(sentCloseFrameState.SENT) | |
| } | |
| // Upon either sending or receiving a Close control frame, it is said | |
| // that _The WebSocket Closing Handshake is Started_ and that the | |
| // WebSocket connection is in the CLOSING state. | |
| this.#handler.readyState = states.CLOSING | |
| this.#handler.closeState.add(sentCloseFrameState.RECEIVED) | |
| return false | |
| } else if (opcode === opcodes.PING) { | |
| // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in | |
| // response, unless it already received a Close frame. | |
| // A Pong frame sent in response to a Ping frame must have identical | |
| // "Application data" | |
| if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) { | |
| const frame = new WebsocketFrameSend(body) | |
| this.#handler.socket.write(frame.createFrame(opcodes.PONG)) | |
| this.#handler.onPing(body) | |
| } | |
| } else if (opcode === opcodes.PONG) { | |
| // A Pong frame MAY be sent unsolicited. This serves as a | |
| // unidirectional heartbeat. A response to an unsolicited Pong frame is | |
| // not expected. | |
| this.#handler.onPong(body) | |
| } | |
| return true | |
| } | |
| get closingInfo () { | |
| return this.#info.closeInfo | |
| } | |
| } | |
| module.exports = { | |
| ByteParser | |
| } | |