This commit is contained in:
Lukian 2023-06-20 15:25:19 +02:00
parent 13ec9babde
commit 68f4b60012
1429 changed files with 2481 additions and 272836 deletions

View file

@ -123,6 +123,7 @@ function extractBody (object, keepalive = false) {
const blobParts = []
const rn = new Uint8Array([13, 10]) // '\r\n'
length = 0
let hasUnknownSizeValue = false
for (const [name, value] of object) {
if (typeof value === 'string') {
@ -138,13 +139,20 @@ function extractBody (object, keepalive = false) {
value.type || 'application/octet-stream'
}\r\n\r\n`)
blobParts.push(chunk, value, rn)
length += chunk.byteLength + value.size + rn.byteLength
if (typeof value.size === 'number') {
length += chunk.byteLength + value.size + rn.byteLength
} else {
hasUnknownSizeValue = true
}
}
}
const chunk = enc.encode(`--${boundary}--`)
blobParts.push(chunk)
length += chunk.byteLength
if (hasUnknownSizeValue) {
length = null
}
// Set source to object.
source = object

View file

@ -48,11 +48,17 @@ const requestCache = [
'only-if-cached'
]
// https://fetch.spec.whatwg.org/#request-body-header-name
const requestBodyHeader = [
'content-encoding',
'content-language',
'content-location',
'content-type'
'content-type',
// See https://github.com/nodejs/undici/issues/2021
// 'Content-Length' is a forbidden header name, which is typically
// removed in the Headers implementation. However, undici doesn't
// filter out headers, so we add it here.
'content-length'
]
// https://fetch.spec.whatwg.org/#enumdef-requestduplex

View file

@ -1,15 +1,18 @@
const assert = require('assert')
const { atob } = require('buffer')
const { format } = require('url')
const { isValidHTTPToken, isomorphicDecode } = require('./util')
const { isomorphicDecode } = require('./util')
const encoder = new TextEncoder()
// Regex
const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-z0-9]+$/
/**
* @see https://mimesniff.spec.whatwg.org/#http-token-code-point
*/
const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/
const HTTP_WHITESPACE_REGEX = /(\u000A|\u000D|\u0009|\u0020)/ // eslint-disable-line
// https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
const HTTP_QUOTED_STRING_TOKENS = /^(\u0009|\x{0020}-\x{007E}|\x{0080}-\x{00FF})+$/ // eslint-disable-line
/**
* @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
*/
const HTTP_QUOTED_STRING_TOKENS = /[\u0009|\u0020-\u007E|\u0080-\u00FF]/ // eslint-disable-line
// https://fetch.spec.whatwg.org/#data-url-processor
/** @param {URL} dataURL */
@ -31,22 +34,20 @@ function dataURLProcessor (dataURL) {
// 5. Let mimeType be the result of collecting a
// sequence of code points that are not equal
// to U+002C (,), given position.
let mimeType = collectASequenceOfCodePoints(
(char) => char !== ',',
let mimeType = collectASequenceOfCodePointsFast(
',',
input,
position
)
// 6. Strip leading and trailing ASCII whitespace
// from mimeType.
// Note: This will only remove U+0020 SPACE code
// points, if any.
// Undici implementation note: we need to store the
// length because if the mimetype has spaces removed,
// the wrong amount will be sliced from the input in
// step #9
const mimeTypeLength = mimeType.length
mimeType = mimeType.replace(/^(\u0020)+|(\u0020)+$/g, '')
mimeType = removeASCIIWhitespace(mimeType, true, true)
// 7. If position is past the end of input, then
// return failure
@ -118,7 +119,17 @@ function dataURLProcessor (dataURL) {
* @param {boolean} excludeFragment
*/
function URLSerializer (url, excludeFragment = false) {
return format(url, { fragment: !excludeFragment })
const href = url.href
if (!excludeFragment) {
return href
}
const hash = href.lastIndexOf('#')
if (hash === -1) {
return href
}
return href.slice(0, hash)
}
// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
@ -145,6 +156,25 @@ function collectASequenceOfCodePoints (condition, input, position) {
return result
}
/**
* A faster collectASequenceOfCodePoints that only works when comparing a single character.
* @param {string} char
* @param {string} input
* @param {{ position: number }} position
*/
function collectASequenceOfCodePointsFast (char, input, position) {
const idx = input.indexOf(char, position.position)
const start = position.position
if (idx === -1) {
position.position = input.length
return input.slice(start)
}
position.position = idx
return input.slice(start, position.position)
}
// https://url.spec.whatwg.org/#string-percent-decode
/** @param {string} input */
function stringPercentDecode (input) {
@ -205,7 +235,7 @@ function percentDecode (input) {
function parseMIMEType (input) {
// 1. Remove any leading and trailing HTTP whitespace
// from input.
input = input.trim()
input = removeHTTPWhitespace(input, true, true)
// 2. Let position be a position variable for input,
// initially pointing at the start of input.
@ -214,8 +244,8 @@ function parseMIMEType (input) {
// 3. Let type be the result of collecting a sequence
// of code points that are not U+002F (/) from
// input, given position.
const type = collectASequenceOfCodePoints(
(char) => char !== '/',
const type = collectASequenceOfCodePointsFast(
'/',
input,
position
)
@ -239,14 +269,14 @@ function parseMIMEType (input) {
// 7. Let subtype be the result of collecting a sequence of
// code points that are not U+003B (;) from input, given
// position.
let subtype = collectASequenceOfCodePoints(
(char) => char !== ';',
let subtype = collectASequenceOfCodePointsFast(
';',
input,
position
)
// 8. Remove any trailing HTTP whitespace from subtype.
subtype = subtype.trimEnd()
subtype = removeHTTPWhitespace(subtype, false, true)
// 9. If subtype is the empty string or does not solely
// contain HTTP token code points, then return failure.
@ -254,17 +284,20 @@ function parseMIMEType (input) {
return 'failure'
}
const typeLowercase = type.toLowerCase()
const subtypeLowercase = subtype.toLowerCase()
// 10. Let mimeType be a new MIME type record whose type
// is type, in ASCII lowercase, and subtype is subtype,
// in ASCII lowercase.
// https://mimesniff.spec.whatwg.org/#mime-type
const mimeType = {
type: type.toLowerCase(),
subtype: subtype.toLowerCase(),
type: typeLowercase,
subtype: subtypeLowercase,
/** @type {Map<string, string>} */
parameters: new Map(),
// https://mimesniff.spec.whatwg.org/#mime-type-essence
essence: `${type}/${subtype}`
essence: `${typeLowercase}/${subtypeLowercase}`
}
// 11. While position is not past the end of input:
@ -324,8 +357,8 @@ function parseMIMEType (input) {
// 2. Collect a sequence of code points that are not
// U+003B (;) from input, given position.
collectASequenceOfCodePoints(
(char) => char !== ';',
collectASequenceOfCodePointsFast(
';',
input,
position
)
@ -335,15 +368,14 @@ function parseMIMEType (input) {
// 1. Set parameterValue to the result of collecting
// a sequence of code points that are not U+003B (;)
// from input, given position.
parameterValue = collectASequenceOfCodePoints(
(char) => char !== ';',
parameterValue = collectASequenceOfCodePointsFast(
';',
input,
position
)
// 2. Remove any trailing HTTP whitespace from parameterValue.
// Note: it says "trailing" whitespace; leading is fine.
parameterValue = parameterValue.trimEnd()
parameterValue = removeHTTPWhitespace(parameterValue, false, true)
// 3. If parameterValue is the empty string, then continue.
if (parameterValue.length === 0) {
@ -360,7 +392,7 @@ function parseMIMEType (input) {
if (
parameterName.length !== 0 &&
HTTP_TOKEN_CODEPOINTS.test(parameterName) &&
!HTTP_QUOTED_STRING_TOKENS.test(parameterValue) &&
(parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) &&
!mimeType.parameters.has(parameterName)
) {
mimeType.parameters.set(parameterName, parameterValue)
@ -494,11 +526,11 @@ function collectAnHTTPQuotedString (input, position, extractValue) {
*/
function serializeAMimeType (mimeType) {
assert(mimeType !== 'failure')
const { type, subtype, parameters } = mimeType
const { parameters, essence } = mimeType
// 1. Let serialization be the concatenation of mimeTypes
// type, U+002F (/), and mimeTypes subtype.
let serialization = `${type}/${subtype}`
let serialization = essence
// 2. For each name → value of mimeTypes parameters:
for (let [name, value] of parameters.entries()) {
@ -513,7 +545,7 @@ function serializeAMimeType (mimeType) {
// 4. If value does not solely contain HTTP token code
// points or value is the empty string, then:
if (!isValidHTTPToken(value)) {
if (!HTTP_TOKEN_CODEPOINTS.test(value)) {
// 1. Precede each occurence of U+0022 (") or
// U+005C (\) in value with U+005C (\).
value = value.replace(/(\\|")/g, '\\$1')
@ -533,10 +565,64 @@ function serializeAMimeType (mimeType) {
return serialization
}
/**
* @see https://fetch.spec.whatwg.org/#http-whitespace
* @param {string} char
*/
function isHTTPWhiteSpace (char) {
return char === '\r' || char === '\n' || char === '\t' || char === ' '
}
/**
* @see https://fetch.spec.whatwg.org/#http-whitespace
* @param {string} str
*/
function removeHTTPWhitespace (str, leading = true, trailing = true) {
let lead = 0
let trail = str.length - 1
if (leading) {
for (; lead < str.length && isHTTPWhiteSpace(str[lead]); lead++);
}
if (trailing) {
for (; trail > 0 && isHTTPWhiteSpace(str[trail]); trail--);
}
return str.slice(lead, trail + 1)
}
/**
* @see https://infra.spec.whatwg.org/#ascii-whitespace
* @param {string} char
*/
function isASCIIWhitespace (char) {
return char === '\r' || char === '\n' || char === '\t' || char === '\f' || char === ' '
}
/**
* @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace
*/
function removeASCIIWhitespace (str, leading = true, trailing = true) {
let lead = 0
let trail = str.length - 1
if (leading) {
for (; lead < str.length && isASCIIWhitespace(str[lead]); lead++);
}
if (trailing) {
for (; trail > 0 && isASCIIWhitespace(str[trail]); trail--);
}
return str.slice(lead, trail + 1)
}
module.exports = {
dataURLProcessor,
URLSerializer,
collectASequenceOfCodePoints,
collectASequenceOfCodePointsFast,
stringPercentDecode,
parseMIMEType,
collectAnHTTPQuotedString,

View file

@ -61,14 +61,7 @@ class FormData {
// The delete(name) method steps are to remove all entries whose name
// is name from thiss entry list.
const next = []
for (const entry of this[kState]) {
if (entry.name !== name) {
next.push(entry)
}
}
this[kState] = next
this[kState] = this[kState].filter(entry => entry.name !== name)
}
get (name) {

View file

@ -3,7 +3,7 @@
'use strict'
const { kHeadersList } = require('../core/symbols')
const { kGuard, kHeadersCaseInsensitive } = require('./symbols')
const { kGuard } = require('./symbols')
const { kEnumerableProperty } = require('../core/util')
const {
makeIterator,
@ -11,6 +11,7 @@ const {
isValidHeaderValue
} = require('./util')
const { webidl } = require('./webidl')
const assert = require('assert')
const kHeadersMap = Symbol('headers map')
const kHeadersSortedMap = Symbol('headers map sorted')
@ -23,10 +24,12 @@ function headerValueNormalize (potentialValue) {
// To normalize a byte sequence potentialValue, remove
// any leading and trailing HTTP whitespace bytes from
// potentialValue.
return potentialValue.replace(
/^[\r\n\t ]+|[\r\n\t ]+$/g,
''
)
// Trimming the end with `.replace()` and a RegExp is typically subject to
// ReDoS. This is safer and faster.
let i = potentialValue.length
while (/[\r\n\t ]/.test(potentialValue.charAt(--i)));
return potentialValue.slice(0, i + 1).replace(/^[\r\n\t ]+/, '')
}
function fill (headers, object) {
@ -72,6 +75,7 @@ class HeadersList {
if (init instanceof HeadersList) {
this[kHeadersMap] = new Map(init[kHeadersMap])
this[kHeadersSortedMap] = init[kHeadersSortedMap]
this.cookies = init.cookies
} else {
this[kHeadersMap] = new Map(init)
this[kHeadersSortedMap] = null
@ -91,6 +95,7 @@ class HeadersList {
clear () {
this[kHeadersMap].clear()
this[kHeadersSortedMap] = null
this.cookies = null
}
// https://fetch.spec.whatwg.org/#concept-header-list-append
@ -104,14 +109,18 @@ class HeadersList {
// 2. Append (name, value) to list.
if (exists) {
this[kHeadersMap].set(lowercaseName, { name: exists.name, value: `${exists.value}, ${value}` })
const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
this[kHeadersMap].set(lowercaseName, {
name: exists.name,
value: `${exists.value}${delimiter}${value}`
})
} else {
this[kHeadersMap].set(lowercaseName, { name, value })
}
if (lowercaseName === 'set-cookie') {
this.cookies ??= []
this.cookies.push([name, value])
this.cookies.push(value)
}
}
@ -121,7 +130,7 @@ class HeadersList {
const lowercaseName = name.toLowerCase()
if (lowercaseName === 'set-cookie') {
this.cookies = [[name, value]]
this.cookies = [value]
}
// 1. If list contains name, then set the value of
@ -164,15 +173,16 @@ class HeadersList {
}
}
get [kHeadersCaseInsensitive] () {
/** @type {string[]} */
const flatList = []
get entries () {
const headers = {}
for (const { name, value } of this[kHeadersMap].values()) {
flatList.push(name, value)
if (this[kHeadersMap].size) {
for (const { name, value } of this[kHeadersMap].values()) {
headers[name] = value
}
}
return flatList
return headers
}
}
@ -379,18 +389,74 @@ class Headers {
return this[kHeadersList].set(name, value)
}
get [kHeadersSortedMap] () {
if (!this[kHeadersList][kHeadersSortedMap]) {
this[kHeadersList][kHeadersSortedMap] = new Map([...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1))
// https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
getSetCookie () {
webidl.brandCheck(this, Headers)
// 1. If thiss header list does not contain `Set-Cookie`, then return « ».
// 2. Return the values of all headers in thiss header list whose name is
// a byte-case-insensitive match for `Set-Cookie`, in order.
const list = this[kHeadersList].cookies
if (list) {
return [...list]
}
return this[kHeadersList][kHeadersSortedMap]
return []
}
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
get [kHeadersSortedMap] () {
if (this[kHeadersList][kHeadersSortedMap]) {
return this[kHeadersList][kHeadersSortedMap]
}
// 1. Let headers be an empty list of headers with the key being the name
// and value the value.
const headers = []
// 2. Let names be the result of convert header names to a sorted-lowercase
// set with all the names of the headers in list.
const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1)
const cookies = this[kHeadersList].cookies
// 3. For each name of names:
for (const [name, value] of names) {
// 1. If name is `set-cookie`, then:
if (name === 'set-cookie') {
// 1. Let values be a list of all values of headers in list whose name
// is a byte-case-insensitive match for name, in order.
// 2. For each value of values:
// 1. Append (name, value) to headers.
for (const value of cookies) {
headers.push([name, value])
}
} else {
// 2. Otherwise:
// 1. Let value be the result of getting name from list.
// 2. Assert: value is non-null.
assert(value !== null)
// 3. Append (name, value) to headers.
headers.push([name, value])
}
}
this[kHeadersList][kHeadersSortedMap] = headers
// 4. Return headers.
return headers
}
keys () {
webidl.brandCheck(this, Headers)
return makeIterator(
() => [...this[kHeadersSortedMap].entries()],
() => [...this[kHeadersSortedMap].values()],
'Headers',
'key'
)
@ -400,7 +466,7 @@ class Headers {
webidl.brandCheck(this, Headers)
return makeIterator(
() => [...this[kHeadersSortedMap].entries()],
() => [...this[kHeadersSortedMap].values()],
'Headers',
'value'
)
@ -410,7 +476,7 @@ class Headers {
webidl.brandCheck(this, Headers)
return makeIterator(
() => [...this[kHeadersSortedMap].entries()],
() => [...this[kHeadersSortedMap].values()],
'Headers',
'key+value'
)
@ -451,6 +517,7 @@ Object.defineProperties(Headers.prototype, {
get: kEnumerableProperty,
has: kEnumerableProperty,
set: kEnumerableProperty,
getSetCookie: kEnumerableProperty,
keys: kEnumerableProperty,
values: kEnumerableProperty,
entries: kEnumerableProperty,

View file

@ -37,9 +37,12 @@ const {
isErrorLike,
fullyReadBody,
readableStreamClose,
isomorphicEncode
isomorphicEncode,
urlIsLocal,
urlIsHttpHttpsScheme,
urlHasHttpsScheme
} = require('./util')
const { kState, kHeaders, kGuard, kRealm, kHeadersCaseInsensitive } = require('./symbols')
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
const assert = require('assert')
const { safelyExtractBody } = require('./body')
const {
@ -53,7 +56,7 @@ const {
const { kHeadersList } = require('../core/symbols')
const EE = require('events')
const { Readable, pipeline } = require('stream')
const { isErrored, isReadable } = require('../core/util')
const { isErrored, isReadable, nodeMajor, nodeMinor } = require('../core/util')
const { dataURLProcessor, serializeAMimeType } = require('./dataURL')
const { TransformStream } = require('stream/web')
const { getGlobalDispatcher } = require('../global')
@ -64,10 +67,6 @@ const { STATUS_CODES } = require('http')
let resolveObjectURL
let ReadableStream = globalThis.ReadableStream
const nodeVersion = process.versions.node.split('.')
const nodeMajor = Number(nodeVersion[0])
const nodeMinor = Number(nodeVersion[1])
class Fetch extends EE {
constructor (dispatcher) {
super()
@ -276,7 +275,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
let cacheState = response.cacheState
// 6. If originalURLs scheme is not an HTTP(S) scheme, then return.
if (!/^https?:/.test(originalURL.protocol)) {
if (!urlIsHttpHttpsScheme(originalURL)) {
return
}
@ -301,7 +300,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
// capability.
// TODO: given globals relevant settings objects cross-origin isolated
// capability?
response.timingInfo.endTime = coarsenedSharedCurrentTime()
timingInfo.endTime = coarsenedSharedCurrentTime()
// 10. Set responses timing info to timingInfo.
response.timingInfo = timingInfo
@ -319,7 +318,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing
function markResourceTiming (timingInfo, originalURL, initiatorType, globalThis, cacheState) {
if (nodeMajor >= 18 && nodeMinor >= 2) {
if (nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 2)) {
performance.markResourceTiming(timingInfo, originalURL, initiatorType, globalThis, cacheState)
}
}
@ -534,10 +533,7 @@ async function mainFetch (fetchParams, recursive = false) {
// 3. If requests local-URLs-only flag is set and requests current URL is
// not local, then set response to a network error.
if (
request.localURLsOnly &&
!/^(about|blob|data):/.test(requestCurrentURL(request).protocol)
) {
if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) {
response = makeNetworkError('local URLs only')
}
@ -627,7 +623,7 @@ async function mainFetch (fetchParams, recursive = false) {
}
// requests current URLs scheme is not an HTTP(S) scheme
if (!/^https?:/.test(requestCurrentURL(request).protocol)) {
if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) {
// Return a network error.
return makeNetworkError('URL scheme must be a HTTP(S) scheme')
}
@ -1134,7 +1130,7 @@ async function httpRedirectFetch (fetchParams, response) {
// 6. If locationURLs scheme is not an HTTP(S) scheme, then return a network
// error.
if (!/^https?:/.test(locationURL.protocol)) {
if (!urlIsHttpHttpsScheme(locationURL)) {
return makeNetworkError('URL scheme must be a HTTP(S) scheme')
}
@ -1209,7 +1205,7 @@ async function httpRedirectFetch (fetchParams, response) {
// 14. If requests body is non-null, then set requests body to the first return
// value of safely extracting requests bodys source.
if (request.body != null) {
assert(request.body.source)
assert(request.body.source != null)
request.body = safelyExtractBody(request.body.source)[0]
}
@ -1403,7 +1399,7 @@ async function httpNetworkOrCacheFetch (
// header if httpRequests header list contains that headers name.
// TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129
if (!httpRequest.headersList.contains('accept-encoding')) {
if (/^https:/.test(requestCurrentURL(httpRequest).protocol)) {
if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) {
httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate')
} else {
httpRequest.headersList.append('accept-encoding', 'gzip, deflate')
@ -1849,6 +1845,7 @@ async function httpNetworkFetch (
// 4. Set bytes to the result of handling content codings given
// codings and bytes.
let bytes
let isFailure
try {
const { done, value } = await fetchParams.controller.next()
@ -1863,6 +1860,10 @@ async function httpNetworkFetch (
bytes = undefined
} else {
bytes = err
// err may be propagated from the result of calling readablestream.cancel,
// which might not be an error. https://github.com/nodejs/undici/issues/2009
isFailure = true
}
}
@ -1882,7 +1883,7 @@ async function httpNetworkFetch (
timingInfo.decodedBodySize += bytes?.byteLength ?? 0
// 6. If bytes is failure, then terminate fetchParamss controller.
if (isErrorLike(bytes)) {
if (isFailure) {
fetchParams.controller.terminate(bytes)
return
}
@ -1949,7 +1950,7 @@ async function httpNetworkFetch (
origin: url.origin,
method: request.method,
body: fetchParams.controller.dispatcher.isMockActive ? request.body && request.body.source : body,
headers: request.headersList[kHeadersCaseInsensitive],
headers: request.headersList.entries,
maxRedirections: 0,
upgrade: request.mode === 'websocket' ? 'websocket' : undefined
},
@ -1983,7 +1984,9 @@ async function httpNetworkFetch (
const val = headersList[n + 1].toString('latin1')
if (key.toLowerCase() === 'content-encoding') {
codings = val.split(',').map((x) => x.trim())
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
// "All content-coding values are case-insensitive..."
codings = val.toLowerCase().split(',').map((x) => x.trim())
} else if (key.toLowerCase() === 'location') {
location = val
}
@ -2002,9 +2005,10 @@ async function httpNetworkFetch (
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
for (const coding of codings) {
if (/(x-)?gzip/.test(coding)) {
// https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
if (coding === 'x-gzip' || coding === 'gzip') {
decoders.push(zlib.createGunzip())
} else if (/(x-)?deflate/.test(coding)) {
} else if (coding === 'deflate') {
decoders.push(zlib.createInflate())
} else if (coding === 'br') {
decoders.push(zlib.createBrotliDecompress())

View file

@ -9,7 +9,8 @@ const util = require('../core/util')
const {
isValidHTTPToken,
sameOrigin,
normalizeMethod
normalizeMethod,
makePolicyContainer
} = require('./util')
const {
forbiddenMethods,
@ -28,10 +29,12 @@ const { getGlobalOrigin } = require('./global')
const { URLSerializer } = require('./dataURL')
const { kHeadersList } = require('../core/symbols')
const assert = require('assert')
const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('events')
let TransformStream = globalThis.TransformStream
const kInit = Symbol('init')
const kAbortController = Symbol('abortController')
const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
signal.removeEventListener('abort', abort)
@ -50,10 +53,14 @@ class Request {
input = webidl.converters.RequestInfo(input)
init = webidl.converters.RequestInit(init)
// TODO
// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
this[kRealm] = {
settingsObject: {
baseUrl: getGlobalOrigin()
baseUrl: getGlobalOrigin(),
get origin () {
return this.baseUrl?.origin
},
policyContainer: makePolicyContainer()
}
}
@ -122,12 +129,12 @@ class Request {
}
// 10. If init["window"] exists and is non-null, then throw a TypeError.
if (init.window !== undefined && init.window != null) {
if (init.window != null) {
throw new TypeError(`'window' option '${window}' must be null`)
}
// 11. If init["window"] exists, then set window to "no-window".
if (init.window !== undefined) {
if ('window' in init) {
window = 'no-window'
}
@ -348,12 +355,34 @@ class Request {
if (signal.aborted) {
ac.abort(signal.reason)
} else {
// Keep a strong ref to ac while request object
// is alive. This is needed to prevent AbortController
// from being prematurely garbage collected.
// See, https://github.com/nodejs/undici/issues/1926.
this[kAbortController] = ac
const acRef = new WeakRef(ac)
const abort = function () {
acRef.deref()?.abort(this.reason)
const ac = acRef.deref()
if (ac !== undefined) {
ac.abort(this.reason)
}
}
// Third-party AbortControllers may not work with these.
// See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619.
try {
// If the max amount of listeners is equal to the default, increase it
// This is only available in node >= v19.9.0
if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) {
setMaxListeners(100, signal)
} else if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) {
setMaxListeners(100, signal)
}
} catch {}
signal.addEventListener('abort', abort, { once: true })
requestFinalizer.register(this, { signal, abort })
requestFinalizer.register(ac, { signal, abort })
}
}
@ -413,7 +442,7 @@ class Request {
// non-null, and requests method is `GET` or `HEAD`, then throw a
// TypeError.
if (
((init.body !== undefined && init.body != null) || inputBody != null) &&
(init.body != null || inputBody != null) &&
(request.method === 'GET' || request.method === 'HEAD')
) {
throw new TypeError('Request with GET/HEAD method cannot have body.')
@ -423,7 +452,7 @@ class Request {
let initBody = null
// 36. If init["body"] exists and is non-null, then:
if (init.body !== undefined && init.body != null) {
if (init.body != null) {
// 1. Let Content-Type be null.
// 2. Set initBody and Content-Type to the result of extracting
// init["body"], with keepalive set to requests keepalive.

View file

@ -348,9 +348,7 @@ function makeNetworkError (reason) {
status: 0,
error: isError
? reason
: new Error(reason ? String(reason) : reason, {
cause: isError ? reason : undefined
}),
: new Error(reason ? String(reason) : reason),
aborted: reason && reason.name === 'AbortError'
})
}
@ -469,7 +467,7 @@ function initializeResponse (response, init, body) {
// 5. If init["headers"] exists, then fill responses headers with init["headers"].
if ('headers' in init && init.headers != null) {
fill(response[kState].headersList, init.headers)
fill(response[kHeaders], init.headers)
}
// 6. If body was given, then:
@ -571,5 +569,6 @@ module.exports = {
makeResponse,
makeAppropriateNetworkError,
filterResponse,
Response
Response,
cloneResponse
}

View file

@ -6,6 +6,5 @@ module.exports = {
kSignal: Symbol('signal'),
kState: Symbol('state'),
kGuard: Symbol('guard'),
kRealm: Symbol('realm'),
kHeadersCaseInsensitive: Symbol('headers case insensitive')
kRealm: Symbol('realm')
}

238
node_modules/undici/lib/fetch/util.js generated vendored
View file

@ -1,6 +1,7 @@
'use strict'
const { redirectStatus, badPorts, referrerPolicy: referrerPolicyTokens } = require('./constants')
const { getGlobalOrigin } = require('./global')
const { performance } = require('perf_hooks')
const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
const assert = require('assert')
@ -36,9 +37,11 @@ function responseLocationURL (response, requestFragment) {
// `Location` and responses header list.
let location = response.headersList.get('location')
// 3. If location is a value, then set location to the result of parsing
// location with responses URL.
location = location ? new URL(location, responseURL(response)) : null
// 3. If location is a header value, then set location to the result of
// parsing location with responses URL.
if (location !== null && isValidHeaderValue(location)) {
location = new URL(location, responseURL(response))
}
// 4. If location is a URL whose fragment is null, then set locations
// fragment to requestFragment.
@ -61,7 +64,7 @@ function requestBadPort (request) {
// 2. If urls scheme is an HTTP(S) scheme and urls port is a bad port,
// then return blocked.
if (/^https?:/.test(url.protocol) && badPorts.includes(url.port)) {
if (urlIsHttpHttpsScheme(url) && badPorts.includes(url.port)) {
return 'blocked'
}
@ -267,7 +270,7 @@ function appendRequestOriginHeader (request) {
// 2. If requests response tainting is "cors" or requests mode is "websocket", then append (`Origin`, serializedOrigin) to requests header list.
if (request.responseTainting === 'cors' || request.mode === 'websocket') {
if (serializedOrigin) {
request.headersList.append('Origin', serializedOrigin)
request.headersList.append('origin', serializedOrigin)
}
// 3. Otherwise, if requests method is neither `GET` nor `HEAD`, then:
@ -282,7 +285,7 @@ function appendRequestOriginHeader (request) {
case 'strict-origin':
case 'strict-origin-when-cross-origin':
// If requests origin is a tuple origin, its scheme is "https", and requests current URLs scheme is not "https", then set serializedOrigin to `null`.
if (/^https:/.test(request.origin) && !/^https:/.test(requestCurrentURL(request))) {
if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) {
serializedOrigin = null
}
break
@ -298,7 +301,7 @@ function appendRequestOriginHeader (request) {
if (serializedOrigin) {
// 2. Append (`Origin`, serializedOrigin) to requests header list.
request.headersList.append('Origin', serializedOrigin)
request.headersList.append('origin', serializedOrigin)
}
}
}
@ -327,14 +330,17 @@ function createOpaqueTimingInfo (timingInfo) {
// https://html.spec.whatwg.org/multipage/origin.html#policy-container
function makePolicyContainer () {
// TODO
return {}
// 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 () {
// TODO
return {}
function clonePolicyContainer (policyContainer) {
return {
referrerPolicy: policyContainer.referrerPolicy
}
}
// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
@ -342,104 +348,76 @@ function determineRequestsReferrer (request) {
// 1. Let policy be request's referrer policy.
const policy = request.referrerPolicy
// Return no-referrer when empty or policy says so
if (policy == null || policy === '' || policy === 'no-referrer') {
return 'no-referrer'
}
// Note: policy cannot (shouldn't) be null or an empty string.
assert(policy)
// 2. Let environment be requests client.
// 2. Let environment be the request client
const environment = request.client
let referrerSource = null
/**
* 3, Switch on requests referrer:
"client"
If environments global object is a Window object, then
Let document be the associated Document of environments global object.
If documents origin is an opaque origin, return no referrer.
While document is an iframe srcdoc document,
let document be documents browsing contexts browsing context containers node document.
Let referrerSource be documents URL.
Otherwise, let referrerSource be environments creation URL.
a URL
Let referrerSource be requests referrer.
*/
// 3. Switch on requests referrer:
if (request.referrer === 'client') {
// Not defined in Node but part of the spec
if (request.client?.globalObject?.constructor?.name === 'Window' ) { // eslint-disable-line
const origin = environment.globalObject.self?.origin ?? environment.globalObject.location?.origin
// Note: node isn't a browser and doesn't implement document/iframes,
// so we bypass this step and replace it with our own.
// If documents origin is an opaque origin, return no referrer.
if (origin == null || origin === 'null') return 'no-referrer'
const globalOrigin = getGlobalOrigin()
// Let referrerSource be documents URL.
referrerSource = new URL(environment.globalObject.location.href)
} else {
// 3(a)(II) If environment's global object is not Window,
// Let referrerSource be environments creationURL
if (environment?.globalObject?.location == null) {
return 'no-referrer'
}
referrerSource = new URL(environment.globalObject.location.href)
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) {
// 3(b) If requests's referrer is a URL instance, then make
// referrerSource be requests's referrer.
// Let referrerSource be requests referrer.
referrerSource = request.referrer
} else {
// If referrerSource neither client nor instance of URL
// then return "no-referrer".
return 'no-referrer'
}
const urlProtocol = referrerSource.protocol
// 4. Let requests referrerURL be the result of stripping referrerSource for
// use as a referrer.
let referrerURL = stripURLForReferrer(referrerSource)
// If url's scheme is a local scheme (i.e. one of "about", "data", "javascript", "file")
// then return "no-referrer".
if (
urlProtocol === 'about:' || urlProtocol === 'data:' ||
urlProtocol === 'blob:'
) {
return 'no-referrer'
// 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
}
let temp
let referrerOrigin
// 4. Let requests's referrerURL be the result of stripping referrer
// source for use as referrer (using util function, without origin only)
const referrerUrl = (temp = stripURLForReferrer(referrerSource)).length > 4096
// 5. Let referrerOrigin be the result of stripping referrer
// source for use as referrer (using util function, with originOnly true)
? (referrerOrigin = stripURLForReferrer(referrerSource, true))
// 6. If result of seralizing referrerUrl is a string whose length is greater than
// 4096, then set referrerURL to referrerOrigin
: temp
const areSameOrigin = sameOrigin(request, referrerUrl)
const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerUrl) &&
const areSameOrigin = sameOrigin(request, referrerURL)
const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) &&
!isURLPotentiallyTrustworthy(request.url)
// NOTE: How to treat step 7?
// 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 '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':
/**
* 1. If the origin of referrerURL and the origin of requests current URL are the same,
* then return referrerURL.
* 2. If referrerURL is a potentially trustworthy URL and requests current URL is not a
* potentially trustworthy URL, then return no referrer.
* 3. Return referrerOrigin
*/
if (areSameOrigin) return referrerOrigin
// else return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin
return areSameOrigin ? referrerURL : referrerOrigin
case 'strict-origin-when-cross-origin': {
const currentURL = requestCurrentURL(request)
// 1. If the origin of referrerURL and the origin of requests current
// URL are the same, then return referrerURL.
if (sameOrigin(referrerURL, currentURL)) {
return referrerURL
}
// 2. If referrerURL is a potentially trustworthy URL and requests
// 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
@ -458,15 +436,42 @@ function determineRequestsReferrer (request) {
default: // eslint-disable-line
return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin
}
}
function stripURLForReferrer (url, originOnly = false) {
const urlObject = new URL(url.href)
urlObject.username = ''
urlObject.password = ''
urlObject.hash = ''
/**
* @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)
return originOnly ? urlObject.origin : urlObject.href
// 2. If urls scheme is a local scheme, then return no referrer.
if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') {
return 'no-referrer'
}
// 3. Set urls username to the empty string.
url.username = ''
// 4. Set urls password to the empty string.
url.password = ''
// 5. Set urls fragment to null.
url.hash = ''
// 6. If the origin-only flag is true, then:
if (originOnly) {
// 1. Set urls path to « the empty string ».
url.pathname = ''
// 2. Set urls query to null.
url.search = ''
}
// 7. Return url.
return url
}
function isURLPotentiallyTrustworthy (url) {
@ -633,7 +638,9 @@ function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
*/
function sameOrigin (A, B) {
// 1. If A and B are the same opaque origin, then return true.
// "opaque origin" is an internal value we cannot access, ignore.
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.
@ -939,6 +946,41 @@ async function readAllBytes (reader, successSteps, 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.
*/
@ -983,5 +1025,9 @@ module.exports = {
isReadableStreamLike,
readableStreamClose,
isomorphicEncode,
isomorphicDecode
isomorphicDecode,
urlIsLocal,
urlHasHttpsScheme,
urlIsHttpHttpsScheme,
readAllBytes
}

View file

@ -51,6 +51,13 @@ webidl.argumentLengthCheck = function ({ length }, min, ctx) {
}
}
webidl.illegalConstructor = function () {
throw webidl.errors.exception({
header: 'TypeError',
message: 'Illegal constructor'
})
}
// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
webidl.util.Type = function (V) {
switch (typeof V) {