From 784ba9ccef3ceca28d08001cf9227e8f537d0c67 Mon Sep 17 00:00:00 2001 From: aduh95 <14309773+aduh95@users.noreply.github.com> Date: Mon, 4 May 2026 07:48:55 +0000 Subject: [PATCH] deps: update undici to 6.25.0 --- deps/undici/src/.gitignore | 6 + deps/undici/src/docs/docs/api/Client.md | 2 + deps/undici/src/lib/dispatcher/agent.js | 3 +- deps/undici/src/lib/dispatcher/client.js | 5 +- .../src/lib/dispatcher/dispatcher-base.js | 10 +- deps/undici/src/lib/dispatcher/pool-base.js | 4 +- deps/undici/src/lib/dispatcher/pool.js | 4 +- deps/undici/src/lib/llhttp/wasm_build_env.txt | 32 ++-- .../lib/web/websocket/permessage-deflate.js | 44 ++--- deps/undici/src/lib/web/websocket/receiver.js | 111 ++++++++++--- .../undici/src/lib/web/websocket/websocket.js | 6 +- deps/undici/src/package.json | 2 +- deps/undici/src/scripts/release.js | 2 + deps/undici/src/types/client.d.ts | 11 ++ deps/undici/undici.js | 151 ++++++++++++------ src/undici_version.h | 2 +- 16 files changed, 260 insertions(+), 135 deletions(-) diff --git a/deps/undici/src/.gitignore b/deps/undici/src/.gitignore index 7cba7df889f509..733f347f5373dd 100644 --- a/deps/undici/src/.gitignore +++ b/deps/undici/src/.gitignore @@ -87,3 +87,9 @@ undici-fetch.js # File generated by /test/request-timeout.js test/request-timeout.10mb.bin + +# Local agent configuration +CLAUDE.md +AGENTS.md +.pi/ +.claude/ diff --git a/deps/undici/src/docs/docs/api/Client.md b/deps/undici/src/docs/docs/api/Client.md index 03342f59959db0..fdee5ea702671b 100644 --- a/deps/undici/src/docs/docs/api/Client.md +++ b/deps/undici/src/docs/docs/api/Client.md @@ -26,6 +26,8 @@ Returns: `Client` * **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds. * **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB. * **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable. +* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options. + * **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit. * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections. * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`. * **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. diff --git a/deps/undici/src/lib/dispatcher/agent.js b/deps/undici/src/lib/dispatcher/agent.js index 98f1486cac096f..db2f817d0fe978 100644 --- a/deps/undici/src/lib/dispatcher/agent.js +++ b/deps/undici/src/lib/dispatcher/agent.js @@ -24,7 +24,6 @@ function defaultFactory (origin, opts) { class Agent extends DispatcherBase { constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { - super() if (typeof factory !== 'function') { throw new InvalidArgumentError('factory must be a function.') @@ -38,6 +37,8 @@ class Agent extends DispatcherBase { throw new InvalidArgumentError('maxRedirections must be a positive number') } + super(options) + if (connect && typeof connect !== 'function') { connect = { ...connect } } diff --git a/deps/undici/src/lib/dispatcher/client.js b/deps/undici/src/lib/dispatcher/client.js index 3dc356618ba99a..18472fffd773fc 100644 --- a/deps/undici/src/lib/dispatcher/client.js +++ b/deps/undici/src/lib/dispatcher/client.js @@ -106,9 +106,10 @@ class Client extends DispatcherBase { autoSelectFamilyAttemptTimeout, // h2 maxConcurrentStreams, - allowH2 + allowH2, + webSocket } = {}) { - super() + super({ webSocket }) if (keepAlive !== undefined) { throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') diff --git a/deps/undici/src/lib/dispatcher/dispatcher-base.js b/deps/undici/src/lib/dispatcher/dispatcher-base.js index bd860acdcf45f5..c999b2c2fb6740 100644 --- a/deps/undici/src/lib/dispatcher/dispatcher-base.js +++ b/deps/undici/src/lib/dispatcher/dispatcher-base.js @@ -11,15 +11,23 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch, kInterceptors } = requ const kOnDestroyed = Symbol('onDestroyed') const kOnClosed = Symbol('onClosed') const kInterceptedDispatch = Symbol('Intercepted Dispatch') +const kWebSocketOptions = Symbol('webSocketOptions') class DispatcherBase extends Dispatcher { - constructor () { + constructor (opts) { super() this[kDestroyed] = false this[kOnDestroyed] = null this[kClosed] = false this[kOnClosed] = [] + this[kWebSocketOptions] = opts?.webSocket ?? {} + } + + get webSocketOptions () { + return { + maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 + } } get destroyed () { diff --git a/deps/undici/src/lib/dispatcher/pool-base.js b/deps/undici/src/lib/dispatcher/pool-base.js index d0ba2c3c53a0b0..6f9ec5e120b728 100644 --- a/deps/undici/src/lib/dispatcher/pool-base.js +++ b/deps/undici/src/lib/dispatcher/pool-base.js @@ -19,8 +19,8 @@ const kRemoveClient = Symbol('remove client') const kStats = Symbol('stats') class PoolBase extends DispatcherBase { - constructor () { - super() + constructor (opts) { + super(opts) this[kQueue] = new FixedQueue() this[kClients] = [] diff --git a/deps/undici/src/lib/dispatcher/pool.js b/deps/undici/src/lib/dispatcher/pool.js index 0b8a2da6da4966..9eaf3fd03a983e 100644 --- a/deps/undici/src/lib/dispatcher/pool.js +++ b/deps/undici/src/lib/dispatcher/pool.js @@ -37,8 +37,6 @@ class Pool extends PoolBase { allowH2, ...options } = {}) { - super() - if (connections != null && (!Number.isFinite(connections) || connections < 0)) { throw new InvalidArgumentError('invalid connections') } @@ -63,6 +61,8 @@ class Pool extends PoolBase { }) } + super(options) + this[kInterceptors] = options.interceptors?.Pool && Array.isArray(options.interceptors.Pool) ? options.interceptors.Pool : [] diff --git a/deps/undici/src/lib/llhttp/wasm_build_env.txt b/deps/undici/src/lib/llhttp/wasm_build_env.txt index 7ccb566421391c..234fbb3743fd1a 100644 --- a/deps/undici/src/lib/llhttp/wasm_build_env.txt +++ b/deps/undici/src/lib/llhttp/wasm_build_env.txt @@ -1,17 +1,17 @@ - -> undici@6.24.1 prebuild:wasm -> node build/wasm.js --prebuild - -> docker build --platform=linux/x86_64 -t llhttp_wasm_builder -f /home/runner/work/node/node/deps/undici/src/build/Dockerfile /home/runner/work/node/node/deps/undici/src - - - -> undici@6.24.1 build:wasm -> node build/wasm.js --docker - -> docker run --rm -t --platform=linux/x86_64 --user 1000:1000 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/undici/lib/llhttp llhttp_wasm_builder node build/wasm.js - - + +> undici@6.25.0 prebuild:wasm +> node build/wasm.js --prebuild + +> docker build --platform=linux/x86_64 -t llhttp_wasm_builder -f /home/runner/work/node/node/deps/undici/src/build/Dockerfile /home/runner/work/node/node/deps/undici/src + + + +> undici@6.25.0 build:wasm +> node build/wasm.js --docker + +> docker run --rm -t --platform=linux/x86_64 --user 1001:1001 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/undici/lib/llhttp llhttp_wasm_builder node build/wasm.js + + alpine-baselayout-3.4.3-r2 alpine-baselayout-data-3.4.3-r2 alpine-keys-2.4-r1 @@ -44,8 +44,8 @@ llvm17-libs-17.0.5-r0 llvm17-linker-tools-17.0.5-r0 mpc1-1.3.1-r1 mpfr4-4.2.1-r0 -musl-1.2.4_git20230717-r5 -musl-dev-1.2.4_git20230717-r5 +musl-1.2.4_git20230717-r6 +musl-dev-1.2.4_git20230717-r6 musl-utils-1.2.4_git20230717-r4 scanelf-1.3.7-r2 scudo-malloc-17.0.5-r0 diff --git a/deps/undici/src/lib/web/websocket/permessage-deflate.js b/deps/undici/src/lib/web/websocket/permessage-deflate.js index 1f1a13038afb5f..6a6e43899c5a95 100644 --- a/deps/undici/src/lib/web/websocket/permessage-deflate.js +++ b/deps/undici/src/lib/web/websocket/permessage-deflate.js @@ -8,40 +8,35 @@ 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 + #maxPayloadSize = 0 /** * @param {Map} extensions */ - constructor (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.#aborted) { - callback(new MessageSizeExceededError()) - return - } - if (!this.#inflate) { let windowBits = Z_DEFAULT_WINDOWBITS @@ -64,23 +59,12 @@ class PerMessageDeflate { this.#inflate[kLength] = 0 this.#inflate.on('data', (data) => { - if (this.#aborted) { - return - } - this.#inflate[kLength] += data.length - if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) { - this.#aborted = true + if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) { + callback(new MessageSizeExceededError()) this.#inflate.removeAllListeners() - this.#inflate.destroy() this.#inflate = null - - if (this.#currentCallback) { - const cb = this.#currentCallback - this.#currentCallback = null - cb(new MessageSizeExceededError()) - } return } @@ -93,14 +77,13 @@ class PerMessageDeflate { }) } - this.#currentCallback = callback this.#inflate.write(chunk) if (fin) { this.#inflate.write(tail) } this.#inflate.flush(() => { - if (this.#aborted || !this.#inflate) { + if (!this.#inflate) { return } @@ -108,7 +91,6 @@ class PerMessageDeflate { this.#inflate[kBuffer].length = 0 this.#inflate[kLength] = 0 - this.#currentCallback = null callback(null, full) }) diff --git a/deps/undici/src/lib/web/websocket/receiver.js b/deps/undici/src/lib/web/websocket/receiver.js index e7f75127aa583c..53e427eb2e4642 100644 --- a/deps/undici/src/lib/web/websocket/receiver.js +++ b/deps/undici/src/lib/web/websocket/receiver.js @@ -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 @@ -26,6 +27,7 @@ const { PerMessageDeflate } = require('./permessage-deflate') class ByteParser extends Writable { #buffers = [] + #fragmentsBytes = 0 #byteOffset = 0 #loop = false @@ -37,18 +39,23 @@ class ByteParser extends Writable { /** @type {Map} */ #extensions + /** @type {number} */ + #maxPayloadSize + /** * @param {import('./websocket').WebSocket} ws * @param {Map|null} extensions + * @param {{ maxPayloadSize?: number }} [options] */ - constructor (ws, extensions) { + 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)) } } @@ -64,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, @@ -152,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) { @@ -176,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() @@ -198,6 +226,10 @@ class ByteParser extends Writable { 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() @@ -210,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) { - failWebsocketConnection(this.ws, error.message) - return - } + this.#extensions.get('permessage-deflate').decompress( + body, + this.#info.fin, + (error, data) => { + if (error) { + failWebsocketConnection(this.ws, error.message) + return + } + + 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()) - this.#fragments.push(data) - - 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 @@ -297,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) diff --git a/deps/undici/src/lib/web/websocket/websocket.js b/deps/undici/src/lib/web/websocket/websocket.js index aa2a20a4f6c9a3..ccedb792169a10 100644 --- a/deps/undici/src/lib/web/websocket/websocket.js +++ b/deps/undici/src/lib/web/websocket/websocket.js @@ -435,7 +435,11 @@ class WebSocket extends EventTarget { // 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)) diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json index 0c57391efcc519..46cb9a8292618f 100644 --- a/deps/undici/src/package.json +++ b/deps/undici/src/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "6.24.1", + "version": "6.25.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { diff --git a/deps/undici/src/scripts/release.js b/deps/undici/src/scripts/release.js index 68587a0c00daec..5b078f5a1edda3 100644 --- a/deps/undici/src/scripts/release.js +++ b/deps/undici/src/scripts/release.js @@ -42,6 +42,7 @@ const generatePr = async ({ github, context, releaseBranch, versionTag }) => { const release = async ({ github, context, releaseBranch, versionTag }) => { const { owner, repo } = context.repo const releaseNotes = await generateReleaseNotes({ github, owner, repo, versionTag, releaseBranch }) + const makeLatest = releaseBranch === 'v6.x' ? 'false' : 'legacy' await github.rest.repos.createRelease({ owner, @@ -52,6 +53,7 @@ const release = async ({ github, context, releaseBranch, versionTag }) => { body: releaseNotes, draft: false, prerelease: false, + make_latest: makeLatest, generate_release_notes: false }) diff --git a/deps/undici/src/types/client.d.ts b/deps/undici/src/types/client.d.ts index d0a5379f33cd70..22833ae671a612 100644 --- a/deps/undici/src/types/client.d.ts +++ b/deps/undici/src/types/client.d.ts @@ -78,6 +78,8 @@ export declare namespace Client { localAddress?: string; /** Max response body size in bytes, -1 is disabled */ maxResponseSize?: number; + /** WebSocket-specific options */ + webSocket?: Client.WebSocketOptions; /** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */ autoSelectFamily?: boolean; /** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */ @@ -103,6 +105,15 @@ export declare namespace Client { bytesWritten?: number bytesRead?: number } + export interface WebSocketOptions { + /** + * Maximum allowed payload size in bytes for WebSocket messages. + * Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. + * Set to 0 to disable the limit. + * @default 134217728 (128 MB) + */ + maxPayloadSize?: number; + } } export default Client; diff --git a/deps/undici/undici.js b/deps/undici/undici.js index d7b149b36673ff..705b85032a06e4 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -584,16 +584,23 @@ var require_dispatcher_base = __commonJS({ var kOnDestroyed = Symbol("onDestroyed"); var kOnClosed = Symbol("onClosed"); var kInterceptedDispatch = Symbol("Intercepted Dispatch"); + var kWebSocketOptions = Symbol("webSocketOptions"); var DispatcherBase = class extends Dispatcher2 { static { __name(this, "DispatcherBase"); } - constructor() { + constructor(opts) { super(); this[kDestroyed] = false; this[kOnDestroyed] = null; this[kClosed] = false; this[kOnClosed] = []; + this[kWebSocketOptions] = opts?.webSocket ?? {}; + } + get webSocketOptions() { + return { + maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 + }; } get destroyed() { return this[kDestroyed]; @@ -856,8 +863,8 @@ var require_pool_base = __commonJS({ static { __name(this, "PoolBase"); } - constructor() { - super(); + constructor(opts) { + super(opts); this[kQueue] = new FixedQueue(); this[kClients] = []; this[kQueued] = 0; @@ -7793,9 +7800,10 @@ var require_client = __commonJS({ autoSelectFamilyAttemptTimeout, // h2 maxConcurrentStreams, - allowH2 + allowH2, + webSocket } = {}) { - super(); + super({ webSocket }); if (keepAlive !== void 0) { throw new InvalidArgumentError("unsupported keepAlive, use pipelining=0 instead"); } @@ -8238,7 +8246,6 @@ var require_pool = __commonJS({ allowH2, ...options } = {}) { - super(); if (connections != null && (!Number.isFinite(connections) || connections < 0)) { throw new InvalidArgumentError("invalid connections"); } @@ -8259,6 +8266,7 @@ var require_pool = __commonJS({ ...connect }); } + super(options); this[kInterceptors] = options.interceptors?.Pool && Array.isArray(options.interceptors.Pool) ? options.interceptors.Pool : []; this[kConnections] = connections || null; this[kUrl] = util.parseOrigin(origin); @@ -8318,7 +8326,6 @@ var require_agent = __commonJS({ __name(this, "Agent"); } constructor({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { - super(); if (typeof factory !== "function") { throw new InvalidArgumentError("factory must be a function."); } @@ -8328,6 +8335,7 @@ var require_agent = __commonJS({ if (!Number.isInteger(maxRedirections) || maxRedirections < 0) { throw new InvalidArgumentError("maxRedirections must be a positive number"); } + super(options); if (connect && typeof connect !== "function") { connect = { ...connect }; } @@ -12323,7 +12331,6 @@ var require_permessage_deflate = __commonJS({ var tail = Buffer.from([0, 0, 255, 255]); var kBuffer = Symbol("kBuffer"); var kLength = Symbol("kLength"); - var kDefaultMaxDecompressedSize = 4 * 1024 * 1024; var PerMessageDeflate = class { static { __name(this, "PerMessageDeflate"); @@ -12331,22 +12338,22 @@ var require_permessage_deflate = __commonJS({ /** @type {import('node:zlib').InflateRaw} */ #inflate; #options = {}; - /** @type {boolean} */ - #aborted = false; - /** @type {Function|null} */ - #currentCallback = null; + #maxPayloadSize = 0; /** * @param {Map} extensions */ - constructor(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) { - if (this.#aborted) { - callback(new MessageSizeExceededError()); - return; - } if (!this.#inflate) { let windowBits = Z_DEFAULT_WINDOWBITS; if (this.#options.serverMaxWindowBits) { @@ -12365,20 +12372,11 @@ var require_permessage_deflate = __commonJS({ this.#inflate[kBuffer] = []; this.#inflate[kLength] = 0; this.#inflate.on("data", (data) => { - if (this.#aborted) { - return; - } this.#inflate[kLength] += data.length; - if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) { - this.#aborted = true; + if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) { + callback(new MessageSizeExceededError()); 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); @@ -12388,19 +12386,17 @@ var require_permessage_deflate = __commonJS({ callback(err); }); } - this.#currentCallback = callback; this.#inflate.write(chunk); if (fin) { this.#inflate.write(tail); } this.#inflate.flush(() => { - if (this.#aborted || !this.#inflate) { + if (!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); }); } @@ -12431,11 +12427,13 @@ var require_receiver = __commonJS({ var { WebsocketFrameSend } = require_frame(); var { closeWebSocketConnection } = require_connection(); var { PerMessageDeflate } = require_permessage_deflate(); + var { MessageSizeExceededError } = require_errors(); var ByteParser = class extends Writable { static { __name(this, "ByteParser"); } #buffers = []; + #fragmentsBytes = 0; #byteOffset = 0; #loop = false; #state = parserStates.INFO; @@ -12443,16 +12441,20 @@ var require_receiver = __commonJS({ #fragments = []; /** @type {Map} */ #extensions; + /** @type {number} */ + #maxPayloadSize; /** * @param {import('./websocket').WebSocket} ws * @param {Map|null} extensions + * @param {{ maxPayloadSize?: number }} [options] */ - constructor(ws, extensions) { + constructor(ws, extensions, options = {}) { super(); this.ws = ws; this.#extensions = extensions == null ? /* @__PURE__ */ 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)); } } /** @@ -12465,6 +12467,13 @@ var require_receiver = __commonJS({ this.#loop = true; 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, @@ -12524,6 +12533,9 @@ var require_receiver = __commonJS({ 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) { @@ -12544,6 +12556,9 @@ var require_receiver = __commonJS({ 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(); @@ -12557,6 +12572,9 @@ var require_receiver = __commonJS({ } 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(); @@ -12567,32 +12585,41 @@ var require_receiver = __commonJS({ 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 (!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) { - failWebsocketConnection(this.ws, error.message); - return; - } - this.#fragments.push(data); - if (!this.#info.fin) { - this.#state = parserStates.INFO; + this.#extensions.get("permessage-deflate").decompress( + body, + this.#info.fin, + (error, data) => { + if (error) { + failWebsocketConnection(this.ws, error.message); + return; + } + 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()); 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; } @@ -12635,6 +12662,21 @@ var require_receiver = __commonJS({ this.#byteOffset -= n; 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); let code; @@ -13080,7 +13122,10 @@ var require_websocket = __commonJS({ */ #onConnectionEstablished(response, parsedExtensions) { 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)); response.socket.ws = this; diff --git a/src/undici_version.h b/src/undici_version.h index 080aa30ea137dd..54a42155b02139 100644 --- a/src/undici_version.h +++ b/src/undici_version.h @@ -2,5 +2,5 @@ // Refer to tools/dep_updaters/update-undici.sh #ifndef SRC_UNDICI_VERSION_H_ #define SRC_UNDICI_VERSION_H_ -#define UNDICI_VERSION "6.24.1" +#define UNDICI_VERSION "6.25.0" #endif // SRC_UNDICI_VERSION_H_