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