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.
@@ -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,
@@ -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 parsedMetadata is the empty set, return true.
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
- // 4. Let metadata be the result of getting the strongest
557
+ // 5. Let metadata be the result of getting the strongest
551
558
  // metadata from parsedMetadata.
552
- const list = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo))
553
- // get the strongest algorithm
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
- // 5. For each item in metadata:
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
- let expectedValue = item.hash
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.endsWith('==')) {
577
- actualValue = actualValue.slice(0, -2)
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 === expectedValue) {
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
- // 6. Return false.
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 = /((?<algo>sha256|sha384|sha512)-(?<hash>[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i
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 (parsedToken === null || parsedToken.groups === undefined) {
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.toLowerCase())) {
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
- return (
188
- (header.length === 4 && header.toString().toLowerCase() === 'host') ||
189
- (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) ||
190
- (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') ||
191
- (unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie')
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.2",
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": "^6.0.3",
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",