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