mirror of
https://gitea.com/actions/setup-java.git
synced 2026-10-02 21:25:25 +08:00
chore(deps): bump undici from 6.28.0 to 6.29.0 (#1274)
* chore(deps): bump undici from 6.28.0 to 6.29.0 Bumps [undici](https://github.com/nodejs/undici) from 6.28.0 to 6.29.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v6.28.0...v6.29.0) --- updated-dependencies: - dependency-name: undici dependency-version: 6.29.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): update generated undici artifacts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * chore(deps): bump brace-expansion to 5.0.12 to fix high-severity audit Resolves GHSA-q2hr-2g5m-vwhr, GHSA-qhr7-859c-m2p7, GHSA-6j4f-fj2g-mc7p. Regenerates dist/ and updates licensed cache. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bruno Borges <brborges@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: brace-expansion
|
name: brace-expansion
|
||||||
version: 5.0.9
|
version: 5.0.12
|
||||||
type: npm
|
type: npm
|
||||||
summary: Brace expansion as known from sh/bash
|
summary: Brace expansion as known from sh/bash
|
||||||
homepage:
|
homepage:
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: undici
|
name: undici
|
||||||
version: 6.28.0
|
version: 6.29.0
|
||||||
type: npm
|
type: npm
|
||||||
summary: An HTTP/1.1 client, written from scratch for Node.js
|
summary: An HTTP/1.1 client, written from scratch for Node.js
|
||||||
homepage: https://undici.nodejs.org
|
homepage: https://undici.nodejs.org
|
||||||
|
|||||||
Vendored
+68
-24
@@ -59498,6 +59498,24 @@ const EXPANSION_MAX = 100_000;
|
|||||||
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
|
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
|
||||||
// characters) so legitimate input is unaffected.
|
// characters) so legitimate input is unaffected.
|
||||||
const EXPANSION_MAX_LENGTH = 4_000_000;
|
const EXPANSION_MAX_LENGTH = 4_000_000;
|
||||||
|
// `expand_` recurses once per level of brace *nesting* - both when expanding a
|
||||||
|
// set's comma members and when re-wrapping a set whose body is a single part.
|
||||||
|
// The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one
|
||||||
|
// level per chained group), which left nesting depth unbounded: about 3,100
|
||||||
|
// levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack
|
||||||
|
// and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser
|
||||||
|
// will follow nesting. It sits far above any realistic pattern and well below
|
||||||
|
// the depth at which the stack runs out.
|
||||||
|
const EXPANSION_MAX_DEPTH = 1_000;
|
||||||
|
// Bash keeps a quirk where a brace group followed by a comma set still expands
|
||||||
|
// (`{a},b}`). The parser implements it by rewriting the string and restarting
|
||||||
|
// the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n`
|
||||||
|
// full passes over a string that itself grows by one `escClose` sentinel each
|
||||||
|
// time - quadratic in `n`, with a ~26x constant from the sentinel's length.
|
||||||
|
// 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27
|
||||||
|
// seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many
|
||||||
|
// times the scan may restart. Real `{a},b}` input needs a handful.
|
||||||
|
const EXPANSION_MAX_REWRITES = 1_000;
|
||||||
function numeric(str) {
|
function numeric(str) {
|
||||||
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
|
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
|
||||||
}
|
}
|
||||||
@@ -59517,37 +59535,52 @@ function unescapeBraces(str) {
|
|||||||
.replace(escCommaPattern, ',')
|
.replace(escCommaPattern, ',')
|
||||||
.replace(escPeriodPattern, '.');
|
.replace(escPeriodPattern, '.');
|
||||||
}
|
}
|
||||||
|
// Like `target.push(...items)` but doesn't overflow the stack
|
||||||
|
function pushAll(target, items) {
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
target.push(items[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Basically just str.split(","), but handling cases
|
* Basically just str.split(","), but handling cases
|
||||||
* where we have nested braced sections, which should be
|
* where we have nested braced sections, which should be
|
||||||
* treated as individual members, like {a,{b,c},d}
|
* treated as individual members, like {a,{b,c},d}
|
||||||
*/
|
*/
|
||||||
function parseCommaParts(str) {
|
function parseCommaParts(str) {
|
||||||
if (!str) {
|
|
||||||
return [''];
|
|
||||||
}
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
const m = balanced('{', '}', str);
|
// Walk the brace groups iteratively. Recursing on `post` once per group let a
|
||||||
if (!m) {
|
// chain of them exhaust the stack - the parsing-side counterpart to
|
||||||
return str.split(',');
|
// the `expand_` overflow fixed for CVE-2026-14257, and not something `max` or
|
||||||
|
// `maxLength` can bound, since it happens before expansion.
|
||||||
|
//
|
||||||
|
// The part the next chunk continues
|
||||||
|
let carry = '';
|
||||||
|
for (;;) {
|
||||||
|
const m = balanced('{', '}', str);
|
||||||
|
if (!m) {
|
||||||
|
const tail = str.split(',');
|
||||||
|
tail[0] = carry + tail[0];
|
||||||
|
pushAll(parts, tail);
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
const { pre, body, post } = m;
|
||||||
|
const p = pre.split(',');
|
||||||
|
p[0] = carry + p[0];
|
||||||
|
p[p.length - 1] += '{' + body + '}';
|
||||||
|
if (!post.length) {
|
||||||
|
pushAll(parts, p);
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
carry = p.pop();
|
||||||
|
pushAll(parts, p);
|
||||||
|
str = post;
|
||||||
}
|
}
|
||||||
const { pre, body, post } = m;
|
|
||||||
const p = pre.split(',');
|
|
||||||
p[p.length - 1] += '{' + body + '}';
|
|
||||||
const postParts = parseCommaParts(post);
|
|
||||||
if (post.length) {
|
|
||||||
;
|
|
||||||
p[p.length - 1] += postParts.shift();
|
|
||||||
p.push.apply(p, postParts);
|
|
||||||
}
|
|
||||||
parts.push.apply(parts, p);
|
|
||||||
return parts;
|
|
||||||
}
|
}
|
||||||
function expand(str, options = {}) {
|
function expand(str, options = {}) {
|
||||||
if (!str) {
|
if (!str) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH } = options;
|
const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH, maxDepth = EXPANSION_MAX_DEPTH, maxRewrites = EXPANSION_MAX_REWRITES, } = options;
|
||||||
// I don't know why Bash 4.3 does this, but it does.
|
// I don't know why Bash 4.3 does this, but it does.
|
||||||
// Anything starting with {} will have the first two bytes preserved
|
// Anything starting with {} will have the first two bytes preserved
|
||||||
// but *only* at the top level, so {},a}b will not expand to anything,
|
// but *only* at the top level, so {},a}b will not expand to anything,
|
||||||
@@ -59557,7 +59590,7 @@ function expand(str, options = {}) {
|
|||||||
if (str.slice(0, 2) === '{}') {
|
if (str.slice(0, 2) === '{}') {
|
||||||
str = '\\{\\}' + str.slice(2);
|
str = '\\{\\}' + str.slice(2);
|
||||||
}
|
}
|
||||||
return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
|
return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
|
||||||
}
|
}
|
||||||
function embrace(str) {
|
function embrace(str) {
|
||||||
return '{' + str + '}';
|
return '{' + str + '}';
|
||||||
@@ -59652,7 +59685,13 @@ function expandSequence(body, isAlphaSequence, max, maxLength) {
|
|||||||
}
|
}
|
||||||
return N;
|
return N;
|
||||||
}
|
}
|
||||||
function expand_(str, max, maxLength, isTop) {
|
function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
|
||||||
|
// Too deeply nested to keep following: treat the rest as literal, the same
|
||||||
|
// way a group that cannot expand is already handled. Truncating rather than
|
||||||
|
// throwing keeps `expand` total, matching `max` and `maxLength`.
|
||||||
|
if (depth > maxDepth) {
|
||||||
|
return [str];
|
||||||
|
}
|
||||||
// Consume the string's top-level brace groups left to right, threading a
|
// Consume the string's top-level brace groups left to right, threading a
|
||||||
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
|
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
|
||||||
// rather than recursing on `m.post` once per group - keeps the native stack
|
// rather than recursing on `m.post` once per group - keeps the native stack
|
||||||
@@ -59664,6 +59703,9 @@ function expand_(str, max, maxLength, isTop) {
|
|||||||
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
|
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
|
||||||
// is on the final strings, so it is applied to whichever `combine` produces
|
// is on the final strings, so it is applied to whichever `combine` produces
|
||||||
// them (the one with no brace set left in the tail).
|
// them (the one with no brace set left in the tail).
|
||||||
|
// How many times the `{a},b}` rewrite below has restarted the scan. Each pass
|
||||||
|
// re-reads the whole string, so leaving this unbounded is quadratic.
|
||||||
|
let rewrites = 0;
|
||||||
let dropEmpties = false;
|
let dropEmpties = false;
|
||||||
let firstGroup = true;
|
let firstGroup = true;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -59688,7 +59730,8 @@ function expand_(str, max, maxLength, isTop) {
|
|||||||
const isOptions = m.body.indexOf(',') >= 0;
|
const isOptions = m.body.indexOf(',') >= 0;
|
||||||
if (!isSequence && !isOptions) {
|
if (!isSequence && !isOptions) {
|
||||||
// {a},b}
|
// {a},b}
|
||||||
if (m.post.match(/,(?!,).*\}/)) {
|
if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
|
||||||
|
rewrites++;
|
||||||
str = m.pre + '{' + m.body + escClose + m.post;
|
str = m.pre + '{' + m.body + escClose + m.post;
|
||||||
isTop = true;
|
isTop = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -59708,7 +59751,7 @@ function expand_(str, max, maxLength, isTop) {
|
|||||||
let n = parseCommaParts(m.body);
|
let n = parseCommaParts(m.body);
|
||||||
if (n.length === 1 && n[0] !== undefined) {
|
if (n.length === 1 && n[0] !== undefined) {
|
||||||
// x{{a,b}}y ==> x{a}y x{b}y
|
// x{{a,b}}y ==> x{a}y x{b}y
|
||||||
n = expand_(n[0], max, maxLength, false).map(embrace);
|
n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
|
||||||
//XXX is this necessary? Can't seem to hit it in tests.
|
//XXX is this necessary? Can't seem to hit it in tests.
|
||||||
/* c8 ignore start */
|
/* c8 ignore start */
|
||||||
if (n.length === 1) {
|
if (n.length === 1) {
|
||||||
@@ -59734,12 +59777,13 @@ function expand_(str, max, maxLength, isTop) {
|
|||||||
values = [];
|
values = [];
|
||||||
let valuesLength = 0;
|
let valuesLength = 0;
|
||||||
outer: for (let j = 0; j < n.length; j++) {
|
outer: for (let j = 0; j < n.length; j++) {
|
||||||
const expanded = expand_(n[j], max, maxLength, false);
|
const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
|
||||||
for (let k = 0; k < expanded.length; k++) {
|
for (let k = 0; k < expanded.length; k++) {
|
||||||
const v = expanded[k];
|
const v = expanded[k];
|
||||||
if (dropsEmpties && !v)
|
if (dropsEmpties && !v)
|
||||||
continue;
|
continue;
|
||||||
if (values.length >= max || valuesLength + v.length > maxLength) {
|
if (values.length >= max ||
|
||||||
|
valuesLength + v.length > maxLength) {
|
||||||
break outer;
|
break outer;
|
||||||
}
|
}
|
||||||
values.push(v);
|
values.push(v);
|
||||||
|
|||||||
Vendored
+453
-187
@@ -6009,11 +6009,77 @@ class Request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpgrade (statusCode, headers, socket) {
|
/**
|
||||||
|
* @param {number|null} statusCode
|
||||||
|
* @param {Buffer[]|null} headers
|
||||||
|
* @param {import('node:stream').Duplex} socket
|
||||||
|
* @param {string} [statusText]
|
||||||
|
*/
|
||||||
|
onUpgrade (statusCode, headers, socket, statusText = '') {
|
||||||
|
this.onFinally()
|
||||||
|
|
||||||
assert(!this.aborted)
|
assert(!this.aborted)
|
||||||
assert(!this.completed)
|
assert(!this.completed)
|
||||||
|
|
||||||
return this[kHandler].onUpgrade(statusCode, headers, socket)
|
if (statusCode !== null) {
|
||||||
|
this.#publishUpgradeHeaders(statusCode, headers, statusText)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = this[kHandler].onUpgrade(statusCode, headers, socket)
|
||||||
|
|
||||||
|
if (!this.aborted) {
|
||||||
|
this.completed = true
|
||||||
|
if (statusCode !== null) {
|
||||||
|
this.#publishUpgradeTrailers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} statusCode
|
||||||
|
* @param {import('node:http2').IncomingHttpHeaders} headers
|
||||||
|
* @param {(headers: import('node:http2').IncomingHttpHeaders) => Buffer[]} parseHeaders
|
||||||
|
* @param {string} [statusText]
|
||||||
|
*/
|
||||||
|
onUpgradeResponse (statusCode, headers, parseHeaders, statusText = '') {
|
||||||
|
assert(!this.aborted)
|
||||||
|
assert(this.completed)
|
||||||
|
|
||||||
|
if (channels.headers.hasSubscribers) {
|
||||||
|
this.#publishUpgradeHeaders(statusCode, parseHeaders(headers), statusText)
|
||||||
|
}
|
||||||
|
this.#publishUpgradeTrailers()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Error} error
|
||||||
|
*/
|
||||||
|
onUpgradeError (error) {
|
||||||
|
assert(!this.aborted)
|
||||||
|
assert(this.completed)
|
||||||
|
|
||||||
|
if (channels.error.hasSubscribers) {
|
||||||
|
channels.error.publish({ request: this, error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} statusCode
|
||||||
|
* @param {Buffer[]} headers
|
||||||
|
* @param {string} statusText
|
||||||
|
*/
|
||||||
|
#publishUpgradeHeaders (statusCode, headers, statusText) {
|
||||||
|
if (channels.headers.hasSubscribers) {
|
||||||
|
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#publishUpgradeTrailers () {
|
||||||
|
if (channels.trailers.hasSubscribers) {
|
||||||
|
channels.trailers.publish({ request: this, trailers: [] })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onComplete (trailers) {
|
onComplete (trailers) {
|
||||||
@@ -7912,7 +7978,7 @@ class Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onUpgrade (head) {
|
onUpgrade (head) {
|
||||||
const { upgrade, client, socket, headers, statusCode } = this
|
const { upgrade, client, socket, headers, statusCode, statusText } = this
|
||||||
|
|
||||||
assert(upgrade)
|
assert(upgrade)
|
||||||
assert(client[kSocket] === socket)
|
assert(client[kSocket] === socket)
|
||||||
@@ -7947,9 +8013,10 @@ class Parser {
|
|||||||
client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
|
client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
request.onUpgrade(statusCode, headers, socket)
|
request.onUpgrade(statusCode, headers, socket, statusText)
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
util.destroy(socket, err)
|
util.errorRequest(client, request, error)
|
||||||
|
util.destroy(socket, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
client[kResume]()
|
client[kResume]()
|
||||||
@@ -8356,7 +8423,7 @@ async function connectH1 (client, socket) {
|
|||||||
|
|
||||||
function clearIdleSocketValidation (socket) {
|
function clearIdleSocketValidation (socket) {
|
||||||
if (socket[kIdleSocketValidationTimeout]) {
|
if (socket[kIdleSocketValidationTimeout]) {
|
||||||
clearTimeout(socket[kIdleSocketValidationTimeout])
|
clearImmediate(socket[kIdleSocketValidationTimeout])
|
||||||
socket[kIdleSocketValidationTimeout] = null
|
socket[kIdleSocketValidationTimeout] = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8365,15 +8432,23 @@ function clearIdleSocketValidation (socket) {
|
|||||||
|
|
||||||
function scheduleIdleSocketValidation (client, socket) {
|
function scheduleIdleSocketValidation (client, socket) {
|
||||||
socket[kIdleSocketValidation] = 1
|
socket[kIdleSocketValidation] = 1
|
||||||
socket[kIdleSocketValidationTimeout] = setTimeout(() => {
|
// Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
|
||||||
|
// already pending on this idle keep-alive socket are processed before the
|
||||||
|
// next request is written (GHSA-35p6-xmwp-9g52).
|
||||||
|
//
|
||||||
|
// setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
|
||||||
|
// (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
|
||||||
|
// block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
|
||||||
|
// A ref'd Immediate both keeps the pending request alive and makes poll
|
||||||
|
// return immediately — the hybrid those issues asked for.
|
||||||
|
socket[kIdleSocketValidationTimeout] = setImmediate(() => {
|
||||||
socket[kIdleSocketValidationTimeout] = null
|
socket[kIdleSocketValidationTimeout] = null
|
||||||
socket[kIdleSocketValidation] = 2
|
socket[kIdleSocketValidation] = 2
|
||||||
|
|
||||||
if (client[kSocket] === socket && !socket.destroyed) {
|
if (client[kSocket] === socket && !socket.destroyed) {
|
||||||
client[kResume]()
|
client[kResume]()
|
||||||
}
|
}
|
||||||
}, 0)
|
})
|
||||||
socket[kIdleSocketValidationTimeout].unref?.()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8522,12 +8597,22 @@ function writeH1 (client, request) {
|
|||||||
const socket = client[kSocket]
|
const socket = client[kSocket]
|
||||||
clearIdleSocketValidation(socket)
|
clearIdleSocketValidation(socket)
|
||||||
|
|
||||||
const abort = (err) => {
|
/**
|
||||||
if (request.aborted || request.completed) {
|
* @param {Error} [error]
|
||||||
|
*/
|
||||||
|
const abort = (error) => {
|
||||||
|
if (request.aborted) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
util.errorRequest(client, request, err || new RequestAbortedError())
|
if (request.completed) {
|
||||||
|
if (request.upgrade || request.method === 'CONNECT') {
|
||||||
|
util.destroy(socket, new InformationalError('aborted'))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.errorRequest(client, request, error || new RequestAbortedError())
|
||||||
|
|
||||||
util.destroy(body)
|
util.destroy(body)
|
||||||
util.destroy(socket, new InformationalError('aborted'))
|
util.destroy(socket, new InformationalError('aborted'))
|
||||||
@@ -8984,6 +9069,7 @@ module.exports = connectH1
|
|||||||
|
|
||||||
|
|
||||||
const assert = __nccwpck_require__(4589)
|
const assert = __nccwpck_require__(4589)
|
||||||
|
const { errorMonitor } = __nccwpck_require__(8474)
|
||||||
const { pipeline } = __nccwpck_require__(7075)
|
const { pipeline } = __nccwpck_require__(7075)
|
||||||
const util = __nccwpck_require__(3440)
|
const util = __nccwpck_require__(3440)
|
||||||
const {
|
const {
|
||||||
@@ -9060,6 +9146,15 @@ function parseH2Headers (headers) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('node:http2').IncomingHttpHeaders} headers
|
||||||
|
* @returns {Buffer[]}
|
||||||
|
*/
|
||||||
|
function parseH2ResponseHeaders (headers) {
|
||||||
|
const { [HTTP2_HEADER_STATUS]: _statusCode, ...realHeaders } = headers
|
||||||
|
return parseH2Headers(realHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
async function connectH2 (client, socket) {
|
async function connectH2 (client, socket) {
|
||||||
client[kSocket] = socket
|
client[kSocket] = socket
|
||||||
|
|
||||||
@@ -9280,22 +9375,32 @@ function writeH2 (client, request) {
|
|||||||
headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
|
headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
|
||||||
headers[HTTP2_HEADER_METHOD] = method
|
headers[HTTP2_HEADER_METHOD] = method
|
||||||
|
|
||||||
const abort = (err) => {
|
/**
|
||||||
if (request.aborted || request.completed) {
|
* @param {Error} [error]
|
||||||
|
*/
|
||||||
|
const abort = (error) => {
|
||||||
|
if (request.aborted) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = err || new RequestAbortedError()
|
if (request.completed) {
|
||||||
|
if (method === 'CONNECT' && stream != null) {
|
||||||
|
util.destroy(stream, error || new RequestAbortedError())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
util.errorRequest(client, request, err)
|
error = error || new RequestAbortedError()
|
||||||
|
|
||||||
|
util.errorRequest(client, request, error)
|
||||||
|
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
util.destroy(stream, err)
|
util.destroy(stream, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We do not destroy the socket as we can continue using the session
|
// We do not destroy the socket as we can continue using the session
|
||||||
// the stream get's destroyed and the session remains to create new streams
|
// the stream get's destroyed and the session remains to create new streams
|
||||||
util.destroy(body, err)
|
util.destroy(body, error)
|
||||||
client[kQueue][client[kRunningIdx]++] = null
|
client[kQueue][client[kRunningIdx]++] = null
|
||||||
client[kResume]()
|
client[kResume]()
|
||||||
}
|
}
|
||||||
@@ -9314,25 +9419,57 @@ function writeH2 (client, request) {
|
|||||||
|
|
||||||
if (method === 'CONNECT') {
|
if (method === 'CONNECT') {
|
||||||
session.ref()
|
session.ref()
|
||||||
// We are already connected, streams are pending, first request
|
|
||||||
// will create a new stream. We trigger a request to create the stream and wait until
|
|
||||||
// `ready` event is triggered
|
|
||||||
// We disabled endStream to allow the user to write to the stream
|
// We disabled endStream to allow the user to write to the stream
|
||||||
stream = session.request(headers, { endStream: false, signal })
|
stream = session.request(headers, { endStream: false, signal })
|
||||||
|
let upgradeResponseFinished = false
|
||||||
|
|
||||||
if (stream.id && !stream.pending) {
|
/**
|
||||||
request.onUpgrade(null, null, stream)
|
* @param {import('node:http2').IncomingHttpHeaders} headers
|
||||||
++session[kOpenStreams]
|
*/
|
||||||
client[kQueue][client[kRunningIdx]++] = null
|
const onResponse = (headers) => {
|
||||||
} else {
|
upgradeResponseFinished = true
|
||||||
stream.once('ready', () => {
|
stream.off(errorMonitor, onUpgradeError)
|
||||||
request.onUpgrade(null, null, stream)
|
request.onUpgradeResponse(Number(headers[HTTP2_HEADER_STATUS]), headers, parseH2ResponseHeaders)
|
||||||
++session[kOpenStreams]
|
|
||||||
client[kQueue][client[kRunningIdx]++] = null
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Error} error
|
||||||
|
*/
|
||||||
|
const onUpgradeError = (error) => {
|
||||||
|
upgradeResponseFinished = true
|
||||||
|
stream.off('response', onResponse)
|
||||||
|
request.onUpgradeError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onReady = () => {
|
||||||
|
try {
|
||||||
|
request.onUpgrade(null, null, stream)
|
||||||
|
} catch (error) {
|
||||||
|
stream.off('response', onResponse)
|
||||||
|
abort(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.aborted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.off('error', abort)
|
||||||
|
stream.once(errorMonitor, onUpgradeError)
|
||||||
|
client[kQueue][client[kRunningIdx]++] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.once('response', onResponse)
|
||||||
|
stream.once('error', abort)
|
||||||
|
++session[kOpenStreams]
|
||||||
|
onReady()
|
||||||
|
|
||||||
stream.once('close', () => {
|
stream.once('close', () => {
|
||||||
|
if (!upgradeResponseFinished && request.completed) {
|
||||||
|
stream.off('response', onResponse)
|
||||||
|
stream.off(errorMonitor, onUpgradeError)
|
||||||
|
request.onUpgradeError(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`))
|
||||||
|
}
|
||||||
session[kOpenStreams] -= 1
|
session[kOpenStreams] -= 1
|
||||||
if (session[kOpenStreams] === 0) session.unref()
|
if (session[kOpenStreams] === 0) session.unref()
|
||||||
})
|
})
|
||||||
@@ -12031,6 +12168,7 @@ class RetryHandler {
|
|||||||
this.end = null
|
this.end = null
|
||||||
this.etag = null
|
this.etag = null
|
||||||
this.resume = null
|
this.resume = null
|
||||||
|
this.headersSent = false
|
||||||
|
|
||||||
// Handle possible onConnect duplication
|
// Handle possible onConnect duplication
|
||||||
this.handler.onConnect(reason => {
|
this.handler.onConnect(reason => {
|
||||||
@@ -12043,6 +12181,20 @@ class RetryHandler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkpointResponseEnd (headers, resume) {
|
||||||
|
if (this.end == null && this.opts.method !== 'HEAD') {
|
||||||
|
const contentLength = headers['content-length']
|
||||||
|
this.end = contentLength != null ? Number(contentLength) - 1 : null
|
||||||
|
|
||||||
|
assert(
|
||||||
|
this.end == null || Number.isFinite(this.end),
|
||||||
|
'invalid content-length'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resume = this.end != null ? resume : null
|
||||||
|
}
|
||||||
|
|
||||||
onRequestSent () {
|
onRequestSent () {
|
||||||
if (this.handler.onRequestSent) {
|
if (this.handler.onRequestSent) {
|
||||||
this.handler.onRequestSent()
|
this.handler.onRequestSent()
|
||||||
@@ -12131,7 +12283,12 @@ class RetryHandler {
|
|||||||
this.retryCount += 1
|
this.retryCount += 1
|
||||||
|
|
||||||
if (statusCode >= 300) {
|
if (statusCode >= 300) {
|
||||||
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
|
// Only expose a response if no earlier attempt has reached the caller.
|
||||||
|
// Otherwise abort this attempt so the error settles the existing body
|
||||||
|
// instead of replacing it with a new response.
|
||||||
|
if (!this.headersSent && this.retryOpts.statusCodes.includes(statusCode) === false) {
|
||||||
|
this.headersSent = true
|
||||||
|
this.checkpointResponseEnd(headers, resume)
|
||||||
return this.handler.onHeaders(
|
return this.handler.onHeaders(
|
||||||
statusCode,
|
statusCode,
|
||||||
rawHeaders,
|
rawHeaders,
|
||||||
@@ -12200,8 +12357,15 @@ class RetryHandler {
|
|||||||
|
|
||||||
const { start, size, end = size - 1 } = contentRange
|
const { start, size, end = size - 1 } = contentRange
|
||||||
|
|
||||||
assert(this.start === start, 'content-range mismatch')
|
if (this.start !== start || (this.end != null && this.end !== end)) {
|
||||||
assert(this.end == null || this.end === end, 'content-range mismatch')
|
this.abort(
|
||||||
|
new RequestRetryError('Content-Range mismatch', statusCode, {
|
||||||
|
headers,
|
||||||
|
data: { count: this.retryCount }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
this.resume = resume
|
this.resume = resume
|
||||||
return true
|
return true
|
||||||
@@ -12213,6 +12377,7 @@ class RetryHandler {
|
|||||||
const range = parseRangeHeader(headers['content-range'])
|
const range = parseRangeHeader(headers['content-range'])
|
||||||
|
|
||||||
if (range == null) {
|
if (range == null) {
|
||||||
|
this.headersSent = true
|
||||||
return this.handler.onHeaders(
|
return this.handler.onHeaders(
|
||||||
statusCode,
|
statusCode,
|
||||||
rawHeaders,
|
rawHeaders,
|
||||||
@@ -12251,6 +12416,7 @@ class RetryHandler {
|
|||||||
)
|
)
|
||||||
|
|
||||||
this.resume = resume
|
this.resume = resume
|
||||||
|
this.headersSent = true
|
||||||
this.etag = headers.etag != null ? headers.etag : null
|
this.etag = headers.etag != null ? headers.etag : null
|
||||||
|
|
||||||
// Weak etags are not useful for comparison nor cache
|
// Weak etags are not useful for comparison nor cache
|
||||||
@@ -12290,7 +12456,7 @@ class RetryHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onError (err) {
|
onError (err) {
|
||||||
if (this.aborted || isDisturbed(this.opts.body)) {
|
if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) {
|
||||||
return this.handler.onError(err)
|
return this.handler.onError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16748,6 +16914,49 @@ const COLON = 0x3A
|
|||||||
*/
|
*/
|
||||||
const SPACE = 0x20
|
const SPACE = 0x20
|
||||||
|
|
||||||
|
const DATA = Buffer.from('data')
|
||||||
|
const EVENT = Buffer.from('event')
|
||||||
|
const ID = Buffer.from('id')
|
||||||
|
const RETRY = Buffer.from('retry')
|
||||||
|
|
||||||
|
function isASCIINumberBytes (buffer, start) {
|
||||||
|
if (start >= buffer.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i < buffer.length; i++) {
|
||||||
|
if (buffer[i] < 0x30 || buffer[i] > 0x39) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidLastEventIdBytes (buffer, start) {
|
||||||
|
for (let i = start; i < buffer.length; i++) {
|
||||||
|
if (buffer[i] === 0x00) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFieldName (line, length, field) {
|
||||||
|
if (length !== field.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
if (line[i] !== field[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} EventSourceStreamEvent
|
* @typedef {object} EventSourceStreamEvent
|
||||||
* @type {object}
|
* @type {object}
|
||||||
@@ -16788,11 +16997,14 @@ class EventSourceStream extends Transform {
|
|||||||
eventEndCheck = false
|
eventEndCheck = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Buffer}
|
* @type {Buffer[]}
|
||||||
*/
|
*/
|
||||||
buffer = null
|
chunks = []
|
||||||
|
|
||||||
|
chunkIndex = 0
|
||||||
pos = 0
|
pos = 0
|
||||||
|
lineChunkIndex = 0
|
||||||
|
linePos = 0
|
||||||
|
|
||||||
event = {
|
event = {
|
||||||
data: undefined,
|
data: undefined,
|
||||||
@@ -16831,92 +17043,20 @@ class EventSourceStream extends Transform {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the chunk in the buffer, as the data might not be complete while
|
this.chunks.push(chunk)
|
||||||
// processing it
|
|
||||||
// TODO: Investigate if there is a more performant way to handle
|
|
||||||
// incoming chunks
|
|
||||||
// see: https://github.com/nodejs/undici/issues/2630
|
|
||||||
if (this.buffer) {
|
|
||||||
this.buffer = Buffer.concat([this.buffer, chunk])
|
|
||||||
} else {
|
|
||||||
this.buffer = chunk
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip leading byte-order-mark if we opened the stream and started
|
// Strip leading byte-order-mark if we opened the stream and started
|
||||||
// the processing of the incoming data
|
// the processing of the incoming data
|
||||||
if (this.checkBOM) {
|
if (this.checkBOM) {
|
||||||
switch (this.buffer.length) {
|
if (this.handleBOM()) {
|
||||||
case 1:
|
callback()
|
||||||
// Check if the first byte is the same as the first byte of the BOM
|
return
|
||||||
if (this.buffer[0] === BOM[0]) {
|
|
||||||
// If it is, we need to wait for more data
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Set the checkBOM flag to false as we don't need to check for the
|
|
||||||
// BOM anymore
|
|
||||||
this.checkBOM = false
|
|
||||||
|
|
||||||
// The buffer only contains one byte so we need to wait for more data
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
case 2:
|
|
||||||
// Check if the first two bytes are the same as the first two bytes
|
|
||||||
// of the BOM
|
|
||||||
if (
|
|
||||||
this.buffer[0] === BOM[0] &&
|
|
||||||
this.buffer[1] === BOM[1]
|
|
||||||
) {
|
|
||||||
// If it is, we need to wait for more data, because the third byte
|
|
||||||
// is needed to determine if it is the BOM or not
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the checkBOM flag to false as we don't need to check for the
|
|
||||||
// BOM anymore
|
|
||||||
this.checkBOM = false
|
|
||||||
break
|
|
||||||
case 3:
|
|
||||||
// Check if the first three bytes are the same as the first three
|
|
||||||
// bytes of the BOM
|
|
||||||
if (
|
|
||||||
this.buffer[0] === BOM[0] &&
|
|
||||||
this.buffer[1] === BOM[1] &&
|
|
||||||
this.buffer[2] === BOM[2]
|
|
||||||
) {
|
|
||||||
// If it is, we can drop the buffered data, as it is only the BOM
|
|
||||||
this.buffer = Buffer.alloc(0)
|
|
||||||
// Set the checkBOM flag to false as we don't need to check for the
|
|
||||||
// BOM anymore
|
|
||||||
this.checkBOM = false
|
|
||||||
|
|
||||||
// Await more data
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If it is not the BOM, we can start processing the data
|
|
||||||
this.checkBOM = false
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
// The buffer is longer than 3 bytes, so we can drop the BOM if it is
|
|
||||||
// present
|
|
||||||
if (
|
|
||||||
this.buffer[0] === BOM[0] &&
|
|
||||||
this.buffer[1] === BOM[1] &&
|
|
||||||
this.buffer[2] === BOM[2]
|
|
||||||
) {
|
|
||||||
// Remove the BOM from the buffer
|
|
||||||
this.buffer = this.buffer.subarray(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the checkBOM flag to false as we don't need to check for the
|
|
||||||
this.checkBOM = false
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (this.pos < this.buffer.length) {
|
while (this.hasCurrentByte()) {
|
||||||
|
const byte = this.currentByte()
|
||||||
|
|
||||||
// If the previous line ended with an end-of-line, we need to check
|
// If the previous line ended with an end-of-line, we need to check
|
||||||
// if the next character is also an end-of-line.
|
// if the next character is also an end-of-line.
|
||||||
if (this.eventEndCheck) {
|
if (this.eventEndCheck) {
|
||||||
@@ -16929,10 +17069,9 @@ class EventSourceStream extends Transform {
|
|||||||
if (this.crlfCheck) {
|
if (this.crlfCheck) {
|
||||||
// If the current character is a line feed, we can remove it
|
// If the current character is a line feed, we can remove it
|
||||||
// from the buffer and reset the crlfCheck flag
|
// from the buffer and reset the crlfCheck flag
|
||||||
if (this.buffer[this.pos] === LF) {
|
if (byte === LF) {
|
||||||
this.buffer = this.buffer.subarray(this.pos + 1)
|
|
||||||
this.pos = 0
|
|
||||||
this.crlfCheck = false
|
this.crlfCheck = false
|
||||||
|
this.consumeCurrentByte()
|
||||||
|
|
||||||
// It is possible that the line feed is not the end of the
|
// It is possible that the line feed is not the end of the
|
||||||
// event. We need to check if the next character is an
|
// event. We need to check if the next character is an
|
||||||
@@ -16948,19 +17087,17 @@ class EventSourceStream extends Transform {
|
|||||||
this.crlfCheck = false
|
this.crlfCheck = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
|
if (byte === LF || byte === CR) {
|
||||||
// If the current character is a carriage return, we need to
|
// If the current character is a carriage return, we need to
|
||||||
// set the crlfCheck flag to true, as we need to check if the
|
// set the crlfCheck flag to true, as we need to check if the
|
||||||
// next character is a line feed so we can remove it from the
|
// next character is a line feed so we can remove it from the
|
||||||
// buffer
|
// buffer
|
||||||
if (this.buffer[this.pos] === CR) {
|
if (byte === CR) {
|
||||||
this.crlfCheck = true
|
this.crlfCheck = true
|
||||||
}
|
}
|
||||||
|
|
||||||
this.buffer = this.buffer.subarray(this.pos + 1)
|
this.consumeCurrentByte()
|
||||||
this.pos = 0
|
if (this.hasPendingEvent()) {
|
||||||
if (
|
|
||||||
this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
|
|
||||||
this.processEvent(this.event)
|
this.processEvent(this.event)
|
||||||
}
|
}
|
||||||
this.clearEvent()
|
this.clearEvent()
|
||||||
@@ -16974,22 +17111,18 @@ class EventSourceStream extends Transform {
|
|||||||
|
|
||||||
// If the current character is an end-of-line, we can process the
|
// If the current character is an end-of-line, we can process the
|
||||||
// line
|
// line
|
||||||
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
|
if (byte === LF || byte === CR) {
|
||||||
// If the current character is a carriage return, we need to
|
// If the current character is a carriage return, we need to
|
||||||
// set the crlfCheck flag to true, as we need to check if the
|
// set the crlfCheck flag to true, as we need to check if the
|
||||||
// next character is a line feed
|
// next character is a line feed
|
||||||
if (this.buffer[this.pos] === CR) {
|
if (byte === CR) {
|
||||||
this.crlfCheck = true
|
this.crlfCheck = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// In any case, we can process the line as we reached an
|
// In any case, we can process the line as we reached an
|
||||||
// end-of-line character
|
// end-of-line character
|
||||||
this.parseLine(this.buffer.subarray(0, this.pos), this.event)
|
this.parseLine(this.readLine(), this.event)
|
||||||
|
this.consumeCurrentByte()
|
||||||
// Remove the processed line from the buffer
|
|
||||||
this.buffer = this.buffer.subarray(this.pos + 1)
|
|
||||||
// Reset the position as we removed the processed line from the buffer
|
|
||||||
this.pos = 0
|
|
||||||
// A line was processed and this could be the end of the event. We need
|
// A line was processed and this could be the end of the event. We need
|
||||||
// to check if the next line is empty to determine if the event is
|
// to check if the next line is empty to determine if the event is
|
||||||
// finished.
|
// finished.
|
||||||
@@ -16997,7 +17130,7 @@ class EventSourceStream extends Transform {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pos++
|
this.advanceCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
callback()
|
callback()
|
||||||
@@ -17022,64 +17155,53 @@ class EventSourceStream extends Transform {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let field = ''
|
let fieldLength = line.length
|
||||||
let value = ''
|
let valueStart = line.length
|
||||||
|
|
||||||
// If the line contains a U+003A COLON character (:)
|
// If the line contains a U+003A COLON character (:)
|
||||||
if (colonPosition !== -1) {
|
if (colonPosition !== -1) {
|
||||||
// Collect the characters on the line before the first U+003A COLON
|
fieldLength = colonPosition
|
||||||
// character (:), and let field be that string.
|
|
||||||
// TODO: Investigate if there is a more performant way to extract the
|
|
||||||
// field
|
|
||||||
// see: https://github.com/nodejs/undici/issues/2630
|
|
||||||
field = line.subarray(0, colonPosition).toString('utf8')
|
|
||||||
|
|
||||||
// Collect the characters on the line after the first U+003A COLON
|
// Collect the characters on the line after the first U+003A COLON
|
||||||
// character (:), and let value be that string.
|
// character (:), and let value be that string.
|
||||||
// If value starts with a U+0020 SPACE character, remove it from value.
|
// If value starts with a U+0020 SPACE character, remove it from value.
|
||||||
let valueStart = colonPosition + 1
|
valueStart = colonPosition + 1
|
||||||
if (line[valueStart] === SPACE) {
|
if (line[valueStart] === SPACE) {
|
||||||
++valueStart
|
++valueStart
|
||||||
}
|
}
|
||||||
// TODO: Investigate if there is a more performant way to extract the
|
|
||||||
// value
|
|
||||||
// see: https://github.com/nodejs/undici/issues/2630
|
|
||||||
value = line.subarray(valueStart).toString('utf8')
|
|
||||||
|
|
||||||
// Otherwise, the string is not empty but does not contain a U+003A COLON
|
|
||||||
// character (:)
|
|
||||||
} else {
|
|
||||||
// Process the field using the steps described below, using the whole
|
|
||||||
// line as the field name, and the empty string as the field value.
|
|
||||||
field = line.toString('utf8')
|
|
||||||
value = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modify the event with the field name and value. The value is also
|
if (isFieldName(line, fieldLength, DATA)) {
|
||||||
// decoded as UTF-8
|
const value = line.toString('utf8', valueStart)
|
||||||
switch (field) {
|
|
||||||
case 'data':
|
if (event.data === undefined) {
|
||||||
if (event[field] === undefined) {
|
event.data = value
|
||||||
event[field] = value
|
} else {
|
||||||
} else {
|
event.data += `\n${value}`
|
||||||
event[field] += `\n${value}`
|
}
|
||||||
}
|
return
|
||||||
break
|
}
|
||||||
case 'retry':
|
|
||||||
if (isASCIINumber(value)) {
|
if (isFieldName(line, fieldLength, RETRY)) {
|
||||||
event[field] = value
|
if (isASCIINumberBytes(line, valueStart)) {
|
||||||
}
|
event.retry = line.toString('utf8', valueStart)
|
||||||
break
|
}
|
||||||
case 'id':
|
return
|
||||||
if (isValidLastEventId(value)) {
|
}
|
||||||
event[field] = value
|
|
||||||
}
|
if (isFieldName(line, fieldLength, ID)) {
|
||||||
break
|
if (isValidLastEventIdBytes(line, valueStart)) {
|
||||||
case 'event':
|
event.id = line.toString('utf8', valueStart)
|
||||||
if (value.length > 0) {
|
}
|
||||||
event[field] = value
|
return
|
||||||
}
|
}
|
||||||
break
|
|
||||||
|
if (isFieldName(line, fieldLength, EVENT)) {
|
||||||
|
const value = line.toString('utf8', valueStart)
|
||||||
|
|
||||||
|
if (value.length > 0) {
|
||||||
|
event.event = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17109,13 +17231,152 @@ class EventSourceStream extends Transform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearEvent () {
|
clearEvent () {
|
||||||
this.event = {
|
this.event.data = undefined
|
||||||
data: undefined,
|
this.event.event = undefined
|
||||||
event: undefined,
|
this.event.id = undefined
|
||||||
id: undefined,
|
this.event.retry = undefined
|
||||||
retry: undefined
|
}
|
||||||
|
|
||||||
|
hasPendingEvent () {
|
||||||
|
return this.event.data !== undefined ||
|
||||||
|
this.event.event !== undefined ||
|
||||||
|
this.event.id !== undefined ||
|
||||||
|
this.event.retry !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCurrentByte () {
|
||||||
|
return this.chunkIndex < this.chunks.length &&
|
||||||
|
this.pos < this.chunks[this.chunkIndex].length
|
||||||
|
}
|
||||||
|
|
||||||
|
currentByte () {
|
||||||
|
return this.chunks[this.chunkIndex][this.pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeCurrentByte () {
|
||||||
|
this.advanceCursor()
|
||||||
|
this.syncLineStartToCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
advanceCursor () {
|
||||||
|
this.pos++
|
||||||
|
|
||||||
|
while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) {
|
||||||
|
this.chunkIndex++
|
||||||
|
this.pos = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncLineStartToCursor () {
|
||||||
|
this.lineChunkIndex = this.chunkIndex
|
||||||
|
this.linePos = this.pos
|
||||||
|
this.dropConsumedChunks()
|
||||||
|
}
|
||||||
|
|
||||||
|
dropConsumedChunks () {
|
||||||
|
while (this.lineChunkIndex > 0) {
|
||||||
|
this.chunks.shift()
|
||||||
|
this.lineChunkIndex--
|
||||||
|
this.chunkIndex--
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.chunkIndex === this.chunks.length) {
|
||||||
|
this.chunks.length = 0
|
||||||
|
this.chunkIndex = 0
|
||||||
|
this.pos = 0
|
||||||
|
this.lineChunkIndex = 0
|
||||||
|
this.linePos = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readLine () {
|
||||||
|
if (this.lineChunkIndex === this.chunkIndex) {
|
||||||
|
return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = []
|
||||||
|
let length = 0
|
||||||
|
|
||||||
|
for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) {
|
||||||
|
const chunk = this.chunks[i]
|
||||||
|
const start = i === this.lineChunkIndex ? this.linePos : 0
|
||||||
|
const end = i === this.chunkIndex ? this.pos : chunk.length
|
||||||
|
const slice = chunk.subarray(start, end)
|
||||||
|
length += slice.length
|
||||||
|
chunks.push(slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.concat(chunks, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
peekBufferedByte (offset) {
|
||||||
|
let chunkIndex = this.lineChunkIndex
|
||||||
|
let pos = this.linePos
|
||||||
|
|
||||||
|
while (chunkIndex < this.chunks.length) {
|
||||||
|
const chunk = this.chunks[chunkIndex]
|
||||||
|
const remaining = chunk.length - pos
|
||||||
|
|
||||||
|
if (offset < remaining) {
|
||||||
|
return chunk[pos + offset]
|
||||||
|
}
|
||||||
|
|
||||||
|
offset -= remaining
|
||||||
|
chunkIndex++
|
||||||
|
pos = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
discardLeadingBytes (count) {
|
||||||
|
while (count > 0 && this.lineChunkIndex < this.chunks.length) {
|
||||||
|
const chunk = this.chunks[this.lineChunkIndex]
|
||||||
|
const remaining = chunk.length - this.linePos
|
||||||
|
|
||||||
|
if (count < remaining) {
|
||||||
|
this.linePos += count
|
||||||
|
count = 0
|
||||||
|
} else {
|
||||||
|
count -= remaining
|
||||||
|
this.lineChunkIndex++
|
||||||
|
this.linePos = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chunkIndex = this.lineChunkIndex
|
||||||
|
this.pos = this.linePos
|
||||||
|
this.dropConsumedChunks()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBOM () {
|
||||||
|
const first = this.peekBufferedByte(0)
|
||||||
|
const second = this.peekBufferedByte(1)
|
||||||
|
const third = this.peekBufferedByte(2)
|
||||||
|
|
||||||
|
if (second === undefined) {
|
||||||
|
if (first === BOM[0]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkBOM = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (third === undefined) {
|
||||||
|
if (first === BOM[0] && second === BOM[1]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkBOM = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first === BOM[0] && second === BOM[1] && third === BOM[2]) {
|
||||||
|
this.discardLeadingBytes(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkBOM = false
|
||||||
|
return !this.hasCurrentByte()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -28383,7 +28644,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
|
|||||||
// is specified, the server needs to include the same field and one of
|
// is specified, the server needs to include the same field and one of
|
||||||
// the selected subprotocol values in its response for the connection to
|
// the selected subprotocol values in its response for the connection to
|
||||||
// be established.
|
// be established.
|
||||||
if (!requestProtocols.includes(secProtocol)) {
|
if (requestProtocols === null || !requestProtocols.includes(secProtocol)) {
|
||||||
failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
|
failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -29144,7 +29405,12 @@ class PerMessageDeflate {
|
|||||||
|
|
||||||
if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
|
if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
|
||||||
callback(new MessageSizeExceededError())
|
callback(new MessageSizeExceededError())
|
||||||
|
// The inflater may still hold buffered input that can emit a late
|
||||||
|
// zlib error. Remove the data listener, then deterministically stop
|
||||||
|
// the stream so a subsequent 'error' cannot fire without a listener
|
||||||
|
// (which would terminate the process as an unhandled error event).
|
||||||
this.#inflate.removeAllListeners()
|
this.#inflate.removeAllListeners()
|
||||||
|
this.#inflate.destroy()
|
||||||
this.#inflate = null
|
this.#inflate = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+68
-24
@@ -52431,6 +52431,24 @@ const EXPANSION_MAX = 100_000;
|
|||||||
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
|
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
|
||||||
// characters) so legitimate input is unaffected.
|
// characters) so legitimate input is unaffected.
|
||||||
const EXPANSION_MAX_LENGTH = 4_000_000;
|
const EXPANSION_MAX_LENGTH = 4_000_000;
|
||||||
|
// `expand_` recurses once per level of brace *nesting* - both when expanding a
|
||||||
|
// set's comma members and when re-wrapping a set whose body is a single part.
|
||||||
|
// The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one
|
||||||
|
// level per chained group), which left nesting depth unbounded: about 3,100
|
||||||
|
// levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack
|
||||||
|
// and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser
|
||||||
|
// will follow nesting. It sits far above any realistic pattern and well below
|
||||||
|
// the depth at which the stack runs out.
|
||||||
|
const EXPANSION_MAX_DEPTH = 1_000;
|
||||||
|
// Bash keeps a quirk where a brace group followed by a comma set still expands
|
||||||
|
// (`{a},b}`). The parser implements it by rewriting the string and restarting
|
||||||
|
// the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n`
|
||||||
|
// full passes over a string that itself grows by one `escClose` sentinel each
|
||||||
|
// time - quadratic in `n`, with a ~26x constant from the sentinel's length.
|
||||||
|
// 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27
|
||||||
|
// seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many
|
||||||
|
// times the scan may restart. Real `{a},b}` input needs a handful.
|
||||||
|
const EXPANSION_MAX_REWRITES = 1_000;
|
||||||
function numeric(str) {
|
function numeric(str) {
|
||||||
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
|
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
|
||||||
}
|
}
|
||||||
@@ -52450,37 +52468,52 @@ function unescapeBraces(str) {
|
|||||||
.replace(escCommaPattern, ',')
|
.replace(escCommaPattern, ',')
|
||||||
.replace(escPeriodPattern, '.');
|
.replace(escPeriodPattern, '.');
|
||||||
}
|
}
|
||||||
|
// Like `target.push(...items)` but doesn't overflow the stack
|
||||||
|
function pushAll(target, items) {
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
target.push(items[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Basically just str.split(","), but handling cases
|
* Basically just str.split(","), but handling cases
|
||||||
* where we have nested braced sections, which should be
|
* where we have nested braced sections, which should be
|
||||||
* treated as individual members, like {a,{b,c},d}
|
* treated as individual members, like {a,{b,c},d}
|
||||||
*/
|
*/
|
||||||
function parseCommaParts(str) {
|
function parseCommaParts(str) {
|
||||||
if (!str) {
|
|
||||||
return [''];
|
|
||||||
}
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
const m = balanced('{', '}', str);
|
// Walk the brace groups iteratively. Recursing on `post` once per group let a
|
||||||
if (!m) {
|
// chain of them exhaust the stack - the parsing-side counterpart to
|
||||||
return str.split(',');
|
// the `expand_` overflow fixed for CVE-2026-14257, and not something `max` or
|
||||||
|
// `maxLength` can bound, since it happens before expansion.
|
||||||
|
//
|
||||||
|
// The part the next chunk continues
|
||||||
|
let carry = '';
|
||||||
|
for (;;) {
|
||||||
|
const m = balanced('{', '}', str);
|
||||||
|
if (!m) {
|
||||||
|
const tail = str.split(',');
|
||||||
|
tail[0] = carry + tail[0];
|
||||||
|
pushAll(parts, tail);
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
const { pre, body, post } = m;
|
||||||
|
const p = pre.split(',');
|
||||||
|
p[0] = carry + p[0];
|
||||||
|
p[p.length - 1] += '{' + body + '}';
|
||||||
|
if (!post.length) {
|
||||||
|
pushAll(parts, p);
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
carry = p.pop();
|
||||||
|
pushAll(parts, p);
|
||||||
|
str = post;
|
||||||
}
|
}
|
||||||
const { pre, body, post } = m;
|
|
||||||
const p = pre.split(',');
|
|
||||||
p[p.length - 1] += '{' + body + '}';
|
|
||||||
const postParts = parseCommaParts(post);
|
|
||||||
if (post.length) {
|
|
||||||
;
|
|
||||||
p[p.length - 1] += postParts.shift();
|
|
||||||
p.push.apply(p, postParts);
|
|
||||||
}
|
|
||||||
parts.push.apply(parts, p);
|
|
||||||
return parts;
|
|
||||||
}
|
}
|
||||||
function expand(str, options = {}) {
|
function expand(str, options = {}) {
|
||||||
if (!str) {
|
if (!str) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH } = options;
|
const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH, maxDepth = EXPANSION_MAX_DEPTH, maxRewrites = EXPANSION_MAX_REWRITES, } = options;
|
||||||
// I don't know why Bash 4.3 does this, but it does.
|
// I don't know why Bash 4.3 does this, but it does.
|
||||||
// Anything starting with {} will have the first two bytes preserved
|
// Anything starting with {} will have the first two bytes preserved
|
||||||
// but *only* at the top level, so {},a}b will not expand to anything,
|
// but *only* at the top level, so {},a}b will not expand to anything,
|
||||||
@@ -52490,7 +52523,7 @@ function expand(str, options = {}) {
|
|||||||
if (str.slice(0, 2) === '{}') {
|
if (str.slice(0, 2) === '{}') {
|
||||||
str = '\\{\\}' + str.slice(2);
|
str = '\\{\\}' + str.slice(2);
|
||||||
}
|
}
|
||||||
return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
|
return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
|
||||||
}
|
}
|
||||||
function embrace(str) {
|
function embrace(str) {
|
||||||
return '{' + str + '}';
|
return '{' + str + '}';
|
||||||
@@ -52585,7 +52618,13 @@ function expandSequence(body, isAlphaSequence, max, maxLength) {
|
|||||||
}
|
}
|
||||||
return N;
|
return N;
|
||||||
}
|
}
|
||||||
function expand_(str, max, maxLength, isTop) {
|
function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
|
||||||
|
// Too deeply nested to keep following: treat the rest as literal, the same
|
||||||
|
// way a group that cannot expand is already handled. Truncating rather than
|
||||||
|
// throwing keeps `expand` total, matching `max` and `maxLength`.
|
||||||
|
if (depth > maxDepth) {
|
||||||
|
return [str];
|
||||||
|
}
|
||||||
// Consume the string's top-level brace groups left to right, threading a
|
// Consume the string's top-level brace groups left to right, threading a
|
||||||
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
|
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
|
||||||
// rather than recursing on `m.post` once per group - keeps the native stack
|
// rather than recursing on `m.post` once per group - keeps the native stack
|
||||||
@@ -52597,6 +52636,9 @@ function expand_(str, max, maxLength, isTop) {
|
|||||||
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
|
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
|
||||||
// is on the final strings, so it is applied to whichever `combine` produces
|
// is on the final strings, so it is applied to whichever `combine` produces
|
||||||
// them (the one with no brace set left in the tail).
|
// them (the one with no brace set left in the tail).
|
||||||
|
// How many times the `{a},b}` rewrite below has restarted the scan. Each pass
|
||||||
|
// re-reads the whole string, so leaving this unbounded is quadratic.
|
||||||
|
let rewrites = 0;
|
||||||
let dropEmpties = false;
|
let dropEmpties = false;
|
||||||
let firstGroup = true;
|
let firstGroup = true;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -52621,7 +52663,8 @@ function expand_(str, max, maxLength, isTop) {
|
|||||||
const isOptions = m.body.indexOf(',') >= 0;
|
const isOptions = m.body.indexOf(',') >= 0;
|
||||||
if (!isSequence && !isOptions) {
|
if (!isSequence && !isOptions) {
|
||||||
// {a},b}
|
// {a},b}
|
||||||
if (m.post.match(/,(?!,).*\}/)) {
|
if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
|
||||||
|
rewrites++;
|
||||||
str = m.pre + '{' + m.body + escClose + m.post;
|
str = m.pre + '{' + m.body + escClose + m.post;
|
||||||
isTop = true;
|
isTop = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -52641,7 +52684,7 @@ function expand_(str, max, maxLength, isTop) {
|
|||||||
let n = parseCommaParts(m.body);
|
let n = parseCommaParts(m.body);
|
||||||
if (n.length === 1 && n[0] !== undefined) {
|
if (n.length === 1 && n[0] !== undefined) {
|
||||||
// x{{a,b}}y ==> x{a}y x{b}y
|
// x{{a,b}}y ==> x{a}y x{b}y
|
||||||
n = expand_(n[0], max, maxLength, false).map(embrace);
|
n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
|
||||||
//XXX is this necessary? Can't seem to hit it in tests.
|
//XXX is this necessary? Can't seem to hit it in tests.
|
||||||
/* c8 ignore start */
|
/* c8 ignore start */
|
||||||
if (n.length === 1) {
|
if (n.length === 1) {
|
||||||
@@ -52667,12 +52710,13 @@ function expand_(str, max, maxLength, isTop) {
|
|||||||
values = [];
|
values = [];
|
||||||
let valuesLength = 0;
|
let valuesLength = 0;
|
||||||
outer: for (let j = 0; j < n.length; j++) {
|
outer: for (let j = 0; j < n.length; j++) {
|
||||||
const expanded = expand_(n[j], max, maxLength, false);
|
const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
|
||||||
for (let k = 0; k < expanded.length; k++) {
|
for (let k = 0; k < expanded.length; k++) {
|
||||||
const v = expanded[k];
|
const v = expanded[k];
|
||||||
if (dropsEmpties && !v)
|
if (dropsEmpties && !v)
|
||||||
continue;
|
continue;
|
||||||
if (values.length >= max || valuesLength + v.length > maxLength) {
|
if (values.length >= max ||
|
||||||
|
valuesLength + v.length > maxLength) {
|
||||||
break outer;
|
break outer;
|
||||||
}
|
}
|
||||||
values.push(v);
|
values.push(v);
|
||||||
|
|||||||
Vendored
+453
-187
@@ -6009,11 +6009,77 @@ class Request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpgrade (statusCode, headers, socket) {
|
/**
|
||||||
|
* @param {number|null} statusCode
|
||||||
|
* @param {Buffer[]|null} headers
|
||||||
|
* @param {import('node:stream').Duplex} socket
|
||||||
|
* @param {string} [statusText]
|
||||||
|
*/
|
||||||
|
onUpgrade (statusCode, headers, socket, statusText = '') {
|
||||||
|
this.onFinally()
|
||||||
|
|
||||||
assert(!this.aborted)
|
assert(!this.aborted)
|
||||||
assert(!this.completed)
|
assert(!this.completed)
|
||||||
|
|
||||||
return this[kHandler].onUpgrade(statusCode, headers, socket)
|
if (statusCode !== null) {
|
||||||
|
this.#publishUpgradeHeaders(statusCode, headers, statusText)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = this[kHandler].onUpgrade(statusCode, headers, socket)
|
||||||
|
|
||||||
|
if (!this.aborted) {
|
||||||
|
this.completed = true
|
||||||
|
if (statusCode !== null) {
|
||||||
|
this.#publishUpgradeTrailers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} statusCode
|
||||||
|
* @param {import('node:http2').IncomingHttpHeaders} headers
|
||||||
|
* @param {(headers: import('node:http2').IncomingHttpHeaders) => Buffer[]} parseHeaders
|
||||||
|
* @param {string} [statusText]
|
||||||
|
*/
|
||||||
|
onUpgradeResponse (statusCode, headers, parseHeaders, statusText = '') {
|
||||||
|
assert(!this.aborted)
|
||||||
|
assert(this.completed)
|
||||||
|
|
||||||
|
if (channels.headers.hasSubscribers) {
|
||||||
|
this.#publishUpgradeHeaders(statusCode, parseHeaders(headers), statusText)
|
||||||
|
}
|
||||||
|
this.#publishUpgradeTrailers()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Error} error
|
||||||
|
*/
|
||||||
|
onUpgradeError (error) {
|
||||||
|
assert(!this.aborted)
|
||||||
|
assert(this.completed)
|
||||||
|
|
||||||
|
if (channels.error.hasSubscribers) {
|
||||||
|
channels.error.publish({ request: this, error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} statusCode
|
||||||
|
* @param {Buffer[]} headers
|
||||||
|
* @param {string} statusText
|
||||||
|
*/
|
||||||
|
#publishUpgradeHeaders (statusCode, headers, statusText) {
|
||||||
|
if (channels.headers.hasSubscribers) {
|
||||||
|
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#publishUpgradeTrailers () {
|
||||||
|
if (channels.trailers.hasSubscribers) {
|
||||||
|
channels.trailers.publish({ request: this, trailers: [] })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onComplete (trailers) {
|
onComplete (trailers) {
|
||||||
@@ -7912,7 +7978,7 @@ class Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onUpgrade (head) {
|
onUpgrade (head) {
|
||||||
const { upgrade, client, socket, headers, statusCode } = this
|
const { upgrade, client, socket, headers, statusCode, statusText } = this
|
||||||
|
|
||||||
assert(upgrade)
|
assert(upgrade)
|
||||||
assert(client[kSocket] === socket)
|
assert(client[kSocket] === socket)
|
||||||
@@ -7947,9 +8013,10 @@ class Parser {
|
|||||||
client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
|
client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
request.onUpgrade(statusCode, headers, socket)
|
request.onUpgrade(statusCode, headers, socket, statusText)
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
util.destroy(socket, err)
|
util.errorRequest(client, request, error)
|
||||||
|
util.destroy(socket, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
client[kResume]()
|
client[kResume]()
|
||||||
@@ -8356,7 +8423,7 @@ async function connectH1 (client, socket) {
|
|||||||
|
|
||||||
function clearIdleSocketValidation (socket) {
|
function clearIdleSocketValidation (socket) {
|
||||||
if (socket[kIdleSocketValidationTimeout]) {
|
if (socket[kIdleSocketValidationTimeout]) {
|
||||||
clearTimeout(socket[kIdleSocketValidationTimeout])
|
clearImmediate(socket[kIdleSocketValidationTimeout])
|
||||||
socket[kIdleSocketValidationTimeout] = null
|
socket[kIdleSocketValidationTimeout] = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8365,15 +8432,23 @@ function clearIdleSocketValidation (socket) {
|
|||||||
|
|
||||||
function scheduleIdleSocketValidation (client, socket) {
|
function scheduleIdleSocketValidation (client, socket) {
|
||||||
socket[kIdleSocketValidation] = 1
|
socket[kIdleSocketValidation] = 1
|
||||||
socket[kIdleSocketValidationTimeout] = setTimeout(() => {
|
// Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
|
||||||
|
// already pending on this idle keep-alive socket are processed before the
|
||||||
|
// next request is written (GHSA-35p6-xmwp-9g52).
|
||||||
|
//
|
||||||
|
// setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
|
||||||
|
// (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
|
||||||
|
// block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
|
||||||
|
// A ref'd Immediate both keeps the pending request alive and makes poll
|
||||||
|
// return immediately — the hybrid those issues asked for.
|
||||||
|
socket[kIdleSocketValidationTimeout] = setImmediate(() => {
|
||||||
socket[kIdleSocketValidationTimeout] = null
|
socket[kIdleSocketValidationTimeout] = null
|
||||||
socket[kIdleSocketValidation] = 2
|
socket[kIdleSocketValidation] = 2
|
||||||
|
|
||||||
if (client[kSocket] === socket && !socket.destroyed) {
|
if (client[kSocket] === socket && !socket.destroyed) {
|
||||||
client[kResume]()
|
client[kResume]()
|
||||||
}
|
}
|
||||||
}, 0)
|
})
|
||||||
socket[kIdleSocketValidationTimeout].unref?.()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8522,12 +8597,22 @@ function writeH1 (client, request) {
|
|||||||
const socket = client[kSocket]
|
const socket = client[kSocket]
|
||||||
clearIdleSocketValidation(socket)
|
clearIdleSocketValidation(socket)
|
||||||
|
|
||||||
const abort = (err) => {
|
/**
|
||||||
if (request.aborted || request.completed) {
|
* @param {Error} [error]
|
||||||
|
*/
|
||||||
|
const abort = (error) => {
|
||||||
|
if (request.aborted) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
util.errorRequest(client, request, err || new RequestAbortedError())
|
if (request.completed) {
|
||||||
|
if (request.upgrade || request.method === 'CONNECT') {
|
||||||
|
util.destroy(socket, new InformationalError('aborted'))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.errorRequest(client, request, error || new RequestAbortedError())
|
||||||
|
|
||||||
util.destroy(body)
|
util.destroy(body)
|
||||||
util.destroy(socket, new InformationalError('aborted'))
|
util.destroy(socket, new InformationalError('aborted'))
|
||||||
@@ -8984,6 +9069,7 @@ module.exports = connectH1
|
|||||||
|
|
||||||
|
|
||||||
const assert = __nccwpck_require__(4589)
|
const assert = __nccwpck_require__(4589)
|
||||||
|
const { errorMonitor } = __nccwpck_require__(8474)
|
||||||
const { pipeline } = __nccwpck_require__(7075)
|
const { pipeline } = __nccwpck_require__(7075)
|
||||||
const util = __nccwpck_require__(3440)
|
const util = __nccwpck_require__(3440)
|
||||||
const {
|
const {
|
||||||
@@ -9060,6 +9146,15 @@ function parseH2Headers (headers) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('node:http2').IncomingHttpHeaders} headers
|
||||||
|
* @returns {Buffer[]}
|
||||||
|
*/
|
||||||
|
function parseH2ResponseHeaders (headers) {
|
||||||
|
const { [HTTP2_HEADER_STATUS]: _statusCode, ...realHeaders } = headers
|
||||||
|
return parseH2Headers(realHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
async function connectH2 (client, socket) {
|
async function connectH2 (client, socket) {
|
||||||
client[kSocket] = socket
|
client[kSocket] = socket
|
||||||
|
|
||||||
@@ -9280,22 +9375,32 @@ function writeH2 (client, request) {
|
|||||||
headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
|
headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
|
||||||
headers[HTTP2_HEADER_METHOD] = method
|
headers[HTTP2_HEADER_METHOD] = method
|
||||||
|
|
||||||
const abort = (err) => {
|
/**
|
||||||
if (request.aborted || request.completed) {
|
* @param {Error} [error]
|
||||||
|
*/
|
||||||
|
const abort = (error) => {
|
||||||
|
if (request.aborted) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = err || new RequestAbortedError()
|
if (request.completed) {
|
||||||
|
if (method === 'CONNECT' && stream != null) {
|
||||||
|
util.destroy(stream, error || new RequestAbortedError())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
util.errorRequest(client, request, err)
|
error = error || new RequestAbortedError()
|
||||||
|
|
||||||
|
util.errorRequest(client, request, error)
|
||||||
|
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
util.destroy(stream, err)
|
util.destroy(stream, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We do not destroy the socket as we can continue using the session
|
// We do not destroy the socket as we can continue using the session
|
||||||
// the stream get's destroyed and the session remains to create new streams
|
// the stream get's destroyed and the session remains to create new streams
|
||||||
util.destroy(body, err)
|
util.destroy(body, error)
|
||||||
client[kQueue][client[kRunningIdx]++] = null
|
client[kQueue][client[kRunningIdx]++] = null
|
||||||
client[kResume]()
|
client[kResume]()
|
||||||
}
|
}
|
||||||
@@ -9314,25 +9419,57 @@ function writeH2 (client, request) {
|
|||||||
|
|
||||||
if (method === 'CONNECT') {
|
if (method === 'CONNECT') {
|
||||||
session.ref()
|
session.ref()
|
||||||
// We are already connected, streams are pending, first request
|
|
||||||
// will create a new stream. We trigger a request to create the stream and wait until
|
|
||||||
// `ready` event is triggered
|
|
||||||
// We disabled endStream to allow the user to write to the stream
|
// We disabled endStream to allow the user to write to the stream
|
||||||
stream = session.request(headers, { endStream: false, signal })
|
stream = session.request(headers, { endStream: false, signal })
|
||||||
|
let upgradeResponseFinished = false
|
||||||
|
|
||||||
if (stream.id && !stream.pending) {
|
/**
|
||||||
request.onUpgrade(null, null, stream)
|
* @param {import('node:http2').IncomingHttpHeaders} headers
|
||||||
++session[kOpenStreams]
|
*/
|
||||||
client[kQueue][client[kRunningIdx]++] = null
|
const onResponse = (headers) => {
|
||||||
} else {
|
upgradeResponseFinished = true
|
||||||
stream.once('ready', () => {
|
stream.off(errorMonitor, onUpgradeError)
|
||||||
request.onUpgrade(null, null, stream)
|
request.onUpgradeResponse(Number(headers[HTTP2_HEADER_STATUS]), headers, parseH2ResponseHeaders)
|
||||||
++session[kOpenStreams]
|
|
||||||
client[kQueue][client[kRunningIdx]++] = null
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Error} error
|
||||||
|
*/
|
||||||
|
const onUpgradeError = (error) => {
|
||||||
|
upgradeResponseFinished = true
|
||||||
|
stream.off('response', onResponse)
|
||||||
|
request.onUpgradeError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onReady = () => {
|
||||||
|
try {
|
||||||
|
request.onUpgrade(null, null, stream)
|
||||||
|
} catch (error) {
|
||||||
|
stream.off('response', onResponse)
|
||||||
|
abort(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.aborted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.off('error', abort)
|
||||||
|
stream.once(errorMonitor, onUpgradeError)
|
||||||
|
client[kQueue][client[kRunningIdx]++] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.once('response', onResponse)
|
||||||
|
stream.once('error', abort)
|
||||||
|
++session[kOpenStreams]
|
||||||
|
onReady()
|
||||||
|
|
||||||
stream.once('close', () => {
|
stream.once('close', () => {
|
||||||
|
if (!upgradeResponseFinished && request.completed) {
|
||||||
|
stream.off('response', onResponse)
|
||||||
|
stream.off(errorMonitor, onUpgradeError)
|
||||||
|
request.onUpgradeError(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`))
|
||||||
|
}
|
||||||
session[kOpenStreams] -= 1
|
session[kOpenStreams] -= 1
|
||||||
if (session[kOpenStreams] === 0) session.unref()
|
if (session[kOpenStreams] === 0) session.unref()
|
||||||
})
|
})
|
||||||
@@ -12031,6 +12168,7 @@ class RetryHandler {
|
|||||||
this.end = null
|
this.end = null
|
||||||
this.etag = null
|
this.etag = null
|
||||||
this.resume = null
|
this.resume = null
|
||||||
|
this.headersSent = false
|
||||||
|
|
||||||
// Handle possible onConnect duplication
|
// Handle possible onConnect duplication
|
||||||
this.handler.onConnect(reason => {
|
this.handler.onConnect(reason => {
|
||||||
@@ -12043,6 +12181,20 @@ class RetryHandler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkpointResponseEnd (headers, resume) {
|
||||||
|
if (this.end == null && this.opts.method !== 'HEAD') {
|
||||||
|
const contentLength = headers['content-length']
|
||||||
|
this.end = contentLength != null ? Number(contentLength) - 1 : null
|
||||||
|
|
||||||
|
assert(
|
||||||
|
this.end == null || Number.isFinite(this.end),
|
||||||
|
'invalid content-length'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resume = this.end != null ? resume : null
|
||||||
|
}
|
||||||
|
|
||||||
onRequestSent () {
|
onRequestSent () {
|
||||||
if (this.handler.onRequestSent) {
|
if (this.handler.onRequestSent) {
|
||||||
this.handler.onRequestSent()
|
this.handler.onRequestSent()
|
||||||
@@ -12131,7 +12283,12 @@ class RetryHandler {
|
|||||||
this.retryCount += 1
|
this.retryCount += 1
|
||||||
|
|
||||||
if (statusCode >= 300) {
|
if (statusCode >= 300) {
|
||||||
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
|
// Only expose a response if no earlier attempt has reached the caller.
|
||||||
|
// Otherwise abort this attempt so the error settles the existing body
|
||||||
|
// instead of replacing it with a new response.
|
||||||
|
if (!this.headersSent && this.retryOpts.statusCodes.includes(statusCode) === false) {
|
||||||
|
this.headersSent = true
|
||||||
|
this.checkpointResponseEnd(headers, resume)
|
||||||
return this.handler.onHeaders(
|
return this.handler.onHeaders(
|
||||||
statusCode,
|
statusCode,
|
||||||
rawHeaders,
|
rawHeaders,
|
||||||
@@ -12200,8 +12357,15 @@ class RetryHandler {
|
|||||||
|
|
||||||
const { start, size, end = size - 1 } = contentRange
|
const { start, size, end = size - 1 } = contentRange
|
||||||
|
|
||||||
assert(this.start === start, 'content-range mismatch')
|
if (this.start !== start || (this.end != null && this.end !== end)) {
|
||||||
assert(this.end == null || this.end === end, 'content-range mismatch')
|
this.abort(
|
||||||
|
new RequestRetryError('Content-Range mismatch', statusCode, {
|
||||||
|
headers,
|
||||||
|
data: { count: this.retryCount }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
this.resume = resume
|
this.resume = resume
|
||||||
return true
|
return true
|
||||||
@@ -12213,6 +12377,7 @@ class RetryHandler {
|
|||||||
const range = parseRangeHeader(headers['content-range'])
|
const range = parseRangeHeader(headers['content-range'])
|
||||||
|
|
||||||
if (range == null) {
|
if (range == null) {
|
||||||
|
this.headersSent = true
|
||||||
return this.handler.onHeaders(
|
return this.handler.onHeaders(
|
||||||
statusCode,
|
statusCode,
|
||||||
rawHeaders,
|
rawHeaders,
|
||||||
@@ -12251,6 +12416,7 @@ class RetryHandler {
|
|||||||
)
|
)
|
||||||
|
|
||||||
this.resume = resume
|
this.resume = resume
|
||||||
|
this.headersSent = true
|
||||||
this.etag = headers.etag != null ? headers.etag : null
|
this.etag = headers.etag != null ? headers.etag : null
|
||||||
|
|
||||||
// Weak etags are not useful for comparison nor cache
|
// Weak etags are not useful for comparison nor cache
|
||||||
@@ -12290,7 +12456,7 @@ class RetryHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onError (err) {
|
onError (err) {
|
||||||
if (this.aborted || isDisturbed(this.opts.body)) {
|
if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) {
|
||||||
return this.handler.onError(err)
|
return this.handler.onError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16748,6 +16914,49 @@ const COLON = 0x3A
|
|||||||
*/
|
*/
|
||||||
const SPACE = 0x20
|
const SPACE = 0x20
|
||||||
|
|
||||||
|
const DATA = Buffer.from('data')
|
||||||
|
const EVENT = Buffer.from('event')
|
||||||
|
const ID = Buffer.from('id')
|
||||||
|
const RETRY = Buffer.from('retry')
|
||||||
|
|
||||||
|
function isASCIINumberBytes (buffer, start) {
|
||||||
|
if (start >= buffer.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i < buffer.length; i++) {
|
||||||
|
if (buffer[i] < 0x30 || buffer[i] > 0x39) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidLastEventIdBytes (buffer, start) {
|
||||||
|
for (let i = start; i < buffer.length; i++) {
|
||||||
|
if (buffer[i] === 0x00) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFieldName (line, length, field) {
|
||||||
|
if (length !== field.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
if (line[i] !== field[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} EventSourceStreamEvent
|
* @typedef {object} EventSourceStreamEvent
|
||||||
* @type {object}
|
* @type {object}
|
||||||
@@ -16788,11 +16997,14 @@ class EventSourceStream extends Transform {
|
|||||||
eventEndCheck = false
|
eventEndCheck = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Buffer}
|
* @type {Buffer[]}
|
||||||
*/
|
*/
|
||||||
buffer = null
|
chunks = []
|
||||||
|
|
||||||
|
chunkIndex = 0
|
||||||
pos = 0
|
pos = 0
|
||||||
|
lineChunkIndex = 0
|
||||||
|
linePos = 0
|
||||||
|
|
||||||
event = {
|
event = {
|
||||||
data: undefined,
|
data: undefined,
|
||||||
@@ -16831,92 +17043,20 @@ class EventSourceStream extends Transform {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the chunk in the buffer, as the data might not be complete while
|
this.chunks.push(chunk)
|
||||||
// processing it
|
|
||||||
// TODO: Investigate if there is a more performant way to handle
|
|
||||||
// incoming chunks
|
|
||||||
// see: https://github.com/nodejs/undici/issues/2630
|
|
||||||
if (this.buffer) {
|
|
||||||
this.buffer = Buffer.concat([this.buffer, chunk])
|
|
||||||
} else {
|
|
||||||
this.buffer = chunk
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip leading byte-order-mark if we opened the stream and started
|
// Strip leading byte-order-mark if we opened the stream and started
|
||||||
// the processing of the incoming data
|
// the processing of the incoming data
|
||||||
if (this.checkBOM) {
|
if (this.checkBOM) {
|
||||||
switch (this.buffer.length) {
|
if (this.handleBOM()) {
|
||||||
case 1:
|
callback()
|
||||||
// Check if the first byte is the same as the first byte of the BOM
|
return
|
||||||
if (this.buffer[0] === BOM[0]) {
|
|
||||||
// If it is, we need to wait for more data
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Set the checkBOM flag to false as we don't need to check for the
|
|
||||||
// BOM anymore
|
|
||||||
this.checkBOM = false
|
|
||||||
|
|
||||||
// The buffer only contains one byte so we need to wait for more data
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
case 2:
|
|
||||||
// Check if the first two bytes are the same as the first two bytes
|
|
||||||
// of the BOM
|
|
||||||
if (
|
|
||||||
this.buffer[0] === BOM[0] &&
|
|
||||||
this.buffer[1] === BOM[1]
|
|
||||||
) {
|
|
||||||
// If it is, we need to wait for more data, because the third byte
|
|
||||||
// is needed to determine if it is the BOM or not
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the checkBOM flag to false as we don't need to check for the
|
|
||||||
// BOM anymore
|
|
||||||
this.checkBOM = false
|
|
||||||
break
|
|
||||||
case 3:
|
|
||||||
// Check if the first three bytes are the same as the first three
|
|
||||||
// bytes of the BOM
|
|
||||||
if (
|
|
||||||
this.buffer[0] === BOM[0] &&
|
|
||||||
this.buffer[1] === BOM[1] &&
|
|
||||||
this.buffer[2] === BOM[2]
|
|
||||||
) {
|
|
||||||
// If it is, we can drop the buffered data, as it is only the BOM
|
|
||||||
this.buffer = Buffer.alloc(0)
|
|
||||||
// Set the checkBOM flag to false as we don't need to check for the
|
|
||||||
// BOM anymore
|
|
||||||
this.checkBOM = false
|
|
||||||
|
|
||||||
// Await more data
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If it is not the BOM, we can start processing the data
|
|
||||||
this.checkBOM = false
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
// The buffer is longer than 3 bytes, so we can drop the BOM if it is
|
|
||||||
// present
|
|
||||||
if (
|
|
||||||
this.buffer[0] === BOM[0] &&
|
|
||||||
this.buffer[1] === BOM[1] &&
|
|
||||||
this.buffer[2] === BOM[2]
|
|
||||||
) {
|
|
||||||
// Remove the BOM from the buffer
|
|
||||||
this.buffer = this.buffer.subarray(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the checkBOM flag to false as we don't need to check for the
|
|
||||||
this.checkBOM = false
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (this.pos < this.buffer.length) {
|
while (this.hasCurrentByte()) {
|
||||||
|
const byte = this.currentByte()
|
||||||
|
|
||||||
// If the previous line ended with an end-of-line, we need to check
|
// If the previous line ended with an end-of-line, we need to check
|
||||||
// if the next character is also an end-of-line.
|
// if the next character is also an end-of-line.
|
||||||
if (this.eventEndCheck) {
|
if (this.eventEndCheck) {
|
||||||
@@ -16929,10 +17069,9 @@ class EventSourceStream extends Transform {
|
|||||||
if (this.crlfCheck) {
|
if (this.crlfCheck) {
|
||||||
// If the current character is a line feed, we can remove it
|
// If the current character is a line feed, we can remove it
|
||||||
// from the buffer and reset the crlfCheck flag
|
// from the buffer and reset the crlfCheck flag
|
||||||
if (this.buffer[this.pos] === LF) {
|
if (byte === LF) {
|
||||||
this.buffer = this.buffer.subarray(this.pos + 1)
|
|
||||||
this.pos = 0
|
|
||||||
this.crlfCheck = false
|
this.crlfCheck = false
|
||||||
|
this.consumeCurrentByte()
|
||||||
|
|
||||||
// It is possible that the line feed is not the end of the
|
// It is possible that the line feed is not the end of the
|
||||||
// event. We need to check if the next character is an
|
// event. We need to check if the next character is an
|
||||||
@@ -16948,19 +17087,17 @@ class EventSourceStream extends Transform {
|
|||||||
this.crlfCheck = false
|
this.crlfCheck = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
|
if (byte === LF || byte === CR) {
|
||||||
// If the current character is a carriage return, we need to
|
// If the current character is a carriage return, we need to
|
||||||
// set the crlfCheck flag to true, as we need to check if the
|
// set the crlfCheck flag to true, as we need to check if the
|
||||||
// next character is a line feed so we can remove it from the
|
// next character is a line feed so we can remove it from the
|
||||||
// buffer
|
// buffer
|
||||||
if (this.buffer[this.pos] === CR) {
|
if (byte === CR) {
|
||||||
this.crlfCheck = true
|
this.crlfCheck = true
|
||||||
}
|
}
|
||||||
|
|
||||||
this.buffer = this.buffer.subarray(this.pos + 1)
|
this.consumeCurrentByte()
|
||||||
this.pos = 0
|
if (this.hasPendingEvent()) {
|
||||||
if (
|
|
||||||
this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
|
|
||||||
this.processEvent(this.event)
|
this.processEvent(this.event)
|
||||||
}
|
}
|
||||||
this.clearEvent()
|
this.clearEvent()
|
||||||
@@ -16974,22 +17111,18 @@ class EventSourceStream extends Transform {
|
|||||||
|
|
||||||
// If the current character is an end-of-line, we can process the
|
// If the current character is an end-of-line, we can process the
|
||||||
// line
|
// line
|
||||||
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
|
if (byte === LF || byte === CR) {
|
||||||
// If the current character is a carriage return, we need to
|
// If the current character is a carriage return, we need to
|
||||||
// set the crlfCheck flag to true, as we need to check if the
|
// set the crlfCheck flag to true, as we need to check if the
|
||||||
// next character is a line feed
|
// next character is a line feed
|
||||||
if (this.buffer[this.pos] === CR) {
|
if (byte === CR) {
|
||||||
this.crlfCheck = true
|
this.crlfCheck = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// In any case, we can process the line as we reached an
|
// In any case, we can process the line as we reached an
|
||||||
// end-of-line character
|
// end-of-line character
|
||||||
this.parseLine(this.buffer.subarray(0, this.pos), this.event)
|
this.parseLine(this.readLine(), this.event)
|
||||||
|
this.consumeCurrentByte()
|
||||||
// Remove the processed line from the buffer
|
|
||||||
this.buffer = this.buffer.subarray(this.pos + 1)
|
|
||||||
// Reset the position as we removed the processed line from the buffer
|
|
||||||
this.pos = 0
|
|
||||||
// A line was processed and this could be the end of the event. We need
|
// A line was processed and this could be the end of the event. We need
|
||||||
// to check if the next line is empty to determine if the event is
|
// to check if the next line is empty to determine if the event is
|
||||||
// finished.
|
// finished.
|
||||||
@@ -16997,7 +17130,7 @@ class EventSourceStream extends Transform {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pos++
|
this.advanceCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
callback()
|
callback()
|
||||||
@@ -17022,64 +17155,53 @@ class EventSourceStream extends Transform {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let field = ''
|
let fieldLength = line.length
|
||||||
let value = ''
|
let valueStart = line.length
|
||||||
|
|
||||||
// If the line contains a U+003A COLON character (:)
|
// If the line contains a U+003A COLON character (:)
|
||||||
if (colonPosition !== -1) {
|
if (colonPosition !== -1) {
|
||||||
// Collect the characters on the line before the first U+003A COLON
|
fieldLength = colonPosition
|
||||||
// character (:), and let field be that string.
|
|
||||||
// TODO: Investigate if there is a more performant way to extract the
|
|
||||||
// field
|
|
||||||
// see: https://github.com/nodejs/undici/issues/2630
|
|
||||||
field = line.subarray(0, colonPosition).toString('utf8')
|
|
||||||
|
|
||||||
// Collect the characters on the line after the first U+003A COLON
|
// Collect the characters on the line after the first U+003A COLON
|
||||||
// character (:), and let value be that string.
|
// character (:), and let value be that string.
|
||||||
// If value starts with a U+0020 SPACE character, remove it from value.
|
// If value starts with a U+0020 SPACE character, remove it from value.
|
||||||
let valueStart = colonPosition + 1
|
valueStart = colonPosition + 1
|
||||||
if (line[valueStart] === SPACE) {
|
if (line[valueStart] === SPACE) {
|
||||||
++valueStart
|
++valueStart
|
||||||
}
|
}
|
||||||
// TODO: Investigate if there is a more performant way to extract the
|
|
||||||
// value
|
|
||||||
// see: https://github.com/nodejs/undici/issues/2630
|
|
||||||
value = line.subarray(valueStart).toString('utf8')
|
|
||||||
|
|
||||||
// Otherwise, the string is not empty but does not contain a U+003A COLON
|
|
||||||
// character (:)
|
|
||||||
} else {
|
|
||||||
// Process the field using the steps described below, using the whole
|
|
||||||
// line as the field name, and the empty string as the field value.
|
|
||||||
field = line.toString('utf8')
|
|
||||||
value = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modify the event with the field name and value. The value is also
|
if (isFieldName(line, fieldLength, DATA)) {
|
||||||
// decoded as UTF-8
|
const value = line.toString('utf8', valueStart)
|
||||||
switch (field) {
|
|
||||||
case 'data':
|
if (event.data === undefined) {
|
||||||
if (event[field] === undefined) {
|
event.data = value
|
||||||
event[field] = value
|
} else {
|
||||||
} else {
|
event.data += `\n${value}`
|
||||||
event[field] += `\n${value}`
|
}
|
||||||
}
|
return
|
||||||
break
|
}
|
||||||
case 'retry':
|
|
||||||
if (isASCIINumber(value)) {
|
if (isFieldName(line, fieldLength, RETRY)) {
|
||||||
event[field] = value
|
if (isASCIINumberBytes(line, valueStart)) {
|
||||||
}
|
event.retry = line.toString('utf8', valueStart)
|
||||||
break
|
}
|
||||||
case 'id':
|
return
|
||||||
if (isValidLastEventId(value)) {
|
}
|
||||||
event[field] = value
|
|
||||||
}
|
if (isFieldName(line, fieldLength, ID)) {
|
||||||
break
|
if (isValidLastEventIdBytes(line, valueStart)) {
|
||||||
case 'event':
|
event.id = line.toString('utf8', valueStart)
|
||||||
if (value.length > 0) {
|
}
|
||||||
event[field] = value
|
return
|
||||||
}
|
}
|
||||||
break
|
|
||||||
|
if (isFieldName(line, fieldLength, EVENT)) {
|
||||||
|
const value = line.toString('utf8', valueStart)
|
||||||
|
|
||||||
|
if (value.length > 0) {
|
||||||
|
event.event = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17109,13 +17231,152 @@ class EventSourceStream extends Transform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearEvent () {
|
clearEvent () {
|
||||||
this.event = {
|
this.event.data = undefined
|
||||||
data: undefined,
|
this.event.event = undefined
|
||||||
event: undefined,
|
this.event.id = undefined
|
||||||
id: undefined,
|
this.event.retry = undefined
|
||||||
retry: undefined
|
}
|
||||||
|
|
||||||
|
hasPendingEvent () {
|
||||||
|
return this.event.data !== undefined ||
|
||||||
|
this.event.event !== undefined ||
|
||||||
|
this.event.id !== undefined ||
|
||||||
|
this.event.retry !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCurrentByte () {
|
||||||
|
return this.chunkIndex < this.chunks.length &&
|
||||||
|
this.pos < this.chunks[this.chunkIndex].length
|
||||||
|
}
|
||||||
|
|
||||||
|
currentByte () {
|
||||||
|
return this.chunks[this.chunkIndex][this.pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeCurrentByte () {
|
||||||
|
this.advanceCursor()
|
||||||
|
this.syncLineStartToCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
advanceCursor () {
|
||||||
|
this.pos++
|
||||||
|
|
||||||
|
while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) {
|
||||||
|
this.chunkIndex++
|
||||||
|
this.pos = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncLineStartToCursor () {
|
||||||
|
this.lineChunkIndex = this.chunkIndex
|
||||||
|
this.linePos = this.pos
|
||||||
|
this.dropConsumedChunks()
|
||||||
|
}
|
||||||
|
|
||||||
|
dropConsumedChunks () {
|
||||||
|
while (this.lineChunkIndex > 0) {
|
||||||
|
this.chunks.shift()
|
||||||
|
this.lineChunkIndex--
|
||||||
|
this.chunkIndex--
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.chunkIndex === this.chunks.length) {
|
||||||
|
this.chunks.length = 0
|
||||||
|
this.chunkIndex = 0
|
||||||
|
this.pos = 0
|
||||||
|
this.lineChunkIndex = 0
|
||||||
|
this.linePos = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readLine () {
|
||||||
|
if (this.lineChunkIndex === this.chunkIndex) {
|
||||||
|
return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = []
|
||||||
|
let length = 0
|
||||||
|
|
||||||
|
for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) {
|
||||||
|
const chunk = this.chunks[i]
|
||||||
|
const start = i === this.lineChunkIndex ? this.linePos : 0
|
||||||
|
const end = i === this.chunkIndex ? this.pos : chunk.length
|
||||||
|
const slice = chunk.subarray(start, end)
|
||||||
|
length += slice.length
|
||||||
|
chunks.push(slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.concat(chunks, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
peekBufferedByte (offset) {
|
||||||
|
let chunkIndex = this.lineChunkIndex
|
||||||
|
let pos = this.linePos
|
||||||
|
|
||||||
|
while (chunkIndex < this.chunks.length) {
|
||||||
|
const chunk = this.chunks[chunkIndex]
|
||||||
|
const remaining = chunk.length - pos
|
||||||
|
|
||||||
|
if (offset < remaining) {
|
||||||
|
return chunk[pos + offset]
|
||||||
|
}
|
||||||
|
|
||||||
|
offset -= remaining
|
||||||
|
chunkIndex++
|
||||||
|
pos = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
discardLeadingBytes (count) {
|
||||||
|
while (count > 0 && this.lineChunkIndex < this.chunks.length) {
|
||||||
|
const chunk = this.chunks[this.lineChunkIndex]
|
||||||
|
const remaining = chunk.length - this.linePos
|
||||||
|
|
||||||
|
if (count < remaining) {
|
||||||
|
this.linePos += count
|
||||||
|
count = 0
|
||||||
|
} else {
|
||||||
|
count -= remaining
|
||||||
|
this.lineChunkIndex++
|
||||||
|
this.linePos = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chunkIndex = this.lineChunkIndex
|
||||||
|
this.pos = this.linePos
|
||||||
|
this.dropConsumedChunks()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBOM () {
|
||||||
|
const first = this.peekBufferedByte(0)
|
||||||
|
const second = this.peekBufferedByte(1)
|
||||||
|
const third = this.peekBufferedByte(2)
|
||||||
|
|
||||||
|
if (second === undefined) {
|
||||||
|
if (first === BOM[0]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkBOM = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (third === undefined) {
|
||||||
|
if (first === BOM[0] && second === BOM[1]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkBOM = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first === BOM[0] && second === BOM[1] && third === BOM[2]) {
|
||||||
|
this.discardLeadingBytes(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkBOM = false
|
||||||
|
return !this.hasCurrentByte()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -28383,7 +28644,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
|
|||||||
// is specified, the server needs to include the same field and one of
|
// is specified, the server needs to include the same field and one of
|
||||||
// the selected subprotocol values in its response for the connection to
|
// the selected subprotocol values in its response for the connection to
|
||||||
// be established.
|
// be established.
|
||||||
if (!requestProtocols.includes(secProtocol)) {
|
if (requestProtocols === null || !requestProtocols.includes(secProtocol)) {
|
||||||
failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
|
failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -29144,7 +29405,12 @@ class PerMessageDeflate {
|
|||||||
|
|
||||||
if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
|
if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
|
||||||
callback(new MessageSizeExceededError())
|
callback(new MessageSizeExceededError())
|
||||||
|
// The inflater may still hold buffered input that can emit a late
|
||||||
|
// zlib error. Remove the data listener, then deterministically stop
|
||||||
|
// the stream so a subsequent 'error' cannot fire without a listener
|
||||||
|
// (which would terminate the process as an unhandled error event).
|
||||||
this.#inflate.removeAllListeners()
|
this.#inflate.removeAllListeners()
|
||||||
|
this.#inflate.destroy()
|
||||||
this.#inflate = null
|
this.#inflate = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6
-6
@@ -2769,9 +2769,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
"resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-5.0.12.tgz",
|
||||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^4.0.2"
|
"balanced-match": "^4.0.2"
|
||||||
@@ -6020,9 +6020,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.28.0",
|
"version": "6.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz",
|
||||||
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
|
"integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">=18.17"
|
||||||
|
|||||||
Reference in New Issue
Block a user