diff --git a/deps/undici/src/README.md b/deps/undici/src/README.md index e49064fa2973a2..05a5d21ed1195c 100644 --- a/deps/undici/src/README.md +++ b/deps/undici/src/README.md @@ -407,7 +407,7 @@ Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling ## Workarounds -### Network address family autoselection. +### Network address family autoselection. If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record) first, there are chances that your local router or ISP might have problem connecting to IPv6 networks. In that case diff --git a/deps/undici/src/docs/api/ProxyAgent.md b/deps/undici/src/docs/api/ProxyAgent.md index 05d67a094cfe2b..6a8b07fe6bfb75 100644 --- a/deps/undici/src/docs/api/ProxyAgent.md +++ b/deps/undici/src/docs/api/ProxyAgent.md @@ -19,6 +19,7 @@ Extends: [`AgentOptions`](Agent.md#parameter-agentoptions) * **uri** `string` (required) - It can be passed either by a string or a object containing `uri` as string. * **token** `string` (optional) - It can be passed by a string of token for authentication. * **auth** `string` (**deprecated**) - Use token. +* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)` Examples: @@ -83,7 +84,8 @@ import { setGlobalDispatcher, request, ProxyAgent } from 'undici'; const proxyAgent = new ProxyAgent({ uri: 'my.proxy.server', - token: 'Bearer xxxx' + // token: 'Bearer xxxx' + token: `Basic ${Buffer.from('username:password').toString('base64')}` }); setGlobalDispatcher(proxyAgent); diff --git a/deps/undici/src/lib/api/api-stream.js b/deps/undici/src/lib/api/api-stream.js index f33f459f9d438f..7560a2e6505781 100644 --- a/deps/undici/src/lib/api/api-stream.js +++ b/deps/undici/src/lib/api/api-stream.js @@ -1,10 +1,11 @@ 'use strict' -const { finished } = require('stream') +const { finished, PassThrough } = require('stream') const { InvalidArgumentError, InvalidReturnValueError, - RequestAbortedError + RequestAbortedError, + ResponseStatusCodeError } = require('../core/errors') const util = require('../core/util') const { AsyncResource } = require('async_hooks') @@ -16,7 +17,7 @@ class StreamHandler extends AsyncResource { throw new InvalidArgumentError('invalid opts') } - const { signal, method, opaque, body, onInfo, responseHeaders } = opts + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts try { if (typeof callback !== 'function') { @@ -57,6 +58,7 @@ class StreamHandler extends AsyncResource { this.trailers = null this.body = body this.onInfo = onInfo || null + this.throwOnError = throwOnError || false if (util.isStream(body)) { body.on('error', (err) => { @@ -76,8 +78,8 @@ class StreamHandler extends AsyncResource { this.context = context } - onHeaders (statusCode, rawHeaders, resume) { - const { factory, opaque, context } = this + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const { factory, opaque, context, callback } = this if (statusCode < 200) { if (this.onInfo) { @@ -96,6 +98,32 @@ class StreamHandler extends AsyncResource { context }) + if (this.throwOnError && statusCode >= 400) { + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + const chunks = [] + const pt = new PassThrough() + pt + .on('data', (chunk) => chunks.push(chunk)) + .on('end', () => { + const payload = Buffer.concat(chunks).toString('utf8') + this.runInAsyncScope( + callback, + null, + new ResponseStatusCodeError( + `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, + statusCode, + headers, + payload + ) + ) + }) + .on('error', (err) => { + this.onError(err) + }) + this.res = pt + return + } + if ( !res || typeof res.write !== 'function' || diff --git a/deps/undici/src/lib/api/readable.js b/deps/undici/src/lib/api/readable.js index 9c184d14e1c432..a184e8eb51b8f7 100644 --- a/deps/undici/src/lib/api/readable.js +++ b/deps/undici/src/lib/api/readable.js @@ -4,7 +4,7 @@ const assert = require('assert') const { Readable } = require('stream') -const { RequestAbortedError, NotSupportedError } = require('../core/errors') +const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = require('../core/errors') const util = require('../core/util') const { ReadableStreamFrom, toUSVString } = require('../core/util') @@ -146,15 +146,31 @@ module.exports = class BodyReadable extends Readable { async dump (opts) { let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144 + const signal = opts && opts.signal + const abortFn = () => { + this.destroy() + } + if (signal) { + if (typeof signal !== 'object' || !('aborted' in signal)) { + throw new InvalidArgumentError('signal must be an AbortSignal') + } + util.throwIfAborted(signal) + signal.addEventListener('abort', abortFn, { once: true }) + } try { for await (const chunk of this) { + util.throwIfAborted(signal) limit -= Buffer.byteLength(chunk) if (limit < 0) { return } } } catch { - // Do nothing... + util.throwIfAborted(signal) + } finally { + if (signal) { + signal.removeEventListener('abort', abortFn) + } } } } diff --git a/deps/undici/src/lib/client.js b/deps/undici/src/lib/client.js index 9dbd2eefe47847..b230c368dabd7f 100644 --- a/deps/undici/src/lib/client.js +++ b/deps/undici/src/lib/client.js @@ -1,3 +1,5 @@ +// @ts-check + 'use strict' /* global WebAssembly */ @@ -85,7 +87,15 @@ try { channels.connected = { hasSubscribers: false } } +/** + * @type {import('../types/client').default} + */ class Client extends DispatcherBase { + /** + * + * @param {string|URL} url + * @param {import('../types/client').Client.Options} options + */ constructor (url, { interceptors, maxHeaderSize, @@ -1658,6 +1668,8 @@ class AsyncWriter { process.emitWarning(new RequestContentLengthMismatchError()) } + socket.cork() + if (bytesWritten === 0) { if (!expectsPayload) { socket[kReset] = true @@ -1678,6 +1690,8 @@ class AsyncWriter { const ret = socket.write(chunk) + socket.uncork() + request.onBodySent(chunk) if (!ret) { diff --git a/deps/undici/src/lib/core/util.js b/deps/undici/src/lib/core/util.js index ef9b4570dcdb94..ab94bcfe51c8e3 100644 --- a/deps/undici/src/lib/core/util.js +++ b/deps/undici/src/lib/core/util.js @@ -15,7 +15,7 @@ const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number( function nop () {} function isStream (obj) { - return obj && typeof obj.pipe === 'function' + return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function' } // based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License) @@ -46,6 +46,12 @@ function buildURL (url, queryParams) { function parseURL (url) { if (typeof url === 'string') { url = new URL(url) + + if (!/^https?:/.test(url.origin || url.protocol)) { + throw new InvalidArgumentError('invalid protocol') + } + + return url } if (!url || typeof url !== 'object') { @@ -375,23 +381,34 @@ function ReadableStreamFrom (iterable) { // The chunk should be a FormData instance and contains // all the required methods. -function isFormDataLike (chunk) { - return (chunk && - chunk.constructor && chunk.constructor.name === 'FormData' && - typeof chunk === 'object' && - (typeof chunk.append === 'function' && - typeof chunk.delete === 'function' && - typeof chunk.get === 'function' && - typeof chunk.getAll === 'function' && - typeof chunk.has === 'function' && - typeof chunk.set === 'function' && - typeof chunk.entries === 'function' && - typeof chunk.keys === 'function' && - typeof chunk.values === 'function' && - typeof chunk.forEach === 'function') +function isFormDataLike (object) { + return ( + object && + typeof object === 'object' && + typeof object.append === 'function' && + typeof object.delete === 'function' && + typeof object.get === 'function' && + typeof object.getAll === 'function' && + typeof object.has === 'function' && + typeof object.set === 'function' && + object[Symbol.toStringTag] === 'FormData' ) } +function throwIfAborted (signal) { + if (!signal) { return } + if (typeof signal.throwIfAborted === 'function') { + signal.throwIfAborted() + } else { + if (signal.aborted) { + // DOMException not available < v17.0.0 + const err = new Error('The operation was aborted') + err.name = 'AbortError' + throw err + } + } +} + const kEnumerableProperty = Object.create(null) kEnumerableProperty.enumerable = true @@ -423,6 +440,7 @@ module.exports = { getSocketInfo, isFormDataLike, buildURL, + throwIfAborted, nodeMajor, nodeMinor, nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13) diff --git a/deps/undici/src/lib/fetch/dataURL.js b/deps/undici/src/lib/fetch/dataURL.js index 0d4a46956db260..beefad154824e4 100644 --- a/deps/undici/src/lib/fetch/dataURL.js +++ b/deps/undici/src/lib/fetch/dataURL.js @@ -1,6 +1,5 @@ const assert = require('assert') const { atob } = require('buffer') -const { format } = require('url') const { isValidHTTPToken, isomorphicDecode } = require('./util') const encoder = new TextEncoder() @@ -118,7 +117,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 diff --git a/deps/undici/src/lib/fetch/index.js b/deps/undici/src/lib/fetch/index.js index 6e356de81289a8..e3834a7f1c614a 100644 --- a/deps/undici/src/lib/fetch/index.js +++ b/deps/undici/src/lib/fetch/index.js @@ -297,7 +297,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') { // capability. // TODO: given global’s relevant settings object’s cross-origin isolated // capability? - response.timingInfo.endTime = coarsenedSharedCurrentTime() + timingInfo.endTime = coarsenedSharedCurrentTime() // 10. Set response’s timing info to timingInfo. response.timingInfo = timingInfo diff --git a/deps/undici/src/lib/fetch/request.js b/deps/undici/src/lib/fetch/request.js index eca7b060e0ea1f..080a5d7bfa37d3 100644 --- a/deps/undici/src/lib/fetch/request.js +++ b/deps/undici/src/lib/fetch/request.js @@ -9,7 +9,8 @@ const util = require('../core/util') const { isValidHTTPToken, sameOrigin, - normalizeMethod + normalizeMethod, + makePolicyContainer } = require('./util') const { forbiddenMethods, @@ -51,10 +52,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() } } @@ -349,14 +354,17 @@ class Request { if (signal.aborted) { ac.abort(signal.reason) } else { - const acRef = new WeakRef(ac) const abort = function () { - acRef.deref()?.abort(this.reason) + ac.abort(this.reason) } - if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { - setMaxListeners(100, signal) - } + // Third-party AbortControllers may not work with these. + // See https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619 + try { + if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { + setMaxListeners(100, signal) + } + } catch {} signal.addEventListener('abort', abort, { once: true }) requestFinalizer.register(this, { signal, abort }) diff --git a/deps/undici/src/lib/fetch/util.js b/deps/undici/src/lib/fetch/util.js index a0faed913545b6..2d8977f17baaae 100644 --- a/deps/undici/src/lib/fetch/util.js +++ b/deps/undici/src/lib/fetch/util.js @@ -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 response’s header list. let location = response.headersList.get('location') - // 3. If location is a value, then set location to the result of parsing - // location with response’s 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 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. @@ -267,7 +270,7 @@ function appendRequestOriginHeader (request) { // 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) + request.headersList.append('origin', serializedOrigin) } // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: @@ -298,7 +301,7 @@ function appendRequestOriginHeader (request) { if (serializedOrigin) { // 2. Append (`Origin`, serializedOrigin) to request’s 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 request’s client. - // 2. Let environment be the request client - const environment = request.client let referrerSource = null - /** - * 3, Switch on request’s referrer: - "client" - If environment’s global object is a Window object, then - Let document be the associated Document of environment’s global object. - If document’s origin is an opaque origin, return no referrer. - While document is an iframe srcdoc document, - let document be document’s browsing context’s browsing context container’s node document. - Let referrerSource be document’s URL. - - Otherwise, let referrerSource be environment’s creation URL. - - a URL - Let referrerSource be request’s referrer. - */ + // 3. Switch on request’s 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 - - // If document’s origin is an opaque origin, return no referrer. - if (origin == null || origin === 'null') return 'no-referrer' - - // Let referrerSource be document’s 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' - } + // 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() - 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 request’s 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 request’s 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 request’s current URL are the same, - * then 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. - * 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 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 @@ -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 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) { diff --git a/deps/undici/src/lib/fileapi/encoding.js b/deps/undici/src/lib/fileapi/encoding.js index 4aac20cce130b5..1d1d2b6544f9f0 100644 --- a/deps/undici/src/lib/fileapi/encoding.js +++ b/deps/undici/src/lib/fileapi/encoding.js @@ -2,9 +2,13 @@ /** * @see https://encoding.spec.whatwg.org/#concept-encoding-get - * @param {string} label + * @param {string|undefined} label */ function getEncoding (label) { + if (!label) { + return 'failure' + } + // 1. Remove any leading and trailing ASCII whitespace from label. // 2. If label is an ASCII case-insensitive match for any of the // labels listed in the table below, then return the diff --git a/deps/undici/src/lib/proxy-agent.js b/deps/undici/src/lib/proxy-agent.js index 128daddbef27a0..c710948cc5bbca 100644 --- a/deps/undici/src/lib/proxy-agent.js +++ b/deps/undici/src/lib/proxy-agent.js @@ -3,7 +3,7 @@ const { kProxy, kClose, kDestroy, kInterceptors } = require('./core/symbols') const { URL } = require('url') const Agent = require('./agent') -const Client = require('./client') +const Pool = require('./pool') const DispatcherBase = require('./dispatcher-base') const { InvalidArgumentError, RequestAbortedError } = require('./core/errors') const buildConnector = require('./core/connect') @@ -34,6 +34,10 @@ function buildProxyOptions (opts) { } } +function defaultFactory (origin, opts) { + return new Pool(origin, opts) +} + class ProxyAgent extends DispatcherBase { constructor (opts) { super(opts) @@ -51,6 +55,12 @@ class ProxyAgent extends DispatcherBase { throw new InvalidArgumentError('Proxy opts.uri is mandatory') } + const { clientFactory = defaultFactory } = opts + + if (typeof clientFactory !== 'function') { + throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.') + } + this[kRequestTls] = opts.requestTls this[kProxyTls] = opts.proxyTls this[kProxyHeaders] = opts.headers || {} @@ -69,7 +79,7 @@ class ProxyAgent extends DispatcherBase { const connect = buildConnector({ ...opts.proxyTls }) this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) - this[kClient] = new Client(resolvedUrl, { connect }) + this[kClient] = clientFactory(resolvedUrl, { connect }) this[kAgent] = new Agent({ ...opts, connect: async (opts, callback) => { diff --git a/deps/undici/src/lib/timers.js b/deps/undici/src/lib/timers.js index f96bc62f2863a1..5782217a0e0c8b 100644 --- a/deps/undici/src/lib/timers.js +++ b/deps/undici/src/lib/timers.js @@ -13,13 +13,15 @@ function onTimeout () { while (idx < len) { const timer = fastTimers[idx] - if (timer.expires && fastNow >= timer.expires) { - timer.expires = 0 + if (timer.state === 0) { + timer.state = fastNow + timer.delay + } else if (timer.state > 0 && fastNow >= timer.state) { + timer.state = -1 timer.callback(timer.opaque) } - if (timer.expires === 0) { - timer.active = false + if (timer.state === -1) { + timer.state = -2 if (idx !== len - 1) { fastTimers[idx] = fastTimers.pop() } else { @@ -53,37 +55,43 @@ class Timeout { this.callback = callback this.delay = delay this.opaque = opaque - this.expires = 0 - this.active = false + + // -2 not in timer list + // -1 in timer list but inactive + // 0 in timer list waiting for time + // > 0 in timer list waiting for time to expire + this.state = -2 this.refresh() } refresh () { - if (!this.active) { - this.active = true + if (this.state === -2) { fastTimers.push(this) if (!fastNowTimeout || fastTimers.length === 1) { refreshTimeout() - fastNow = Date.now() } } - this.expires = fastNow + this.delay + this.state = 0 } clear () { - this.expires = 0 + this.state = -1 } } module.exports = { setTimeout (callback, delay, opaque) { - return new Timeout(callback, delay, opaque) + return delay < 1e3 + ? setTimeout(callback, delay, opaque) + : new Timeout(callback, delay, opaque) }, clearTimeout (timeout) { - if (timeout && timeout.clear) { + if (timeout instanceof Timeout) { timeout.clear() + } else { + clearTimeout(timeout) } } } diff --git a/deps/undici/src/lib/websocket/connection.js b/deps/undici/src/lib/websocket/connection.js index df8e551c26ef34..09770247e3fd00 100644 --- a/deps/undici/src/lib/websocket/connection.js +++ b/deps/undici/src/lib/websocket/connection.js @@ -5,19 +5,15 @@ const diagnosticsChannel = require('diagnostics_channel') const { uid, states } = require('./constants') const { kReadyState, - kResponse, - kExtensions, - kProtocol, kSentClose, kByteParser, kReceivedClose } = require('./symbols') const { fireEvent, failWebsocketConnection } = require('./util') const { CloseEvent } = require('./events') -const { ByteParser } = require('./receiver') const { makeRequest } = require('../fetch/request') const { fetching } = require('../fetch/index') -const { getGlobalDispatcher } = require('../..') +const { getGlobalDispatcher } = require('../global') const channels = {} channels.open = diagnosticsChannel.channel('undici:websocket:open') @@ -29,8 +25,9 @@ channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error * @param {URL} url * @param {string|string[]} protocols * @param {import('./websocket').WebSocket} ws + * @param {(response: any) => void} onEstablish */ -function establishWebSocketConnection (url, protocols, ws) { +function establishWebSocketConnection (url, protocols, ws, onEstablish) { // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s // scheme is "ws", and to "https" otherwise. const requestURL = url @@ -173,67 +170,25 @@ function establishWebSocketConnection (url, protocols, ws) { return } - // processResponse is called when the "response’s header list has been received and initialized." - // once this happens, the connection is open - ws[kResponse] = response - - const parser = new ByteParser(ws) - response.socket.ws = ws // TODO: use symbol - ws[kByteParser] = parser - - whenConnectionEstablished(ws) - response.socket.on('data', onSocketData) response.socket.on('close', onSocketClose) response.socket.on('error', onSocketError) - parser.on('drain', onParserDrain) + if (channels.open.hasSubscribers) { + channels.open.publish({ + address: response.socket.address(), + protocol: secProtocol, + extensions: secExtension + }) + } + + onEstablish(response) } }) return controller } -/** - * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol - * @param {import('./websocket').WebSocket} ws - */ -function whenConnectionEstablished (ws) { - const { [kResponse]: response } = ws - - // 1. Change the ready state to OPEN (1). - ws[kReadyState] = states.OPEN - - // 2. Change the extensions attribute’s value to the extensions in use, if - // it is not the null value. - // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 - const extensions = response.headersList.get('sec-websocket-extensions') - - if (extensions !== null) { - ws[kExtensions] = extensions - } - - // 3. Change the protocol attribute’s value to the subprotocol in use, if - // it is not the null value. - // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9 - const protocol = response.headersList.get('sec-websocket-protocol') - - if (protocol !== null) { - ws[kProtocol] = protocol - } - - // 4. Fire an event named open at the WebSocket object. - fireEvent('open', ws) - - if (channels.open.hasSubscribers) { - channels.open.publish({ - address: response.socket.address(), - protocol, - extensions - }) - } -} - /** * @param {Buffer} chunk */ @@ -243,10 +198,6 @@ function onSocketData (chunk) { } } -function onParserDrain () { - this.ws[kResponse].socket.resume() -} - /** * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4 diff --git a/deps/undici/src/lib/websocket/symbols.js b/deps/undici/src/lib/websocket/symbols.js index 5e135862f811e3..11d03e38a86609 100644 --- a/deps/undici/src/lib/websocket/symbols.js +++ b/deps/undici/src/lib/websocket/symbols.js @@ -5,10 +5,7 @@ module.exports = { kReadyState: Symbol('ready state'), kController: Symbol('controller'), kResponse: Symbol('response'), - kExtensions: Symbol('extensions'), - kProtocol: Symbol('protocol'), kBinaryType: Symbol('binary type'), - kClosingFrame: Symbol('closing frame'), kSentClose: Symbol('sent close'), kReceivedClose: Symbol('received close'), kByteParser: Symbol('byte parser') diff --git a/deps/undici/src/lib/websocket/websocket.js b/deps/undici/src/lib/websocket/websocket.js index 79c9be439c1fb4..164d24c6f8a28a 100644 --- a/deps/undici/src/lib/websocket/websocket.js +++ b/deps/undici/src/lib/websocket/websocket.js @@ -8,15 +8,15 @@ const { kWebSocketURL, kReadyState, kController, - kExtensions, - kProtocol, kBinaryType, kResponse, - kSentClose + kSentClose, + kByteParser } = require('./symbols') -const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection } = require('./util') +const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = require('./util') const { establishWebSocketConnection } = require('./connection') const { WebsocketFrameSend } = require('./frame') +const { ByteParser } = require('./receiver') const { kEnumerableProperty, isBlobLike } = require('../core/util') const { types } = require('util') @@ -32,6 +32,8 @@ class WebSocket extends EventTarget { } #bufferedAmount = 0 + #protocol = '' + #extensions = '' /** * @param {string} url @@ -104,7 +106,12 @@ class WebSocket extends EventTarget { // 1. Establish a WebSocket connection given urlRecord, protocols, // and client. - this[kController] = establishWebSocketConnection(urlRecord, protocols, this) + this[kController] = establishWebSocketConnection( + urlRecord, + protocols, + this, + (response) => this.#onConnectionEstablished(response) + ) // Each WebSocket object has an associated ready state, which is a // number representing the state of the connection. Initially it must @@ -112,10 +119,8 @@ class WebSocket extends EventTarget { this[kReadyState] = WebSocket.CONNECTING // The extensions attribute must initially return the empty string. - this[kExtensions] = '' // The protocol attribute must initially return the empty string. - this[kProtocol] = '' // Each WebSocket object has an associated binary type, which is a // BinaryType. Initially it must be "blob". @@ -368,13 +373,13 @@ class WebSocket extends EventTarget { get extensions () { webidl.brandCheck(this, WebSocket) - return this[kExtensions] + return this.#extensions } get protocol () { webidl.brandCheck(this, WebSocket) - return this[kProtocol] + return this.#protocol } get onopen () { @@ -476,6 +481,47 @@ class WebSocket extends EventTarget { this[kBinaryType] = type } } + + /** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + */ + #onConnectionEstablished (response) { + // processResponse is called when the "response’s header list has been received and initialized." + // once this happens, the connection is open + this[kResponse] = response + + const parser = new ByteParser(this) + parser.on('drain', function onParserDrain () { + this.ws[kResponse].socket.resume() + }) + + response.socket.ws = this + this[kByteParser] = parser + + // 1. Change the ready state to OPEN (1). + this[kReadyState] = states.OPEN + + // 2. Change the extensions attribute’s value to the extensions in use, if + // it is not the null value. + // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 + const extensions = response.headersList.get('sec-websocket-extensions') + + if (extensions !== null) { + this.#extensions = extensions + } + + // 3. Change the protocol attribute’s value to the subprotocol in use, if + // it is not the null value. + // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9 + const protocol = response.headersList.get('sec-websocket-protocol') + + if (protocol !== null) { + this.#protocol = protocol + } + + // 4. Fire an event named open at the WebSocket object. + fireEvent('open', this) + } } // https://websockets.spec.whatwg.org/#dom-websocket-connecting diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json index 37baa25665b354..39be1736f065d2 100644 --- a/deps/undici/src/package.json +++ b/deps/undici/src/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.20.0", + "version": "5.21.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { @@ -86,6 +86,7 @@ "husky": "^8.0.1", "import-fresh": "^3.3.0", "jest": "^29.0.2", + "jsdom": "^21.1.0", "jsfuzz": "^1.0.15", "mocha": "^10.0.0", "p-timeout": "^3.2.0", @@ -112,8 +113,7 @@ "ignore": [ "lib/llhttp/constants.js", "lib/llhttp/utils.js", - "test/wpt/tests", - "test/wpt/runner/resources" + "test/wpt/tests" ] }, "tsd": { diff --git a/deps/undici/src/types/balanced-pool.d.ts b/deps/undici/src/types/balanced-pool.d.ts index b5de726eeca0e1..d1e9375875f391 100644 --- a/deps/undici/src/types/balanced-pool.d.ts +++ b/deps/undici/src/types/balanced-pool.d.ts @@ -5,10 +5,10 @@ import { URL } from 'url' export default BalancedPool declare class BalancedPool extends Dispatcher { - constructor(url: string | URL | string[], options?: Pool.Options); + constructor(url: string | string[] | URL | URL[], options?: Pool.Options); - addUpstream(upstream: string): BalancedPool; - removeUpstream(upstream: string): BalancedPool; + addUpstream(upstream: string | URL): BalancedPool; + removeUpstream(upstream: string | URL): BalancedPool; upstreams: Array; /** `true` after `pool.close()` has been called. */ diff --git a/deps/undici/src/types/client.d.ts b/deps/undici/src/types/client.d.ts index 871e3c2448a486..56074a15ae7a13 100644 --- a/deps/undici/src/types/client.d.ts +++ b/deps/undici/src/types/client.d.ts @@ -4,10 +4,10 @@ import Dispatcher from './dispatcher' import DispatchInterceptor from './dispatcher' import buildConnector from "./connector"; -export default Client - -/** A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default. */ -declare class Client extends Dispatcher { +/** + * A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default. + */ +export class Client extends Dispatcher { constructor(url: string | URL, options?: Client.Options); /** Property to get and set the pipelining factor. */ pipelining: number; @@ -17,40 +17,62 @@ declare class Client extends Dispatcher { destroyed: boolean; } -declare namespace Client { +export declare namespace Client { + export interface OptionsInterceptors { + Client: readonly DispatchInterceptor[]; + } export interface Options { + /** TODO */ + interceptors?: OptionsInterceptors; + /** The maximum length of request headers in bytes. Default: `16384` (16KiB). */ + maxHeaderSize?: number; + /** The amount of time the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `300e3` milliseconds (300s). */ + headersTimeout?: number; + /** @deprecated unsupported socketTimeout, use headersTimeout & bodyTimeout instead */ + socketTimeout?: never; + /** @deprecated unsupported requestTimeout, use headersTimeout & bodyTimeout instead */ + requestTimeout?: never; + /** TODO */ + connectTimeout?: number; + /** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Default: `300e3` milliseconds (300s). */ + bodyTimeout?: number; + /** @deprecated unsupported idleTimeout, use keepAliveTimeout instead */ + idleTimeout?: never; + /** @deprecated unsupported keepAlive, use pipelining=0 instead */ + keepAlive?: never; /** the timeout after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. Default: `4e3` milliseconds (4s). */ - keepAliveTimeout?: number | null; + keepAliveTimeout?: number; + /** @deprecated unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead */ + maxKeepAliveTimeout?: never; /** the maximum allowed `idleTimeout` when overridden by *keep-alive* hints from the server. Default: `600e3` milliseconds (10min). */ - keepAliveMaxTimeout?: number | null; + keepAliveMaxTimeout?: number; /** A number subtracted from server *keep-alive* hints when overriding `idleTimeout` to account for timing inaccuracies caused by e.g. transport latency. Default: `1e3` milliseconds (1s). */ - keepAliveTimeoutThreshold?: number | null; + keepAliveTimeoutThreshold?: number; + /** TODO */ + socketPath?: string; /** The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Default: `1`. */ - pipelining?: number | null; - /** **/ - connect?: buildConnector.BuildOptions | buildConnector.connector | null; - /** The maximum length of request headers in bytes. Default: `16384` (16KiB). */ - maxHeaderSize?: number | null; - /** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Default: `300e3` milliseconds (300s). */ - bodyTimeout?: number | null; - /** The amount of time the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `300e3` milliseconds (300s). */ - headersTimeout?: number | null; + pipelining?: number; + /** @deprecated use the connect option instead */ + tls?: never; /** If `true`, an error is thrown when the request content-length header doesn't match the length of the request body. Default: `true`. */ strictContentLength?: boolean; - /** @deprecated use the connect option instead */ - tls?: TlsOptions | null; - /** */ + /** TODO */ + maxCachedSessions?: number; + /** TODO */ + maxRedirections?: number; + /** TODO */ + connect?: buildConnector.BuildOptions | buildConnector.connector; + /** TODO */ maxRequestsPerClient?: number; + /** TODO */ + localAddress?: string; /** Max response body size in bytes, -1 is disabled */ - maxResponseSize?: number | null; + maxResponseSize?: number; /** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */ autoSelectFamily?: boolean; /** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */ - autoSelectFamilyAttemptTimeout?: number; - - interceptors?: {Client: readonly DispatchInterceptor[] | undefined} + autoSelectFamilyAttemptTimeout?: number; } - export interface SocketInfo { localAddress?: string localPort?: number @@ -61,6 +83,6 @@ declare namespace Client { bytesWritten?: number bytesRead?: number } - - } + +export default Client; diff --git a/deps/undici/src/types/proxy-agent.d.ts b/deps/undici/src/types/proxy-agent.d.ts index d312cb3f9a5b27..96b26381ced5df 100644 --- a/deps/undici/src/types/proxy-agent.d.ts +++ b/deps/undici/src/types/proxy-agent.d.ts @@ -1,7 +1,9 @@ import Agent from './agent' import buildConnector from './connector'; +import Client from './client' import Dispatcher from './dispatcher' import { IncomingHttpHeaders } from './header' +import Pool from './pool' export default ProxyAgent @@ -23,5 +25,6 @@ declare namespace ProxyAgent { headers?: IncomingHttpHeaders; requestTls?: buildConnector.BuildOptions; proxyTls?: buildConnector.BuildOptions; + clientFactory?(origin: URL, opts: object): Dispatcher; } } diff --git a/deps/undici/src/types/websocket.d.ts b/deps/undici/src/types/websocket.d.ts index 25c46a14c37478..dadd8013c1e1c2 100644 --- a/deps/undici/src/types/websocket.d.ts +++ b/deps/undici/src/types/websocket.d.ts @@ -1,5 +1,6 @@ /// +import type { MessagePort } from 'worker_threads' import { EventTarget, Event, diff --git a/deps/undici/undici.js b/deps/undici/undici.js index 145977f83d7b10..cccf3f10431455 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -300,7 +300,7 @@ var require_util = __commonJS({ function nop() { } function isStream(obj) { - return obj && typeof obj.pipe === "function"; + return obj && typeof obj === "object" && typeof obj.pipe === "function" && typeof obj.on === "function"; } function isBlobLike(object) { return Blob && object instanceof Blob || object && typeof object === "object" && (typeof object.stream === "function" || typeof object.arrayBuffer === "function") && /^(Blob|File)$/.test(object[Symbol.toStringTag]); @@ -318,6 +318,10 @@ var require_util = __commonJS({ function parseURL(url) { if (typeof url === "string") { url = new URL(url); + if (!/^https?:/.test(url.origin || url.protocol)) { + throw new InvalidArgumentError("invalid protocol"); + } + return url; } if (!url || typeof url !== "object") { throw new InvalidArgumentError("invalid url"); @@ -549,8 +553,22 @@ var require_util = __commonJS({ } }, 0); } - function isFormDataLike(chunk) { - return chunk && chunk.constructor && chunk.constructor.name === "FormData" && typeof chunk === "object" && (typeof chunk.append === "function" && typeof chunk.delete === "function" && typeof chunk.get === "function" && typeof chunk.getAll === "function" && typeof chunk.has === "function" && typeof chunk.set === "function" && typeof chunk.entries === "function" && typeof chunk.keys === "function" && typeof chunk.values === "function" && typeof chunk.forEach === "function"); + function isFormDataLike(object) { + return object && typeof object === "object" && typeof object.append === "function" && typeof object.delete === "function" && typeof object.get === "function" && typeof object.getAll === "function" && typeof object.has === "function" && typeof object.set === "function" && object[Symbol.toStringTag] === "FormData"; + } + function throwIfAborted(signal) { + if (!signal) { + return; + } + if (typeof signal.throwIfAborted === "function") { + signal.throwIfAborted(); + } else { + if (signal.aborted) { + const err = new Error("The operation was aborted"); + err.name = "AbortError"; + throw err; + } + } } var kEnumerableProperty = /* @__PURE__ */ Object.create(null); kEnumerableProperty.enumerable = true; @@ -582,6 +600,7 @@ var require_util = __commonJS({ getSocketInfo, isFormDataLike, buildURL, + throwIfAborted, nodeMajor, nodeMinor, nodeHasAutoSelectFamily: nodeMajor > 18 || nodeMajor === 18 && nodeMinor >= 13 @@ -767,11 +786,51 @@ var require_constants = __commonJS({ } }); +// lib/fetch/global.js +var require_global = __commonJS({ + "lib/fetch/global.js"(exports2, module2) { + "use strict"; + var globalOrigin = Symbol.for("undici.globalOrigin.1"); + function getGlobalOrigin() { + return globalThis[globalOrigin]; + } + function setGlobalOrigin(newOrigin) { + if (newOrigin !== void 0 && typeof newOrigin !== "string" && !(newOrigin instanceof URL)) { + throw new Error("Invalid base url"); + } + if (newOrigin === void 0) { + Object.defineProperty(globalThis, globalOrigin, { + value: void 0, + writable: true, + enumerable: false, + configurable: false + }); + return; + } + const parsedURL = new URL(newOrigin); + if (parsedURL.protocol !== "http:" && parsedURL.protocol !== "https:") { + throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`); + } + Object.defineProperty(globalThis, globalOrigin, { + value: parsedURL, + writable: true, + enumerable: false, + configurable: false + }); + } + module2.exports = { + getGlobalOrigin, + setGlobalOrigin + }; + } +}); + // lib/fetch/util.js var require_util2 = __commonJS({ "lib/fetch/util.js"(exports2, module2) { "use strict"; var { redirectStatus, badPorts, referrerPolicy: referrerPolicyTokens } = require_constants(); + var { getGlobalOrigin } = require_global(); var { performance: performance2 } = require("perf_hooks"); var { isBlobLike, toUSVString, ReadableStreamFrom } = require_util(); var assert = require("assert"); @@ -791,7 +850,9 @@ var require_util2 = __commonJS({ return null; } let location = response.headersList.get("location"); - location = location ? new URL(location, responseURL(response)) : null; + if (location !== null && isValidHeaderValue(location)) { + location = new URL(location, responseURL(response)); + } if (location && !location.hash) { location.hash = requestFragment; } @@ -884,7 +945,7 @@ var require_util2 = __commonJS({ let serializedOrigin = request.origin; if (request.responseTainting === "cors" || request.mode === "websocket") { if (serializedOrigin) { - request.headersList.append("Origin", serializedOrigin); + request.headersList.append("origin", serializedOrigin); } } else if (request.method !== "GET" && request.method !== "HEAD") { switch (request.referrerPolicy) { @@ -906,7 +967,7 @@ var require_util2 = __commonJS({ default: } if (serializedOrigin) { - request.headersList.append("Origin", serializedOrigin); + request.headersList.append("origin", serializedOrigin); } } } @@ -929,68 +990,73 @@ var require_util2 = __commonJS({ }; } function makePolicyContainer() { - return {}; + return { + referrerPolicy: "strict-origin-when-cross-origin" + }; } - function clonePolicyContainer() { - return {}; + function clonePolicyContainer(policyContainer) { + return { + referrerPolicy: policyContainer.referrerPolicy + }; } function determineRequestsReferrer(request) { const policy = request.referrerPolicy; - if (policy == null || policy === "" || policy === "no-referrer") { - return "no-referrer"; - } - const environment = request.client; + assert(policy); let referrerSource = null; if (request.referrer === "client") { - if (request.client?.globalObject?.constructor?.name === "Window") { - const origin = environment.globalObject.self?.origin ?? environment.globalObject.location?.origin; - if (origin == null || origin === "null") - return "no-referrer"; - referrerSource = new URL(environment.globalObject.location.href); - } else { - if (environment?.globalObject?.location == null) { - return "no-referrer"; - } - referrerSource = new URL(environment.globalObject.location.href); + const globalOrigin = getGlobalOrigin(); + if (!globalOrigin || globalOrigin.origin === "null") { + return "no-referrer"; } + referrerSource = new URL(globalOrigin); } else if (request.referrer instanceof URL) { referrerSource = request.referrer; - } else { - return "no-referrer"; } - const urlProtocol = referrerSource.protocol; - if (urlProtocol === "about:" || urlProtocol === "data:" || urlProtocol === "blob:") { - return "no-referrer"; + let referrerURL = stripURLForReferrer(referrerSource); + const referrerOrigin = stripURLForReferrer(referrerSource, true); + if (referrerURL.toString().length > 4096) { + referrerURL = referrerOrigin; } - let temp; - let referrerOrigin; - const referrerUrl = (temp = stripURLForReferrer(referrerSource)).length > 4096 ? referrerOrigin = stripURLForReferrer(referrerSource, true) : temp; - const areSameOrigin = sameOrigin(request, referrerUrl); - const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerUrl) && !isURLPotentiallyTrustworthy(request.url); + const areSameOrigin = sameOrigin(request, referrerURL); + const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(request.url); switch (policy) { case "origin": return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true); case "unsafe-url": - return referrerUrl; + 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": - if (areSameOrigin) - return referrerOrigin; + return areSameOrigin ? referrerURL : referrerOrigin; + case "strict-origin-when-cross-origin": { + const currentURL = requestCurrentURL(request); + if (sameOrigin(referrerURL, currentURL)) { + return referrerURL; + } + if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) { + return "no-referrer"; + } + return referrerOrigin; + } case "strict-origin": case "no-referrer-when-downgrade": default: return isNonPotentiallyTrustWorthy ? "no-referrer" : referrerOrigin; } - function stripURLForReferrer(url, originOnly = false) { - const urlObject = new URL(url.href); - urlObject.username = ""; - urlObject.password = ""; - urlObject.hash = ""; - return originOnly ? urlObject.origin : urlObject.href; + } + function stripURLForReferrer(url, originOnly) { + assert(url instanceof URL); + if (url.protocol === "file:" || url.protocol === "about:" || url.protocol === "blank:") { + return "no-referrer"; + } + url.username = ""; + url.password = ""; + url.hash = ""; + if (originOnly) { + url.pathname = ""; + url.search = ""; } + return url; } function isURLPotentiallyTrustworthy(url) { if (!(url instanceof URL)) { @@ -5620,7 +5686,6 @@ var require_dataURL = __commonJS({ "lib/fetch/dataURL.js"(exports2, module2) { var assert = require("assert"); var { atob: atob2 } = require("buffer"); - var { format } = require("url"); var { isValidHTTPToken, isomorphicDecode } = require_util2(); var encoder = new TextEncoder(); var HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-z0-9]+$/; @@ -5660,7 +5725,15 @@ var require_dataURL = __commonJS({ return { mimeType: mimeTypeRecord, body }; } 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); } function collectASequenceOfCodePoints(condition, input, position) { let result = ""; @@ -6514,45 +6587,6 @@ Content-Type: ${value.type || "application/octet-stream"}\r } }); -// lib/fetch/global.js -var require_global = __commonJS({ - "lib/fetch/global.js"(exports2, module2) { - "use strict"; - var globalOrigin = Symbol.for("undici.globalOrigin.1"); - function getGlobalOrigin() { - return globalThis[globalOrigin]; - } - function setGlobalOrigin(newOrigin) { - if (newOrigin !== void 0 && typeof newOrigin !== "string" && !(newOrigin instanceof URL)) { - throw new Error("Invalid base url"); - } - if (newOrigin === void 0) { - Object.defineProperty(globalThis, globalOrigin, { - value: void 0, - writable: true, - enumerable: false, - configurable: false - }); - return; - } - const parsedURL = new URL(newOrigin); - if (parsedURL.protocol !== "http:" && parsedURL.protocol !== "https:") { - throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`); - } - Object.defineProperty(globalThis, globalOrigin, { - value: parsedURL, - writable: true, - enumerable: false, - configurable: false - }); - } - module2.exports = { - getGlobalOrigin, - setGlobalOrigin - }; - } -}); - // lib/fetch/response.js var require_response = __commonJS({ "lib/fetch/response.js"(exports2, module2) { @@ -6954,7 +6988,8 @@ var require_request = __commonJS({ var { isValidHTTPToken, sameOrigin, - normalizeMethod + normalizeMethod, + makePolicyContainer } = require_util2(); var { forbiddenMethods, @@ -6989,7 +7024,11 @@ var require_request = __commonJS({ init = webidl.converters.RequestInit(init); this[kRealm] = { settingsObject: { - baseUrl: getGlobalOrigin() + baseUrl: getGlobalOrigin(), + get origin() { + return this.baseUrl?.origin; + }, + policyContainer: makePolicyContainer() } }; let request = null; @@ -7131,12 +7170,14 @@ var require_request = __commonJS({ if (signal.aborted) { ac.abort(signal.reason); } else { - const acRef = new WeakRef(ac); const abort = function() { - acRef.deref()?.abort(this.reason); + ac.abort(this.reason); }; - if (getEventListeners(signal, "abort").length >= defaultMaxListeners) { - setMaxListeners(100, signal); + try { + if (getEventListeners(signal, "abort").length >= defaultMaxListeners) { + setMaxListeners(100, signal); + } + } catch { } signal.addEventListener("abort", abort, { once: true }); requestFinalizer.register(this, { signal, abort }); @@ -7900,12 +7941,14 @@ var require_timers = __commonJS({ let idx = 0; while (idx < len) { const timer = fastTimers[idx]; - if (timer.expires && fastNow >= timer.expires) { - timer.expires = 0; + if (timer.state === 0) { + timer.state = fastNow + timer.delay; + } else if (timer.state > 0 && fastNow >= timer.state) { + timer.state = -1; timer.callback(timer.opaque); } - if (timer.expires === 0) { - timer.active = false; + if (timer.state === -1) { + timer.state = -2; if (idx !== len - 1) { fastTimers[idx] = fastTimers.pop(); } else { @@ -7936,32 +7979,31 @@ var require_timers = __commonJS({ this.callback = callback; this.delay = delay; this.opaque = opaque; - this.expires = 0; - this.active = false; + this.state = -2; this.refresh(); } refresh() { - if (!this.active) { - this.active = true; + if (this.state === -2) { fastTimers.push(this); if (!fastNowTimeout || fastTimers.length === 1) { refreshTimeout(); - fastNow = Date.now(); } } - this.expires = fastNow + this.delay; + this.state = 0; } clear() { - this.expires = 0; + this.state = -1; } }; module2.exports = { setTimeout(callback, delay, opaque) { - return new Timeout(callback, delay, opaque); + return delay < 1e3 ? setTimeout(callback, delay, opaque) : new Timeout(callback, delay, opaque); }, clearTimeout(timeout) { - if (timeout && timeout.clear) { + if (timeout instanceof Timeout) { timeout.clear(); + } else { + clearTimeout(timeout); } } }; @@ -10140,6 +10182,7 @@ upgrade: ${upgrade}\r } process.emitWarning(new RequestContentLengthMismatchError()); } + socket.cork(); if (bytesWritten === 0) { if (!expectsPayload) { socket[kReset] = true; @@ -10160,6 +10203,7 @@ ${len.toString(16)}\r } this.bytesWritten += len; const ret = socket.write(chunk); + socket.uncork(); request.onBodySent(chunk); if (!ret) { if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { @@ -10627,7 +10671,7 @@ var require_fetch = __commonJS({ }); cacheState = ""; } - response.timingInfo.endTime = coarsenedSharedCurrentTime(); + timingInfo.endTime = coarsenedSharedCurrentTime(); response.timingInfo = timingInfo; markResourceTiming(timingInfo, originalURL, initiatorType, globalThis, cacheState); } diff --git a/src/undici_version.h b/src/undici_version.h index d0534a81382216..6ef1d2afc63371 100644 --- a/src/undici_version.h +++ b/src/undici_version.h @@ -2,5 +2,5 @@ // Refer to tools/update-undici.sh #ifndef SRC_UNDICI_VERSION_H_ #define SRC_UNDICI_VERSION_H_ -#define UNDICI_VERSION "5.20.0" +#define UNDICI_VERSION "5.21.0" #endif // SRC_UNDICI_VERSION_H_