'use strict'; const { Transform } = require('node:stream'); const zlib = require('node:zlib'); const { redirectStatusSet, referrerPolicyTokens, badPortsSet, } = require('./constants'); const { getGlobalOrigin } = require('./global'); const { collectASequenceOfCodePoints, collectAnHTTPQuotedString, removeChars, parseMIMEType, } = require('./data-url'); const { performance } = require('node:perf_hooks'); const { ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase, } = require('../../core/util'); const assert = require('node:assert'); const { isUint8Array } = require('node:util/types'); const { webidl } = require('./webidl'); let supportedHashes = []; // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable /** @type {import('crypto')} */ let crypto; try { crypto = require('node:crypto'); const possibleRelevantHashes = ['sha256', 'sha384', 'sha512']; supportedHashes = crypto .getHashes() .filter((hash) => possibleRelevantHashes.includes(hash)); /* c8 ignore next 3 */ } 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', true); // 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)) { if (!isValidEncodedURL(location)) { // Some websites respond location header in UTF-8 form without encoding them as ASCII // and major browsers redirect them to correctly UTF-8 encoded addresses. // Here, we handle that behavior in the same way. location = normalizeBinaryStringToUtf8(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; } /** * @see https://www.rfc-editor.org/rfc/rfc1738#section-2.2 * @param {string} url * @returns {boolean} */ function isValidEncodedURL(url) { for (let i = 0; i < url.length; ++i) { const code = url.charCodeAt(i); if ( code > 0x7e || // Non-US-ASCII + DEL code < 0x20 // Control characters NUL - US ) { return false; } } return true; } /** * If string contains non-ASCII characters, assumes it's UTF-8 encoded and decodes it. * Since UTF-8 is a superset of ASCII, this will work for ASCII strings as well. * @param {string} value * @returns {string} */ function normalizeBinaryStringToUtf8(value) { return Buffer.from(value, 'binary').toString('utf8'); } /** @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://fetch.spec.whatwg.org/#header-name * @param {string} potentialValue */ const isValidHeaderName = isValidHTTPToken; /** * @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. return ( (potentialValue[0] === '\t' || potentialValue[0] === ' ' || potentialValue[potentialValue.length - 1] === '\t' || potentialValue[potentialValue.length - 1] === ' ' || potentialValue.includes('\n') || potentialValue.includes('\r') || potentialValue.includes('\0')) === false ); } /** * Parse a referrer policy from a Referrer-Policy header * @see https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header */ function parseReferrerPolicy(actualResponse) { // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list. const policyHeader = ( actualResponse.headersList.get('referrer-policy', true) ?? '' ).split(','); // 2. Let policy be the empty string. let policy = ''; // 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. // 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 if (policyHeader.length) { // 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; } } } // 4. Return policy. return policy; } /** * 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. * @see https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect * @param {import('./request').Request} request * @param {import('./response').Response} actualResponse */ function setRequestReferrerPolicyOnRedirect(request, actualResponse) { // 1. Let policy be the result of executing § 8.1 Parse a referrer policy // from a Referrer-Policy header on actualResponse. const policy = parseReferrerPolicy(actualResponse); // 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, true); // 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. // TODO: implement "byte-serializing a request origin" let serializedOrigin = request.origin; // - "'client' is changed to an origin during fetching." // This doesn't happen in undici (in most cases) because undici, by default, // has no concept of origin. // - request.origin can also be set to request.client.origin (client being // an environment settings object), which is undefined without using // setGlobalOrigin. if (serializedOrigin === 'client' || serializedOrigin === undefined) { return; } // 2. If request’s response tainting is "cors" or request’s mode is "websocket", // then append (`Origin`, serializedOrigin) to request’s header list. // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: if (request.responseTainting === 'cors' || request.mode === 'websocket') { request.headersList.append('origin', serializedOrigin, true); } 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. } // 2. Append (`Origin`, serializedOrigin) to request’s header list. request.headersList.append('origin', serializedOrigin, true); } } // https://w3c.github.io/hr-time/#dfn-coarsen-time function coarsenTime(timestamp, crossOriginIsolatedCapability) { // TODO return timestamp; } // https://fetch.spec.whatwg.org/#clamp-and-coarsen-connection-timing-info function clampAndCoarsenConnectionTimingInfo( connectionTimingInfo, defaultStartTime, crossOriginIsolatedCapability ) { if ( !connectionTimingInfo?.startTime || connectionTimingInfo.startTime < defaultStartTime ) { return { domainLookupStartTime: defaultStartTime, domainLookupEndTime: defaultStartTime, connectionStartTime: defaultStartTime, connectionEndTime: defaultStartTime, secureConnectionStartTime: defaultStartTime, ALPNNegotiatedProtocol: connectionTimingInfo?.ALPNNegotiatedProtocol, }; } return { domainLookupStartTime: coarsenTime( connectionTimingInfo.domainLookupStartTime, crossOriginIsolatedCapability ), domainLookupEndTime: coarsenTime( connectionTimingInfo.domainLookupEndTime, crossOriginIsolatedCapability ), connectionStartTime: coarsenTime( connectionTimingInfo.connectionStartTime, crossOriginIsolatedCapability ), connectionEndTime: coarsenTime( connectionTimingInfo.connectionEndTime, crossOriginIsolatedCapability ), secureConnectionStartTime: coarsenTime( connectionTimingInfo.secureConnectionStartTime, crossOriginIsolatedCapability ), ALPNNegotiatedProtocol: connectionTimingInfo.ALPNNegotiatedProtocol, }; } // https://w3c.github.io/hr-time/#dfn-coarsened-shared-current-time function coarsenedSharedCurrentTime(crossOriginIsolatedCapability) { return coarsenTime(performance.now(), crossOriginIsolatedCapability); } // 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, }; } /** * Determine request’s Referrer * * @see https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer */ function determineRequestsReferrer(request) { // Given a request request, we can determine the correct referrer information // to send by examining its referrer policy as detailed in the following // steps, which return either no referrer or a URL: // 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: // "client" 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); // a URL } else if (webidl.is.URL(request.referrer)) { // 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; } // 7. The user agent MAY alter referrerURL or referrerOrigin at this point // to enforce arbitrary policy considerations in the interests of minimizing // data leakage. For example, the user agent could strip the URL down to an // origin, modify its host, replace it with an empty string, etc. // 8. Execute the switch statements corresponding to the value of policy: switch (policy) { case 'no-referrer': // Return no referrer return 'no-referrer'; case 'origin': // Return referrerOrigin if (referrerOrigin != null) { return referrerOrigin; } return stripURLForReferrer(referrerSource, true); case 'unsafe-url': // Return referrerURL. return referrerURL; case 'strict-origin': { const currentURL = requestCurrentURL(request); // 1. 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'; } // 2. Return referrerOrigin return 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 'same-origin': // 1. If the origin of referrerURL and the origin of request’s current // URL are the same, then return referrerURL. if (sameOrigin(request, referrerURL)) { return referrerURL; } // 2. Return no referrer. return 'no-referrer'; case 'origin-when-cross-origin': // 1. If the origin of referrerURL and the origin of request’s current // URL are the same, then return referrerURL. if (sameOrigin(request, referrerURL)) { return referrerURL; } // 2. Return referrerOrigin. return referrerOrigin; case 'no-referrer-when-downgrade': { const currentURL = requestCurrentURL(request); // 1. 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'; } // 2. Return referrerOrigin return referrerOrigin; } } } /** * Certain portions of URLs must not be included when sending a URL as the * value of a `Referer` header: a URLs fragment, username, and password * components must be stripped from the URL before it’s sent out. This * algorithm accepts a origin-only flag, which defaults to false. If set to * true, the algorithm will additionally remove the URL’s path and query * components, leaving only the scheme, host, and port. * * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url * @param {URL} url * @param {boolean} [originOnly=false] */ function stripURLForReferrer(url, originOnly = false) { // 1. Assert: url is a URL. assert(webidl.is.URL(url)); // Note: Create a new URL instance to avoid mutating the original URL. url = new URL(url); // 2. If url’s scheme is a local scheme, then return no referrer. if (urlIsLocal(url)) { 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 === true) { // 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; } const potentialleTrustworthyIPv4RegExp = new RegExp( '^(?:' + '(?:127\\.)' + '(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){2}' + '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])' + ')$' ); const potentialleTrustworthyIPv6RegExp = new RegExp( '^(?:' + '(?:(?:0{1,4}):){7}(?:(?:0{0,3}1))|' + '(?:(?:0{1,4}):){1,6}(?::(?:0{0,3}1))|' + '(?:::(?:0{0,3}1))|' + ')$' ); /** * Check if host matches one of the CIDR notations 127.0.0.0/8 or ::1/128. * * @param {string} origin * @returns {boolean} */ function isOriginIPPotentiallyTrustworthy(origin) { // IPv6 if (origin.includes(':')) { // Remove brackets from IPv6 addresses if (origin[0] === '[' && origin[origin.length - 1] === ']') { origin = origin.slice(1, -1); } return potentialleTrustworthyIPv6RegExp.test(origin); } // IPv4 return potentialleTrustworthyIPv4RegExp.test(origin); } /** * A potentially trustworthy origin is one which a user agent can generally * trust as delivering data securely. * * Return value `true` means `Potentially Trustworthy`. * Return value `false` means `Not Trustworthy`. * * @see https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy * @param {string} origin * @returns {boolean} */ function isOriginPotentiallyTrustworthy(origin) { // 1. If origin is an opaque origin, return "Not Trustworthy". if (origin == null || origin === 'null') { return false; } // 2. Assert: origin is a tuple origin. origin = new URL(origin); // 3. If origin’s scheme is either "https" or "wss", // return "Potentially Trustworthy". if (origin.protocol === 'https:' || origin.protocol === 'wss:') { return true; } // 4. If origin’s host matches one of the CIDR notations 127.0.0.0/8 or // ::1/128 [RFC4632], return "Potentially Trustworthy". if (isOriginIPPotentiallyTrustworthy(origin.hostname)) { return true; } // 5. If the user agent conforms to the name resolution rules in // [let-localhost-be-localhost] and one of the following is true: // origin’s host is "localhost" or "localhost." if (origin.hostname === 'localhost' || origin.hostname === 'localhost.') { return true; } // origin’s host ends with ".localhost" or ".localhost." if ( origin.hostname.endsWith('.localhost') || origin.hostname.endsWith('.localhost.') ) { return true; } // 6. If origin’s scheme is "file", return "Potentially Trustworthy". if (origin.protocol === 'file:') { return true; } // 7. If origin’s scheme component is one which the user agent considers to // be authenticated, return "Potentially Trustworthy". // 8. If origin has been configured as a trustworthy origin, return // "Potentially Trustworthy". // 9. Return "Not Trustworthy". return false; } /** * A potentially trustworthy URL is one which either inherits context from its * creator (about:blank, about:srcdoc, data) or one whose origin is a * potentially trustworthy origin. * * Return value `true` means `Potentially Trustworthy`. * Return value `false` means `Not Trustworthy`. * * @see https://www.w3.org/TR/secure-contexts/#is-url-trustworthy * @param {URL} url * @returns {boolean} */ function isURLPotentiallyTrustworthy(url) { // Given a URL record (url), the following algorithm returns "Potentially // Trustworthy" or "Not Trustworthy" as appropriate: if (!webidl.is.URL(url)) { return false; } // 1. If url is "about:blank" or "about:srcdoc", // return "Potentially Trustworthy". if (url.href === 'about:blank' || url.href === 'about:srcdoc') { return true; } // 2. If url’s scheme is "data", return "Potentially Trustworthy". if (url.protocol === 'data:') return true; // Note: The origin of blob: URLs is the origin of the context in which they // were created. Therefore, blobs created in a trustworthy origin will // themselves be potentially trustworthy. if (url.protocol === 'blob:') return true; // 3. Return the result of executing § 3.1 Is origin potentially trustworthy? // on url’s origin. return isOriginPotentiallyTrustworthy(url.origin); } /** * @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 response is not eligible for integrity validation, return false. // TODO // 4. If parsedMetadata is the empty set, return true. if (parsedMetadata.length === 0) { return true; } // 5. Let metadata be the result of getting the strongest // metadata from parsedMetadata. const strongest = getStrongestMetadata(parsedMetadata); const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest); // 6. 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. const 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. // 3. Let actualValue be the result of applying algorithm to bytes. let actualValue = crypto .createHash(algorithm) .update(bytes) .digest('base64'); if (actualValue[actualValue.length - 1] === '=') { if (actualValue[actualValue.length - 2] === '=') { actualValue = actualValue.slice(0, -2); } else { actualValue = actualValue.slice(0, -1); } } // 4. If actualValue is a case-sensitive match for expectedValue, // return true. if (compareBase64Mixed(actualValue, expectedValue)) { return true; } } // 7. 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 = /(?sha256|sha384|sha512)-((?[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/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; // 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 || parsedToken.groups.algo === 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.toLowerCase(); // 5. If algorithm is a hash function recognized by the user // agent, add the parsed token to result. if (supportedHashes.includes(algorithm)) { result.push(parsedToken.groups); } } // 4. Return no metadata if empty is true, otherwise return result. if (empty === true) { return 'no metadata'; } return result; } /** * @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList */ function getStrongestMetadata(metadataList) { // Let algorithm be the algo component of the first item in metadataList. // Can be sha256 let algorithm = metadataList[0].algo; // If the algorithm is sha512, then it is the strongest // and we can return immediately if (algorithm[3] === '5') { return algorithm; } for (let i = 1; i < metadataList.length; ++i) { const metadata = metadataList[i]; // If the algorithm is sha512, then it is the strongest // and we can break the loop immediately if (metadata.algo[3] === '5') { algorithm = 'sha512'; break; // If the algorithm is sha384, then a potential sha256 or sha384 is ignored } else if (algorithm[3] === '3') { continue; // algorithm is sha256, check if algorithm is sha384 and if so, set it as // the strongest } else if (metadata.algo[3] === '3') { algorithm = 'sha384'; } } return algorithm; } function filterMetadataListByAlgorithm(metadataList, algorithm) { if (metadataList.length === 1) { return metadataList; } let pos = 0; for (let i = 0; i < metadataList.length; ++i) { if (metadataList[i].algo === algorithm) { metadataList[pos++] = metadataList[i]; } } metadataList.length = pos; return metadataList; } /** * Compares two base64 strings, allowing for base64url * in the second string. * * @param {string} actualValue always base64 * @param {string} expectedValue base64 or base64url * @returns {boolean} */ function compareBase64Mixed(actualValue, expectedValue) { if (actualValue.length !== expectedValue.length) { return false; } for (let i = 0; i < actualValue.length; ++i) { if (actualValue[i] !== expectedValue[i]) { if ( (actualValue[i] === '+' && expectedValue[i] === '-') || (actualValue[i] === '/' && expectedValue[i] === '_') ) { continue; } return false; } } return true; } // 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' ); } /** * @see https://fetch.spec.whatwg.org/#concept-method-normalize * @param {string} method */ function normalizeMethod(method) { return normalizedMethodRecordsBase[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 {string} name name of the instance * @param {((target: any) => any)} kInternalIterator * @param {string | number} [keyIndex] * @param {string | number} [valueIndex] */ function createIterator(name, kInternalIterator, keyIndex = 0, valueIndex = 1) { class FastIterableIterator { /** @type {any} */ #target; /** @type {'key' | 'value' | 'key+value'} */ #kind; /** @type {number} */ #index; /** * @see https://webidl.spec.whatwg.org/#dfn-default-iterator-object * @param {unknown} target * @param {'key' | 'value' | 'key+value'} kind */ constructor(target, kind) { this.#target = target; this.#kind = kind; this.#index = 0; } 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 (typeof this !== 'object' || this === null || !(#target in this)) { 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 = this.#index; const values = kInternalIterator(this.#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 { [keyIndex]: key, [valueIndex]: value } = values[index]; // 12. Set object’s index to index + 1. this.#index = index + 1; // 13. Return the iterator result for pair and kind. // https://webidl.spec.whatwg.org/#iterator-result // 1. Let result be a value determined by the value of kind: let result; switch (this.#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 = key; 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 = value; 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 = [key, value]; break; } // 2. Return CreateIterResultObject(result, false). return { value: result, done: false, }; } } // https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object // @ts-ignore delete FastIterableIterator.prototype.constructor; Object.setPrototypeOf(FastIterableIterator.prototype, esIteratorPrototype); Object.defineProperties(FastIterableIterator.prototype, { [Symbol.toStringTag]: { writable: false, enumerable: false, configurable: true, value: `${name} Iterator`, }, next: { writable: true, enumerable: true, configurable: true }, }); /** * @param {unknown} target * @param {'key' | 'value' | 'key+value'} kind * @returns {IterableIterator} */ return function (target, kind) { return new FastIterableIterator(target, kind); }; } /** * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object * @param {string} name name of the instance * @param {any} object class * @param {(target: any) => any} kInternalIterator * @param {string | number} [keyIndex] * @param {string | number} [valueIndex] */ function iteratorMixin( name, object, kInternalIterator, keyIndex = 0, valueIndex = 1 ) { const makeIterator = createIterator( name, kInternalIterator, keyIndex, valueIndex ); const properties = { keys: { writable: true, enumerable: true, configurable: true, value: function keys() { webidl.brandCheck(this, object); return makeIterator(this, 'key'); }, }, values: { writable: true, enumerable: true, configurable: true, value: function values() { webidl.brandCheck(this, object); return makeIterator(this, 'value'); }, }, entries: { writable: true, enumerable: true, configurable: true, value: function entries() { webidl.brandCheck(this, object); return makeIterator(this, 'key+value'); }, }, forEach: { writable: true, enumerable: true, configurable: true, value: function forEach(callbackfn, thisArg = globalThis) { webidl.brandCheck(this, object); webidl.argumentLengthCheck(arguments, 1, `${name}.forEach`); if (typeof callbackfn !== 'function') { throw new TypeError( `Failed to execute 'forEach' on '${name}': parameter 1 is not of type 'Function'.` ); } for (const { 0: key, 1: value } of makeIterator(this, 'key+value')) { callbackfn.call(thisArg, value, key, this); } }, }, }; return Object.defineProperties(object.prototype, { ...properties, [Symbol.iterator]: { writable: true, enumerable: false, configurable: true, value: properties.entries.value, }, }); } /** * @see https://fetch.spec.whatwg.org/#body-fully-read */ 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. readAllBytes(reader, successSteps, errorSteps); } /** * @param {ReadableStreamController} controller */ function readableStreamClose(controller) { try { controller.close(); controller.byobRequest?.respond(0); } catch (err) { // TODO: add comment explaining why this error occurs. if ( !err.message.includes('Controller is already closed') && !err.message.includes('ReadableStream is already closed') ) { throw err; } } } const invalidIsomorphicEncodeValueRegex = /[^\x00-\xFF]/; // eslint-disable-line /** * @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. assert(!invalidIsomorphicEncodeValueRegex.test(input)); // 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 * @param {(bytes: Uint8Array) => void} successSteps * @param {(error: Error) => void} failureSteps */ async function readAllBytes(reader, successSteps, failureSteps) { const bytes = []; let byteLength = 0; try { do { const { done, value: chunk } = await reader.read(); if (done) { // 1. Call successSteps with bytes. successSteps(Buffer.concat(bytes, byteLength)); return; } // 1. If chunk is not a Uint8Array object, call failureSteps // with a TypeError and abort these steps. if (!isUint8Array(chunk)) { failureSteps(TypeError('Received non-Uint8Array chunk')); return; } // 2. Append the bytes represented by chunk to bytes. bytes.push(chunk); byteLength += chunk.length; // 3. Read-loop given reader, bytes, successSteps, and failureSteps. } while (true); } catch (e) { // 1. Call failureSteps with e. failureSteps(e); } } /** * @see https://fetch.spec.whatwg.org/#is-local * @param {URL} url * @returns {boolean} */ function urlIsLocal(url) { assert('protocol' in url); // ensure it's a url object const protocol = url.protocol; // A URL is local if its scheme is a local scheme. // A local scheme is "about", "blob", or "data". return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:'; } /** * @param {string|URL} url * @returns {boolean} */ function urlHasHttpsScheme(url) { return ( (typeof url === 'string' && url[5] === ':' && url[0] === 'h' && url[1] === 't' && url[2] === 't' && url[3] === 'p' && url[4] === 's') || 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:'; } /** * @see https://fetch.spec.whatwg.org/#simple-range-header-value * @param {string} value * @param {boolean} allowWhitespace */ function simpleRangeHeaderValue(value, allowWhitespace) { // 1. Let data be the isomorphic decoding of value. // Note: isomorphic decoding takes a sequence of bytes (ie. a Uint8Array) and turns it into a string, // nothing more. We obviously don't need to do that if value is a string already. const data = value; // 2. If data does not start with "bytes", then return failure. if (!data.startsWith('bytes')) { return 'failure'; } // 3. Let position be a position variable for data, initially pointing at the 5th code point of data. const position = { position: 5 }; // 4. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, // from data given position. if (allowWhitespace) { collectASequenceOfCodePoints( (char) => char === '\t' || char === ' ', data, position ); } // 5. If the code point at position within data is not U+003D (=), then return failure. if (data.charCodeAt(position.position) !== 0x3d) { return 'failure'; } // 6. Advance position by 1. position.position++; // 7. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from // data given position. if (allowWhitespace) { collectASequenceOfCodePoints( (char) => char === '\t' || char === ' ', data, position ); } // 8. Let rangeStart be the result of collecting a sequence of code points that are ASCII digits, // from data given position. const rangeStart = collectASequenceOfCodePoints( (char) => { const code = char.charCodeAt(0); return code >= 0x30 && code <= 0x39; }, data, position ); // 9. Let rangeStartValue be rangeStart, interpreted as decimal number, if rangeStart is not the // empty string; otherwise null. const rangeStartValue = rangeStart.length ? Number(rangeStart) : null; // 10. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, // from data given position. if (allowWhitespace) { collectASequenceOfCodePoints( (char) => char === '\t' || char === ' ', data, position ); } // 11. If the code point at position within data is not U+002D (-), then return failure. if (data.charCodeAt(position.position) !== 0x2d) { return 'failure'; } // 12. Advance position by 1. position.position++; // 13. If allowWhitespace is true, collect a sequence of code points that are HTTP tab // or space, from data given position. // Note from Khafra: its the same step as in #8 again lol if (allowWhitespace) { collectASequenceOfCodePoints( (char) => char === '\t' || char === ' ', data, position ); } // 14. Let rangeEnd be the result of collecting a sequence of code points that are // ASCII digits, from data given position. // Note from Khafra: you wouldn't guess it, but this is also the same step as #8 const rangeEnd = collectASequenceOfCodePoints( (char) => { const code = char.charCodeAt(0); return code >= 0x30 && code <= 0x39; }, data, position ); // 15. Let rangeEndValue be rangeEnd, interpreted as decimal number, if rangeEnd // is not the empty string; otherwise null. // Note from Khafra: THE SAME STEP, AGAIN!!! // Note: why interpret as a decimal if we only collect ascii digits? const rangeEndValue = rangeEnd.length ? Number(rangeEnd) : null; // 16. If position is not past the end of data, then return failure. if (position.position < data.length) { return 'failure'; } // 17. If rangeEndValue and rangeStartValue are null, then return failure. if (rangeEndValue === null && rangeStartValue === null) { return 'failure'; } // 18. If rangeStartValue and rangeEndValue are numbers, and rangeStartValue is // greater than rangeEndValue, then return failure. // Note: ... when can they not be numbers? if (rangeStartValue > rangeEndValue) { return 'failure'; } // 19. Return (rangeStartValue, rangeEndValue). return { rangeStartValue, rangeEndValue }; } /** * @see https://fetch.spec.whatwg.org/#build-a-content-range * @param {number} rangeStart * @param {number} rangeEnd * @param {number} fullLength */ function buildContentRange(rangeStart, rangeEnd, fullLength) { // 1. Let contentRange be `bytes `. let contentRange = 'bytes '; // 2. Append rangeStart, serialized and isomorphic encoded, to contentRange. contentRange += isomorphicEncode(`${rangeStart}`); // 3. Append 0x2D (-) to contentRange. contentRange += '-'; // 4. Append rangeEnd, serialized and isomorphic encoded to contentRange. contentRange += isomorphicEncode(`${rangeEnd}`); // 5. Append 0x2F (/) to contentRange. contentRange += '/'; // 6. Append fullLength, serialized and isomorphic encoded to contentRange. contentRange += isomorphicEncode(`${fullLength}`); // 7. Return contentRange. return contentRange; } // A Stream, which pipes the response to zlib.createInflate() or // zlib.createInflateRaw() depending on the first byte of the Buffer. // If the lower byte of the first byte is 0x08, then the stream is // interpreted as a zlib stream, otherwise it's interpreted as a // raw deflate stream. class InflateStream extends Transform { #zlibOptions; /** @param {zlib.ZlibOptions} [zlibOptions] */ constructor(zlibOptions) { super(); this.#zlibOptions = zlibOptions; } _transform(chunk, encoding, callback) { if (!this._inflateStream) { if (chunk.length === 0) { callback(); return; } this._inflateStream = (chunk[0] & 0x0f) === 0x08 ? zlib.createInflate(this.#zlibOptions) : zlib.createInflateRaw(this.#zlibOptions); this._inflateStream.on('data', this.push.bind(this)); this._inflateStream.on('end', () => this.push(null)); this._inflateStream.on('error', (err) => this.destroy(err)); } this._inflateStream.write(chunk, encoding, callback); } _final(callback) { if (this._inflateStream) { this._inflateStream.end(); this._inflateStream = null; } callback(); } } /** * @param {zlib.ZlibOptions} [zlibOptions] * @returns {InflateStream} */ function createInflate(zlibOptions) { return new InflateStream(zlibOptions); } /** * @see https://fetch.spec.whatwg.org/#concept-header-extract-mime-type * @param {import('./headers').HeadersList} headers */ function extractMimeType(headers) { // 1. Let charset be null. let charset = null; // 2. Let essence be null. let essence = null; // 3. Let mimeType be null. let mimeType = null; // 4. Let values be the result of getting, decoding, and splitting `Content-Type` from headers. const values = getDecodeSplit('content-type', headers); // 5. If values is null, then return failure. if (values === null) { return 'failure'; } // 6. For each value of values: for (const value of values) { // 6.1. Let temporaryMimeType be the result of parsing value. const temporaryMimeType = parseMIMEType(value); // 6.2. If temporaryMimeType is failure or its essence is "*/*", then continue. if ( temporaryMimeType === 'failure' || temporaryMimeType.essence === '*/*' ) { continue; } // 6.3. Set mimeType to temporaryMimeType. mimeType = temporaryMimeType; // 6.4. If mimeType’s essence is not essence, then: if (mimeType.essence !== essence) { // 6.4.1. Set charset to null. charset = null; // 6.4.2. If mimeType’s parameters["charset"] exists, then set charset to // mimeType’s parameters["charset"]. if (mimeType.parameters.has('charset')) { charset = mimeType.parameters.get('charset'); } // 6.4.3. Set essence to mimeType’s essence. essence = mimeType.essence; } else if (!mimeType.parameters.has('charset') && charset !== null) { // 6.5. Otherwise, if mimeType’s parameters["charset"] does not exist, and // charset is non-null, set mimeType’s parameters["charset"] to charset. mimeType.parameters.set('charset', charset); } } // 7. If mimeType is null, then return failure. if (mimeType == null) { return 'failure'; } // 8. Return mimeType. return mimeType; } /** * @see https://fetch.spec.whatwg.org/#header-value-get-decode-and-split * @param {string|null} value */ function gettingDecodingSplitting(value) { // 1. Let input be the result of isomorphic decoding value. const input = value; // 2. Let position be a position variable for input, initially pointing at the start of input. const position = { position: 0 }; // 3. Let values be a list of strings, initially empty. const values = []; // 4. Let temporaryValue be the empty string. let temporaryValue = ''; // 5. While position is not past the end of input: while (position.position < input.length) { // 5.1. Append the result of collecting a sequence of code points that are not U+0022 (") // or U+002C (,) from input, given position, to temporaryValue. temporaryValue += collectASequenceOfCodePoints( (char) => char !== '"' && char !== ',', input, position ); // 5.2. If position is not past the end of input, then: if (position.position < input.length) { // 5.2.1. If the code point at position within input is U+0022 ("), then: if (input.charCodeAt(position.position) === 0x22) { // 5.2.1.1. Append the result of collecting an HTTP quoted string from input, given position, to temporaryValue. temporaryValue += collectAnHTTPQuotedString(input, position); // 5.2.1.2. If position is not past the end of input, then continue. if (position.position < input.length) { continue; } } else { // 5.2.2. Otherwise: // 5.2.2.1. Assert: the code point at position within input is U+002C (,). assert(input.charCodeAt(position.position) === 0x2c); // 5.2.2.2. Advance position by 1. position.position++; } } // 5.3. Remove all HTTP tab or space from the start and end of temporaryValue. temporaryValue = removeChars( temporaryValue, true, true, (char) => char === 0x9 || char === 0x20 ); // 5.4. Append temporaryValue to values. values.push(temporaryValue); // 5.6. Set temporaryValue to the empty string. temporaryValue = ''; } // 6. Return values. return values; } /** * @see https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split * @param {string} name lowercase header name * @param {import('./headers').HeadersList} list */ function getDecodeSplit(name, list) { // 1. Let value be the result of getting name from list. const value = list.get(name, true); // 2. If value is null, then return null. if (value === null) { return null; } // 3. Return the result of getting, decoding, and splitting value. return gettingDecodingSplitting(value); } const textDecoder = new TextDecoder(); /** * @see https://encoding.spec.whatwg.org/#utf-8-decode * @param {Buffer} buffer */ function utf8DecodeBytes(buffer) { if (buffer.length === 0) { return ''; } // 1. Let buffer be the result of peeking three bytes from // ioQueue, converted to a byte sequence. // 2. If buffer is 0xEF 0xBB 0xBF, then read three // bytes from ioQueue. (Do nothing with those bytes.) if (buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) { buffer = buffer.subarray(3); } // 3. Process a queue with an instance of UTF-8’s // decoder, ioQueue, output, and "replacement". const output = textDecoder.decode(buffer); // 4. Return output. return output; } class EnvironmentSettingsObjectBase { get baseUrl() { return getGlobalOrigin(); } get origin() { return this.baseUrl?.origin; } policyContainer = makePolicyContainer(); } class EnvironmentSettingsObject { settingsObject = new EnvironmentSettingsObjectBase(); } const environmentSettingsObject = new EnvironmentSettingsObject(); module.exports = { isAborted, isCancelled, isValidEncodedURL, createDeferredPromise, ReadableStreamFrom, tryUpgradeRequestToAPotentiallyTrustworthyURL, clampAndCoarsenConnectionTimingInfo, coarsenedSharedCurrentTime, determineRequestsReferrer, makePolicyContainer, clonePolicyContainer, appendFetchMetadata, appendRequestOriginHeader, TAOCheck, corsCheck, crossOriginResourcePolicyCheck, createOpaqueTimingInfo, setRequestReferrerPolicyOnRedirect, isValidHTTPToken, requestBadPort, requestCurrentURL, responseURL, responseLocationURL, isURLPotentiallyTrustworthy, isValidReasonPhrase, sameOrigin, normalizeMethod, serializeJavascriptValueToJSONString, iteratorMixin, createIterator, isValidHeaderName, isValidHeaderValue, isErrorLike, fullyReadBody, bytesMatch, readableStreamClose, isomorphicEncode, urlIsLocal, urlHasHttpsScheme, urlIsHttpHttpsScheme, readAllBytes, simpleRangeHeaderValue, buildContentRange, parseMetadata, createInflate, extractMimeType, getDecodeSplit, utf8DecodeBytes, environmentSettingsObject, isOriginIPPotentiallyTrustworthy, };