chore: build
This commit is contained in:
+34
-4
@@ -2,6 +2,7 @@
|
||||
|
||||
const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib')
|
||||
const { isValidClientWindowBits } = require('./util')
|
||||
const { MessageSizeExceededError } = require('../../core/errors')
|
||||
|
||||
const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
|
||||
const kBuffer = Symbol('kBuffer')
|
||||
@@ -13,17 +14,29 @@ class PerMessageDeflate {
|
||||
|
||||
#options = {}
|
||||
|
||||
constructor (extensions) {
|
||||
#maxPayloadSize = 0
|
||||
|
||||
/**
|
||||
* @param {Map<string, string>} extensions
|
||||
*/
|
||||
constructor (extensions, options) {
|
||||
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
|
||||
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
|
||||
|
||||
this.#maxPayloadSize = options.maxPayloadSize
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress a compressed payload.
|
||||
* @param {Buffer} chunk Compressed data
|
||||
* @param {boolean} fin Final fragment flag
|
||||
* @param {Function} callback Callback function
|
||||
*/
|
||||
decompress (chunk, fin, callback) {
|
||||
// An endpoint uses the following algorithm to decompress a message.
|
||||
// 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
|
||||
// payload of the message.
|
||||
// 2. Decompress the resulting data using DEFLATE.
|
||||
|
||||
if (!this.#inflate) {
|
||||
let windowBits = Z_DEFAULT_WINDOWBITS
|
||||
|
||||
@@ -36,13 +49,26 @@ class PerMessageDeflate {
|
||||
windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
|
||||
}
|
||||
|
||||
this.#inflate = createInflateRaw({ windowBits })
|
||||
try {
|
||||
this.#inflate = createInflateRaw({ windowBits })
|
||||
} catch (err) {
|
||||
callback(err)
|
||||
return
|
||||
}
|
||||
this.#inflate[kBuffer] = []
|
||||
this.#inflate[kLength] = 0
|
||||
|
||||
this.#inflate.on('data', (data) => {
|
||||
this.#inflate[kBuffer].push(data)
|
||||
this.#inflate[kLength] += data.length
|
||||
|
||||
if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
|
||||
callback(new MessageSizeExceededError())
|
||||
this.#inflate.removeAllListeners()
|
||||
this.#inflate = null
|
||||
return
|
||||
}
|
||||
|
||||
this.#inflate[kBuffer].push(data)
|
||||
})
|
||||
|
||||
this.#inflate.on('error', (err) => {
|
||||
@@ -57,6 +83,10 @@ class PerMessageDeflate {
|
||||
}
|
||||
|
||||
this.#inflate.flush(() => {
|
||||
if (!this.#inflate) {
|
||||
return
|
||||
}
|
||||
|
||||
const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
|
||||
|
||||
this.#inflate[kBuffer].length = 0
|
||||
|
||||
+93
-27
@@ -18,6 +18,7 @@ const {
|
||||
const { WebsocketFrameSend } = require('./frame')
|
||||
const { closeWebSocketConnection } = require('./connection')
|
||||
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>
|
||||
@@ -26,6 +27,7 @@ const { PerMessageDeflate } = require('./permessage-deflate')
|
||||
|
||||
class ByteParser extends Writable {
|
||||
#buffers = []
|
||||
#fragmentsBytes = 0
|
||||
#byteOffset = 0
|
||||
#loop = false
|
||||
|
||||
@@ -37,14 +39,23 @@ class ByteParser extends Writable {
|
||||
/** @type {Map<string, PerMessageDeflate>} */
|
||||
#extensions
|
||||
|
||||
constructor (ws, extensions) {
|
||||
/** @type {number} */
|
||||
#maxPayloadSize
|
||||
|
||||
/**
|
||||
* @param {import('./websocket').WebSocket} ws
|
||||
* @param {Map<string, string>|null} extensions
|
||||
* @param {{ maxPayloadSize?: number }} [options]
|
||||
*/
|
||||
constructor (ws, extensions, options = {}) {
|
||||
super()
|
||||
|
||||
this.ws = ws
|
||||
this.#extensions = extensions == null ? new Map() : extensions
|
||||
this.#maxPayloadSize = options.maxPayloadSize ?? 0
|
||||
|
||||
if (this.#extensions.has('permessage-deflate')) {
|
||||
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
|
||||
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +71,19 @@ class ByteParser extends Writable {
|
||||
this.run(callback)
|
||||
}
|
||||
|
||||
#validatePayloadLength () {
|
||||
if (
|
||||
this.#maxPayloadSize > 0 &&
|
||||
!isControlFrame(this.#info.opcode) &&
|
||||
this.#info.payloadLength > this.#maxPayloadSize
|
||||
) {
|
||||
failWebsocketConnection(this.ws, '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,
|
||||
@@ -148,6 +172,10 @@ class ByteParser extends Writable {
|
||||
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) {
|
||||
@@ -172,6 +200,10 @@ class ByteParser extends Writable {
|
||||
|
||||
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()
|
||||
@@ -179,6 +211,7 @@ class ByteParser extends Writable {
|
||||
|
||||
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
|
||||
@@ -186,15 +219,17 @@ class ByteParser extends Writable {
|
||||
// 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 > 2 ** 31 - 1) {
|
||||
if (upper !== 0 || lower > 2 ** 31 - 1) {
|
||||
failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.')
|
||||
return
|
||||
}
|
||||
|
||||
const lower = buffer.readUInt32BE(4)
|
||||
|
||||
this.#info.payloadLength = (upper << 8) + lower
|
||||
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()
|
||||
@@ -207,42 +242,53 @@ class ByteParser extends Writable {
|
||||
this.#state = parserStates.INFO
|
||||
} else {
|
||||
if (!this.#info.compressed) {
|
||||
this.#fragments.push(body)
|
||||
this.writeFragments(body)
|
||||
|
||||
if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
|
||||
failWebsocketConnection(this.ws, new MessageSizeExceededError().message)
|
||||
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) {
|
||||
const fullMessage = Buffer.concat(this.#fragments)
|
||||
websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
|
||||
this.#fragments.length = 0
|
||||
websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments())
|
||||
}
|
||||
|
||||
this.#state = parserStates.INFO
|
||||
} else {
|
||||
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
|
||||
if (error) {
|
||||
closeWebSocketConnection(this.ws, 1007, error.message, error.message.length)
|
||||
return
|
||||
}
|
||||
this.#extensions.get('permessage-deflate').decompress(
|
||||
body,
|
||||
this.#info.fin,
|
||||
(error, data) => {
|
||||
if (error) {
|
||||
failWebsocketConnection(this.ws, error.message)
|
||||
return
|
||||
}
|
||||
|
||||
this.#fragments.push(data)
|
||||
this.writeFragments(data)
|
||||
|
||||
if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
|
||||
failWebsocketConnection(this.ws, new MessageSizeExceededError().message)
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.#info.fin) {
|
||||
this.#state = parserStates.INFO
|
||||
this.#loop = true
|
||||
this.run(callback)
|
||||
return
|
||||
}
|
||||
|
||||
websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments())
|
||||
|
||||
if (!this.#info.fin) {
|
||||
this.#state = parserStates.INFO
|
||||
this.#loop = true
|
||||
this.#state = parserStates.INFO
|
||||
this.run(callback)
|
||||
return
|
||||
}
|
||||
|
||||
websocketMessageReceived(this.ws, this.#info.binaryType, Buffer.concat(this.#fragments))
|
||||
|
||||
this.#loop = true
|
||||
this.#state = parserStates.INFO
|
||||
this.#fragments.length = 0
|
||||
this.run(callback)
|
||||
})
|
||||
)
|
||||
|
||||
this.#loop = false
|
||||
break
|
||||
@@ -294,6 +340,26 @@ class ByteParser extends Writable {
|
||||
return buffer
|
||||
}
|
||||
|
||||
writeFragments (fragment) {
|
||||
this.#fragmentsBytes += fragment.length
|
||||
this.#fragments.push(fragment)
|
||||
}
|
||||
|
||||
consumeFragments () {
|
||||
const fragments = this.#fragments
|
||||
|
||||
if (fragments.length === 1) {
|
||||
this.#fragmentsBytes = 0
|
||||
return fragments.shift()
|
||||
}
|
||||
|
||||
const output = Buffer.concat(fragments, this.#fragmentsBytes)
|
||||
this.#fragments = []
|
||||
this.#fragmentsBytes = 0
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
parseCloseBody (data) {
|
||||
assert(data.length !== 1)
|
||||
|
||||
|
||||
+9
-1
@@ -266,6 +266,12 @@ function parseExtensions (extensions) {
|
||||
* @param {string} value
|
||||
*/
|
||||
function isValidClientWindowBits (value) {
|
||||
// Must have at least one character
|
||||
if (value.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check all characters are ASCII digits
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const byte = value.charCodeAt(i)
|
||||
|
||||
@@ -274,7 +280,9 @@ function isValidClientWindowBits (value) {
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
// Check numeric range: zlib requires windowBits in range 8-15
|
||||
const num = Number.parseInt(value, 10)
|
||||
return num >= 8 && num <= 15
|
||||
}
|
||||
|
||||
// https://nodejs.org/api/intl.html#detecting-internationalization-support
|
||||
|
||||
+6
-2
@@ -431,11 +431,15 @@ class WebSocket extends EventTarget {
|
||||
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
|
||||
*/
|
||||
#onConnectionEstablished (response, parsedExtensions) {
|
||||
// processResponse is called when the "response’s header list has been received and initialized."
|
||||
// processResponse is called when the "response's header list has been received and initialized."
|
||||
// once this happens, the connection is open
|
||||
this[kResponse] = response
|
||||
|
||||
const parser = new ByteParser(this, parsedExtensions)
|
||||
const maxPayloadSize = this[kController]?.dispatcher?.webSocketOptions?.maxPayloadSize
|
||||
|
||||
const parser = new ByteParser(this, parsedExtensions, {
|
||||
maxPayloadSize
|
||||
})
|
||||
parser.on('drain', onParserDrain)
|
||||
parser.on('error', onParserError.bind(this))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user