diff --git a/lib/entry-points.js b/lib/entry-points.js index 321b9ded49..406916cf6e 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -2339,13 +2339,21 @@ var require_dispatcher_base = __commonJS({ var kOnDestroyed = /* @__PURE__ */ Symbol("onDestroyed"); var kOnClosed = /* @__PURE__ */ Symbol("onClosed"); var kInterceptedDispatch = /* @__PURE__ */ Symbol("Intercepted Dispatch"); + var kWebSocketOptions = /* @__PURE__ */ Symbol("webSocketOptions"); var DispatcherBase = class extends Dispatcher { - constructor() { + constructor(opts) { super(); this[kDestroyed] = false; this[kOnDestroyed] = null; this[kClosed] = false; this[kOnClosed] = []; + this[kWebSocketOptions] = opts?.webSocket ?? {}; + } + get webSocketOptions() { + return { + maxFragments: this[kWebSocketOptions].maxFragments ?? 131072, + maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 + }; } get destroyed() { return this[kDestroyed]; @@ -5998,6 +6006,9 @@ var require_client_h1 = __commonJS({ var FastBuffer = Buffer[Symbol.species]; var addListener = util3.addListener; var removeAllListeners = util3.removeAllListeners; + var kIdleSocketValidation = /* @__PURE__ */ Symbol("kIdleSocketValidation"); + var kIdleSocketValidationTimeout = /* @__PURE__ */ Symbol("kIdleSocketValidationTimeout"); + var kSocketUsed = /* @__PURE__ */ Symbol("kSocketUsed"); var extractBody; async function lazyllhttp() { const llhttpWasmData = process.env.JEST_WORKER_ID ? require_llhttp_wasm() : void 0; @@ -6160,24 +6171,55 @@ var require_client_h1 = __commonJS({ currentBufferRef = null; } const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr; - if (ret === constants.ERROR.PAUSED_UPGRADE) { - this.onUpgrade(data.slice(offset)); - } else if (ret === constants.ERROR.PAUSED) { - this.paused = true; - socket.unshift(data.slice(offset)); - } else if (ret !== constants.ERROR.OK) { - const ptr = llhttp.llhttp_get_error_reason(this.ptr); - let message = ""; - if (ptr) { - const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0); - message = "Response does not match the HTTP/1.1 protocol (" + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + ")"; - } - throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)); + if (ret !== constants.ERROR.OK) { + const body = data.subarray(offset); + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(body); + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true; + socket.unshift(body); + } else { + throw this.createError(ret, body); + } } } catch (err) { util3.destroy(socket, err); } } + finish() { + assert(currentParser === null); + assert(this.ptr != null); + assert(!this.paused); + const { llhttp } = this; + let ret; + try { + currentParser = this; + ret = llhttp.llhttp_finish(this.ptr); + } finally { + currentParser = null; + } + if (ret === constants.ERROR.OK) { + return null; + } + if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) { + this.paused = true; + return null; + } + return this.createError(ret, EMPTY_BUF); + } + createError(ret, data) { + const { llhttp, contentLength, bytesRead } = this; + if (contentLength && bytesRead !== parseInt(contentLength, 10)) { + return new ResponseContentLengthMismatchError(); + } + const ptr = llhttp.llhttp_get_error_reason(this.ptr); + let message = ""; + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0); + message = "Response does not match the HTTP/1.1 protocol (" + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + ")"; + } + return new HTTPParserError(message, constants.ERROR[ret], data); + } destroy() { assert(this.ptr != null); assert(currentParser == null); @@ -6197,6 +6239,10 @@ var require_client_h1 = __commonJS({ if (socket.destroyed) { return -1; } + if (client[kRunning] === 0) { + util3.destroy(socket, new SocketError("bad response", util3.getSocketInfo(socket))); + return -1; + } const request3 = client[kQueue][client[kRunningIdx]]; if (!request3) { return -1; @@ -6276,6 +6322,10 @@ var require_client_h1 = __commonJS({ if (socket.destroyed) { return -1; } + if (client[kRunning] === 0) { + util3.destroy(socket, new SocketError("bad response", util3.getSocketInfo(socket))); + return -1; + } const request3 = client[kQueue][client[kRunningIdx]]; if (!request3) { return -1; @@ -6401,6 +6451,7 @@ var require_client_h1 = __commonJS({ } request3.onComplete(headers); client[kQueue][client[kRunningIdx]++] = null; + socket[kSocketUsed] = true; if (socket[kWriting]) { assert(client[kRunning] === 0); util3.destroy(socket, new InformationalError("reset")); @@ -6444,12 +6495,19 @@ var require_client_h1 = __commonJS({ socket[kWriting] = false; socket[kReset] = false; socket[kBlocking] = false; + socket[kIdleSocketValidation] = 0; + socket[kIdleSocketValidationTimeout] = null; + socket[kSocketUsed] = false; socket[kParser] = new Parser(client, socket, llhttpInstance); addListener(socket, "error", function(err) { assert(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID"); const parser = this[kParser]; if (err.code === "ECONNRESET" && parser.statusCode && !parser.shouldKeepAlive) { - parser.onMessageComplete(); + const parserErr = parser.finish(); + if (parserErr) { + this[kError] = parserErr; + this[kClient][kOnError](parserErr); + } return; } this[kError] = err; @@ -6464,7 +6522,10 @@ var require_client_h1 = __commonJS({ addListener(socket, "end", function() { const parser = this[kParser]; if (parser.statusCode && !parser.shouldKeepAlive) { - parser.onMessageComplete(); + const parserErr = parser.finish(); + if (parserErr) { + util3.destroy(this, parserErr); + } return; } util3.destroy(this, new SocketError("other side closed", util3.getSocketInfo(this))); @@ -6472,9 +6533,10 @@ var require_client_h1 = __commonJS({ addListener(socket, "close", function() { const client2 = this[kClient]; const parser = this[kParser]; + clearIdleSocketValidation(this); if (parser) { if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { - parser.onMessageComplete(); + this[kError] = parser.finish() || this[kError]; } this[kParser].destroy(); this[kParser] = null; @@ -6523,7 +6585,7 @@ var require_client_h1 = __commonJS({ return socket.destroyed; }, busy(request3) { - if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { + if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) { return true; } if (request3) { @@ -6541,6 +6603,24 @@ var require_client_h1 = __commonJS({ } }; } + function clearIdleSocketValidation(socket) { + if (socket[kIdleSocketValidationTimeout]) { + clearTimeout(socket[kIdleSocketValidationTimeout]); + socket[kIdleSocketValidationTimeout] = null; + } + socket[kIdleSocketValidation] = 0; + } + function scheduleIdleSocketValidation(client, socket) { + socket[kIdleSocketValidation] = 1; + socket[kIdleSocketValidationTimeout] = setTimeout(() => { + socket[kIdleSocketValidationTimeout] = null; + socket[kIdleSocketValidation] = 2; + if (client[kSocket] === socket && !socket.destroyed) { + client[kResume](); + } + }, 0); + socket[kIdleSocketValidationTimeout].unref?.(); + } function resumeH1(client) { const socket = client[kSocket]; if (socket && !socket.destroyed) { @@ -6553,6 +6633,29 @@ var require_client_h1 = __commonJS({ socket.ref(); socket[kNoRef] = false; } + if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) { + if (socket[kIdleSocketValidation] === 0) { + scheduleIdleSocketValidation(client, socket); + socket[kParser].readMore(); + if (socket.destroyed) { + return; + } + return; + } + if (socket[kIdleSocketValidation] === 1) { + socket[kParser].readMore(); + if (socket.destroyed) { + return; + } + return; + } + } + if (client[kRunning] === 0) { + socket[kParser].readMore(); + if (socket.destroyed) { + return; + } + } if (client[kSize] === 0) { if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) { socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE); @@ -6605,6 +6708,7 @@ var require_client_h1 = __commonJS({ process.emitWarning(new RequestContentLengthMismatchError()); } const socket = client[kSocket]; + clearIdleSocketValidation(socket); const abort = (err) => { if (request3.aborted || request3.completed) { return; @@ -7784,9 +7888,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"); } @@ -8292,8 +8397,8 @@ var require_pool_base = __commonJS({ var kRemoveClient = /* @__PURE__ */ Symbol("remove client"); var kStats = /* @__PURE__ */ Symbol("stats"); var PoolBase = class extends DispatcherBase { - constructor() { - super(); + constructor(opts) { + super(opts); this[kQueue] = new FixedQueue(); this[kClients] = []; this[kQueued] = 0; @@ -8464,7 +8569,6 @@ var require_pool = __commonJS({ allowH2, ...options } = {}) { - super(); if (connections != null && (!Number.isFinite(connections) || connections < 0)) { throw new InvalidArgumentError("invalid connections"); } @@ -8485,6 +8589,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] = util3.parseOrigin(origin); @@ -8684,7 +8789,6 @@ var require_agent = __commonJS({ } var Agent = class extends DispatcherBase { constructor({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { - super(); if (typeof factory !== "function") { throw new InvalidArgumentError("factory must be a function."); } @@ -8694,6 +8798,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 }; } @@ -16388,18 +16493,14 @@ var require_parse = __commonJS({ } else if (attributeNameLowercase === "httponly") { cookieAttributeList.httpOnly = true; } else if (attributeNameLowercase === "samesite") { - let enforcement = "Default"; const attributeValueLowercase = attributeValue.toLowerCase(); - if (attributeValueLowercase.includes("none")) { - enforcement = "None"; - } - if (attributeValueLowercase.includes("strict")) { - enforcement = "Strict"; + if (attributeValueLowercase === "none") { + cookieAttributeList.sameSite = "None"; + } else if (attributeValueLowercase === "strict") { + cookieAttributeList.sameSite = "Strict"; + } else if (attributeValueLowercase === "lax") { + cookieAttributeList.sameSite = "Lax"; } - if (attributeValueLowercase.includes("lax")) { - enforcement = "Lax"; - } - cookieAttributeList.sameSite = enforcement; } else { cookieAttributeList.unparsed ??= []; cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`); @@ -17327,27 +17428,26 @@ var require_permessage_deflate = __commonJS({ var tail = Buffer.from([0, 0, 255, 255]); var kBuffer = /* @__PURE__ */ Symbol("kBuffer"); var kLength = /* @__PURE__ */ Symbol("kLength"); - var kDefaultMaxDecompressedSize = 4 * 1024 * 1024; var PerMessageDeflate = class { /** @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) { @@ -17366,20 +17466,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); @@ -17389,19 +17480,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); }); } @@ -17432,8 +17521,14 @@ var require_receiver = __commonJS({ var { WebsocketFrameSend } = require_frame(); var { closeWebSocketConnection } = require_connection(); var { PerMessageDeflate } = require_permessage_deflate(); + var { MessageSizeExceededError } = require_errors(); + function failWebsocketConnectionWithCode(ws, code, reason) { + closeWebSocketConnection(ws, code, reason, Buffer.byteLength(reason)); + failWebsocketConnection(ws, reason); + } var ByteParser = class extends Writable { #buffers = []; + #fragmentsBytes = 0; #byteOffset = 0; #loop = false; #state = parserStates.INFO; @@ -17441,16 +17536,23 @@ var require_receiver = __commonJS({ #fragments = []; /** @type {Map} */ #extensions; + /** @type {number} */ + #maxFragments; + /** @type {number} */ + #maxPayloadSize; /** * @param {import('./websocket').WebSocket} ws * @param {Map|null} extensions + * @param {{ maxFragments?: number, maxPayloadSize?: number }} [options] */ - constructor(ws, extensions) { + constructor(ws, extensions, options = {}) { super(); this.ws = ws; this.#extensions = extensions == null ? /* @__PURE__ */ 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)); + this.#extensions.set("permessage-deflate", new PerMessageDeflate(extensions, options)); } } /** @@ -17463,6 +17565,13 @@ var require_receiver = __commonJS({ this.#loop = true; this.run(callback); } + #validatePayloadLength() { + if (this.#maxPayloadSize > 0 && !isControlFrame(this.#info.opcode) && this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize) { + failWebsocketConnectionWithCode(this.ws, 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, @@ -17522,6 +17631,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) { @@ -17542,6 +17654,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(); @@ -17555,6 +17670,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(); @@ -17565,32 +17683,46 @@ var require_receiver = __commonJS({ this.#state = parserStates.INFO; } else { if (!this.#info.compressed) { - this.#fragments.push(body); + if (!this.writeFragments(body)) { + return; + } + if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { + failWebsocketConnectionWithCode(this.ws, 1009, 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, (error3, data) => { - if (error3) { - failWebsocketConnection(this.ws, error3.message); - return; - } - this.#fragments.push(data); - if (!this.#info.fin) { - this.#state = parserStates.INFO; + this.#extensions.get("permessage-deflate").decompress( + body, + this.#info.fin, + (error3, data) => { + if (error3) { + const code = error3 instanceof MessageSizeExceededError ? 1009 : 1007; + failWebsocketConnectionWithCode(this.ws, code, error3.message); + return; + } + if (!this.writeFragments(data)) { + return; + } + if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { + failWebsocketConnectionWithCode(this.ws, 1009, 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; } @@ -17633,6 +17765,26 @@ var require_receiver = __commonJS({ this.#byteOffset -= n; return buffer; } + writeFragments(fragment) { + if (this.#maxFragments > 0 && this.#fragments.length === this.#maxFragments) { + failWebsocketConnectionWithCode(this.ws, 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) { + 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; @@ -18070,7 +18222,13 @@ var require_websocket = __commonJS({ */ #onConnectionEstablished(response, parsedExtensions) { this[kResponse] = response; - const parser = new ByteParser(this, parsedExtensions); + const webSocketOptions = this[kController]?.dispatcher?.webSocketOptions; + const maxFragments = webSocketOptions?.maxFragments; + const maxPayloadSize = webSocketOptions?.maxPayloadSize; + const parser = new ByteParser(this, parsedExtensions, { + maxFragments, + maxPayloadSize + }); parser.on("drain", onParserDrain); parser.on("error", onParserError.bind(this)); response.socket.ws = this; diff --git a/package-lock.json b/package-lock.json index 83775deef8..a580a2c5b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8896,9 +8896,9 @@ } }, "node_modules/supertap/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "dev": true, "license": "MIT", "dependencies": { @@ -9360,9 +9360,9 @@ } }, "node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", "license": "MIT", "engines": { "node": ">=18.17"