| 'use strict' |
| |
| const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require('./constants') |
| const { getGlobalOrigin } = require('./global') |
| const { performance } = require('perf_hooks') |
| const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util') |
| const assert = require('assert') |
| const { isUint8Array } = require('util/types') |
| |
| // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable |
| /** @type {import('crypto')|undefined} */ |
| let crypto |
| |
| try { |
| crypto = require('crypto') |
| } catch { |
| |
| } |
| |
| function responseURL (response) { |
| // https://fetch.spec.whatwg.org/#responses |
| // A response has an associated URL. It is a pointer to the last URL |
| // in response’s URL list and null if response’s URL list is empty. |
| const urlList = response.urlList |
| const length = urlList.length |
| return length === 0 ? null : urlList[length - 1].toString() |
| } |
| |
| // https://fetch.spec.whatwg.org/#concept-response-location-url |
| function responseLocationURL (response, requestFragment) { |
| // 1. If response’s status is not a redirect status, then return null. |
| if (!redirectStatusSet.has(response.status)) { |
| return null |
| } |
| |
| // 2. Let location be the result of extracting header list values given |
| // `Location` and response’s header list. |
| let location = response.headersList.get('location') |
| |
| // 3. If location is a header value, then set location to the result of |
| // parsing location with response’s URL. |
| if (location !== null && isValidHeaderValue(location)) { |
| location = new URL(location, responseURL(response)) |
| } |
| |
| // 4. If location is a URL whose fragment is null, then set location’s |
| // fragment to requestFragment. |
| if (location && !location.hash) { |
| location.hash = requestFragment |
| } |
| |
| // 5. Return location. |
| return location |
| } |
| |
| /** @returns {URL} */ |
| function requestCurrentURL (request) { |
| return request.urlList[request.urlList.length - 1] |
| } |
| |
| function requestBadPort (request) { |
| // 1. Let url be request’s current URL. |
| const url = requestCurrentURL(request) |
| |
| // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port, |
| // then return blocked. |
| if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) { |
| return 'blocked' |
| } |
| |
| // 3. Return allowed. |
| return 'allowed' |
| } |
| |
| function isErrorLike (object) { |
| return object instanceof Error || ( |
| object?.constructor?.name === 'Error' || |
| object?.constructor?.name === 'DOMException' |
| ) |
| } |
| |
| // Check whether |statusText| is a ByteString and |
| // matches the Reason-Phrase token production. |
| // RFC 2616: https://tools.ietf.org/html/rfc2616 |
| // RFC 7230: https://tools.ietf.org/html/rfc7230 |
| // "reason-phrase = *( HTAB / SP / VCHAR / obs-text )" |
| // https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116 |
| function isValidReasonPhrase (statusText) { |
| for (let i = 0; i < statusText.length; ++i) { |
| const c = statusText.charCodeAt(i) |
| if ( |
| !( |
| ( |
| c === 0x09 || // HTAB |
| (c >= 0x20 && c <= 0x7e) || // SP / VCHAR |
| (c >= 0x80 && c <= 0xff) |
| ) // obs-text |
| ) |
| ) { |
| return false |
| } |
| } |
| return true |
| } |
| |
| /** |
| * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 |
| * @param {number} c |
| */ |
| function isTokenCharCode (c) { |
| switch (c) { |
| case 0x22: |
| case 0x28: |
| case 0x29: |
| case 0x2c: |
| case 0x2f: |
| case 0x3a: |
| case 0x3b: |
| case 0x3c: |
| case 0x3d: |
| case 0x3e: |
| case 0x3f: |
| case 0x40: |
| case 0x5b: |
| case 0x5c: |
| case 0x5d: |
| case 0x7b: |
| case 0x7d: |
| // DQUOTE and "(),/:;<=>?@[\]{}" |
| return false |
| default: |
| // VCHAR %x21-7E |
| return c >= 0x21 && c <= 0x7e |
| } |
| } |
| |
| /** |
| * @param {string} characters |
| */ |
| function isValidHTTPToken (characters) { |
| if (characters.length === 0) { |
| return false |
| } |
| for (let i = 0; i < characters.length; ++i) { |
| if (!isTokenCharCode(characters.charCodeAt(i))) { |
| return false |
| } |
| } |
| return true |
| } |
| |
| /** |
| * @see https://fetch.spec.whatwg.org/#header-name |
| * @param {string} potentialValue |
| */ |
| function isValidHeaderName (potentialValue) { |
| return isValidHTTPToken(potentialValue) |
| } |
| |
| /** |
| * @see https://fetch.spec.whatwg.org/#header-value |
| * @param {string} potentialValue |
| */ |
| function isValidHeaderValue (potentialValue) { |
| // - Has no leading or trailing HTTP tab or space bytes. |
| // - Contains no 0x00 (NUL) or HTTP newline bytes. |
| if ( |
| potentialValue.startsWith('\t') || |
| potentialValue.startsWith(' ') || |
| potentialValue.endsWith('\t') || |
| potentialValue.endsWith(' ') |
| ) { |
| return false |
| } |
| |
| if ( |
| potentialValue.includes('\0') || |
| potentialValue.includes('\r') || |
| potentialValue.includes('\n') |
| ) { |
| return false |
| } |
| |
| return true |
| } |
| |
| // https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect |
| function setRequestReferrerPolicyOnRedirect (request, actualResponse) { |
| // Given a request request and a response actualResponse, this algorithm |
| // updates request’s referrer policy according to the Referrer-Policy |
| // header (if any) in actualResponse. |
| |
| // 1. Let policy be the result of executing § 8.1 Parse a referrer policy |
| // from a Referrer-Policy header on actualResponse. |
| |
| // 8.1 Parse a referrer policy from a Referrer-Policy header |
| // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list. |
| const { headersList } = actualResponse |
| // 2. Let policy be the empty string. |
| // 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. |
| // 4. Return policy. |
| const policyHeader = (headersList.get('referrer-policy') ?? '').split(',') |
| |
| // Note: As the referrer-policy can contain multiple policies |
| // separated by comma, we need to loop through all of them |
| // and pick the first valid one. |
| // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy |
| let policy = '' |
| if (policyHeader.length > 0) { |
| // The right-most policy takes precedence. |
| // The left-most policy is the fallback. |
| for (let i = policyHeader.length; i !== 0; i--) { |
| const token = policyHeader[i - 1].trim() |
| if (referrerPolicyTokens.has(token)) { |
| policy = token |
| break |
| } |
| } |
| } |
| |
| // 2. If policy is not the empty string, then set request’s referrer policy to policy. |
| if (policy !== '') { |
| request.referrerPolicy = policy |
| } |
| } |
| |
| // https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check |
| function crossOriginResourcePolicyCheck () { |
| // TODO |
| return 'allowed' |
| } |
| |
| // https://fetch.spec.whatwg.org/#concept-cors-check |
| function corsCheck () { |
| // TODO |
| return 'success' |
| } |
| |
| // https://fetch.spec.whatwg.org/#concept-tao-check |
| function TAOCheck () { |
| // TODO |
| return 'success' |
| } |
| |
| function appendFetchMetadata (httpRequest) { |
| // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header |
| // TODO |
| |
| // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header |
| |
| // 1. Assert: r’s url is a potentially trustworthy URL. |
| // TODO |
| |
| // 2. Let header be a Structured Header whose value is a token. |
| let header = null |
| |
| // 3. Set header’s value to r’s mode. |
| header = httpRequest.mode |
| |
| // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list. |
| httpRequest.headersList.set('sec-fetch-mode', header) |
| |
| // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header |
| // TODO |
| |
| // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header |
| // TODO |
| } |
| |
| // https://fetch.spec.whatwg.org/#append-a-request-origin-header |
| function appendRequestOriginHeader (request) { |
| // 1. Let serializedOrigin be the result of byte-serializing a request origin with request. |
| let serializedOrigin = request.origin |
| |
| // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list. |
| if (request.responseTainting === 'cors' || request.mode === 'websocket') { |
| if (serializedOrigin) { |
| request.headersList.append('origin', serializedOrigin) |
| } |
| |
| // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: |
| } else if (request.method !== 'GET' && request.method !== 'HEAD') { |
| // 1. Switch on request’s referrer policy: |
| switch (request.referrerPolicy) { |
| case 'no-referrer': |
| // Set serializedOrigin to `null`. |
| serializedOrigin = null |
| break |
| case 'no-referrer-when-downgrade': |
| case 'strict-origin': |
| case 'strict-origin-when-cross-origin': |
| // 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`. |
| if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) { |
| serializedOrigin = null |
| } |
| break |
| case 'same-origin': |
| // If request’s origin is not same origin with request’s current URL’s origin, then set serializedOrigin to `null`. |
| if (!sameOrigin(request, requestCurrentURL(request))) { |
| serializedOrigin = null |
| } |
| break |
| default: |
| // Do nothing. |
| } |
| |
| if (serializedOrigin) { |
| // 2. Append (`Origin`, serializedOrigin) to request’s header list. |
| request.headersList.append('origin', serializedOrigin) |
| } |
| } |
| } |
| |
| function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) { |
| // TODO |
| return performance.now() |
| } |
| |
| // https://fetch.spec.whatwg.org/#create-an-opaque-timing-info |
| function createOpaqueTimingInfo (timingInfo) { |
| return { |
| startTime: timingInfo.startTime ?? 0, |
| redirectStartTime: 0, |
| redirectEndTime: 0, |
| postRedirectStartTime: timingInfo.startTime ?? 0, |
| finalServiceWorkerStartTime: 0, |
| finalNetworkResponseStartTime: 0, |
| finalNetworkRequestStartTime: 0, |
| endTime: 0, |
| encodedBodySize: 0, |
| decodedBodySize: 0, |
| finalConnectionTimingInfo: null |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/origin.html#policy-container |
| function makePolicyContainer () { |
| // Note: the fetch spec doesn't make use of embedder policy or CSP list |
| return { |
| referrerPolicy: 'strict-origin-when-cross-origin' |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container |
| function clonePolicyContainer (policyContainer) { |
| return { |
| referrerPolicy: policyContainer.referrerPolicy |
| } |
| } |
| |
| // https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer |
| function determineRequestsReferrer (request) { |
| // 1. Let policy be request's referrer policy. |
| const policy = request.referrerPolicy |
| |
| // Note: policy cannot (shouldn't) be null or an empty string. |
| assert(policy) |
| |
| // 2. Let environment be request’s client. |
| |
| let referrerSource = null |
| |
| // 3. Switch on request’s referrer: |
| if (request.referrer === 'client') { |
| // Note: node isn't a browser and doesn't implement document/iframes, |
| // so we bypass this step and replace it with our own. |
| |
| const globalOrigin = getGlobalOrigin() |
| |
| if (!globalOrigin || globalOrigin.origin === 'null') { |
| return 'no-referrer' |
| } |
| |
| // note: we need to clone it as it's mutated |
| referrerSource = new URL(globalOrigin) |
| } else if (request.referrer instanceof URL) { |
| // Let referrerSource be request’s referrer. |
| referrerSource = request.referrer |
| } |
| |
| // 4. Let request’s referrerURL be the result of stripping referrerSource for |
| // use as a referrer. |
| let referrerURL = stripURLForReferrer(referrerSource) |
| |
| // 5. Let referrerOrigin be the result of stripping referrerSource for use as |
| // a referrer, with the origin-only flag set to true. |
| const referrerOrigin = stripURLForReferrer(referrerSource, true) |
| |
| // 6. If the result of serializing referrerURL is a string whose length is |
| // greater than 4096, set referrerURL to referrerOrigin. |
| if (referrerURL.toString().length > 4096) { |
| referrerURL = referrerOrigin |
| } |
| |
| const areSameOrigin = sameOrigin(request, referrerURL) |
| const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) && |
| !isURLPotentiallyTrustworthy(request.url) |
| |
| // 8. Execute the switch statements corresponding to the value of policy: |
| switch (policy) { |
| case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true) |
| case 'unsafe-url': return referrerURL |
| case 'same-origin': |
| return areSameOrigin ? referrerOrigin : 'no-referrer' |
| case 'origin-when-cross-origin': |
| return areSameOrigin ? referrerURL : referrerOrigin |
| case 'strict-origin-when-cross-origin': { |
| const currentURL = requestCurrentURL(request) |
| |
| // 1. If the origin of referrerURL and the origin of request’s current |
| // URL are the same, then return referrerURL. |
| if (sameOrigin(referrerURL, currentURL)) { |
| return referrerURL |
| } |
| |
| // 2. If referrerURL is a potentially trustworthy URL and request’s |
| // current URL is not a potentially trustworthy URL, then return no |
| // referrer. |
| if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) { |
| return 'no-referrer' |
| } |
| |
| // 3. Return referrerOrigin. |
| return referrerOrigin |
| } |
| case 'strict-origin': // eslint-disable-line |
| /** |
| * 1. If referrerURL is a potentially trustworthy URL and |
| * request’s current URL is not a potentially trustworthy URL, |
| * then return no referrer. |
| * 2. Return referrerOrigin |
| */ |
| case 'no-referrer-when-downgrade': // eslint-disable-line |
| /** |
| * 1. If referrerURL is a potentially trustworthy URL and |
| * request’s current URL is not a potentially trustworthy URL, |
| * then return no referrer. |
| * 2. Return referrerOrigin |
| */ |
| |
| default: // eslint-disable-line |
| return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin |
| } |
| } |
| |
| /** |
| * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url |
| * @param {URL} url |
| * @param {boolean|undefined} originOnly |
| */ |
| function stripURLForReferrer (url, originOnly) { |
| // 1. Assert: url is a URL. |
| assert(url instanceof URL) |
| |
| // 2. If url’s scheme is a local scheme, then return no referrer. |
| if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') { |
| return 'no-referrer' |
| } |
| |
| // 3. Set url’s username to the empty string. |
| url.username = '' |
| |
| // 4. Set url’s password to the empty string. |
| url.password = '' |
| |
| // 5. Set url’s fragment to null. |
| url.hash = '' |
| |
| // 6. If the origin-only flag is true, then: |
| if (originOnly) { |
| // 1. Set url’s path to « the empty string ». |
| url.pathname = '' |
| |
| // 2. Set url’s query to null. |
| url.search = '' |
| } |
| |
| // 7. Return url. |
| return url |
| } |
| |
| function isURLPotentiallyTrustworthy (url) { |
| if (!(url instanceof URL)) { |
| return false |
| } |
| |
| // If child of about, return true |
| if (url.href === 'about:blank' || url.href === 'about:srcdoc') { |
| return true |
| } |
| |
| // If scheme is data, return true |
| if (url.protocol === 'data:') return true |
| |
| // If file, return true |
| if (url.protocol === 'file:') return true |
| |
| return isOriginPotentiallyTrustworthy(url.origin) |
| |
| function isOriginPotentiallyTrustworthy (origin) { |
| // If origin is explicitly null, return false |
| if (origin == null || origin === 'null') return false |
| |
| const originAsURL = new URL(origin) |
| |
| // If secure, return true |
| if (originAsURL.protocol === 'https:' || originAsURL.protocol === 'wss:') { |
| return true |
| } |
| |
| // If localhost or variants, return true |
| if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) || |
| (originAsURL.hostname === 'localhost' || originAsURL.hostname.includes('localhost.')) || |
| (originAsURL.hostname.endsWith('.localhost'))) { |
| return true |
| } |
| |
| // If any other, return false |
| return false |
| } |
| } |
| |
| /** |
| * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist |
| * @param {Uint8Array} bytes |
| * @param {string} metadataList |
| */ |
| function bytesMatch (bytes, metadataList) { |
| // If node is not built with OpenSSL support, we cannot check |
| // a request's integrity, so allow it by default (the spec will |
| // allow requests if an invalid hash is given, as precedence). |
| /* istanbul ignore if: only if node is built with --without-ssl */ |
| if (crypto === undefined) { |
| return true |
| } |
| |
| // 1. Let parsedMetadata be the result of parsing metadataList. |
| const parsedMetadata = parseMetadata(metadataList) |
| |
| // 2. If parsedMetadata is no metadata, return true. |
| if (parsedMetadata === 'no metadata') { |
| return true |
| } |
| |
| // 3. If parsedMetadata is the empty set, return true. |
| if (parsedMetadata.length === 0) { |
| return true |
| } |
| |
| // 4. Let metadata be the result of getting the strongest |
| // metadata from parsedMetadata. |
| const list = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo)) |
| // get the strongest algorithm |
| const strongest = list[0].algo |
| // get all entries that use the strongest algorithm; ignore weaker |
| const metadata = list.filter((item) => item.algo === strongest) |
| |
| // 5. For each item in metadata: |
| for (const item of metadata) { |
| // 1. Let algorithm be the alg component of item. |
| const algorithm = item.algo |
| |
| // 2. Let expectedValue be the val component of item. |
| let expectedValue = item.hash |
| |
| // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e |
| // "be liberal with padding". This is annoying, and it's not even in the spec. |
| |
| if (expectedValue.endsWith('==')) { |
| expectedValue = expectedValue.slice(0, -2) |
| } |
| |
| // 3. Let actualValue be the result of applying algorithm to bytes. |
| let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') |
| |
| if (actualValue.endsWith('==')) { |
| actualValue = actualValue.slice(0, -2) |
| } |
| |
| // 4. If actualValue is a case-sensitive match for expectedValue, |
| // return true. |
| if (actualValue === expectedValue) { |
| return true |
| } |
| |
| let actualBase64URL = crypto.createHash(algorithm).update(bytes).digest('base64url') |
| |
| if (actualBase64URL.endsWith('==')) { |
| actualBase64URL = actualBase64URL.slice(0, -2) |
| } |
| |
| if (actualBase64URL === expectedValue) { |
| return true |
| } |
| } |
| |
| // 6. Return false. |
| return false |
| } |
| |
| // https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options |
| // https://www.w3.org/TR/CSP2/#source-list-syntax |
| // https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 |
| const parseHashWithOptions = /((?<algo>sha256|sha384|sha512)-(?<hash>[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i |
| |
| /** |
| * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata |
| * @param {string} metadata |
| */ |
| function parseMetadata (metadata) { |
| // 1. Let result be the empty set. |
| /** @type {{ algo: string, hash: string }[]} */ |
| const result = [] |
| |
| // 2. Let empty be equal to true. |
| let empty = true |
| |
| const supportedHashes = crypto.getHashes() |
| |
| // 3. For each token returned by splitting metadata on spaces: |
| for (const token of metadata.split(' ')) { |
| // 1. Set empty to false. |
| empty = false |
| |
| // 2. Parse token as a hash-with-options. |
| const parsedToken = parseHashWithOptions.exec(token) |
| |
| // 3. If token does not parse, continue to the next token. |
| if (parsedToken === null || parsedToken.groups === undefined) { |
| // Note: Chromium blocks the request at this point, but Firefox |
| // gives a warning that an invalid integrity was given. The |
| // correct behavior is to ignore these, and subsequently not |
| // check the integrity of the resource. |
| continue |
| } |
| |
| // 4. Let algorithm be the hash-algo component of token. |
| const algorithm = parsedToken.groups.algo |
| |
| // 5. If algorithm is a hash function recognized by the user |
| // agent, add the parsed token to result. |
| if (supportedHashes.includes(algorithm.toLowerCase())) { |
| result.push(parsedToken.groups) |
| } |
| } |
| |
| // 4. Return no metadata if empty is true, otherwise return result. |
| if (empty === true) { |
| return 'no metadata' |
| } |
| |
| return result |
| } |
| |
| // https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request |
| function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { |
| // TODO |
| } |
| |
| /** |
| * @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin} |
| * @param {URL} A |
| * @param {URL} B |
| */ |
| function sameOrigin (A, B) { |
| // 1. If A and B are the same opaque origin, then return true. |
| if (A.origin === B.origin && A.origin === 'null') { |
| return true |
| } |
| |
| // 2. If A and B are both tuple origins and their schemes, |
| // hosts, and port are identical, then return true. |
| if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) { |
| return true |
| } |
| |
| // 3. Return false. |
| return false |
| } |
| |
| function createDeferredPromise () { |
| let res |
| let rej |
| const promise = new Promise((resolve, reject) => { |
| res = resolve |
| rej = reject |
| }) |
| |
| return { promise, resolve: res, reject: rej } |
| } |
| |
| function isAborted (fetchParams) { |
| return fetchParams.controller.state === 'aborted' |
| } |
| |
| function isCancelled (fetchParams) { |
| return fetchParams.controller.state === 'aborted' || |
| fetchParams.controller.state === 'terminated' |
| } |
| |
| const normalizeMethodRecord = { |
| delete: 'DELETE', |
| DELETE: 'DELETE', |
| get: 'GET', |
| GET: 'GET', |
| head: 'HEAD', |
| HEAD: 'HEAD', |
| options: 'OPTIONS', |
| OPTIONS: 'OPTIONS', |
| post: 'POST', |
| POST: 'POST', |
| put: 'PUT', |
| PUT: 'PUT' |
| } |
| |
| // Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. |
| Object.setPrototypeOf(normalizeMethodRecord, null) |
| |
| /** |
| * @see https://fetch.spec.whatwg.org/#concept-method-normalize |
| * @param {string} method |
| */ |
| function normalizeMethod (method) { |
| return normalizeMethodRecord[method.toLowerCase()] ?? method |
| } |
| |
| // https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string |
| function serializeJavascriptValueToJSONString (value) { |
| // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »). |
| const result = JSON.stringify(value) |
| |
| // 2. If result is undefined, then throw a TypeError. |
| if (result === undefined) { |
| throw new TypeError('Value is not JSON serializable') |
| } |
| |
| // 3. Assert: result is a string. |
| assert(typeof result === 'string') |
| |
| // 4. Return result. |
| return result |
| } |
| |
| // https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object |
| const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) |
| |
| /** |
| * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object |
| * @param {() => unknown[]} iterator |
| * @param {string} name name of the instance |
| * @param {'key'|'value'|'key+value'} kind |
| */ |
| function makeIterator (iterator, name, kind) { |
| const object = { |
| index: 0, |
| kind, |
| target: iterator |
| } |
| |
| const i = { |
| next () { |
| // 1. Let interface be the interface for which the iterator prototype object exists. |
| |
| // 2. Let thisValue be the this value. |
| |
| // 3. Let object be ? ToObject(thisValue). |
| |
| // 4. If object is a platform object, then perform a security |
| // check, passing: |
| |
| // 5. If object is not a default iterator object for interface, |
| // then throw a TypeError. |
| if (Object.getPrototypeOf(this) !== i) { |
| throw new TypeError( |
| `'next' called on an object that does not implement interface ${name} Iterator.` |
| ) |
| } |
| |
| // 6. Let index be object’s index. |
| // 7. Let kind be object’s kind. |
| // 8. Let values be object’s target's value pairs to iterate over. |
| const { index, kind, target } = object |
| const values = target() |
| |
| // 9. Let len be the length of values. |
| const len = values.length |
| |
| // 10. If index is greater than or equal to len, then return |
| // CreateIterResultObject(undefined, true). |
| if (index >= len) { |
| return { value: undefined, done: true } |
| } |
| |
| // 11. Let pair be the entry in values at index index. |
| const pair = values[index] |
| |
| // 12. Set object’s index to index + 1. |
| object.index = index + 1 |
| |
| // 13. Return the iterator result for pair and kind. |
| return iteratorResult(pair, kind) |
| }, |
| // The class string of an iterator prototype object for a given interface is the |
| // result of concatenating the identifier of the interface and the string " Iterator". |
| [Symbol.toStringTag]: `${name} Iterator` |
| } |
| |
| // The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%. |
| Object.setPrototypeOf(i, esIteratorPrototype) |
| // esIteratorPrototype needs to be the prototype of i |
| // which is the prototype of an empty object. Yes, it's confusing. |
| return Object.setPrototypeOf({}, i) |
| } |
| |
| // https://webidl.spec.whatwg.org/#iterator-result |
| function iteratorResult (pair, kind) { |
| let result |
| |
| // 1. Let result be a value determined by the value of kind: |
| switch (kind) { |
| case 'key': { |
| // 1. Let idlKey be pair’s key. |
| // 2. Let key be the result of converting idlKey to an |
| // ECMAScript value. |
| // 3. result is key. |
| result = pair[0] |
| break |
| } |
| case 'value': { |
| // 1. Let idlValue be pair’s value. |
| // 2. Let value be the result of converting idlValue to |
| // an ECMAScript value. |
| // 3. result is value. |
| result = pair[1] |
| break |
| } |
| case 'key+value': { |
| // 1. Let idlKey be pair’s key. |
| // 2. Let idlValue be pair’s value. |
| // 3. Let key be the result of converting idlKey to an |
| // ECMAScript value. |
| // 4. Let value be the result of converting idlValue to |
| // an ECMAScript value. |
| // 5. Let array be ! ArrayCreate(2). |
| // 6. Call ! CreateDataProperty(array, "0", key). |
| // 7. Call ! CreateDataProperty(array, "1", value). |
| // 8. result is array. |
| result = pair |
| break |
| } |
| } |
| |
| // 2. Return CreateIterResultObject(result, false). |
| return { value: result, done: false } |
| } |
| |
| /** |
| * @see https://fetch.spec.whatwg.org/#body-fully-read |
| */ |
| async function fullyReadBody (body, processBody, processBodyError) { |
| // 1. If taskDestination is null, then set taskDestination to |
| // the result of starting a new parallel queue. |
| |
| // 2. Let successSteps given a byte sequence bytes be to queue a |
| // fetch task to run processBody given bytes, with taskDestination. |
| const successSteps = processBody |
| |
| // 3. Let errorSteps be to queue a fetch task to run processBodyError, |
| // with taskDestination. |
| const errorSteps = processBodyError |
| |
| // 4. Let reader be the result of getting a reader for body’s stream. |
| // If that threw an exception, then run errorSteps with that |
| // exception and return. |
| let reader |
| |
| try { |
| reader = body.stream.getReader() |
| } catch (e) { |
| errorSteps(e) |
| return |
| } |
| |
| // 5. Read all bytes from reader, given successSteps and errorSteps. |
| try { |
| const result = await readAllBytes(reader) |
| successSteps(result) |
| } catch (e) { |
| errorSteps(e) |
| } |
| } |
| |
| /** @type {ReadableStream} */ |
| let ReadableStream = globalThis.ReadableStream |
| |
| function isReadableStreamLike (stream) { |
| if (!ReadableStream) { |
| ReadableStream = require('stream/web').ReadableStream |
| } |
| |
| return stream instanceof ReadableStream || ( |
| stream[Symbol.toStringTag] === 'ReadableStream' && |
| typeof stream.tee === 'function' |
| ) |
| } |
| |
| const MAXIMUM_ARGUMENT_LENGTH = 65535 |
| |
| /** |
| * @see https://infra.spec.whatwg.org/#isomorphic-decode |
| * @param {number[]|Uint8Array} input |
| */ |
| function isomorphicDecode (input) { |
| // 1. To isomorphic decode a byte sequence input, return a string whose code point |
| // length is equal to input’s length and whose code points have the same values |
| // as the values of input’s bytes, in the same order. |
| |
| if (input.length < MAXIMUM_ARGUMENT_LENGTH) { |
| return String.fromCharCode(...input) |
| } |
| |
| return input.reduce((previous, current) => previous + String.fromCharCode(current), '') |
| } |
| |
| /** |
| * @param {ReadableStreamController<Uint8Array>} controller |
| */ |
| function readableStreamClose (controller) { |
| try { |
| controller.close() |
| } catch (err) { |
| // TODO: add comment explaining why this error occurs. |
| if (!err.message.includes('Controller is already closed')) { |
| throw err |
| } |
| } |
| } |
| |
| /** |
| * @see https://infra.spec.whatwg.org/#isomorphic-encode |
| * @param {string} input |
| */ |
| function isomorphicEncode (input) { |
| // 1. Assert: input contains no code points greater than U+00FF. |
| for (let i = 0; i < input.length; i++) { |
| assert(input.charCodeAt(i) <= 0xFF) |
| } |
| |
| // 2. Return a byte sequence whose length is equal to input’s code |
| // point length and whose bytes have the same values as the |
| // values of input’s code points, in the same order |
| return input |
| } |
| |
| /** |
| * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes |
| * @see https://streams.spec.whatwg.org/#read-loop |
| * @param {ReadableStreamDefaultReader} reader |
| */ |
| async function readAllBytes (reader) { |
| const bytes = [] |
| let byteLength = 0 |
| |
| while (true) { |
| const { done, value: chunk } = await reader.read() |
| |
| if (done) { |
| // 1. Call successSteps with bytes. |
| return Buffer.concat(bytes, byteLength) |
| } |
| |
| // 1. If chunk is not a Uint8Array object, call failureSteps |
| // with a TypeError and abort these steps. |
| if (!isUint8Array(chunk)) { |
| throw new TypeError('Received non-Uint8Array chunk') |
| } |
| |
| // 2. Append the bytes represented by chunk to bytes. |
| bytes.push(chunk) |
| byteLength += chunk.length |
| |
| // 3. Read-loop given reader, bytes, successSteps, and failureSteps. |
| } |
| } |
| |
| /** |
| * @see https://fetch.spec.whatwg.org/#is-local |
| * @param {URL} url |
| */ |
| function urlIsLocal (url) { |
| assert('protocol' in url) // ensure it's a url object |
| |
| const protocol = url.protocol |
| |
| return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:' |
| } |
| |
| /** |
| * @param {string|URL} url |
| */ |
| function urlHasHttpsScheme (url) { |
| if (typeof url === 'string') { |
| return url.startsWith('https:') |
| } |
| |
| return url.protocol === 'https:' |
| } |
| |
| /** |
| * @see https://fetch.spec.whatwg.org/#http-scheme |
| * @param {URL} url |
| */ |
| function urlIsHttpHttpsScheme (url) { |
| assert('protocol' in url) // ensure it's a url object |
| |
| const protocol = url.protocol |
| |
| return protocol === 'http:' || protocol === 'https:' |
| } |
| |
| /** |
| * Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0. |
| */ |
| const hasOwn = Object.hasOwn || ((dict, key) => Object.prototype.hasOwnProperty.call(dict, key)) |
| |
| module.exports = { |
| isAborted, |
| isCancelled, |
| createDeferredPromise, |
| ReadableStreamFrom, |
| toUSVString, |
| tryUpgradeRequestToAPotentiallyTrustworthyURL, |
| coarsenedSharedCurrentTime, |
| determineRequestsReferrer, |
| makePolicyContainer, |
| clonePolicyContainer, |
| appendFetchMetadata, |
| appendRequestOriginHeader, |
| TAOCheck, |
| corsCheck, |
| crossOriginResourcePolicyCheck, |
| createOpaqueTimingInfo, |
| setRequestReferrerPolicyOnRedirect, |
| isValidHTTPToken, |
| requestBadPort, |
| requestCurrentURL, |
| responseURL, |
| responseLocationURL, |
| isBlobLike, |
| isURLPotentiallyTrustworthy, |
| isValidReasonPhrase, |
| sameOrigin, |
| normalizeMethod, |
| serializeJavascriptValueToJSONString, |
| makeIterator, |
| isValidHeaderName, |
| isValidHeaderValue, |
| hasOwn, |
| isErrorLike, |
| fullyReadBody, |
| bytesMatch, |
| isReadableStreamLike, |
| readableStreamClose, |
| isomorphicEncode, |
| isomorphicDecode, |
| urlIsLocal, |
| urlHasHttpsScheme, |
| urlIsHttpHttpsScheme, |
| readAllBytes, |
| normalizeMethodRecord |
| } |