undici 7.16.0 → 7.18.0
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/README.md +53 -1
- package/docs/docs/api/Client.md +1 -0
- package/docs/docs/api/DiagnosticsChannel.md +57 -0
- package/docs/docs/api/Dispatcher.md +86 -0
- package/docs/docs/api/RoundRobinPool.md +145 -0
- package/docs/docs/api/WebSocket.md +21 -0
- package/docs/docs/best-practices/crawling.md +58 -0
- package/index.js +4 -1
- package/lib/api/api-upgrade.js +2 -1
- package/lib/core/connect.js +4 -1
- package/lib/core/diagnostics.js +28 -1
- package/lib/core/symbols.js +3 -0
- package/lib/core/util.js +29 -31
- package/lib/dispatcher/balanced-pool.js +10 -0
- package/lib/dispatcher/client-h1.js +0 -16
- package/lib/dispatcher/client-h2.js +153 -23
- package/lib/dispatcher/client.js +7 -2
- package/lib/dispatcher/dispatcher-base.js +11 -12
- package/lib/dispatcher/h2c-client.js +7 -78
- package/lib/dispatcher/pool-base.js +1 -1
- package/lib/dispatcher/proxy-agent.js +13 -2
- package/lib/dispatcher/round-robin-pool.js +137 -0
- package/lib/encoding/index.js +33 -0
- package/lib/handler/cache-handler.js +84 -27
- package/lib/handler/deduplication-handler.js +216 -0
- package/lib/handler/retry-handler.js +0 -2
- package/lib/interceptor/cache.js +35 -17
- package/lib/interceptor/decompress.js +2 -1
- package/lib/interceptor/deduplicate.js +109 -0
- package/lib/interceptor/dns.js +55 -13
- package/lib/mock/mock-utils.js +1 -2
- package/lib/mock/snapshot-agent.js +11 -5
- package/lib/mock/snapshot-recorder.js +12 -4
- package/lib/mock/snapshot-utils.js +4 -4
- package/lib/util/cache.js +29 -1
- package/lib/util/runtime-features.js +124 -0
- package/lib/web/cookies/parse.js +1 -1
- package/lib/web/fetch/body.js +29 -39
- package/lib/web/fetch/data-url.js +12 -160
- package/lib/web/fetch/formdata-parser.js +204 -127
- package/lib/web/fetch/index.js +18 -6
- package/lib/web/fetch/request.js +6 -0
- package/lib/web/fetch/response.js +2 -3
- package/lib/web/fetch/util.js +2 -65
- package/lib/web/infra/index.js +229 -0
- package/lib/web/subresource-integrity/subresource-integrity.js +6 -5
- package/lib/web/webidl/index.js +4 -2
- package/lib/web/websocket/connection.js +31 -21
- package/lib/web/websocket/frame.js +9 -15
- package/lib/web/websocket/stream/websocketstream.js +1 -1
- package/lib/web/websocket/util.js +2 -1
- package/package.json +5 -4
- package/types/agent.d.ts +1 -1
- package/types/api.d.ts +2 -2
- package/types/balanced-pool.d.ts +2 -1
- package/types/cache-interceptor.d.ts +1 -0
- package/types/client.d.ts +1 -1
- package/types/connector.d.ts +2 -2
- package/types/diagnostics-channel.d.ts +2 -2
- package/types/dispatcher.d.ts +12 -12
- package/types/fetch.d.ts +4 -4
- package/types/formdata.d.ts +1 -1
- package/types/h2c-client.d.ts +1 -1
- package/types/index.d.ts +9 -1
- package/types/interceptors.d.ts +36 -2
- package/types/pool.d.ts +1 -1
- package/types/readable.d.ts +2 -2
- package/types/round-robin-pool.d.ts +41 -0
- package/types/websocket.d.ts +9 -9
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { bufferToLowerCasedHeaderName } = require('../../core/util')
|
|
4
|
-
const {
|
|
5
|
-
const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
|
|
4
|
+
const { HTTP_TOKEN_CODEPOINTS } = require('./data-url')
|
|
6
5
|
const { makeEntry } = require('./formdata')
|
|
7
6
|
const { webidl } = require('../webidl')
|
|
8
7
|
const assert = require('node:assert')
|
|
8
|
+
const { isomorphicDecode } = require('../infra')
|
|
9
|
+
const { utf8DecodeBytes } = require('../../encoding')
|
|
9
10
|
|
|
10
|
-
const formDataNameBuffer = Buffer.from('form-data; name="')
|
|
11
|
-
const filenameBuffer = Buffer.from('filename')
|
|
12
11
|
const dd = Buffer.from('--')
|
|
13
|
-
const
|
|
12
|
+
const decoder = new TextDecoder()
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* @param {string} chars
|
|
@@ -84,20 +83,16 @@ function multipartFormDataParser (input, mimeType) {
|
|
|
84
83
|
// the first byte.
|
|
85
84
|
const position = { position: 0 }
|
|
86
85
|
|
|
87
|
-
// Note:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
let trailing = input.length
|
|
86
|
+
// Note: Per RFC 2046 Section 5.1.1, we must ignore anything before the
|
|
87
|
+
// first boundary delimiter line (preamble). Search for the first boundary.
|
|
88
|
+
const firstBoundaryIndex = input.indexOf(boundary)
|
|
93
89
|
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
if (firstBoundaryIndex === -1) {
|
|
91
|
+
throw parsingError('no boundary found in multipart body')
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
94
|
+
// Start parsing from the first boundary, ignoring any preamble
|
|
95
|
+
position.position = firstBoundaryIndex
|
|
101
96
|
|
|
102
97
|
// 5. While true:
|
|
103
98
|
while (true) {
|
|
@@ -113,11 +108,11 @@ function multipartFormDataParser (input, mimeType) {
|
|
|
113
108
|
|
|
114
109
|
// 5.2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A
|
|
115
110
|
// (`--` followed by CR LF) followed by the end of input, return entry list.
|
|
116
|
-
// Note:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
// Note: Per RFC 2046 Section 5.1.1, we must ignore anything after the
|
|
112
|
+
// final boundary delimiter (epilogue). Check for -- or --CRLF and return
|
|
113
|
+
// regardless of what follows.
|
|
114
|
+
if (bufferStartsWith(input, dd, position)) {
|
|
115
|
+
// Found closing boundary delimiter (--), ignore any epilogue
|
|
121
116
|
return entryList
|
|
122
117
|
}
|
|
123
118
|
|
|
@@ -205,6 +200,113 @@ function multipartFormDataParser (input, mimeType) {
|
|
|
205
200
|
}
|
|
206
201
|
}
|
|
207
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Parses content-disposition attributes (e.g., name="value" or filename*=utf-8''encoded)
|
|
205
|
+
* @param {Buffer} input
|
|
206
|
+
* @param {{ position: number }} position
|
|
207
|
+
* @returns {{ name: string, value: string }}
|
|
208
|
+
*/
|
|
209
|
+
function parseContentDispositionAttribute (input, position) {
|
|
210
|
+
// Skip leading semicolon and whitespace
|
|
211
|
+
if (input[position.position] === 0x3b /* ; */) {
|
|
212
|
+
position.position++
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Skip whitespace
|
|
216
|
+
collectASequenceOfBytes(
|
|
217
|
+
(char) => char === 0x20 || char === 0x09,
|
|
218
|
+
input,
|
|
219
|
+
position
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
// Collect attribute name (token characters)
|
|
223
|
+
const attributeName = collectASequenceOfBytes(
|
|
224
|
+
(char) => isToken(char) && char !== 0x3d && char !== 0x2a, // not = or *
|
|
225
|
+
input,
|
|
226
|
+
position
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if (attributeName.length === 0) {
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const attrNameStr = attributeName.toString('ascii').toLowerCase()
|
|
234
|
+
|
|
235
|
+
// Check for extended notation (attribute*)
|
|
236
|
+
const isExtended = input[position.position] === 0x2a /* * */
|
|
237
|
+
if (isExtended) {
|
|
238
|
+
position.position++ // skip *
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Expect = sign
|
|
242
|
+
if (input[position.position] !== 0x3d /* = */) {
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
position.position++ // skip =
|
|
246
|
+
|
|
247
|
+
// Skip whitespace
|
|
248
|
+
collectASequenceOfBytes(
|
|
249
|
+
(char) => char === 0x20 || char === 0x09,
|
|
250
|
+
input,
|
|
251
|
+
position
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
let value
|
|
255
|
+
|
|
256
|
+
if (isExtended) {
|
|
257
|
+
// Extended attribute format: charset'language'encoded-value
|
|
258
|
+
const headerValue = collectASequenceOfBytes(
|
|
259
|
+
(char) => char !== 0x20 && char !== 0x0d && char !== 0x0a && char !== 0x3b, // not space, CRLF, or ;
|
|
260
|
+
input,
|
|
261
|
+
position
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
// Check for utf-8'' prefix (case insensitive)
|
|
265
|
+
if (
|
|
266
|
+
(headerValue[0] !== 0x75 && headerValue[0] !== 0x55) || // u or U
|
|
267
|
+
(headerValue[1] !== 0x74 && headerValue[1] !== 0x54) || // t or T
|
|
268
|
+
(headerValue[2] !== 0x66 && headerValue[2] !== 0x46) || // f or F
|
|
269
|
+
headerValue[3] !== 0x2d || // -
|
|
270
|
+
headerValue[4] !== 0x38 // 8
|
|
271
|
+
) {
|
|
272
|
+
throw parsingError('unknown encoding, expected utf-8\'\'')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Skip utf-8'' and decode the rest
|
|
276
|
+
value = decodeURIComponent(decoder.decode(headerValue.subarray(7)))
|
|
277
|
+
} else if (input[position.position] === 0x22 /* " */) {
|
|
278
|
+
// Quoted string
|
|
279
|
+
position.position++ // skip opening quote
|
|
280
|
+
|
|
281
|
+
const quotedValue = collectASequenceOfBytes(
|
|
282
|
+
(char) => char !== 0x0a && char !== 0x0d && char !== 0x22, // not LF, CR, or "
|
|
283
|
+
input,
|
|
284
|
+
position
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if (input[position.position] !== 0x22) {
|
|
288
|
+
throw parsingError('Closing quote not found')
|
|
289
|
+
}
|
|
290
|
+
position.position++ // skip closing quote
|
|
291
|
+
|
|
292
|
+
value = decoder.decode(quotedValue)
|
|
293
|
+
.replace(/%0A/ig, '\n')
|
|
294
|
+
.replace(/%0D/ig, '\r')
|
|
295
|
+
.replace(/%22/g, '"')
|
|
296
|
+
} else {
|
|
297
|
+
// Token value (no quotes)
|
|
298
|
+
const tokenValue = collectASequenceOfBytes(
|
|
299
|
+
(char) => isToken(char) && char !== 0x3b, // not ;
|
|
300
|
+
input,
|
|
301
|
+
position
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
value = decoder.decode(tokenValue)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { name: attrNameStr, value }
|
|
308
|
+
}
|
|
309
|
+
|
|
208
310
|
/**
|
|
209
311
|
* @see https://andreubotella.github.io/multipart-form-data/#parse-multipart-form-data-headers
|
|
210
312
|
* @param {Buffer} input
|
|
@@ -265,80 +367,40 @@ function parseMultipartFormDataHeaders (input, position) {
|
|
|
265
367
|
// 2.8. Byte-lowercase header name and switch on the result:
|
|
266
368
|
switch (bufferToLowerCasedHeaderName(headerName)) {
|
|
267
369
|
case 'content-disposition': {
|
|
268
|
-
// 1. Set name and filename to null.
|
|
269
370
|
name = filename = null
|
|
270
371
|
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
372
|
+
// Collect the disposition type (should be "form-data")
|
|
373
|
+
const dispositionType = collectASequenceOfBytes(
|
|
374
|
+
(char) => isToken(char),
|
|
375
|
+
input,
|
|
376
|
+
position
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if (dispositionType.toString('ascii').toLowerCase() !== 'form-data') {
|
|
380
|
+
throw parsingError('expected form-data for content-disposition header')
|
|
275
381
|
}
|
|
276
382
|
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (input[position.position] === 0x3b /* ; */ && input[position.position + 1] === 0x20 /* ' ' */) {
|
|
288
|
-
const at = { position: position.position + 2 }
|
|
289
|
-
|
|
290
|
-
if (bufferStartsWith(input, filenameBuffer, at)) {
|
|
291
|
-
if (input[at.position + 8] === 0x2a /* '*' */) {
|
|
292
|
-
at.position += 10 // skip past filename*=
|
|
293
|
-
|
|
294
|
-
// Remove leading http tab and spaces. See RFC for examples.
|
|
295
|
-
// https://datatracker.ietf.org/doc/html/rfc6266#section-5
|
|
296
|
-
collectASequenceOfBytes(
|
|
297
|
-
(char) => char === 0x20 || char === 0x09,
|
|
298
|
-
input,
|
|
299
|
-
at
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
const headerValue = collectASequenceOfBytes(
|
|
303
|
-
(char) => char !== 0x20 && char !== 0x0d && char !== 0x0a, // ' ' or CRLF
|
|
304
|
-
input,
|
|
305
|
-
at
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
(headerValue[0] !== 0x75 && headerValue[0] !== 0x55) || // u or U
|
|
310
|
-
(headerValue[1] !== 0x74 && headerValue[1] !== 0x54) || // t or T
|
|
311
|
-
(headerValue[2] !== 0x66 && headerValue[2] !== 0x46) || // f or F
|
|
312
|
-
headerValue[3] !== 0x2d || // -
|
|
313
|
-
headerValue[4] !== 0x38 // 8
|
|
314
|
-
) {
|
|
315
|
-
throw parsingError('unknown encoding, expected utf-8\'\'')
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// skip utf-8''
|
|
319
|
-
filename = decodeURIComponent(new TextDecoder().decode(headerValue.subarray(7)))
|
|
320
|
-
|
|
321
|
-
position.position = at.position
|
|
322
|
-
} else {
|
|
323
|
-
// 1. Advance position so it points at the byte after the next 0x22 (") byte
|
|
324
|
-
// (the one in the sequence of bytes matched above).
|
|
325
|
-
position.position += 11
|
|
326
|
-
|
|
327
|
-
// Remove leading http tab and spaces. See RFC for examples.
|
|
328
|
-
// https://datatracker.ietf.org/doc/html/rfc6266#section-5
|
|
329
|
-
collectASequenceOfBytes(
|
|
330
|
-
(char) => char === 0x20 || char === 0x09,
|
|
331
|
-
input,
|
|
332
|
-
position
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
position.position++ // skip past " after removing whitespace
|
|
336
|
-
|
|
337
|
-
// 2. Set filename to the result of parsing a multipart/form-data name given
|
|
338
|
-
// input and position, if the result is not failure. Otherwise, return failure.
|
|
339
|
-
filename = parseMultipartFormDataName(input, position)
|
|
340
|
-
}
|
|
383
|
+
// Parse attributes recursively until CRLF
|
|
384
|
+
while (
|
|
385
|
+
position.position < input.length &&
|
|
386
|
+
input[position.position] !== 0x0d &&
|
|
387
|
+
input[position.position + 1] !== 0x0a
|
|
388
|
+
) {
|
|
389
|
+
const attribute = parseContentDispositionAttribute(input, position)
|
|
390
|
+
|
|
391
|
+
if (!attribute) {
|
|
392
|
+
break
|
|
341
393
|
}
|
|
394
|
+
|
|
395
|
+
if (attribute.name === 'name') {
|
|
396
|
+
name = attribute.value
|
|
397
|
+
} else if (attribute.name === 'filename') {
|
|
398
|
+
filename = attribute.value
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (name === null) {
|
|
403
|
+
throw parsingError('name attribute is required in content-disposition header')
|
|
342
404
|
}
|
|
343
405
|
|
|
344
406
|
break
|
|
@@ -394,43 +456,6 @@ function parseMultipartFormDataHeaders (input, position) {
|
|
|
394
456
|
}
|
|
395
457
|
}
|
|
396
458
|
|
|
397
|
-
/**
|
|
398
|
-
* @see https://andreubotella.github.io/multipart-form-data/#parse-a-multipart-form-data-name
|
|
399
|
-
* @param {Buffer} input
|
|
400
|
-
* @param {{ position: number }} position
|
|
401
|
-
*/
|
|
402
|
-
function parseMultipartFormDataName (input, position) {
|
|
403
|
-
// 1. Assert: The byte at (position - 1) is 0x22 (").
|
|
404
|
-
assert(input[position.position - 1] === 0x22)
|
|
405
|
-
|
|
406
|
-
// 2. Let name be the result of collecting a sequence of bytes that are not 0x0A (LF), 0x0D (CR) or 0x22 ("), given position.
|
|
407
|
-
/** @type {string | Buffer} */
|
|
408
|
-
let name = collectASequenceOfBytes(
|
|
409
|
-
(char) => char !== 0x0a && char !== 0x0d && char !== 0x22,
|
|
410
|
-
input,
|
|
411
|
-
position
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
// 3. If the byte at position is not 0x22 ("), return failure. Otherwise, advance position by 1.
|
|
415
|
-
if (input[position.position] !== 0x22) {
|
|
416
|
-
throw parsingError('expected "')
|
|
417
|
-
} else {
|
|
418
|
-
position.position++
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// 4. Replace any occurrence of the following subsequences in name with the given byte:
|
|
422
|
-
// - `%0A`: 0x0A (LF)
|
|
423
|
-
// - `%0D`: 0x0D (CR)
|
|
424
|
-
// - `%22`: 0x22 (")
|
|
425
|
-
name = new TextDecoder().decode(name)
|
|
426
|
-
.replace(/%0A/ig, '\n')
|
|
427
|
-
.replace(/%0D/ig, '\r')
|
|
428
|
-
.replace(/%22/g, '"')
|
|
429
|
-
|
|
430
|
-
// 5. Return the UTF-8 decoding without BOM of name.
|
|
431
|
-
return name
|
|
432
|
-
}
|
|
433
|
-
|
|
434
459
|
/**
|
|
435
460
|
* @param {(char: number) => boolean} condition
|
|
436
461
|
* @param {Buffer} input
|
|
@@ -492,6 +517,58 @@ function parsingError (cause) {
|
|
|
492
517
|
return new TypeError('Failed to parse body as FormData.', { cause: new TypeError(cause) })
|
|
493
518
|
}
|
|
494
519
|
|
|
520
|
+
/**
|
|
521
|
+
* CTL = <any US-ASCII control character
|
|
522
|
+
* (octets 0 - 31) and DEL (127)>
|
|
523
|
+
* @param {number} char
|
|
524
|
+
*/
|
|
525
|
+
function isCTL (char) {
|
|
526
|
+
return char <= 0x1f || char === 0x7f
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* tspecials := "(" / ")" / "<" / ">" / "@" /
|
|
531
|
+
* "," / ";" / ":" / "\" / <">
|
|
532
|
+
* "/" / "[" / "]" / "?" / "="
|
|
533
|
+
* ; Must be in quoted-string,
|
|
534
|
+
* ; to use within parameter values
|
|
535
|
+
* @param {number} char
|
|
536
|
+
*/
|
|
537
|
+
function isTSpecial (char) {
|
|
538
|
+
return (
|
|
539
|
+
char === 0x28 || // (
|
|
540
|
+
char === 0x29 || // )
|
|
541
|
+
char === 0x3c || // <
|
|
542
|
+
char === 0x3e || // >
|
|
543
|
+
char === 0x40 || // @
|
|
544
|
+
char === 0x2c || // ,
|
|
545
|
+
char === 0x3b || // ;
|
|
546
|
+
char === 0x3a || // :
|
|
547
|
+
char === 0x5c || // \
|
|
548
|
+
char === 0x22 || // "
|
|
549
|
+
char === 0x2f || // /
|
|
550
|
+
char === 0x5b || // [
|
|
551
|
+
char === 0x5d || // ]
|
|
552
|
+
char === 0x3f || // ?
|
|
553
|
+
char === 0x3d // +
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
|
|
559
|
+
* or tspecials>
|
|
560
|
+
* @param {number} char
|
|
561
|
+
*/
|
|
562
|
+
function isToken (char) {
|
|
563
|
+
return (
|
|
564
|
+
char <= 0x7f && // ascii
|
|
565
|
+
char !== 0x20 && // space
|
|
566
|
+
char !== 0x09 &&
|
|
567
|
+
!isCTL(char) &&
|
|
568
|
+
!isTSpecial(char)
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
|
|
495
572
|
module.exports = {
|
|
496
573
|
multipartFormDataParser,
|
|
497
574
|
validateBoundary
|
package/lib/web/fetch/index.js
CHANGED
|
@@ -35,7 +35,6 @@ const {
|
|
|
35
35
|
isErrorLike,
|
|
36
36
|
fullyReadBody,
|
|
37
37
|
readableStreamClose,
|
|
38
|
-
isomorphicEncode,
|
|
39
38
|
urlIsLocal,
|
|
40
39
|
urlIsHttpHttpsScheme,
|
|
41
40
|
urlHasHttpsScheme,
|
|
@@ -63,8 +62,11 @@ const { webidl } = require('../webidl')
|
|
|
63
62
|
const { STATUS_CODES } = require('node:http')
|
|
64
63
|
const { bytesMatch } = require('../subresource-integrity/subresource-integrity')
|
|
65
64
|
const { createDeferredPromise } = require('../../util/promise')
|
|
65
|
+
const { isomorphicEncode } = require('../infra')
|
|
66
|
+
const { runtimeFeatures } = require('../../util/runtime-features')
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
// Node.js v23.8.0+ and v22.15.0+ supports Zstandard
|
|
69
|
+
const hasZstd = runtimeFeatures.has('zstd')
|
|
68
70
|
|
|
69
71
|
const GET_OR_HEAD = ['GET', 'HEAD']
|
|
70
72
|
|
|
@@ -887,7 +889,7 @@ function schemeFetch (fetchParams) {
|
|
|
887
889
|
|
|
888
890
|
// 8. Let slicedBlob be the result of invoking slice blob given blob, rangeStart,
|
|
889
891
|
// rangeEnd + 1, and type.
|
|
890
|
-
const slicedBlob = blob.slice(rangeStart, rangeEnd, type)
|
|
892
|
+
const slicedBlob = blob.slice(rangeStart, rangeEnd + 1, type)
|
|
891
893
|
|
|
892
894
|
// 9. Let slicedBodyWithType be the result of safely extracting slicedBlob.
|
|
893
895
|
// Note: same reason as mentioned above as to why we use extractBody
|
|
@@ -2128,6 +2130,15 @@ async function httpNetworkFetch (
|
|
|
2128
2130
|
// "All content-coding values are case-insensitive..."
|
|
2129
2131
|
/** @type {string[]} */
|
|
2130
2132
|
const codings = contentEncoding ? contentEncoding.toLowerCase().split(',') : []
|
|
2133
|
+
|
|
2134
|
+
// Limit the number of content-encodings to prevent resource exhaustion.
|
|
2135
|
+
// CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
|
|
2136
|
+
const maxContentEncodings = 5
|
|
2137
|
+
if (codings.length > maxContentEncodings) {
|
|
2138
|
+
reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
|
|
2139
|
+
return true
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2131
2142
|
for (let i = codings.length - 1; i >= 0; --i) {
|
|
2132
2143
|
const coding = codings[i].trim()
|
|
2133
2144
|
// https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
|
|
@@ -2151,7 +2162,6 @@ async function httpNetworkFetch (
|
|
|
2151
2162
|
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
|
|
2152
2163
|
}))
|
|
2153
2164
|
} else if (coding === 'zstd' && hasZstd) {
|
|
2154
|
-
// Node.js v23.8.0+ and v22.15.0+ supports Zstandard
|
|
2155
2165
|
decoders.push(zlib.createZstdDecompress({
|
|
2156
2166
|
flush: zlib.constants.ZSTD_e_continue,
|
|
2157
2167
|
finishFlush: zlib.constants.ZSTD_e_end
|
|
@@ -2227,8 +2237,10 @@ async function httpNetworkFetch (
|
|
|
2227
2237
|
},
|
|
2228
2238
|
|
|
2229
2239
|
onUpgrade (status, rawHeaders, socket) {
|
|
2230
|
-
|
|
2231
|
-
|
|
2240
|
+
// We need to support 200 for websocket over h2 as per RFC-8441
|
|
2241
|
+
// Absence of session means H1
|
|
2242
|
+
if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
|
|
2243
|
+
return false
|
|
2232
2244
|
}
|
|
2233
2245
|
|
|
2234
2246
|
const headersList = new HeadersList()
|
package/lib/web/fetch/request.js
CHANGED
|
@@ -1094,6 +1094,12 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([
|
|
|
1094
1094
|
{
|
|
1095
1095
|
key: 'dispatcher', // undici specific option
|
|
1096
1096
|
converter: webidl.converters.any
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
key: 'priority',
|
|
1100
|
+
converter: webidl.converters.DOMString,
|
|
1101
|
+
allowedValues: ['high', 'low', 'auto'],
|
|
1102
|
+
defaultValue: () => 'auto'
|
|
1097
1103
|
}
|
|
1098
1104
|
])
|
|
1099
1105
|
|
|
@@ -9,9 +9,7 @@ const {
|
|
|
9
9
|
isValidReasonPhrase,
|
|
10
10
|
isCancelled,
|
|
11
11
|
isAborted,
|
|
12
|
-
serializeJavascriptValueToJSONString,
|
|
13
12
|
isErrorLike,
|
|
14
|
-
isomorphicEncode,
|
|
15
13
|
environmentSettingsObject: relevantRealm
|
|
16
14
|
} = require('./util')
|
|
17
15
|
const {
|
|
@@ -22,6 +20,7 @@ const { webidl } = require('../webidl')
|
|
|
22
20
|
const { URLSerializer } = require('./data-url')
|
|
23
21
|
const { kConstruct } = require('../../core/symbols')
|
|
24
22
|
const assert = require('node:assert')
|
|
23
|
+
const { isomorphicEncode, serializeJavascriptValueToJSONString } = require('../infra')
|
|
25
24
|
|
|
26
25
|
const textEncoder = new TextEncoder('utf-8')
|
|
27
26
|
|
|
@@ -454,7 +453,7 @@ function filterResponse (response, type) {
|
|
|
454
453
|
|
|
455
454
|
return makeFilteredResponse(response, {
|
|
456
455
|
type: 'opaque',
|
|
457
|
-
urlList:
|
|
456
|
+
urlList: [],
|
|
458
457
|
status: 0,
|
|
459
458
|
statusText: '',
|
|
460
459
|
body: null
|
package/lib/web/fetch/util.js
CHANGED
|
@@ -4,12 +4,13 @@ const { Transform } = require('node:stream')
|
|
|
4
4
|
const zlib = require('node:zlib')
|
|
5
5
|
const { redirectStatusSet, referrerPolicyTokens, badPortsSet } = require('./constants')
|
|
6
6
|
const { getGlobalOrigin } = require('./global')
|
|
7
|
-
const {
|
|
7
|
+
const { collectAnHTTPQuotedString, parseMIMEType } = require('./data-url')
|
|
8
8
|
const { performance } = require('node:perf_hooks')
|
|
9
9
|
const { ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require('../../core/util')
|
|
10
10
|
const assert = require('node:assert')
|
|
11
11
|
const { isUint8Array } = require('node:util/types')
|
|
12
12
|
const { webidl } = require('../webidl')
|
|
13
|
+
const { isomorphicEncode, collectASequenceOfCodePoints, removeChars } = require('../infra')
|
|
13
14
|
|
|
14
15
|
function responseURL (response) {
|
|
15
16
|
// https://fetch.spec.whatwg.org/#responses
|
|
@@ -721,23 +722,6 @@ function normalizeMethod (method) {
|
|
|
721
722
|
return normalizedMethodRecordsBase[method.toLowerCase()] ?? method
|
|
722
723
|
}
|
|
723
724
|
|
|
724
|
-
// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
|
|
725
|
-
function serializeJavascriptValueToJSONString (value) {
|
|
726
|
-
// 1. Let result be ? Call(%JSON.stringify%, undefined, « value »).
|
|
727
|
-
const result = JSON.stringify(value)
|
|
728
|
-
|
|
729
|
-
// 2. If result is undefined, then throw a TypeError.
|
|
730
|
-
if (result === undefined) {
|
|
731
|
-
throw new TypeError('Value is not JSON serializable')
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// 3. Assert: result is a string.
|
|
735
|
-
assert(typeof result === 'string')
|
|
736
|
-
|
|
737
|
-
// 4. Return result.
|
|
738
|
-
return result
|
|
739
|
-
}
|
|
740
|
-
|
|
741
725
|
// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
|
|
742
726
|
const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
|
|
743
727
|
|
|
@@ -993,22 +977,6 @@ function readableStreamClose (controller) {
|
|
|
993
977
|
}
|
|
994
978
|
}
|
|
995
979
|
|
|
996
|
-
const invalidIsomorphicEncodeValueRegex = /[^\x00-\xFF]/ // eslint-disable-line
|
|
997
|
-
|
|
998
|
-
/**
|
|
999
|
-
* @see https://infra.spec.whatwg.org/#isomorphic-encode
|
|
1000
|
-
* @param {string} input
|
|
1001
|
-
*/
|
|
1002
|
-
function isomorphicEncode (input) {
|
|
1003
|
-
// 1. Assert: input contains no code points greater than U+00FF.
|
|
1004
|
-
assert(!invalidIsomorphicEncodeValueRegex.test(input))
|
|
1005
|
-
|
|
1006
|
-
// 2. Return a byte sequence whose length is equal to input’s code
|
|
1007
|
-
// point length and whose bytes have the same values as the
|
|
1008
|
-
// values of input’s code points, in the same order
|
|
1009
|
-
return input
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
980
|
/**
|
|
1013
981
|
* @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
|
|
1014
982
|
* @see https://streams.spec.whatwg.org/#read-loop
|
|
@@ -1461,34 +1429,6 @@ function getDecodeSplit (name, list) {
|
|
|
1461
1429
|
return gettingDecodingSplitting(value)
|
|
1462
1430
|
}
|
|
1463
1431
|
|
|
1464
|
-
const textDecoder = new TextDecoder()
|
|
1465
|
-
|
|
1466
|
-
/**
|
|
1467
|
-
* @see https://encoding.spec.whatwg.org/#utf-8-decode
|
|
1468
|
-
* @param {Buffer} buffer
|
|
1469
|
-
*/
|
|
1470
|
-
function utf8DecodeBytes (buffer) {
|
|
1471
|
-
if (buffer.length === 0) {
|
|
1472
|
-
return ''
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// 1. Let buffer be the result of peeking three bytes from
|
|
1476
|
-
// ioQueue, converted to a byte sequence.
|
|
1477
|
-
|
|
1478
|
-
// 2. If buffer is 0xEF 0xBB 0xBF, then read three
|
|
1479
|
-
// bytes from ioQueue. (Do nothing with those bytes.)
|
|
1480
|
-
if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
|
|
1481
|
-
buffer = buffer.subarray(3)
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
// 3. Process a queue with an instance of UTF-8’s
|
|
1485
|
-
// decoder, ioQueue, output, and "replacement".
|
|
1486
|
-
const output = textDecoder.decode(buffer)
|
|
1487
|
-
|
|
1488
|
-
// 4. Return output.
|
|
1489
|
-
return output
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
1432
|
class EnvironmentSettingsObjectBase {
|
|
1493
1433
|
get baseUrl () {
|
|
1494
1434
|
return getGlobalOrigin()
|
|
@@ -1534,7 +1474,6 @@ module.exports = {
|
|
|
1534
1474
|
isValidReasonPhrase,
|
|
1535
1475
|
sameOrigin,
|
|
1536
1476
|
normalizeMethod,
|
|
1537
|
-
serializeJavascriptValueToJSONString,
|
|
1538
1477
|
iteratorMixin,
|
|
1539
1478
|
createIterator,
|
|
1540
1479
|
isValidHeaderName,
|
|
@@ -1542,7 +1481,6 @@ module.exports = {
|
|
|
1542
1481
|
isErrorLike,
|
|
1543
1482
|
fullyReadBody,
|
|
1544
1483
|
readableStreamClose,
|
|
1545
|
-
isomorphicEncode,
|
|
1546
1484
|
urlIsLocal,
|
|
1547
1485
|
urlHasHttpsScheme,
|
|
1548
1486
|
urlIsHttpHttpsScheme,
|
|
@@ -1552,7 +1490,6 @@ module.exports = {
|
|
|
1552
1490
|
createInflate,
|
|
1553
1491
|
extractMimeType,
|
|
1554
1492
|
getDecodeSplit,
|
|
1555
|
-
utf8DecodeBytes,
|
|
1556
1493
|
environmentSettingsObject,
|
|
1557
1494
|
isOriginIPPotentiallyTrustworthy
|
|
1558
1495
|
}
|