Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 100 additions & 12 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25769,6 +25769,24 @@ class SecureProxyConnectionError extends UndiciError {
[kSecureProxyConnectionError] = true
}

const kMessageSizeExceededError = Symbol.for('undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED')
class MessageSizeExceededError extends UndiciError {
constructor (message) {
super(message)
this.name = 'MessageSizeExceededError'
this.message = message || 'Max decompressed message size exceeded'
this.code = 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'
}

static [Symbol.hasInstance] (instance) {
return instance && instance[kMessageSizeExceededError] === true
}

get [kMessageSizeExceededError] () {
return true
}
}

module.exports = {
AbortError,
HTTPParserError,
Expand All @@ -25792,7 +25810,8 @@ module.exports = {
ResponseExceededMaxSizeError,
RequestRetryError,
ResponseError,
SecureProxyConnectionError
SecureProxyConnectionError,
MessageSizeExceededError
}


Expand Down Expand Up @@ -25869,6 +25888,10 @@ class Request {
throw new InvalidArgumentError('upgrade must be a string')
}

if (upgrade && !isValidHeaderValue(upgrade)) {
throw new InvalidArgumentError('invalid upgrade header')
}

if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
throw new InvalidArgumentError('invalid headersTimeout')
}
Expand Down Expand Up @@ -26163,13 +26186,19 @@ function processHeader (request, key, val) {
val = `${val}`
}

if (request.host === null && headerName === 'host') {
if (headerName === 'host') {
if (request.host !== null) {
throw new InvalidArgumentError('duplicate host header')
}
if (typeof val !== 'string') {
throw new InvalidArgumentError('invalid host header')
}
// Consumed by Client
request.host = val
} else if (request.contentLength === null && headerName === 'content-length') {
} else if (headerName === 'content-length') {
if (request.contentLength !== null) {
throw new InvalidArgumentError('duplicate content-length header')
}
request.contentLength = parseInt(val, 10)
if (!Number.isFinite(request.contentLength)) {
throw new InvalidArgumentError('invalid content-length header')
Expand Down Expand Up @@ -48886,17 +48915,30 @@ module.exports = {

const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = __nccwpck_require__(8522)
const { isValidClientWindowBits } = __nccwpck_require__(8625)
const { MessageSizeExceededError } = __nccwpck_require__(8707)

const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
const kBuffer = Symbol('kBuffer')
const kLength = Symbol('kLength')

// Default maximum decompressed message size: 4 MB
const kDefaultMaxDecompressedSize = 4 * 1024 * 1024

class PerMessageDeflate {
/** @type {import('node:zlib').InflateRaw} */
#inflate

#options = {}

/** @type {boolean} */
#aborted = false

/** @type {Function|null} */
#currentCallback = null

/**
* @param {Map<string, string>} extensions
*/
constructor (extensions) {
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
Expand All @@ -48908,6 +48950,11 @@ class PerMessageDeflate {
// payload of the message.
// 2. Decompress the resulting data using DEFLATE.

if (this.#aborted) {
callback(new MessageSizeExceededError())
return
}

if (!this.#inflate) {
let windowBits = Z_DEFAULT_WINDOWBITS

Expand All @@ -48920,13 +48967,37 @@ 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)
if (this.#aborted) {
return
}

this.#inflate[kLength] += data.length

if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) {
this.#aborted = true
this.#inflate.removeAllListeners()
this.#inflate.destroy()
this.#inflate = null

if (this.#currentCallback) {
const cb = this.#currentCallback
this.#currentCallback = null
cb(new MessageSizeExceededError())
}
return
}

this.#inflate[kBuffer].push(data)
})

this.#inflate.on('error', (err) => {
Expand All @@ -48935,16 +49006,22 @@ class PerMessageDeflate {
})
}

this.#currentCallback = callback
this.#inflate.write(chunk)
if (fin) {
this.#inflate.write(tail)
}

this.#inflate.flush(() => {
if (this.#aborted || !this.#inflate) {
return
}

const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])

this.#inflate[kBuffer].length = 0
this.#inflate[kLength] = 0
this.#currentCallback = null

callback(null, full)
})
Expand Down Expand Up @@ -48998,6 +49075,10 @@ class ByteParser extends Writable {
/** @type {Map<string, PerMessageDeflate>} */
#extensions

/**
* @param {import('./websocket').WebSocket} ws
* @param {Map<string, string>|null} extensions
*/
constructor (ws, extensions) {
super()

Expand Down Expand Up @@ -49140,21 +49221,20 @@ 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
// 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 > 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
} else if (this.#state === parserStates.READ_DATA) {
if (this.#byteOffset < this.#info.payloadLength) {
Expand Down Expand Up @@ -49184,7 +49264,7 @@ class ByteParser extends Writable {
} else {
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
if (error) {
closeWebSocketConnection(this.ws, 1007, error.message, error.message.length)
failWebsocketConnection(this.ws, error.message)
return
}

Expand Down Expand Up @@ -49788,6 +49868,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)

Expand All @@ -49796,7 +49882,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
Expand Down Expand Up @@ -50274,7 +50362,7 @@ class WebSocket extends EventTarget {
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
*/
#onConnectionEstablished (response, parsedExtensions) {
// processResponse is called when the "responses 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

Expand Down
7 changes: 3 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading