forked from waja/action-debian-package
1072 lines
31 KiB
JavaScript
1072 lines
31 KiB
JavaScript
|
'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
|
|||
|
}
|