diff --git a/deps/undici/src/docs/docs/api/EnvHttpProxyAgent.md b/deps/undici/src/docs/docs/api/EnvHttpProxyAgent.md new file mode 100644 index 00000000000000..a4932de8be7908 --- /dev/null +++ b/deps/undici/src/docs/docs/api/EnvHttpProxyAgent.md @@ -0,0 +1,162 @@ +# Class: EnvHttpProxyAgent + +Stability: Experimental. + +Extends: `undici.Dispatcher` + +EnvHttpProxyAgent automatically reads the proxy configuration from the environment variables `http_proxy`, `https_proxy`, and `no_proxy` and sets up the proxy agents accordingly. When `http_proxy` and `https_proxy` are set, `http_proxy` is used for HTTP requests and `https_proxy` is used for HTTPS requests. If only `http_proxy` is set, `http_proxy` is used for both HTTP and HTTPS requests. If only `https_proxy` is set, it is only used for HTTPS requests. + +`no_proxy` is a comma or space-separated list of hostnames that should not be proxied. The list may contain leading wildcard characters (`*`). If `no_proxy` is set, the EnvHttpProxyAgent will bypass the proxy for requests to hosts that match the list. If `no_proxy` is set to `"*"`, the EnvHttpProxyAgent will bypass the proxy for all requests. + +Uppercase environment variables are also supported: `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`. However, if both the lowercase and uppercase environment variables are set, the uppercase environment variables will be ignored. + +## `new EnvHttpProxyAgent([options])` + +Arguments: + +* **options** `EnvHttpProxyAgentOptions` (optional) - extends the `Agent` options. + +Returns: `EnvHttpProxyAgent` + +### Parameter: `EnvHttpProxyAgentOptions` + +Extends: [`AgentOptions`](Agent.md#parameter-agentoptions) + +* **httpProxy** `string` (optional) - When set, it will override the `HTTP_PROXY` environment variable. +* **httpsProxy** `string` (optional) - When set, it will override the `HTTPS_PROXY` environment variable. +* **noProxy** `string` (optional) - When set, it will override the `NO_PROXY` environment variable. + +Examples: + +```js +import { EnvHttpProxyAgent } from 'undici' + +const envHttpProxyAgent = new EnvHttpProxyAgent() +// or +const envHttpProxyAgent = new EnvHttpProxyAgent({ httpProxy: 'my.proxy.server:8080', httpsProxy: 'my.proxy.server:8443', noProxy: 'localhost' }) +``` + +#### Example - EnvHttpProxyAgent instantiation + +This will instantiate the EnvHttpProxyAgent. It will not do anything until registered as the agent to use with requests. + +```js +import { EnvHttpProxyAgent } from 'undici' + +const envHttpProxyAgent = new EnvHttpProxyAgent() +``` + +#### Example - Basic Proxy Fetch with global agent dispatcher + +```js +import { setGlobalDispatcher, fetch, EnvHttpProxyAgent } from 'undici' + +const envHttpProxyAgent = new EnvHttpProxyAgent() +setGlobalDispatcher(envHttpProxyAgent) + +const { status, json } = await fetch('http://localhost:3000/foo') + +console.log('response received', status) // response received 200 + +const data = await json() // data { foo: "bar" } +``` + +#### Example - Basic Proxy Request with global agent dispatcher + +```js +import { setGlobalDispatcher, request, EnvHttpProxyAgent } from 'undici' + +const envHttpProxyAgent = new EnvHttpProxyAgent() +setGlobalDispatcher(envHttpProxyAgent) + +const { statusCode, body } = await request('http://localhost:3000/foo') + +console.log('response received', statusCode) // response received 200 + +for await (const data of body) { + console.log('data', data.toString('utf8')) // data foo +} +``` + +#### Example - Basic Proxy Request with local agent dispatcher + +```js +import { EnvHttpProxyAgent, request } from 'undici' + +const envHttpProxyAgent = new EnvHttpProxyAgent() + +const { + statusCode, + body +} = await request('http://localhost:3000/foo', { dispatcher: envHttpProxyAgent }) + +console.log('response received', statusCode) // response received 200 + +for await (const data of body) { + console.log('data', data.toString('utf8')) // data foo +} +``` + +#### Example - Basic Proxy Fetch with local agent dispatcher + +```js +import { EnvHttpProxyAgent, fetch } from 'undici' + +const envHttpProxyAgent = new EnvHttpProxyAgent() + +const { + status, + json +} = await fetch('http://localhost:3000/foo', { dispatcher: envHttpProxyAgent }) + +console.log('response received', status) // response received 200 + +const data = await json() // data { foo: "bar" } +``` + +## Instance Methods + +### `EnvHttpProxyAgent.close([callback])` + +Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallback-promise). + +### `EnvHttpProxyAgent.destroy([error, callback])` + +Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). + +### `EnvHttpProxyAgent.dispatch(options, handler: AgentDispatchOptions)` + +Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler). + +#### Parameter: `AgentDispatchOptions` + +Extends: [`DispatchOptions`](Dispatcher.md#parameter-dispatchoptions) + +* **origin** `string | URL` +* **maxRedirections** `Integer`. + +Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). + +### `EnvHttpProxyAgent.connect(options[, callback])` + +See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback). + +### `EnvHttpProxyAgent.dispatch(options, handler)` + +Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler). + +### `EnvHttpProxyAgent.pipeline(options, handler)` + +See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler). + +### `EnvHttpProxyAgent.request(options[, callback])` + +See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). + +### `EnvHttpProxyAgent.stream(options, factory[, callback])` + +See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback). + +### `EnvHttpProxyAgent.upgrade(options[, callback])` + +See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback). diff --git a/deps/undici/src/docs/docs/api/Util.md b/deps/undici/src/docs/docs/api/Util.md index 2393d079dfcbf1..53b96e3ed3f503 100644 --- a/deps/undici/src/docs/docs/api/Util.md +++ b/deps/undici/src/docs/docs/api/Util.md @@ -8,11 +8,11 @@ Receives a header object and returns the parsed value. Arguments: -- **headers** `Record | (Buffer | string | (Buffer | string)[])[]` (required) - Header object. +- **headers** `(Buffer | string | (Buffer | string)[])[]` (required) - Header object. - **obj** `Record` (optional) - Object to specify a proxy object. The parsed value is assigned to this object. But, if **headers** is an object, it is not used. -Returns: `Record` If **headers** is an object, it is **headers**. Otherwise, if **obj** is specified, it is equivalent to **obj**. +Returns: `Record` If **obj** is specified, it is equivalent to **obj**. ## `headerNameToString(value)` diff --git a/deps/undici/src/docs/docs/best-practices/proxy.md b/deps/undici/src/docs/docs/best-practices/proxy.md index 60f3372cf750b3..5764ff38b3c677 100644 --- a/deps/undici/src/docs/docs/best-practices/proxy.md +++ b/deps/undici/src/docs/docs/best-practices/proxy.md @@ -2,7 +2,7 @@ Connecting through a proxy is possible by: -- Using [AgentProxy](../api/ProxyAgent.md). +- Using [ProxyAgent](../api/ProxyAgent.md). - Configuring `Client` or `Pool` constructor. The proxy url should be passed to the `Client` or `Pool` constructor, while the upstream server url diff --git a/deps/undici/src/lib/api/api-request.js b/deps/undici/src/lib/api/api-request.js index e5d598aa6dd5d8..ced5590d21053d 100644 --- a/deps/undici/src/lib/api/api-request.js +++ b/deps/undici/src/lib/api/api-request.js @@ -2,11 +2,10 @@ const assert = require('node:assert') const { Readable } = require('./readable') -const { InvalidArgumentError } = require('../core/errors') +const { InvalidArgumentError, RequestAbortedError } = require('../core/errors') const util = require('../core/util') const { getResolveErrorBodyCallback } = require('./util') const { AsyncResource } = require('node:async_hooks') -const { addSignal, removeSignal } = require('./abort-signal') class RequestHandler extends AsyncResource { constructor (opts, callback) { @@ -45,6 +44,7 @@ class RequestHandler extends AsyncResource { throw err } + this.method = method this.responseHeaders = responseHeaders || null this.opaque = opaque || null this.callback = callback @@ -56,6 +56,9 @@ class RequestHandler extends AsyncResource { this.onInfo = onInfo || null this.throwOnError = throwOnError this.highWaterMark = highWaterMark + this.signal = signal + this.reason = null + this.removeAbortListener = null if (util.isStream(body)) { body.on('error', (err) => { @@ -63,7 +66,26 @@ class RequestHandler extends AsyncResource { }) } - addSignal(this, signal) + if (this.signal) { + if (this.signal.aborted) { + this.reason = this.signal.reason ?? new RequestAbortedError() + } else { + this.removeAbortListener = util.addAbortListener(this.signal, () => { + this.reason = this.signal.reason ?? new RequestAbortedError() + if (this.res) { + util.destroy(this.res, this.reason) + } else if (this.abort) { + this.abort(this.reason) + } + + if (this.removeAbortListener) { + this.res?.off('close', this.removeAbortListener) + this.removeAbortListener() + this.removeAbortListener = null + } + }) + } + } } onConnect (abort, context) { @@ -93,14 +115,26 @@ class RequestHandler extends AsyncResource { const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers const contentType = parsedHeaders['content-type'] const contentLength = parsedHeaders['content-length'] - const body = new Readable({ resume, abort, contentType, contentLength, highWaterMark }) + const res = new Readable({ + resume, + abort, + contentType, + contentLength: this.method !== 'HEAD' && contentLength + ? Number(contentLength) + : null, + highWaterMark + }) + + if (this.removeAbortListener) { + res.on('close', this.removeAbortListener) + } this.callback = null - this.res = body + this.res = res if (callback !== null) { if (this.throwOnError && statusCode >= 400) { this.runInAsyncScope(getResolveErrorBodyCallback, null, - { callback, body, contentType, statusCode, statusMessage, headers } + { callback, body: res, contentType, statusCode, statusMessage, headers } ) } else { this.runInAsyncScope(callback, null, null, { @@ -108,7 +142,7 @@ class RequestHandler extends AsyncResource { headers, trailers: this.trailers, opaque, - body, + body: res, context }) } @@ -116,25 +150,17 @@ class RequestHandler extends AsyncResource { } onData (chunk) { - const { res } = this - return res.push(chunk) + return this.res.push(chunk) } onComplete (trailers) { - const { res } = this - - removeSignal(this) - util.parseHeaders(trailers, this.trailers) - - res.push(null) + this.res.push(null) } onError (err) { const { res, callback, body, opaque } = this - removeSignal(this) - if (callback) { // TODO: Does this need queueMicrotask? this.callback = null @@ -155,6 +181,12 @@ class RequestHandler extends AsyncResource { this.body = null util.destroy(body, err) } + + if (this.removeAbortListener) { + res?.off('close', this.removeAbortListener) + this.removeAbortListener() + this.removeAbortListener = null + } } } diff --git a/deps/undici/src/lib/api/readable.js b/deps/undici/src/lib/api/readable.js index f0e4dbd34bc124..a65a7fcb557106 100644 --- a/deps/undici/src/lib/api/readable.js +++ b/deps/undici/src/lib/api/readable.js @@ -63,9 +63,13 @@ class BodyReadable extends Readable { // tick as it is created, then a user who is waiting for a // promise (i.e micro tick) for installing a 'error' listener will // never get a chance and will always encounter an unhandled exception. - setImmediate(() => { + if (!this[kReading]) { + setImmediate(() => { + callback(err) + }) + } else { callback(err) - }) + } } on (ev, ...args) { diff --git a/deps/undici/src/lib/core/connect.js b/deps/undici/src/lib/core/connect.js index 21e16af8bc4e9d..4ad2c91d2cb99c 100644 --- a/deps/undici/src/lib/core/connect.js +++ b/deps/undici/src/lib/core/connect.js @@ -165,7 +165,7 @@ function setupTimeout (onConnectTimeout, timeout) { let s1 = null let s2 = null const timeoutId = setTimeout(() => { - // setImmediate is added to make sure that we priotorise socket error events over timeouts + // setImmediate is added to make sure that we prioritize socket error events over timeouts s1 = setImmediate(() => { if (process.platform === 'win32') { // Windows needs an extra setImmediate probably due to implementation differences in the socket logic diff --git a/deps/undici/src/lib/dispatcher/client-h2.js b/deps/undici/src/lib/dispatcher/client-h2.js index cd63bcda2f2ca7..c78fe602c4890f 100644 --- a/deps/undici/src/lib/dispatcher/client-h2.js +++ b/deps/undici/src/lib/dispatcher/client-h2.js @@ -524,6 +524,7 @@ function writeH2 (client, request) { } } else if (util.isStream(body)) { writeStream({ + abort, body, client, request, @@ -535,6 +536,7 @@ function writeH2 (client, request) { }) } else if (util.isIterable(body)) { writeIterable({ + abort, body, client, request, diff --git a/deps/undici/src/lib/dispatcher/env-http-proxy-agent.js b/deps/undici/src/lib/dispatcher/env-http-proxy-agent.js new file mode 100644 index 00000000000000..897011adbcd635 --- /dev/null +++ b/deps/undici/src/lib/dispatcher/env-http-proxy-agent.js @@ -0,0 +1,160 @@ +'use strict' + +const DispatcherBase = require('./dispatcher-base') +const { kClose, kDestroy, kClosed, kDestroyed, kDispatch, kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent } = require('../core/symbols') +const ProxyAgent = require('./proxy-agent') +const Agent = require('./agent') + +const DEFAULT_PORTS = { + 'http:': 80, + 'https:': 443 +} + +let experimentalWarned = false + +class EnvHttpProxyAgent extends DispatcherBase { + #noProxyValue = null + #noProxyEntries = null + #opts = null + + constructor (opts = {}) { + super() + this.#opts = opts + + if (!experimentalWarned) { + experimentalWarned = true + process.emitWarning('EnvHttpProxyAgent is experimental, expect them to change at any time.', { + code: 'UNDICI-EHPA' + }) + } + + const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts + + this[kNoProxyAgent] = new Agent(agentOpts) + + const HTTP_PROXY = httpProxy ?? process.env.http_proxy ?? process.env.HTTP_PROXY + if (HTTP_PROXY) { + this[kHttpProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTP_PROXY }) + } else { + this[kHttpProxyAgent] = this[kNoProxyAgent] + } + + const HTTPS_PROXY = httpsProxy ?? process.env.https_proxy ?? process.env.HTTPS_PROXY + if (HTTPS_PROXY) { + this[kHttpsProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTPS_PROXY }) + } else { + this[kHttpsProxyAgent] = this[kHttpProxyAgent] + } + + this.#parseNoProxy() + } + + [kDispatch] (opts, handler) { + const url = new URL(opts.origin) + const agent = this.#getProxyAgentForUrl(url) + return agent.dispatch(opts, handler) + } + + async [kClose] () { + await this[kNoProxyAgent].close() + if (!this[kHttpProxyAgent][kClosed]) { + await this[kHttpProxyAgent].close() + } + if (!this[kHttpsProxyAgent][kClosed]) { + await this[kHttpsProxyAgent].close() + } + } + + async [kDestroy] (err) { + await this[kNoProxyAgent].destroy(err) + if (!this[kHttpProxyAgent][kDestroyed]) { + await this[kHttpProxyAgent].destroy(err) + } + if (!this[kHttpsProxyAgent][kDestroyed]) { + await this[kHttpsProxyAgent].destroy(err) + } + } + + #getProxyAgentForUrl (url) { + let { protocol, host: hostname, port } = url + + // Stripping ports in this way instead of using parsedUrl.hostname to make + // sure that the brackets around IPv6 addresses are kept. + hostname = hostname.replace(/:\d*$/, '').toLowerCase() + port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0 + if (!this.#shouldProxy(hostname, port)) { + return this[kNoProxyAgent] + } + if (protocol === 'https:') { + return this[kHttpsProxyAgent] + } + return this[kHttpProxyAgent] + } + + #shouldProxy (hostname, port) { + if (this.#noProxyChanged) { + this.#parseNoProxy() + } + + if (this.#noProxyEntries.length === 0) { + return true // Always proxy if NO_PROXY is not set or empty. + } + if (this.#noProxyValue === '*') { + return false // Never proxy if wildcard is set. + } + + for (let i = 0; i < this.#noProxyEntries.length; i++) { + const entry = this.#noProxyEntries[i] + if (entry.port && entry.port !== port) { + continue // Skip if ports don't match. + } + if (!/^[.*]/.test(entry.hostname)) { + // No wildcards, so don't proxy only if there is not an exact match. + if (hostname === entry.hostname) { + return false + } + } else { + // Don't proxy if the hostname ends with the no_proxy host. + if (hostname.endsWith(entry.hostname.replace(/^\*/, ''))) { + return false + } + } + } + + return true + } + + #parseNoProxy () { + const noProxyValue = this.#opts.noProxy ?? this.#noProxyEnv + const noProxySplit = noProxyValue.split(/[,\s]/) + const noProxyEntries = [] + + for (let i = 0; i < noProxySplit.length; i++) { + const entry = noProxySplit[i] + if (!entry) { + continue + } + const parsed = entry.match(/^(.+):(\d+)$/) + noProxyEntries.push({ + hostname: (parsed ? parsed[1] : entry).toLowerCase(), + port: parsed ? Number.parseInt(parsed[2], 10) : 0 + }) + } + + this.#noProxyValue = noProxyValue + this.#noProxyEntries = noProxyEntries + } + + get #noProxyChanged () { + if (this.#opts.noProxy !== undefined) { + return false + } + return this.#noProxyValue !== this.#noProxyEnv + } + + get #noProxyEnv () { + return process.env.no_proxy ?? process.env.NO_PROXY ?? '' + } +} + +module.exports = EnvHttpProxyAgent diff --git a/deps/undici/src/lib/llhttp/.gitkeep b/deps/undici/src/lib/llhttp/.gitkeep new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/deps/undici/src/lib/web/fetch/data-url.js b/deps/undici/src/lib/web/fetch/data-url.js index ddef36390f3ffd..3f42e2eb6b22bc 100644 --- a/deps/undici/src/lib/web/fetch/data-url.js +++ b/deps/undici/src/lib/web/fetch/data-url.js @@ -7,13 +7,13 @@ const encoder = new TextEncoder() /** * @see https://mimesniff.spec.whatwg.org/#http-token-code-point */ -const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/ +const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+\-.^_|~A-Za-z0-9]+$/ const HTTP_WHITESPACE_REGEX = /[\u000A\u000D\u0009\u0020]/ // eslint-disable-line const ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g // 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 +const HTTP_QUOTED_STRING_TOKENS = /^[\u0009\u0020-\u007E\u0080-\u00FF]+$/ // eslint-disable-line // https://fetch.spec.whatwg.org/#data-url-processor /** @param {URL} dataURL */ diff --git a/deps/undici/src/lib/web/fetch/headers.js b/deps/undici/src/lib/web/fetch/headers.js index 3157561b47054a..d726b5a5d10636 100644 --- a/deps/undici/src/lib/web/fetch/headers.js +++ b/deps/undici/src/lib/web/fetch/headers.js @@ -250,7 +250,7 @@ class HeadersList { get entries () { const headers = {} - if (this[kHeadersMap].size) { + if (this[kHeadersMap].size !== 0) { for (const { name, value } of this[kHeadersMap].values()) { headers[name] = value } @@ -259,6 +259,10 @@ class HeadersList { return headers } + rawValues () { + return this[kHeadersMap].values() + } + get entriesList () { const headers = [] diff --git a/deps/undici/src/lib/web/fetch/index.js b/deps/undici/src/lib/web/fetch/index.js index 82ccb26865c168..784e0c2cdbbf0b 100644 --- a/deps/undici/src/lib/web/fetch/index.js +++ b/deps/undici/src/lib/web/fetch/index.js @@ -120,12 +120,16 @@ class Fetch extends EE { } } +function handleFetchDone (response) { + finalizeAndReportTiming(response, 'fetch') +} + // https://fetch.spec.whatwg.org/#fetch-method function fetch (input, init = undefined) { webidl.argumentLengthCheck(arguments, 1, 'globalThis.fetch') // 1. Let p be a new promise. - const p = createDeferredPromise() + let p = createDeferredPromise() // 2. Let requestObject be the result of invoking the initial value of // Request as constructor with input and init as arguments. If this throws @@ -185,16 +189,17 @@ function fetch (input, init = undefined) { // 3. Abort controller with requestObject’s signal’s abort reason. controller.abort(requestObject.signal.reason) + const realResponse = responseObject?.deref() + // 4. Abort the fetch() call with p, request, responseObject, // and requestObject’s signal’s abort reason. - abortFetch(p, request, responseObject, requestObject.signal.reason) + abortFetch(p, request, realResponse, requestObject.signal.reason) } ) // 12. Let handleFetchDone given response response be to finalize and // report timing with response, globalObject, and "fetch". - const handleFetchDone = (response) => - finalizeAndReportTiming(response, 'fetch') + // see function handleFetchDone // 13. Set controller to the result of calling fetch given request, // with processResponseEndOfBody set to handleFetchDone, and processResponse @@ -228,10 +233,11 @@ function fetch (input, init = undefined) { // 4. Set responseObject to the result of creating a Response object, // given response, "immutable", and relevantRealm. - responseObject = fromInnerResponse(response, 'immutable') + responseObject = new WeakRef(fromInnerResponse(response, 'immutable')) // 5. Resolve p with responseObject. - p.resolve(responseObject) + p.resolve(responseObject.deref()) + p = null } controller = fetching({ @@ -314,7 +320,10 @@ const markResourceTiming = performance.markResourceTiming // https://fetch.spec.whatwg.org/#abort-fetch function abortFetch (p, request, responseObject, error) { // 1. Reject promise with error. - p.reject(error) + if (p) { + // We might have already resolved the promise at this stage + p.reject(error) + } // 2. If request’s body is not null and is readable, then cancel request’s // body with error. @@ -435,8 +444,7 @@ function fetching ({ // 9. If request’s origin is "client", then set request’s origin to request’s // client’s origin. if (request.origin === 'client') { - // TODO: What if request.client is null? - request.origin = request.client?.origin + request.origin = request.client.origin } // 10. If all of the following conditions are true: @@ -1066,7 +1074,10 @@ function fetchFinale (fetchParams, response) { // 4. If fetchParams’s process response is non-null, then queue a fetch task to run fetchParams’s // process response given response, with fetchParams’s task destination. if (fetchParams.processResponse != null) { - queueMicrotask(() => fetchParams.processResponse(response)) + queueMicrotask(() => { + fetchParams.processResponse(response) + fetchParams.processResponse = null + }) } // 5. Let internalResponse be response, if response is a network error; otherwise response’s internal response. @@ -1884,7 +1895,11 @@ async function httpNetworkFetch ( // 12. Let cancelAlgorithm be an algorithm that aborts fetchParams’s // controller with reason, given reason. const cancelAlgorithm = (reason) => { - fetchParams.controller.abort(reason) + // If the aborted fetch was already terminated, then we do not + // need to do anything. + if (!isCancelled(fetchParams)) { + fetchParams.controller.abort(reason) + } } // 13. Let highWaterMark be a non-negative, non-NaN number, chosen by @@ -2102,20 +2117,16 @@ async function httpNetworkFetch ( const headersList = new HeadersList() - // For H2, the rawHeaders are a plain JS object - // We distinguish between them and iterate accordingly - if (Array.isArray(rawHeaders)) { - for (let i = 0; i < rawHeaders.length; i += 2) { - headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true) - } - const contentEncoding = headersList.get('content-encoding', true) - if (contentEncoding) { - // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 - // "All content-coding values are case-insensitive..." - codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim()) - } - location = headersList.get('location', true) + for (let i = 0; i < rawHeaders.length; i += 2) { + headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true) + } + const contentEncoding = headersList.get('content-encoding', true) + if (contentEncoding) { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // "All content-coding values are case-insensitive..." + codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim()) } + location = headersList.get('location', true) this.body = new Readable({ read: resume }) @@ -2125,7 +2136,7 @@ async function httpNetworkFetch ( redirectStatusSet.has(status) // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding - if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) { + if (codings.length !== 0 && request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) { for (let i = 0; i < codings.length; ++i) { const coding = codings[i] // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2 diff --git a/deps/undici/src/lib/web/fetch/request.js b/deps/undici/src/lib/web/fetch/request.js index 1441a58b06e2c0..c6d42fd36d6808 100644 --- a/deps/undici/src/lib/web/fetch/request.js +++ b/deps/undici/src/lib/web/fetch/request.js @@ -477,9 +477,8 @@ class Request { // 4. If headers is a Headers object, then for each header in its header // list, append header’s name/header’s value to this’s headers. if (headers instanceof HeadersList) { - for (const { 0: key, 1: val } of headers) { - // Note: The header names are already in lowercase. - headersList.append(key, val, true) + for (const { name, value } of headers.rawValues()) { + headersList.append(name, value, false) } // Note: Copy the `set-cookie` meta-data. headersList.cookies = headers.cookies @@ -820,51 +819,50 @@ class Request { mixinBody(Request) +// https://fetch.spec.whatwg.org/#requests function makeRequest (init) { - // https://fetch.spec.whatwg.org/#requests - const request = { - method: 'GET', - localURLsOnly: false, - unsafeRequest: false, - body: null, - client: null, - reservedClient: null, - replacesClientId: '', - window: 'client', - keepalive: false, - serviceWorkers: 'all', - initiator: '', - destination: '', - priority: null, - origin: 'client', - policyContainer: 'client', - referrer: 'client', - referrerPolicy: '', - mode: 'no-cors', - useCORSPreflightFlag: false, - credentials: 'same-origin', - useCredentials: false, - cache: 'default', - redirect: 'follow', - integrity: '', - cryptoGraphicsNonceMetadata: '', - parserMetadata: '', - reloadNavigation: false, - historyNavigation: false, - userActivation: false, - taintedOrigin: false, - redirectCount: 0, - responseTainting: 'basic', - preventNoCacheCacheControlHeaderModification: false, - done: false, - timingAllowFailed: false, - ...init, + return { + method: init.method ?? 'GET', + localURLsOnly: init.localURLsOnly ?? false, + unsafeRequest: init.unsafeRequest ?? false, + body: init.body ?? null, + client: init.client ?? null, + reservedClient: init.reservedClient ?? null, + replacesClientId: init.replacesClientId ?? '', + window: init.window ?? 'client', + keepalive: init.keepalive ?? false, + serviceWorkers: init.serviceWorkers ?? 'all', + initiator: init.initiator ?? '', + destination: init.destination ?? '', + priority: init.priority ?? null, + origin: init.origin ?? 'client', + policyContainer: init.policyContainer ?? 'client', + referrer: init.referrer ?? 'client', + referrerPolicy: init.referrerPolicy ?? '', + mode: init.mode ?? 'no-cors', + useCORSPreflightFlag: init.useCORSPreflightFlag ?? false, + credentials: init.credentials ?? 'same-origin', + useCredentials: init.useCredentials ?? false, + cache: init.cache ?? 'default', + redirect: init.redirect ?? 'follow', + integrity: init.integrity ?? '', + cryptoGraphicsNonceMetadata: init.cryptoGraphicsNonceMetadata ?? '', + parserMetadata: init.parserMetadata ?? '', + reloadNavigation: init.reloadNavigation ?? false, + historyNavigation: init.historyNavigation ?? false, + userActivation: init.userActivation ?? false, + taintedOrigin: init.taintedOrigin ?? false, + redirectCount: init.redirectCount ?? 0, + responseTainting: init.responseTainting ?? 'basic', + preventNoCacheCacheControlHeaderModification: init.preventNoCacheCacheControlHeaderModification ?? false, + done: init.done ?? false, + timingAllowFailed: init.timingAllowFailed ?? false, + urlList: init.urlList, + url: init.urlList[0], headersList: init.headersList ? new HeadersList(init.headersList) : new HeadersList() } - request.url = request.urlList[0] - return request } // https://fetch.spec.whatwg.org/#concept-request-clone diff --git a/deps/undici/src/lib/web/fetch/response.js b/deps/undici/src/lib/web/fetch/response.js index 222a9a5b2f7b2a..81c32fe3e51d0d 100644 --- a/deps/undici/src/lib/web/fetch/response.js +++ b/deps/undici/src/lib/web/fetch/response.js @@ -26,9 +26,23 @@ const { URLSerializer } = require('./data-url') const { kHeadersList, kConstruct } = require('../../core/symbols') const assert = require('node:assert') const { types } = require('node:util') +const { isDisturbed, isErrored } = require('node:stream') const textEncoder = new TextEncoder('utf-8') +const hasFinalizationRegistry = globalThis.FinalizationRegistry && process.version.indexOf('v18') !== 0 +let registry + +if (hasFinalizationRegistry) { + registry = new FinalizationRegistry((stream) => { + if (!stream.locked && !isDisturbed(stream) && !isErrored(stream)) { + stream.cancel('Response object has been garbage collected').catch(noop) + } + }) +} + +function noop () {} + // https://fetch.spec.whatwg.org/#response-class class Response { // Creates network error Response. @@ -510,6 +524,11 @@ function fromInnerResponse (innerResponse, guard) { response[kHeaders] = new Headers(kConstruct) response[kHeaders][kHeadersList] = innerResponse.headersList response[kHeaders][kGuard] = guard + + if (hasFinalizationRegistry && innerResponse.body?.stream) { + registry.register(response, innerResponse.body.stream) + } + return response } diff --git a/deps/undici/src/lib/web/fetch/util.js b/deps/undici/src/lib/web/fetch/util.js index 09680ebeeaaafc..2d30d41093aacd 100644 --- a/deps/undici/src/lib/web/fetch/util.js +++ b/deps/undici/src/lib/web/fetch/util.js @@ -254,16 +254,23 @@ function appendFetchMetadata (httpRequest) { // https://fetch.spec.whatwg.org/#append-a-request-origin-header function appendRequestOriginHeader (request) { - // 1. Let serializedOrigin be the result of byte-serializing a request origin with request. + // 1. Let serializedOrigin be the result of byte-serializing a request origin + // with request. + // TODO: implement "byte-serializing a request origin" let serializedOrigin = request.origin - // 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, true) - } + // "'client' is changed to an origin during fetching." + // This doesn't happen in undici (in most cases) because undici, by default, + // has no concept of origin. + if (serializedOrigin === 'client') { + return + } + // 2. If request’s response tainting is "cors" or request’s mode is "websocket", + // then append (`Origin`, serializedOrigin) to request’s header list. // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: + if (request.responseTainting === 'cors' || request.mode === 'websocket') { + request.headersList.append('origin', serializedOrigin, true) } else if (request.method !== 'GET' && request.method !== 'HEAD') { // 1. Switch on request’s referrer policy: switch (request.referrerPolicy) { @@ -274,13 +281,16 @@ function appendRequestOriginHeader (request) { case 'no-referrer-when-downgrade': case 'strict-origin': case 'strict-origin-when-cross-origin': - // If request’s origin is a tuple origin, its scheme is "https", and request’s current URL’s scheme is not "https", then set serializedOrigin to `null`. + // If request’s origin is a tuple origin, its scheme is "https", and + // request’s current URL’s scheme is not "https", then set + // serializedOrigin to `null`. if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) { serializedOrigin = null } break case 'same-origin': - // If request’s origin is not same origin with request’s current URL’s origin, then set serializedOrigin to `null`. + // If request’s origin is not same origin with request’s current URL’s + // origin, then set serializedOrigin to `null`. if (!sameOrigin(request, requestCurrentURL(request))) { serializedOrigin = null } @@ -289,10 +299,8 @@ function appendRequestOriginHeader (request) { // Do nothing. } - if (serializedOrigin) { - // 2. Append (`Origin`, serializedOrigin) to request’s header list. - request.headersList.append('origin', serializedOrigin, true) - } + // 2. Append (`Origin`, serializedOrigin) to request’s header list. + request.headersList.append('origin', serializedOrigin, true) } } diff --git a/deps/undici/src/lib/web/fetch/webidl.js b/deps/undici/src/lib/web/fetch/webidl.js index 96ec7767a1a9ea..13cafae6f1b813 100644 --- a/deps/undici/src/lib/web/fetch/webidl.js +++ b/deps/undici/src/lib/web/fetch/webidl.js @@ -250,6 +250,7 @@ webidl.sequenceConverter = function (converter) { /** @type {Generator} */ const method = typeof Iterable === 'function' ? Iterable() : V?.[Symbol.iterator]?.() const seq = [] + let index = 0 // 3. If method is undefined, throw a TypeError. if ( @@ -270,7 +271,7 @@ webidl.sequenceConverter = function (converter) { break } - seq.push(converter(value, prefix, argument)) + seq.push(converter(value, prefix, `${argument}[${index++}]`)) } return seq diff --git a/deps/undici/src/lib/web/websocket/connection.js b/deps/undici/src/lib/web/websocket/connection.js index 8a0ce1914c132d..5058fc58095cd4 100644 --- a/deps/undici/src/lib/web/websocket/connection.js +++ b/deps/undici/src/lib/web/websocket/connection.js @@ -1,13 +1,14 @@ 'use strict' -const { uid, states, sentCloseFrameState } = require('./constants') +const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants') const { kReadyState, kSentClose, kByteParser, - kReceivedClose + kReceivedClose, + kResponse } = require('./symbols') -const { fireEvent, failWebsocketConnection } = require('./util') +const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished } = require('./util') const { channels } = require('../../core/diagnostics') const { CloseEvent } = require('./events') const { makeRequest } = require('../fetch/request') @@ -15,6 +16,7 @@ const { fetching } = require('../fetch/index') const { Headers } = require('../fetch/headers') const { getDecodeSplit } = require('../fetch/util') const { kHeadersList } = require('../../core/symbols') +const { WebsocketFrameSend } = require('./frame') /** @type {import('crypto')} */ let crypto @@ -33,7 +35,7 @@ try { * @param {(response: any) => void} onEstablish * @param {Partial} options */ -function establishWebSocketConnection (url, protocols, ws, onEstablish, options) { +function establishWebSocketConnection (url, protocols, client, ws, onEstablish, options) { // 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 @@ -46,6 +48,7 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish, options) // and redirect mode is "error". const request = makeRequest({ urlList: [requestURL], + client, serviceWorkers: 'none', referrer: 'no-referrer', mode: 'websocket', @@ -211,6 +214,72 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish, options) return controller } +function closeWebSocketConnection (ws, code, reason, reasonByteLength) { + if (isClosing(ws) || isClosed(ws)) { + // If this's ready state is CLOSING (2) or CLOSED (3) + // Do nothing. + } else if (!isEstablished(ws)) { + // If the WebSocket connection is not yet established + // Fail the WebSocket connection and set this's ready state + // to CLOSING (2). + failWebsocketConnection(ws, 'Connection was closed before it was established.') + ws[kReadyState] = states.CLOSING + } else if (ws[kSentClose] === sentCloseFrameState.NOT_SENT) { + // If the WebSocket closing handshake has not yet been started + // Start the WebSocket closing handshake and set this's ready + // state to CLOSING (2). + // - If neither code nor reason is present, the WebSocket Close + // message must not have a body. + // - If code is present, then the status code to use in the + // WebSocket Close message must be the integer given by code. + // - If reason is also present, then reasonBytes must be + // provided in the Close message after the status code. + + ws[kSentClose] = sentCloseFrameState.PROCESSING + + const frame = new WebsocketFrameSend() + + // If neither code nor reason is present, the WebSocket Close + // message must not have a body. + + // If code is present, then the status code to use in the + // WebSocket Close message must be the integer given by code. + if (code !== undefined && reason === undefined) { + frame.frameData = Buffer.allocUnsafe(2) + frame.frameData.writeUInt16BE(code, 0) + } else if (code !== undefined && reason !== undefined) { + // If reason is also present, then reasonBytes must be + // provided in the Close message after the status code. + frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength) + frame.frameData.writeUInt16BE(code, 0) + // the body MAY contain UTF-8-encoded data with value /reason/ + frame.frameData.write(reason, 2, 'utf-8') + } else { + frame.frameData = emptyBuffer + } + + /** @type {import('stream').Duplex} */ + const socket = ws[kResponse].socket + + socket.write(frame.createFrame(opcodes.CLOSE), (err) => { + if (!err) { + ws[kSentClose] = sentCloseFrameState.SENT + } + }) + + ws[kSentClose] = sentCloseFrameState.PROCESSING + + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + ws[kReadyState] = states.CLOSING + } else { + // Otherwise + // Set this's ready state to CLOSING (2). + ws[kReadyState] = states.CLOSING + } +} + /** * @param {Buffer} chunk */ @@ -237,10 +306,10 @@ function onSocketClose () { const result = ws[kByteParser].closingInfo - if (result) { + if (result && !result.error) { code = result.code ?? 1005 reason = result.reason - } else if (ws[kSentClose] !== sentCloseFrameState.SENT) { + } else if (!ws[kReceivedClose]) { // If _The WebSocket // Connection is Closed_ and no Close control frame was received by the // endpoint (such as could occur if the underlying transport connection @@ -293,5 +362,6 @@ function onSocketError (error) { } module.exports = { - establishWebSocketConnection + establishWebSocketConnection, + closeWebSocketConnection } diff --git a/deps/undici/src/lib/web/websocket/frame.js b/deps/undici/src/lib/web/websocket/frame.js index 30c68c811cdf85..b062ffde8ecb01 100644 --- a/deps/undici/src/lib/web/websocket/frame.js +++ b/deps/undici/src/lib/web/websocket/frame.js @@ -2,13 +2,34 @@ const { maxUnsigned16Bit } = require('./constants') +const BUFFER_SIZE = 16386 + /** @type {import('crypto')} */ let crypto +let buffer = null +let bufIdx = BUFFER_SIZE + try { crypto = require('node:crypto') /* c8 ignore next 3 */ } catch { + crypto = { + // not full compatibility, but minimum. + randomFillSync: function randomFillSync (buffer, _offset, _size) { + for (let i = 0; i < buffer.length; ++i) { + buffer[i] = Math.random() * 255 | 0 + } + return buffer + } + } +} +function generateMask () { + if (bufIdx === BUFFER_SIZE) { + bufIdx = 0 + crypto.randomFillSync((buffer ??= Buffer.allocUnsafe(BUFFER_SIZE)), 0, BUFFER_SIZE) + } + return [buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++]] } class WebsocketFrameSend { @@ -17,11 +38,12 @@ class WebsocketFrameSend { */ constructor (data) { this.frameData = data - this.maskKey = crypto.randomBytes(4) } createFrame (opcode) { - const bodyLength = this.frameData?.byteLength ?? 0 + const frameData = this.frameData + const maskKey = generateMask() + const bodyLength = frameData?.byteLength ?? 0 /** @type {number} */ let payloadLength = bodyLength // 0-125 @@ -43,10 +65,10 @@ class WebsocketFrameSend { buffer[0] = (buffer[0] & 0xF0) + opcode // opcode /*! ws. MIT License. Einar Otto Stangvik */ - buffer[offset - 4] = this.maskKey[0] - buffer[offset - 3] = this.maskKey[1] - buffer[offset - 2] = this.maskKey[2] - buffer[offset - 1] = this.maskKey[3] + buffer[offset - 4] = maskKey[0] + buffer[offset - 3] = maskKey[1] + buffer[offset - 2] = maskKey[2] + buffer[offset - 1] = maskKey[3] buffer[1] = payloadLength @@ -61,8 +83,8 @@ class WebsocketFrameSend { buffer[1] |= 0x80 // MASK // mask body - for (let i = 0; i < bodyLength; i++) { - buffer[offset + i] = this.frameData[i] ^ this.maskKey[i % 4] + for (let i = 0; i < bodyLength; ++i) { + buffer[offset + i] = frameData[i] ^ maskKey[i & 3] } return buffer diff --git a/deps/undici/src/lib/web/websocket/receiver.js b/deps/undici/src/lib/web/websocket/receiver.js index 4b35ceb54a4a7e..d1af7b46349424 100644 --- a/deps/undici/src/lib/web/websocket/receiver.js +++ b/deps/undici/src/lib/web/websocket/receiver.js @@ -6,6 +6,7 @@ const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbol const { channels } = require('../../core/diagnostics') const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived, utf8Decode } = require('./util') const { WebsocketFrameSend } = require('./frame') +const { CloseEvent } = require('./events') // This code was influenced by ws released under the MIT license. // Copyright (c) 2011 Einar Otto Stangvik @@ -55,6 +56,12 @@ class ByteParser extends Writable { this.#info.fin = (buffer[0] & 0x80) !== 0 this.#info.opcode = buffer[0] & 0x0F + this.#info.masked = (buffer[1] & 0x80) === 0x80 + + if (this.#info.masked) { + failWebsocketConnection(this.ws, 'Frame cannot be masked') + return callback() + } // If we receive a fragmented message, we use the type of the first // frame to parse the full message as binary/text, when it's terminated @@ -102,6 +109,13 @@ class ByteParser extends Writable { this.#info.closeInfo = this.parseCloseBody(body) + if (this.#info.closeInfo.error) { + const { code, reason } = this.#info.closeInfo + + callback(new CloseEvent('close', { wasClean: false, reason, code })) + return + } + if (this.ws[kSentClose] !== sentCloseFrameState.SENT) { // If an endpoint receives a Close frame and did not previously send a // Close frame, the endpoint MUST send a Close frame in response. (When @@ -198,7 +212,7 @@ class ByteParser extends Writable { const buffer = this.consume(8) const upper = buffer.readUInt32BE(0) - // 2^31 is the maxinimum bytes an arraybuffer can contain + // 2^31 is the maximum bytes an arraybuffer can contain // on 32-bit systems. Although, on 64-bit systems, this is // 2^53-1 bytes. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length @@ -239,7 +253,7 @@ class ByteParser extends Writable { } } - if (this.#byteOffset === 0) { + if (this.#byteOffset === 0 && this.#info.payloadLength !== 0) { callback() break } @@ -300,6 +314,10 @@ class ByteParser extends Writable { code = data.readUInt16BE(0) } + if (code !== undefined && !isValidStatusCode(code)) { + return { code: 1002, reason: 'Invalid status code', error: true } + } + // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6 /** @type {Buffer} */ let reason = data.subarray(2) @@ -309,17 +327,13 @@ class ByteParser extends Writable { reason = reason.subarray(3) } - if (code !== undefined && !isValidStatusCode(code)) { - return null - } - try { reason = utf8Decode(reason) } catch { - return null + return { code: 1007, reason: 'Invalid UTF-8', error: true } } - return { code, reason } + return { code, reason, error: false } } get closingInfo () { diff --git a/deps/undici/src/lib/web/websocket/util.js b/deps/undici/src/lib/web/websocket/util.js index 79d9d208182506..b023e1f1f609de 100644 --- a/deps/undici/src/lib/web/websocket/util.js +++ b/deps/undici/src/lib/web/websocket/util.js @@ -104,7 +104,7 @@ function websocketMessageReceived (ws, type, data) { // -> type indicates that the data is Binary and binary type is "arraybuffer" // a new ArrayBuffer object, created in the relevant Realm of the // WebSocket object, whose contents are data - dataForEvent = new Uint8Array(data).buffer + dataForEvent = toArrayBuffer(data) } } @@ -117,6 +117,13 @@ function websocketMessageReceived (ws, type, data) { }) } +function toArrayBuffer (buffer) { + if (buffer.byteLength === buffer.buffer.byteLength) { + return buffer.buffer + } + return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) +} + /** * @see https://datatracker.ietf.org/doc/html/rfc6455 * @see https://datatracker.ietf.org/doc/html/rfc2616 @@ -197,7 +204,8 @@ function failWebsocketConnection (ws, reason) { if (reason) { // TODO: process.nextTick fireEvent('error', ws, (type, init) => new ErrorEvent(type, init), { - error: new Error(reason) + error: new Error(reason), + message: reason }) } } diff --git a/deps/undici/src/lib/web/websocket/websocket.js b/deps/undici/src/lib/web/websocket/websocket.js index 00e81cbafca7fe..97e86c3801e82a 100644 --- a/deps/undici/src/lib/web/websocket/websocket.js +++ b/deps/undici/src/lib/web/websocket/websocket.js @@ -2,8 +2,8 @@ const { webidl } = require('../fetch/webidl') const { URLSerializer } = require('../fetch/data-url') -const { getGlobalOrigin } = require('../fetch/global') -const { staticPropertyDescriptors, states, sentCloseFrameState, opcodes, emptyBuffer } = require('./constants') +const { environmentSettingsObject } = require('../fetch/util') +const { staticPropertyDescriptors, states, sentCloseFrameState, opcodes } = require('./constants') const { kWebSocketURL, kReadyState, @@ -16,21 +16,22 @@ const { const { isConnecting, isEstablished, - isClosed, isClosing, isValidSubprotocol, - failWebsocketConnection, fireEvent } = require('./util') -const { establishWebSocketConnection } = require('./connection') +const { establishWebSocketConnection, closeWebSocketConnection } = require('./connection') const { WebsocketFrameSend } = require('./frame') const { ByteParser } = require('./receiver') const { kEnumerableProperty, isBlobLike } = require('../../core/util') const { getGlobalDispatcher } = require('../../global') const { types } = require('node:util') +const { ErrorEvent } = require('./events') let experimentalWarned = false +const FastBuffer = Buffer[Symbol.species] + // https://websockets.spec.whatwg.org/#interface-definition class WebSocket extends EventTarget { #events = { @@ -67,7 +68,7 @@ class WebSocket extends EventTarget { protocols = options.protocols // 1. Let baseURL be this's relevant settings object's API base URL. - const baseURL = getGlobalOrigin() + const baseURL = environmentSettingsObject.settingsObject.baseUrl // 1. Let urlRecord be the result of applying the URL parser to url with baseURL. let urlRecord @@ -123,6 +124,7 @@ class WebSocket extends EventTarget { this[kWebSocketURL] = new URL(urlRecord.href) // 11. Let client be this's relevant settings object. + const client = environmentSettingsObject.settingsObject // 12. Run this step in parallel: @@ -131,6 +133,7 @@ class WebSocket extends EventTarget { this[kController] = establishWebSocketConnection( urlRecord, protocols, + client, this, (response) => this.#onConnectionEstablished(response), options @@ -197,67 +200,7 @@ class WebSocket extends EventTarget { } // 3. Run the first matching steps from the following list: - if (isClosing(this) || isClosed(this)) { - // If this's ready state is CLOSING (2) or CLOSED (3) - // Do nothing. - } else if (!isEstablished(this)) { - // If the WebSocket connection is not yet established - // Fail the WebSocket connection and set this's ready state - // to CLOSING (2). - failWebsocketConnection(this, 'Connection was closed before it was established.') - this[kReadyState] = WebSocket.CLOSING - } else if (this[kSentClose] === sentCloseFrameState.NOT_SENT) { - // If the WebSocket closing handshake has not yet been started - // Start the WebSocket closing handshake and set this's ready - // state to CLOSING (2). - // - If neither code nor reason is present, the WebSocket Close - // message must not have a body. - // - If code is present, then the status code to use in the - // WebSocket Close message must be the integer given by code. - // - If reason is also present, then reasonBytes must be - // provided in the Close message after the status code. - - this[kSentClose] = sentCloseFrameState.PROCESSING - - const frame = new WebsocketFrameSend() - - // If neither code nor reason is present, the WebSocket Close - // message must not have a body. - - // If code is present, then the status code to use in the - // WebSocket Close message must be the integer given by code. - if (code !== undefined && reason === undefined) { - frame.frameData = Buffer.allocUnsafe(2) - frame.frameData.writeUInt16BE(code, 0) - } else if (code !== undefined && reason !== undefined) { - // If reason is also present, then reasonBytes must be - // provided in the Close message after the status code. - frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength) - frame.frameData.writeUInt16BE(code, 0) - // the body MAY contain UTF-8-encoded data with value /reason/ - frame.frameData.write(reason, 2, 'utf-8') - } else { - frame.frameData = emptyBuffer - } - - /** @type {import('stream').Duplex} */ - const socket = this[kResponse].socket - - socket.write(frame.createFrame(opcodes.CLOSE), (err) => { - if (!err) { - this[kSentClose] = sentCloseFrameState.SENT - } - }) - - // Upon either sending or receiving a Close control frame, it is said - // that _The WebSocket Closing Handshake is Started_ and that the - // WebSocket connection is in the CLOSING state. - this[kReadyState] = states.CLOSING - } else { - // Otherwise - // Set this's ready state to CLOSING (2). - this[kReadyState] = WebSocket.CLOSING - } + closeWebSocketConnection(this, code, reason, reasonByteLength) } /** @@ -323,7 +266,7 @@ class WebSocket extends EventTarget { // increase the bufferedAmount attribute by the length of the // ArrayBuffer in bytes. - const value = Buffer.from(data) + const value = new FastBuffer(data) const frame = new WebsocketFrameSend(value) const buffer = frame.createFrame(opcodes.BINARY) @@ -344,7 +287,7 @@ class WebSocket extends EventTarget { // not throw an exception must increase the bufferedAmount attribute // by the length of data’s buffer in bytes. - const ab = Buffer.from(data, data.byteOffset, data.byteLength) + const ab = new FastBuffer(data, data.byteOffset, data.byteLength) const frame = new WebsocketFrameSend(ab) const buffer = frame.createFrame(opcodes.BINARY) @@ -368,7 +311,7 @@ class WebSocket extends EventTarget { const frame = new WebsocketFrameSend() data.arrayBuffer().then((ab) => { - const value = Buffer.from(ab) + const value = new FastBuffer(ab) frame.frameData = value const buffer = frame.createFrame(opcodes.BINARY) @@ -521,9 +464,8 @@ class WebSocket extends EventTarget { this[kResponse] = response const parser = new ByteParser(this) - parser.on('drain', function onParserDrain () { - this.ws[kResponse].socket.resume() - }) + parser.on('drain', onParserDrain) + parser.on('error', onParserError.bind(this)) response.socket.ws = this this[kByteParser] = parser @@ -607,7 +549,7 @@ webidl.converters['DOMString or sequence'] = function (V, prefix, arg return webidl.converters.DOMString(V, prefix, argument) } -// This implements the propsal made in https://github.com/whatwg/websockets/issues/42 +// This implements the proposal made in https://github.com/whatwg/websockets/issues/42 webidl.converters.WebSocketInit = webidl.dictionaryConverter([ { key: 'protocols', @@ -647,6 +589,16 @@ webidl.converters.WebSocketSendData = function (V) { return webidl.converters.USVString(V) } +function onParserDrain () { + this.ws[kResponse].socket.resume() +} + +function onParserError (err) { + fireEvent('error', this, () => new ErrorEvent('error', { error: err, message: err.reason })) + + closeWebSocketConnection(this, err.code) +} + module.exports = { WebSocket } diff --git a/deps/undici/src/package-lock.json b/deps/undici/src/package-lock.json index acd8dd06ea80c5..e70e63f660f9eb 100644 --- a/deps/undici/src/package-lock.json +++ b/deps/undici/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "undici", - "version": "6.15.0", + "version": "6.16.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "undici", - "version": "6.15.0", + "version": "6.16.1", "license": "MIT", "devDependencies": { "@fastify/busboy": "2.1.1", @@ -14,7 +14,7 @@ "@sinonjs/fake-timers": "^11.1.0", "@types/node": "^18.0.3", "abort-controller": "^3.0.0", - "borp": "^0.12.0", + "borp": "^0.13.0", "c8": "^9.1.0", "cross-env": "^7.0.3", "dns-packet": "^5.4.0", @@ -1541,9 +1541,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.19.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", - "integrity": "sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==", + "version": "18.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.33.tgz", + "integrity": "sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -2138,9 +2138,9 @@ "dev": true }, "node_modules/borp": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/borp/-/borp-0.12.0.tgz", - "integrity": "sha512-42kZobvDzqbUFoe1jhwVWRFYNEM/7ua+7tvyDyHxWcxtZp4lppWpizlLuaSAzgci8w4DJRmWdQuhrjXs/sSr5A==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/borp/-/borp-0.13.0.tgz", + "integrity": "sha512-cee/VLiTvqRdqaUkfJfOqw9e6FFHZD4gwoyHaz9dxFE5KqDJPQGlzCSPyBiDKFqf37pTyZYr7TfO2eVCSI0AQg==", "dev": true, "dependencies": { "@reporters/github": "^1.5.4", @@ -2230,26 +2230,11 @@ "semver": "^7.0.0" } }, - "node_modules/builtins/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/builtins/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -2257,12 +2242,6 @@ "node": ">=10" } }, - "node_modules/builtins/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/c8": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", @@ -2398,9 +2377,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001614", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001614.tgz", - "integrity": "sha512-jmZQ1VpmlRwHgdP1/uiKzgiAuGOfLEJsYFP4+GBou/QQ4U6IOJCB4NP1c+1p9RGLpwObcT94jA5/uO+F1vBbog==", + "version": "1.0.30001617", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz", + "integrity": "sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==", "dev": true, "funding": [ { @@ -2969,9 +2948,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.751", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.751.tgz", - "integrity": "sha512-2DEPi++qa89SMGRhufWTiLmzqyuGmNF3SK4+PQetW1JKiZdEpF4XQonJXJCzyuYSA6mauiMhbyVhqYAP45Hvfw==", + "version": "1.4.763", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.763.tgz", + "integrity": "sha512-k4J8NrtJ9QrvHLRo8Q18OncqBCB7tIUyqxRcJnlonQ0ioHKYB988GcDFF3ZePmnb8eHEopDs/wPHR/iGAFgoUQ==", "dev": true }, "node_modules/emittery": { @@ -3558,18 +3537,6 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint-plugin-n/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-plugin-n/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3583,13 +3550,10 @@ } }, "node_modules/eslint-plugin-n/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -3597,12 +3561,6 @@ "node": ">=10" } }, - "node_modules/eslint-plugin-n/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/eslint-plugin-promise": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", @@ -4382,16 +4340,16 @@ } }, "node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "version": "10.3.14", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.14.tgz", + "integrity": "sha512-4fkAqu93xe9Mk7le9v0y3VrPDqLKHarNi2s4Pv7f2yOvfhWfhc7hRPHC/JyqMqb8B/Dt/eGS4n7ykwf3fOsl8g==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.6", "minimatch": "^9.0.1", "minipass": "^7.0.4", - "path-scurry": "^1.10.2" + "path-scurry": "^1.11.0" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -5250,26 +5208,11 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-instrument/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -5277,12 +5220,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-instrument/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -6006,26 +5943,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -6033,12 +5955,6 @@ "node": ">=10" } }, - "node_modules/jest-snapshot/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -6437,26 +6353,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-dir/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -6464,12 +6365,6 @@ "node": ">=10" } }, - "node_modules/make-dir/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -6647,9 +6542,9 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", + "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" @@ -6712,26 +6607,11 @@ "node": ">=10" } }, - "node_modules/normalize-package-data/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -6739,12 +6619,6 @@ "node": ">=10" } }, - "node_modules/normalize-package-data/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7104,9 +6978,9 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.0.tgz", + "integrity": "sha512-LNHTaVkzaYaLGlO+0u3rQTz7QrHTFOuKyba9JMTQutkmtNew8dw8wOD7mTU/5fCPZzCWpfW0XnQKzY61P0aTaw==", "dev": true, "dependencies": { "lru-cache": "^10.2.0", @@ -9051,9 +8925,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", + "integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==", "dev": true, "funding": [ { @@ -9070,7 +8944,7 @@ } ], "dependencies": { - "escalade": "^3.1.1", + "escalade": "^3.1.2", "picocolors": "^1.0.0" }, "bin": { diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json index 35ae9afdbfc129..077aa7402528fc 100644 --- a/deps/undici/src/package.json +++ b/deps/undici/src/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "6.15.0", + "version": "6.16.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { @@ -77,7 +77,7 @@ "test:eventsource:nobuild": "borp --expose-gc -p \"test/eventsource/*.js\"", "test:fuzzing": "node test/fuzzing/fuzzing.test.js", "test:fetch": "npm run build:node && npm run test:fetch:nobuild", - "test:fetch:nobuild": "borp --expose-gc -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy", + "test:fetch:nobuild": "borp --timeout 180000 --expose-gc --concurrency 1 -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy", "test:interceptors": "borp -p \"test/interceptors/*.js\"", "test:jest": "cross-env NODE_V8_COVERAGE= jest", "test:unit": "borp --expose-gc -p \"test/*.js\"", @@ -105,7 +105,7 @@ "@sinonjs/fake-timers": "^11.1.0", "@types/node": "^18.0.3", "abort-controller": "^3.0.0", - "borp": "^0.12.0", + "borp": "^0.13.0", "c8": "^9.1.0", "cross-env": "^7.0.3", "dns-packet": "^5.4.0", diff --git a/deps/undici/src/types/env-http-proxy-agent.d.ts b/deps/undici/src/types/env-http-proxy-agent.d.ts new file mode 100644 index 00000000000000..d6509dc673b4d8 --- /dev/null +++ b/deps/undici/src/types/env-http-proxy-agent.d.ts @@ -0,0 +1,21 @@ +import Agent from './agent' +import Dispatcher from './dispatcher' + +export default EnvHttpProxyAgent + +declare class EnvHttpProxyAgent extends Dispatcher { + constructor(opts?: EnvHttpProxyAgent.Options) + + dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean; +} + +declare namespace EnvHttpProxyAgent { + export interface Options extends Agent.Options { + /** Overrides the value of the HTTP_PROXY environment variable */ + httpProxy?: string; + /** Overrides the value of the HTTPS_PROXY environment variable */ + httpsProxy?: string; + /** Overrides the value of the NO_PROXY environment variable */ + noProxy?: string; + } +} diff --git a/deps/undici/src/types/fetch.d.ts b/deps/undici/src/types/fetch.d.ts index 18eadb6ac984cc..7e94375ecdc2a3 100644 --- a/deps/undici/src/types/fetch.d.ts +++ b/deps/undici/src/types/fetch.d.ts @@ -85,7 +85,7 @@ export declare class Headers implements SpecIterable<[string, string]> { readonly keys: () => SpecIterableIterator readonly values: () => SpecIterableIterator readonly entries: () => SpecIterableIterator<[string, string]> - readonly [Symbol.iterator]: () => SpecIterator<[string, string]> + readonly [Symbol.iterator]: () => SpecIterableIterator<[string, string]> } export type RequestCache = @@ -163,6 +163,7 @@ export declare class Request extends BodyMixin { readonly method: string readonly mode: RequestMode readonly redirect: RequestRedirect + readonly referrer: string readonly referrerPolicy: ReferrerPolicy readonly url: string diff --git a/deps/undici/src/types/mock-interceptor.d.ts b/deps/undici/src/types/mock-interceptor.d.ts index 6b3961c0482927..33f3f14d1bfa3e 100644 --- a/deps/undici/src/types/mock-interceptor.d.ts +++ b/deps/undici/src/types/mock-interceptor.d.ts @@ -71,11 +71,11 @@ declare namespace MockInterceptor { export interface MockResponseCallbackOptions { path: string; - origin: string; method: string; - body?: BodyInit | Dispatcher.DispatchOptions['body']; - headers: Headers | Record; - maxRedirections: number; + headers?: Headers | Record; + origin?: string; + body?: BodyInit | Dispatcher.DispatchOptions['body'] | null; + maxRedirections?: number; } export type MockResponseDataHandler = ( diff --git a/deps/undici/src/types/util.d.ts b/deps/undici/src/types/util.d.ts index 2a604148fd813f..77cf1473a24f1f 100644 --- a/deps/undici/src/types/util.d.ts +++ b/deps/undici/src/types/util.d.ts @@ -8,24 +8,11 @@ export namespace util { /** * Receives a header object and returns the parsed value. * @param headers Header object + * @param obj Object to specify a proxy object. Used to assign parsed values. + * @returns If `obj` is specified, it is equivalent to `obj`. */ export function parseHeaders( - headers: - | Record - | (Buffer | string | (Buffer | string)[])[] - ): Record; - /** - * Receives a header object and returns the parsed value. - * @param headers Header object - * @param obj Object to specify a proxy object. Used to assign parsed values. But, if `headers` is an object, it is not used. - * @returns If `headers` is an object, it is `headers`. Otherwise, if `obj` is specified, it is equivalent to `obj`. - */ - export function parseHeaders< - H extends - | Record - | (Buffer | string | (Buffer | string)[])[] - >( - headers: H, - obj?: H extends any[] ? Record : never + headers: (Buffer | string | (Buffer | string)[])[], + obj?: Record ): Record; } diff --git a/deps/undici/undici.js b/deps/undici/undici.js index fd1acef9d95222..1bb339f6728f75 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -2940,10 +2940,10 @@ var require_data_url = __commonJS({ "use strict"; var assert = require("node:assert"); var encoder = new TextEncoder(); - var HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/; + var HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+\-.^_|~A-Za-z0-9]+$/; var HTTP_WHITESPACE_REGEX = /[\u000A\u000D\u0009\u0020]/; var ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g; - var HTTP_QUOTED_STRING_TOKENS = /[\u0009\u0020-\u007E\u0080-\u00FF]/; + var HTTP_QUOTED_STRING_TOKENS = /^[\u0009\u0020-\u007E\u0080-\u00FF]+$/; function dataURLProcessor(dataURL) { assert(dataURL.protocol === "data:"); let input = URLSerializer(dataURL, true); @@ -3471,6 +3471,7 @@ var require_webidl = __commonJS({ } const method = typeof Iterable === "function" ? Iterable() : V?.[Symbol.iterator]?.(); const seq = []; + let index = 0; if (method === void 0 || typeof method.next !== "function") { throw webidl.errors.exception({ header: prefix, @@ -3482,7 +3483,7 @@ var require_webidl = __commonJS({ if (done) { break; } - seq.push(converter(value, prefix, argument)); + seq.push(converter(value, prefix, `${argument}[${index++}]`)); } return seq; }; @@ -3852,10 +3853,11 @@ var require_util2 = __commonJS({ __name(appendFetchMetadata, "appendFetchMetadata"); function appendRequestOriginHeader(request) { let serializedOrigin = request.origin; + if (serializedOrigin === "client") { + return; + } if (request.responseTainting === "cors" || request.mode === "websocket") { - if (serializedOrigin) { - request.headersList.append("origin", serializedOrigin, true); - } + request.headersList.append("origin", serializedOrigin, true); } else if (request.method !== "GET" && request.method !== "HEAD") { switch (request.referrerPolicy) { case "no-referrer": @@ -3875,9 +3877,7 @@ var require_util2 = __commonJS({ break; default: } - if (serializedOrigin) { - request.headersList.append("origin", serializedOrigin, true); - } + request.headersList.append("origin", serializedOrigin, true); } } __name(appendRequestOriginHeader, "appendRequestOriginHeader"); @@ -6854,6 +6854,7 @@ var require_client_h2 = __commonJS({ } } else if (util.isStream(body)) { writeStream({ + abort, body, client, request, @@ -6865,6 +6866,7 @@ var require_client_h2 = __commonJS({ }); } else if (util.isIterable(body)) { writeIterable({ + abort, body, client, request, @@ -8397,13 +8399,16 @@ var require_headers = __commonJS({ } get entries() { const headers = {}; - if (this[kHeadersMap].size) { + if (this[kHeadersMap].size !== 0) { for (const { name, value } of this[kHeadersMap].values()) { headers[name] = value; } } return headers; } + rawValues() { + return this[kHeadersMap].values(); + } get entriesList() { const headers = []; if (this[kHeadersMap].size !== 0) { @@ -8680,7 +8685,20 @@ var require_response = __commonJS({ var { kHeadersList, kConstruct } = require_symbols(); var assert = require("node:assert"); var { types } = require("node:util"); + var { isDisturbed, isErrored } = require("node:stream"); var textEncoder = new TextEncoder("utf-8"); + var hasFinalizationRegistry = globalThis.FinalizationRegistry && process.version.indexOf("v18") !== 0; + var registry; + if (hasFinalizationRegistry) { + registry = new FinalizationRegistry((stream) => { + if (!stream.locked && !isDisturbed(stream) && !isErrored(stream)) { + stream.cancel("Response object has been garbage collected").catch(noop); + } + }); + } + function noop() { + } + __name(noop, "noop"); var Response = class _Response { static { __name(this, "Response"); @@ -8986,6 +9004,9 @@ var require_response = __commonJS({ response[kHeaders] = new Headers(kConstruct); response[kHeaders][kHeadersList] = innerResponse.headersList; response[kHeaders][kGuard] = guard; + if (hasFinalizationRegistry && innerResponse.body?.stream) { + registry.register(response, innerResponse.body.stream); + } return response; } __name(fromInnerResponse, "fromInnerResponse"); @@ -9396,8 +9417,8 @@ var require_request2 = __commonJS({ const headers = init.headers !== void 0 ? init.headers : new HeadersList(headersList); headersList.clear(); if (headers instanceof HeadersList) { - for (const { 0: key, 1: val } of headers) { - headersList.append(key, val, true); + for (const { name, value } of headers.rawValues()) { + headersList.append(name, value, false); } headersList.cookies = headers.cookies; } else { @@ -9617,47 +9638,46 @@ var require_request2 = __commonJS({ }; mixinBody(Request); function makeRequest(init) { - const request = { - method: "GET", - localURLsOnly: false, - unsafeRequest: false, - body: null, - client: null, - reservedClient: null, - replacesClientId: "", - window: "client", - keepalive: false, - serviceWorkers: "all", - initiator: "", - destination: "", - priority: null, - origin: "client", - policyContainer: "client", - referrer: "client", - referrerPolicy: "", - mode: "no-cors", - useCORSPreflightFlag: false, - credentials: "same-origin", - useCredentials: false, - cache: "default", - redirect: "follow", - integrity: "", - cryptoGraphicsNonceMetadata: "", - parserMetadata: "", - reloadNavigation: false, - historyNavigation: false, - userActivation: false, - taintedOrigin: false, - redirectCount: 0, - responseTainting: "basic", - preventNoCacheCacheControlHeaderModification: false, - done: false, - timingAllowFailed: false, - ...init, + return { + method: init.method ?? "GET", + localURLsOnly: init.localURLsOnly ?? false, + unsafeRequest: init.unsafeRequest ?? false, + body: init.body ?? null, + client: init.client ?? null, + reservedClient: init.reservedClient ?? null, + replacesClientId: init.replacesClientId ?? "", + window: init.window ?? "client", + keepalive: init.keepalive ?? false, + serviceWorkers: init.serviceWorkers ?? "all", + initiator: init.initiator ?? "", + destination: init.destination ?? "", + priority: init.priority ?? null, + origin: init.origin ?? "client", + policyContainer: init.policyContainer ?? "client", + referrer: init.referrer ?? "client", + referrerPolicy: init.referrerPolicy ?? "", + mode: init.mode ?? "no-cors", + useCORSPreflightFlag: init.useCORSPreflightFlag ?? false, + credentials: init.credentials ?? "same-origin", + useCredentials: init.useCredentials ?? false, + cache: init.cache ?? "default", + redirect: init.redirect ?? "follow", + integrity: init.integrity ?? "", + cryptoGraphicsNonceMetadata: init.cryptoGraphicsNonceMetadata ?? "", + parserMetadata: init.parserMetadata ?? "", + reloadNavigation: init.reloadNavigation ?? false, + historyNavigation: init.historyNavigation ?? false, + userActivation: init.userActivation ?? false, + taintedOrigin: init.taintedOrigin ?? false, + redirectCount: init.redirectCount ?? 0, + responseTainting: init.responseTainting ?? "basic", + preventNoCacheCacheControlHeaderModification: init.preventNoCacheCacheControlHeaderModification ?? false, + done: init.done ?? false, + timingAllowFailed: init.timingAllowFailed ?? false, + urlList: init.urlList, + url: init.urlList[0], headersList: init.headersList ? new HeadersList(init.headersList) : new HeadersList() }; - request.url = request.urlList[0]; - return request; } __name(makeRequest, "makeRequest"); function cloneRequest(request) { @@ -9908,9 +9928,13 @@ var require_fetch = __commonJS({ this.emit("terminated", error); } }; + function handleFetchDone(response) { + finalizeAndReportTiming(response, "fetch"); + } + __name(handleFetchDone, "handleFetchDone"); function fetch2(input, init = void 0) { webidl.argumentLengthCheck(arguments, 1, "globalThis.fetch"); - const p = createDeferredPromise(); + let p = createDeferredPromise(); let requestObject; try { requestObject = new Request(input, init); @@ -9936,10 +9960,10 @@ var require_fetch = __commonJS({ locallyAborted = true; assert(controller != null); controller.abort(requestObject.signal.reason); - abortFetch(p, request, responseObject, requestObject.signal.reason); + const realResponse = responseObject?.deref(); + abortFetch(p, request, realResponse, requestObject.signal.reason); } ); - const handleFetchDone = /* @__PURE__ */ __name((response) => finalizeAndReportTiming(response, "fetch"), "handleFetchDone"); const processResponse = /* @__PURE__ */ __name((response) => { if (locallyAborted) { return; @@ -9952,8 +9976,9 @@ var require_fetch = __commonJS({ p.reject(new TypeError("fetch failed", { cause: response.error })); return; } - responseObject = fromInnerResponse(response, "immutable"); - p.resolve(responseObject); + responseObject = new WeakRef(fromInnerResponse(response, "immutable")); + p.resolve(responseObject.deref()); + p = null; }, "processResponse"); controller = fetching({ request, @@ -10000,7 +10025,9 @@ var require_fetch = __commonJS({ __name(finalizeAndReportTiming, "finalizeAndReportTiming"); var markResourceTiming = performance.markResourceTiming; function abortFetch(p, request, responseObject, error) { - p.reject(error); + if (p) { + p.reject(error); + } if (request.body != null && isReadable(request.body?.stream)) { request.body.stream.cancel(error).catch((err) => { if (err.code === "ERR_INVALID_STATE") { @@ -10062,7 +10089,7 @@ var require_fetch = __commonJS({ request.window = request.client?.globalObject?.constructor?.name === "Window" ? request.client : "no-window"; } if (request.origin === "client") { - request.origin = request.client?.origin; + request.origin = request.client.origin; } if (request.policyContainer === "client") { if (request.client != null) { @@ -10329,7 +10356,10 @@ var require_fetch = __commonJS({ queueMicrotask(() => processResponseEndOfBodyTask()); }, "processResponseEndOfBody"); if (fetchParams.processResponse != null) { - queueMicrotask(() => fetchParams.processResponse(response)); + queueMicrotask(() => { + fetchParams.processResponse(response); + fetchParams.processResponse = null; + }); } const internalResponse = response.type === "error" ? response : response.internalResponse ?? response; if (internalResponse.body == null) { @@ -10652,7 +10682,9 @@ var require_fetch = __commonJS({ await fetchParams.controller.resume(); }, "pullAlgorithm"); const cancelAlgorithm = /* @__PURE__ */ __name((reason) => { - fetchParams.controller.abort(reason); + if (!isCancelled(fetchParams)) { + fetchParams.controller.abort(reason); + } }, "cancelAlgorithm"); const stream = new ReadableStream( { @@ -10768,20 +10800,18 @@ var require_fetch = __commonJS({ let codings = []; let location = ""; const headersList = new HeadersList(); - if (Array.isArray(rawHeaders)) { - for (let i = 0; i < rawHeaders.length; i += 2) { - headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true); - } - const contentEncoding = headersList.get("content-encoding", true); - if (contentEncoding) { - codings = contentEncoding.toLowerCase().split(",").map((x) => x.trim()); - } - location = headersList.get("location", true); + for (let i = 0; i < rawHeaders.length; i += 2) { + headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true); + } + const contentEncoding = headersList.get("content-encoding", true); + if (contentEncoding) { + codings = contentEncoding.toLowerCase().split(",").map((x) => x.trim()); } + location = headersList.get("location", true); this.body = new Readable({ read: resume }); const decoders = []; const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status); - if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) { + if (codings.length !== 0 && request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) { for (let i = 0; i < codings.length; ++i) { const coding = codings[i]; if (coding === "x-gzip" || coding === "gzip") { @@ -11253,7 +11283,7 @@ var require_util3 = __commonJS({ if (ws[kBinaryType] === "blob") { dataForEvent = new Blob([data]); } else { - dataForEvent = new Uint8Array(data).buffer; + dataForEvent = toArrayBuffer(data); } } fireEvent("message", ws, createFastMessageEvent2, { @@ -11262,6 +11292,13 @@ var require_util3 = __commonJS({ }); } __name(websocketMessageReceived, "websocketMessageReceived"); + function toArrayBuffer(buffer) { + if (buffer.byteLength === buffer.buffer.byteLength) { + return buffer.buffer; + } + return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); + } + __name(toArrayBuffer, "toArrayBuffer"); function isValidSubprotocol(protocol) { if (protocol.length === 0) { return false; @@ -11309,7 +11346,8 @@ var require_util3 = __commonJS({ } if (reason) { fireEvent("error", ws, (type, init) => new ErrorEvent2(type, init), { - error: new Error(reason) + error: new Error(reason), + message: reason }); } } @@ -11337,18 +11375,100 @@ var require_util3 = __commonJS({ } }); +// lib/web/websocket/frame.js +var require_frame = __commonJS({ + "lib/web/websocket/frame.js"(exports2, module2) { + "use strict"; + var { maxUnsigned16Bit } = require_constants4(); + var BUFFER_SIZE = 16386; + var crypto; + var buffer = null; + var bufIdx = BUFFER_SIZE; + try { + crypto = require("node:crypto"); + } catch { + crypto = { + // not full compatibility, but minimum. + randomFillSync: /* @__PURE__ */ __name(function randomFillSync(buffer2, _offset, _size) { + for (let i = 0; i < buffer2.length; ++i) { + buffer2[i] = Math.random() * 255 | 0; + } + return buffer2; + }, "randomFillSync") + }; + } + function generateMask() { + if (bufIdx === BUFFER_SIZE) { + bufIdx = 0; + crypto.randomFillSync(buffer ??= Buffer.allocUnsafe(BUFFER_SIZE), 0, BUFFER_SIZE); + } + return [buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++]]; + } + __name(generateMask, "generateMask"); + var WebsocketFrameSend = class { + static { + __name(this, "WebsocketFrameSend"); + } + /** + * @param {Buffer|undefined} data + */ + constructor(data) { + this.frameData = data; + } + createFrame(opcode) { + const frameData = this.frameData; + const maskKey = generateMask(); + const bodyLength = frameData?.byteLength ?? 0; + let payloadLength = bodyLength; + let offset = 6; + if (bodyLength > maxUnsigned16Bit) { + offset += 8; + payloadLength = 127; + } else if (bodyLength > 125) { + offset += 2; + payloadLength = 126; + } + const buffer2 = Buffer.allocUnsafe(bodyLength + offset); + buffer2[0] = buffer2[1] = 0; + buffer2[0] |= 128; + buffer2[0] = (buffer2[0] & 240) + opcode; + buffer2[offset - 4] = maskKey[0]; + buffer2[offset - 3] = maskKey[1]; + buffer2[offset - 2] = maskKey[2]; + buffer2[offset - 1] = maskKey[3]; + buffer2[1] = payloadLength; + if (payloadLength === 126) { + buffer2.writeUInt16BE(bodyLength, 2); + } else if (payloadLength === 127) { + buffer2[2] = buffer2[3] = 0; + buffer2.writeUIntBE(bodyLength, 4, 6); + } + buffer2[1] |= 128; + for (let i = 0; i < bodyLength; ++i) { + buffer2[offset + i] = frameData[i] ^ maskKey[i & 3]; + } + return buffer2; + } + }; + module2.exports = { + WebsocketFrameSend + }; + } +}); + // lib/web/websocket/connection.js var require_connection = __commonJS({ "lib/web/websocket/connection.js"(exports2, module2) { "use strict"; - var { uid, states, sentCloseFrameState } = require_constants4(); + var { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require_constants4(); var { kReadyState, kSentClose, kByteParser, - kReceivedClose + kReceivedClose, + kResponse } = require_symbols3(); - var { fireEvent, failWebsocketConnection } = require_util3(); + var { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished } = require_util3(); var { channels } = require_diagnostics(); var { CloseEvent: CloseEvent2 } = require_events(); var { makeRequest } = require_request2(); @@ -11356,16 +11476,18 @@ var require_connection = __commonJS({ var { Headers } = require_headers(); var { getDecodeSplit } = require_util2(); var { kHeadersList } = require_symbols(); + var { WebsocketFrameSend } = require_frame(); var crypto; try { crypto = require("node:crypto"); } catch { } - function establishWebSocketConnection(url, protocols, ws, onEstablish, options) { + function establishWebSocketConnection(url, protocols, client, ws, onEstablish, options) { const requestURL = url; requestURL.protocol = url.protocol === "ws:" ? "http:" : "https:"; const request = makeRequest({ urlList: [requestURL], + client, serviceWorkers: "none", referrer: "no-referrer", mode: "websocket", @@ -11440,6 +11562,37 @@ var require_connection = __commonJS({ return controller; } __name(establishWebSocketConnection, "establishWebSocketConnection"); + function closeWebSocketConnection(ws, code, reason, reasonByteLength) { + if (isClosing(ws) || isClosed(ws)) { + } else if (!isEstablished(ws)) { + failWebsocketConnection(ws, "Connection was closed before it was established."); + ws[kReadyState] = states.CLOSING; + } else if (ws[kSentClose] === sentCloseFrameState.NOT_SENT) { + ws[kSentClose] = sentCloseFrameState.PROCESSING; + const frame = new WebsocketFrameSend(); + if (code !== void 0 && reason === void 0) { + frame.frameData = Buffer.allocUnsafe(2); + frame.frameData.writeUInt16BE(code, 0); + } else if (code !== void 0 && reason !== void 0) { + frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength); + frame.frameData.writeUInt16BE(code, 0); + frame.frameData.write(reason, 2, "utf-8"); + } else { + frame.frameData = emptyBuffer; + } + const socket = ws[kResponse].socket; + socket.write(frame.createFrame(opcodes.CLOSE), (err) => { + if (!err) { + ws[kSentClose] = sentCloseFrameState.SENT; + } + }); + ws[kSentClose] = sentCloseFrameState.PROCESSING; + ws[kReadyState] = states.CLOSING; + } else { + ws[kReadyState] = states.CLOSING; + } + } + __name(closeWebSocketConnection, "closeWebSocketConnection"); function onSocketData(chunk) { if (!this.ws[kByteParser].write(chunk)) { this.pause(); @@ -11452,10 +11605,10 @@ var require_connection = __commonJS({ let code = 1005; let reason = ""; const result = ws[kByteParser].closingInfo; - if (result) { + if (result && !result.error) { code = result.code ?? 1005; reason = result.reason; - } else if (ws[kSentClose] !== sentCloseFrameState.SENT) { + } else if (!ws[kReceivedClose]) { code = 1006; } ws[kReadyState] = states.CLOSED; @@ -11483,67 +11636,8 @@ var require_connection = __commonJS({ } __name(onSocketError, "onSocketError"); module2.exports = { - establishWebSocketConnection - }; - } -}); - -// lib/web/websocket/frame.js -var require_frame = __commonJS({ - "lib/web/websocket/frame.js"(exports2, module2) { - "use strict"; - var { maxUnsigned16Bit } = require_constants4(); - var crypto; - try { - crypto = require("node:crypto"); - } catch { - } - var WebsocketFrameSend = class { - static { - __name(this, "WebsocketFrameSend"); - } - /** - * @param {Buffer|undefined} data - */ - constructor(data) { - this.frameData = data; - this.maskKey = crypto.randomBytes(4); - } - createFrame(opcode) { - const bodyLength = this.frameData?.byteLength ?? 0; - let payloadLength = bodyLength; - let offset = 6; - if (bodyLength > maxUnsigned16Bit) { - offset += 8; - payloadLength = 127; - } else if (bodyLength > 125) { - offset += 2; - payloadLength = 126; - } - const buffer = Buffer.allocUnsafe(bodyLength + offset); - buffer[0] = buffer[1] = 0; - buffer[0] |= 128; - buffer[0] = (buffer[0] & 240) + opcode; - buffer[offset - 4] = this.maskKey[0]; - buffer[offset - 3] = this.maskKey[1]; - buffer[offset - 2] = this.maskKey[2]; - buffer[offset - 1] = this.maskKey[3]; - buffer[1] = payloadLength; - if (payloadLength === 126) { - buffer.writeUInt16BE(bodyLength, 2); - } else if (payloadLength === 127) { - buffer[2] = buffer[3] = 0; - buffer.writeUIntBE(bodyLength, 4, 6); - } - buffer[1] |= 128; - for (let i = 0; i < bodyLength; i++) { - buffer[offset + i] = this.frameData[i] ^ this.maskKey[i % 4]; - } - return buffer; - } - }; - module2.exports = { - WebsocketFrameSend + establishWebSocketConnection, + closeWebSocketConnection }; } }); @@ -11558,6 +11652,7 @@ var require_receiver = __commonJS({ var { channels } = require_diagnostics(); var { isValidStatusCode, failWebsocketConnection, websocketMessageReceived, utf8Decode } = require_util3(); var { WebsocketFrameSend } = require_frame(); + var { CloseEvent: CloseEvent2 } = require_events(); var ByteParser = class extends Writable { static { __name(this, "ByteParser"); @@ -11594,6 +11689,11 @@ var require_receiver = __commonJS({ const buffer = this.consume(2); this.#info.fin = (buffer[0] & 128) !== 0; this.#info.opcode = buffer[0] & 15; + this.#info.masked = (buffer[1] & 128) === 128; + if (this.#info.masked) { + failWebsocketConnection(this.ws, "Frame cannot be masked"); + return callback(); + } this.#info.originalOpcode ??= this.#info.opcode; this.#info.fragmented = !this.#info.fin && this.#info.opcode !== opcodes.CONTINUATION; if (this.#info.fragmented && this.#info.opcode !== opcodes.BINARY && this.#info.opcode !== opcodes.TEXT) { @@ -11622,6 +11722,11 @@ var require_receiver = __commonJS({ } const body = this.consume(payloadLength); this.#info.closeInfo = this.parseCloseBody(body); + if (this.#info.closeInfo.error) { + const { code, reason } = this.#info.closeInfo; + callback(new CloseEvent2("close", { wasClean: false, reason, code })); + return; + } if (this.ws[kSentClose] !== sentCloseFrameState.SENT) { let body2 = emptyBuffer; if (this.#info.closeInfo.code) { @@ -11709,7 +11814,7 @@ var require_receiver = __commonJS({ this.#state = parserStates.INFO; } } - if (this.#byteOffset === 0) { + if (this.#byteOffset === 0 && this.#info.payloadLength !== 0) { callback(); break; } @@ -11755,19 +11860,19 @@ var require_receiver = __commonJS({ if (data.length >= 2) { code = data.readUInt16BE(0); } + if (code !== void 0 && !isValidStatusCode(code)) { + return { code: 1002, reason: "Invalid status code", error: true }; + } let reason = data.subarray(2); if (reason[0] === 239 && reason[1] === 187 && reason[2] === 191) { reason = reason.subarray(3); } - if (code !== void 0 && !isValidStatusCode(code)) { - return null; - } try { reason = utf8Decode(reason); } catch { - return null; + return { code: 1007, reason: "Invalid UTF-8", error: true }; } - return { code, reason }; + return { code, reason, error: false }; } get closingInfo() { return this.#info.closeInfo; @@ -11785,8 +11890,8 @@ var require_websocket = __commonJS({ "use strict"; var { webidl } = require_webidl(); var { URLSerializer } = require_data_url(); - var { getGlobalOrigin } = require_global(); - var { staticPropertyDescriptors, states, sentCloseFrameState, opcodes, emptyBuffer } = require_constants4(); + var { environmentSettingsObject } = require_util2(); + var { staticPropertyDescriptors, states, sentCloseFrameState, opcodes } = require_constants4(); var { kWebSocketURL, kReadyState, @@ -11799,19 +11904,19 @@ var require_websocket = __commonJS({ var { isConnecting, isEstablished, - isClosed, isClosing, isValidSubprotocol, - failWebsocketConnection, fireEvent } = require_util3(); - var { establishWebSocketConnection } = require_connection(); + var { establishWebSocketConnection, closeWebSocketConnection } = require_connection(); var { WebsocketFrameSend } = require_frame(); var { ByteParser } = require_receiver(); var { kEnumerableProperty, isBlobLike } = require_util(); var { getGlobalDispatcher: getGlobalDispatcher2 } = require_global2(); var { types } = require("node:util"); + var { ErrorEvent: ErrorEvent2 } = require_events(); var experimentalWarned = false; + var FastBuffer = Buffer[Symbol.species]; var WebSocket = class _WebSocket extends EventTarget { static { __name(this, "WebSocket"); @@ -11842,7 +11947,7 @@ var require_websocket = __commonJS({ const options = webidl.converters["DOMString or sequence or WebSocketInit"](protocols, prefix, "options"); url = webidl.converters.USVString(url, prefix, "url"); protocols = options.protocols; - const baseURL = getGlobalOrigin(); + const baseURL = environmentSettingsObject.settingsObject.baseUrl; let urlRecord; try { urlRecord = new URL(url, baseURL); @@ -11873,9 +11978,11 @@ var require_websocket = __commonJS({ throw new DOMException("Invalid Sec-WebSocket-Protocol value", "SyntaxError"); } this[kWebSocketURL] = new URL(urlRecord.href); + const client = environmentSettingsObject.settingsObject; this[kController] = establishWebSocketConnection( urlRecord, protocols, + client, this, (response) => this.#onConnectionEstablished(response), options @@ -11913,33 +12020,7 @@ var require_websocket = __commonJS({ ); } } - if (isClosing(this) || isClosed(this)) { - } else if (!isEstablished(this)) { - failWebsocketConnection(this, "Connection was closed before it was established."); - this[kReadyState] = _WebSocket.CLOSING; - } else if (this[kSentClose] === sentCloseFrameState.NOT_SENT) { - this[kSentClose] = sentCloseFrameState.PROCESSING; - const frame = new WebsocketFrameSend(); - if (code !== void 0 && reason === void 0) { - frame.frameData = Buffer.allocUnsafe(2); - frame.frameData.writeUInt16BE(code, 0); - } else if (code !== void 0 && reason !== void 0) { - frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength); - frame.frameData.writeUInt16BE(code, 0); - frame.frameData.write(reason, 2, "utf-8"); - } else { - frame.frameData = emptyBuffer; - } - const socket = this[kResponse].socket; - socket.write(frame.createFrame(opcodes.CLOSE), (err) => { - if (!err) { - this[kSentClose] = sentCloseFrameState.SENT; - } - }); - this[kReadyState] = states.CLOSING; - } else { - this[kReadyState] = _WebSocket.CLOSING; - } + closeWebSocketConnection(this, code, reason, reasonByteLength); } /** * @see https://websockets.spec.whatwg.org/#dom-websocket-send @@ -11966,7 +12047,7 @@ var require_websocket = __commonJS({ this.#bufferedAmount -= value.byteLength; }); } else if (types.isArrayBuffer(data)) { - const value = Buffer.from(data); + const value = new FastBuffer(data); const frame = new WebsocketFrameSend(value); const buffer = frame.createFrame(opcodes.BINARY); this.#bufferedAmount += value.byteLength; @@ -11974,7 +12055,7 @@ var require_websocket = __commonJS({ this.#bufferedAmount -= value.byteLength; }); } else if (ArrayBuffer.isView(data)) { - const ab = Buffer.from(data, data.byteOffset, data.byteLength); + const ab = new FastBuffer(data, data.byteOffset, data.byteLength); const frame = new WebsocketFrameSend(ab); const buffer = frame.createFrame(opcodes.BINARY); this.#bufferedAmount += ab.byteLength; @@ -11984,7 +12065,7 @@ var require_websocket = __commonJS({ } else if (isBlobLike(data)) { const frame = new WebsocketFrameSend(); data.arrayBuffer().then((ab) => { - const value = Buffer.from(ab); + const value = new FastBuffer(ab); frame.frameData = value; const buffer = frame.createFrame(opcodes.BINARY); this.#bufferedAmount += value.byteLength; @@ -12096,9 +12177,8 @@ var require_websocket = __commonJS({ #onConnectionEstablished(response) { this[kResponse] = response; const parser = new ByteParser(this); - parser.on("drain", /* @__PURE__ */ __name(function onParserDrain() { - this.ws[kResponse].socket.resume(); - }, "onParserDrain")); + parser.on("drain", onParserDrain); + parser.on("error", onParserError.bind(this)); response.socket.ws = this; this[kByteParser] = parser; this[kReadyState] = states.OPEN; @@ -12189,6 +12269,15 @@ var require_websocket = __commonJS({ } return webidl.converters.USVString(V); }; + function onParserDrain() { + this.ws[kResponse].socket.resume(); + } + __name(onParserDrain, "onParserDrain"); + function onParserError(err) { + fireEvent("error", this, () => new ErrorEvent2("error", { error: err, message: err.reason })); + closeWebSocketConnection(this, err.code); + } + __name(onParserError, "onParserError"); module2.exports = { WebSocket }; diff --git a/src/undici_version.h b/src/undici_version.h index 8e4ceea5956503..b969e12a7a39f6 100644 --- a/src/undici_version.h +++ b/src/undici_version.h @@ -2,5 +2,5 @@ // Refer to tools/dep_updaters/update-undici.sh #ifndef SRC_UNDICI_VERSION_H_ #define SRC_UNDICI_VERSION_H_ -#define UNDICI_VERSION "6.15.0" +#define UNDICI_VERSION "6.16.1" #endif // SRC_UNDICI_VERSION_H_