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