undici 8.12.1 → 8.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/docs/api/Client.md +7 -2
- package/lib/core/symbols.js +1 -0
- package/lib/dispatcher/agent.js +6 -2
- package/lib/dispatcher/client-h1.js +0 -2
- package/lib/dispatcher/client-h2.js +0 -4
- package/lib/handler/cache-handler.js +58 -0
- package/lib/handler/deduplication-handler.js +57 -13
- package/lib/handler/redirect-handler.js +15 -1
- package/lib/interceptor/decompress.js +15 -5
- package/lib/web/eventsource/eventsource-stream.js +66 -44
- package/lib/web/fetch/index.js +3 -3
- package/lib/web/subresource-integrity/subresource-integrity.js +5 -5
- package/lib/web/websocket/receiver.js +13 -0
- package/package.json +1 -1
- package/types/client.d.ts +6 -2
package/docs/docs/api/Client.md
CHANGED
|
@@ -148,8 +148,13 @@ added: v1.0.0
|
|
|
148
148
|
payloads, and decompressed (`permessage-deflate`) messages. Set to `0` to
|
|
149
149
|
disable the limit. **Default:** `134217728`.
|
|
150
150
|
* `eventSource` {Object} (optional) EventSource-specific configuration.
|
|
151
|
-
* `maxEventSize` {number} The maximum
|
|
152
|
-
|
|
151
|
+
* `maxEventSize` {number} The maximum total bytes of data and metadata
|
|
152
|
+
field values retained for a pending EventSource event. Each unfinished
|
|
153
|
+
line is separately limited to this value plus seven syntax bytes, before
|
|
154
|
+
its line ending arrives. The number of `data:` lines per event and input
|
|
155
|
+
chunks buffered for an unfinished line are each limited to
|
|
156
|
+
`min(131072, max(1024, floor(maxEventSize / 128)))` to bound object overhead.
|
|
157
|
+
Set to `0` to disable these limits.
|
|
153
158
|
**Default:** `buffer.kStringMaxLength`.
|
|
154
159
|
* Returns: {Client}
|
|
155
160
|
|
package/lib/core/symbols.js
CHANGED
|
@@ -6,6 +6,7 @@ module.exports = {
|
|
|
6
6
|
kDispatch: Symbol('dispatch'),
|
|
7
7
|
kUrl: Symbol('url'),
|
|
8
8
|
kRequestOrigin: Symbol('request origin'),
|
|
9
|
+
kRedirectCacheControl: Symbol('redirect cache control'),
|
|
9
10
|
kOriginless: Symbol('originless'),
|
|
10
11
|
kWriting: Symbol('writing'),
|
|
11
12
|
kResuming: Symbol('resuming'),
|
package/lib/dispatcher/agent.js
CHANGED
|
@@ -22,7 +22,7 @@ function defaultFactory (origin, opts) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
class Agent extends DispatcherBase {
|
|
25
|
-
constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) {
|
|
25
|
+
constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, tls, ...options } = {}) {
|
|
26
26
|
if (typeof factory !== 'function') {
|
|
27
27
|
throw new InvalidArgumentError('factory must be a function.')
|
|
28
28
|
}
|
|
@@ -41,7 +41,11 @@ class Agent extends DispatcherBase {
|
|
|
41
41
|
connect = { ...connect }
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
if (tls && typeof tls !== 'function') {
|
|
45
|
+
tls = { ...tls }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this[kOptions] = { ...util.deepClone(options), maxOrigins, connect, tls }
|
|
45
49
|
this[kFactory] = factory
|
|
46
50
|
this[kClients] = new Map()
|
|
47
51
|
this[kOrigins] = new Set()
|
|
@@ -988,8 +988,6 @@ function connectH1 (client, socket) {
|
|
|
988
988
|
}
|
|
989
989
|
|
|
990
990
|
function onHttpSocketError (err) {
|
|
991
|
-
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
992
|
-
|
|
993
991
|
const parser = this[kParser]
|
|
994
992
|
|
|
995
993
|
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
|
|
@@ -600,8 +600,6 @@ function onHttp2SendPing (session) {
|
|
|
600
600
|
}
|
|
601
601
|
|
|
602
602
|
function onHttp2SessionError (err) {
|
|
603
|
-
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
604
|
-
|
|
605
603
|
this[kSocket][kError] = err
|
|
606
604
|
|
|
607
605
|
if (this[kReceivedGoAway]) {
|
|
@@ -767,8 +765,6 @@ function onHttp2SocketClose () {
|
|
|
767
765
|
}
|
|
768
766
|
|
|
769
767
|
function onHttp2SocketError (err) {
|
|
770
|
-
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
771
|
-
|
|
772
768
|
this[kError] = err
|
|
773
769
|
|
|
774
770
|
if (this[kHTTP2Session]?.[kReceivedGoAway]) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const util = require('../core/util')
|
|
4
|
+
const { kRedirectCacheControl } = require('../core/symbols')
|
|
4
5
|
const {
|
|
5
6
|
parseCacheControlHeader,
|
|
6
7
|
hasInvalidCacheControlDirective,
|
|
@@ -57,6 +58,19 @@ class CacheController {
|
|
|
57
58
|
set rawTrailers (value) { this.target.rawTrailers = value }
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* @param {string | URL} a
|
|
63
|
+
* @param {string | URL} b
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
function sameOrigin (a, b) {
|
|
67
|
+
try {
|
|
68
|
+
return new URL(String(a)).origin === new URL(String(b)).origin
|
|
69
|
+
} catch {
|
|
70
|
+
return String(a) === String(b)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
// Status codes that we can use some heuristics on to cache
|
|
61
75
|
const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
|
|
62
76
|
200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
|
|
@@ -203,6 +217,14 @@ class CacheHandler {
|
|
|
203
217
|
*/
|
|
204
218
|
#writeStream
|
|
205
219
|
|
|
220
|
+
/**
|
|
221
|
+
* The URLs visited while following redirects, shared by reference from the
|
|
222
|
+
* redirect handler through the request context. Carries the suppressed hops'
|
|
223
|
+
* cache-control under {@link kRedirectCacheControl}.
|
|
224
|
+
* @type {URL[] | undefined}
|
|
225
|
+
*/
|
|
226
|
+
#history
|
|
227
|
+
|
|
206
228
|
/**
|
|
207
229
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
|
|
208
230
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
@@ -220,6 +242,13 @@ class CacheHandler {
|
|
|
220
242
|
this.#writeStream?.destroy()
|
|
221
243
|
this.#writeStream = undefined
|
|
222
244
|
this.#downstreamController = new CacheController(controller)
|
|
245
|
+
// The redirect handler threads the list of visited URLs through the
|
|
246
|
+
// handler-chain context. Keep a reference to it (it is mutated as redirects
|
|
247
|
+
// are followed) so onResponseStart can tell which request actually produced
|
|
248
|
+
// the response. Unlike the request options object, this reference is not
|
|
249
|
+
// broken by an interceptor that copies the options between the cache and
|
|
250
|
+
// the redirect.
|
|
251
|
+
this.#history = context?.history
|
|
223
252
|
this.#handler.onRequestStart?.(this.#downstreamController, context)
|
|
224
253
|
}
|
|
225
254
|
|
|
@@ -267,6 +296,35 @@ class CacheHandler {
|
|
|
267
296
|
return downstreamOnHeaders()
|
|
268
297
|
}
|
|
269
298
|
|
|
299
|
+
// A cross-origin redirect can change the origin that actually produced the
|
|
300
|
+
// final response. Keying that response on the original (trusted) origin
|
|
301
|
+
// would let a redirect target control an entry for a URL the application
|
|
302
|
+
// trusts. The final request is the last URL in the redirect history; only
|
|
303
|
+
// cache when its origin still matches the cache key origin.
|
|
304
|
+
if (this.#history && this.#history.length > 0) {
|
|
305
|
+
const finalUrl = this.#history[this.#history.length - 1]
|
|
306
|
+
if (!sameOrigin(finalUrl.origin, this.#cacheKey.origin)) {
|
|
307
|
+
return downstreamOnHeaders()
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// A redirect hop that is not forwarded can still carry a restrictive
|
|
312
|
+
// storage policy (for example `no-store`, or `private` for a shared cache).
|
|
313
|
+
// Honor it, otherwise a redirect that must not be stored could be laundered
|
|
314
|
+
// into a cacheable final response under the original key.
|
|
315
|
+
const redirectCacheControl = this.#history?.[kRedirectCacheControl]
|
|
316
|
+
if (redirectCacheControl) {
|
|
317
|
+
for (const hopCacheControl of redirectCacheControl) {
|
|
318
|
+
const hopDirectives = hopCacheControl ? parseCacheControlHeader(hopCacheControl) : {}
|
|
319
|
+
if (
|
|
320
|
+
hopDirectives['no-store'] === true ||
|
|
321
|
+
(this.#cacheType === 'shared' && hopDirectives.private === true)
|
|
322
|
+
) {
|
|
323
|
+
return downstreamOnHeaders()
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
270
328
|
const cacheControlHeader = resHeaders['cache-control']
|
|
271
329
|
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
|
|
272
330
|
|
|
@@ -16,6 +16,7 @@ const DEFAULT_MAX_BUFFER_SIZE = 5 * 1024 * 1024
|
|
|
16
16
|
* @property {number} bufferedBytes
|
|
17
17
|
* @property {object | null} pendingTrailers
|
|
18
18
|
* @property {boolean} done
|
|
19
|
+
* @property {boolean} [pendingStart] onRequestStart deferred until the primary request's context is known
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -30,6 +31,20 @@ class DeduplicationHandler {
|
|
|
30
31
|
*/
|
|
31
32
|
#primaryHandler
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Whether the primary request has started (onRequestStart received). Until
|
|
36
|
+
* then the request context is unknown and cannot be given to waiting handlers.
|
|
37
|
+
* @type {boolean}
|
|
38
|
+
*/
|
|
39
|
+
#requestStarted = false
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The primary request context, replayed to waiting handlers so they observe
|
|
43
|
+
* the same redirect history as the primary.
|
|
44
|
+
* @type {any}
|
|
45
|
+
*/
|
|
46
|
+
#context = null
|
|
47
|
+
|
|
33
48
|
/**
|
|
34
49
|
* @type {WaitingHandler[]}
|
|
35
50
|
*/
|
|
@@ -112,20 +127,30 @@ class DeduplicationHandler {
|
|
|
112
127
|
const waitingController = waitingHandler.controller
|
|
113
128
|
|
|
114
129
|
try {
|
|
115
|
-
|
|
130
|
+
if (this.#requestStarted) {
|
|
131
|
+
// The primary request has started, so its context (including the
|
|
132
|
+
// redirect history) is known and can be delivered immediately.
|
|
133
|
+
handler.onRequestStart?.(waitingController, this.#context)
|
|
116
134
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
135
|
+
if (waitingController.aborted) {
|
|
136
|
+
waitingHandler.done = true
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
121
139
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
if (this.#responseStarted) {
|
|
141
|
+
handler.onResponseStart?.(
|
|
142
|
+
waitingController,
|
|
143
|
+
this.#statusCode,
|
|
144
|
+
this.#headers,
|
|
145
|
+
this.#statusMessage
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// The primary request has not started yet. Defer onRequestStart until
|
|
150
|
+
// its context is known, so the waiting handler observes the same
|
|
151
|
+
// redirect history as the primary (a cache handler relies on it to
|
|
152
|
+
// reject cross-origin redirect responses).
|
|
153
|
+
waitingHandler.pendingStart = true
|
|
129
154
|
}
|
|
130
155
|
} catch {
|
|
131
156
|
// Ignore errors from waiting handlers
|
|
@@ -146,6 +171,24 @@ class DeduplicationHandler {
|
|
|
146
171
|
*/
|
|
147
172
|
onRequestStart (controller, context) {
|
|
148
173
|
this.#controller = controller
|
|
174
|
+
if (!this.#requestStarted) {
|
|
175
|
+
this.#requestStarted = true
|
|
176
|
+
this.#context = context
|
|
177
|
+
// Deliver the now-known context to handlers that joined before the
|
|
178
|
+
// primary request started.
|
|
179
|
+
for (const waitingHandler of this.#waitingHandlers) {
|
|
180
|
+
if (waitingHandler.pendingStart && !waitingHandler.done && !waitingHandler.controller.aborted) {
|
|
181
|
+
waitingHandler.pendingStart = false
|
|
182
|
+
try {
|
|
183
|
+
waitingHandler.handler.onRequestStart?.(waitingHandler.controller, context)
|
|
184
|
+
} catch {
|
|
185
|
+
// Ignore errors from waiting handlers
|
|
186
|
+
waitingHandler.done = true
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
this.#pruneDoneWaitingHandlers()
|
|
191
|
+
}
|
|
149
192
|
this.#primaryHandler.onRequestStart?.(controller, context)
|
|
150
193
|
}
|
|
151
194
|
|
|
@@ -330,7 +373,8 @@ class DeduplicationHandler {
|
|
|
330
373
|
bufferedChunks: [],
|
|
331
374
|
bufferedBytes: 0,
|
|
332
375
|
pendingTrailers: null,
|
|
333
|
-
done: false
|
|
376
|
+
done: false,
|
|
377
|
+
pendingStart: false
|
|
334
378
|
}
|
|
335
379
|
|
|
336
380
|
const state = {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const util = require('../core/util')
|
|
4
4
|
const assert = require('node:assert')
|
|
5
5
|
const { InvalidArgumentError, MaxRedirectsError } = require('../core/errors')
|
|
6
|
-
const { kRequestOrigin } = require('../core/symbols')
|
|
6
|
+
const { kRequestOrigin, kRedirectCacheControl } = require('../core/symbols')
|
|
7
7
|
|
|
8
8
|
const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
|
|
9
9
|
|
|
@@ -38,6 +38,15 @@ class RedirectHandler {
|
|
|
38
38
|
this.maxRedirections = maxRedirections
|
|
39
39
|
this.handler = handler
|
|
40
40
|
this.history = []
|
|
41
|
+
// `cache-control` of each intermediate (suppressed) redirect hop. An outer
|
|
42
|
+
// cache uses it to preserve a hop's storage policy (e.g. `no-store`) that
|
|
43
|
+
// would otherwise be lost because the hop's response is not forwarded. It
|
|
44
|
+
// travels as a non-enumerable property of the history array so it reaches
|
|
45
|
+
// the cache without becoming part of the public request/response context.
|
|
46
|
+
Object.defineProperty(this.history, kRedirectCacheControl, {
|
|
47
|
+
value: [],
|
|
48
|
+
enumerable: false
|
|
49
|
+
})
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
onRequestStart (controller, context) {
|
|
@@ -114,6 +123,11 @@ class RedirectHandler {
|
|
|
114
123
|
return
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
// This hop's response is suppressed (only its Location is used). Record its
|
|
127
|
+
// cache-control so an outer cache can honor a restrictive policy that would
|
|
128
|
+
// otherwise be lost when the final response is stored.
|
|
129
|
+
this.history[kRedirectCacheControl].push(headers['cache-control'])
|
|
130
|
+
|
|
117
131
|
const baseUrl = requestOrigin
|
|
118
132
|
? new URL(this.opts.path, requestOrigin)
|
|
119
133
|
: undefined
|
|
@@ -223,9 +223,14 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
223
223
|
this.#decompressedSize = decompressedSize
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.#controller.
|
|
226
|
+
try {
|
|
227
|
+
const result = super.onResponseData(this.#controller, chunk)
|
|
228
|
+
if (result === false && !this.#controller.paused) {
|
|
229
|
+
this.#controller.pause()
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
this.#fail(err)
|
|
233
|
+
return
|
|
229
234
|
}
|
|
230
235
|
}
|
|
231
236
|
} while (this.#drainRequested && !this.#terminated && !this.#controller.paused)
|
|
@@ -242,9 +247,14 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
242
247
|
return
|
|
243
248
|
}
|
|
244
249
|
|
|
245
|
-
this.#terminated = true
|
|
246
250
|
this.#cleanupDecompressors()
|
|
247
|
-
|
|
251
|
+
try {
|
|
252
|
+
super.onResponseEnd(this.#controller, this.#trailers)
|
|
253
|
+
} catch (err) {
|
|
254
|
+
this.#fail(err)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
this.#terminated = true
|
|
248
258
|
}
|
|
249
259
|
|
|
250
260
|
#onDecompressionEnd () {
|
|
@@ -31,12 +31,11 @@ const EVENT = Buffer.from('event')
|
|
|
31
31
|
const ID = Buffer.from('id')
|
|
32
32
|
const RETRY = Buffer.from('retry')
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const DATA_FIELD_PREFIX_SIZE = DATA.length + 2
|
|
34
|
+
// Each data field creates a string node, and each unconsumed input chunk keeps
|
|
35
|
+
// a Buffer wrapper. The absolute ceiling protects the large default byte limit.
|
|
36
|
+
const minParserItems = 1024
|
|
37
|
+
const maxParserItems = 131072
|
|
38
|
+
const bytesPerParserItem = 128
|
|
40
39
|
|
|
41
40
|
function isASCIINumberBytes (buffer, start) {
|
|
42
41
|
if (start >= buffer.length) {
|
|
@@ -130,9 +129,13 @@ class EventSourceStream extends Transform {
|
|
|
130
129
|
pos = 0
|
|
131
130
|
lineChunkIndex = 0
|
|
132
131
|
linePos = 0
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
lineLength = 0
|
|
133
|
+
eventSize = 0
|
|
134
|
+
eventFieldSizes = { data: 0, event: 0, id: 0, retry: 0 }
|
|
135
|
+
dataLineCount = 0
|
|
135
136
|
maxEventSize
|
|
137
|
+
maxDataLines
|
|
138
|
+
maxBufferedChunks
|
|
136
139
|
|
|
137
140
|
event = {
|
|
138
141
|
data: undefined,
|
|
@@ -157,6 +160,11 @@ class EventSourceStream extends Transform {
|
|
|
157
160
|
|
|
158
161
|
this.state = options.eventSourceSettings || {}
|
|
159
162
|
this.maxEventSize = options.maxEventSize ?? defaultMaxEventSize
|
|
163
|
+
const parserItemLimit = this.maxEventSize > 0
|
|
164
|
+
? Math.min(maxParserItems, Math.max(minParserItems, Math.floor(this.maxEventSize / bytesPerParserItem)))
|
|
165
|
+
: 0
|
|
166
|
+
this.maxDataLines = parserItemLimit
|
|
167
|
+
this.maxBufferedChunks = parserItemLimit
|
|
160
168
|
if (options.push) {
|
|
161
169
|
this.push = options.push
|
|
162
170
|
}
|
|
@@ -174,6 +182,14 @@ class EventSourceStream extends Transform {
|
|
|
174
182
|
return
|
|
175
183
|
}
|
|
176
184
|
|
|
185
|
+
// A leading line ending can immediately release the currently buffered
|
|
186
|
+
// line, even when it uses the final allowed chunk slot.
|
|
187
|
+
if (this.maxEventSize > 0 &&
|
|
188
|
+
this.chunks.length >= this.maxBufferedChunks &&
|
|
189
|
+
chunk[0] !== LF && chunk[0] !== CR) {
|
|
190
|
+
callback(createMaxEventSizeExceededError())
|
|
191
|
+
return
|
|
192
|
+
}
|
|
177
193
|
this.chunks.push(chunk)
|
|
178
194
|
|
|
179
195
|
// Strip leading byte-order-mark if we opened the stream and started
|
|
@@ -266,21 +282,25 @@ class EventSourceStream extends Transform {
|
|
|
266
282
|
continue
|
|
267
283
|
}
|
|
268
284
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// maxEventSize here to prevent unbounded buffering when a server streams a
|
|
274
|
-
// field that never contains a line terminator.
|
|
275
|
-
if (this.currentLineSize > this.getMaxEventSizeLineLength()) {
|
|
285
|
+
// readLine() is only called after CR or LF. Reject an unfinished line
|
|
286
|
+
// before it can retain an unbounded number of decoded chunks. The seven
|
|
287
|
+
// extra bytes allow the longest field prefix ("event: " or "retry: ").
|
|
288
|
+
if (this.maxEventSize > 0 && ++this.lineLength > this.maxEventSize + 7) {
|
|
276
289
|
callback(createMaxEventSizeExceededError())
|
|
277
290
|
return
|
|
278
291
|
}
|
|
292
|
+
this.advanceCursor()
|
|
279
293
|
}
|
|
280
294
|
|
|
281
295
|
callback()
|
|
282
296
|
}
|
|
283
297
|
|
|
298
|
+
_destroy (error, callback) {
|
|
299
|
+
this.chunks.length = 0
|
|
300
|
+
this.clearEvent()
|
|
301
|
+
super._destroy(error, callback)
|
|
302
|
+
}
|
|
303
|
+
|
|
284
304
|
/**
|
|
285
305
|
* @param {Buffer} line
|
|
286
306
|
* @param {EventSourceStreamEvent} event
|
|
@@ -317,12 +337,12 @@ class EventSourceStream extends Transform {
|
|
|
317
337
|
}
|
|
318
338
|
|
|
319
339
|
if (isFieldName(line, fieldLength, DATA)) {
|
|
320
|
-
|
|
321
|
-
const eventDataSize = this.eventDataSize + (event.data === undefined ? 0 : 1) + valueBytes
|
|
322
|
-
|
|
323
|
-
if (this.maxEventSize > 0 && eventDataSize > this.maxEventSize) {
|
|
340
|
+
if (this.maxEventSize > 0 && this.dataLineCount >= this.maxDataLines) {
|
|
324
341
|
throw createMaxEventSizeExceededError()
|
|
325
342
|
}
|
|
343
|
+
const valueBytes = line.length - valueStart
|
|
344
|
+
const dataSize = this.eventFieldSizes.data + (event.data === undefined ? 0 : 1) + valueBytes
|
|
345
|
+
this.setEventFieldSize('data', dataSize)
|
|
326
346
|
|
|
327
347
|
const value = line.toString('utf8', valueStart)
|
|
328
348
|
|
|
@@ -331,12 +351,13 @@ class EventSourceStream extends Transform {
|
|
|
331
351
|
} else {
|
|
332
352
|
event.data += `\n${value}`
|
|
333
353
|
}
|
|
334
|
-
this.
|
|
354
|
+
this.dataLineCount++
|
|
335
355
|
return
|
|
336
356
|
}
|
|
337
357
|
|
|
338
358
|
if (isFieldName(line, fieldLength, RETRY)) {
|
|
339
359
|
if (isASCIINumberBytes(line, valueStart)) {
|
|
360
|
+
this.setEventFieldSize('retry', line.length - valueStart)
|
|
340
361
|
event.retry = line.toString('utf8', valueStart)
|
|
341
362
|
}
|
|
342
363
|
return
|
|
@@ -344,20 +365,29 @@ class EventSourceStream extends Transform {
|
|
|
344
365
|
|
|
345
366
|
if (isFieldName(line, fieldLength, ID)) {
|
|
346
367
|
if (isValidLastEventIdBytes(line, valueStart)) {
|
|
368
|
+
this.setEventFieldSize('id', line.length - valueStart)
|
|
347
369
|
event.id = line.toString('utf8', valueStart)
|
|
348
370
|
}
|
|
349
371
|
return
|
|
350
372
|
}
|
|
351
373
|
|
|
352
374
|
if (isFieldName(line, fieldLength, EVENT)) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
event.event = value
|
|
375
|
+
if (valueStart < line.length) {
|
|
376
|
+
this.setEventFieldSize('event', line.length - valueStart)
|
|
377
|
+
event.event = line.toString('utf8', valueStart)
|
|
357
378
|
}
|
|
358
379
|
}
|
|
359
380
|
}
|
|
360
381
|
|
|
382
|
+
setEventFieldSize (field, size) {
|
|
383
|
+
const eventSize = this.eventSize - this.eventFieldSizes[field] + size
|
|
384
|
+
if (this.maxEventSize > 0 && eventSize > this.maxEventSize) {
|
|
385
|
+
throw createMaxEventSizeExceededError()
|
|
386
|
+
}
|
|
387
|
+
this.eventSize = eventSize
|
|
388
|
+
this.eventFieldSizes[field] = size
|
|
389
|
+
}
|
|
390
|
+
|
|
361
391
|
/**
|
|
362
392
|
* @param {EventSourceStreamEvent} event
|
|
363
393
|
*/
|
|
@@ -388,7 +418,12 @@ class EventSourceStream extends Transform {
|
|
|
388
418
|
this.event.event = undefined
|
|
389
419
|
this.event.id = undefined
|
|
390
420
|
this.event.retry = undefined
|
|
391
|
-
this.
|
|
421
|
+
this.eventSize = 0
|
|
422
|
+
this.dataLineCount = 0
|
|
423
|
+
this.eventFieldSizes.data = 0
|
|
424
|
+
this.eventFieldSizes.event = 0
|
|
425
|
+
this.eventFieldSizes.id = 0
|
|
426
|
+
this.eventFieldSizes.retry = 0
|
|
392
427
|
}
|
|
393
428
|
|
|
394
429
|
hasPendingEvent () {
|
|
@@ -424,29 +459,16 @@ class EventSourceStream extends Transform {
|
|
|
424
459
|
syncLineStartToCursor () {
|
|
425
460
|
this.lineChunkIndex = this.chunkIndex
|
|
426
461
|
this.linePos = this.pos
|
|
427
|
-
this.
|
|
462
|
+
this.lineLength = 0
|
|
428
463
|
this.dropConsumedChunks()
|
|
429
464
|
}
|
|
430
465
|
|
|
431
|
-
/**
|
|
432
|
-
* @returns {number} The maximum length, in bytes, of the currently buffered
|
|
433
|
-
* (possibly unterminated) line. The extra {@link DATA_FIELD_PREFIX_SIZE}
|
|
434
|
-
* accounts for the longest `data:` field prefix, so a terminated data line
|
|
435
|
-
* carrying exactly {@link maxEventSize} value bytes remains accepted.
|
|
436
|
-
*/
|
|
437
|
-
getMaxEventSizeLineLength () {
|
|
438
|
-
if (this.maxEventSize <= 0) {
|
|
439
|
-
return Infinity
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return this.maxEventSize + DATA_FIELD_PREFIX_SIZE
|
|
443
|
-
}
|
|
444
|
-
|
|
445
466
|
dropConsumedChunks () {
|
|
446
|
-
|
|
447
|
-
this.
|
|
448
|
-
this.
|
|
449
|
-
this.
|
|
467
|
+
if (this.lineChunkIndex > 0) {
|
|
468
|
+
const consumed = this.lineChunkIndex
|
|
469
|
+
this.chunks.splice(0, consumed)
|
|
470
|
+
this.lineChunkIndex = 0
|
|
471
|
+
this.chunkIndex -= consumed
|
|
450
472
|
}
|
|
451
473
|
|
|
452
474
|
if (this.chunkIndex === this.chunks.length) {
|
package/lib/web/fetch/index.js
CHANGED
|
@@ -2280,8 +2280,6 @@ async function httpNetworkFetch (
|
|
|
2280
2280
|
appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders)
|
|
2281
2281
|
const location = headersList.get('location', true)
|
|
2282
2282
|
|
|
2283
|
-
this.body = new Readable({ read: () => controller.resume() })
|
|
2284
|
-
|
|
2285
2283
|
const willFollow = location && request.redirect === 'follow' &&
|
|
2286
2284
|
redirectStatusSet.has(status)
|
|
2287
2285
|
|
|
@@ -2299,7 +2297,7 @@ async function httpNetworkFetch (
|
|
|
2299
2297
|
// CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
|
|
2300
2298
|
const maxContentEncodings = 5
|
|
2301
2299
|
if (codings.length > maxContentEncodings) {
|
|
2302
|
-
|
|
2300
|
+
controller.abort(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
|
|
2303
2301
|
return
|
|
2304
2302
|
}
|
|
2305
2303
|
|
|
@@ -2341,6 +2339,8 @@ async function httpNetworkFetch (
|
|
|
2341
2339
|
}
|
|
2342
2340
|
}
|
|
2343
2341
|
|
|
2342
|
+
this.body = new Readable({ read: () => controller.resume() })
|
|
2343
|
+
|
|
2344
2344
|
const onError = (err) => this.onResponseError(controller, err)
|
|
2345
2345
|
|
|
2346
2346
|
resolve({
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert')
|
|
4
4
|
const { runtimeFeatures } = require('../../util/runtime-features.js')
|
|
5
|
+
const { asciiLowercase } = require('../infra/index.js')
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @typedef {object} Metadata
|
|
@@ -66,14 +67,12 @@ const getSRIHashAlgorithmIndex = /** @type {GetSRIHashAlgorithmIndex} */ (Map.pr
|
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
69
|
* @typedef IsValidSRIHashAlgorithm
|
|
70
|
+
* @see https://w3c.github.io/webappsec-subresource-integrity/#valid-sri-hash-algorithm-token
|
|
69
71
|
* @type {(algorithm: string) => algorithm is SRIHashAlgorithm}
|
|
70
72
|
* @param {*} algorithm
|
|
71
73
|
* @returns {algorithm is SRIHashAlgorithm}
|
|
72
74
|
*/
|
|
73
|
-
|
|
74
|
-
const isValidSRIHashAlgorithm = /** @type {IsValidSRIHashAlgorithm} */ (
|
|
75
|
-
Map.prototype.has.bind(validSRIHashAlgorithmTokenSet)
|
|
76
|
-
)
|
|
75
|
+
const isValidSRIHashAlgorithm = (algorithm) => validSRIHashAlgorithmTokenSet.has(asciiLowercase(algorithm))
|
|
77
76
|
|
|
78
77
|
/**
|
|
79
78
|
* @param {Uint8Array} bytes
|
|
@@ -229,7 +228,8 @@ function parseMetadata (metadata) {
|
|
|
229
228
|
// 8. Let metadata be the ordered map
|
|
230
229
|
// «["alg" → algorithm, "val" → base64-value]».
|
|
231
230
|
const metadata = {
|
|
232
|
-
|
|
231
|
+
// Possible spec bug?
|
|
232
|
+
alg: asciiLowercase(algorithm),
|
|
233
233
|
val: base64Value
|
|
234
234
|
}
|
|
235
235
|
|
|
@@ -252,6 +252,19 @@ class ByteParser extends Writable {
|
|
|
252
252
|
if (isControlFrame(this.#info.opcode)) {
|
|
253
253
|
this.#loop = this.parseControlFrame(body)
|
|
254
254
|
this.#state = parserStates.INFO
|
|
255
|
+
|
|
256
|
+
// If the automatic PONG/CLOSE reply filled the socket's write buffer,
|
|
257
|
+
// stop parsing and resume on drain so a PING flood cannot grow the
|
|
258
|
+
// outbound queue without bound.
|
|
259
|
+
if (this.#loop && this.#handler.socket.writableNeedDrain) {
|
|
260
|
+
this.#loop = false
|
|
261
|
+
this.#handler.socket.once('drain', () => {
|
|
262
|
+
if (this.destroyed) return
|
|
263
|
+
this.#loop = true
|
|
264
|
+
this.run(callback)
|
|
265
|
+
})
|
|
266
|
+
return
|
|
267
|
+
}
|
|
255
268
|
} else {
|
|
256
269
|
if (!this.#info.compressed) {
|
|
257
270
|
if (!this.writeFragments(body)) {
|
package/package.json
CHANGED
package/types/client.d.ts
CHANGED
|
@@ -175,8 +175,12 @@ export declare namespace Client {
|
|
|
175
175
|
}
|
|
176
176
|
export interface EventSourceOptions {
|
|
177
177
|
/**
|
|
178
|
-
* Maximum
|
|
179
|
-
*
|
|
178
|
+
* Maximum total bytes of data and metadata field values retained for a
|
|
179
|
+
* pending EventSource event. Each unfinished line is separately bounded
|
|
180
|
+
* to this value plus seven syntax bytes. The number of data lines per
|
|
181
|
+
* event and buffered input chunks are each limited to
|
|
182
|
+
* min(131072, max(1024, floor(maxEventSize / 128))).
|
|
183
|
+
* Set to 0 to disable these limits.
|
|
180
184
|
* @default buffer.kStringMaxLength
|
|
181
185
|
*/
|
|
182
186
|
maxEventSize?: number;
|