undici 5.28.2 → 5.28.4
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/lib/core/constants.js +118 -0
- package/lib/core/util.js +11 -0
- package/lib/fetch/index.js +3 -0
- package/lib/fetch/util.js +108 -35
- package/lib/handler/RedirectHandler.js +11 -6
- package/package.json +2 -2
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/** @type {Record<string, string | undefined>} */
|
|
4
|
+
const headerNameLowerCasedRecord = {}
|
|
5
|
+
|
|
6
|
+
// https://developer.mozilla.org/docs/Web/HTTP/Headers
|
|
7
|
+
const wellknownHeaderNames = [
|
|
8
|
+
'Accept',
|
|
9
|
+
'Accept-Encoding',
|
|
10
|
+
'Accept-Language',
|
|
11
|
+
'Accept-Ranges',
|
|
12
|
+
'Access-Control-Allow-Credentials',
|
|
13
|
+
'Access-Control-Allow-Headers',
|
|
14
|
+
'Access-Control-Allow-Methods',
|
|
15
|
+
'Access-Control-Allow-Origin',
|
|
16
|
+
'Access-Control-Expose-Headers',
|
|
17
|
+
'Access-Control-Max-Age',
|
|
18
|
+
'Access-Control-Request-Headers',
|
|
19
|
+
'Access-Control-Request-Method',
|
|
20
|
+
'Age',
|
|
21
|
+
'Allow',
|
|
22
|
+
'Alt-Svc',
|
|
23
|
+
'Alt-Used',
|
|
24
|
+
'Authorization',
|
|
25
|
+
'Cache-Control',
|
|
26
|
+
'Clear-Site-Data',
|
|
27
|
+
'Connection',
|
|
28
|
+
'Content-Disposition',
|
|
29
|
+
'Content-Encoding',
|
|
30
|
+
'Content-Language',
|
|
31
|
+
'Content-Length',
|
|
32
|
+
'Content-Location',
|
|
33
|
+
'Content-Range',
|
|
34
|
+
'Content-Security-Policy',
|
|
35
|
+
'Content-Security-Policy-Report-Only',
|
|
36
|
+
'Content-Type',
|
|
37
|
+
'Cookie',
|
|
38
|
+
'Cross-Origin-Embedder-Policy',
|
|
39
|
+
'Cross-Origin-Opener-Policy',
|
|
40
|
+
'Cross-Origin-Resource-Policy',
|
|
41
|
+
'Date',
|
|
42
|
+
'Device-Memory',
|
|
43
|
+
'Downlink',
|
|
44
|
+
'ECT',
|
|
45
|
+
'ETag',
|
|
46
|
+
'Expect',
|
|
47
|
+
'Expect-CT',
|
|
48
|
+
'Expires',
|
|
49
|
+
'Forwarded',
|
|
50
|
+
'From',
|
|
51
|
+
'Host',
|
|
52
|
+
'If-Match',
|
|
53
|
+
'If-Modified-Since',
|
|
54
|
+
'If-None-Match',
|
|
55
|
+
'If-Range',
|
|
56
|
+
'If-Unmodified-Since',
|
|
57
|
+
'Keep-Alive',
|
|
58
|
+
'Last-Modified',
|
|
59
|
+
'Link',
|
|
60
|
+
'Location',
|
|
61
|
+
'Max-Forwards',
|
|
62
|
+
'Origin',
|
|
63
|
+
'Permissions-Policy',
|
|
64
|
+
'Pragma',
|
|
65
|
+
'Proxy-Authenticate',
|
|
66
|
+
'Proxy-Authorization',
|
|
67
|
+
'RTT',
|
|
68
|
+
'Range',
|
|
69
|
+
'Referer',
|
|
70
|
+
'Referrer-Policy',
|
|
71
|
+
'Refresh',
|
|
72
|
+
'Retry-After',
|
|
73
|
+
'Sec-WebSocket-Accept',
|
|
74
|
+
'Sec-WebSocket-Extensions',
|
|
75
|
+
'Sec-WebSocket-Key',
|
|
76
|
+
'Sec-WebSocket-Protocol',
|
|
77
|
+
'Sec-WebSocket-Version',
|
|
78
|
+
'Server',
|
|
79
|
+
'Server-Timing',
|
|
80
|
+
'Service-Worker-Allowed',
|
|
81
|
+
'Service-Worker-Navigation-Preload',
|
|
82
|
+
'Set-Cookie',
|
|
83
|
+
'SourceMap',
|
|
84
|
+
'Strict-Transport-Security',
|
|
85
|
+
'Supports-Loading-Mode',
|
|
86
|
+
'TE',
|
|
87
|
+
'Timing-Allow-Origin',
|
|
88
|
+
'Trailer',
|
|
89
|
+
'Transfer-Encoding',
|
|
90
|
+
'Upgrade',
|
|
91
|
+
'Upgrade-Insecure-Requests',
|
|
92
|
+
'User-Agent',
|
|
93
|
+
'Vary',
|
|
94
|
+
'Via',
|
|
95
|
+
'WWW-Authenticate',
|
|
96
|
+
'X-Content-Type-Options',
|
|
97
|
+
'X-DNS-Prefetch-Control',
|
|
98
|
+
'X-Frame-Options',
|
|
99
|
+
'X-Permitted-Cross-Domain-Policies',
|
|
100
|
+
'X-Powered-By',
|
|
101
|
+
'X-Requested-With',
|
|
102
|
+
'X-XSS-Protection'
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
|
|
106
|
+
const key = wellknownHeaderNames[i]
|
|
107
|
+
const lowerCasedKey = key.toLowerCase()
|
|
108
|
+
headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] =
|
|
109
|
+
lowerCasedKey
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
|
|
113
|
+
Object.setPrototypeOf(headerNameLowerCasedRecord, null)
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
wellknownHeaderNames,
|
|
117
|
+
headerNameLowerCasedRecord
|
|
118
|
+
}
|
package/lib/core/util.js
CHANGED
|
@@ -9,6 +9,7 @@ const { InvalidArgumentError } = require('./errors')
|
|
|
9
9
|
const { Blob } = require('buffer')
|
|
10
10
|
const nodeUtil = require('util')
|
|
11
11
|
const { stringify } = require('querystring')
|
|
12
|
+
const { headerNameLowerCasedRecord } = require('./constants')
|
|
12
13
|
|
|
13
14
|
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
|
|
14
15
|
|
|
@@ -218,6 +219,15 @@ function parseKeepAliveTimeout (val) {
|
|
|
218
219
|
return m ? parseInt(m[1], 10) * 1000 : null
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Retrieves a header name and returns its lowercase value.
|
|
224
|
+
* @param {string | Buffer} value Header name
|
|
225
|
+
* @returns {string}
|
|
226
|
+
*/
|
|
227
|
+
function headerNameToString (value) {
|
|
228
|
+
return headerNameLowerCasedRecord[value] || value.toLowerCase()
|
|
229
|
+
}
|
|
230
|
+
|
|
221
231
|
function parseHeaders (headers, obj = {}) {
|
|
222
232
|
// For H2 support
|
|
223
233
|
if (!Array.isArray(headers)) return headers
|
|
@@ -489,6 +499,7 @@ module.exports = {
|
|
|
489
499
|
isIterable,
|
|
490
500
|
isAsyncIterable,
|
|
491
501
|
isDestroyed,
|
|
502
|
+
headerNameToString,
|
|
492
503
|
parseRawHeaders,
|
|
493
504
|
parseHeaders,
|
|
494
505
|
parseKeepAliveTimeout,
|
package/lib/fetch/index.js
CHANGED
|
@@ -1203,6 +1203,9 @@ function httpRedirectFetch (fetchParams, response) {
|
|
|
1203
1203
|
// https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name
|
|
1204
1204
|
request.headersList.delete('authorization')
|
|
1205
1205
|
|
|
1206
|
+
// https://fetch.spec.whatwg.org/#authentication-entries
|
|
1207
|
+
request.headersList.delete('proxy-authorization', true)
|
|
1208
|
+
|
|
1206
1209
|
// "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement.
|
|
1207
1210
|
request.headersList.delete('cookie')
|
|
1208
1211
|
request.headersList.delete('host')
|
package/lib/fetch/util.js
CHANGED
|
@@ -7,14 +7,18 @@ const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
|
|
|
7
7
|
const assert = require('assert')
|
|
8
8
|
const { isUint8Array } = require('util/types')
|
|
9
9
|
|
|
10
|
+
let supportedHashes = []
|
|
11
|
+
|
|
10
12
|
// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
|
|
11
13
|
/** @type {import('crypto')|undefined} */
|
|
12
14
|
let crypto
|
|
13
15
|
|
|
14
16
|
try {
|
|
15
17
|
crypto = require('crypto')
|
|
18
|
+
const possibleRelevantHashes = ['sha256', 'sha384', 'sha512']
|
|
19
|
+
supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash))
|
|
20
|
+
/* c8 ignore next 3 */
|
|
16
21
|
} catch {
|
|
17
|
-
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
function responseURL (response) {
|
|
@@ -542,66 +546,56 @@ function bytesMatch (bytes, metadataList) {
|
|
|
542
546
|
return true
|
|
543
547
|
}
|
|
544
548
|
|
|
545
|
-
// 3. If
|
|
549
|
+
// 3. If response is not eligible for integrity validation, return false.
|
|
550
|
+
// TODO
|
|
551
|
+
|
|
552
|
+
// 4. If parsedMetadata is the empty set, return true.
|
|
546
553
|
if (parsedMetadata.length === 0) {
|
|
547
554
|
return true
|
|
548
555
|
}
|
|
549
556
|
|
|
550
|
-
//
|
|
557
|
+
// 5. Let metadata be the result of getting the strongest
|
|
551
558
|
// metadata from parsedMetadata.
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
const strongest = list[0].algo
|
|
555
|
-
// get all entries that use the strongest algorithm; ignore weaker
|
|
556
|
-
const metadata = list.filter((item) => item.algo === strongest)
|
|
559
|
+
const strongest = getStrongestMetadata(parsedMetadata)
|
|
560
|
+
const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest)
|
|
557
561
|
|
|
558
|
-
//
|
|
562
|
+
// 6. For each item in metadata:
|
|
559
563
|
for (const item of metadata) {
|
|
560
564
|
// 1. Let algorithm be the alg component of item.
|
|
561
565
|
const algorithm = item.algo
|
|
562
566
|
|
|
563
567
|
// 2. Let expectedValue be the val component of item.
|
|
564
|
-
|
|
568
|
+
const expectedValue = item.hash
|
|
565
569
|
|
|
566
570
|
// See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
|
|
567
571
|
// "be liberal with padding". This is annoying, and it's not even in the spec.
|
|
568
572
|
|
|
569
|
-
if (expectedValue.endsWith('==')) {
|
|
570
|
-
expectedValue = expectedValue.slice(0, -2)
|
|
571
|
-
}
|
|
572
|
-
|
|
573
573
|
// 3. Let actualValue be the result of applying algorithm to bytes.
|
|
574
574
|
let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64')
|
|
575
575
|
|
|
576
|
-
if (actualValue.
|
|
577
|
-
|
|
576
|
+
if (actualValue[actualValue.length - 1] === '=') {
|
|
577
|
+
if (actualValue[actualValue.length - 2] === '=') {
|
|
578
|
+
actualValue = actualValue.slice(0, -2)
|
|
579
|
+
} else {
|
|
580
|
+
actualValue = actualValue.slice(0, -1)
|
|
581
|
+
}
|
|
578
582
|
}
|
|
579
583
|
|
|
580
584
|
// 4. If actualValue is a case-sensitive match for expectedValue,
|
|
581
585
|
// return true.
|
|
582
|
-
if (actualValue
|
|
583
|
-
return true
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
let actualBase64URL = crypto.createHash(algorithm).update(bytes).digest('base64url')
|
|
587
|
-
|
|
588
|
-
if (actualBase64URL.endsWith('==')) {
|
|
589
|
-
actualBase64URL = actualBase64URL.slice(0, -2)
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
if (actualBase64URL === expectedValue) {
|
|
586
|
+
if (compareBase64Mixed(actualValue, expectedValue)) {
|
|
593
587
|
return true
|
|
594
588
|
}
|
|
595
589
|
}
|
|
596
590
|
|
|
597
|
-
//
|
|
591
|
+
// 7. Return false.
|
|
598
592
|
return false
|
|
599
593
|
}
|
|
600
594
|
|
|
601
595
|
// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
|
|
602
596
|
// https://www.w3.org/TR/CSP2/#source-list-syntax
|
|
603
597
|
// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
|
|
604
|
-
const parseHashWithOptions = /(
|
|
598
|
+
const parseHashWithOptions = /(?<algo>sha256|sha384|sha512)-((?<hash>[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i
|
|
605
599
|
|
|
606
600
|
/**
|
|
607
601
|
* @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
|
|
@@ -615,8 +609,6 @@ function parseMetadata (metadata) {
|
|
|
615
609
|
// 2. Let empty be equal to true.
|
|
616
610
|
let empty = true
|
|
617
611
|
|
|
618
|
-
const supportedHashes = crypto.getHashes()
|
|
619
|
-
|
|
620
612
|
// 3. For each token returned by splitting metadata on spaces:
|
|
621
613
|
for (const token of metadata.split(' ')) {
|
|
622
614
|
// 1. Set empty to false.
|
|
@@ -626,7 +618,11 @@ function parseMetadata (metadata) {
|
|
|
626
618
|
const parsedToken = parseHashWithOptions.exec(token)
|
|
627
619
|
|
|
628
620
|
// 3. If token does not parse, continue to the next token.
|
|
629
|
-
if (
|
|
621
|
+
if (
|
|
622
|
+
parsedToken === null ||
|
|
623
|
+
parsedToken.groups === undefined ||
|
|
624
|
+
parsedToken.groups.algo === undefined
|
|
625
|
+
) {
|
|
630
626
|
// Note: Chromium blocks the request at this point, but Firefox
|
|
631
627
|
// gives a warning that an invalid integrity was given. The
|
|
632
628
|
// correct behavior is to ignore these, and subsequently not
|
|
@@ -635,11 +631,11 @@ function parseMetadata (metadata) {
|
|
|
635
631
|
}
|
|
636
632
|
|
|
637
633
|
// 4. Let algorithm be the hash-algo component of token.
|
|
638
|
-
const algorithm = parsedToken.groups.algo
|
|
634
|
+
const algorithm = parsedToken.groups.algo.toLowerCase()
|
|
639
635
|
|
|
640
636
|
// 5. If algorithm is a hash function recognized by the user
|
|
641
637
|
// agent, add the parsed token to result.
|
|
642
|
-
if (supportedHashes.includes(algorithm
|
|
638
|
+
if (supportedHashes.includes(algorithm)) {
|
|
643
639
|
result.push(parsedToken.groups)
|
|
644
640
|
}
|
|
645
641
|
}
|
|
@@ -652,6 +648,82 @@ function parseMetadata (metadata) {
|
|
|
652
648
|
return result
|
|
653
649
|
}
|
|
654
650
|
|
|
651
|
+
/**
|
|
652
|
+
* @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList
|
|
653
|
+
*/
|
|
654
|
+
function getStrongestMetadata (metadataList) {
|
|
655
|
+
// Let algorithm be the algo component of the first item in metadataList.
|
|
656
|
+
// Can be sha256
|
|
657
|
+
let algorithm = metadataList[0].algo
|
|
658
|
+
// If the algorithm is sha512, then it is the strongest
|
|
659
|
+
// and we can return immediately
|
|
660
|
+
if (algorithm[3] === '5') {
|
|
661
|
+
return algorithm
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
for (let i = 1; i < metadataList.length; ++i) {
|
|
665
|
+
const metadata = metadataList[i]
|
|
666
|
+
// If the algorithm is sha512, then it is the strongest
|
|
667
|
+
// and we can break the loop immediately
|
|
668
|
+
if (metadata.algo[3] === '5') {
|
|
669
|
+
algorithm = 'sha512'
|
|
670
|
+
break
|
|
671
|
+
// If the algorithm is sha384, then a potential sha256 or sha384 is ignored
|
|
672
|
+
} else if (algorithm[3] === '3') {
|
|
673
|
+
continue
|
|
674
|
+
// algorithm is sha256, check if algorithm is sha384 and if so, set it as
|
|
675
|
+
// the strongest
|
|
676
|
+
} else if (metadata.algo[3] === '3') {
|
|
677
|
+
algorithm = 'sha384'
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return algorithm
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function filterMetadataListByAlgorithm (metadataList, algorithm) {
|
|
684
|
+
if (metadataList.length === 1) {
|
|
685
|
+
return metadataList
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
let pos = 0
|
|
689
|
+
for (let i = 0; i < metadataList.length; ++i) {
|
|
690
|
+
if (metadataList[i].algo === algorithm) {
|
|
691
|
+
metadataList[pos++] = metadataList[i]
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
metadataList.length = pos
|
|
696
|
+
|
|
697
|
+
return metadataList
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Compares two base64 strings, allowing for base64url
|
|
702
|
+
* in the second string.
|
|
703
|
+
*
|
|
704
|
+
* @param {string} actualValue always base64
|
|
705
|
+
* @param {string} expectedValue base64 or base64url
|
|
706
|
+
* @returns {boolean}
|
|
707
|
+
*/
|
|
708
|
+
function compareBase64Mixed (actualValue, expectedValue) {
|
|
709
|
+
if (actualValue.length !== expectedValue.length) {
|
|
710
|
+
return false
|
|
711
|
+
}
|
|
712
|
+
for (let i = 0; i < actualValue.length; ++i) {
|
|
713
|
+
if (actualValue[i] !== expectedValue[i]) {
|
|
714
|
+
if (
|
|
715
|
+
(actualValue[i] === '+' && expectedValue[i] === '-') ||
|
|
716
|
+
(actualValue[i] === '/' && expectedValue[i] === '_')
|
|
717
|
+
) {
|
|
718
|
+
continue
|
|
719
|
+
}
|
|
720
|
+
return false
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return true
|
|
725
|
+
}
|
|
726
|
+
|
|
655
727
|
// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
|
|
656
728
|
function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
|
|
657
729
|
// TODO
|
|
@@ -1067,5 +1139,6 @@ module.exports = {
|
|
|
1067
1139
|
urlHasHttpsScheme,
|
|
1068
1140
|
urlIsHttpHttpsScheme,
|
|
1069
1141
|
readAllBytes,
|
|
1070
|
-
normalizeMethodRecord
|
|
1142
|
+
normalizeMethodRecord,
|
|
1143
|
+
parseMetadata
|
|
1071
1144
|
}
|
|
@@ -184,12 +184,17 @@ function parseLocation (statusCode, headers) {
|
|
|
184
184
|
|
|
185
185
|
// https://tools.ietf.org/html/rfc7231#section-6.4.4
|
|
186
186
|
function shouldRemoveHeader (header, removeContent, unknownOrigin) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
if (header.length === 4) {
|
|
188
|
+
return util.headerNameToString(header) === 'host'
|
|
189
|
+
}
|
|
190
|
+
if (removeContent && util.headerNameToString(header).startsWith('content-')) {
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) {
|
|
194
|
+
const name = util.headerNameToString(header)
|
|
195
|
+
return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization'
|
|
196
|
+
}
|
|
197
|
+
return false
|
|
193
198
|
}
|
|
194
199
|
|
|
195
200
|
// https://tools.ietf.org/html/rfc7231#section-6.4
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "5.28.
|
|
3
|
+
"version": "5.28.4",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"dns-packet": "^5.4.0",
|
|
111
111
|
"docsify-cli": "^4.4.3",
|
|
112
112
|
"form-data": "^4.0.0",
|
|
113
|
-
"formdata-node": "^
|
|
113
|
+
"formdata-node": "^4.3.1",
|
|
114
114
|
"https-pem": "^3.0.0",
|
|
115
115
|
"husky": "^8.0.1",
|
|
116
116
|
"import-fresh": "^3.3.0",
|