mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1520 lines
47 KiB
1520 lines
47 KiB
'use strict'
|
|
|
|
const { Transform } = require('node:stream')
|
|
const zlib = require('node:zlib')
|
|
const { redirectStatusSet, referrerPolicyTokens, badPortsSet } = require('./constants')
|
|
const { getGlobalOrigin } = require('./global')
|
|
const { collectAnHTTPQuotedString, 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')
|
|
const { isomorphicEncode, collectASequenceOfCodePoints, removeChars } = require('../infra')
|
|
|
|
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 referrerURL.
|
|
return referrerURL
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 isPotentialleTrustworthyIPv4 = RegExp.prototype.test
|
|
.bind(/^127\.(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){2}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)$/)
|
|
|
|
const isPotentiallyTrustworthyIPv6 = RegExp.prototype.test
|
|
.bind(/^(?:(?:0{1,4}:){7}|(?:0{1,4}:){1,6}:|::)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 isPotentiallyTrustworthyIPv6(origin)
|
|
}
|
|
|
|
// IPv4
|
|
return isPotentialleTrustworthyIPv4(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)
|
|
}
|
|
|
|
// 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 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://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<any>}
|
|
*/
|
|
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
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @param {import('./body').ExtractBodyResult} body
|
|
* @param {(bytes: Uint8Array) => void} processBody
|
|
* @param {(error: Error) => void} processBodyError
|
|
* @returns {void}
|
|
*
|
|
* @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
|
|
|
|
try {
|
|
// 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.
|
|
const reader = body.stream.getReader()
|
|
|
|
// 5. Read all bytes from reader, given successSteps and errorSteps.
|
|
readAllBytes(reader, successSteps, errorSteps)
|
|
} catch (e) {
|
|
errorSteps(e)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {ReadableStreamController<Uint8Array>} 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
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
|
|
* @see https://streams.spec.whatwg.org/#read-loop
|
|
* @param {ReadableStream<Uint8Array<ArrayBuffer>>} reader
|
|
* @param {(bytes: Uint8Array) => void} successSteps
|
|
* @param {(error: Error) => void} failureSteps
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function readAllBytes (reader, successSteps, failureSteps) {
|
|
try {
|
|
const bytes = []
|
|
let byteLength = 0
|
|
|
|
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(new 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:'
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object} RangeHeaderValue
|
|
* @property {number|null} rangeStartValue
|
|
* @property {number|null} rangeEndValue
|
|
*/
|
|
|
|
/**
|
|
* @see https://fetch.spec.whatwg.org/#simple-range-header-value
|
|
* @param {string} value
|
|
* @param {boolean} allowWhitespace
|
|
* @return {RangeHeaderValue|'failure'}
|
|
*/
|
|
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)
|
|
}
|
|
|
|
function hasAuthenticationEntry (request) {
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* @see https://url.spec.whatwg.org/#include-credentials
|
|
* @param {URL} url
|
|
*/
|
|
function includesCredentials (url) {
|
|
// A URL includes credentials if its username or password is not the empty string.
|
|
return !!(url.username || url.password)
|
|
}
|
|
|
|
/**
|
|
* @see https://html.spec.whatwg.org/multipage/document-sequences.html#traversable-navigable
|
|
* @param {object|string} navigable
|
|
*/
|
|
function isTraversableNavigable (navigable) {
|
|
// TODO
|
|
return true
|
|
}
|
|
|
|
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,
|
|
ReadableStreamFrom,
|
|
tryUpgradeRequestToAPotentiallyTrustworthyURL,
|
|
clampAndCoarsenConnectionTimingInfo,
|
|
coarsenedSharedCurrentTime,
|
|
determineRequestsReferrer,
|
|
makePolicyContainer,
|
|
clonePolicyContainer,
|
|
appendFetchMetadata,
|
|
appendRequestOriginHeader,
|
|
TAOCheck,
|
|
corsCheck,
|
|
crossOriginResourcePolicyCheck,
|
|
createOpaqueTimingInfo,
|
|
setRequestReferrerPolicyOnRedirect,
|
|
isValidHTTPToken,
|
|
requestBadPort,
|
|
requestCurrentURL,
|
|
responseURL,
|
|
responseLocationURL,
|
|
isURLPotentiallyTrustworthy,
|
|
isValidReasonPhrase,
|
|
sameOrigin,
|
|
normalizeMethod,
|
|
iteratorMixin,
|
|
createIterator,
|
|
isValidHeaderName,
|
|
isValidHeaderValue,
|
|
isErrorLike,
|
|
fullyReadBody,
|
|
readableStreamClose,
|
|
urlIsLocal,
|
|
urlHasHttpsScheme,
|
|
urlIsHttpHttpsScheme,
|
|
readAllBytes,
|
|
simpleRangeHeaderValue,
|
|
buildContentRange,
|
|
createInflate,
|
|
extractMimeType,
|
|
getDecodeSplit,
|
|
environmentSettingsObject,
|
|
isOriginIPPotentiallyTrustworthy,
|
|
hasAuthenticationEntry,
|
|
includesCredentials,
|
|
isTraversableNavigable
|
|
}
|
|
|