From 1eaa2a6e86d5fafe5c327d0fa82d6bdda6c20e4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 10:26:58 +0000 Subject: [PATCH] build(deps): bump undici from 7.30.0 to 7.30.1 (#1430) Co-authored-by: Fernandez Ludovic --- dist/post_run/index.js | 640 +++++++++++++++++++++++++++++------------ dist/run/index.js | 640 +++++++++++++++++++++++++++++------------ package-lock.json | 6 +- 3 files changed, 903 insertions(+), 383 deletions(-) diff --git a/dist/post_run/index.js b/dist/post_run/index.js index 5af4943..73f2ee1 100644 --- a/dist/post_run/index.js +++ b/dist/post_run/index.js @@ -33621,16 +33621,22 @@ const SessionCache = class WeakSessionCache { if (this._sessionCache.has(sessionKey)) { this._sessionCache.delete(sessionKey) } else if (this._sessionCache.size >= this._maxCachedSessions) { + // Make room for the new session: prefer an entry whose session has + // already been collected, otherwise evict the oldest one. + let evicted = false for (const [key, ref] of this._sessionCache) { if (ref.deref() === undefined) { this._sessionCache.delete(key) - return + evicted = true + break } } - const oldest = this._sessionCache.keys().next() - if (!oldest.done) { - this._sessionCache.delete(oldest.value) + if (!evicted) { + const oldest = this._sessionCache.keys().next() + if (!oldest.done) { + this._sessionCache.delete(oldest.value) + } } } @@ -35760,6 +35766,7 @@ module.exports = { kDestroy: Symbol('destroy'), kDispatch: Symbol('dispatch'), kUrl: Symbol('url'), + kRedirectCacheControl: Symbol('redirect cache control'), kWriting: Symbol('writing'), kResuming: Symbol('resuming'), kQueue: Symbol('queue'), @@ -37028,7 +37035,7 @@ function defaultFactory (origin, opts) { } class Agent extends DispatcherBase { - constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) { + constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, tls, ...options } = {}) { if (typeof factory !== 'function') { throw new InvalidArgumentError('factory must be a function.') } @@ -37047,7 +37054,11 @@ class Agent extends DispatcherBase { connect = { ...connect } } - this[kOptions] = { ...util.deepClone(options), maxOrigins, connect } + if (tls && typeof tls !== 'function') { + tls = { ...tls } + } + + this[kOptions] = { ...util.deepClone(options), maxOrigins, connect, tls } this[kFactory] = factory this[kClients] = new Map() this[kOrigins] = new Set() @@ -38329,8 +38340,6 @@ function connectH1 (client, socket) { } function onHttpSocketError (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - const parser = this[kParser] // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded @@ -39448,8 +39457,6 @@ function onHttp2SendPing (session) { } function onHttp2SessionError (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - this[kSocket][kError] = err this[kClient][kOnError](err) } @@ -39591,8 +39598,6 @@ function onHttp2SocketClose () { } function onHttp2SocketError (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - this[kError] = err this[kClient][kOnError](err) @@ -40338,7 +40343,8 @@ class Client extends DispatcherBase { initialWindowSize, connectionWindowSize, pingInterval, - webSocket + webSocket, + eventSource } = {}) { if (keepAlive !== undefined) { throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') @@ -40446,7 +40452,7 @@ class Client extends DispatcherBase { throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0') } - super({ webSocket }) + super({ webSocket, eventSource }) if (typeof connect !== 'function') { connect = buildConnector({ @@ -40899,6 +40905,7 @@ module.exports = Client "use strict"; +const buffer = __nccwpck_require__(4573) const Dispatcher = __nccwpck_require__(30883) const UnwrapHandler = __nccwpck_require__(92365) const { @@ -40911,6 +40918,7 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = __nccwpck_require__ const kOnDestroyed = Symbol('onDestroyed') const kOnClosed = Symbol('onClosed') const kWebSocketOptions = Symbol('webSocketOptions') +const kEventSourceOptions = Symbol('eventSourceOptions') class DispatcherBase extends Dispatcher { /** @type {boolean} */ @@ -40931,6 +40939,7 @@ class DispatcherBase extends Dispatcher { constructor (opts) { super() this[kWebSocketOptions] = opts?.webSocket ?? {} + this[kEventSourceOptions] = opts?.eventSource ?? {} } /** @@ -40943,6 +40952,15 @@ class DispatcherBase extends Dispatcher { } } + /** + * @returns {import('../../types/client').Client.EventSourceOptions} + */ + get eventSourceOptions () { + return { + maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength + } + } + /** @returns {boolean} */ get destroyed () { return this[kDestroyed] @@ -42743,6 +42761,7 @@ module.exports = { const util = __nccwpck_require__(3440) +const { kRedirectCacheControl } = __nccwpck_require__(36443) const { parseCacheControlHeader, hasInvalidCacheControlDirective, @@ -42755,6 +42774,19 @@ const { parseHttpDate } = __nccwpck_require__(25453) function noop () {} +/** + * @param {string | URL} a + * @param {string | URL} b + * @returns {boolean} + */ +function sameOrigin (a, b) { + try { + return new URL(String(a)).origin === new URL(String(b)).origin + } catch { + return String(a) === String(b) + } +} + // Status codes that we can use some heuristics on to cache const HEURISTICALLY_CACHEABLE_STATUS_CODES = [ 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501 @@ -42893,6 +42925,14 @@ class CacheHandler { */ #writeStream + /** + * The URLs visited while following redirects, shared by reference from the + * redirect handler through the request context. Carries the suppressed hops' + * cache-control under {@link kRedirectCacheControl}. + * @type {URL[] | undefined} + */ + #history + /** * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey @@ -42909,6 +42949,9 @@ class CacheHandler { onRequestStart (controller, context) { this.#writeStream?.destroy() this.#writeStream = undefined + // The redirect handler mutates this shared history as redirects are + // followed, allowing the cache to identify the response's actual origin. + this.#history = context?.history this.#handler.onRequestStart?.(controller, context) } @@ -42948,6 +42991,29 @@ class CacheHandler { return downstreamOnHeaders() } + // Never store a response produced by a different origin under the original + // request's cache key. + if (this.#history && this.#history.length > 0) { + const finalUrl = this.#history[this.#history.length - 1] + if (!sameOrigin(finalUrl.origin, this.#cacheKey.origin)) { + return downstreamOnHeaders() + } + } + + // Preserve restrictive cache policy from suppressed redirect responses. + const redirectCacheControl = this.#history?.[kRedirectCacheControl] + if (redirectCacheControl) { + for (const hopCacheControl of redirectCacheControl) { + const hopDirectives = hopCacheControl ? parseCacheControlHeader(hopCacheControl) : {} + if ( + hopDirectives['no-store'] === true || + (this.#cacheType === 'shared' && hopDirectives.private === true) + ) { + return downstreamOnHeaders() + } + } + } + const cacheControlHeader = resHeaders['cache-control'] const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {} @@ -43749,6 +43815,7 @@ const DEFAULT_MAX_BUFFER_SIZE = 5 * 1024 * 1024 * @property {number} bufferedBytes * @property {object | null} pendingTrailers * @property {boolean} done + * @property {boolean} [pendingStart] onRequestStart deferred until the primary request's context is known */ /** @@ -43763,6 +43830,20 @@ class DeduplicationHandler { */ #primaryHandler + /** + * Whether the primary request has started (onRequestStart received). Until + * then the request context is unknown and cannot be given to waiting handlers. + * @type {boolean} + */ + #requestStarted = false + + /** + * The primary request context, replayed to waiting handlers so they observe + * the same redirect history as the primary. + * @type {any} + */ + #context = null + /** * @type {WaitingHandler[]} */ @@ -43845,20 +43926,30 @@ class DeduplicationHandler { const waitingController = waitingHandler.controller try { - handler.onRequestStart?.(waitingController, null) + if (this.#requestStarted) { + // The primary request has started, so its context (including the + // redirect history) is known and can be delivered immediately. + handler.onRequestStart?.(waitingController, this.#context) - if (waitingController.aborted) { - waitingHandler.done = true - return true - } + if (waitingController.aborted) { + waitingHandler.done = true + return true + } - if (this.#responseStarted) { - handler.onResponseStart?.( - waitingController, - this.#statusCode, - this.#headers, - this.#statusMessage - ) + if (this.#responseStarted) { + handler.onResponseStart?.( + waitingController, + this.#statusCode, + this.#headers, + this.#statusMessage + ) + } + } else { + // The primary request has not started yet. Defer onRequestStart until + // its context is known, so the waiting handler observes the same + // redirect history as the primary (a cache handler relies on it to + // reject cross-origin redirect responses). + waitingHandler.pendingStart = true } } catch { // Ignore errors from waiting handlers @@ -43879,6 +43970,24 @@ class DeduplicationHandler { */ onRequestStart (controller, context) { this.#controller = controller + if (!this.#requestStarted) { + this.#requestStarted = true + this.#context = context + // Deliver the now-known context to handlers that joined before the + // primary request started. + for (const waitingHandler of this.#waitingHandlers) { + if (waitingHandler.pendingStart && !waitingHandler.done && !waitingHandler.controller.aborted) { + waitingHandler.pendingStart = false + try { + waitingHandler.handler.onRequestStart?.(waitingHandler.controller, context) + } catch { + // Ignore errors from waiting handlers + waitingHandler.done = true + } + } + } + this.#pruneDoneWaitingHandlers() + } this.#primaryHandler.onRequestStart?.(controller, context) } @@ -44052,7 +44161,8 @@ class DeduplicationHandler { bufferedChunks: [], bufferedBytes: 0, pendingTrailers: null, - done: false + done: false, + pendingStart: false } const state = { @@ -44202,7 +44312,7 @@ module.exports = DeduplicationHandler const util = __nccwpck_require__(3440) -const { kBodyUsed } = __nccwpck_require__(36443) +const { kBodyUsed, kRedirectCacheControl } = __nccwpck_require__(36443) const assert = __nccwpck_require__(34589) const { InvalidArgumentError } = __nccwpck_require__(68707) const EE = __nccwpck_require__(78474) @@ -44248,6 +44358,11 @@ class RedirectHandler { this.maxRedirections = maxRedirections this.handler = handler this.history = [] + // Retain cache-control from suppressed redirect hops for an outer cache. + Object.defineProperty(this.history, kRedirectCacheControl, { + value: [], + enumerable: false + }) if (util.isStream(this.opts.body)) { // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp @@ -44331,6 +44446,8 @@ class RedirectHandler { return } + this.history[kRedirectCacheControl].push(headers['cache-control']) + const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))) const path = search ? `${pathname}${search}` : pathname @@ -46035,9 +46152,14 @@ class DecompressHandler extends DecoratorHandler { this.#decompressedSize = decompressedSize } - const result = super.onResponseData(this.#controller, chunk) - if (result === false && !this.#controller.paused) { - this.#controller.pause() + try { + const result = super.onResponseData(this.#controller, chunk) + if (result === false && !this.#controller.paused) { + this.#controller.pause() + } + } catch (err) { + this.#fail(err) + return } } } while (this.#drainRequested && !this.#terminated && !this.#controller.paused) @@ -46054,9 +46176,14 @@ class DecompressHandler extends DecoratorHandler { return } - this.#terminated = true this.#cleanupDecompressors() - super.onResponseEnd(this.#controller, this.#trailers) + try { + super.onResponseEnd(this.#controller, this.#trailers) + } catch (err) { + this.#fail(err) + return + } + this.#terminated = true } #onDecompressionEnd () { @@ -53701,8 +53828,10 @@ function getCookies (headers) { const cookie = headers.get('cookie') + // A null prototype keeps a cookie named `__proto__` from hitting the + // Object.prototype setter, which would silently drop it. /** @type {Record} */ - const out = {} + const out = { __proto__: null } if (!cookie) { return out @@ -53968,220 +54097,221 @@ function parseSetCookie (header) { * @param {Object.} [cookieAttributeList={}] */ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) { - // 1. If the unparsed-attributes string is empty, skip the rest of - // these steps. - if (unparsedAttributes.length === 0) { - return cookieAttributeList - } + while (true) { + // 1. If the unparsed-attributes string is empty, skip the rest of + // these steps. + if (unparsedAttributes.length === 0) { + return cookieAttributeList + } - // 2. Discard the first character of the unparsed-attributes (which - // will be a %x3B (";") character). - assert(unparsedAttributes[0] === ';') - unparsedAttributes = unparsedAttributes.slice(1) + // 2. Discard the first character of the unparsed-attributes (which + // will be a %x3B (";") character). + assert(unparsedAttributes[0] === ';') + unparsedAttributes = unparsedAttributes.slice(1) - let cookieAv = '' + let cookieAv = '' - // 3. If the remaining unparsed-attributes contains a %x3B (";") - // character: - if (unparsedAttributes.includes(';')) { + // 3. If the remaining unparsed-attributes contains a %x3B (";") + // character: + if (unparsedAttributes.includes(';')) { // 1. Consume the characters of the unparsed-attributes up to, but // not including, the first %x3B (";") character. - cookieAv = collectASequenceOfCodePointsFast( - ';', - unparsedAttributes, - { position: 0 } - ) - unparsedAttributes = unparsedAttributes.slice(cookieAv.length) - } else { + cookieAv = collectASequenceOfCodePointsFast( + ';', + unparsedAttributes, + { position: 0 } + ) + unparsedAttributes = unparsedAttributes.slice(cookieAv.length) + } else { // Otherwise: - // 1. Consume the remainder of the unparsed-attributes. - cookieAv = unparsedAttributes - unparsedAttributes = '' - } + // 1. Consume the remainder of the unparsed-attributes. + cookieAv = unparsedAttributes + unparsedAttributes = '' + } - // Let the cookie-av string be the characters consumed in this step. + // Let the cookie-av string be the characters consumed in this step. - let attributeName = '' - let attributeValue = '' + let attributeName = '' + let attributeValue = '' - // 4. If the cookie-av string contains a %x3D ("=") character: - if (cookieAv.includes('=')) { + // 4. If the cookie-av string contains a %x3D ("=") character: + if (cookieAv.includes('=')) { // 1. The (possibly empty) attribute-name string consists of the // characters up to, but not including, the first %x3D ("=") // character, and the (possibly empty) attribute-value string // consists of the characters after the first %x3D ("=") // character. - const position = { position: 0 } + const position = { position: 0 } - attributeName = collectASequenceOfCodePointsFast( - '=', - cookieAv, - position - ) - attributeValue = cookieAv.slice(position.position + 1) - } else { + attributeName = collectASequenceOfCodePointsFast( + '=', + cookieAv, + position + ) + attributeValue = cookieAv.slice(position.position + 1) + } else { // Otherwise: - // 1. The attribute-name string consists of the entire cookie-av - // string, and the attribute-value string is empty. - attributeName = cookieAv - } + // 1. The attribute-name string consists of the entire cookie-av + // string, and the attribute-value string is empty. + attributeName = cookieAv + } - // 5. Remove any leading or trailing WSP characters from the attribute- - // name string and the attribute-value string. - attributeName = attributeName.trim() - attributeValue = attributeValue.trim() + // 5. Remove any leading or trailing WSP characters from the attribute- + // name string and the attribute-value string. + attributeName = attributeName.trim() + attributeValue = attributeValue.trim() - // 6. If the attribute-value is longer than 1024 octets, ignore the - // cookie-av string and return to Step 1 of this algorithm. - if (attributeValue.length > maxAttributeValueSize) { - return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) - } + // 6. If the attribute-value is longer than 1024 octets, ignore the + // cookie-av string and return to Step 1 of this algorithm. + if (attributeValue.length > maxAttributeValueSize) { + continue + } - // 7. Process the attribute-name and attribute-value according to the - // requirements in the following subsections. (Notice that - // attributes with unrecognized attribute-names are ignored.) - const attributeNameLowercase = attributeName.toLowerCase() + // 7. Process the attribute-name and attribute-value according to the + // requirements in the following subsections. (Notice that + // attributes with unrecognized attribute-names are ignored.) + const attributeNameLowercase = attributeName.toLowerCase() - // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1 - // If the attribute-name case-insensitively matches the string - // "Expires", the user agent MUST process the cookie-av as follows. - if (attributeNameLowercase === 'expires') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1 + // If the attribute-name case-insensitively matches the string + // "Expires", the user agent MUST process the cookie-av as follows. + if (attributeNameLowercase === 'expires') { // 1. Let the expiry-time be the result of parsing the attribute-value // as cookie-date (see Section 5.1.1). - const expiryTime = new Date(attributeValue) + const expiryTime = new Date(attributeValue) - // 2. If the attribute-value failed to parse as a cookie date, ignore - // the cookie-av. + // 2. If the attribute-value failed to parse as a cookie date, ignore + // the cookie-av. - cookieAttributeList.expires = expiryTime - } else if (attributeNameLowercase === 'max-age') { + cookieAttributeList.expires = expiryTime + } else if (attributeNameLowercase === 'max-age') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2 // If the attribute-name case-insensitively matches the string "Max- // Age", the user agent MUST process the cookie-av as follows. - // 1. If the first character of the attribute-value is not a DIGIT or a - // "-" character, ignore the cookie-av. - const charCode = attributeValue.charCodeAt(0) + // 1. If the first character of the attribute-value is not a DIGIT or a + // "-" character, ignore the cookie-av. + const charCode = attributeValue.charCodeAt(0) - if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') { - return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) - } + if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') { + continue + } - // 2. If the remainder of attribute-value contains a non-DIGIT - // character, ignore the cookie-av. - if (!/^\d+$/.test(attributeValue)) { - return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) - } + // 2. If the remainder of attribute-value contains a non-DIGIT + // character, ignore the cookie-av. + if (!/^\d+$/.test(attributeValue)) { + continue + } - // 3. Let delta-seconds be the attribute-value converted to an integer. - const deltaSeconds = Number(attributeValue) + // 3. Let delta-seconds be the attribute-value converted to an integer. + const deltaSeconds = Number(attributeValue) - // 4. Let cookie-age-limit be the maximum age of the cookie (which - // SHOULD be 400 days or less, see Section 4.1.2.2). + // 4. Let cookie-age-limit be the maximum age of the cookie (which + // SHOULD be 400 days or less, see Section 4.1.2.2). - // 5. Set delta-seconds to the smaller of its present value and cookie- - // age-limit. - // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs) + // 5. Set delta-seconds to the smaller of its present value and cookie- + // age-limit. + // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs) - // 6. If delta-seconds is less than or equal to zero (0), let expiry- - // time be the earliest representable date and time. Otherwise, let - // the expiry-time be the current date and time plus delta-seconds - // seconds. - // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds + // 6. If delta-seconds is less than or equal to zero (0), let expiry- + // time be the earliest representable date and time. Otherwise, let + // the expiry-time be the current date and time plus delta-seconds + // seconds. + // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds - // 7. Append an attribute to the cookie-attribute-list with an - // attribute-name of Max-Age and an attribute-value of expiry-time. - cookieAttributeList.maxAge = deltaSeconds - } else if (attributeNameLowercase === 'domain') { + // 7. Append an attribute to the cookie-attribute-list with an + // attribute-name of Max-Age and an attribute-value of expiry-time. + cookieAttributeList.maxAge = deltaSeconds + } else if (attributeNameLowercase === 'domain') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3 // If the attribute-name case-insensitively matches the string "Domain", // the user agent MUST process the cookie-av as follows. - // 1. Let cookie-domain be the attribute-value. - let cookieDomain = attributeValue + // 1. Let cookie-domain be the attribute-value. + let cookieDomain = attributeValue - // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be - // cookie-domain without its leading %x2E ("."). - if (cookieDomain[0] === '.') { - cookieDomain = cookieDomain.slice(1) - } + // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be + // cookie-domain without its leading %x2E ("."). + if (cookieDomain[0] === '.') { + cookieDomain = cookieDomain.slice(1) + } - // 3. Convert the cookie-domain to lower case. - cookieDomain = cookieDomain.toLowerCase() + // 3. Convert the cookie-domain to lower case. + cookieDomain = cookieDomain.toLowerCase() - // 4. Append an attribute to the cookie-attribute-list with an - // attribute-name of Domain and an attribute-value of cookie-domain. - cookieAttributeList.domain = cookieDomain - } else if (attributeNameLowercase === 'path') { + // 4. Append an attribute to the cookie-attribute-list with an + // attribute-name of Domain and an attribute-value of cookie-domain. + cookieAttributeList.domain = cookieDomain + } else if (attributeNameLowercase === 'path') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4 // If the attribute-name case-insensitively matches the string "Path", // the user agent MUST process the cookie-av as follows. - // 1. If the attribute-value is empty or if the first character of the - // attribute-value is not %x2F ("/"): - let cookiePath = '' - if (attributeValue.length === 0 || attributeValue[0] !== '/') { + // 1. If the attribute-value is empty or if the first character of the + // attribute-value is not %x2F ("/"): + let cookiePath = '' + if (attributeValue.length === 0 || attributeValue[0] !== '/') { // 1. Let cookie-path be the default-path. - cookiePath = '/' - } else { + cookiePath = '/' + } else { // Otherwise: - // 1. Let cookie-path be the attribute-value. - cookiePath = attributeValue - } + // 1. Let cookie-path be the attribute-value. + cookiePath = attributeValue + } - // 2. Append an attribute to the cookie-attribute-list with an - // attribute-name of Path and an attribute-value of cookie-path. - cookieAttributeList.path = cookiePath - } else if (attributeNameLowercase === 'secure') { + // 2. Append an attribute to the cookie-attribute-list with an + // attribute-name of Path and an attribute-value of cookie-path. + cookieAttributeList.path = cookiePath + } else if (attributeNameLowercase === 'secure') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5 // If the attribute-name case-insensitively matches the string "Secure", // the user agent MUST append an attribute to the cookie-attribute-list // with an attribute-name of Secure and an empty attribute-value. - cookieAttributeList.secure = true - } else if (attributeNameLowercase === 'httponly') { + cookieAttributeList.secure = true + } else if (attributeNameLowercase === 'httponly') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6 // If the attribute-name case-insensitively matches the string // "HttpOnly", the user agent MUST append an attribute to the cookie- // attribute-list with an attribute-name of HttpOnly and an empty // attribute-value. - cookieAttributeList.httpOnly = true - } else if (attributeNameLowercase === 'samesite') { + cookieAttributeList.httpOnly = true + } else if (attributeNameLowercase === 'samesite') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7 // If the attribute-name case-insensitively matches the string // "SameSite", the user agent MUST process the cookie-av as follows: - const attributeValueLowercase = attributeValue.toLowerCase() + const attributeValueLowercase = attributeValue.toLowerCase() - // 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') { + // 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') { + 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' - } - } else { - cookieAttributeList.unparsed ??= [] + cookieAttributeList.sameSite = 'Lax' + } + } else { + cookieAttributeList.unparsed ??= [] - cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`) - } + cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`) + } // 8. Return to Step 1 of this algorithm. - return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } } module.exports = { @@ -54513,8 +54643,27 @@ function stringify (cookie) { out.push(`Path=${cookie.path}`) } - if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') { - out.push(`Expires=${toIMFDate(cookie.expires)}`) + // A numeric 0 is the Unix epoch, not an absent value -- the same reason the + // Max-Age check above tests the type rather than truthiness. + // + // expires is Date | number, so the "Invalid Date" check only guards the Date + // form. A number that is not a valid time value (NaN, +/-Infinity, or outside + // the Date range) would otherwise reach toIMFDate, which splices undefined and + // NaN into the header value instead of failing. + if (cookie.expires != null) { + const expiresMs = cookie.expires instanceof Date + ? cookie.expires.getTime() + : cookie.expires + + // The largest time value a Date can represent, in milliseconds, i.e. + // 100,000,000 days either side of the epoch (ECMA-262 Date Time Values). + // Anything within it is safe for toIMFDate to format; anything beyond it + // produces an invalid Date and must be ignored instead. + const maxTimeValue = 8640000000000000 + + if (typeof expiresMs === 'number' && Number.isFinite(expiresMs) && Math.abs(expiresMs) <= maxTimeValue) { + out.push(`Expires=${toIMFDate(cookie.expires)}`) + } } if (cookie.sameSite) { @@ -54557,6 +54706,7 @@ module.exports = { "use strict"; +const buffer = __nccwpck_require__(4573) const { Transform } = __nccwpck_require__(57075) const { isASCIINumber, isValidLastEventId } = __nccwpck_require__(94811) @@ -54581,11 +54731,19 @@ const COLON = 0x3A */ const SPACE = 0x20 +const defaultMaxEventSize = buffer.kStringMaxLength + const DATA = Buffer.from('data') const EVENT = Buffer.from('event') const ID = Buffer.from('id') const RETRY = Buffer.from('retry') +// Each data field creates a string node, and each unconsumed input chunk keeps +// a Buffer wrapper. The absolute ceiling protects the large default byte limit. +const minParserItems = 1024 +const maxParserItems = 131072 +const bytesPerParserItem = 128 + function isASCIINumberBytes (buffer, start) { if (start >= buffer.length) { return false @@ -54624,6 +54782,12 @@ function isFieldName (line, length, field) { return true } +function createMaxEventSizeExceededError () { + const error = new Error('EventSource message size exceeded') + error.aborted = false + return error +} + /** * @typedef {object} EventSourceStreamEvent * @type {object} @@ -54672,6 +54836,13 @@ class EventSourceStream extends Transform { pos = 0 lineChunkIndex = 0 linePos = 0 + lineLength = 0 + eventSize = 0 + eventFieldSizes = { data: 0, event: 0, id: 0, retry: 0 } + dataLineCount = 0 + maxEventSize + maxDataLines + maxBufferedChunks event = { data: undefined, @@ -54683,6 +54854,7 @@ class EventSourceStream extends Transform { /** * @param {object} options * @param {boolean} [options.readableObjectMode] + * @param {number} [options.maxEventSize] * @param {eventSourceSettings} [options.eventSourceSettings] * @param {(chunk: any, encoding?: BufferEncoding | undefined) => boolean} [options.push] */ @@ -54694,6 +54866,12 @@ class EventSourceStream extends Transform { super(options) this.state = options.eventSourceSettings || {} + this.maxEventSize = options.maxEventSize ?? defaultMaxEventSize + const parserItemLimit = this.maxEventSize > 0 + ? Math.min(maxParserItems, Math.max(minParserItems, Math.floor(this.maxEventSize / bytesPerParserItem))) + : 0 + this.maxDataLines = parserItemLimit + this.maxBufferedChunks = parserItemLimit if (options.push) { this.push = options.push } @@ -54711,6 +54889,14 @@ class EventSourceStream extends Transform { return } + // A leading line ending can immediately release the currently buffered + // line, even when it uses the final allowed chunk slot. + if (this.maxEventSize > 0 && + this.chunks.length >= this.maxBufferedChunks && + chunk[0] !== LF && chunk[0] !== CR) { + callback(createMaxEventSizeExceededError()) + return + } this.chunks.push(chunk) // Strip leading byte-order-mark if we opened the stream and started @@ -54789,7 +54975,12 @@ class EventSourceStream extends Transform { // In any case, we can process the line as we reached an // end-of-line character - this.parseLine(this.readLine(), this.event) + try { + this.parseLine(this.readLine(), this.event) + } catch (error) { + callback(error) + return + } this.consumeCurrentByte() // A line was processed and this could be the end of the event. We need // to check if the next line is empty to determine if the event is @@ -54798,12 +54989,25 @@ class EventSourceStream extends Transform { continue } + // readLine() is only called after CR or LF. Reject an unfinished line + // before it can retain an unbounded number of decoded chunks. The seven + // extra bytes allow the longest field prefix ("event: " or "retry: "). + if (this.maxEventSize > 0 && ++this.lineLength > this.maxEventSize + 7) { + callback(createMaxEventSizeExceededError()) + return + } this.advanceCursor() } callback() } + _destroy (error, callback) { + this.chunks.length = 0 + this.clearEvent() + super._destroy(error, callback) + } + /** * @param {Buffer} line * @param {EventSourceStreamEvent} event @@ -54840,6 +55044,13 @@ class EventSourceStream extends Transform { } if (isFieldName(line, fieldLength, DATA)) { + if (this.maxEventSize > 0 && this.dataLineCount >= this.maxDataLines) { + throw createMaxEventSizeExceededError() + } + const valueBytes = line.length - valueStart + const dataSize = this.eventFieldSizes.data + (event.data === undefined ? 0 : 1) + valueBytes + this.setEventFieldSize('data', dataSize) + const value = line.toString('utf8', valueStart) if (event.data === undefined) { @@ -54847,11 +55058,13 @@ class EventSourceStream extends Transform { } else { event.data += `\n${value}` } + this.dataLineCount++ return } if (isFieldName(line, fieldLength, RETRY)) { if (isASCIINumberBytes(line, valueStart)) { + this.setEventFieldSize('retry', line.length - valueStart) event.retry = line.toString('utf8', valueStart) } return @@ -54859,20 +55072,29 @@ class EventSourceStream extends Transform { if (isFieldName(line, fieldLength, ID)) { if (isValidLastEventIdBytes(line, valueStart)) { + this.setEventFieldSize('id', line.length - valueStart) event.id = line.toString('utf8', valueStart) } return } if (isFieldName(line, fieldLength, EVENT)) { - const value = line.toString('utf8', valueStart) - - if (value.length > 0) { - event.event = value + if (valueStart < line.length) { + this.setEventFieldSize('event', line.length - valueStart) + event.event = line.toString('utf8', valueStart) } } } + setEventFieldSize (field, size) { + const eventSize = this.eventSize - this.eventFieldSizes[field] + size + if (this.maxEventSize > 0 && eventSize > this.maxEventSize) { + throw createMaxEventSizeExceededError() + } + this.eventSize = eventSize + this.eventFieldSizes[field] = size + } + /** * @param {EventSourceStreamEvent} event */ @@ -54903,6 +55125,12 @@ class EventSourceStream extends Transform { this.event.event = undefined this.event.id = undefined this.event.retry = undefined + this.eventSize = 0 + this.dataLineCount = 0 + this.eventFieldSizes.data = 0 + this.eventFieldSizes.event = 0 + this.eventFieldSizes.id = 0 + this.eventFieldSizes.retry = 0 } hasPendingEvent () { @@ -54938,14 +55166,16 @@ class EventSourceStream extends Transform { syncLineStartToCursor () { this.lineChunkIndex = this.chunkIndex this.linePos = this.pos + this.lineLength = 0 this.dropConsumedChunks() } dropConsumedChunks () { - while (this.lineChunkIndex > 0) { - this.chunks.shift() - this.lineChunkIndex-- - this.chunkIndex-- + if (this.lineChunkIndex > 0) { + const consumed = this.lineChunkIndex + this.chunks.splice(0, consumed) + this.lineChunkIndex = 0 + this.chunkIndex -= consumed } if (this.chunkIndex === this.chunks.length) { @@ -55070,6 +55300,7 @@ const { createFastMessageEvent } = __nccwpck_require__(15188) const { isNetworkError } = __nccwpck_require__(99051) const { kEnumerableProperty } = __nccwpck_require__(3440) const { environmentSettingsObject } = __nccwpck_require__(73168) +const { getGlobalDispatcher } = __nccwpck_require__(32581) let experimentalWarned = false @@ -55352,6 +55583,7 @@ class EventSource extends EventTarget { const eventSourceStream = new EventSourceStream({ eventSourceSettings: this.#state, + maxEventSize: this.#dispatcher.eventSourceOptions?.maxEventSize, push: (event) => { this.dispatchEvent(createFastMessageEvent( event.type, @@ -55536,7 +55768,8 @@ webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([ }, { key: 'dispatcher', // undici only - converter: webidl.converters.any + converter: webidl.converters.any, + defaultValue: () => getGlobalDispatcher() }, { key: 'node', // undici only @@ -60717,8 +60950,6 @@ async function httpNetworkFetch ( } const location = headersList.get('location', true) - this.body = new Readable({ read: resume }) - const willFollow = location && request.redirect === 'follow' && redirectStatusSet.has(status) @@ -60736,7 +60967,7 @@ async function httpNetworkFetch ( // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206). const maxContentEncodings = 5 if (codings.length > maxContentEncodings) { - reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`)) + this.abort(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`)) return true } @@ -60774,6 +61005,8 @@ async function httpNetworkFetch ( } } + this.body = new Readable({ read: resume }) + const onError = this.onError.bind(this) resolve({ @@ -64277,6 +64510,19 @@ function collectASequenceOfCodePointsFast (char, input, position) { } const ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g // eslint-disable-line no-control-regex +const nonASCIIRegex = /[^\x00-\x7F]/ // eslint-disable-line no-control-regex + +/** + * @param {string} str + * @returns {string} + * + * @see https://infra.spec.whatwg.org/#ascii-lowercase + */ +function asciiLowercase (str) { + return nonASCIIRegex.test(str) + ? str.replace(/[A-Z]+/g, (upper) => upper.toLowerCase()) + : str.toLowerCase() +} /** * @param {string} data @@ -64441,6 +64687,7 @@ function serializeJavascriptValueToJSONString (value) { } module.exports = { + asciiLowercase, collectASequenceOfCodePoints, collectASequenceOfCodePointsFast, forgivingBase64, @@ -64464,6 +64711,7 @@ module.exports = { const assert = __nccwpck_require__(34589) const { runtimeFeatures } = __nccwpck_require__(313) +const { asciiLowercase } = __nccwpck_require__(8116) /** * @typedef {object} Metadata @@ -64528,14 +64776,12 @@ const getSRIHashAlgorithmIndex = /** @type {GetSRIHashAlgorithmIndex} */ (Map.pr /** * @typedef IsValidSRIHashAlgorithm + * @see https://w3c.github.io/webappsec-subresource-integrity/#valid-sri-hash-algorithm-token * @type {(algorithm: string) => algorithm is SRIHashAlgorithm} * @param {*} algorithm * @returns {algorithm is SRIHashAlgorithm} */ - -const isValidSRIHashAlgorithm = /** @type {IsValidSRIHashAlgorithm} */ ( - Map.prototype.has.bind(validSRIHashAlgorithmTokenSet) -) +const isValidSRIHashAlgorithm = (algorithm) => validSRIHashAlgorithmTokenSet.has(asciiLowercase(algorithm)) /** * @param {Uint8Array} bytes @@ -64691,7 +64937,8 @@ function parseMetadata (metadata) { // 8. Let metadata be the ordered map // «["alg" → algorithm, "val" → base64-value]». const metadata = { - alg: algorithm, + // Possible spec bug? + alg: asciiLowercase(algorithm), val: base64Value } @@ -67109,6 +67356,19 @@ class ByteParser extends Writable { if (isControlFrame(this.#info.opcode)) { this.#loop = this.parseControlFrame(body) this.#state = parserStates.INFO + + // If the automatic PONG/CLOSE reply filled the socket's write buffer, + // stop parsing and resume on drain so a PING flood cannot grow the + // outbound queue without bound. + if (this.#loop && this.#handler.socket.writableNeedDrain) { + this.#loop = false + this.#handler.socket.once('drain', () => { + if (this.destroyed) return + this.#loop = true + this.run(callback) + }) + return + } } else { if (!this.#info.compressed) { if (!this.writeFragments(body)) { diff --git a/dist/run/index.js b/dist/run/index.js index 2741e10..58c2418 100644 --- a/dist/run/index.js +++ b/dist/run/index.js @@ -33621,16 +33621,22 @@ const SessionCache = class WeakSessionCache { if (this._sessionCache.has(sessionKey)) { this._sessionCache.delete(sessionKey) } else if (this._sessionCache.size >= this._maxCachedSessions) { + // Make room for the new session: prefer an entry whose session has + // already been collected, otherwise evict the oldest one. + let evicted = false for (const [key, ref] of this._sessionCache) { if (ref.deref() === undefined) { this._sessionCache.delete(key) - return + evicted = true + break } } - const oldest = this._sessionCache.keys().next() - if (!oldest.done) { - this._sessionCache.delete(oldest.value) + if (!evicted) { + const oldest = this._sessionCache.keys().next() + if (!oldest.done) { + this._sessionCache.delete(oldest.value) + } } } @@ -35760,6 +35766,7 @@ module.exports = { kDestroy: Symbol('destroy'), kDispatch: Symbol('dispatch'), kUrl: Symbol('url'), + kRedirectCacheControl: Symbol('redirect cache control'), kWriting: Symbol('writing'), kResuming: Symbol('resuming'), kQueue: Symbol('queue'), @@ -37028,7 +37035,7 @@ function defaultFactory (origin, opts) { } class Agent extends DispatcherBase { - constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) { + constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, tls, ...options } = {}) { if (typeof factory !== 'function') { throw new InvalidArgumentError('factory must be a function.') } @@ -37047,7 +37054,11 @@ class Agent extends DispatcherBase { connect = { ...connect } } - this[kOptions] = { ...util.deepClone(options), maxOrigins, connect } + if (tls && typeof tls !== 'function') { + tls = { ...tls } + } + + this[kOptions] = { ...util.deepClone(options), maxOrigins, connect, tls } this[kFactory] = factory this[kClients] = new Map() this[kOrigins] = new Set() @@ -38329,8 +38340,6 @@ function connectH1 (client, socket) { } function onHttpSocketError (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - const parser = this[kParser] // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded @@ -39448,8 +39457,6 @@ function onHttp2SendPing (session) { } function onHttp2SessionError (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - this[kSocket][kError] = err this[kClient][kOnError](err) } @@ -39591,8 +39598,6 @@ function onHttp2SocketClose () { } function onHttp2SocketError (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - this[kError] = err this[kClient][kOnError](err) @@ -40338,7 +40343,8 @@ class Client extends DispatcherBase { initialWindowSize, connectionWindowSize, pingInterval, - webSocket + webSocket, + eventSource } = {}) { if (keepAlive !== undefined) { throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') @@ -40446,7 +40452,7 @@ class Client extends DispatcherBase { throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0') } - super({ webSocket }) + super({ webSocket, eventSource }) if (typeof connect !== 'function') { connect = buildConnector({ @@ -40899,6 +40905,7 @@ module.exports = Client "use strict"; +const buffer = __nccwpck_require__(4573) const Dispatcher = __nccwpck_require__(30883) const UnwrapHandler = __nccwpck_require__(92365) const { @@ -40911,6 +40918,7 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = __nccwpck_require__ const kOnDestroyed = Symbol('onDestroyed') const kOnClosed = Symbol('onClosed') const kWebSocketOptions = Symbol('webSocketOptions') +const kEventSourceOptions = Symbol('eventSourceOptions') class DispatcherBase extends Dispatcher { /** @type {boolean} */ @@ -40931,6 +40939,7 @@ class DispatcherBase extends Dispatcher { constructor (opts) { super() this[kWebSocketOptions] = opts?.webSocket ?? {} + this[kEventSourceOptions] = opts?.eventSource ?? {} } /** @@ -40943,6 +40952,15 @@ class DispatcherBase extends Dispatcher { } } + /** + * @returns {import('../../types/client').Client.EventSourceOptions} + */ + get eventSourceOptions () { + return { + maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength + } + } + /** @returns {boolean} */ get destroyed () { return this[kDestroyed] @@ -42743,6 +42761,7 @@ module.exports = { const util = __nccwpck_require__(3440) +const { kRedirectCacheControl } = __nccwpck_require__(36443) const { parseCacheControlHeader, hasInvalidCacheControlDirective, @@ -42755,6 +42774,19 @@ const { parseHttpDate } = __nccwpck_require__(25453) function noop () {} +/** + * @param {string | URL} a + * @param {string | URL} b + * @returns {boolean} + */ +function sameOrigin (a, b) { + try { + return new URL(String(a)).origin === new URL(String(b)).origin + } catch { + return String(a) === String(b) + } +} + // Status codes that we can use some heuristics on to cache const HEURISTICALLY_CACHEABLE_STATUS_CODES = [ 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501 @@ -42893,6 +42925,14 @@ class CacheHandler { */ #writeStream + /** + * The URLs visited while following redirects, shared by reference from the + * redirect handler through the request context. Carries the suppressed hops' + * cache-control under {@link kRedirectCacheControl}. + * @type {URL[] | undefined} + */ + #history + /** * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey @@ -42909,6 +42949,9 @@ class CacheHandler { onRequestStart (controller, context) { this.#writeStream?.destroy() this.#writeStream = undefined + // The redirect handler mutates this shared history as redirects are + // followed, allowing the cache to identify the response's actual origin. + this.#history = context?.history this.#handler.onRequestStart?.(controller, context) } @@ -42948,6 +42991,29 @@ class CacheHandler { return downstreamOnHeaders() } + // Never store a response produced by a different origin under the original + // request's cache key. + if (this.#history && this.#history.length > 0) { + const finalUrl = this.#history[this.#history.length - 1] + if (!sameOrigin(finalUrl.origin, this.#cacheKey.origin)) { + return downstreamOnHeaders() + } + } + + // Preserve restrictive cache policy from suppressed redirect responses. + const redirectCacheControl = this.#history?.[kRedirectCacheControl] + if (redirectCacheControl) { + for (const hopCacheControl of redirectCacheControl) { + const hopDirectives = hopCacheControl ? parseCacheControlHeader(hopCacheControl) : {} + if ( + hopDirectives['no-store'] === true || + (this.#cacheType === 'shared' && hopDirectives.private === true) + ) { + return downstreamOnHeaders() + } + } + } + const cacheControlHeader = resHeaders['cache-control'] const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {} @@ -43749,6 +43815,7 @@ const DEFAULT_MAX_BUFFER_SIZE = 5 * 1024 * 1024 * @property {number} bufferedBytes * @property {object | null} pendingTrailers * @property {boolean} done + * @property {boolean} [pendingStart] onRequestStart deferred until the primary request's context is known */ /** @@ -43763,6 +43830,20 @@ class DeduplicationHandler { */ #primaryHandler + /** + * Whether the primary request has started (onRequestStart received). Until + * then the request context is unknown and cannot be given to waiting handlers. + * @type {boolean} + */ + #requestStarted = false + + /** + * The primary request context, replayed to waiting handlers so they observe + * the same redirect history as the primary. + * @type {any} + */ + #context = null + /** * @type {WaitingHandler[]} */ @@ -43845,20 +43926,30 @@ class DeduplicationHandler { const waitingController = waitingHandler.controller try { - handler.onRequestStart?.(waitingController, null) + if (this.#requestStarted) { + // The primary request has started, so its context (including the + // redirect history) is known and can be delivered immediately. + handler.onRequestStart?.(waitingController, this.#context) - if (waitingController.aborted) { - waitingHandler.done = true - return true - } + if (waitingController.aborted) { + waitingHandler.done = true + return true + } - if (this.#responseStarted) { - handler.onResponseStart?.( - waitingController, - this.#statusCode, - this.#headers, - this.#statusMessage - ) + if (this.#responseStarted) { + handler.onResponseStart?.( + waitingController, + this.#statusCode, + this.#headers, + this.#statusMessage + ) + } + } else { + // The primary request has not started yet. Defer onRequestStart until + // its context is known, so the waiting handler observes the same + // redirect history as the primary (a cache handler relies on it to + // reject cross-origin redirect responses). + waitingHandler.pendingStart = true } } catch { // Ignore errors from waiting handlers @@ -43879,6 +43970,24 @@ class DeduplicationHandler { */ onRequestStart (controller, context) { this.#controller = controller + if (!this.#requestStarted) { + this.#requestStarted = true + this.#context = context + // Deliver the now-known context to handlers that joined before the + // primary request started. + for (const waitingHandler of this.#waitingHandlers) { + if (waitingHandler.pendingStart && !waitingHandler.done && !waitingHandler.controller.aborted) { + waitingHandler.pendingStart = false + try { + waitingHandler.handler.onRequestStart?.(waitingHandler.controller, context) + } catch { + // Ignore errors from waiting handlers + waitingHandler.done = true + } + } + } + this.#pruneDoneWaitingHandlers() + } this.#primaryHandler.onRequestStart?.(controller, context) } @@ -44052,7 +44161,8 @@ class DeduplicationHandler { bufferedChunks: [], bufferedBytes: 0, pendingTrailers: null, - done: false + done: false, + pendingStart: false } const state = { @@ -44202,7 +44312,7 @@ module.exports = DeduplicationHandler const util = __nccwpck_require__(3440) -const { kBodyUsed } = __nccwpck_require__(36443) +const { kBodyUsed, kRedirectCacheControl } = __nccwpck_require__(36443) const assert = __nccwpck_require__(34589) const { InvalidArgumentError } = __nccwpck_require__(68707) const EE = __nccwpck_require__(78474) @@ -44248,6 +44358,11 @@ class RedirectHandler { this.maxRedirections = maxRedirections this.handler = handler this.history = [] + // Retain cache-control from suppressed redirect hops for an outer cache. + Object.defineProperty(this.history, kRedirectCacheControl, { + value: [], + enumerable: false + }) if (util.isStream(this.opts.body)) { // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp @@ -44331,6 +44446,8 @@ class RedirectHandler { return } + this.history[kRedirectCacheControl].push(headers['cache-control']) + const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))) const path = search ? `${pathname}${search}` : pathname @@ -46035,9 +46152,14 @@ class DecompressHandler extends DecoratorHandler { this.#decompressedSize = decompressedSize } - const result = super.onResponseData(this.#controller, chunk) - if (result === false && !this.#controller.paused) { - this.#controller.pause() + try { + const result = super.onResponseData(this.#controller, chunk) + if (result === false && !this.#controller.paused) { + this.#controller.pause() + } + } catch (err) { + this.#fail(err) + return } } } while (this.#drainRequested && !this.#terminated && !this.#controller.paused) @@ -46054,9 +46176,14 @@ class DecompressHandler extends DecoratorHandler { return } - this.#terminated = true this.#cleanupDecompressors() - super.onResponseEnd(this.#controller, this.#trailers) + try { + super.onResponseEnd(this.#controller, this.#trailers) + } catch (err) { + this.#fail(err) + return + } + this.#terminated = true } #onDecompressionEnd () { @@ -53701,8 +53828,10 @@ function getCookies (headers) { const cookie = headers.get('cookie') + // A null prototype keeps a cookie named `__proto__` from hitting the + // Object.prototype setter, which would silently drop it. /** @type {Record} */ - const out = {} + const out = { __proto__: null } if (!cookie) { return out @@ -53968,220 +54097,221 @@ function parseSetCookie (header) { * @param {Object.} [cookieAttributeList={}] */ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) { - // 1. If the unparsed-attributes string is empty, skip the rest of - // these steps. - if (unparsedAttributes.length === 0) { - return cookieAttributeList - } + while (true) { + // 1. If the unparsed-attributes string is empty, skip the rest of + // these steps. + if (unparsedAttributes.length === 0) { + return cookieAttributeList + } - // 2. Discard the first character of the unparsed-attributes (which - // will be a %x3B (";") character). - assert(unparsedAttributes[0] === ';') - unparsedAttributes = unparsedAttributes.slice(1) + // 2. Discard the first character of the unparsed-attributes (which + // will be a %x3B (";") character). + assert(unparsedAttributes[0] === ';') + unparsedAttributes = unparsedAttributes.slice(1) - let cookieAv = '' + let cookieAv = '' - // 3. If the remaining unparsed-attributes contains a %x3B (";") - // character: - if (unparsedAttributes.includes(';')) { + // 3. If the remaining unparsed-attributes contains a %x3B (";") + // character: + if (unparsedAttributes.includes(';')) { // 1. Consume the characters of the unparsed-attributes up to, but // not including, the first %x3B (";") character. - cookieAv = collectASequenceOfCodePointsFast( - ';', - unparsedAttributes, - { position: 0 } - ) - unparsedAttributes = unparsedAttributes.slice(cookieAv.length) - } else { + cookieAv = collectASequenceOfCodePointsFast( + ';', + unparsedAttributes, + { position: 0 } + ) + unparsedAttributes = unparsedAttributes.slice(cookieAv.length) + } else { // Otherwise: - // 1. Consume the remainder of the unparsed-attributes. - cookieAv = unparsedAttributes - unparsedAttributes = '' - } + // 1. Consume the remainder of the unparsed-attributes. + cookieAv = unparsedAttributes + unparsedAttributes = '' + } - // Let the cookie-av string be the characters consumed in this step. + // Let the cookie-av string be the characters consumed in this step. - let attributeName = '' - let attributeValue = '' + let attributeName = '' + let attributeValue = '' - // 4. If the cookie-av string contains a %x3D ("=") character: - if (cookieAv.includes('=')) { + // 4. If the cookie-av string contains a %x3D ("=") character: + if (cookieAv.includes('=')) { // 1. The (possibly empty) attribute-name string consists of the // characters up to, but not including, the first %x3D ("=") // character, and the (possibly empty) attribute-value string // consists of the characters after the first %x3D ("=") // character. - const position = { position: 0 } + const position = { position: 0 } - attributeName = collectASequenceOfCodePointsFast( - '=', - cookieAv, - position - ) - attributeValue = cookieAv.slice(position.position + 1) - } else { + attributeName = collectASequenceOfCodePointsFast( + '=', + cookieAv, + position + ) + attributeValue = cookieAv.slice(position.position + 1) + } else { // Otherwise: - // 1. The attribute-name string consists of the entire cookie-av - // string, and the attribute-value string is empty. - attributeName = cookieAv - } + // 1. The attribute-name string consists of the entire cookie-av + // string, and the attribute-value string is empty. + attributeName = cookieAv + } - // 5. Remove any leading or trailing WSP characters from the attribute- - // name string and the attribute-value string. - attributeName = attributeName.trim() - attributeValue = attributeValue.trim() + // 5. Remove any leading or trailing WSP characters from the attribute- + // name string and the attribute-value string. + attributeName = attributeName.trim() + attributeValue = attributeValue.trim() - // 6. If the attribute-value is longer than 1024 octets, ignore the - // cookie-av string and return to Step 1 of this algorithm. - if (attributeValue.length > maxAttributeValueSize) { - return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) - } + // 6. If the attribute-value is longer than 1024 octets, ignore the + // cookie-av string and return to Step 1 of this algorithm. + if (attributeValue.length > maxAttributeValueSize) { + continue + } - // 7. Process the attribute-name and attribute-value according to the - // requirements in the following subsections. (Notice that - // attributes with unrecognized attribute-names are ignored.) - const attributeNameLowercase = attributeName.toLowerCase() + // 7. Process the attribute-name and attribute-value according to the + // requirements in the following subsections. (Notice that + // attributes with unrecognized attribute-names are ignored.) + const attributeNameLowercase = attributeName.toLowerCase() - // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1 - // If the attribute-name case-insensitively matches the string - // "Expires", the user agent MUST process the cookie-av as follows. - if (attributeNameLowercase === 'expires') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1 + // If the attribute-name case-insensitively matches the string + // "Expires", the user agent MUST process the cookie-av as follows. + if (attributeNameLowercase === 'expires') { // 1. Let the expiry-time be the result of parsing the attribute-value // as cookie-date (see Section 5.1.1). - const expiryTime = new Date(attributeValue) + const expiryTime = new Date(attributeValue) - // 2. If the attribute-value failed to parse as a cookie date, ignore - // the cookie-av. + // 2. If the attribute-value failed to parse as a cookie date, ignore + // the cookie-av. - cookieAttributeList.expires = expiryTime - } else if (attributeNameLowercase === 'max-age') { + cookieAttributeList.expires = expiryTime + } else if (attributeNameLowercase === 'max-age') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2 // If the attribute-name case-insensitively matches the string "Max- // Age", the user agent MUST process the cookie-av as follows. - // 1. If the first character of the attribute-value is not a DIGIT or a - // "-" character, ignore the cookie-av. - const charCode = attributeValue.charCodeAt(0) + // 1. If the first character of the attribute-value is not a DIGIT or a + // "-" character, ignore the cookie-av. + const charCode = attributeValue.charCodeAt(0) - if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') { - return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) - } + if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') { + continue + } - // 2. If the remainder of attribute-value contains a non-DIGIT - // character, ignore the cookie-av. - if (!/^\d+$/.test(attributeValue)) { - return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) - } + // 2. If the remainder of attribute-value contains a non-DIGIT + // character, ignore the cookie-av. + if (!/^\d+$/.test(attributeValue)) { + continue + } - // 3. Let delta-seconds be the attribute-value converted to an integer. - const deltaSeconds = Number(attributeValue) + // 3. Let delta-seconds be the attribute-value converted to an integer. + const deltaSeconds = Number(attributeValue) - // 4. Let cookie-age-limit be the maximum age of the cookie (which - // SHOULD be 400 days or less, see Section 4.1.2.2). + // 4. Let cookie-age-limit be the maximum age of the cookie (which + // SHOULD be 400 days or less, see Section 4.1.2.2). - // 5. Set delta-seconds to the smaller of its present value and cookie- - // age-limit. - // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs) + // 5. Set delta-seconds to the smaller of its present value and cookie- + // age-limit. + // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs) - // 6. If delta-seconds is less than or equal to zero (0), let expiry- - // time be the earliest representable date and time. Otherwise, let - // the expiry-time be the current date and time plus delta-seconds - // seconds. - // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds + // 6. If delta-seconds is less than or equal to zero (0), let expiry- + // time be the earliest representable date and time. Otherwise, let + // the expiry-time be the current date and time plus delta-seconds + // seconds. + // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds - // 7. Append an attribute to the cookie-attribute-list with an - // attribute-name of Max-Age and an attribute-value of expiry-time. - cookieAttributeList.maxAge = deltaSeconds - } else if (attributeNameLowercase === 'domain') { + // 7. Append an attribute to the cookie-attribute-list with an + // attribute-name of Max-Age and an attribute-value of expiry-time. + cookieAttributeList.maxAge = deltaSeconds + } else if (attributeNameLowercase === 'domain') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3 // If the attribute-name case-insensitively matches the string "Domain", // the user agent MUST process the cookie-av as follows. - // 1. Let cookie-domain be the attribute-value. - let cookieDomain = attributeValue + // 1. Let cookie-domain be the attribute-value. + let cookieDomain = attributeValue - // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be - // cookie-domain without its leading %x2E ("."). - if (cookieDomain[0] === '.') { - cookieDomain = cookieDomain.slice(1) - } + // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be + // cookie-domain without its leading %x2E ("."). + if (cookieDomain[0] === '.') { + cookieDomain = cookieDomain.slice(1) + } - // 3. Convert the cookie-domain to lower case. - cookieDomain = cookieDomain.toLowerCase() + // 3. Convert the cookie-domain to lower case. + cookieDomain = cookieDomain.toLowerCase() - // 4. Append an attribute to the cookie-attribute-list with an - // attribute-name of Domain and an attribute-value of cookie-domain. - cookieAttributeList.domain = cookieDomain - } else if (attributeNameLowercase === 'path') { + // 4. Append an attribute to the cookie-attribute-list with an + // attribute-name of Domain and an attribute-value of cookie-domain. + cookieAttributeList.domain = cookieDomain + } else if (attributeNameLowercase === 'path') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4 // If the attribute-name case-insensitively matches the string "Path", // the user agent MUST process the cookie-av as follows. - // 1. If the attribute-value is empty or if the first character of the - // attribute-value is not %x2F ("/"): - let cookiePath = '' - if (attributeValue.length === 0 || attributeValue[0] !== '/') { + // 1. If the attribute-value is empty or if the first character of the + // attribute-value is not %x2F ("/"): + let cookiePath = '' + if (attributeValue.length === 0 || attributeValue[0] !== '/') { // 1. Let cookie-path be the default-path. - cookiePath = '/' - } else { + cookiePath = '/' + } else { // Otherwise: - // 1. Let cookie-path be the attribute-value. - cookiePath = attributeValue - } + // 1. Let cookie-path be the attribute-value. + cookiePath = attributeValue + } - // 2. Append an attribute to the cookie-attribute-list with an - // attribute-name of Path and an attribute-value of cookie-path. - cookieAttributeList.path = cookiePath - } else if (attributeNameLowercase === 'secure') { + // 2. Append an attribute to the cookie-attribute-list with an + // attribute-name of Path and an attribute-value of cookie-path. + cookieAttributeList.path = cookiePath + } else if (attributeNameLowercase === 'secure') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5 // If the attribute-name case-insensitively matches the string "Secure", // the user agent MUST append an attribute to the cookie-attribute-list // with an attribute-name of Secure and an empty attribute-value. - cookieAttributeList.secure = true - } else if (attributeNameLowercase === 'httponly') { + cookieAttributeList.secure = true + } else if (attributeNameLowercase === 'httponly') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6 // If the attribute-name case-insensitively matches the string // "HttpOnly", the user agent MUST append an attribute to the cookie- // attribute-list with an attribute-name of HttpOnly and an empty // attribute-value. - cookieAttributeList.httpOnly = true - } else if (attributeNameLowercase === 'samesite') { + cookieAttributeList.httpOnly = true + } else if (attributeNameLowercase === 'samesite') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7 // If the attribute-name case-insensitively matches the string // "SameSite", the user agent MUST process the cookie-av as follows: - const attributeValueLowercase = attributeValue.toLowerCase() + const attributeValueLowercase = attributeValue.toLowerCase() - // 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') { + // 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') { + 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' - } - } else { - cookieAttributeList.unparsed ??= [] + cookieAttributeList.sameSite = 'Lax' + } + } else { + cookieAttributeList.unparsed ??= [] - cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`) - } + cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`) + } // 8. Return to Step 1 of this algorithm. - return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } } module.exports = { @@ -54513,8 +54643,27 @@ function stringify (cookie) { out.push(`Path=${cookie.path}`) } - if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') { - out.push(`Expires=${toIMFDate(cookie.expires)}`) + // A numeric 0 is the Unix epoch, not an absent value -- the same reason the + // Max-Age check above tests the type rather than truthiness. + // + // expires is Date | number, so the "Invalid Date" check only guards the Date + // form. A number that is not a valid time value (NaN, +/-Infinity, or outside + // the Date range) would otherwise reach toIMFDate, which splices undefined and + // NaN into the header value instead of failing. + if (cookie.expires != null) { + const expiresMs = cookie.expires instanceof Date + ? cookie.expires.getTime() + : cookie.expires + + // The largest time value a Date can represent, in milliseconds, i.e. + // 100,000,000 days either side of the epoch (ECMA-262 Date Time Values). + // Anything within it is safe for toIMFDate to format; anything beyond it + // produces an invalid Date and must be ignored instead. + const maxTimeValue = 8640000000000000 + + if (typeof expiresMs === 'number' && Number.isFinite(expiresMs) && Math.abs(expiresMs) <= maxTimeValue) { + out.push(`Expires=${toIMFDate(cookie.expires)}`) + } } if (cookie.sameSite) { @@ -54557,6 +54706,7 @@ module.exports = { "use strict"; +const buffer = __nccwpck_require__(4573) const { Transform } = __nccwpck_require__(57075) const { isASCIINumber, isValidLastEventId } = __nccwpck_require__(94811) @@ -54581,11 +54731,19 @@ const COLON = 0x3A */ const SPACE = 0x20 +const defaultMaxEventSize = buffer.kStringMaxLength + const DATA = Buffer.from('data') const EVENT = Buffer.from('event') const ID = Buffer.from('id') const RETRY = Buffer.from('retry') +// Each data field creates a string node, and each unconsumed input chunk keeps +// a Buffer wrapper. The absolute ceiling protects the large default byte limit. +const minParserItems = 1024 +const maxParserItems = 131072 +const bytesPerParserItem = 128 + function isASCIINumberBytes (buffer, start) { if (start >= buffer.length) { return false @@ -54624,6 +54782,12 @@ function isFieldName (line, length, field) { return true } +function createMaxEventSizeExceededError () { + const error = new Error('EventSource message size exceeded') + error.aborted = false + return error +} + /** * @typedef {object} EventSourceStreamEvent * @type {object} @@ -54672,6 +54836,13 @@ class EventSourceStream extends Transform { pos = 0 lineChunkIndex = 0 linePos = 0 + lineLength = 0 + eventSize = 0 + eventFieldSizes = { data: 0, event: 0, id: 0, retry: 0 } + dataLineCount = 0 + maxEventSize + maxDataLines + maxBufferedChunks event = { data: undefined, @@ -54683,6 +54854,7 @@ class EventSourceStream extends Transform { /** * @param {object} options * @param {boolean} [options.readableObjectMode] + * @param {number} [options.maxEventSize] * @param {eventSourceSettings} [options.eventSourceSettings] * @param {(chunk: any, encoding?: BufferEncoding | undefined) => boolean} [options.push] */ @@ -54694,6 +54866,12 @@ class EventSourceStream extends Transform { super(options) this.state = options.eventSourceSettings || {} + this.maxEventSize = options.maxEventSize ?? defaultMaxEventSize + const parserItemLimit = this.maxEventSize > 0 + ? Math.min(maxParserItems, Math.max(minParserItems, Math.floor(this.maxEventSize / bytesPerParserItem))) + : 0 + this.maxDataLines = parserItemLimit + this.maxBufferedChunks = parserItemLimit if (options.push) { this.push = options.push } @@ -54711,6 +54889,14 @@ class EventSourceStream extends Transform { return } + // A leading line ending can immediately release the currently buffered + // line, even when it uses the final allowed chunk slot. + if (this.maxEventSize > 0 && + this.chunks.length >= this.maxBufferedChunks && + chunk[0] !== LF && chunk[0] !== CR) { + callback(createMaxEventSizeExceededError()) + return + } this.chunks.push(chunk) // Strip leading byte-order-mark if we opened the stream and started @@ -54789,7 +54975,12 @@ class EventSourceStream extends Transform { // In any case, we can process the line as we reached an // end-of-line character - this.parseLine(this.readLine(), this.event) + try { + this.parseLine(this.readLine(), this.event) + } catch (error) { + callback(error) + return + } this.consumeCurrentByte() // A line was processed and this could be the end of the event. We need // to check if the next line is empty to determine if the event is @@ -54798,12 +54989,25 @@ class EventSourceStream extends Transform { continue } + // readLine() is only called after CR or LF. Reject an unfinished line + // before it can retain an unbounded number of decoded chunks. The seven + // extra bytes allow the longest field prefix ("event: " or "retry: "). + if (this.maxEventSize > 0 && ++this.lineLength > this.maxEventSize + 7) { + callback(createMaxEventSizeExceededError()) + return + } this.advanceCursor() } callback() } + _destroy (error, callback) { + this.chunks.length = 0 + this.clearEvent() + super._destroy(error, callback) + } + /** * @param {Buffer} line * @param {EventSourceStreamEvent} event @@ -54840,6 +55044,13 @@ class EventSourceStream extends Transform { } if (isFieldName(line, fieldLength, DATA)) { + if (this.maxEventSize > 0 && this.dataLineCount >= this.maxDataLines) { + throw createMaxEventSizeExceededError() + } + const valueBytes = line.length - valueStart + const dataSize = this.eventFieldSizes.data + (event.data === undefined ? 0 : 1) + valueBytes + this.setEventFieldSize('data', dataSize) + const value = line.toString('utf8', valueStart) if (event.data === undefined) { @@ -54847,11 +55058,13 @@ class EventSourceStream extends Transform { } else { event.data += `\n${value}` } + this.dataLineCount++ return } if (isFieldName(line, fieldLength, RETRY)) { if (isASCIINumberBytes(line, valueStart)) { + this.setEventFieldSize('retry', line.length - valueStart) event.retry = line.toString('utf8', valueStart) } return @@ -54859,20 +55072,29 @@ class EventSourceStream extends Transform { if (isFieldName(line, fieldLength, ID)) { if (isValidLastEventIdBytes(line, valueStart)) { + this.setEventFieldSize('id', line.length - valueStart) event.id = line.toString('utf8', valueStart) } return } if (isFieldName(line, fieldLength, EVENT)) { - const value = line.toString('utf8', valueStart) - - if (value.length > 0) { - event.event = value + if (valueStart < line.length) { + this.setEventFieldSize('event', line.length - valueStart) + event.event = line.toString('utf8', valueStart) } } } + setEventFieldSize (field, size) { + const eventSize = this.eventSize - this.eventFieldSizes[field] + size + if (this.maxEventSize > 0 && eventSize > this.maxEventSize) { + throw createMaxEventSizeExceededError() + } + this.eventSize = eventSize + this.eventFieldSizes[field] = size + } + /** * @param {EventSourceStreamEvent} event */ @@ -54903,6 +55125,12 @@ class EventSourceStream extends Transform { this.event.event = undefined this.event.id = undefined this.event.retry = undefined + this.eventSize = 0 + this.dataLineCount = 0 + this.eventFieldSizes.data = 0 + this.eventFieldSizes.event = 0 + this.eventFieldSizes.id = 0 + this.eventFieldSizes.retry = 0 } hasPendingEvent () { @@ -54938,14 +55166,16 @@ class EventSourceStream extends Transform { syncLineStartToCursor () { this.lineChunkIndex = this.chunkIndex this.linePos = this.pos + this.lineLength = 0 this.dropConsumedChunks() } dropConsumedChunks () { - while (this.lineChunkIndex > 0) { - this.chunks.shift() - this.lineChunkIndex-- - this.chunkIndex-- + if (this.lineChunkIndex > 0) { + const consumed = this.lineChunkIndex + this.chunks.splice(0, consumed) + this.lineChunkIndex = 0 + this.chunkIndex -= consumed } if (this.chunkIndex === this.chunks.length) { @@ -55070,6 +55300,7 @@ const { createFastMessageEvent } = __nccwpck_require__(15188) const { isNetworkError } = __nccwpck_require__(99051) const { kEnumerableProperty } = __nccwpck_require__(3440) const { environmentSettingsObject } = __nccwpck_require__(73168) +const { getGlobalDispatcher } = __nccwpck_require__(32581) let experimentalWarned = false @@ -55352,6 +55583,7 @@ class EventSource extends EventTarget { const eventSourceStream = new EventSourceStream({ eventSourceSettings: this.#state, + maxEventSize: this.#dispatcher.eventSourceOptions?.maxEventSize, push: (event) => { this.dispatchEvent(createFastMessageEvent( event.type, @@ -55536,7 +55768,8 @@ webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([ }, { key: 'dispatcher', // undici only - converter: webidl.converters.any + converter: webidl.converters.any, + defaultValue: () => getGlobalDispatcher() }, { key: 'node', // undici only @@ -60717,8 +60950,6 @@ async function httpNetworkFetch ( } const location = headersList.get('location', true) - this.body = new Readable({ read: resume }) - const willFollow = location && request.redirect === 'follow' && redirectStatusSet.has(status) @@ -60736,7 +60967,7 @@ async function httpNetworkFetch ( // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206). const maxContentEncodings = 5 if (codings.length > maxContentEncodings) { - reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`)) + this.abort(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`)) return true } @@ -60774,6 +61005,8 @@ async function httpNetworkFetch ( } } + this.body = new Readable({ read: resume }) + const onError = this.onError.bind(this) resolve({ @@ -64277,6 +64510,19 @@ function collectASequenceOfCodePointsFast (char, input, position) { } const ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g // eslint-disable-line no-control-regex +const nonASCIIRegex = /[^\x00-\x7F]/ // eslint-disable-line no-control-regex + +/** + * @param {string} str + * @returns {string} + * + * @see https://infra.spec.whatwg.org/#ascii-lowercase + */ +function asciiLowercase (str) { + return nonASCIIRegex.test(str) + ? str.replace(/[A-Z]+/g, (upper) => upper.toLowerCase()) + : str.toLowerCase() +} /** * @param {string} data @@ -64441,6 +64687,7 @@ function serializeJavascriptValueToJSONString (value) { } module.exports = { + asciiLowercase, collectASequenceOfCodePoints, collectASequenceOfCodePointsFast, forgivingBase64, @@ -64464,6 +64711,7 @@ module.exports = { const assert = __nccwpck_require__(34589) const { runtimeFeatures } = __nccwpck_require__(313) +const { asciiLowercase } = __nccwpck_require__(8116) /** * @typedef {object} Metadata @@ -64528,14 +64776,12 @@ const getSRIHashAlgorithmIndex = /** @type {GetSRIHashAlgorithmIndex} */ (Map.pr /** * @typedef IsValidSRIHashAlgorithm + * @see https://w3c.github.io/webappsec-subresource-integrity/#valid-sri-hash-algorithm-token * @type {(algorithm: string) => algorithm is SRIHashAlgorithm} * @param {*} algorithm * @returns {algorithm is SRIHashAlgorithm} */ - -const isValidSRIHashAlgorithm = /** @type {IsValidSRIHashAlgorithm} */ ( - Map.prototype.has.bind(validSRIHashAlgorithmTokenSet) -) +const isValidSRIHashAlgorithm = (algorithm) => validSRIHashAlgorithmTokenSet.has(asciiLowercase(algorithm)) /** * @param {Uint8Array} bytes @@ -64691,7 +64937,8 @@ function parseMetadata (metadata) { // 8. Let metadata be the ordered map // «["alg" → algorithm, "val" → base64-value]». const metadata = { - alg: algorithm, + // Possible spec bug? + alg: asciiLowercase(algorithm), val: base64Value } @@ -67109,6 +67356,19 @@ class ByteParser extends Writable { if (isControlFrame(this.#info.opcode)) { this.#loop = this.parseControlFrame(body) this.#state = parserStates.INFO + + // If the automatic PONG/CLOSE reply filled the socket's write buffer, + // stop parsing and resume on drain so a PING flood cannot grow the + // outbound queue without bound. + if (this.#loop && this.#handler.socket.writableNeedDrain) { + this.#loop = false + this.#handler.socket.once('drain', () => { + if (this.destroyed) return + this.#loop = true + this.run(callback) + }) + return + } } else { if (!this.#info.compressed) { if (!this.writeFragments(body)) { diff --git a/package-lock.json b/package-lock.json index a30ccef..20d83e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4233,9 +4233,9 @@ } }, "node_modules/undici": { - "version": "7.30.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", - "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "version": "7.30.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz", + "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==", "license": "MIT", "engines": { "node": ">=20.18.1"