mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-08-14 12:40:29 +07:00
node_modules: update (#307)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
+10
-10
@@ -58,15 +58,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.6",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^4.0.2"
|
"balanced-match": "^4.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18 || 20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
@@ -94,9 +94,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemailer": {
|
"node_modules/nodemailer": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz",
|
||||||
"integrity": "sha512-tbPTid7d/p9jAA8CRZ3iomvrMaST0o6NYuY7v6JQZHpPRZ61mLFSPKYd7342NtOFuej9/+L48SOIxwfu2uDvtw==",
|
"integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
|
||||||
"license": "MIT-0",
|
"license": "MIT-0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@@ -128,9 +128,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.24.1",
|
"version": "6.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
|
||||||
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
|
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">=18.17"
|
||||||
|
|||||||
+11
@@ -64,6 +64,17 @@ const expansions = expand('{1..100}'.repeat(5), {
|
|||||||
// expansions.length will be 100, not 100^5
|
// expansions.length will be 100, not 100^5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `options` object can also provide a `maxLength` value to cap the
|
||||||
|
total number of characters across all expansions. This is limited to
|
||||||
|
`4_000_000` by default, to prevent memory exhaustion from inputs whose
|
||||||
|
result count stays under `max` while each result grows very long.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const expansions = expand('{a,b}'.repeat(1500), {
|
||||||
|
maxLength: 10_000,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
Valid expansions are:
|
Valid expansions are:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
+2
@@ -1,6 +1,8 @@
|
|||||||
export declare const EXPANSION_MAX = 100000;
|
export declare const EXPANSION_MAX = 100000;
|
||||||
|
export declare const EXPANSION_MAX_LENGTH = 4000000;
|
||||||
export type BraceExpansionOptions = {
|
export type BraceExpansionOptions = {
|
||||||
max?: number;
|
max?: number;
|
||||||
|
maxLength?: number;
|
||||||
};
|
};
|
||||||
export declare function expand(str: string, options?: BraceExpansionOptions): string[];
|
export declare function expand(str: string, options?: BraceExpansionOptions): string[];
|
||||||
//# sourceMappingURL=index.d.ts.map
|
//# sourceMappingURL=index.d.ts.map
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAwDpC,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YAkBtE"}
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAYpC,eAAO,MAAM,oBAAoB,UAAY,CAAA;AAwD7C,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YAkBtE"}
|
||||||
+167
-79
@@ -1,6 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.EXPANSION_MAX = void 0;
|
exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
|
||||||
exports.expand = expand;
|
exports.expand = expand;
|
||||||
const balanced_match_1 = require("balanced-match");
|
const balanced_match_1 = require("balanced-match");
|
||||||
const escSlash = '\0SLASH' + Math.random() + '\0';
|
const escSlash = '\0SLASH' + Math.random() + '\0';
|
||||||
@@ -19,6 +19,17 @@ const closePattern = /\\}/g;
|
|||||||
const commaPattern = /\\,/g;
|
const commaPattern = /\\,/g;
|
||||||
const periodPattern = /\\\./g;
|
const periodPattern = /\\\./g;
|
||||||
exports.EXPANSION_MAX = 100_000;
|
exports.EXPANSION_MAX = 100_000;
|
||||||
|
// `EXPANSION_MAX` caps the *number* of expansions, but not their length. An
|
||||||
|
// input like `'{a,b}'.repeat(1500)` stays under that count - its output is
|
||||||
|
// truncated to 100k results - while making every result ~1500 characters
|
||||||
|
// long. The result set, and the intermediate arrays built while combining
|
||||||
|
// brace sets, then grow large enough to exhaust memory and crash the process
|
||||||
|
// (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of
|
||||||
|
// characters the accumulator may hold at any point, so memory stays flat no
|
||||||
|
// matter how many brace groups are chained. The limit sits well above any
|
||||||
|
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
|
||||||
|
// characters) so legitimate input is unaffected.
|
||||||
|
exports.EXPANSION_MAX_LENGTH = 4_000_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);
|
||||||
}
|
}
|
||||||
@@ -68,7 +79,7 @@ function expand(str, options = {}) {
|
|||||||
if (!str) {
|
if (!str) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const { max = exports.EXPANSION_MAX } = options;
|
const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH } = 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,
|
||||||
@@ -78,7 +89,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, true).map(unescapeBraces);
|
return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
|
||||||
}
|
}
|
||||||
function embrace(str) {
|
function embrace(str) {
|
||||||
return '{' + str + '}';
|
return '{' + str + '}';
|
||||||
@@ -92,22 +103,117 @@ function lte(i, y) {
|
|||||||
function gte(i, y) {
|
function gte(i, y) {
|
||||||
return i >= y;
|
return i >= y;
|
||||||
}
|
}
|
||||||
function expand_(str, max, isTop) {
|
// Build `{ acc[a] + pre + values[v] }` for every combination, capping the
|
||||||
/** @type {string[]} */
|
// number of results at `max` and the total number of characters at `maxLength`.
|
||||||
const expansions = [];
|
// This is the one place output grows, so bounding it here keeps the single
|
||||||
const m = (0, balanced_match_1.balanced)('{', '}', str);
|
// accumulator - and therefore memory - flat regardless of how many brace groups
|
||||||
if (!m)
|
// are combined (CVE-2026-14257).
|
||||||
return [str];
|
function combine(acc, pre, values, max, maxLength, dropEmpties) {
|
||||||
// no need to expand pre, since it is guaranteed to be free of brace-sets
|
const out = [];
|
||||||
const pre = m.pre;
|
let length = 0;
|
||||||
const post = m.post.length ? expand_(m.post, max, false) : [''];
|
for (let a = 0; a < acc.length; a++) {
|
||||||
if (/\$$/.test(m.pre)) {
|
for (let v = 0; v < values.length; v++) {
|
||||||
for (let k = 0; k < post.length && k < max; k++) {
|
if (out.length >= max)
|
||||||
const expansion = pre + '{' + m.body + '}' + post[k];
|
return out;
|
||||||
expansions.push(expansion);
|
const expansion = acc[a] + pre + values[v];
|
||||||
|
// Bash drops empty results at the top level. Skip them before they count
|
||||||
|
// against `max`, so `max` bounds the number of *kept* results.
|
||||||
|
if (dropEmpties && !expansion)
|
||||||
|
continue;
|
||||||
|
if (length + expansion.length > maxLength)
|
||||||
|
return out;
|
||||||
|
out.push(expansion);
|
||||||
|
length += expansion.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
return out;
|
||||||
|
}
|
||||||
|
// The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
|
||||||
|
// sequence body.
|
||||||
|
function expandSequence(body, isAlphaSequence, max, maxLength) {
|
||||||
|
const n = body.split(/\.\./);
|
||||||
|
const N = [];
|
||||||
|
// A sequence body always splits into two or three parts, but the compiler
|
||||||
|
// can't know that.
|
||||||
|
/* c8 ignore start */
|
||||||
|
if (n[0] === undefined || n[1] === undefined) {
|
||||||
|
return N;
|
||||||
|
}
|
||||||
|
/* c8 ignore stop */
|
||||||
|
const x = numeric(n[0]);
|
||||||
|
const y = numeric(n[1]);
|
||||||
|
const width = Math.max(n[0].length, n[1].length);
|
||||||
|
let incr = n.length === 3 && n[2] !== undefined ?
|
||||||
|
Math.max(Math.abs(numeric(n[2])), 1)
|
||||||
|
: 1;
|
||||||
|
let test = lte;
|
||||||
|
const reverse = y < x;
|
||||||
|
if (reverse) {
|
||||||
|
incr *= -1;
|
||||||
|
test = gte;
|
||||||
|
}
|
||||||
|
const pad = n.some(isPadded);
|
||||||
|
let length = 0;
|
||||||
|
for (let i = x; test(i, y) && N.length < max; i += incr) {
|
||||||
|
let c;
|
||||||
|
if (isAlphaSequence) {
|
||||||
|
c = String.fromCharCode(i);
|
||||||
|
if (c === '\\') {
|
||||||
|
c = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
c = String(i);
|
||||||
|
if (pad) {
|
||||||
|
const need = width - c.length;
|
||||||
|
if (need > 0) {
|
||||||
|
const z = new Array(need + 1).join('0');
|
||||||
|
if (i < 0) {
|
||||||
|
c = '-' + z + c.slice(1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
c = z + c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (length + c.length > maxLength)
|
||||||
|
break;
|
||||||
|
N.push(c);
|
||||||
|
length += c.length;
|
||||||
|
}
|
||||||
|
return N;
|
||||||
|
}
|
||||||
|
function expand_(str, max, maxLength, isTop) {
|
||||||
|
// Consume the string's top-level brace groups left to right, threading a
|
||||||
|
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
|
||||||
|
// rather than recursing on `m.post` once per group - keeps the native stack
|
||||||
|
// depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no
|
||||||
|
// longer overflow the stack, and leaves a single accumulator whose size
|
||||||
|
// `maxLength` bounds directly (CVE-2026-14257).
|
||||||
|
let acc = [''];
|
||||||
|
// Bash drops empty results, but only when the *first* top-level group is a
|
||||||
|
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
|
||||||
|
// is on the final strings, so it is applied to whichever `combine` produces
|
||||||
|
// them (the one with no brace set left in the tail).
|
||||||
|
let dropEmpties = false;
|
||||||
|
let firstGroup = true;
|
||||||
|
for (;;) {
|
||||||
|
const m = (0, balanced_match_1.balanced)('{', '}', str);
|
||||||
|
// No brace set left: the rest of the string is literal.
|
||||||
|
if (!m) {
|
||||||
|
return combine(acc, str, [''], max, maxLength, dropEmpties);
|
||||||
|
}
|
||||||
|
// no need to expand pre, since it is guaranteed to be free of brace-sets
|
||||||
|
const pre = m.pre;
|
||||||
|
if (/\$$/.test(pre)) {
|
||||||
|
acc = combine(acc, pre + '{' + m.body + '}', [''], max, maxLength, dropEmpties && !m.post.length);
|
||||||
|
firstGroup = false;
|
||||||
|
if (!m.post.length)
|
||||||
|
break;
|
||||||
|
str = m.post;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
|
const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
|
||||||
const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
|
const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
|
||||||
const isSequence = isNumericSequence || isAlphaSequence;
|
const isSequence = isNumericSequence || isAlphaSequence;
|
||||||
@@ -116,86 +222,68 @@ function expand_(str, max, isTop) {
|
|||||||
// {a},b}
|
// {a},b}
|
||||||
if (m.post.match(/,(?!,).*\}/)) {
|
if (m.post.match(/,(?!,).*\}/)) {
|
||||||
str = m.pre + '{' + m.body + escClose + m.post;
|
str = m.pre + '{' + m.body + escClose + m.post;
|
||||||
return expand_(str, max, true);
|
isTop = true;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
return [str];
|
// Nothing here expands, so the whole remaining string is literal.
|
||||||
|
return combine(acc, pre + '{' + m.body + '}' + m.post, [''], max, maxLength, dropEmpties);
|
||||||
}
|
}
|
||||||
let n;
|
if (firstGroup) {
|
||||||
|
dropEmpties = isTop && !isSequence;
|
||||||
|
firstGroup = false;
|
||||||
|
}
|
||||||
|
let values;
|
||||||
if (isSequence) {
|
if (isSequence) {
|
||||||
n = m.body.split(/\.\./);
|
values = expandSequence(m.body, isAlphaSequence, max, maxLength);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
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, false).map(embrace);
|
n = expand_(n[0], max, maxLength, 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) {
|
||||||
return post.map(p => m.pre + n[0] + p);
|
acc = combine(acc, pre + n[0], [''], max, maxLength, dropEmpties && !m.post.length);
|
||||||
|
if (!m.post.length)
|
||||||
|
break;
|
||||||
|
str = m.post;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
/* c8 ignore stop */
|
/* c8 ignore stop */
|
||||||
}
|
}
|
||||||
}
|
// Values that `combine` is going to drop as empty produce no result, so
|
||||||
// at this point, n is the parts, and we know it's not a comma set
|
// they must not count against `max` - otherwise `{a,,b}` with `max: 2`
|
||||||
// with a single entry.
|
// would stop at `['a', '']` and yield one result instead of two. Skipping
|
||||||
let N;
|
// them outright keeps `values` bounded while leaving `max` a bound on
|
||||||
if (isSequence && n[0] !== undefined && n[1] !== undefined) {
|
// *kept* results.
|
||||||
const x = numeric(n[0]);
|
let dropsEmpties = dropEmpties && !m.post.length && !pre;
|
||||||
const y = numeric(n[1]);
|
for (let d = 0; dropsEmpties && d < acc.length; d++) {
|
||||||
const width = Math.max(n[0].length, n[1].length);
|
if (acc[d]) {
|
||||||
let incr = n.length === 3 && n[2] !== undefined ?
|
dropsEmpties = false;
|
||||||
Math.max(Math.abs(numeric(n[2])), 1)
|
}
|
||||||
: 1;
|
|
||||||
let test = lte;
|
|
||||||
const reverse = y < x;
|
|
||||||
if (reverse) {
|
|
||||||
incr *= -1;
|
|
||||||
test = gte;
|
|
||||||
}
|
}
|
||||||
const pad = n.some(isPadded);
|
values = [];
|
||||||
N = [];
|
let valuesLength = 0;
|
||||||
for (let i = x; test(i, y) && N.length < max; i += incr) {
|
outer: for (let j = 0; j < n.length; j++) {
|
||||||
let c;
|
const expanded = expand_(n[j], max, maxLength, false);
|
||||||
if (isAlphaSequence) {
|
for (let k = 0; k < expanded.length; k++) {
|
||||||
c = String.fromCharCode(i);
|
const v = expanded[k];
|
||||||
if (c === '\\') {
|
if (dropsEmpties && !v)
|
||||||
c = '';
|
continue;
|
||||||
|
if (values.length >= max || valuesLength + v.length > maxLength) {
|
||||||
|
break outer;
|
||||||
}
|
}
|
||||||
}
|
values.push(v);
|
||||||
else {
|
valuesLength += v.length;
|
||||||
c = String(i);
|
|
||||||
if (pad) {
|
|
||||||
const need = width - c.length;
|
|
||||||
if (need > 0) {
|
|
||||||
const z = new Array(need + 1).join('0');
|
|
||||||
if (i < 0) {
|
|
||||||
c = '-' + z + c.slice(1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
c = z + c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
N.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
N = [];
|
|
||||||
for (let j = 0; j < n.length; j++) {
|
|
||||||
N.push.apply(N, expand_(n[j], max, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let j = 0; j < N.length; j++) {
|
|
||||||
for (let k = 0; k < post.length && expansions.length < max; k++) {
|
|
||||||
const expansion = pre + N[j] + post[k];
|
|
||||||
if (!isTop || isSequence || expansion) {
|
|
||||||
expansions.push(expansion);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length);
|
||||||
|
if (!m.post.length)
|
||||||
|
break;
|
||||||
|
str = m.post;
|
||||||
}
|
}
|
||||||
return expansions;
|
return acc;
|
||||||
}
|
}
|
||||||
//# sourceMappingURL=index.js.map
|
//# sourceMappingURL=index.js.map
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+2
@@ -1,6 +1,8 @@
|
|||||||
export declare const EXPANSION_MAX = 100000;
|
export declare const EXPANSION_MAX = 100000;
|
||||||
|
export declare const EXPANSION_MAX_LENGTH = 4000000;
|
||||||
export type BraceExpansionOptions = {
|
export type BraceExpansionOptions = {
|
||||||
max?: number;
|
max?: number;
|
||||||
|
maxLength?: number;
|
||||||
};
|
};
|
||||||
export declare function expand(str: string, options?: BraceExpansionOptions): string[];
|
export declare function expand(str: string, options?: BraceExpansionOptions): string[];
|
||||||
//# sourceMappingURL=index.d.ts.map
|
//# sourceMappingURL=index.d.ts.map
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAwDpC,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YAkBtE"}
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAYpC,eAAO,MAAM,oBAAoB,UAAY,CAAA;AAwD7C,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YAkBtE"}
|
||||||
+166
-78
@@ -15,6 +15,17 @@ const closePattern = /\\}/g;
|
|||||||
const commaPattern = /\\,/g;
|
const commaPattern = /\\,/g;
|
||||||
const periodPattern = /\\\./g;
|
const periodPattern = /\\\./g;
|
||||||
export const EXPANSION_MAX = 100_000;
|
export const EXPANSION_MAX = 100_000;
|
||||||
|
// `EXPANSION_MAX` caps the *number* of expansions, but not their length. An
|
||||||
|
// input like `'{a,b}'.repeat(1500)` stays under that count - its output is
|
||||||
|
// truncated to 100k results - while making every result ~1500 characters
|
||||||
|
// long. The result set, and the intermediate arrays built while combining
|
||||||
|
// brace sets, then grow large enough to exhaust memory and crash the process
|
||||||
|
// (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of
|
||||||
|
// characters the accumulator may hold at any point, so memory stays flat no
|
||||||
|
// matter how many brace groups are chained. The limit sits well above any
|
||||||
|
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
|
||||||
|
// characters) so legitimate input is unaffected.
|
||||||
|
export const EXPANSION_MAX_LENGTH = 4_000_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);
|
||||||
}
|
}
|
||||||
@@ -64,7 +75,7 @@ export function expand(str, options = {}) {
|
|||||||
if (!str) {
|
if (!str) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const { max = EXPANSION_MAX } = options;
|
const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH } = 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,
|
||||||
@@ -74,7 +85,7 @@ export 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, true).map(unescapeBraces);
|
return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
|
||||||
}
|
}
|
||||||
function embrace(str) {
|
function embrace(str) {
|
||||||
return '{' + str + '}';
|
return '{' + str + '}';
|
||||||
@@ -88,22 +99,117 @@ function lte(i, y) {
|
|||||||
function gte(i, y) {
|
function gte(i, y) {
|
||||||
return i >= y;
|
return i >= y;
|
||||||
}
|
}
|
||||||
function expand_(str, max, isTop) {
|
// Build `{ acc[a] + pre + values[v] }` for every combination, capping the
|
||||||
/** @type {string[]} */
|
// number of results at `max` and the total number of characters at `maxLength`.
|
||||||
const expansions = [];
|
// This is the one place output grows, so bounding it here keeps the single
|
||||||
const m = balanced('{', '}', str);
|
// accumulator - and therefore memory - flat regardless of how many brace groups
|
||||||
if (!m)
|
// are combined (CVE-2026-14257).
|
||||||
return [str];
|
function combine(acc, pre, values, max, maxLength, dropEmpties) {
|
||||||
// no need to expand pre, since it is guaranteed to be free of brace-sets
|
const out = [];
|
||||||
const pre = m.pre;
|
let length = 0;
|
||||||
const post = m.post.length ? expand_(m.post, max, false) : [''];
|
for (let a = 0; a < acc.length; a++) {
|
||||||
if (/\$$/.test(m.pre)) {
|
for (let v = 0; v < values.length; v++) {
|
||||||
for (let k = 0; k < post.length && k < max; k++) {
|
if (out.length >= max)
|
||||||
const expansion = pre + '{' + m.body + '}' + post[k];
|
return out;
|
||||||
expansions.push(expansion);
|
const expansion = acc[a] + pre + values[v];
|
||||||
|
// Bash drops empty results at the top level. Skip them before they count
|
||||||
|
// against `max`, so `max` bounds the number of *kept* results.
|
||||||
|
if (dropEmpties && !expansion)
|
||||||
|
continue;
|
||||||
|
if (length + expansion.length > maxLength)
|
||||||
|
return out;
|
||||||
|
out.push(expansion);
|
||||||
|
length += expansion.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
return out;
|
||||||
|
}
|
||||||
|
// The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
|
||||||
|
// sequence body.
|
||||||
|
function expandSequence(body, isAlphaSequence, max, maxLength) {
|
||||||
|
const n = body.split(/\.\./);
|
||||||
|
const N = [];
|
||||||
|
// A sequence body always splits into two or three parts, but the compiler
|
||||||
|
// can't know that.
|
||||||
|
/* c8 ignore start */
|
||||||
|
if (n[0] === undefined || n[1] === undefined) {
|
||||||
|
return N;
|
||||||
|
}
|
||||||
|
/* c8 ignore stop */
|
||||||
|
const x = numeric(n[0]);
|
||||||
|
const y = numeric(n[1]);
|
||||||
|
const width = Math.max(n[0].length, n[1].length);
|
||||||
|
let incr = n.length === 3 && n[2] !== undefined ?
|
||||||
|
Math.max(Math.abs(numeric(n[2])), 1)
|
||||||
|
: 1;
|
||||||
|
let test = lte;
|
||||||
|
const reverse = y < x;
|
||||||
|
if (reverse) {
|
||||||
|
incr *= -1;
|
||||||
|
test = gte;
|
||||||
|
}
|
||||||
|
const pad = n.some(isPadded);
|
||||||
|
let length = 0;
|
||||||
|
for (let i = x; test(i, y) && N.length < max; i += incr) {
|
||||||
|
let c;
|
||||||
|
if (isAlphaSequence) {
|
||||||
|
c = String.fromCharCode(i);
|
||||||
|
if (c === '\\') {
|
||||||
|
c = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
c = String(i);
|
||||||
|
if (pad) {
|
||||||
|
const need = width - c.length;
|
||||||
|
if (need > 0) {
|
||||||
|
const z = new Array(need + 1).join('0');
|
||||||
|
if (i < 0) {
|
||||||
|
c = '-' + z + c.slice(1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
c = z + c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (length + c.length > maxLength)
|
||||||
|
break;
|
||||||
|
N.push(c);
|
||||||
|
length += c.length;
|
||||||
|
}
|
||||||
|
return N;
|
||||||
|
}
|
||||||
|
function expand_(str, max, maxLength, isTop) {
|
||||||
|
// Consume the string's top-level brace groups left to right, threading a
|
||||||
|
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
|
||||||
|
// rather than recursing on `m.post` once per group - keeps the native stack
|
||||||
|
// depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no
|
||||||
|
// longer overflow the stack, and leaves a single accumulator whose size
|
||||||
|
// `maxLength` bounds directly (CVE-2026-14257).
|
||||||
|
let acc = [''];
|
||||||
|
// Bash drops empty results, but only when the *first* top-level group is a
|
||||||
|
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
|
||||||
|
// is on the final strings, so it is applied to whichever `combine` produces
|
||||||
|
// them (the one with no brace set left in the tail).
|
||||||
|
let dropEmpties = false;
|
||||||
|
let firstGroup = true;
|
||||||
|
for (;;) {
|
||||||
|
const m = balanced('{', '}', str);
|
||||||
|
// No brace set left: the rest of the string is literal.
|
||||||
|
if (!m) {
|
||||||
|
return combine(acc, str, [''], max, maxLength, dropEmpties);
|
||||||
|
}
|
||||||
|
// no need to expand pre, since it is guaranteed to be free of brace-sets
|
||||||
|
const pre = m.pre;
|
||||||
|
if (/\$$/.test(pre)) {
|
||||||
|
acc = combine(acc, pre + '{' + m.body + '}', [''], max, maxLength, dropEmpties && !m.post.length);
|
||||||
|
firstGroup = false;
|
||||||
|
if (!m.post.length)
|
||||||
|
break;
|
||||||
|
str = m.post;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
|
const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
|
||||||
const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
|
const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
|
||||||
const isSequence = isNumericSequence || isAlphaSequence;
|
const isSequence = isNumericSequence || isAlphaSequence;
|
||||||
@@ -112,86 +218,68 @@ function expand_(str, max, isTop) {
|
|||||||
// {a},b}
|
// {a},b}
|
||||||
if (m.post.match(/,(?!,).*\}/)) {
|
if (m.post.match(/,(?!,).*\}/)) {
|
||||||
str = m.pre + '{' + m.body + escClose + m.post;
|
str = m.pre + '{' + m.body + escClose + m.post;
|
||||||
return expand_(str, max, true);
|
isTop = true;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
return [str];
|
// Nothing here expands, so the whole remaining string is literal.
|
||||||
|
return combine(acc, pre + '{' + m.body + '}' + m.post, [''], max, maxLength, dropEmpties);
|
||||||
}
|
}
|
||||||
let n;
|
if (firstGroup) {
|
||||||
|
dropEmpties = isTop && !isSequence;
|
||||||
|
firstGroup = false;
|
||||||
|
}
|
||||||
|
let values;
|
||||||
if (isSequence) {
|
if (isSequence) {
|
||||||
n = m.body.split(/\.\./);
|
values = expandSequence(m.body, isAlphaSequence, max, maxLength);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
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, false).map(embrace);
|
n = expand_(n[0], max, maxLength, 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) {
|
||||||
return post.map(p => m.pre + n[0] + p);
|
acc = combine(acc, pre + n[0], [''], max, maxLength, dropEmpties && !m.post.length);
|
||||||
|
if (!m.post.length)
|
||||||
|
break;
|
||||||
|
str = m.post;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
/* c8 ignore stop */
|
/* c8 ignore stop */
|
||||||
}
|
}
|
||||||
}
|
// Values that `combine` is going to drop as empty produce no result, so
|
||||||
// at this point, n is the parts, and we know it's not a comma set
|
// they must not count against `max` - otherwise `{a,,b}` with `max: 2`
|
||||||
// with a single entry.
|
// would stop at `['a', '']` and yield one result instead of two. Skipping
|
||||||
let N;
|
// them outright keeps `values` bounded while leaving `max` a bound on
|
||||||
if (isSequence && n[0] !== undefined && n[1] !== undefined) {
|
// *kept* results.
|
||||||
const x = numeric(n[0]);
|
let dropsEmpties = dropEmpties && !m.post.length && !pre;
|
||||||
const y = numeric(n[1]);
|
for (let d = 0; dropsEmpties && d < acc.length; d++) {
|
||||||
const width = Math.max(n[0].length, n[1].length);
|
if (acc[d]) {
|
||||||
let incr = n.length === 3 && n[2] !== undefined ?
|
dropsEmpties = false;
|
||||||
Math.max(Math.abs(numeric(n[2])), 1)
|
}
|
||||||
: 1;
|
|
||||||
let test = lte;
|
|
||||||
const reverse = y < x;
|
|
||||||
if (reverse) {
|
|
||||||
incr *= -1;
|
|
||||||
test = gte;
|
|
||||||
}
|
}
|
||||||
const pad = n.some(isPadded);
|
values = [];
|
||||||
N = [];
|
let valuesLength = 0;
|
||||||
for (let i = x; test(i, y) && N.length < max; i += incr) {
|
outer: for (let j = 0; j < n.length; j++) {
|
||||||
let c;
|
const expanded = expand_(n[j], max, maxLength, false);
|
||||||
if (isAlphaSequence) {
|
for (let k = 0; k < expanded.length; k++) {
|
||||||
c = String.fromCharCode(i);
|
const v = expanded[k];
|
||||||
if (c === '\\') {
|
if (dropsEmpties && !v)
|
||||||
c = '';
|
continue;
|
||||||
|
if (values.length >= max || valuesLength + v.length > maxLength) {
|
||||||
|
break outer;
|
||||||
}
|
}
|
||||||
}
|
values.push(v);
|
||||||
else {
|
valuesLength += v.length;
|
||||||
c = String(i);
|
|
||||||
if (pad) {
|
|
||||||
const need = width - c.length;
|
|
||||||
if (need > 0) {
|
|
||||||
const z = new Array(need + 1).join('0');
|
|
||||||
if (i < 0) {
|
|
||||||
c = '-' + z + c.slice(1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
c = z + c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
N.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
N = [];
|
|
||||||
for (let j = 0; j < n.length; j++) {
|
|
||||||
N.push.apply(N, expand_(n[j], max, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let j = 0; j < N.length; j++) {
|
|
||||||
for (let k = 0; k < post.length && expansions.length < max; k++) {
|
|
||||||
const expansion = pre + N[j] + post[k];
|
|
||||||
if (!isTop || isSequence || expansion) {
|
|
||||||
expansions.push(expansion);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length);
|
||||||
|
if (!m.post.length)
|
||||||
|
break;
|
||||||
|
str = m.post;
|
||||||
}
|
}
|
||||||
return expansions;
|
return acc;
|
||||||
}
|
}
|
||||||
//# sourceMappingURL=index.js.map
|
//# sourceMappingURL=index.js.map
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "brace-expansion",
|
"name": "brace-expansion",
|
||||||
"description": "Brace expansion as known from sh/bash",
|
"description": "Brace expansion as known from sh/bash",
|
||||||
"version": "5.0.6",
|
"version": "5.0.9",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18 || 20 || >=22"
|
"node": "20 || >=22"
|
||||||
},
|
},
|
||||||
"tshy": {
|
"tshy": {
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -59,6 +59,6 @@
|
|||||||
"module": "./dist/esm/index.js",
|
"module": "./dist/esm/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+ssh://git@github.com/juliangruber/brace-expansion.git"
|
"url": "git+https://github.com/juliangruber/brace-expansion.git"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
@@ -1,5 +1,30 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## [9.0.3](https://github.com/nodemailer/nodemailer/compare/v9.0.2...v9.0.3) (2026-06-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **smtp-connection:** harden STARTTLS upgrade and secure socket handling ([#1835](https://github.com/nodemailer/nodemailer/issues/1835)) ([07d8253](https://github.com/nodemailer/nodemailer/commit/07d8253326ecefff9f7d92c157429ce8bc7335f8))
|
||||||
|
|
||||||
|
## [9.0.2](https://github.com/nodemailer/nodemailer/compare/v9.0.1...v9.0.2) (2026-06-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **addressparser:** keep operator chars inside an address-literal as text ([#1829](https://github.com/nodemailer/nodemailer/issues/1829)) ([9ba1064](https://github.com/nodemailer/nodemailer/commit/9ba1064f3b115cd60cb63aa954a3c0961e86fbb7))
|
||||||
|
* harden smtp-connection low-severity issues ([22ddcea](https://github.com/nodemailer/nodemailer/commit/22ddcea8ed043e4ea23b8971b75a2df196b5a581))
|
||||||
|
* harden smtp-connection response parsing and socket lifecycle ([68860b9](https://github.com/nodemailer/nodemailer/commit/68860b94311b5b6837754e5e790f186ca8a00b70))
|
||||||
|
* prevent SES transport callback double-invocation and hang on sync errors ([#1831](https://github.com/nodemailer/nodemailer/issues/1831)) ([9517bc5](https://github.com/nodemailer/nodemailer/commit/9517bc5dc94e77907bb157e3466bf73a2b327f5c))
|
||||||
|
* reject CRLF in HTTP proxy CONNECT destination to prevent request injection ([6347b47](https://github.com/nodemailer/nodemailer/commit/6347b47c7d12f9d3acf53d391b921e836f400640))
|
||||||
|
|
||||||
|
## [9.0.1](https://github.com/nodemailer/nodemailer/compare/v9.0.0...v9.0.1) (2026-06-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* enforce disableFileAccess/disableUrlAccess for raw message option ([a82e060](https://github.com/nodemailer/nodemailer/commit/a82e060d978f27e5f41369a9a9807b1e3dedc2e2))
|
||||||
|
|
||||||
## [9.0.0](https://github.com/nodemailer/nodemailer/compare/v8.0.11...v9.0.0) (2026-06-14)
|
## [9.0.0](https://github.com/nodemailer/nodemailer/compare/v8.0.11...v9.0.0) (2026-06-14)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -181,6 +181,7 @@ class Tokenizer {
|
|||||||
this.operatorExpecting = '';
|
this.operatorExpecting = '';
|
||||||
this.node = null;
|
this.node = null;
|
||||||
this.escaped = false;
|
this.escaped = false;
|
||||||
|
this.inDomainLiteral = false;
|
||||||
|
|
||||||
this.list = [];
|
this.list = [];
|
||||||
/**
|
/**
|
||||||
@@ -232,6 +233,21 @@ class Tokenizer {
|
|||||||
* @param {String} chr Character from the address field
|
* @param {String} chr Character from the address field
|
||||||
*/
|
*/
|
||||||
checkChar(chr, nextChr) {
|
checkChar(chr, nextChr) {
|
||||||
|
// Track RFC 5322 domain-literals ("[" *dtext "]"). Operator characters such
|
||||||
|
// as the ":" of an IPv6 address-literal (user@[IPv6:2001:db8::1]) are dtext
|
||||||
|
// and must not be treated as the group delimiter while inside the brackets.
|
||||||
|
// Quoted strings and comments are handled separately via operatorExpecting,
|
||||||
|
// so only enter this state when no operator is open. The list separators ","
|
||||||
|
// and ";" are the exception: they always end the literal (and split the
|
||||||
|
// address list) so that an unclosed "[" cannot swallow later recipients.
|
||||||
|
if (!this.escaped && !this.operatorExpecting) {
|
||||||
|
if (!this.inDomainLiteral && chr === '[') {
|
||||||
|
this.inDomainLiteral = true;
|
||||||
|
} else if (this.inDomainLiteral && (chr === ']' || chr === ',' || chr === ';')) {
|
||||||
|
this.inDomainLiteral = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.escaped) {
|
if (this.escaped) {
|
||||||
// ignore next condition blocks
|
// ignore next condition blocks
|
||||||
} else if (chr === this.operatorExpecting) {
|
} else if (chr === this.operatorExpecting) {
|
||||||
@@ -250,7 +266,7 @@ class Tokenizer {
|
|||||||
this.escaped = false;
|
this.escaped = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else if (!this.operatorExpecting && chr in this.operators) {
|
} else if (!this.operatorExpecting && !this.inDomainLiteral && chr in this.operators) {
|
||||||
this.node = {
|
this.node = {
|
||||||
type: 'operator',
|
type: 'operator',
|
||||||
value: chr
|
value: chr
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ class RelaxedBody extends Transform {
|
|||||||
options = options || {};
|
options = options || {};
|
||||||
this.chunkBuffer = [];
|
this.chunkBuffer = [];
|
||||||
this.chunkBufferLen = 0;
|
this.chunkBufferLen = 0;
|
||||||
this.bodyHash = crypto.createHash(options.hashAlgo || 'sha1');
|
this.bodyHash = crypto.createHash(options.hashAlgo || 'sha256');
|
||||||
this.remainder = '';
|
this.remainder = '';
|
||||||
this.byteLength = 0;
|
this.byteLength = 0;
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -32,7 +32,11 @@ class MailComposer {
|
|||||||
|
|
||||||
// Compose MIME tree
|
// Compose MIME tree
|
||||||
if (this.mail.raw) {
|
if (this.mail.raw) {
|
||||||
this.message = new MimeNode('message/rfc822', { newline: this.mail.newline }).setRaw(this.mail.raw);
|
this.message = new MimeNode('message/rfc822', {
|
||||||
|
newline: this.mail.newline,
|
||||||
|
disableUrlAccess: this.mail.disableUrlAccess,
|
||||||
|
disableFileAccess: this.mail.disableFileAccess
|
||||||
|
}).setRaw(this.mail.raw);
|
||||||
} else if (this._useMixed) {
|
} else if (this._useMixed) {
|
||||||
this.message = this._createMixed();
|
this.message = this._createMixed();
|
||||||
} else if (this._useAlternative) {
|
} else if (this._useAlternative) {
|
||||||
|
|||||||
+43
-13
@@ -43,11 +43,12 @@ class SESTransport extends EventEmitter {
|
|||||||
|
|
||||||
getRegion(cb) {
|
getRegion(cb) {
|
||||||
if (this.ses.sesClient.config && typeof this.ses.sesClient.config.region === 'function') {
|
if (this.ses.sesClient.config && typeof this.ses.sesClient.config.region === 'function') {
|
||||||
// promise
|
// Resolve the region provider. Use the two-argument form of then() so that a
|
||||||
return this.ses.sesClient.config
|
// synchronous throw from cb is not recaught here and used to invoke cb a second time.
|
||||||
.region()
|
return this.ses.sesClient.config.region().then(
|
||||||
.then(region => cb(null, region))
|
region => cb(null, region),
|
||||||
.catch(err => cb(err));
|
err => cb(err)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return cb(null, false);
|
return cb(null, false);
|
||||||
}
|
}
|
||||||
@@ -150,8 +151,27 @@ class SESTransport extends EventEmitter {
|
|||||||
region = 'us-east-1';
|
region = 'us-east-1';
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = new this.ses.SendEmailCommand(sesMessage);
|
let sendPromise;
|
||||||
const sendPromise = this.ses.sesClient.send(command);
|
try {
|
||||||
|
// command construction or dispatch can throw synchronously on a
|
||||||
|
// misconfigured SDK; surface it as a single error callback instead
|
||||||
|
// of letting it escape into getRegion's promise chain
|
||||||
|
const command = new this.ses.SendEmailCommand(sesMessage);
|
||||||
|
sendPromise = this.ses.sesClient.send(command);
|
||||||
|
} catch (err) {
|
||||||
|
tagSesError(err);
|
||||||
|
this.logger.error(
|
||||||
|
{
|
||||||
|
err,
|
||||||
|
tnx: 'send'
|
||||||
|
},
|
||||||
|
'Send error for %s: %s',
|
||||||
|
messageId,
|
||||||
|
err.message
|
||||||
|
);
|
||||||
|
setImmediate(() => callback(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
sendPromise
|
sendPromise
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -159,7 +179,7 @@ class SESTransport extends EventEmitter {
|
|||||||
region = 'email';
|
region = 'email';
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(null, {
|
const info = {
|
||||||
envelope: {
|
envelope: {
|
||||||
from: envelope.from,
|
from: envelope.from,
|
||||||
to: envelope.to
|
to: envelope.to
|
||||||
@@ -167,7 +187,11 @@ class SESTransport extends EventEmitter {
|
|||||||
messageId: '<' + data.MessageId + (!/@/.test(data.MessageId) ? '@' + region + '.amazonses.com' : '') + '>',
|
messageId: '<' + data.MessageId + (!/@/.test(data.MessageId) ? '@' + region + '.amazonses.com' : '') + '>',
|
||||||
response: data.MessageId,
|
response: data.MessageId,
|
||||||
raw
|
raw
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// invoke the callback outside the promise chain so a throw from it
|
||||||
|
// is not recaught by .catch() and used to call it a second time
|
||||||
|
setImmediate(() => callback(null, info));
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
tagSesError(err);
|
tagSesError(err);
|
||||||
@@ -180,7 +204,7 @@ class SESTransport extends EventEmitter {
|
|||||||
messageId,
|
messageId,
|
||||||
err.message
|
err.message
|
||||||
);
|
);
|
||||||
callback(err);
|
setImmediate(() => callback(err));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -222,10 +246,16 @@ class SESTransport extends EventEmitter {
|
|||||||
// the region value is not used for anything when verifying, but the lookup
|
// the region value is not used for anything when verifying, but the lookup
|
||||||
// exercises the client configuration the same way as send() does
|
// exercises the client configuration the same way as send() does
|
||||||
this.getRegion(() => {
|
this.getRegion(() => {
|
||||||
const command = new this.ses.SendEmailCommand(sesMessage);
|
let sendPromise;
|
||||||
const sendPromise = this.ses.sesClient.send(command);
|
try {
|
||||||
|
const command = new this.ses.SendEmailCommand(sesMessage);
|
||||||
|
sendPromise = this.ses.sesClient.send(command);
|
||||||
|
} catch (err) {
|
||||||
|
setImmediate(() => cb(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
sendPromise.then(() => cb(null)).catch(err => cb(err));
|
sendPromise.then(() => setImmediate(() => cb(null))).catch(err => setImmediate(() => cb(err)));
|
||||||
});
|
});
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
|
|||||||
+21
@@ -9,6 +9,10 @@ const tls = require('tls');
|
|||||||
const urllib = require('../shared/url');
|
const urllib = require('../shared/url');
|
||||||
const errors = require('../errors');
|
const errors = require('../errors');
|
||||||
|
|
||||||
|
// Cap the CONNECT response we buffer before the header terminator, so a proxy that
|
||||||
|
// never sends \r\n\r\n cannot grow memory unboundedly before the socket times out.
|
||||||
|
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Establishes proxied connection to destinationPort
|
* Establishes proxied connection to destinationPort
|
||||||
*
|
*
|
||||||
@@ -29,6 +33,16 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
|||||||
}
|
}
|
||||||
tlsOptions = tlsOptions || {};
|
tlsOptions = tlsOptions || {};
|
||||||
|
|
||||||
|
// Reject CRLF in the destination before it reaches the CONNECT request line
|
||||||
|
// and Host header. A tainted host/port could otherwise inject additional
|
||||||
|
// request headers into the proxy connection (HTTP request splitting).
|
||||||
|
destinationPort = Number(destinationPort) || 0;
|
||||||
|
if (!destinationPort || /[\r\n]/.test(destinationHost)) {
|
||||||
|
const err = new Error('Invalid proxy destination');
|
||||||
|
err.code = errors.EPROXY;
|
||||||
|
return setImmediate(() => callback(err));
|
||||||
|
}
|
||||||
|
|
||||||
const proxy = urllib.parse(proxyUrl);
|
const proxy = urllib.parse(proxyUrl);
|
||||||
|
|
||||||
const connectOptions = {
|
const connectOptions = {
|
||||||
@@ -140,6 +154,13 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
|||||||
|
|
||||||
return callback(null, socket);
|
return callback(null, socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (headers.length > MAX_RESPONSE_HEADER_BYTES) {
|
||||||
|
socket.removeListener('data', onSocketData);
|
||||||
|
const err = new Error('Proxy response headers too large');
|
||||||
|
err.code = errors.EPROXY;
|
||||||
|
return tempSocketErr(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
socket.on('data', onSocketData);
|
socket.on('data', onSocketData);
|
||||||
});
|
});
|
||||||
|
|||||||
+80
-22
@@ -298,6 +298,20 @@ class SMTPConnection extends EventEmitter {
|
|||||||
try {
|
try {
|
||||||
this._socket.connect(this.port, this.host, () => {
|
this._socket.connect(this.port, this.host, () => {
|
||||||
this._socket.setKeepAlive(true);
|
this._socket.setKeepAlive(true);
|
||||||
|
|
||||||
|
// a `secure` connection over a caller-provided socket must still
|
||||||
|
// perform the TLS handshake, otherwise AUTH and the message body
|
||||||
|
// would be sent in cleartext despite the caller requesting TLS
|
||||||
|
if (this.secureConnection && !this.alreadySecured) {
|
||||||
|
return this._upgradeConnection(err => {
|
||||||
|
if (err) {
|
||||||
|
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._onConnect();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this._onConnect();
|
this._onConnect();
|
||||||
});
|
});
|
||||||
this._setupConnectionHandlers();
|
this._setupConnectionHandlers();
|
||||||
@@ -365,6 +379,14 @@ class SMTPConnection extends EventEmitter {
|
|||||||
* @param {Boolean} secure Whether to use TLS
|
* @param {Boolean} secure Whether to use TLS
|
||||||
*/
|
*/
|
||||||
_connectToHost(opts, secure) {
|
_connectToHost(opts, secure) {
|
||||||
|
// If the client was closed while DNS resolution was in flight, do not open
|
||||||
|
// a socket here: close() ran with this._socket still unset and so had
|
||||||
|
// nothing to tear down, and _onConnect's remedial close() is a no-op once
|
||||||
|
// _closing is set — the freshly connected socket would leak.
|
||||||
|
if (this._destroyed || this._closing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._connectionAttemptId++;
|
this._connectionAttemptId++;
|
||||||
const currentAttemptId = this._connectionAttemptId;
|
const currentAttemptId = this._connectionAttemptId;
|
||||||
|
|
||||||
@@ -431,6 +453,9 @@ class SMTPConnection extends EventEmitter {
|
|||||||
if (this._socket) {
|
if (this._socket) {
|
||||||
try {
|
try {
|
||||||
this._socket.removeListener('error', this._onConnectionSocketError);
|
this._socket.removeListener('error', this._onConnectionSocketError);
|
||||||
|
// Absorb any late teardown error (e.g. a TLS fallback socket emitting
|
||||||
|
// after destroy), mirroring the guard used in close()
|
||||||
|
this._socket.on('error', TEARDOWN_NOOP);
|
||||||
this._socket.destroy();
|
this._socket.destroy();
|
||||||
} catch (_E) {
|
} catch (_E) {
|
||||||
// ignore
|
// ignore
|
||||||
@@ -757,6 +782,11 @@ class SMTPConnection extends EventEmitter {
|
|||||||
* @param {Function} callback Callback to return once connection is reset
|
* @param {Function} callback Callback to return once connection is reset
|
||||||
*/
|
*/
|
||||||
reset(callback) {
|
reset(callback) {
|
||||||
|
const isDestroyedMessage = this._isDestroyedMessage('reset');
|
||||||
|
if (isDestroyedMessage) {
|
||||||
|
return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
|
||||||
|
}
|
||||||
|
|
||||||
this._sendCommand('RSET');
|
this._sendCommand('RSET');
|
||||||
this._responseActions.push(str => {
|
this._responseActions.push(str => {
|
||||||
if (str.charAt(0) !== '2') {
|
if (str.charAt(0) !== '2') {
|
||||||
@@ -805,6 +835,9 @@ class SMTPConnection extends EventEmitter {
|
|||||||
this._socket.removeListener('end', this._onSocketEnd);
|
this._socket.removeListener('end', this._onSocketEnd);
|
||||||
// Switch from connection-phase error handler to normal error handler
|
// Switch from connection-phase error handler to normal error handler
|
||||||
this._socket.removeListener('error', this._onConnectionSocketError);
|
this._socket.removeListener('error', this._onConnectionSocketError);
|
||||||
|
// _upgradeConnection (options.connection + secure) may already have attached
|
||||||
|
// the normal handler; remove it first so we never end up with a duplicate
|
||||||
|
this._socket.removeListener('error', this._onSocketError);
|
||||||
|
|
||||||
this._socket.on('error', this._onSocketError);
|
this._socket.on('error', this._onSocketError);
|
||||||
this._socket.on('data', this._onSocketData);
|
this._socket.on('data', this._onSocketData);
|
||||||
@@ -994,6 +1027,8 @@ class SMTPConnection extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
|
// keep the documented public flag in sync with the private state
|
||||||
|
this.destroyed = true;
|
||||||
this.emit('end');
|
this.emit('end');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1004,6 +1039,15 @@ class SMTPConnection extends EventEmitter {
|
|||||||
* has been secured
|
* has been secured
|
||||||
*/
|
*/
|
||||||
_upgradeConnection(callback) {
|
_upgradeConnection(callback) {
|
||||||
|
// RFC 3207 section 6: the client MUST discard any knowledge obtained from
|
||||||
|
// the server that was not received over the TLS-protected session. Drop any
|
||||||
|
// buffered input received before the handshake so a man-in-the-middle cannot
|
||||||
|
// inject plaintext bytes after the "220" reply (e.g. a CRLF-free fragment that
|
||||||
|
// would otherwise be prepended to the first post-TLS response and parsed as
|
||||||
|
// part of the secured EHLO capabilities). STARTTLS response injection.
|
||||||
|
this._remainder = '';
|
||||||
|
this._responseQueue = [];
|
||||||
|
|
||||||
// do not remove all listeners or it breaks node v0.10 as there's
|
// do not remove all listeners or it breaks node v0.10 as there's
|
||||||
// apparently a 'finish' event set that would be cleared as well
|
// apparently a 'finish' event set that would be cleared as well
|
||||||
|
|
||||||
@@ -1032,6 +1076,9 @@ class SMTPConnection extends EventEmitter {
|
|||||||
socketPlain.removeListener('close', this._onSocketClose);
|
socketPlain.removeListener('close', this._onSocketClose);
|
||||||
socketPlain.removeListener('end', this._onSocketEnd);
|
socketPlain.removeListener('end', this._onSocketEnd);
|
||||||
socketPlain.removeListener('error', this._onSocketError);
|
socketPlain.removeListener('error', this._onSocketError);
|
||||||
|
// the connection-phase handler is attached when upgrading a pre-opened
|
||||||
|
// options.connection socket; strip it so nothing lingers on the plain socket
|
||||||
|
socketPlain.removeListener('error', this._onConnectionSocketError);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.upgrading = true;
|
this.upgrading = true;
|
||||||
@@ -1064,18 +1111,27 @@ class SMTPConnection extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes queued responses from the server
|
* Processes queued responses from the server
|
||||||
*
|
|
||||||
* @param {Boolean} force If true, ignores _processing flag
|
|
||||||
*/
|
*/
|
||||||
_processResponse() {
|
_processResponse() {
|
||||||
if (!this._responseQueue.length) {
|
if (!this._responseQueue.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let str = (this.lastServerResponse = decodeServerResponse((this._responseQueue.shift() || '').toString()));
|
const raw = (this._responseQueue.shift() || '').toString();
|
||||||
|
|
||||||
|
// Skip unexpected empty lines without consuming a response action or
|
||||||
|
// overwriting lastServerResponse; reprocess whatever else is queued.
|
||||||
|
if (!raw.trim()) {
|
||||||
|
setImmediate(() => this._processResponse());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let str = (this.lastServerResponse = decodeServerResponse(raw));
|
||||||
|
|
||||||
if (/^\d+-/.test(str.split('\n').pop())) {
|
if (/^\d+-/.test(str.split('\n').pop())) {
|
||||||
// keep waiting for the final part of multiline response
|
// last line is still a continuation: put the partial response back on the
|
||||||
|
// queue and wait for the rest rather than dropping it
|
||||||
|
this._responseQueue.unshift(raw);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1088,11 +1144,6 @@ class SMTPConnection extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!str.trim()) {
|
|
||||||
// skip unexpected empty lines
|
|
||||||
setImmediate(() => this._processResponse());
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = this._responseActions.shift();
|
const action = this._responseActions.shift();
|
||||||
|
|
||||||
if (typeof action === 'function') {
|
if (typeof action === 'function') {
|
||||||
@@ -1189,6 +1240,23 @@ class SMTPConnection extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC 8689: validate REQUIRETLS eligibility before queuing the MAIL FROM
|
||||||
|
// response action, so a rejection here cannot leave an orphaned action in
|
||||||
|
// _responseActions (which would consume the next reply and desync a reused
|
||||||
|
// connection).
|
||||||
|
if (this._envelope.requireTLSExtensionEnabled) {
|
||||||
|
if (!this.secure) {
|
||||||
|
return callback(
|
||||||
|
this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!this._supportedExtensions.includes('REQUIRETLS')) {
|
||||||
|
return callback(
|
||||||
|
this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._responseActions.push(str => {
|
this._responseActions.push(str => {
|
||||||
this._actionMAIL(str, callback);
|
this._actionMAIL(str, callback);
|
||||||
});
|
});
|
||||||
@@ -1225,20 +1293,10 @@ class SMTPConnection extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC 8689: If the envelope requests REQUIRETLS extension
|
// RFC 8689: append the REQUIRETLS keyword to MAIL FROM. Eligibility
|
||||||
// then append REQUIRETLS keyword to the MAIL FROM command
|
// (TLS connection + server support) was already validated above, before
|
||||||
// Note: REQUIRETLS can only be used over TLS connections and requires server support
|
// the response action was queued.
|
||||||
if (this._envelope.requireTLSExtensionEnabled) {
|
if (this._envelope.requireTLSExtensionEnabled) {
|
||||||
if (!this.secure) {
|
|
||||||
return callback(
|
|
||||||
this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!this._supportedExtensions.includes('REQUIRETLS')) {
|
|
||||||
return callback(
|
|
||||||
this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
args.push('REQUIRETLS');
|
args.push('REQUIRETLS');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nodemailer",
|
"name": "nodemailer",
|
||||||
"version": "9.0.0",
|
"version": "9.0.3",
|
||||||
"description": "Easy as cake e-mail sending from your Node.js applications",
|
"description": "Easy as cake e-mail sending from your Node.js applications",
|
||||||
"main": "lib/nodemailer.js",
|
"main": "lib/nodemailer.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -27,10 +27,10 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://nodemailer.com/",
|
"homepage": "https://nodemailer.com/",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-sesv2": "3.1065.0",
|
"@aws-sdk/client-sesv2": "3.1068.0",
|
||||||
"bunyan": "1.8.15",
|
"bunyan": "1.8.15",
|
||||||
"c8": "11.0.0",
|
"c8": "11.0.0",
|
||||||
"eslint": "10.4.1",
|
"eslint": "10.5.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"globals": "17.6.0",
|
"globals": "17.6.0",
|
||||||
"libbase64": "1.3.0",
|
"libbase64": "1.3.0",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"prettier": "3.8.4",
|
"prettier": "3.8.4",
|
||||||
"proxy": "1.0.2",
|
"proxy": "1.0.2",
|
||||||
"proxy-test-server": "1.0.0",
|
"proxy-test-server": "1.0.0",
|
||||||
"smtp-server": "3.18.5"
|
"smtp-server": "3.19.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
|
|||||||
+3
@@ -26,6 +26,9 @@ Returns: `Client`
|
|||||||
* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds.
|
* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds.
|
||||||
* **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
|
* **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
|
||||||
* **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
|
* **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
|
||||||
|
* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
|
||||||
|
* **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit.
|
||||||
|
* **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit.
|
||||||
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
|
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
|
||||||
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
|
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
|
||||||
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body.
|
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body.
|
||||||
|
|||||||
+12
-1
@@ -350,7 +350,13 @@ function processHeader (request, key, val) {
|
|||||||
} else if (typeof val[i] === 'object') {
|
} else if (typeof val[i] === 'object') {
|
||||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||||
} else {
|
} else {
|
||||||
arr.push(`${val[i]}`)
|
// Coerce primitives (and reject unsafe coercions such as functions
|
||||||
|
// with a crafted toString/Symbol.toPrimitive).
|
||||||
|
const str = `${val[i]}`
|
||||||
|
if (!isValidHeaderValue(str)) {
|
||||||
|
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||||
|
}
|
||||||
|
arr.push(str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val = arr
|
val = arr
|
||||||
@@ -361,7 +367,12 @@ function processHeader (request, key, val) {
|
|||||||
} else if (val === null) {
|
} else if (val === null) {
|
||||||
val = ''
|
val = ''
|
||||||
} else {
|
} else {
|
||||||
|
// Coerce primitives (and reject unsafe coercions such as functions
|
||||||
|
// with a crafted toString/Symbol.toPrimitive).
|
||||||
val = `${val}`
|
val = `${val}`
|
||||||
|
if (!isValidHeaderValue(val)) {
|
||||||
|
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (headerName === 'host') {
|
if (headerName === 'host') {
|
||||||
|
|||||||
+2
-2
@@ -24,8 +24,6 @@ function defaultFactory (origin, opts) {
|
|||||||
|
|
||||||
class Agent extends DispatcherBase {
|
class Agent extends DispatcherBase {
|
||||||
constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) {
|
constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) {
|
||||||
super()
|
|
||||||
|
|
||||||
if (typeof factory !== 'function') {
|
if (typeof factory !== 'function') {
|
||||||
throw new InvalidArgumentError('factory must be a function.')
|
throw new InvalidArgumentError('factory must be a function.')
|
||||||
}
|
}
|
||||||
@@ -38,6 +36,8 @@ class Agent extends DispatcherBase {
|
|||||||
throw new InvalidArgumentError('maxRedirections must be a positive number')
|
throw new InvalidArgumentError('maxRedirections must be a positive number')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
super(options)
|
||||||
|
|
||||||
if (connect && typeof connect !== 'function') {
|
if (connect && typeof connect !== 'function') {
|
||||||
connect = { ...connect }
|
connect = { ...connect }
|
||||||
}
|
}
|
||||||
|
|||||||
+151
-25
@@ -10,6 +10,7 @@ const {
|
|||||||
RequestContentLengthMismatchError,
|
RequestContentLengthMismatchError,
|
||||||
ResponseContentLengthMismatchError,
|
ResponseContentLengthMismatchError,
|
||||||
RequestAbortedError,
|
RequestAbortedError,
|
||||||
|
InvalidArgumentError,
|
||||||
HeadersTimeoutError,
|
HeadersTimeoutError,
|
||||||
HeadersOverflowError,
|
HeadersOverflowError,
|
||||||
SocketError,
|
SocketError,
|
||||||
@@ -57,6 +58,9 @@ const EMPTY_BUF = Buffer.alloc(0)
|
|||||||
const FastBuffer = Buffer[Symbol.species]
|
const FastBuffer = Buffer[Symbol.species]
|
||||||
const addListener = util.addListener
|
const addListener = util.addListener
|
||||||
const removeAllListeners = util.removeAllListeners
|
const removeAllListeners = util.removeAllListeners
|
||||||
|
const kIdleSocketValidation = Symbol('kIdleSocketValidation')
|
||||||
|
const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout')
|
||||||
|
const kSocketUsed = Symbol('kSocketUsed')
|
||||||
|
|
||||||
let extractBody
|
let extractBody
|
||||||
|
|
||||||
@@ -279,29 +283,71 @@ class Parser {
|
|||||||
|
|
||||||
const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr
|
const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr
|
||||||
|
|
||||||
if (ret === constants.ERROR.PAUSED_UPGRADE) {
|
if (ret !== constants.ERROR.OK) {
|
||||||
this.onUpgrade(data.slice(offset))
|
const body = data.subarray(offset)
|
||||||
} else if (ret === constants.ERROR.PAUSED) {
|
|
||||||
this.paused = true
|
if (ret === constants.ERROR.PAUSED_UPGRADE) {
|
||||||
socket.unshift(data.slice(offset))
|
this.onUpgrade(body)
|
||||||
} else if (ret !== constants.ERROR.OK) {
|
} else if (ret === constants.ERROR.PAUSED) {
|
||||||
const ptr = llhttp.llhttp_get_error_reason(this.ptr)
|
this.paused = true
|
||||||
let message = ''
|
socket.unshift(body)
|
||||||
/* istanbul ignore else: difficult to make a test case for */
|
} else {
|
||||||
if (ptr) {
|
throw this.createError(ret, body)
|
||||||
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
|
|
||||||
message =
|
|
||||||
'Response does not match the HTTP/1.1 protocol (' +
|
|
||||||
Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
|
|
||||||
')'
|
|
||||||
}
|
}
|
||||||
throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset))
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
util.destroy(socket, err)
|
util.destroy(socket, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finish () {
|
||||||
|
assert(currentParser === null)
|
||||||
|
assert(this.ptr != null)
|
||||||
|
assert(!this.paused)
|
||||||
|
|
||||||
|
const { llhttp } = this
|
||||||
|
|
||||||
|
let ret
|
||||||
|
|
||||||
|
try {
|
||||||
|
currentParser = this
|
||||||
|
ret = llhttp.llhttp_finish(this.ptr)
|
||||||
|
} finally {
|
||||||
|
currentParser = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret === constants.ERROR.OK) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) {
|
||||||
|
this.paused = true
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createError(ret, EMPTY_BUF)
|
||||||
|
}
|
||||||
|
|
||||||
|
createError (ret, data) {
|
||||||
|
const { llhttp, contentLength, bytesRead } = this
|
||||||
|
|
||||||
|
if (contentLength && bytesRead !== parseInt(contentLength, 10)) {
|
||||||
|
return new ResponseContentLengthMismatchError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const ptr = llhttp.llhttp_get_error_reason(this.ptr)
|
||||||
|
let message = ''
|
||||||
|
if (ptr) {
|
||||||
|
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
|
||||||
|
message =
|
||||||
|
'Response does not match the HTTP/1.1 protocol (' +
|
||||||
|
Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
|
||||||
|
')'
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HTTPParserError(message, constants.ERROR[ret], data)
|
||||||
|
}
|
||||||
|
|
||||||
destroy () {
|
destroy () {
|
||||||
assert(this.ptr != null)
|
assert(this.ptr != null)
|
||||||
assert(currentParser == null)
|
assert(currentParser == null)
|
||||||
@@ -329,6 +375,11 @@ class Parser {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client[kRunning] === 0) {
|
||||||
|
util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
const request = client[kQueue][client[kRunningIdx]]
|
const request = client[kQueue][client[kRunningIdx]]
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return -1
|
return -1
|
||||||
@@ -432,6 +483,11 @@ class Parser {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client[kRunning] === 0) {
|
||||||
|
util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
const request = client[kQueue][client[kRunningIdx]]
|
const request = client[kQueue][client[kRunningIdx]]
|
||||||
|
|
||||||
/* istanbul ignore next: difficult to make a test case for */
|
/* istanbul ignore next: difficult to make a test case for */
|
||||||
@@ -605,6 +661,7 @@ class Parser {
|
|||||||
request.onComplete(headers)
|
request.onComplete(headers)
|
||||||
|
|
||||||
client[kQueue][client[kRunningIdx]++] = null
|
client[kQueue][client[kRunningIdx]++] = null
|
||||||
|
socket[kSocketUsed] = true
|
||||||
|
|
||||||
if (socket[kWriting]) {
|
if (socket[kWriting]) {
|
||||||
assert(client[kRunning] === 0)
|
assert(client[kRunning] === 0)
|
||||||
@@ -663,6 +720,9 @@ async function connectH1 (client, socket) {
|
|||||||
socket[kWriting] = false
|
socket[kWriting] = false
|
||||||
socket[kReset] = false
|
socket[kReset] = false
|
||||||
socket[kBlocking] = false
|
socket[kBlocking] = false
|
||||||
|
socket[kIdleSocketValidation] = 0
|
||||||
|
socket[kIdleSocketValidationTimeout] = null
|
||||||
|
socket[kSocketUsed] = false
|
||||||
socket[kParser] = new Parser(client, socket, llhttpInstance)
|
socket[kParser] = new Parser(client, socket, llhttpInstance)
|
||||||
|
|
||||||
addListener(socket, 'error', function (err) {
|
addListener(socket, 'error', function (err) {
|
||||||
@@ -673,8 +733,11 @@ async function connectH1 (client, socket) {
|
|||||||
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
|
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
|
||||||
// to the user.
|
// to the user.
|
||||||
if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
|
if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
|
||||||
// We treat all incoming data so for as a valid response.
|
const parserErr = parser.finish()
|
||||||
parser.onMessageComplete()
|
if (parserErr) {
|
||||||
|
this[kError] = parserErr
|
||||||
|
this[kClient][kOnError](parserErr)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,8 +756,10 @@ async function connectH1 (client, socket) {
|
|||||||
const parser = this[kParser]
|
const parser = this[kParser]
|
||||||
|
|
||||||
if (parser.statusCode && !parser.shouldKeepAlive) {
|
if (parser.statusCode && !parser.shouldKeepAlive) {
|
||||||
// We treat all incoming data so far as a valid response.
|
const parserErr = parser.finish()
|
||||||
parser.onMessageComplete()
|
if (parserErr) {
|
||||||
|
util.destroy(this, parserErr)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,10 +769,11 @@ async function connectH1 (client, socket) {
|
|||||||
const client = this[kClient]
|
const client = this[kClient]
|
||||||
const parser = this[kParser]
|
const parser = this[kParser]
|
||||||
|
|
||||||
|
clearIdleSocketValidation(this)
|
||||||
|
|
||||||
if (parser) {
|
if (parser) {
|
||||||
if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
|
if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
|
||||||
// We treat all incoming data so far as a valid response.
|
this[kError] = parser.finish() || this[kError]
|
||||||
parser.onMessageComplete()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this[kParser].destroy()
|
this[kParser].destroy()
|
||||||
@@ -770,7 +836,7 @@ async function connectH1 (client, socket) {
|
|||||||
return socket.destroyed
|
return socket.destroyed
|
||||||
},
|
},
|
||||||
busy (request) {
|
busy (request) {
|
||||||
if (socket[kWriting] || socket[kReset] || socket[kBlocking]) {
|
if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,6 +874,31 @@ async function connectH1 (client, socket) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearIdleSocketValidation (socket) {
|
||||||
|
if (socket[kIdleSocketValidationTimeout]) {
|
||||||
|
clearTimeout(socket[kIdleSocketValidationTimeout])
|
||||||
|
socket[kIdleSocketValidationTimeout] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
socket[kIdleSocketValidation] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleIdleSocketValidation (client, socket) {
|
||||||
|
socket[kIdleSocketValidation] = 1
|
||||||
|
socket[kIdleSocketValidationTimeout] = setTimeout(() => {
|
||||||
|
socket[kIdleSocketValidationTimeout] = null
|
||||||
|
socket[kIdleSocketValidation] = 2
|
||||||
|
|
||||||
|
if (client[kSocket] === socket && !socket.destroyed) {
|
||||||
|
client[kResume]()
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
socket[kIdleSocketValidationTimeout].unref?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('./client.js')} client
|
||||||
|
*/
|
||||||
function resumeH1 (client) {
|
function resumeH1 (client) {
|
||||||
const socket = client[kSocket]
|
const socket = client[kSocket]
|
||||||
|
|
||||||
@@ -822,6 +913,32 @@ function resumeH1 (client) {
|
|||||||
socket[kNoRef] = false
|
socket[kNoRef] = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) {
|
||||||
|
if (socket[kIdleSocketValidation] === 0) {
|
||||||
|
scheduleIdleSocketValidation(client, socket)
|
||||||
|
socket[kParser].readMore()
|
||||||
|
if (socket.destroyed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socket[kIdleSocketValidation] === 1) {
|
||||||
|
socket[kParser].readMore()
|
||||||
|
if (socket.destroyed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client[kRunning] === 0) {
|
||||||
|
socket[kParser].readMore()
|
||||||
|
if (socket.destroyed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (client[kSize] === 0) {
|
if (client[kSize] === 0) {
|
||||||
if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
|
if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
|
||||||
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
|
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
|
||||||
@@ -877,8 +994,16 @@ function writeH1 (client, request) {
|
|||||||
}
|
}
|
||||||
body = bodyStream.stream
|
body = bodyStream.stream
|
||||||
contentLength = bodyStream.length
|
contentLength = bodyStream.length
|
||||||
} else if (util.isBlobLike(body) && request.contentType == null && body.type) {
|
} else if (util.isBlobLike(body) && request.contentType == null) {
|
||||||
headers.push('content-type', body.type)
|
const contentType = body.type
|
||||||
|
if (contentType) {
|
||||||
|
const contentTypeValue = `${contentType}`
|
||||||
|
if (!util.isValidHeaderValue(contentTypeValue)) {
|
||||||
|
util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
headers.push('content-type', contentTypeValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body && typeof body.read === 'function') {
|
if (body && typeof body.read === 'function') {
|
||||||
@@ -915,6 +1040,7 @@ function writeH1 (client, request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const socket = client[kSocket]
|
const socket = client[kSocket]
|
||||||
|
clearIdleSocketValidation(socket)
|
||||||
|
|
||||||
const abort = (err) => {
|
const abort = (err) => {
|
||||||
if (request.aborted || request.completed) {
|
if (request.aborted || request.completed) {
|
||||||
|
|||||||
+3
-2
@@ -106,9 +106,10 @@ class Client extends DispatcherBase {
|
|||||||
autoSelectFamilyAttemptTimeout,
|
autoSelectFamilyAttemptTimeout,
|
||||||
// h2
|
// h2
|
||||||
maxConcurrentStreams,
|
maxConcurrentStreams,
|
||||||
allowH2
|
allowH2,
|
||||||
|
webSocket
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super()
|
super({ webSocket })
|
||||||
|
|
||||||
if (keepAlive !== undefined) {
|
if (keepAlive !== undefined) {
|
||||||
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
|
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
|
||||||
|
|||||||
+10
-1
@@ -11,15 +11,24 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch, kInterceptors } = requ
|
|||||||
const kOnDestroyed = Symbol('onDestroyed')
|
const kOnDestroyed = Symbol('onDestroyed')
|
||||||
const kOnClosed = Symbol('onClosed')
|
const kOnClosed = Symbol('onClosed')
|
||||||
const kInterceptedDispatch = Symbol('Intercepted Dispatch')
|
const kInterceptedDispatch = Symbol('Intercepted Dispatch')
|
||||||
|
const kWebSocketOptions = Symbol('webSocketOptions')
|
||||||
|
|
||||||
class DispatcherBase extends Dispatcher {
|
class DispatcherBase extends Dispatcher {
|
||||||
constructor () {
|
constructor (opts) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this[kDestroyed] = false
|
this[kDestroyed] = false
|
||||||
this[kOnDestroyed] = null
|
this[kOnDestroyed] = null
|
||||||
this[kClosed] = false
|
this[kClosed] = false
|
||||||
this[kOnClosed] = []
|
this[kOnClosed] = []
|
||||||
|
this[kWebSocketOptions] = opts?.webSocket ?? {}
|
||||||
|
}
|
||||||
|
|
||||||
|
get webSocketOptions () {
|
||||||
|
return {
|
||||||
|
maxFragments: this[kWebSocketOptions].maxFragments ?? 131072,
|
||||||
|
maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get destroyed () {
|
get destroyed () {
|
||||||
|
|||||||
+2
-2
@@ -19,8 +19,8 @@ const kRemoveClient = Symbol('remove client')
|
|||||||
const kStats = Symbol('stats')
|
const kStats = Symbol('stats')
|
||||||
|
|
||||||
class PoolBase extends DispatcherBase {
|
class PoolBase extends DispatcherBase {
|
||||||
constructor () {
|
constructor (opts) {
|
||||||
super()
|
super(opts)
|
||||||
|
|
||||||
this[kQueue] = new FixedQueue()
|
this[kQueue] = new FixedQueue()
|
||||||
this[kClients] = []
|
this[kClients] = []
|
||||||
|
|||||||
+2
-2
@@ -37,8 +37,6 @@ class Pool extends PoolBase {
|
|||||||
allowH2,
|
allowH2,
|
||||||
...options
|
...options
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super()
|
|
||||||
|
|
||||||
if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
|
if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
|
||||||
throw new InvalidArgumentError('invalid connections')
|
throw new InvalidArgumentError('invalid connections')
|
||||||
}
|
}
|
||||||
@@ -63,6 +61,8 @@ class Pool extends PoolBase {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
super(options)
|
||||||
|
|
||||||
this[kInterceptors] = options.interceptors?.Pool && Array.isArray(options.interceptors.Pool)
|
this[kInterceptors] = options.interceptors?.Pool && Array.isArray(options.interceptors.Pool)
|
||||||
? options.interceptors.Pool
|
? options.interceptors.Pool
|
||||||
: []
|
: []
|
||||||
|
|||||||
+34
@@ -15,6 +15,28 @@ function calculateRetryAfterHeader (retryAfter) {
|
|||||||
return new Date(retryAfter).getTime() - current
|
return new Date(retryAfter).getTime() - current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
|
||||||
|
const contentLength = headers['content-length']
|
||||||
|
if (contentLength == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const length = Number(contentLength)
|
||||||
|
const expectedLength = range.end - range.start + 1
|
||||||
|
if (!Number.isFinite(length) || length !== expectedLength) {
|
||||||
|
return new RequestRetryError('Content-Length mismatch', statusCode, {
|
||||||
|
headers,
|
||||||
|
data: { count: retryCount }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
class RetryHandler {
|
class RetryHandler {
|
||||||
constructor (opts, handlers) {
|
constructor (opts, handlers) {
|
||||||
const { retryOptions, ...dispatchOpts } = opts
|
const { retryOptions, ...dispatchOpts } = opts
|
||||||
@@ -229,6 +251,12 @@ class RetryHandler {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
|
||||||
|
if (contentLengthError != null) {
|
||||||
|
this.abort(contentLengthError)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const { start, size, end = size - 1 } = contentRange
|
const { start, size, end = size - 1 } = contentRange
|
||||||
|
|
||||||
assert(this.start === start, 'content-range mismatch')
|
assert(this.start === start, 'content-range mismatch')
|
||||||
@@ -252,6 +280,12 @@ class RetryHandler {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
|
||||||
|
if (contentLengthError != null) {
|
||||||
|
this.abort(contentLengthError)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const { start, size, end = size - 1 } = range
|
const { start, size, end = size - 1 } = range
|
||||||
assert(
|
assert(
|
||||||
start != null && Number.isFinite(start),
|
start != null && Number.isFinite(start),
|
||||||
|
|||||||
+16
-23
@@ -275,32 +275,25 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
|
|||||||
// If the attribute-name case-insensitively matches the string
|
// If the attribute-name case-insensitively matches the string
|
||||||
// "SameSite", the user agent MUST process the cookie-av as follows:
|
// "SameSite", the user agent MUST process the cookie-av as follows:
|
||||||
|
|
||||||
// 1. Let enforcement be "Default".
|
|
||||||
let enforcement = 'Default'
|
|
||||||
|
|
||||||
const attributeValueLowercase = attributeValue.toLowerCase()
|
const attributeValueLowercase = attributeValue.toLowerCase()
|
||||||
// 2. If cookie-av's attribute-value is a case-insensitive match for
|
|
||||||
// "None", set enforcement to "None".
|
|
||||||
if (attributeValueLowercase.includes('none')) {
|
|
||||||
enforcement = 'None'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. If cookie-av's attribute-value is a case-insensitive match for
|
// 1. If cookie-av's attribute-value is a case-insensitive match for
|
||||||
// "Strict", set enforcement to "Strict".
|
// "None", append an attribute to the cookie-attribute-list with an
|
||||||
if (attributeValueLowercase.includes('strict')) {
|
// attribute-name of "SameSite" and an attribute-value of "None".
|
||||||
enforcement = 'Strict'
|
if (attributeValueLowercase === 'none') {
|
||||||
|
cookieAttributeList.sameSite = 'None'
|
||||||
|
} else if (attributeValueLowercase === 'strict') {
|
||||||
|
// 2. If cookie-av's attribute-value is a case-insensitive match for
|
||||||
|
// "Strict", append an attribute to the cookie-attribute-list with
|
||||||
|
// an attribute-name of "SameSite" and an attribute-value of
|
||||||
|
// "Strict".
|
||||||
|
cookieAttributeList.sameSite = 'Strict'
|
||||||
|
} else if (attributeValueLowercase === 'lax') {
|
||||||
|
// 3. If cookie-av's attribute-value is a case-insensitive match for
|
||||||
|
// "Lax", append an attribute to the cookie-attribute-list with an
|
||||||
|
// attribute-name of "SameSite" and an attribute-value of "Lax".
|
||||||
|
cookieAttributeList.sameSite = 'Lax'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. If cookie-av's attribute-value is a case-insensitive match for
|
|
||||||
// "Lax", set enforcement to "Lax".
|
|
||||||
if (attributeValueLowercase.includes('lax')) {
|
|
||||||
enforcement = 'Lax'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Append an attribute to the cookie-attribute-list with an
|
|
||||||
// attribute-name of "SameSite" and an attribute-value of
|
|
||||||
// enforcement.
|
|
||||||
cookieAttributeList.sameSite = enforcement
|
|
||||||
} else {
|
} else {
|
||||||
cookieAttributeList.unparsed ??= []
|
cookieAttributeList.unparsed ??= []
|
||||||
|
|
||||||
|
|||||||
+79
-9
@@ -105,7 +105,7 @@ function validateCookiePath (path) {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
code < 0x20 || // exclude CTLs (0-31)
|
code < 0x20 || // exclude CTLs (0-31)
|
||||||
code === 0x7F || // DEL
|
code > 0x7E || // exclude DEL and non-ascii
|
||||||
code === 0x3B // ;
|
code === 0x3B // ;
|
||||||
) {
|
) {
|
||||||
throw new Error('Invalid cookie path')
|
throw new Error('Invalid cookie path')
|
||||||
@@ -114,16 +114,80 @@ function validateCookiePath (path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* I have no idea why these values aren't allowed to be honest,
|
* <let-dig> ::= <letter> | <digit>
|
||||||
* but Deno tests these. - Khafra
|
*
|
||||||
|
* <letter> ::= any one of the 52 alphabetic characters A through Z in
|
||||||
|
* upper case and a through z in lower case
|
||||||
|
*
|
||||||
|
* <digit> ::= any one of the ten digits 0 through 9r
|
||||||
|
*
|
||||||
|
* @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
|
||||||
|
* @param {number} code
|
||||||
|
*/
|
||||||
|
function isLetterOrDigit (code) {
|
||||||
|
return (
|
||||||
|
(code >= 0x30 && code <= 0x39) || // 0-9
|
||||||
|
(code >= 0x41 && code <= 0x5A) || // A-Z
|
||||||
|
(code >= 0x61 && code <= 0x7A) // a-z
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a cookie domain against the "preferred name syntax".
|
||||||
|
*
|
||||||
|
* <domain> ::= <subdomain> | " "
|
||||||
|
* <subdomain> ::= <label> | <subdomain> "." <label>
|
||||||
|
* <label> ::= <let-dig> [ [ <ldh-str> ] <let-dig> ]
|
||||||
|
* <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
|
||||||
|
* <let-dig-hyp> ::= <let-dig> | "-"
|
||||||
|
*
|
||||||
|
* @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
|
||||||
|
* @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
|
||||||
|
* @see https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4
|
||||||
* @param {string} domain
|
* @param {string} domain
|
||||||
*/
|
*/
|
||||||
function validateCookieDomain (domain) {
|
function validateCookieDomain (domain) {
|
||||||
if (
|
// <domain> ::= <subdomain> | " "
|
||||||
domain.startsWith('-') ||
|
if (domain === ' ') {
|
||||||
domain.endsWith('.') ||
|
return
|
||||||
domain.endsWith('-')
|
}
|
||||||
) {
|
|
||||||
|
if (domain.length > 255) {
|
||||||
|
throw new Error('Invalid cookie domain')
|
||||||
|
}
|
||||||
|
|
||||||
|
let labelLength = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < domain.length; ++i) {
|
||||||
|
const code = domain.charCodeAt(i)
|
||||||
|
|
||||||
|
if (code === 0x2E) {
|
||||||
|
if (labelLength === 0) {
|
||||||
|
throw new Error('Invalid cookie domain')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.charCodeAt(i - 1) === 0x2D) { // "-"
|
||||||
|
throw new Error('Invalid cookie domain')
|
||||||
|
}
|
||||||
|
|
||||||
|
labelLength = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelLength === 0 && !isLetterOrDigit(code)) {
|
||||||
|
throw new Error('Invalid cookie domain')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLetterOrDigit(code) && code !== 0x2D) { // "-"
|
||||||
|
throw new Error('Invalid cookie domain')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++labelLength > 63) {
|
||||||
|
throw new Error('Invalid cookie domain')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelLength === 0 || domain.charCodeAt(domain.length - 1) === 0x2D) { // "-"
|
||||||
throw new Error('Invalid cookie domain')
|
throw new Error('Invalid cookie domain')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,7 +330,13 @@ function stringify (cookie) {
|
|||||||
|
|
||||||
const [key, ...value] = part.split('=')
|
const [key, ...value] = part.split('=')
|
||||||
|
|
||||||
out.push(`${key.trim()}=${value.join('=')}`)
|
const trimmedKey = key.trim()
|
||||||
|
const joinedValue = value.join('=')
|
||||||
|
|
||||||
|
validateCookieName(trimmedKey)
|
||||||
|
validateCookieValue(joinedValue)
|
||||||
|
|
||||||
|
out.push(`${trimmedKey}=${joinedValue}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return out.join('; ')
|
return out.join('; ')
|
||||||
|
|||||||
+13
-31
@@ -8,40 +8,35 @@ const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
|
|||||||
const kBuffer = Symbol('kBuffer')
|
const kBuffer = Symbol('kBuffer')
|
||||||
const kLength = Symbol('kLength')
|
const kLength = Symbol('kLength')
|
||||||
|
|
||||||
// Default maximum decompressed message size: 4 MB
|
|
||||||
const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
|
|
||||||
|
|
||||||
class PerMessageDeflate {
|
class PerMessageDeflate {
|
||||||
/** @type {import('node:zlib').InflateRaw} */
|
/** @type {import('node:zlib').InflateRaw} */
|
||||||
#inflate
|
#inflate
|
||||||
|
|
||||||
#options = {}
|
#options = {}
|
||||||
|
|
||||||
/** @type {boolean} */
|
#maxPayloadSize = 0
|
||||||
#aborted = false
|
|
||||||
|
|
||||||
/** @type {Function|null} */
|
|
||||||
#currentCallback = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Map<string, string>} extensions
|
* @param {Map<string, string>} extensions
|
||||||
*/
|
*/
|
||||||
constructor (extensions) {
|
constructor (extensions, options) {
|
||||||
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
|
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
|
||||||
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
|
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
|
||||||
|
|
||||||
|
this.#maxPayloadSize = options.maxPayloadSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decompress a compressed payload.
|
||||||
|
* @param {Buffer} chunk Compressed data
|
||||||
|
* @param {boolean} fin Final fragment flag
|
||||||
|
* @param {Function} callback Callback function
|
||||||
|
*/
|
||||||
decompress (chunk, fin, callback) {
|
decompress (chunk, fin, callback) {
|
||||||
// An endpoint uses the following algorithm to decompress a message.
|
// An endpoint uses the following algorithm to decompress a message.
|
||||||
// 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
|
// 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
|
||||||
// payload of the message.
|
// payload of the message.
|
||||||
// 2. Decompress the resulting data using DEFLATE.
|
// 2. Decompress the resulting data using DEFLATE.
|
||||||
|
|
||||||
if (this.#aborted) {
|
|
||||||
callback(new MessageSizeExceededError())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.#inflate) {
|
if (!this.#inflate) {
|
||||||
let windowBits = Z_DEFAULT_WINDOWBITS
|
let windowBits = Z_DEFAULT_WINDOWBITS
|
||||||
|
|
||||||
@@ -64,23 +59,12 @@ class PerMessageDeflate {
|
|||||||
this.#inflate[kLength] = 0
|
this.#inflate[kLength] = 0
|
||||||
|
|
||||||
this.#inflate.on('data', (data) => {
|
this.#inflate.on('data', (data) => {
|
||||||
if (this.#aborted) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#inflate[kLength] += data.length
|
this.#inflate[kLength] += data.length
|
||||||
|
|
||||||
if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) {
|
if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
|
||||||
this.#aborted = true
|
callback(new MessageSizeExceededError())
|
||||||
this.#inflate.removeAllListeners()
|
this.#inflate.removeAllListeners()
|
||||||
this.#inflate.destroy()
|
|
||||||
this.#inflate = null
|
this.#inflate = null
|
||||||
|
|
||||||
if (this.#currentCallback) {
|
|
||||||
const cb = this.#currentCallback
|
|
||||||
this.#currentCallback = null
|
|
||||||
cb(new MessageSizeExceededError())
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,14 +77,13 @@ class PerMessageDeflate {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#currentCallback = callback
|
|
||||||
this.#inflate.write(chunk)
|
this.#inflate.write(chunk)
|
||||||
if (fin) {
|
if (fin) {
|
||||||
this.#inflate.write(tail)
|
this.#inflate.write(tail)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#inflate.flush(() => {
|
this.#inflate.flush(() => {
|
||||||
if (this.#aborted || !this.#inflate) {
|
if (!this.#inflate) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +91,6 @@ class PerMessageDeflate {
|
|||||||
|
|
||||||
this.#inflate[kBuffer].length = 0
|
this.#inflate[kBuffer].length = 0
|
||||||
this.#inflate[kLength] = 0
|
this.#inflate[kLength] = 0
|
||||||
this.#currentCallback = null
|
|
||||||
|
|
||||||
callback(null, full)
|
callback(null, full)
|
||||||
})
|
})
|
||||||
|
|||||||
+109
-23
@@ -18,6 +18,12 @@ const {
|
|||||||
const { WebsocketFrameSend } = require('./frame')
|
const { WebsocketFrameSend } = require('./frame')
|
||||||
const { closeWebSocketConnection } = require('./connection')
|
const { closeWebSocketConnection } = require('./connection')
|
||||||
const { PerMessageDeflate } = require('./permessage-deflate')
|
const { PerMessageDeflate } = require('./permessage-deflate')
|
||||||
|
const { MessageSizeExceededError } = require('../../core/errors')
|
||||||
|
|
||||||
|
function failWebsocketConnectionWithCode (ws, code, reason) {
|
||||||
|
closeWebSocketConnection(ws, code, reason, Buffer.byteLength(reason))
|
||||||
|
failWebsocketConnection(ws, reason)
|
||||||
|
}
|
||||||
|
|
||||||
// This code was influenced by ws released under the MIT license.
|
// This code was influenced by ws released under the MIT license.
|
||||||
// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||||
@@ -26,6 +32,7 @@ const { PerMessageDeflate } = require('./permessage-deflate')
|
|||||||
|
|
||||||
class ByteParser extends Writable {
|
class ByteParser extends Writable {
|
||||||
#buffers = []
|
#buffers = []
|
||||||
|
#fragmentsBytes = 0
|
||||||
#byteOffset = 0
|
#byteOffset = 0
|
||||||
#loop = false
|
#loop = false
|
||||||
|
|
||||||
@@ -37,18 +44,27 @@ class ByteParser extends Writable {
|
|||||||
/** @type {Map<string, PerMessageDeflate>} */
|
/** @type {Map<string, PerMessageDeflate>} */
|
||||||
#extensions
|
#extensions
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
#maxFragments
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
#maxPayloadSize
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('./websocket').WebSocket} ws
|
* @param {import('./websocket').WebSocket} ws
|
||||||
* @param {Map<string, string>|null} extensions
|
* @param {Map<string, string>|null} extensions
|
||||||
|
* @param {{ maxFragments?: number, maxPayloadSize?: number }} [options]
|
||||||
*/
|
*/
|
||||||
constructor (ws, extensions) {
|
constructor (ws, extensions, options = {}) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.ws = ws
|
this.ws = ws
|
||||||
this.#extensions = extensions == null ? new Map() : extensions
|
this.#extensions = extensions == null ? new Map() : extensions
|
||||||
|
this.#maxFragments = options.maxFragments ?? 0
|
||||||
|
this.#maxPayloadSize = options.maxPayloadSize ?? 0
|
||||||
|
|
||||||
if (this.#extensions.has('permessage-deflate')) {
|
if (this.#extensions.has('permessage-deflate')) {
|
||||||
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
|
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +80,19 @@ class ByteParser extends Writable {
|
|||||||
this.run(callback)
|
this.run(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#validatePayloadLength () {
|
||||||
|
if (
|
||||||
|
this.#maxPayloadSize > 0 &&
|
||||||
|
!isControlFrame(this.#info.opcode) &&
|
||||||
|
this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize
|
||||||
|
) {
|
||||||
|
failWebsocketConnectionWithCode(this.ws, 1009, 'Payload size exceeds maximum allowed size')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs whenever a new chunk is received.
|
* Runs whenever a new chunk is received.
|
||||||
* Callback is called whenever there are no more chunks buffering,
|
* Callback is called whenever there are no more chunks buffering,
|
||||||
@@ -152,6 +181,10 @@ class ByteParser extends Writable {
|
|||||||
if (payloadLength <= 125) {
|
if (payloadLength <= 125) {
|
||||||
this.#info.payloadLength = payloadLength
|
this.#info.payloadLength = payloadLength
|
||||||
this.#state = parserStates.READ_DATA
|
this.#state = parserStates.READ_DATA
|
||||||
|
|
||||||
|
if (!this.#validatePayloadLength()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
} else if (payloadLength === 126) {
|
} else if (payloadLength === 126) {
|
||||||
this.#state = parserStates.PAYLOADLENGTH_16
|
this.#state = parserStates.PAYLOADLENGTH_16
|
||||||
} else if (payloadLength === 127) {
|
} else if (payloadLength === 127) {
|
||||||
@@ -176,6 +209,10 @@ class ByteParser extends Writable {
|
|||||||
|
|
||||||
this.#info.payloadLength = buffer.readUInt16BE(0)
|
this.#info.payloadLength = buffer.readUInt16BE(0)
|
||||||
this.#state = parserStates.READ_DATA
|
this.#state = parserStates.READ_DATA
|
||||||
|
|
||||||
|
if (!this.#validatePayloadLength()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
} else if (this.#state === parserStates.PAYLOADLENGTH_64) {
|
} else if (this.#state === parserStates.PAYLOADLENGTH_64) {
|
||||||
if (this.#byteOffset < 8) {
|
if (this.#byteOffset < 8) {
|
||||||
return callback()
|
return callback()
|
||||||
@@ -198,6 +235,10 @@ class ByteParser extends Writable {
|
|||||||
|
|
||||||
this.#info.payloadLength = lower
|
this.#info.payloadLength = lower
|
||||||
this.#state = parserStates.READ_DATA
|
this.#state = parserStates.READ_DATA
|
||||||
|
|
||||||
|
if (!this.#validatePayloadLength()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
} else if (this.#state === parserStates.READ_DATA) {
|
} else if (this.#state === parserStates.READ_DATA) {
|
||||||
if (this.#byteOffset < this.#info.payloadLength) {
|
if (this.#byteOffset < this.#info.payloadLength) {
|
||||||
return callback()
|
return callback()
|
||||||
@@ -210,42 +251,58 @@ class ByteParser extends Writable {
|
|||||||
this.#state = parserStates.INFO
|
this.#state = parserStates.INFO
|
||||||
} else {
|
} else {
|
||||||
if (!this.#info.compressed) {
|
if (!this.#info.compressed) {
|
||||||
this.#fragments.push(body)
|
if (!this.writeFragments(body)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
|
||||||
|
failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// If the frame is not fragmented, a message has been received.
|
// If the frame is not fragmented, a message has been received.
|
||||||
// If the frame is fragmented, it will terminate with a fin bit set
|
// If the frame is fragmented, it will terminate with a fin bit set
|
||||||
// and an opcode of 0 (continuation), therefore we handle that when
|
// and an opcode of 0 (continuation), therefore we handle that when
|
||||||
// parsing continuation frames, not here.
|
// parsing continuation frames, not here.
|
||||||
if (!this.#info.fragmented && this.#info.fin) {
|
if (!this.#info.fragmented && this.#info.fin) {
|
||||||
const fullMessage = Buffer.concat(this.#fragments)
|
websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments())
|
||||||
websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
|
|
||||||
this.#fragments.length = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#state = parserStates.INFO
|
this.#state = parserStates.INFO
|
||||||
} else {
|
} else {
|
||||||
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
|
this.#extensions.get('permessage-deflate').decompress(
|
||||||
if (error) {
|
body,
|
||||||
failWebsocketConnection(this.ws, error.message)
|
this.#info.fin,
|
||||||
return
|
(error, data) => {
|
||||||
}
|
if (error) {
|
||||||
|
const code = error instanceof MessageSizeExceededError ? 1009 : 1007
|
||||||
|
failWebsocketConnectionWithCode(this.ws, code, error.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.#fragments.push(data)
|
if (!this.writeFragments(data)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
|
||||||
|
failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#info.fin) {
|
||||||
|
this.#state = parserStates.INFO
|
||||||
|
this.#loop = true
|
||||||
|
this.run(callback)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments())
|
||||||
|
|
||||||
if (!this.#info.fin) {
|
|
||||||
this.#state = parserStates.INFO
|
|
||||||
this.#loop = true
|
this.#loop = true
|
||||||
|
this.#state = parserStates.INFO
|
||||||
this.run(callback)
|
this.run(callback)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
websocketMessageReceived(this.ws, this.#info.binaryType, Buffer.concat(this.#fragments))
|
|
||||||
|
|
||||||
this.#loop = true
|
|
||||||
this.#state = parserStates.INFO
|
|
||||||
this.#fragments.length = 0
|
|
||||||
this.run(callback)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.#loop = false
|
this.#loop = false
|
||||||
break
|
break
|
||||||
@@ -297,6 +354,35 @@ class ByteParser extends Writable {
|
|||||||
return buffer
|
return buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeFragments (fragment) {
|
||||||
|
if (
|
||||||
|
this.#maxFragments > 0 &&
|
||||||
|
this.#fragments.length === this.#maxFragments
|
||||||
|
) {
|
||||||
|
failWebsocketConnectionWithCode(this.ws, 1008, 'Too many message fragments')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#fragmentsBytes += fragment.length
|
||||||
|
this.#fragments.push(fragment)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeFragments () {
|
||||||
|
const fragments = this.#fragments
|
||||||
|
|
||||||
|
if (fragments.length === 1) {
|
||||||
|
this.#fragmentsBytes = 0
|
||||||
|
return fragments.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = Buffer.concat(fragments, this.#fragmentsBytes)
|
||||||
|
this.#fragments = []
|
||||||
|
this.#fragmentsBytes = 0
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
parseCloseBody (data) {
|
parseCloseBody (data) {
|
||||||
assert(data.length !== 1)
|
assert(data.length !== 1)
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -435,7 +435,14 @@ class WebSocket extends EventTarget {
|
|||||||
// once this happens, the connection is open
|
// once this happens, the connection is open
|
||||||
this[kResponse] = response
|
this[kResponse] = response
|
||||||
|
|
||||||
const parser = new ByteParser(this, parsedExtensions)
|
const webSocketOptions = this[kController]?.dispatcher?.webSocketOptions
|
||||||
|
const maxFragments = webSocketOptions?.maxFragments
|
||||||
|
const maxPayloadSize = webSocketOptions?.maxPayloadSize
|
||||||
|
|
||||||
|
const parser = new ByteParser(this, parsedExtensions, {
|
||||||
|
maxFragments,
|
||||||
|
maxPayloadSize
|
||||||
|
})
|
||||||
parser.on('drain', onParserDrain)
|
parser.on('drain', onParserDrain)
|
||||||
parser.on('error', onParserError.bind(this))
|
parser.on('error', onParserError.bind(this))
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "undici",
|
"name": "undici",
|
||||||
"version": "6.24.1",
|
"version": "6.28.0",
|
||||||
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
||||||
"homepage": "https://undici.nodejs.org",
|
"homepage": "https://undici.nodejs.org",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|||||||
+17
@@ -78,6 +78,8 @@ export declare namespace Client {
|
|||||||
localAddress?: string;
|
localAddress?: string;
|
||||||
/** Max response body size in bytes, -1 is disabled */
|
/** Max response body size in bytes, -1 is disabled */
|
||||||
maxResponseSize?: number;
|
maxResponseSize?: number;
|
||||||
|
/** WebSocket-specific options */
|
||||||
|
webSocket?: Client.WebSocketOptions;
|
||||||
/** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */
|
/** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */
|
||||||
autoSelectFamily?: boolean;
|
autoSelectFamily?: boolean;
|
||||||
/** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */
|
/** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */
|
||||||
@@ -103,6 +105,21 @@ export declare namespace Client {
|
|||||||
bytesWritten?: number
|
bytesWritten?: number
|
||||||
bytesRead?: number
|
bytesRead?: number
|
||||||
}
|
}
|
||||||
|
export interface WebSocketOptions {
|
||||||
|
/**
|
||||||
|
* Maximum number of fragments in a message.
|
||||||
|
* Set to 0 to disable the limit.
|
||||||
|
* @default 131072
|
||||||
|
*/
|
||||||
|
maxFragments?: number;
|
||||||
|
/**
|
||||||
|
* Maximum allowed payload size in bytes for WebSocket messages.
|
||||||
|
* Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages.
|
||||||
|
* Set to 0 to disable the limit.
|
||||||
|
* @default 134217728 (128 MB)
|
||||||
|
*/
|
||||||
|
maxPayloadSize?: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Client;
|
export default Client;
|
||||||
|
|||||||
Reference in New Issue
Block a user