diff --git a/dist/index.js b/dist/index.js index 390fcfd3..0655cb54 100644 --- a/dist/index.js +++ b/dist/index.js @@ -81457,6 +81457,9 @@ const EMPTY_BUF = Buffer.alloc(0) const FastBuffer = Buffer[Symbol.species] const addListener = util.addListener const removeAllListeners = util.removeAllListeners +const kIdleSocketValidation = Symbol('kIdleSocketValidation') +const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout') +const kSocketUsed = Symbol('kSocketUsed') let extractBody @@ -81771,6 +81774,11 @@ class Parser { return -1 } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + const request = client[kQueue][client[kRunningIdx]] if (!request) { return -1 @@ -81874,6 +81882,11 @@ class Parser { return -1 } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + const request = client[kQueue][client[kRunningIdx]] /* istanbul ignore next: difficult to make a test case for */ @@ -82047,6 +82060,7 @@ class Parser { request.onComplete(headers) client[kQueue][client[kRunningIdx]++] = null + socket[kSocketUsed] = true if (socket[kWriting]) { assert(client[kRunning] === 0) @@ -82105,6 +82119,9 @@ async function connectH1 (client, socket) { 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) { @@ -82151,6 +82168,8 @@ async function connectH1 (client, socket) { const client = this[kClient] const parser = this[kParser] + clearIdleSocketValidation(this) + if (parser) { if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { this[kError] = parser.finish() || this[kError] @@ -82216,7 +82235,7 @@ async function connectH1 (client, socket) { return socket.destroyed }, busy (request) { - if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { + if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) { return true } @@ -82254,6 +82273,31 @@ async function connectH1 (client, socket) { } } +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?.() +} + +/** + * @param {import('./client.js')} client + */ function resumeH1 (client) { const socket = client[kSocket] @@ -82268,6 +82312,32 @@ function resumeH1 (client) { 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) @@ -82361,6 +82431,7 @@ function writeH1 (client, request) { } const socket = client[kSocket] + clearIdleSocketValidation(socket) const abort = (err) => { if (request.aborted || request.completed) { @@ -84233,6 +84304,7 @@ class DispatcherBase extends Dispatcher { get webSocketOptions () { return { + maxFragments: this[kWebSocketOptions].maxFragments ?? 131072, maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 } } @@ -90169,32 +90241,25 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) // If the attribute-name case-insensitively matches the string // "SameSite", the user agent MUST process the cookie-av as follows: - // 1. Let enforcement be "Default". - let enforcement = 'Default' - const attributeValueLowercase = attributeValue.toLowerCase() - // 2. If cookie-av's attribute-value is a case-insensitive match for - // "None", set enforcement to "None". - if (attributeValueLowercase.includes('none')) { - enforcement = 'None' - } - // 3. If cookie-av's attribute-value is a case-insensitive match for - // "Strict", set enforcement to "Strict". - if (attributeValueLowercase.includes('strict')) { - enforcement = 'Strict' + // 1. If cookie-av's attribute-value is a case-insensitive match for + // "None", append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of "None". + if (attributeValueLowercase === 'none') { + cookieAttributeList.sameSite = 'None' + } else if (attributeValueLowercase === 'strict') { + // 2. If cookie-av's attribute-value is a case-insensitive match for + // "Strict", append an attribute to the cookie-attribute-list with + // an attribute-name of "SameSite" and an attribute-value of + // "Strict". + cookieAttributeList.sameSite = 'Strict' + } else if (attributeValueLowercase === 'lax') { + // 3. If cookie-av's attribute-value is a case-insensitive match for + // "Lax", append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of "Lax". + cookieAttributeList.sameSite = 'Lax' } - - // 4. If cookie-av's attribute-value is a case-insensitive match for - // "Lax", set enforcement to "Lax". - if (attributeValueLowercase.includes('lax')) { - enforcement = 'Lax' - } - - // 5. Append an attribute to the cookie-attribute-list with an - // attribute-name of "SameSite" and an attribute-value of - // enforcement. - cookieAttributeList.sameSite = enforcement } else { cookieAttributeList.unparsed ??= [] @@ -103020,6 +103085,11 @@ const { closeWebSocketConnection } = __nccwpck_require__(6897) const { PerMessageDeflate } = __nccwpck_require__(9469) const { MessageSizeExceededError } = __nccwpck_require__(8707) +function failWebsocketConnectionWithCode (ws, code, reason) { + closeWebSocketConnection(ws, code, reason, Buffer.byteLength(reason)) + failWebsocketConnection(ws, reason) +} + // This code was influenced by ws released under the MIT license. // Copyright (c) 2011 Einar Otto Stangvik // Copyright (c) 2013 Arnout Kazemier and contributors @@ -103039,19 +103109,23 @@ class ByteParser extends Writable { /** @type {Map} */ #extensions + /** @type {number} */ + #maxFragments + /** @type {number} */ #maxPayloadSize /** * @param {import('./websocket').WebSocket} ws * @param {Map|null} extensions - * @param {{ maxPayloadSize?: number }} [options] + * @param {{ maxFragments?: number, maxPayloadSize?: number }} [options] */ constructor (ws, extensions, options = {}) { super() this.ws = ws this.#extensions = extensions == null ? new Map() : extensions + this.#maxFragments = options.maxFragments ?? 0 this.#maxPayloadSize = options.maxPayloadSize ?? 0 if (this.#extensions.has('permessage-deflate')) { @@ -103075,9 +103149,9 @@ class ByteParser extends Writable { if ( this.#maxPayloadSize > 0 && !isControlFrame(this.#info.opcode) && - this.#info.payloadLength > this.#maxPayloadSize + this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize ) { - failWebsocketConnection(this.ws, 'Payload size exceeds maximum allowed size') + failWebsocketConnectionWithCode(this.ws, 1009, 'Payload size exceeds maximum allowed size') return false } @@ -103242,10 +103316,12 @@ class ByteParser extends Writable { this.#state = parserStates.INFO } else { if (!this.#info.compressed) { - this.writeFragments(body) + if (!this.writeFragments(body)) { + return + } if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { - failWebsocketConnection(this.ws, new MessageSizeExceededError().message) + failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message) return } @@ -103264,14 +103340,17 @@ class ByteParser extends Writable { this.#info.fin, (error, data) => { if (error) { - failWebsocketConnection(this.ws, error.message) + const code = error instanceof MessageSizeExceededError ? 1009 : 1007 + failWebsocketConnectionWithCode(this.ws, code, error.message) return } - this.writeFragments(data) + if (!this.writeFragments(data)) { + return + } if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { - failWebsocketConnection(this.ws, new MessageSizeExceededError().message) + failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message) return } @@ -103341,8 +103420,17 @@ class ByteParser extends Writable { } 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 () { @@ -104395,9 +104483,12 @@ class WebSocket extends EventTarget { // once this happens, the connection is open this[kResponse] = response - const maxPayloadSize = this[kController]?.dispatcher?.webSocketOptions?.maxPayloadSize + 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) diff --git a/package-lock.json b/package-lock.json index 60314117..276c1e31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5637,9 +5637,9 @@ } }, "node_modules/undici": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", - "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "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" diff --git a/package.json b/package.json index 9bded65f..538725a2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "package": "ncc build ./src/index.js" }, "overrides": { - "undici": "^6.26.0", + "undici": "^6.27.0", "test-exclude": "^8.0.0" }, "dependencies": {