diff --git a/dist/index.js b/dist/index.js index 221ab32..05f5b45 100644 --- a/dist/index.js +++ b/dist/index.js @@ -25755,6 +25755,7 @@ function requireDispatcherBase () { if (hasRequiredDispatcherBase) return dispatcherBase; hasRequiredDispatcherBase = 1; + const buffer = require$$0$3; const Dispatcher = requireDispatcher(); const { ClientDestroyedError, @@ -25767,6 +25768,7 @@ function requireDispatcherBase () { const kOnClosed = Symbol('onClosed'); const kInterceptedDispatch = Symbol('Intercepted Dispatch'); const kWebSocketOptions = Symbol('webSocketOptions'); + const kEventSourceOptions = Symbol('eventSourceOptions'); class DispatcherBase extends Dispatcher { constructor (opts) { @@ -25777,6 +25779,7 @@ function requireDispatcherBase () { this[kClosed] = false; this[kOnClosed] = []; this[kWebSocketOptions] = opts?.webSocket ?? {}; + this[kEventSourceOptions] = opts?.eventSource ?? {}; } get webSocketOptions () { @@ -25786,6 +25789,12 @@ function requireDispatcherBase () { } } + get eventSourceOptions () { + return { + maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength + } + } + get destroyed () { return this[kDestroyed] } @@ -32403,8 +32412,6 @@ function requireClientH1 () { socket[kParser] = new Parser(client, socket, llhttpInstance); addListener(socket, 'error', function (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 @@ -33335,8 +33342,6 @@ function requireClientH2 () { socket[kHTTP2Session] = session; util.addListener(socket, 'error', function (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID'); - this[kError] = err; this[kClient][kOnError](err); @@ -33410,8 +33415,6 @@ function requireClientH2 () { } function onHttp2SessionError (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID'); - this[kSocket][kError] = err; this[kClient][kOnError](err); } @@ -34359,9 +34362,10 @@ function requireClient () { // h2 maxConcurrentStreams, allowH2, - webSocket + webSocket, + eventSource } = {}) { - super({ webSocket }); + super({ webSocket, eventSource }); if (keepAlive !== undefined) { throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') @@ -35606,7 +35610,7 @@ function requireAgent () { } class Agent extends DispatcherBase { - constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { + constructor ({ factory = defaultFactory, maxRedirections = 0, connect, tls, ...options } = {}) { if (typeof factory !== 'function') { throw new InvalidArgumentError('factory must be a function.') } @@ -35625,11 +35629,15 @@ function requireAgent () { connect = { ...connect }; } + if (tls && typeof tls !== 'function') { + tls = { ...tls }; + } + this[kInterceptors] = options.interceptors?.Agent && Array.isArray(options.interceptors.Agent) ? options.interceptors.Agent : [createRedirectInterceptor({ maxRedirections })]; - this[kOptions] = { ...util.deepClone(options), connect }; + this[kOptions] = { ...util.deepClone(options), connect, tls }; this[kOptions].interceptors = options.interceptors ? { ...options.interceptors } : undefined; @@ -44172,8 +44180,10 @@ function requireFetch () { // 19. Run these steps in parallel: // 1. Run these steps, but abort when fetchParams is canceled: - fetchParams.controller.onAborted = onAborted; - fetchParams.controller.on('terminated', onAborted); + if (!fetchParams.controller.resume) { + fetchParams.controller.on('terminated', onAborted); + } + fetchParams.controller.resume = async () => { // 1. While true while (true) { @@ -44342,8 +44352,6 @@ function requireFetch () { } location = headersList.get('location', true); - this.body = new Readable({ read: resume }); - const decoders = []; const willFollow = location && request.redirect === 'follow' && @@ -44361,7 +44369,7 @@ function requireFetch () { // 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 } @@ -44394,6 +44402,8 @@ function requireFetch () { } } + this.body = new Readable({ read: resume }); + const onError = this.onError.bind(this); resolve({ @@ -44440,10 +44450,6 @@ function requireFetch () { fetchParams.controller.off('terminated', this.abort); } - if (fetchParams.controller.onAborted) { - fetchParams.controller.off('terminated', fetchParams.controller.onAborted); - } - fetchParams.controller.ended = true; this.body.push(null); @@ -49241,6 +49247,20 @@ function requireReceiver () { 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. + const socket = this.ws[kResponse].socket; + if (this.#loop && socket.writableNeedDrain) { + this.#loop = false; + socket.once('drain', () => { + if (this.destroyed) return + this.#loop = true; + this.run(callback); + }); + return + } } else { if (!this.#info.compressed) { if (!this.writeFragments(body)) { @@ -50272,6 +50292,7 @@ var hasRequiredEventsourceStream; function requireEventsourceStream () { if (hasRequiredEventsourceStream) return eventsourceStream; hasRequiredEventsourceStream = 1; + const buffer = require$$0$3; const { Transform } = require$$0$5; const { isASCIINumber, isValidLastEventId } = requireUtil(); @@ -50296,11 +50317,19 @@ function requireEventsourceStream () { */ 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 @@ -50339,6 +50368,12 @@ function requireEventsourceStream () { return true } + function createMaxEventSizeExceededError () { + const error = new Error('EventSource message size exceeded'); + error.aborted = false; + return error + } + /** * @typedef {object} EventSourceStreamEvent * @type {object} @@ -50387,6 +50422,13 @@ function requireEventsourceStream () { 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, @@ -50397,6 +50439,7 @@ function requireEventsourceStream () { /** * @param {object} options + * @param {number} [options.maxEventSize] * @param {eventSourceSettings} options.eventSourceSettings * @param {Function} [options.push] */ @@ -50408,6 +50451,12 @@ function requireEventsourceStream () { 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; } @@ -50425,6 +50474,14 @@ function requireEventsourceStream () { 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 @@ -50503,7 +50560,12 @@ function requireEventsourceStream () { // 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 @@ -50512,15 +50574,28 @@ function requireEventsourceStream () { 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 {EventStreamEvent} event + * @param {EventSourceStreamEvent} event */ parseLine (line, event) { // If the line is empty (a blank line) @@ -50554,6 +50629,13 @@ function requireEventsourceStream () { } 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) { @@ -50561,11 +50643,13 @@ function requireEventsourceStream () { } 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 @@ -50573,20 +50657,29 @@ function requireEventsourceStream () { 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 */ @@ -50617,6 +50710,12 @@ function requireEventsourceStream () { 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 () { @@ -50652,14 +50751,16 @@ function requireEventsourceStream () { 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) { @@ -50785,6 +50886,7 @@ function requireEventsource () { const { delay } = requireUtil(); const { kEnumerableProperty } = requireUtil$7(); const { environmentSettingsObject } = requireUtil$6(); + const { getGlobalDispatcher } = requireGlobal(); let experimentalWarned = false; @@ -51066,6 +51168,7 @@ function requireEventsource () { const eventSourceStream = new EventSourceStream({ eventSourceSettings: this.#state, + maxEventSize: this.#dispatcher.eventSourceOptions?.maxEventSize, push: (event) => { this.dispatchEvent(createFastMessageEvent( event.type, @@ -51244,7 +51347,8 @@ function requireEventsource () { }, { key: 'dispatcher', // undici only - converter: webidl.converters.any + converter: webidl.converters.any, + defaultValue: () => getGlobalDispatcher() } ]); diff --git a/package-lock.json b/package-lock.json index 9a36716..0a5068a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10352,9 +10352,9 @@ } }, "node_modules/undici": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", - "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", + "version": "6.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz", + "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==", "license": "MIT", "engines": { "node": ">=18.17"