codekingpro/portable-devtools
114k
1'use strict'
2
3const { Transform } = require('node:stream')
4const zlib = require('node:zlib')
5const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require('./constants')
6const { getGlobalOrigin } = require('./global')
7const { collectASequenceOfCodePoints, collectAnHTTPQuotedString, removeChars, parseMIMEType } = require('./data-url')
8const { performance } = require('node:perf_hooks')
9const { isBlobLike, ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require('../../core/util')
10const assert = require('node:assert')
11const { isUint8Array } = require('node:util/types')
12const { webidl } = require('./webidl')
13
14let supportedHashes = []
15
16// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
17/** @type {import('crypto')} */
18let crypto
19try {
20 crypto = require('node:crypto')
21 const possibleRelevantHashes = ['sha256', 'sha384', 'sha512']
22 supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash))
23/* c8 ignore next 3 */
24} catch {
25
26}
27
28function responseURL (response) {
29 // https://fetch.spec.whatwg.org/#responses
30 // A response has an associated URL. It is a pointer to the last URL
31 // in response’s URL list and null if response’s URL list is empty.
32 const urlList = response.urlList
33 const length = urlList.length
34 return length === 0 ? null : urlList[length - 1].toString()
35}
36
37// https://fetch.spec.whatwg.org/#concept-response-location-url
38function responseLocationURL (response, requestFragment) {
39 // 1. If response’s status is not a redirect status, then return null.
40 if (!redirectStatusSet.has(response.status)) {
41 return null
42 }
43
44 // 2. Let location be the result of extracting header list values given
45 // `Location` and response’s header list.
46 let location = response.headersList.get('location', true)
47
48 // 3. If location is a header value, then set location to the result of
49 // parsing location with response’s URL.
50 if (location !== null && isValidHeaderValue(location)) {
51 if (!isValidEncodedURL(location)) {
52 // Some websites respond location header in UTF-8 form without encoding them as ASCII
53 // and major browsers redirect them to correctly UTF-8 encoded addresses.
54 // Here, we handle that behavior in the same way.
55 location = normalizeBinaryStringToUtf8(location)
56 }
57 location = new URL(location, responseURL(response))
58 }
59
60 // 4. If location is a URL whose fragment is null, then set location’s
61 // fragment to requestFragment.
62 if (location && !location.hash) {
63 location.hash = requestFragment
64 }
65
66 // 5. Return location.
67 return location
68}
69
70/**
71 * @see https://www.rfc-editor.org/rfc/rfc1738#section-2.2
72 * @param {string} url
73 * @returns {boolean}
74 */
75function isValidEncodedURL (url) {
76 for (let i = 0; i < url.length; ++i) {
77 const code = url.charCodeAt(i)
78
79 if (
80 code > 0x7E || // Non-US-ASCII + DEL
81 code < 0x20 // Control characters NUL - US
82 ) {
83 return false
84 }
85 }
86 return true
87}
88
89/**
90 * If string contains non-ASCII characters, assumes it's UTF-8 encoded and decodes it.
91 * Since UTF-8 is a superset of ASCII, this will work for ASCII strings as well.
92 * @param {string} value
93 * @returns {string}
94 */
95function normalizeBinaryStringToUtf8 (value) {
96 return Buffer.from(value, 'binary').toString('utf8')
97}
98
99/** @returns {URL} */
100function requestCurrentURL (request) {
101 return request.urlList[request.urlList.length - 1]
102}
103
104function requestBadPort (request) {
105 // 1. Let url be request’s current URL.
106 const url = requestCurrentURL(request)
107
108 // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port,
109 // then return blocked.
110 if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) {
111 return 'blocked'
112 }
113
114 // 3. Return allowed.
115 return 'allowed'
116}
117
118function isErrorLike (object) {
119 return object instanceof Error || (
120 object?.constructor?.name === 'Error' ||
121 object?.constructor?.name === 'DOMException'
122 )
123}
124
125// Check whether |statusText| is a ByteString and
126// matches the Reason-Phrase token production.
127// RFC 2616: https://tools.ietf.org/html/rfc2616
128// RFC 7230: https://tools.ietf.org/html/rfc7230
129// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )"
130// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116
131function isValidReasonPhrase (statusText) {
132 for (let i = 0; i < statusText.length; ++i) {
133 const c = statusText.charCodeAt(i)
134 if (
135 !(
136 (
137 c === 0x09 || // HTAB
138 (c >= 0x20 && c <= 0x7e) || // SP / VCHAR
139 (c >= 0x80 && c <= 0xff)
140 ) // obs-text
141 )
142 ) {
143 return false
144 }
145 }
146 return true
147}
148
149/**
150 * @see https://fetch.spec.whatwg.org/#header-name
151 * @param {string} potentialValue
152 */
153const isValidHeaderName = isValidHTTPToken
154
155/**
156 * @see https://fetch.spec.whatwg.org/#header-value
157 * @param {string} potentialValue
158 */
159function isValidHeaderValue (potentialValue) {
160 // - Has no leading or trailing HTTP tab or space bytes.
161 // - Contains no 0x00 (NUL) or HTTP newline bytes.
162 return (
163 potentialValue[0] === '\t' ||
164 potentialValue[0] === ' ' ||
165 potentialValue[potentialValue.length - 1] === '\t' ||
166 potentialValue[potentialValue.length - 1] === ' ' ||
167 potentialValue.includes('\n') ||
168 potentialValue.includes('\r') ||
169 potentialValue.includes('\0')
170 ) === false
171}
172
173// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect
174function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
175 // Given a request request and a response actualResponse, this algorithm
176 // updates request’s referrer policy according to the Referrer-Policy
177 // header (if any) in actualResponse.
178
179 // 1. Let policy be the result of executing § 8.1 Parse a referrer policy
180 // from a Referrer-Policy header on actualResponse.
181
182 // 8.1 Parse a referrer policy from a Referrer-Policy header
183 // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list.
184 const { headersList } = actualResponse
185 // 2. Let policy be the empty string.
186 // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token.
187 // 4. Return policy.
188 const policyHeader = (headersList.get('referrer-policy', true) ?? '').split(',')
189
190 // Note: As the referrer-policy can contain multiple policies
191 // separated by comma, we need to loop through all of them
192 // and pick the first valid one.
193 // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy
194 let policy = ''
195 if (policyHeader.length > 0) {
196 // The right-most policy takes precedence.
197 // The left-most policy is the fallback.
198 for (let i = policyHeader.length; i !== 0; i--) {
199 const token = policyHeader[i - 1].trim()
200 if (referrerPolicyTokens.has(token)) {
201 policy = token
202 break
203 }
204 }
205 }
206
207 // 2. If policy is not the empty string, then set request’s referrer policy to policy.
208 if (policy !== '') {
209 request.referrerPolicy = policy
210 }
211}
212
213// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check
214function crossOriginResourcePolicyCheck () {
215 // TODO
216 return 'allowed'
217}
218
219// https://fetch.spec.whatwg.org/#concept-cors-check
220function corsCheck () {
221 // TODO
222 return 'success'
223}
224
225// https://fetch.spec.whatwg.org/#concept-tao-check
226function TAOCheck () {
227 // TODO
228 return 'success'
229}
230
231function appendFetchMetadata (httpRequest) {
232 // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header
233 // TODO
234
235 // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header
236
237 // 1. Assert: r’s url is a potentially trustworthy URL.
238 // TODO
239
240 // 2. Let header be a Structured Header whose value is a token.
241 let header = null
242
243 // 3. Set header’s value to r’s mode.
244 header = httpRequest.mode
245
246 // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list.
247 httpRequest.headersList.set('sec-fetch-mode', header, true)
248
249 // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header
250 // TODO
251
252 // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header
253 // TODO
254}
255
256// https://fetch.spec.whatwg.org/#append-a-request-origin-header
257function appendRequestOriginHeader (request) {
258 // 1. Let serializedOrigin be the result of byte-serializing a request origin
259 // with request.
260 // TODO: implement "byte-serializing a request origin"
261 let serializedOrigin = request.origin
262
263 // - "'client' is changed to an origin during fetching."
264 // This doesn't happen in undici (in most cases) because undici, by default,
265 // has no concept of origin.
266 // - request.origin can also be set to request.client.origin (client being
267 // an environment settings object), which is undefined without using
268 // setGlobalOrigin.
269 if (serializedOrigin === 'client' || serializedOrigin === undefined) {
270 return
271 }
272
273 // 2. If request’s response tainting is "cors" or request’s mode is "websocket",
274 // then append (`Origin`, serializedOrigin) to request’s header list.
275 // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
276 if (request.responseTainting === 'cors' || request.mode === 'websocket') {
277 request.headersList.append('origin', serializedOrigin, true)
278 } else if (request.method !== 'GET' && request.method !== 'HEAD') {
279 // 1. Switch on request’s referrer policy:
280 switch (request.referrerPolicy) {
281 case 'no-referrer':
282 // Set serializedOrigin to `null`.
283 serializedOrigin = null
284 break
285 case 'no-referrer-when-downgrade':
286 case 'strict-origin':
287 case 'strict-origin-when-cross-origin':
288 // If request’s origin is a tuple origin, its scheme is "https", and
289 // request’s current URL’s scheme is not "https", then set
290 // serializedOrigin to `null`.
291 if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) {
292 serializedOrigin = null
293 }
294 break
295 case 'same-origin':
296 // If request’s origin is not same origin with request’s current URL’s
297 // origin, then set serializedOrigin to `null`.
298 if (!sameOrigin(request, requestCurrentURL(request))) {
299 serializedOrigin = null
300 }
301 break
302 default:
303 // Do nothing.
304 }
305
306 // 2. Append (`Origin`, serializedOrigin) to request’s header list.
307 request.headersList.append('origin', serializedOrigin, true)
308 }
309}
310
311// https://w3c.github.io/hr-time/#dfn-coarsen-time
312function coarsenTime (timestamp, crossOriginIsolatedCapability) {
313 // TODO
314 return timestamp
315}
316
317// https://fetch.spec.whatwg.org/#clamp-and-coarsen-connection-timing-info
318function clampAndCoarsenConnectionTimingInfo (connectionTimingInfo, defaultStartTime, crossOriginIsolatedCapability) {
319 if (!connectionTimingInfo?.startTime || connectionTimingInfo.startTime < defaultStartTime) {
320 return {
321 domainLookupStartTime: defaultStartTime,
322 domainLookupEndTime: defaultStartTime,
323 connectionStartTime: defaultStartTime,
324 connectionEndTime: defaultStartTime,
325 secureConnectionStartTime: defaultStartTime,
326 ALPNNegotiatedProtocol: connectionTimingInfo?.ALPNNegotiatedProtocol
327 }
328 }
329
330 return {
331 domainLookupStartTime: coarsenTime(connectionTimingInfo.domainLookupStartTime, crossOriginIsolatedCapability),
332 domainLookupEndTime: coarsenTime(connectionTimingInfo.domainLookupEndTime, crossOriginIsolatedCapability),
333 connectionStartTime: coarsenTime(connectionTimingInfo.connectionStartTime, crossOriginIsolatedCapability),
334 connectionEndTime: coarsenTime(connectionTimingInfo.connectionEndTime, crossOriginIsolatedCapability),
335 secureConnectionStartTime: coarsenTime(connectionTimingInfo.secureConnectionStartTime, crossOriginIsolatedCapability),
336 ALPNNegotiatedProtocol: connectionTimingInfo.ALPNNegotiatedProtocol
337 }
338}
339
340// https://w3c.github.io/hr-time/#dfn-coarsened-shared-current-time
341function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) {
342 return coarsenTime(performance.now(), crossOriginIsolatedCapability)
343}
344
345// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info
346function createOpaqueTimingInfo (timingInfo) {
347 return {
348 startTime: timingInfo.startTime ?? 0,
349 redirectStartTime: 0,
350 redirectEndTime: 0,
351 postRedirectStartTime: timingInfo.startTime ?? 0,
352 finalServiceWorkerStartTime: 0,
353 finalNetworkResponseStartTime: 0,
354 finalNetworkRequestStartTime: 0,
355 endTime: 0,
356 encodedBodySize: 0,
357 decodedBodySize: 0,
358 finalConnectionTimingInfo: null
359 }
360}
361
362// https://html.spec.whatwg.org/multipage/origin.html#policy-container
363function makePolicyContainer () {
364 // Note: the fetch spec doesn't make use of embedder policy or CSP list
365 return {
366 referrerPolicy: 'strict-origin-when-cross-origin'
367 }
368}
369
370// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container
371function clonePolicyContainer (policyContainer) {
372 return {
373 referrerPolicy: policyContainer.referrerPolicy
374 }
375}
376
377// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
378function determineRequestsReferrer (request) {
379 // 1. Let policy be request's referrer policy.
380 const policy = request.referrerPolicy
381
382 // Note: policy cannot (shouldn't) be null or an empty string.
383 assert(policy)
384
385 // 2. Let environment be request’s client.
386
387 let referrerSource = null
388
389 // 3. Switch on request’s referrer:
390 if (request.referrer === 'client') {
391 // Note: node isn't a browser and doesn't implement document/iframes,
392 // so we bypass this step and replace it with our own.
393
394 const globalOrigin = getGlobalOrigin()
395
396 if (!globalOrigin || globalOrigin.origin === 'null') {
397 return 'no-referrer'
398 }
399
400 // note: we need to clone it as it's mutated
401 referrerSource = new URL(globalOrigin)
402 } else if (request.referrer instanceof URL) {
403 // Let referrerSource be request’s referrer.
404 referrerSource = request.referrer
405 }
406
407 // 4. Let request’s referrerURL be the result of stripping referrerSource for
408 // use as a referrer.
409 let referrerURL = stripURLForReferrer(referrerSource)
410
411 // 5. Let referrerOrigin be the result of stripping referrerSource for use as
412 // a referrer, with the origin-only flag set to true.
413 const referrerOrigin = stripURLForReferrer(referrerSource, true)
414
415 // 6. If the result of serializing referrerURL is a string whose length is
416 // greater than 4096, set referrerURL to referrerOrigin.
417 if (referrerURL.toString().length > 4096) {
418 referrerURL = referrerOrigin
419 }
420
421 const areSameOrigin = sameOrigin(request, referrerURL)
422 const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) &&
423 !isURLPotentiallyTrustworthy(request.url)
424
425 // 8. Execute the switch statements corresponding to the value of policy:
426 switch (policy) {
427 case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true)
428 case 'unsafe-url': return referrerURL
429 case 'same-origin':
430 return areSameOrigin ? referrerOrigin : 'no-referrer'
431 case 'origin-when-cross-origin':
432 return areSameOrigin ? referrerURL : referrerOrigin
433 case 'strict-origin-when-cross-origin': {
434 const currentURL = requestCurrentURL(request)
435
436 // 1. If the origin of referrerURL and the origin of request’s current
437 // URL are the same, then return referrerURL.
438 if (sameOrigin(referrerURL, currentURL)) {
439 return referrerURL
440 }
441
442 // 2. If referrerURL is a potentially trustworthy URL and request’s
443 // current URL is not a potentially trustworthy URL, then return no
444 // referrer.
445 if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
446 return 'no-referrer'
447 }
448
449 // 3. Return referrerOrigin.
450 return referrerOrigin
451 }
452 case 'strict-origin': // eslint-disable-line
453 /**
454 * 1. If referrerURL is a potentially trustworthy URL and
455 * request’s current URL is not a potentially trustworthy URL,
456 * then return no referrer.
457 * 2. Return referrerOrigin
458 */
459 case 'no-referrer-when-downgrade': // eslint-disable-line
460 /**
461 * 1. If referrerURL is a potentially trustworthy URL and
462 * request’s current URL is not a potentially trustworthy URL,
463 * then return no referrer.
464 * 2. Return referrerOrigin
465 */
466
467 default: // eslint-disable-line
468 return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin
469 }
470}
471
472/**
473 * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url
474 * @param {URL} url
475 * @param {boolean|undefined} originOnly
476 */
477function stripURLForReferrer (url, originOnly) {
478 // 1. Assert: url is a URL.
479 assert(url instanceof URL)
480
481 url = new URL(url)
482
483 // 2. If url’s scheme is a local scheme, then return no referrer.
484 if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') {
485 return 'no-referrer'
486 }
487
488 // 3. Set url’s username to the empty string.
489 url.username = ''
490
491 // 4. Set url’s password to the empty string.
492 url.password = ''
493
494 // 5. Set url’s fragment to null.
495 url.hash = ''
496
497 // 6. If the origin-only flag is true, then:
498 if (originOnly) {
499 // 1. Set url’s path to « the empty string ».
500 url.pathname = ''
501
502 // 2. Set url’s query to null.
503 url.search = ''
504 }
505
506 // 7. Return url.
507 return url
508}
509
510function isURLPotentiallyTrustworthy (url) {
511 if (!(url instanceof URL)) {
512 return false
513 }
514
515 // If child of about, return true
516 if (url.href === 'about:blank' || url.href === 'about:srcdoc') {
517 return true
518 }
519
520 // If scheme is data, return true
521 if (url.protocol === 'data:') return true
522
523 // If file, return true
524 if (url.protocol === 'file:') return true
525
526 return isOriginPotentiallyTrustworthy(url.origin)
527
528 function isOriginPotentiallyTrustworthy (origin) {
529 // If origin is explicitly null, return false
530 if (origin == null || origin === 'null') return false
531
532 const originAsURL = new URL(origin)
533
534 // If secure, return true
535 if (originAsURL.protocol === 'https:' || originAsURL.protocol === 'wss:') {
536 return true
537 }
538
539 // If localhost or variants, return true
540 if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) ||
541 (originAsURL.hostname === 'localhost' || originAsURL.hostname.includes('localhost.')) ||
542 (originAsURL.hostname.endsWith('.localhost'))) {
543 return true
544 }
545
546 // If any other, return false
547 return false
548 }
549}
550
551/**
552 * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist
553 * @param {Uint8Array} bytes
554 * @param {string} metadataList
555 */
556function bytesMatch (bytes, metadataList) {
557 // If node is not built with OpenSSL support, we cannot check
558 // a request's integrity, so allow it by default (the spec will
559 // allow requests if an invalid hash is given, as precedence).
560 /* istanbul ignore if: only if node is built with --without-ssl */
561 if (crypto === undefined) {
562 return true
563 }
564
565 // 1. Let parsedMetadata be the result of parsing metadataList.
566 const parsedMetadata = parseMetadata(metadataList)
567
568 // 2. If parsedMetadata is no metadata, return true.
569 if (parsedMetadata === 'no metadata') {
570 return true
571 }
572
573 // 3. If response is not eligible for integrity validation, return false.
574 // TODO
575
576 // 4. If parsedMetadata is the empty set, return true.
577 if (parsedMetadata.length === 0) {
578 return true
579 }
580
581 // 5. Let metadata be the result of getting the strongest
582 // metadata from parsedMetadata.
583 const strongest = getStrongestMetadata(parsedMetadata)
584 const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest)
585
586 // 6. For each item in metadata:
587 for (const item of metadata) {
588 // 1. Let algorithm be the alg component of item.
589 const algorithm = item.algo
590
591 // 2. Let expectedValue be the val component of item.
592 const expectedValue = item.hash
593
594 // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
595 // "be liberal with padding". This is annoying, and it's not even in the spec.
596
597 // 3. Let actualValue be the result of applying algorithm to bytes.
598 let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64')
599
600 if (actualValue[actualValue.length - 1] === '=') {
601 if (actualValue[actualValue.length - 2] === '=') {
602 actualValue = actualValue.slice(0, -2)
603 } else {
604 actualValue = actualValue.slice(0, -1)
605 }
606 }
607
608 // 4. If actualValue is a case-sensitive match for expectedValue,
609 // return true.
610 if (compareBase64Mixed(actualValue, expectedValue)) {
611 return true
612 }
613 }
614
615 // 7. Return false.
616 return false
617}
618
619// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
620// https://www.w3.org/TR/CSP2/#source-list-syntax
621// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
622const parseHashWithOptions = /(?<algo>sha256|sha384|sha512)-((?<hash>[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i
623
624/**
625 * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
626 * @param {string} metadata
627 */
628function parseMetadata (metadata) {
629 // 1. Let result be the empty set.
630 /** @type {{ algo: string, hash: string }[]} */
631 const result = []
632
633 // 2. Let empty be equal to true.
634 let empty = true
635
636 // 3. For each token returned by splitting metadata on spaces:
637 for (const token of metadata.split(' ')) {
638 // 1. Set empty to false.
639 empty = false
640
641 // 2. Parse token as a hash-with-options.
642 const parsedToken = parseHashWithOptions.exec(token)
643
644 // 3. If token does not parse, continue to the next token.
645 if (
646 parsedToken === null ||
647 parsedToken.groups === undefined ||
648 parsedToken.groups.algo === undefined
649 ) {
650 // Note: Chromium blocks the request at this point, but Firefox
651 // gives a warning that an invalid integrity was given. The
652 // correct behavior is to ignore these, and subsequently not
653 // check the integrity of the resource.
654 continue
655 }
656
657 // 4. Let algorithm be the hash-algo component of token.
658 const algorithm = parsedToken.groups.algo.toLowerCase()
659
660 // 5. If algorithm is a hash function recognized by the user
661 // agent, add the parsed token to result.
662 if (supportedHashes.includes(algorithm)) {
663 result.push(parsedToken.groups)
664 }
665 }
666
667 // 4. Return no metadata if empty is true, otherwise return result.
668 if (empty === true) {
669 return 'no metadata'
670 }
671
672 return result
673}
674
675/**
676 * @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList
677 */
678function getStrongestMetadata (metadataList) {
679 // Let algorithm be the algo component of the first item in metadataList.
680 // Can be sha256
681 let algorithm = metadataList[0].algo
682 // If the algorithm is sha512, then it is the strongest
683 // and we can return immediately
684 if (algorithm[3] === '5') {
685 return algorithm
686 }
687
688 for (let i = 1; i < metadataList.length; ++i) {
689 const metadata = metadataList[i]
690 // If the algorithm is sha512, then it is the strongest
691 // and we can break the loop immediately
692 if (metadata.algo[3] === '5') {
693 algorithm = 'sha512'
694 break
695 // If the algorithm is sha384, then a potential sha256 or sha384 is ignored
696 } else if (algorithm[3] === '3') {
697 continue
698 // algorithm is sha256, check if algorithm is sha384 and if so, set it as
699 // the strongest
700 } else if (metadata.algo[3] === '3') {
701 algorithm = 'sha384'
702 }
703 }
704 return algorithm
705}
706
707function filterMetadataListByAlgorithm (metadataList, algorithm) {
708 if (metadataList.length === 1) {
709 return metadataList
710 }
711
712 let pos = 0
713 for (let i = 0; i < metadataList.length; ++i) {
714 if (metadataList[i].algo === algorithm) {
715 metadataList[pos++] = metadataList[i]
716 }
717 }
718
719 metadataList.length = pos
720
721 return metadataList
722}
723
724/**
725 * Compares two base64 strings, allowing for base64url
726 * in the second string.
727 *
728* @param {string} actualValue always base64
729 * @param {string} expectedValue base64 or base64url
730 * @returns {boolean}
731 */
732function compareBase64Mixed (actualValue, expectedValue) {
733 if (actualValue.length !== expectedValue.length) {
734 return false
735 }
736 for (let i = 0; i < actualValue.length; ++i) {
737 if (actualValue[i] !== expectedValue[i]) {
738 if (
739 (actualValue[i] === '+' && expectedValue[i] === '-') ||
740 (actualValue[i] === '/' && expectedValue[i] === '_')
741 ) {
742 continue
743 }
744 return false
745 }
746 }
747
748 return true
749}
750
751// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
752function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
753 // TODO
754}
755
756/**
757 * @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin}
758 * @param {URL} A
759 * @param {URL} B
760 */
761function sameOrigin (A, B) {
762 // 1. If A and B are the same opaque origin, then return true.
763 if (A.origin === B.origin && A.origin === 'null') {
764 return true
765 }
766
767 // 2. If A and B are both tuple origins and their schemes,
768 // hosts, and port are identical, then return true.
769 if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) {
770 return true
771 }
772
773 // 3. Return false.
774 return false
775}
776
777function createDeferredPromise () {
778 let res
779 let rej
780 const promise = new Promise((resolve, reject) => {
781 res = resolve
782 rej = reject
783 })
784
785 return { promise, resolve: res, reject: rej }
786}
787
788function isAborted (fetchParams) {
789 return fetchParams.controller.state === 'aborted'
790}
791
792function isCancelled (fetchParams) {
793 return fetchParams.controller.state === 'aborted' ||
794 fetchParams.controller.state === 'terminated'
795}
796
797/**
798 * @see https://fetch.spec.whatwg.org/#concept-method-normalize
799 * @param {string} method
800 */
801function normalizeMethod (method) {
802 return normalizedMethodRecordsBase[method.toLowerCase()] ?? method
803}
804
805// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
806function serializeJavascriptValueToJSONString (value) {
807 // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »).
808 const result = JSON.stringify(value)
809
810 // 2. If result is undefined, then throw a TypeError.
811 if (result === undefined) {
812 throw new TypeError('Value is not JSON serializable')
813 }
814
815 // 3. Assert: result is a string.
816 assert(typeof result === 'string')
817
818 // 4. Return result.
819 return result
820}
821
822// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
823const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
824
825/**
826 * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
827 * @param {string} name name of the instance
828 * @param {symbol} kInternalIterator
829 * @param {string | number} [keyIndex]
830 * @param {string | number} [valueIndex]
831 */
832function createIterator (name, kInternalIterator, keyIndex = 0, valueIndex = 1) {
833 class FastIterableIterator {
834 /** @type {any} */
835 #target
836 /** @type {'key' | 'value' | 'key+value'} */
837 #kind
838 /** @type {number} */
839 #index
840
841 /**
842 * @see https://webidl.spec.whatwg.org/#dfn-default-iterator-object
843 * @param {unknown} target
844 * @param {'key' | 'value' | 'key+value'} kind
845 */
846 constructor (target, kind) {
847 this.#target = target
848 this.#kind = kind
849 this.#index = 0
850 }
851
852 next () {
853 // 1. Let interface be the interface for which the iterator prototype object exists.
854 // 2. Let thisValue be the this value.
855 // 3. Let object be ? ToObject(thisValue).
856 // 4. If object is a platform object, then perform a security
857 // check, passing:
858 // 5. If object is not a default iterator object for interface,
859 // then throw a TypeError.
860 if (typeof this !== 'object' || this === null || !(#target in this)) {
861 throw new TypeError(
862 `'next' called on an object that does not implement interface ${name} Iterator.`
863 )
864 }
865
866 // 6. Let index be object’s index.
867 // 7. Let kind be object’s kind.
868 // 8. Let values be object’s target's value pairs to iterate over.
869 const index = this.#index
870 const values = this.#target[kInternalIterator]
871
872 // 9. Let len be the length of values.
873 const len = values.length
874
875 // 10. If index is greater than or equal to len, then return
876 // CreateIterResultObject(undefined, true).
877 if (index >= len) {
878 return {
879 value: undefined,
880 done: true
881 }
882 }
883
884 // 11. Let pair be the entry in values at index index.
885 const { [keyIndex]: key, [valueIndex]: value } = values[index]
886
887 // 12. Set object’s index to index + 1.
888 this.#index = index + 1
889
890 // 13. Return the iterator result for pair and kind.
891
892 // https://webidl.spec.whatwg.org/#iterator-result
893
894 // 1. Let result be a value determined by the value of kind:
895 let result
896 switch (this.#kind) {
897 case 'key':
898 // 1. Let idlKey be pair’s key.
899 // 2. Let key be the result of converting idlKey to an
900 // ECMAScript value.
901 // 3. result is key.
902 result = key
903 break
904 case 'value':
905 // 1. Let idlValue be pair’s value.
906 // 2. Let value be the result of converting idlValue to
907 // an ECMAScript value.
908 // 3. result is value.
909 result = value
910 break
911 case 'key+value':
912 // 1. Let idlKey be pair’s key.
913 // 2. Let idlValue be pair’s value.
914 // 3. Let key be the result of converting idlKey to an
915 // ECMAScript value.
916 // 4. Let value be the result of converting idlValue to
917 // an ECMAScript value.
918 // 5. Let array be ! ArrayCreate(2).
919 // 6. Call ! CreateDataProperty(array, "0", key).
920 // 7. Call ! CreateDataProperty(array, "1", value).
921 // 8. result is array.
922 result = [key, value]
923 break
924 }
925
926 // 2. Return CreateIterResultObject(result, false).
927 return {
928 value: result,
929 done: false
930 }
931 }
932 }
933
934 // https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
935 // @ts-ignore
936 delete FastIterableIterator.prototype.constructor
937
938 Object.setPrototypeOf(FastIterableIterator.prototype, esIteratorPrototype)
939
940 Object.defineProperties(FastIterableIterator.prototype, {
941 [Symbol.toStringTag]: {
942 writable: false,
943 enumerable: false,
944 configurable: true,
945 value: `${name} Iterator`
946 },
947 next: { writable: true, enumerable: true, configurable: true }
948 })
949
950 /**
951 * @param {unknown} target
952 * @param {'key' | 'value' | 'key+value'} kind
953 * @returns {IterableIterator<any>}
954 */
955 return function (target, kind) {
956 return new FastIterableIterator(target, kind)
957 }
958}
959
960/**
961 * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
962 * @param {string} name name of the instance
963 * @param {any} object class
964 * @param {symbol} kInternalIterator
965 * @param {string | number} [keyIndex]
966 * @param {string | number} [valueIndex]
967 */
968function iteratorMixin (name, object, kInternalIterator, keyIndex = 0, valueIndex = 1) {
969 const makeIterator = createIterator(name, kInternalIterator, keyIndex, valueIndex)
970
971 const properties = {
972 keys: {
973 writable: true,
974 enumerable: true,
975 configurable: true,
976 value: function keys () {
977 webidl.brandCheck(this, object)
978 return makeIterator(this, 'key')
979 }
980 },
981 values: {
982 writable: true,
983 enumerable: true,
984 configurable: true,
985 value: function values () {
986 webidl.brandCheck(this, object)
987 return makeIterator(this, 'value')
988 }
989 },
990 entries: {
991 writable: true,
992 enumerable: true,
993 configurable: true,
994 value: function entries () {
995 webidl.brandCheck(this, object)
996 return makeIterator(this, 'key+value')
997 }
998 },
999 forEach: {
1000 writable: true,
1001 enumerable: true,
1002 configurable: true,
1003 value: function forEach (callbackfn, thisArg = globalThis) {
1004 webidl.brandCheck(this, object)
1005 webidl.argumentLengthCheck(arguments, 1, `${name}.forEach`)
1006 if (typeof callbackfn !== 'function') {
1007 throw new TypeError(
1008 `Failed to execute 'forEach' on '${name}': parameter 1 is not of type 'Function'.`
1009 )
1010 }
1011 for (const { 0: key, 1: value } of makeIterator(this, 'key+value')) {
1012 callbackfn.call(thisArg, value, key, this)
1013 }
1014 }
1015 }
1016 }
1017
1018 return Object.defineProperties(object.prototype, {
1019 ...properties,
1020 [Symbol.iterator]: {
1021 writable: true,
1022 enumerable: false,
1023 configurable: true,
1024 value: properties.entries.value
1025 }
1026 })
1027}
1028
1029/**
1030 * @see https://fetch.spec.whatwg.org/#body-fully-read
1031 */
1032async function fullyReadBody (body, processBody, processBodyError) {
1033 // 1. If taskDestination is null, then set taskDestination to
1034 // the result of starting a new parallel queue.
1035
1036 // 2. Let successSteps given a byte sequence bytes be to queue a
1037 // fetch task to run processBody given bytes, with taskDestination.
1038 const successSteps = processBody
1039
1040 // 3. Let errorSteps be to queue a fetch task to run processBodyError,
1041 // with taskDestination.
1042 const errorSteps = processBodyError
1043
1044 // 4. Let reader be the result of getting a reader for body’s stream.
1045 // If that threw an exception, then run errorSteps with that
1046 // exception and return.
1047 let reader
1048
1049 try {
1050 reader = body.stream.getReader()
1051 } catch (e) {
1052 errorSteps(e)
1053 return
1054 }
1055
1056 // 5. Read all bytes from reader, given successSteps and errorSteps.
1057 try {
1058 successSteps(await readAllBytes(reader))
1059 } catch (e) {
1060 errorSteps(e)
1061 }
1062}
1063
1064function isReadableStreamLike (stream) {
1065 return stream instanceof ReadableStream || (
1066 stream[Symbol.toStringTag] === 'ReadableStream' &&
1067 typeof stream.tee === 'function'
1068 )
1069}
1070
1071/**
1072 * @param {ReadableStreamController<Uint8Array>} controller
1073 */
1074function readableStreamClose (controller) {
1075 try {
1076 controller.close()
1077 controller.byobRequest?.respond(0)
1078 } catch (err) {
1079 // TODO: add comment explaining why this error occurs.
1080 if (!err.message.includes('Controller is already closed') && !err.message.includes('ReadableStream is already closed')) {
1081 throw err
1082 }
1083 }
1084}
1085
1086const invalidIsomorphicEncodeValueRegex = /[^\x00-\xFF]/ // eslint-disable-line
1087
1088/**
1089 * @see https://infra.spec.whatwg.org/#isomorphic-encode
1090 * @param {string} input
1091 */
1092function isomorphicEncode (input) {
1093 // 1. Assert: input contains no code points greater than U+00FF.
1094 assert(!invalidIsomorphicEncodeValueRegex.test(input))
1095
1096 // 2. Return a byte sequence whose length is equal to input’s code
1097 // point length and whose bytes have the same values as the
1098 // values of input’s code points, in the same order
1099 return input
1100}
1101
1102/**
1103 * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
1104 * @see https://streams.spec.whatwg.org/#read-loop
1105 * @param {ReadableStreamDefaultReader} reader
1106 */
1107async function readAllBytes (reader) {
1108 const bytes = []
1109 let byteLength = 0
1110
1111 while (true) {
1112 const { done, value: chunk } = await reader.read()
1113
1114 if (done) {
1115 // 1. Call successSteps with bytes.
1116 return Buffer.concat(bytes, byteLength)
1117 }
1118
1119 // 1. If chunk is not a Uint8Array object, call failureSteps
1120 // with a TypeError and abort these steps.
1121 if (!isUint8Array(chunk)) {
1122 throw new TypeError('Received non-Uint8Array chunk')
1123 }
1124
1125 // 2. Append the bytes represented by chunk to bytes.
1126 bytes.push(chunk)
1127 byteLength += chunk.length
1128
1129 // 3. Read-loop given reader, bytes, successSteps, and failureSteps.
1130 }
1131}
1132
1133/**
1134 * @see https://fetch.spec.whatwg.org/#is-local
1135 * @param {URL} url
1136 */
1137function urlIsLocal (url) {
1138 assert('protocol' in url) // ensure it's a url object
1139
1140 const protocol = url.protocol
1141
1142 return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:'
1143}
1144
1145/**
1146 * @param {string|URL} url
1147 * @returns {boolean}
1148 */
1149function urlHasHttpsScheme (url) {
1150 return (
1151 (
1152 typeof url === 'string' &&
1153 url[5] === ':' &&
1154 url[0] === 'h' &&
1155 url[1] === 't' &&
1156 url[2] === 't' &&
1157 url[3] === 'p' &&
1158 url[4] === 's'
1159 ) ||
1160 url.protocol === 'https:'
1161 )
1162}
1163
1164/**
1165 * @see https://fetch.spec.whatwg.org/#http-scheme
1166 * @param {URL} url
1167 */
1168function urlIsHttpHttpsScheme (url) {
1169 assert('protocol' in url) // ensure it's a url object
1170
1171 const protocol = url.protocol
1172
1173 return protocol === 'http:' || protocol === 'https:'
1174}
1175
1176/**
1177 * @see https://fetch.spec.whatwg.org/#simple-range-header-value
1178 * @param {string} value
1179 * @param {boolean} allowWhitespace
1180 */
1181function simpleRangeHeaderValue (value, allowWhitespace) {
1182 // 1. Let data be the isomorphic decoding of value.
1183 // Note: isomorphic decoding takes a sequence of bytes (ie. a Uint8Array) and turns it into a string,
1184 // nothing more. We obviously don't need to do that if value is a string already.
1185 const data = value
1186
1187 // 2. If data does not start with "bytes", then return failure.
1188 if (!data.startsWith('bytes')) {
1189 return 'failure'
1190 }
1191
1192 // 3. Let position be a position variable for data, initially pointing at the 5th code point of data.
1193 const position = { position: 5 }
1194
1195 // 4. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space,
1196 // from data given position.
1197 if (allowWhitespace) {
1198 collectASequenceOfCodePoints(
1199 (char) => char === '\t' || char === ' ',
1200 data,
