@cypress/github-action 7.4.0 → 7.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +60 -81
  2. package/dist/index.js +708 -238
  3. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -15027,6 +15027,20 @@ var escClose = '\0CLOSE'+Math.random()+'\0';
15027
15027
  var escComma = '\0COMMA'+Math.random()+'\0';
15028
15028
  var escPeriod = '\0PERIOD'+Math.random()+'\0';
15029
15029
 
15030
+ var EXPANSION_MAX = 100000
15031
+
15032
+ // `EXPANSION_MAX` caps the *number* of expansions, but not their length. An
15033
+ // input like `'{a,b}'.repeat(1500)` stays under that count - its output is
15034
+ // truncated to 100k results - while making every result ~1500 characters
15035
+ // long. The result set, and the intermediate arrays built while combining
15036
+ // brace sets, then grow large enough to exhaust memory and crash the process
15037
+ // (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of
15038
+ // characters the accumulator may hold at any point, so memory stays flat no
15039
+ // matter how many brace groups are chained. The limit sits well above any
15040
+ // realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
15041
+ // characters) so legitimate input is unaffected.
15042
+ var EXPANSION_MAX_LENGTH = 4000000
15043
+
15030
15044
  function numeric(str) {
15031
15045
  return parseInt(str, 10) == str
15032
15046
  ? parseInt(str, 10)
@@ -15080,10 +15094,14 @@ function parseCommaParts(str) {
15080
15094
  return parts;
15081
15095
  }
15082
15096
 
15083
- function expandTop(str) {
15097
+ function expandTop(str, options) {
15084
15098
  if (!str)
15085
15099
  return [];
15086
15100
 
15101
+ options = options || {};
15102
+ var max = options.max == null ? EXPANSION_MAX : options.max;
15103
+ var maxLength = options.maxLength == null ? EXPANSION_MAX_LENGTH : options.maxLength;
15104
+
15087
15105
  // I don't know why Bash 4.3 does this, but it does.
15088
15106
  // Anything starting with {} will have the first two bytes preserved
15089
15107
  // but *only* at the top level, so {},a}b will not expand to anything,
@@ -15094,7 +15112,7 @@ function expandTop(str) {
15094
15112
  str = '\\{\\}' + str.substr(2);
15095
15113
  }
15096
15114
 
15097
- return expand(escapeBraces(str), true).map(unescapeBraces);
15115
+ return expand(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
15098
15116
  }
15099
15117
 
15100
15118
  function identity(e) {
@@ -15115,106 +15133,270 @@ function gte(i, y) {
15115
15133
  return i >= y;
15116
15134
  }
15117
15135
 
15118
- function expand(str, isTop) {
15119
- var expansions = [];
15120
-
15121
- var m = balanced('{', '}', str);
15122
- if (!m || /\$$/.test(m.pre)) return [str];
15123
-
15124
- var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
15125
- var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
15126
- var isSequence = isNumericSequence || isAlphaSequence;
15127
- var isOptions = m.body.indexOf(',') >= 0;
15128
- if (!isSequence && !isOptions) {
15129
- // {a},b}
15130
- if (m.post.match(/,(?!,).*\}/)) {
15131
- str = m.pre + '{' + m.body + escClose + m.post;
15132
- return expand(str);
15136
+ // Build `{ acc[a] + pre + values[v] }` for every combination, capping the
15137
+ // number of results at `max` and the total number of characters at `maxLength`.
15138
+ // This is the one place output grows, so bounding it here keeps the single
15139
+ // accumulator - and therefore memory - flat regardless of how many brace groups
15140
+ // are combined (CVE-2026-14257).
15141
+ //
15142
+ // `base[a]` is the length of the part of `acc[a]` that predates the current
15143
+ // empty-drop baseline (see `expand`). The matching baselines for the results
15144
+ // are appended to `outBase`, which the caller carries forward alongside them.
15145
+ function combine(
15146
+ acc,
15147
+ base,
15148
+ pre,
15149
+ values,
15150
+ max,
15151
+ maxLength,
15152
+ dropEmpties,
15153
+ outBase
15154
+ ) {
15155
+ var out = []
15156
+ var length = 0
15157
+ for (var a = 0; a < acc.length; a++) {
15158
+ for (var v = 0; v < values.length; v++) {
15159
+ if (out.length >= max) return out
15160
+ var expansion = acc[a] + pre + values[v]
15161
+ // Bash drops empty results at the top level. Skip them before they count
15162
+ // against `max`, so `max` bounds the number of *kept* results. "Empty"
15163
+ // means "adds nothing past the baseline", not "empty overall".
15164
+ if (dropEmpties && expansion.length === base[a]) continue
15165
+ if (length + expansion.length > maxLength) return out
15166
+ out.push(expansion)
15167
+ outBase.push(base[a])
15168
+ length += expansion.length
15133
15169
  }
15134
- return [str];
15135
15170
  }
15171
+ return out
15172
+ }
15136
15173
 
15137
- var n;
15138
- if (isSequence) {
15139
- n = m.body.split(/\.\./);
15140
- } else {
15141
- n = parseCommaParts(m.body);
15142
- if (n.length === 1) {
15143
- // x{{a,b}}y ==> x{a}y x{b}y
15144
- n = expand(n[0], false).map(embrace);
15145
- if (n.length === 1) {
15146
- var post = m.post.length
15147
- ? expand(m.post, false)
15148
- : [''];
15149
- return post.map(function(p) {
15150
- return m.pre + n[0] + p;
15151
- });
15174
+ // The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
15175
+ // sequence body.
15176
+ function expandSequence(
15177
+ body,
15178
+ isAlphaSequence,
15179
+ max,
15180
+ maxLength
15181
+ ) {
15182
+ var n = body.split(/\.\./)
15183
+ var N = []
15184
+ // A sequence body always splits into two or three parts, but the compiler
15185
+ // can't know that.
15186
+ /* c8 ignore start */
15187
+ if (n[0] === undefined || n[1] === undefined) {
15188
+ return N
15189
+ }
15190
+ /* c8 ignore stop */
15191
+ var x = numeric(n[0])
15192
+ var y = numeric(n[1])
15193
+ var width = Math.max(n[0].length, n[1].length)
15194
+ var incr =
15195
+ n.length === 3 && n[2] !== undefined ?
15196
+ Math.max(Math.abs(numeric(n[2])), 1)
15197
+ : 1
15198
+ var test = lte
15199
+ var reverse = y < x
15200
+ if (reverse) {
15201
+ incr *= -1
15202
+ test = gte
15203
+ }
15204
+ var pad = n.some(isPadded)
15205
+
15206
+ var length = 0
15207
+ for (var i = x; test(i, y) && N.length < max; i += incr) {
15208
+ var c
15209
+ if (isAlphaSequence) {
15210
+ c = String.fromCharCode(i)
15211
+ if (c === '\\') {
15212
+ c = ''
15213
+ }
15214
+ } else {
15215
+ c = String(i)
15216
+ if (pad) {
15217
+ var need = width - c.length
15218
+ if (need > 0) {
15219
+ var z = new Array(need + 1).join('0')
15220
+ if (i < 0) {
15221
+ c = '-' + z + c.slice(1)
15222
+ } else {
15223
+ c = z + c
15224
+ }
15225
+ }
15152
15226
  }
15153
15227
  }
15228
+ if (length + c.length > maxLength) break
15229
+ N.push(c)
15230
+ length += c.length
15154
15231
  }
15232
+ return N
15233
+ }
15234
+
15235
+ function expand(
15236
+ str,
15237
+ max,
15238
+ maxLength,
15239
+ isTop
15240
+ ) {
15241
+ // Consume the string's top-level brace groups left to right, threading a
15242
+ // running set of combined prefixes (`acc`). Expanding the tail iteratively -
15243
+ // rather than recursing on `m.post` once per group - keeps the native stack
15244
+ // depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no
15245
+ // longer overflow the stack, and leaves a single accumulator whose size
15246
+ // `maxLength` bounds directly (CVE-2026-14257).
15247
+ var acc = ['']
15248
+
15249
+ // Bash drops empty results, but only when the *first* group of the run is a
15250
+ // comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
15251
+ // is on the final strings, so it is applied to whichever `combine` produces
15252
+ // them (the one with no brace set left in the tail).
15253
+ //
15254
+ // The old implementation recursed on `m.post`, so the drop tested only the
15255
+ // expansion of the current call's substring. The `{a},b}` rewrite below turns
15256
+ // `isTop` back on part-way through a string, starting a fresh such run, so
15257
+ // the drop must ignore whatever `acc` already holds from earlier groups.
15258
+ // `accBase[a]` records how much of `acc[a]` predates the current run;
15259
+ // `combine` treats an expansion as empty when it adds nothing past that.
15260
+ var accBase = [0]
15261
+ var dropEmpties = false
15262
+ var firstGroup = true
15263
+ var nextBase
15264
+
15265
+ for (;;) {
15266
+ var m = balanced('{', '}', str);
15267
+
15268
+ // No brace set left: the rest of the string is literal.
15269
+ if (!m) {
15270
+ return combine(acc, accBase, str, [''], max, maxLength, dropEmpties, [])
15271
+ }
15272
+
15273
+ // no need to expand pre, since it is guaranteed to be free of brace-sets
15274
+ var pre = m.pre;
15275
+
15276
+ // For compatibility reasons, `${` is not eligible for brace expansion, and
15277
+ // on the 1.x line it suppresses expansion of the rest of the string too:
15278
+ // the whole remainder is literal. The 2.x and 5.x lines instead keep
15279
+ // expanding the tail, which is what bash does, but changing that here would
15280
+ // be a breaking change for 1.x consumers. Routed through `combine` so the
15281
+ // result is still bounded by `max` and `maxLength`.
15282
+ if (/\$$/.test(pre)) {
15283
+ return combine(acc, accBase, str, [''], max, maxLength, dropEmpties, [])
15284
+ }
15285
+
15286
+ var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
15287
+ var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
15288
+ var isSequence = isNumericSequence || isAlphaSequence;
15289
+ var isOptions = m.body.indexOf(',') >= 0;
15290
+ if (!isSequence && !isOptions) {
15291
+ // {a},b}
15292
+ if (m.post.match(/,(?!,).*\}/)) {
15293
+ str = m.pre + '{' + m.body + escClose + m.post;
15294
+ // The rewritten string is expanded as if it were a fresh top-level one,
15295
+ // so start a new empty-drop run: anchor the baseline at what `acc`
15296
+ // holds now, and let the next expanding group decide whether to drop.
15297
+ isTop = true
15298
+ firstGroup = true
15299
+ dropEmpties = false
15300
+ accBase = []
15301
+ for (var b = 0; b < acc.length; b++) {
15302
+ accBase.push(acc[b].length)
15303
+ }
15304
+ continue
15305
+ }
15306
+ // Nothing here expands, so the whole remaining string is literal.
15307
+ return combine(
15308
+ acc,
15309
+ accBase,
15310
+ pre + '{' + m.body + '}' + m.post,
15311
+ [''],
15312
+ max,
15313
+ maxLength,
15314
+ dropEmpties,
15315
+ []
15316
+ )
15317
+ }
15155
15318
 
15156
- // at this point, n is the parts, and we know it's not a comma set
15157
- // with a single entry.
15319
+ if (firstGroup) {
15320
+ dropEmpties = isTop && !isSequence
15321
+ firstGroup = false
15322
+ }
15158
15323
 
15159
- // no need to expand pre, since it is guaranteed to be free of brace-sets
15160
- var pre = m.pre;
15161
- var post = m.post.length
15162
- ? expand(m.post, false)
15163
- : [''];
15164
-
15165
- var N;
15166
-
15167
- if (isSequence) {
15168
- var x = numeric(n[0]);
15169
- var y = numeric(n[1]);
15170
- var width = Math.max(n[0].length, n[1].length)
15171
- var incr = n.length == 3
15172
- ? Math.max(Math.abs(numeric(n[2])), 1)
15173
- : 1;
15174
- var test = lte;
15175
- var reverse = y < x;
15176
- if (reverse) {
15177
- incr *= -1;
15178
- test = gte;
15179
- }
15180
- var pad = n.some(isPadded);
15181
-
15182
- N = [];
15183
-
15184
- for (var i = x; test(i, y); i += incr) {
15185
- var c;
15186
- if (isAlphaSequence) {
15187
- c = String.fromCharCode(i);
15188
- if (c === '\\')
15189
- c = '';
15190
- } else {
15191
- c = String(i);
15192
- if (pad) {
15193
- var need = width - c.length;
15194
- if (need > 0) {
15195
- var z = new Array(need + 1).join('0');
15196
- if (i < 0)
15197
- c = '-' + z + c.slice(1);
15198
- else
15199
- c = z + c;
15324
+ var values;
15325
+ if (isSequence) {
15326
+ values = expandSequence(m.body, isAlphaSequence, max, maxLength);
15327
+ } else {
15328
+ var n = parseCommaParts(m.body);
15329
+ if (n.length === 1 && n[0] !== undefined) {
15330
+ // x{{a,b}}y ==> x{a}y x{b}y
15331
+ n = expand(n[0], max, maxLength, false).map(embrace);
15332
+ //XXX is this necessary? Can't seem to hit it in tests.
15333
+ /* c8 ignore start */
15334
+ if (n.length === 1) {
15335
+ nextBase = []
15336
+ acc = combine(
15337
+ acc,
15338
+ accBase,
15339
+ pre + n[0],
15340
+ [''],
15341
+ max,
15342
+ maxLength,
15343
+ dropEmpties && !m.post.length,
15344
+ nextBase
15345
+ )
15346
+ accBase = nextBase
15347
+ if (!m.post.length) break
15348
+ str = m.post
15349
+ continue
15350
+ }
15351
+ /* c8 ignore stop */
15352
+ }
15353
+
15354
+ // Values that `combine` is going to drop as empty produce no result, so
15355
+ // they must not count against `max` - otherwise `{a,,b}` with `max: 2`
15356
+ // would stop at `['a', '']` and yield one result instead of two. Skipping
15357
+ // them outright keeps `values` bounded while leaving `max` a bound on
15358
+ // *kept* results. A value is dropped when it adds nothing past the
15359
+ // baseline, which is what `combine` tests.
15360
+ var dropsEmpties = dropEmpties && !m.post.length && !pre
15361
+ for (var d = 0; dropsEmpties && d < acc.length; d++) {
15362
+ if (acc[d].length !== accBase[d]) {
15363
+ dropsEmpties = false
15364
+ }
15365
+ }
15366
+
15367
+ values = []
15368
+ var valuesLength = 0
15369
+ outer: for (var j = 0; j < n.length; j++) {
15370
+ var expanded = expand(n[j], max, maxLength, false)
15371
+ for (var k = 0; k < expanded.length; k++) {
15372
+ var v = expanded[k]
15373
+ if (dropsEmpties && !v) continue
15374
+ if (values.length >= max || valuesLength + v.length > maxLength) {
15375
+ break outer
15200
15376
  }
15377
+ values.push(v)
15378
+ valuesLength += v.length
15201
15379
  }
15202
15380
  }
15203
- N.push(c);
15204
15381
  }
15205
- } else {
15206
- N = concatMap(n, function(el) { return expand(el, false) });
15207
- }
15208
15382
 
15209
- for (var j = 0; j < N.length; j++) {
15210
- for (var k = 0; k < post.length; k++) {
15211
- var expansion = pre + N[j] + post[k];
15212
- if (!isTop || isSequence || expansion)
15213
- expansions.push(expansion);
15214
- }
15383
+ nextBase = []
15384
+ acc = combine(
15385
+ acc,
15386
+ accBase,
15387
+ pre,
15388
+ values,
15389
+ max,
15390
+ maxLength,
15391
+ dropEmpties && !m.post.length,
15392
+ nextBase
15393
+ )
15394
+ accBase = nextBase
15395
+ if (!m.post.length) break
15396
+ str = m.post
15215
15397
  }
15216
15398
 
15217
- return expansions;
15399
+ return acc
15218
15400
  }
15219
15401
 
15220
15402
 
@@ -35220,7 +35402,13 @@ function processHeader (request, key, val) {
35220
35402
  } else if (typeof val[i] === 'object') {
35221
35403
  throw new InvalidArgumentError(`invalid ${key} header`)
35222
35404
  } else {
35223
- arr.push(`${val[i]}`)
35405
+ // Coerce primitives (and reject unsafe coercions such as functions
35406
+ // with a crafted toString/Symbol.toPrimitive).
35407
+ const str = `${val[i]}`
35408
+ if (!isValidHeaderValue(str)) {
35409
+ throw new InvalidArgumentError(`invalid ${key} header`)
35410
+ }
35411
+ arr.push(str)
35224
35412
  }
35225
35413
  }
35226
35414
  val = arr
@@ -35231,7 +35419,12 @@ function processHeader (request, key, val) {
35231
35419
  } else if (val === null) {
35232
35420
  val = ''
35233
35421
  } else {
35422
+ // Coerce primitives (and reject unsafe coercions such as functions
35423
+ // with a crafted toString/Symbol.toPrimitive).
35234
35424
  val = `${val}`
35425
+ if (!isValidHeaderValue(val)) {
35426
+ throw new InvalidArgumentError(`invalid ${key} header`)
35427
+ }
35235
35428
  }
35236
35429
 
35237
35430
  if (headerName === 'host') {
@@ -36268,8 +36461,6 @@ function defaultFactory (origin, opts) {
36268
36461
 
36269
36462
  class Agent extends DispatcherBase {
36270
36463
  constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) {
36271
- super()
36272
-
36273
36464
  if (typeof factory !== 'function') {
36274
36465
  throw new InvalidArgumentError('factory must be a function.')
36275
36466
  }
@@ -36282,6 +36473,8 @@ class Agent extends DispatcherBase {
36282
36473
  throw new InvalidArgumentError('maxRedirections must be a positive number')
36283
36474
  }
36284
36475
 
36476
+ super(options)
36477
+
36285
36478
  if (connect && typeof connect !== 'function') {
36286
36479
  connect = { ...connect }
36287
36480
  }
@@ -36608,6 +36801,7 @@ const {
36608
36801
  RequestContentLengthMismatchError,
36609
36802
  ResponseContentLengthMismatchError,
36610
36803
  RequestAbortedError,
36804
+ InvalidArgumentError,
36611
36805
  HeadersTimeoutError,
36612
36806
  HeadersOverflowError,
36613
36807
  SocketError,
@@ -36655,6 +36849,9 @@ const EMPTY_BUF = Buffer.alloc(0)
36655
36849
  const FastBuffer = Buffer[Symbol.species]
36656
36850
  const addListener = util.addListener
36657
36851
  const removeAllListeners = util.removeAllListeners
36852
+ const kIdleSocketValidation = Symbol('kIdleSocketValidation')
36853
+ const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout')
36854
+ const kSocketUsed = Symbol('kSocketUsed')
36658
36855
 
36659
36856
  let extractBody
36660
36857
 
@@ -36877,29 +37074,71 @@ class Parser {
36877
37074
 
36878
37075
  const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr
36879
37076
 
36880
- if (ret === constants.ERROR.PAUSED_UPGRADE) {
36881
- this.onUpgrade(data.slice(offset))
36882
- } else if (ret === constants.ERROR.PAUSED) {
36883
- this.paused = true
36884
- socket.unshift(data.slice(offset))
36885
- } else if (ret !== constants.ERROR.OK) {
36886
- const ptr = llhttp.llhttp_get_error_reason(this.ptr)
36887
- let message = ''
36888
- /* istanbul ignore else: difficult to make a test case for */
36889
- if (ptr) {
36890
- const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
36891
- message =
36892
- 'Response does not match the HTTP/1.1 protocol (' +
36893
- Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
36894
- ')'
36895
- }
36896
- throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset))
37077
+ if (ret !== constants.ERROR.OK) {
37078
+ const body = data.subarray(offset)
37079
+
37080
+ if (ret === constants.ERROR.PAUSED_UPGRADE) {
37081
+ this.onUpgrade(body)
37082
+ } else if (ret === constants.ERROR.PAUSED) {
37083
+ this.paused = true
37084
+ socket.unshift(body)
37085
+ } else {
37086
+ throw this.createError(ret, body)
37087
+ }
36897
37088
  }
36898
37089
  } catch (err) {
36899
37090
  util.destroy(socket, err)
36900
37091
  }
36901
37092
  }
36902
37093
 
37094
+ finish () {
37095
+ assert(currentParser === null)
37096
+ assert(this.ptr != null)
37097
+ assert(!this.paused)
37098
+
37099
+ const { llhttp } = this
37100
+
37101
+ let ret
37102
+
37103
+ try {
37104
+ currentParser = this
37105
+ ret = llhttp.llhttp_finish(this.ptr)
37106
+ } finally {
37107
+ currentParser = null
37108
+ }
37109
+
37110
+ if (ret === constants.ERROR.OK) {
37111
+ return null
37112
+ }
37113
+
37114
+ if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) {
37115
+ this.paused = true
37116
+ return null
37117
+ }
37118
+
37119
+ return this.createError(ret, EMPTY_BUF)
37120
+ }
37121
+
37122
+ createError (ret, data) {
37123
+ const { llhttp, contentLength, bytesRead } = this
37124
+
37125
+ if (contentLength && bytesRead !== parseInt(contentLength, 10)) {
37126
+ return new ResponseContentLengthMismatchError()
37127
+ }
37128
+
37129
+ const ptr = llhttp.llhttp_get_error_reason(this.ptr)
37130
+ let message = ''
37131
+ if (ptr) {
37132
+ const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
37133
+ message =
37134
+ 'Response does not match the HTTP/1.1 protocol (' +
37135
+ Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
37136
+ ')'
37137
+ }
37138
+
37139
+ return new HTTPParserError(message, constants.ERROR[ret], data)
37140
+ }
37141
+
36903
37142
  destroy () {
36904
37143
  assert(this.ptr != null)
36905
37144
  assert(currentParser == null)
@@ -36927,6 +37166,11 @@ class Parser {
36927
37166
  return -1
36928
37167
  }
36929
37168
 
37169
+ if (client[kRunning] === 0) {
37170
+ util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
37171
+ return -1
37172
+ }
37173
+
36930
37174
  const request = client[kQueue][client[kRunningIdx]]
36931
37175
  if (!request) {
36932
37176
  return -1
@@ -37030,6 +37274,11 @@ class Parser {
37030
37274
  return -1
37031
37275
  }
37032
37276
 
37277
+ if (client[kRunning] === 0) {
37278
+ util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
37279
+ return -1
37280
+ }
37281
+
37033
37282
  const request = client[kQueue][client[kRunningIdx]]
37034
37283
 
37035
37284
  /* istanbul ignore next: difficult to make a test case for */
@@ -37203,6 +37452,7 @@ class Parser {
37203
37452
  request.onComplete(headers)
37204
37453
 
37205
37454
  client[kQueue][client[kRunningIdx]++] = null
37455
+ socket[kSocketUsed] = true
37206
37456
 
37207
37457
  if (socket[kWriting]) {
37208
37458
  assert(client[kRunning] === 0)
@@ -37261,6 +37511,9 @@ async function connectH1 (client, socket) {
37261
37511
  socket[kWriting] = false
37262
37512
  socket[kReset] = false
37263
37513
  socket[kBlocking] = false
37514
+ socket[kIdleSocketValidation] = 0
37515
+ socket[kIdleSocketValidationTimeout] = null
37516
+ socket[kSocketUsed] = false
37264
37517
  socket[kParser] = new Parser(client, socket, llhttpInstance)
37265
37518
 
37266
37519
  addListener(socket, 'error', function (err) {
@@ -37271,8 +37524,11 @@ async function connectH1 (client, socket) {
37271
37524
  // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
37272
37525
  // to the user.
37273
37526
  if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
37274
- // We treat all incoming data so for as a valid response.
37275
- parser.onMessageComplete()
37527
+ const parserErr = parser.finish()
37528
+ if (parserErr) {
37529
+ this[kError] = parserErr
37530
+ this[kClient][kOnError](parserErr)
37531
+ }
37276
37532
  return
37277
37533
  }
37278
37534
 
@@ -37291,8 +37547,10 @@ async function connectH1 (client, socket) {
37291
37547
  const parser = this[kParser]
37292
37548
 
37293
37549
  if (parser.statusCode && !parser.shouldKeepAlive) {
37294
- // We treat all incoming data so far as a valid response.
37295
- parser.onMessageComplete()
37550
+ const parserErr = parser.finish()
37551
+ if (parserErr) {
37552
+ util.destroy(this, parserErr)
37553
+ }
37296
37554
  return
37297
37555
  }
37298
37556
 
@@ -37302,10 +37560,11 @@ async function connectH1 (client, socket) {
37302
37560
  const client = this[kClient]
37303
37561
  const parser = this[kParser]
37304
37562
 
37563
+ clearIdleSocketValidation(this)
37564
+
37305
37565
  if (parser) {
37306
37566
  if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
37307
- // We treat all incoming data so far as a valid response.
37308
- parser.onMessageComplete()
37567
+ this[kError] = parser.finish() || this[kError]
37309
37568
  }
37310
37569
 
37311
37570
  this[kParser].destroy()
@@ -37368,7 +37627,7 @@ async function connectH1 (client, socket) {
37368
37627
  return socket.destroyed
37369
37628
  },
37370
37629
  busy (request) {
37371
- if (socket[kWriting] || socket[kReset] || socket[kBlocking]) {
37630
+ if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) {
37372
37631
  return true
37373
37632
  }
37374
37633
 
@@ -37406,6 +37665,31 @@ async function connectH1 (client, socket) {
37406
37665
  }
37407
37666
  }
37408
37667
 
37668
+ function clearIdleSocketValidation (socket) {
37669
+ if (socket[kIdleSocketValidationTimeout]) {
37670
+ clearTimeout(socket[kIdleSocketValidationTimeout])
37671
+ socket[kIdleSocketValidationTimeout] = null
37672
+ }
37673
+
37674
+ socket[kIdleSocketValidation] = 0
37675
+ }
37676
+
37677
+ function scheduleIdleSocketValidation (client, socket) {
37678
+ socket[kIdleSocketValidation] = 1
37679
+ socket[kIdleSocketValidationTimeout] = setTimeout(() => {
37680
+ socket[kIdleSocketValidationTimeout] = null
37681
+ socket[kIdleSocketValidation] = 2
37682
+
37683
+ if (client[kSocket] === socket && !socket.destroyed) {
37684
+ client[kResume]()
37685
+ }
37686
+ }, 0)
37687
+ socket[kIdleSocketValidationTimeout].unref?.()
37688
+ }
37689
+
37690
+ /**
37691
+ * @param {import('./client.js')} client
37692
+ */
37409
37693
  function resumeH1 (client) {
37410
37694
  const socket = client[kSocket]
37411
37695
 
@@ -37420,6 +37704,32 @@ function resumeH1 (client) {
37420
37704
  socket[kNoRef] = false
37421
37705
  }
37422
37706
 
37707
+ if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) {
37708
+ if (socket[kIdleSocketValidation] === 0) {
37709
+ scheduleIdleSocketValidation(client, socket)
37710
+ socket[kParser].readMore()
37711
+ if (socket.destroyed) {
37712
+ return
37713
+ }
37714
+ return
37715
+ }
37716
+
37717
+ if (socket[kIdleSocketValidation] === 1) {
37718
+ socket[kParser].readMore()
37719
+ if (socket.destroyed) {
37720
+ return
37721
+ }
37722
+ return
37723
+ }
37724
+ }
37725
+
37726
+ if (client[kRunning] === 0) {
37727
+ socket[kParser].readMore()
37728
+ if (socket.destroyed) {
37729
+ return
37730
+ }
37731
+ }
37732
+
37423
37733
  if (client[kSize] === 0) {
37424
37734
  if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
37425
37735
  socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
@@ -37475,8 +37785,16 @@ function writeH1 (client, request) {
37475
37785
  }
37476
37786
  body = bodyStream.stream
37477
37787
  contentLength = bodyStream.length
37478
- } else if (util.isBlobLike(body) && request.contentType == null && body.type) {
37479
- headers.push('content-type', body.type)
37788
+ } else if (util.isBlobLike(body) && request.contentType == null) {
37789
+ const contentType = body.type
37790
+ if (contentType) {
37791
+ const contentTypeValue = `${contentType}`
37792
+ if (!util.isValidHeaderValue(contentTypeValue)) {
37793
+ util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
37794
+ return false
37795
+ }
37796
+ headers.push('content-type', contentTypeValue)
37797
+ }
37480
37798
  }
37481
37799
 
37482
37800
  if (body && typeof body.read === 'function') {
@@ -37513,6 +37831,7 @@ function writeH1 (client, request) {
37513
37831
  }
37514
37832
 
37515
37833
  const socket = client[kSocket]
37834
+ clearIdleSocketValidation(socket)
37516
37835
 
37517
37836
  const abort = (err) => {
37518
37837
  if (request.aborted || request.completed) {
@@ -38834,9 +39153,10 @@ class Client extends DispatcherBase {
38834
39153
  autoSelectFamilyAttemptTimeout,
38835
39154
  // h2
38836
39155
  maxConcurrentStreams,
38837
- allowH2
39156
+ allowH2,
39157
+ webSocket
38838
39158
  } = {}) {
38839
- super()
39159
+ super({ webSocket })
38840
39160
 
38841
39161
  if (keepAlive !== undefined) {
38842
39162
  throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
@@ -39369,15 +39689,24 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch, kInterceptors } = __nc
39369
39689
  const kOnDestroyed = Symbol('onDestroyed')
39370
39690
  const kOnClosed = Symbol('onClosed')
39371
39691
  const kInterceptedDispatch = Symbol('Intercepted Dispatch')
39692
+ const kWebSocketOptions = Symbol('webSocketOptions')
39372
39693
 
39373
39694
  class DispatcherBase extends Dispatcher {
39374
- constructor () {
39695
+ constructor (opts) {
39375
39696
  super()
39376
39697
 
39377
39698
  this[kDestroyed] = false
39378
39699
  this[kOnDestroyed] = null
39379
39700
  this[kClosed] = false
39380
39701
  this[kOnClosed] = []
39702
+ this[kWebSocketOptions] = opts?.webSocket ?? {}
39703
+ }
39704
+
39705
+ get webSocketOptions () {
39706
+ return {
39707
+ maxFragments: this[kWebSocketOptions].maxFragments ?? 131072,
39708
+ maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024
39709
+ }
39381
39710
  }
39382
39711
 
39383
39712
  get destroyed () {
@@ -39941,8 +40270,8 @@ const kRemoveClient = Symbol('remove client')
39941
40270
  const kStats = Symbol('stats')
39942
40271
 
39943
40272
  class PoolBase extends DispatcherBase {
39944
- constructor () {
39945
- super()
40273
+ constructor (opts) {
40274
+ super(opts)
39946
40275
 
39947
40276
  this[kQueue] = new FixedQueue()
39948
40277
  this[kClients] = []
@@ -40202,8 +40531,6 @@ class Pool extends PoolBase {
40202
40531
  allowH2,
40203
40532
  ...options
40204
40533
  } = {}) {
40205
- super()
40206
-
40207
40534
  if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
40208
40535
  throw new InvalidArgumentError('invalid connections')
40209
40536
  }
@@ -40228,6 +40555,8 @@ class Pool extends PoolBase {
40228
40555
  })
40229
40556
  }
40230
40557
 
40558
+ super(options)
40559
+
40231
40560
  this[kInterceptors] = options.interceptors?.Pool && Array.isArray(options.interceptors.Pool)
40232
40561
  ? options.interceptors.Pool
40233
40562
  : []
@@ -40952,6 +41281,28 @@ function calculateRetryAfterHeader (retryAfter) {
40952
41281
  return new Date(retryAfter).getTime() - current
40953
41282
  }
40954
41283
 
41284
+ function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
41285
+ const contentLength = headers['content-length']
41286
+ if (contentLength == null) {
41287
+ return null
41288
+ }
41289
+
41290
+ if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
41291
+ return null
41292
+ }
41293
+
41294
+ const length = Number(contentLength)
41295
+ const expectedLength = range.end - range.start + 1
41296
+ if (!Number.isFinite(length) || length !== expectedLength) {
41297
+ return new RequestRetryError('Content-Length mismatch', statusCode, {
41298
+ headers,
41299
+ data: { count: retryCount }
41300
+ })
41301
+ }
41302
+
41303
+ return null
41304
+ }
41305
+
40955
41306
  class RetryHandler {
40956
41307
  constructor (opts, handlers) {
40957
41308
  const { retryOptions, ...dispatchOpts } = opts
@@ -41166,6 +41517,12 @@ class RetryHandler {
41166
41517
  return false
41167
41518
  }
41168
41519
 
41520
+ const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
41521
+ if (contentLengthError != null) {
41522
+ this.abort(contentLengthError)
41523
+ return false
41524
+ }
41525
+
41169
41526
  const { start, size, end = size - 1 } = contentRange
41170
41527
 
41171
41528
  assert(this.start === start, 'content-range mismatch')
@@ -41189,6 +41546,12 @@ class RetryHandler {
41189
41546
  )
41190
41547
  }
41191
41548
 
41549
+ const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
41550
+ if (contentLengthError != null) {
41551
+ this.abort(contentLengthError)
41552
+ return false
41553
+ }
41554
+
41192
41555
  const { start, size, end = size - 1 } = range
41193
41556
  assert(
41194
41557
  start != null && Number.isFinite(start),
@@ -45312,32 +45675,25 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
45312
45675
  // If the attribute-name case-insensitively matches the string
45313
45676
  // "SameSite", the user agent MUST process the cookie-av as follows:
45314
45677
 
45315
- // 1. Let enforcement be "Default".
45316
- let enforcement = 'Default'
45317
-
45318
45678
  const attributeValueLowercase = attributeValue.toLowerCase()
45319
- // 2. If cookie-av's attribute-value is a case-insensitive match for
45320
- // "None", set enforcement to "None".
45321
- if (attributeValueLowercase.includes('none')) {
45322
- enforcement = 'None'
45323
- }
45324
-
45325
- // 3. If cookie-av's attribute-value is a case-insensitive match for
45326
- // "Strict", set enforcement to "Strict".
45327
- if (attributeValueLowercase.includes('strict')) {
45328
- enforcement = 'Strict'
45329
- }
45330
45679
 
45331
- // 4. If cookie-av's attribute-value is a case-insensitive match for
45332
- // "Lax", set enforcement to "Lax".
45333
- if (attributeValueLowercase.includes('lax')) {
45334
- enforcement = 'Lax'
45680
+ // 1. If cookie-av's attribute-value is a case-insensitive match for
45681
+ // "None", append an attribute to the cookie-attribute-list with an
45682
+ // attribute-name of "SameSite" and an attribute-value of "None".
45683
+ if (attributeValueLowercase === 'none') {
45684
+ cookieAttributeList.sameSite = 'None'
45685
+ } else if (attributeValueLowercase === 'strict') {
45686
+ // 2. If cookie-av's attribute-value is a case-insensitive match for
45687
+ // "Strict", append an attribute to the cookie-attribute-list with
45688
+ // an attribute-name of "SameSite" and an attribute-value of
45689
+ // "Strict".
45690
+ cookieAttributeList.sameSite = 'Strict'
45691
+ } else if (attributeValueLowercase === 'lax') {
45692
+ // 3. If cookie-av's attribute-value is a case-insensitive match for
45693
+ // "Lax", append an attribute to the cookie-attribute-list with an
45694
+ // attribute-name of "SameSite" and an attribute-value of "Lax".
45695
+ cookieAttributeList.sameSite = 'Lax'
45335
45696
  }
45336
-
45337
- // 5. Append an attribute to the cookie-attribute-list with an
45338
- // attribute-name of "SameSite" and an attribute-value of
45339
- // enforcement.
45340
- cookieAttributeList.sameSite = enforcement
45341
45697
  } else {
45342
45698
  cookieAttributeList.unparsed ??= []
45343
45699
 
@@ -45467,7 +45823,7 @@ function validateCookiePath (path) {
45467
45823
 
45468
45824
  if (
45469
45825
  code < 0x20 || // exclude CTLs (0-31)
45470
- code === 0x7F || // DEL
45826
+ code > 0x7E || // exclude DEL and non-ascii
45471
45827
  code === 0x3B // ;
45472
45828
  ) {
45473
45829
  throw new Error('Invalid cookie path')
@@ -45476,16 +45832,80 @@ function validateCookiePath (path) {
45476
45832
  }
45477
45833
 
45478
45834
  /**
45479
- * I have no idea why these values aren't allowed to be honest,
45480
- * but Deno tests these. - Khafra
45835
+ * <let-dig> ::= <letter> | <digit>
45836
+ *
45837
+ * <letter> ::= any one of the 52 alphabetic characters A through Z in
45838
+ * upper case and a through z in lower case
45839
+ *
45840
+ * <digit> ::= any one of the ten digits 0 through 9r
45841
+ *
45842
+ * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
45843
+ * @param {number} code
45844
+ */
45845
+ function isLetterOrDigit (code) {
45846
+ return (
45847
+ (code >= 0x30 && code <= 0x39) || // 0-9
45848
+ (code >= 0x41 && code <= 0x5A) || // A-Z
45849
+ (code >= 0x61 && code <= 0x7A) // a-z
45850
+ )
45851
+ }
45852
+
45853
+ /**
45854
+ * Validates a cookie domain against the "preferred name syntax".
45855
+ *
45856
+ * <domain> ::= <subdomain> | " "
45857
+ * <subdomain> ::= <label> | <subdomain> "." <label>
45858
+ * <label> ::= <let-dig> [ [ <ldh-str> ] <let-dig> ]
45859
+ * <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
45860
+ * <let-dig-hyp> ::= <let-dig> | "-"
45861
+ *
45862
+ * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
45863
+ * @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
45864
+ * @see https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4
45481
45865
  * @param {string} domain
45482
45866
  */
45483
45867
  function validateCookieDomain (domain) {
45484
- if (
45485
- domain.startsWith('-') ||
45486
- domain.endsWith('.') ||
45487
- domain.endsWith('-')
45488
- ) {
45868
+ // <domain> ::= <subdomain> | " "
45869
+ if (domain === ' ') {
45870
+ return
45871
+ }
45872
+
45873
+ if (domain.length > 255) {
45874
+ throw new Error('Invalid cookie domain')
45875
+ }
45876
+
45877
+ let labelLength = 0
45878
+
45879
+ for (let i = 0; i < domain.length; ++i) {
45880
+ const code = domain.charCodeAt(i)
45881
+
45882
+ if (code === 0x2E) {
45883
+ if (labelLength === 0) {
45884
+ throw new Error('Invalid cookie domain')
45885
+ }
45886
+
45887
+ if (domain.charCodeAt(i - 1) === 0x2D) { // "-"
45888
+ throw new Error('Invalid cookie domain')
45889
+ }
45890
+
45891
+ labelLength = 0
45892
+ continue
45893
+ }
45894
+
45895
+ if (labelLength === 0 && !isLetterOrDigit(code)) {
45896
+ throw new Error('Invalid cookie domain')
45897
+ }
45898
+
45899
+ if (!isLetterOrDigit(code) && code !== 0x2D) { // "-"
45900
+ throw new Error('Invalid cookie domain')
45901
+ }
45902
+
45903
+ if (++labelLength > 63) {
45904
+ throw new Error('Invalid cookie domain')
45905
+ }
45906
+ }
45907
+
45908
+ if (labelLength === 0 || domain.charCodeAt(domain.length - 1) === 0x2D) { // "-"
45489
45909
  throw new Error('Invalid cookie domain')
45490
45910
  }
45491
45911
  }
@@ -45628,7 +46048,13 @@ function stringify (cookie) {
45628
46048
 
45629
46049
  const [key, ...value] = part.split('=')
45630
46050
 
45631
- out.push(`${key.trim()}=${value.join('=')}`)
46051
+ const trimmedKey = key.trim()
46052
+ const joinedValue = value.join('=')
46053
+
46054
+ validateCookieName(trimmedKey)
46055
+ validateCookieValue(joinedValue)
46056
+
46057
+ out.push(`${trimmedKey}=${joinedValue}`)
45632
46058
  }
45633
46059
 
45634
46060
  return out.join('; ')
@@ -58043,45 +58469,35 @@ const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
58043
58469
  const kBuffer = Symbol('kBuffer')
58044
58470
  const kLength = Symbol('kLength')
58045
58471
 
58046
- // Default maximum decompressed message size: 4 MB
58047
- const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
58048
-
58049
58472
  class PerMessageDeflate {
58050
58473
  /** @type {import('node:zlib').InflateRaw} */
58051
58474
  #inflate
58052
58475
 
58053
58476
  #options = {}
58054
58477
 
58055
- /** @type {number} */
58056
- #maxDecompressedSize
58057
-
58058
- /** @type {boolean} */
58059
- #aborted = false
58060
-
58061
- /** @type {Function|null} */
58062
- #currentCallback = null
58478
+ #maxPayloadSize = 0
58063
58479
 
58064
58480
  /**
58065
58481
  * @param {Map<string, string>} extensions
58066
- * @param {{ maxDecompressedMessageSize?: number }} [options]
58067
58482
  */
58068
- constructor (extensions, options = {}) {
58483
+ constructor (extensions, options) {
58069
58484
  this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
58070
58485
  this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
58071
- this.#maxDecompressedSize = options.maxDecompressedMessageSize ?? kDefaultMaxDecompressedSize
58486
+
58487
+ this.#maxPayloadSize = options.maxPayloadSize
58072
58488
  }
58073
58489
 
58490
+ /**
58491
+ * Decompress a compressed payload.
58492
+ * @param {Buffer} chunk Compressed data
58493
+ * @param {boolean} fin Final fragment flag
58494
+ * @param {Function} callback Callback function
58495
+ */
58074
58496
  decompress (chunk, fin, callback) {
58075
58497
  // An endpoint uses the following algorithm to decompress a message.
58076
58498
  // 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
58077
58499
  // payload of the message.
58078
58500
  // 2. Decompress the resulting data using DEFLATE.
58079
-
58080
- if (this.#aborted) {
58081
- callback(new MessageSizeExceededError())
58082
- return
58083
- }
58084
-
58085
58501
  if (!this.#inflate) {
58086
58502
  let windowBits = Z_DEFAULT_WINDOWBITS
58087
58503
 
@@ -58104,23 +58520,12 @@ class PerMessageDeflate {
58104
58520
  this.#inflate[kLength] = 0
58105
58521
 
58106
58522
  this.#inflate.on('data', (data) => {
58107
- if (this.#aborted) {
58108
- return
58109
- }
58110
-
58111
58523
  this.#inflate[kLength] += data.length
58112
58524
 
58113
- if (this.#inflate[kLength] > this.#maxDecompressedSize) {
58114
- this.#aborted = true
58525
+ if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
58526
+ callback(new MessageSizeExceededError())
58115
58527
  this.#inflate.removeAllListeners()
58116
- this.#inflate.destroy()
58117
58528
  this.#inflate = null
58118
-
58119
- if (this.#currentCallback) {
58120
- const cb = this.#currentCallback
58121
- this.#currentCallback = null
58122
- cb(new MessageSizeExceededError())
58123
- }
58124
58529
  return
58125
58530
  }
58126
58531
 
@@ -58133,14 +58538,13 @@ class PerMessageDeflate {
58133
58538
  })
58134
58539
  }
58135
58540
 
58136
- this.#currentCallback = callback
58137
58541
  this.#inflate.write(chunk)
58138
58542
  if (fin) {
58139
58543
  this.#inflate.write(tail)
58140
58544
  }
58141
58545
 
58142
58546
  this.#inflate.flush(() => {
58143
- if (this.#aborted || !this.#inflate) {
58547
+ if (!this.#inflate) {
58144
58548
  return
58145
58549
  }
58146
58550
 
@@ -58148,7 +58552,6 @@ class PerMessageDeflate {
58148
58552
 
58149
58553
  this.#inflate[kBuffer].length = 0
58150
58554
  this.#inflate[kLength] = 0
58151
- this.#currentCallback = null
58152
58555
 
58153
58556
  callback(null, full)
58154
58557
  })
@@ -58184,6 +58587,12 @@ const {
58184
58587
  const { WebsocketFrameSend } = __nccwpck_require__(3264)
58185
58588
  const { closeWebSocketConnection } = __nccwpck_require__(86897)
58186
58589
  const { PerMessageDeflate } = __nccwpck_require__(19469)
58590
+ const { MessageSizeExceededError } = __nccwpck_require__(68707)
58591
+
58592
+ function failWebsocketConnectionWithCode (ws, code, reason) {
58593
+ closeWebSocketConnection(ws, code, reason, Buffer.byteLength(reason))
58594
+ failWebsocketConnection(ws, reason)
58595
+ }
58187
58596
 
58188
58597
  // This code was influenced by ws released under the MIT license.
58189
58598
  // Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
@@ -58192,6 +58601,7 @@ const { PerMessageDeflate } = __nccwpck_require__(19469)
58192
58601
 
58193
58602
  class ByteParser extends Writable {
58194
58603
  #buffers = []
58604
+ #fragmentsBytes = 0
58195
58605
  #byteOffset = 0
58196
58606
  #loop = false
58197
58607
 
@@ -58203,20 +58613,24 @@ class ByteParser extends Writable {
58203
58613
  /** @type {Map<string, PerMessageDeflate>} */
58204
58614
  #extensions
58205
58615
 
58206
- /** @type {{ maxDecompressedMessageSize?: number }} */
58207
- #options
58616
+ /** @type {number} */
58617
+ #maxFragments
58618
+
58619
+ /** @type {number} */
58620
+ #maxPayloadSize
58208
58621
 
58209
58622
  /**
58210
58623
  * @param {import('./websocket').WebSocket} ws
58211
58624
  * @param {Map<string, string>|null} extensions
58212
- * @param {{ maxDecompressedMessageSize?: number }} [options]
58625
+ * @param {{ maxFragments?: number, maxPayloadSize?: number }} [options]
58213
58626
  */
58214
58627
  constructor (ws, extensions, options = {}) {
58215
58628
  super()
58216
58629
 
58217
58630
  this.ws = ws
58218
58631
  this.#extensions = extensions == null ? new Map() : extensions
58219
- this.#options = options
58632
+ this.#maxFragments = options.maxFragments ?? 0
58633
+ this.#maxPayloadSize = options.maxPayloadSize ?? 0
58220
58634
 
58221
58635
  if (this.#extensions.has('permessage-deflate')) {
58222
58636
  this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options))
@@ -58235,6 +58649,19 @@ class ByteParser extends Writable {
58235
58649
  this.run(callback)
58236
58650
  }
58237
58651
 
58652
+ #validatePayloadLength () {
58653
+ if (
58654
+ this.#maxPayloadSize > 0 &&
58655
+ !isControlFrame(this.#info.opcode) &&
58656
+ this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize
58657
+ ) {
58658
+ failWebsocketConnectionWithCode(this.ws, 1009, 'Payload size exceeds maximum allowed size')
58659
+ return false
58660
+ }
58661
+
58662
+ return true
58663
+ }
58664
+
58238
58665
  /**
58239
58666
  * Runs whenever a new chunk is received.
58240
58667
  * Callback is called whenever there are no more chunks buffering,
@@ -58323,6 +58750,10 @@ class ByteParser extends Writable {
58323
58750
  if (payloadLength <= 125) {
58324
58751
  this.#info.payloadLength = payloadLength
58325
58752
  this.#state = parserStates.READ_DATA
58753
+
58754
+ if (!this.#validatePayloadLength()) {
58755
+ return
58756
+ }
58326
58757
  } else if (payloadLength === 126) {
58327
58758
  this.#state = parserStates.PAYLOADLENGTH_16
58328
58759
  } else if (payloadLength === 127) {
@@ -58347,6 +58778,10 @@ class ByteParser extends Writable {
58347
58778
 
58348
58779
  this.#info.payloadLength = buffer.readUInt16BE(0)
58349
58780
  this.#state = parserStates.READ_DATA
58781
+
58782
+ if (!this.#validatePayloadLength()) {
58783
+ return
58784
+ }
58350
58785
  } else if (this.#state === parserStates.PAYLOADLENGTH_64) {
58351
58786
  if (this.#byteOffset < 8) {
58352
58787
  return callback()
@@ -58369,6 +58804,10 @@ class ByteParser extends Writable {
58369
58804
 
58370
58805
  this.#info.payloadLength = lower
58371
58806
  this.#state = parserStates.READ_DATA
58807
+
58808
+ if (!this.#validatePayloadLength()) {
58809
+ return
58810
+ }
58372
58811
  } else if (this.#state === parserStates.READ_DATA) {
58373
58812
  if (this.#byteOffset < this.#info.payloadLength) {
58374
58813
  return callback()
@@ -58381,42 +58820,58 @@ class ByteParser extends Writable {
58381
58820
  this.#state = parserStates.INFO
58382
58821
  } else {
58383
58822
  if (!this.#info.compressed) {
58384
- this.#fragments.push(body)
58823
+ if (!this.writeFragments(body)) {
58824
+ return
58825
+ }
58826
+
58827
+ if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
58828
+ failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message)
58829
+ return
58830
+ }
58385
58831
 
58386
58832
  // If the frame is not fragmented, a message has been received.
58387
58833
  // If the frame is fragmented, it will terminate with a fin bit set
58388
58834
  // and an opcode of 0 (continuation), therefore we handle that when
58389
58835
  // parsing continuation frames, not here.
58390
58836
  if (!this.#info.fragmented && this.#info.fin) {
58391
- const fullMessage = Buffer.concat(this.#fragments)
58392
- websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
58393
- this.#fragments.length = 0
58837
+ websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments())
58394
58838
  }
58395
58839
 
58396
58840
  this.#state = parserStates.INFO
58397
58841
  } else {
58398
- this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
58399
- if (error) {
58400
- failWebsocketConnection(this.ws, error.message)
58401
- return
58402
- }
58842
+ this.#extensions.get('permessage-deflate').decompress(
58843
+ body,
58844
+ this.#info.fin,
58845
+ (error, data) => {
58846
+ if (error) {
58847
+ const code = error instanceof MessageSizeExceededError ? 1009 : 1007
58848
+ failWebsocketConnectionWithCode(this.ws, code, error.message)
58849
+ return
58850
+ }
58403
58851
 
58404
- this.#fragments.push(data)
58852
+ if (!this.writeFragments(data)) {
58853
+ return
58854
+ }
58855
+
58856
+ if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
58857
+ failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message)
58858
+ return
58859
+ }
58860
+
58861
+ if (!this.#info.fin) {
58862
+ this.#state = parserStates.INFO
58863
+ this.#loop = true
58864
+ this.run(callback)
58865
+ return
58866
+ }
58867
+
58868
+ websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments())
58405
58869
 
58406
- if (!this.#info.fin) {
58407
- this.#state = parserStates.INFO
58408
58870
  this.#loop = true
58871
+ this.#state = parserStates.INFO
58409
58872
  this.run(callback)
58410
- return
58411
58873
  }
58412
-
58413
- websocketMessageReceived(this.ws, this.#info.binaryType, Buffer.concat(this.#fragments))
58414
-
58415
- this.#loop = true
58416
- this.#state = parserStates.INFO
58417
- this.#fragments.length = 0
58418
- this.run(callback)
58419
- })
58874
+ )
58420
58875
 
58421
58876
  this.#loop = false
58422
58877
  break
@@ -58468,6 +58923,35 @@ class ByteParser extends Writable {
58468
58923
  return buffer
58469
58924
  }
58470
58925
 
58926
+ writeFragments (fragment) {
58927
+ if (
58928
+ this.#maxFragments > 0 &&
58929
+ this.#fragments.length === this.#maxFragments
58930
+ ) {
58931
+ failWebsocketConnectionWithCode(this.ws, 1008, 'Too many message fragments')
58932
+ return false
58933
+ }
58934
+
58935
+ this.#fragmentsBytes += fragment.length
58936
+ this.#fragments.push(fragment)
58937
+ return true
58938
+ }
58939
+
58940
+ consumeFragments () {
58941
+ const fragments = this.#fragments
58942
+
58943
+ if (fragments.length === 1) {
58944
+ this.#fragmentsBytes = 0
58945
+ return fragments.shift()
58946
+ }
58947
+
58948
+ const output = Buffer.concat(fragments, this.#fragmentsBytes)
58949
+ this.#fragments = []
58950
+ this.#fragmentsBytes = 0
58951
+
58952
+ return output
58953
+ }
58954
+
58471
58955
  parseCloseBody (data) {
58472
58956
  assert(data.length !== 1)
58473
58957
 
@@ -59112,9 +59596,6 @@ class WebSocket extends EventTarget {
59112
59596
  /** @type {SendQueue} */
59113
59597
  #sendQueue
59114
59598
 
59115
- /** @type {{ maxDecompressedMessageSize?: number }} */
59116
- #options
59117
-
59118
59599
  /**
59119
59600
  * @param {string} url
59120
59601
  * @param {string|string[]} protocols
@@ -59188,11 +59669,6 @@ class WebSocket extends EventTarget {
59188
59669
  // 10. Set this's url to urlRecord.
59189
59670
  this[kWebSocketURL] = new URL(urlRecord.href)
59190
59671
 
59191
- // Store options for later use (e.g., maxDecompressedMessageSize)
59192
- this.#options = {
59193
- maxDecompressedMessageSize: options.maxDecompressedMessageSize
59194
- }
59195
-
59196
59672
  // 11. Let client be this's relevant settings object.
59197
59673
  const client = environmentSettingsObject.settingsObject
59198
59674
 
@@ -59511,7 +59987,14 @@ class WebSocket extends EventTarget {
59511
59987
  // once this happens, the connection is open
59512
59988
  this[kResponse] = response
59513
59989
 
59514
- const parser = new ByteParser(this, parsedExtensions, this.#options)
59990
+ const webSocketOptions = this[kController]?.dispatcher?.webSocketOptions
59991
+ const maxFragments = webSocketOptions?.maxFragments
59992
+ const maxPayloadSize = webSocketOptions?.maxPayloadSize
59993
+
59994
+ const parser = new ByteParser(this, parsedExtensions, {
59995
+ maxFragments,
59996
+ maxPayloadSize
59997
+ })
59515
59998
  parser.on('drain', onParserDrain)
59516
59999
  parser.on('error', onParserError.bind(this))
59517
60000
 
@@ -59614,19 +60097,6 @@ webidl.converters.WebSocketInit = webidl.dictionaryConverter([
59614
60097
  {
59615
60098
  key: 'headers',
59616
60099
  converter: webidl.nullableConverter(webidl.converters.HeadersInit)
59617
- },
59618
- {
59619
- key: 'maxDecompressedMessageSize',
59620
- converter: webidl.nullableConverter((V) => {
59621
- V = webidl.converters['unsigned long long'](V)
59622
- if (V <= 0) {
59623
- throw webidl.errors.exception({
59624
- header: 'WebSocket constructor',
59625
- message: 'maxDecompressedMessageSize must be greater than 0'
59626
- })
59627
- }
59628
- return V
59629
- })
59630
60100
  }
59631
60101
  ])
59632
60102