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