undici 6.26.0 → 6.27.0
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.
- package/docs/docs/api/Client.md +1 -0
- package/lib/dispatcher/client-h1.js +72 -1
- package/lib/dispatcher/dispatcher-base.js +1 -0
- package/lib/web/cookies/parse.js +16 -23
- package/lib/web/websocket/receiver.js +31 -8
- package/lib/web/websocket/websocket.js +4 -1
- package/package.json +1 -1
- package/types/client.d.ts +6 -0
package/docs/docs/api/Client.md
CHANGED
|
@@ -27,6 +27,7 @@ Returns: `Client`
|
|
|
27
27
|
* **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.
|
|
28
28
|
* **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
|
|
29
29
|
* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
|
|
30
|
+
* **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit.
|
|
30
31
|
* **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.
|
|
31
32
|
* **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.
|
|
32
33
|
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
|
|
@@ -57,6 +57,9 @@ const EMPTY_BUF = Buffer.alloc(0)
|
|
|
57
57
|
const FastBuffer = Buffer[Symbol.species]
|
|
58
58
|
const addListener = util.addListener
|
|
59
59
|
const removeAllListeners = util.removeAllListeners
|
|
60
|
+
const kIdleSocketValidation = Symbol('kIdleSocketValidation')
|
|
61
|
+
const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout')
|
|
62
|
+
const kSocketUsed = Symbol('kSocketUsed')
|
|
60
63
|
|
|
61
64
|
let extractBody
|
|
62
65
|
|
|
@@ -371,6 +374,11 @@ class Parser {
|
|
|
371
374
|
return -1
|
|
372
375
|
}
|
|
373
376
|
|
|
377
|
+
if (client[kRunning] === 0) {
|
|
378
|
+
util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
|
|
379
|
+
return -1
|
|
380
|
+
}
|
|
381
|
+
|
|
374
382
|
const request = client[kQueue][client[kRunningIdx]]
|
|
375
383
|
if (!request) {
|
|
376
384
|
return -1
|
|
@@ -474,6 +482,11 @@ class Parser {
|
|
|
474
482
|
return -1
|
|
475
483
|
}
|
|
476
484
|
|
|
485
|
+
if (client[kRunning] === 0) {
|
|
486
|
+
util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
|
|
487
|
+
return -1
|
|
488
|
+
}
|
|
489
|
+
|
|
477
490
|
const request = client[kQueue][client[kRunningIdx]]
|
|
478
491
|
|
|
479
492
|
/* istanbul ignore next: difficult to make a test case for */
|
|
@@ -647,6 +660,7 @@ class Parser {
|
|
|
647
660
|
request.onComplete(headers)
|
|
648
661
|
|
|
649
662
|
client[kQueue][client[kRunningIdx]++] = null
|
|
663
|
+
socket[kSocketUsed] = true
|
|
650
664
|
|
|
651
665
|
if (socket[kWriting]) {
|
|
652
666
|
assert(client[kRunning] === 0)
|
|
@@ -705,6 +719,9 @@ async function connectH1 (client, socket) {
|
|
|
705
719
|
socket[kWriting] = false
|
|
706
720
|
socket[kReset] = false
|
|
707
721
|
socket[kBlocking] = false
|
|
722
|
+
socket[kIdleSocketValidation] = 0
|
|
723
|
+
socket[kIdleSocketValidationTimeout] = null
|
|
724
|
+
socket[kSocketUsed] = false
|
|
708
725
|
socket[kParser] = new Parser(client, socket, llhttpInstance)
|
|
709
726
|
|
|
710
727
|
addListener(socket, 'error', function (err) {
|
|
@@ -751,6 +768,8 @@ async function connectH1 (client, socket) {
|
|
|
751
768
|
const client = this[kClient]
|
|
752
769
|
const parser = this[kParser]
|
|
753
770
|
|
|
771
|
+
clearIdleSocketValidation(this)
|
|
772
|
+
|
|
754
773
|
if (parser) {
|
|
755
774
|
if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
|
|
756
775
|
this[kError] = parser.finish() || this[kError]
|
|
@@ -816,7 +835,7 @@ async function connectH1 (client, socket) {
|
|
|
816
835
|
return socket.destroyed
|
|
817
836
|
},
|
|
818
837
|
busy (request) {
|
|
819
|
-
if (socket[kWriting] || socket[kReset] || socket[kBlocking]) {
|
|
838
|
+
if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) {
|
|
820
839
|
return true
|
|
821
840
|
}
|
|
822
841
|
|
|
@@ -854,6 +873,31 @@ async function connectH1 (client, socket) {
|
|
|
854
873
|
}
|
|
855
874
|
}
|
|
856
875
|
|
|
876
|
+
function clearIdleSocketValidation (socket) {
|
|
877
|
+
if (socket[kIdleSocketValidationTimeout]) {
|
|
878
|
+
clearTimeout(socket[kIdleSocketValidationTimeout])
|
|
879
|
+
socket[kIdleSocketValidationTimeout] = null
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
socket[kIdleSocketValidation] = 0
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function scheduleIdleSocketValidation (client, socket) {
|
|
886
|
+
socket[kIdleSocketValidation] = 1
|
|
887
|
+
socket[kIdleSocketValidationTimeout] = setTimeout(() => {
|
|
888
|
+
socket[kIdleSocketValidationTimeout] = null
|
|
889
|
+
socket[kIdleSocketValidation] = 2
|
|
890
|
+
|
|
891
|
+
if (client[kSocket] === socket && !socket.destroyed) {
|
|
892
|
+
client[kResume]()
|
|
893
|
+
}
|
|
894
|
+
}, 0)
|
|
895
|
+
socket[kIdleSocketValidationTimeout].unref?.()
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* @param {import('./client.js')} client
|
|
900
|
+
*/
|
|
857
901
|
function resumeH1 (client) {
|
|
858
902
|
const socket = client[kSocket]
|
|
859
903
|
|
|
@@ -868,6 +912,32 @@ function resumeH1 (client) {
|
|
|
868
912
|
socket[kNoRef] = false
|
|
869
913
|
}
|
|
870
914
|
|
|
915
|
+
if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) {
|
|
916
|
+
if (socket[kIdleSocketValidation] === 0) {
|
|
917
|
+
scheduleIdleSocketValidation(client, socket)
|
|
918
|
+
socket[kParser].readMore()
|
|
919
|
+
if (socket.destroyed) {
|
|
920
|
+
return
|
|
921
|
+
}
|
|
922
|
+
return
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (socket[kIdleSocketValidation] === 1) {
|
|
926
|
+
socket[kParser].readMore()
|
|
927
|
+
if (socket.destroyed) {
|
|
928
|
+
return
|
|
929
|
+
}
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (client[kRunning] === 0) {
|
|
935
|
+
socket[kParser].readMore()
|
|
936
|
+
if (socket.destroyed) {
|
|
937
|
+
return
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
871
941
|
if (client[kSize] === 0) {
|
|
872
942
|
if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
|
|
873
943
|
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
|
|
@@ -961,6 +1031,7 @@ function writeH1 (client, request) {
|
|
|
961
1031
|
}
|
|
962
1032
|
|
|
963
1033
|
const socket = client[kSocket]
|
|
1034
|
+
clearIdleSocketValidation(socket)
|
|
964
1035
|
|
|
965
1036
|
const abort = (err) => {
|
|
966
1037
|
if (request.aborted || request.completed) {
|
package/lib/web/cookies/parse.js
CHANGED
|
@@ -275,32 +275,25 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
|
|
|
275
275
|
// If the attribute-name case-insensitively matches the string
|
|
276
276
|
// "SameSite", the user agent MUST process the cookie-av as follows:
|
|
277
277
|
|
|
278
|
-
// 1. Let enforcement be "Default".
|
|
279
|
-
let enforcement = 'Default'
|
|
280
|
-
|
|
281
278
|
const attributeValueLowercase = attributeValue.toLowerCase()
|
|
282
|
-
// 2. If cookie-av's attribute-value is a case-insensitive match for
|
|
283
|
-
// "None", set enforcement to "None".
|
|
284
|
-
if (attributeValueLowercase.includes('none')) {
|
|
285
|
-
enforcement = 'None'
|
|
286
|
-
}
|
|
287
279
|
|
|
288
|
-
//
|
|
289
|
-
// "
|
|
290
|
-
|
|
291
|
-
|
|
280
|
+
// 1. If cookie-av's attribute-value is a case-insensitive match for
|
|
281
|
+
// "None", append an attribute to the cookie-attribute-list with an
|
|
282
|
+
// attribute-name of "SameSite" and an attribute-value of "None".
|
|
283
|
+
if (attributeValueLowercase === 'none') {
|
|
284
|
+
cookieAttributeList.sameSite = 'None'
|
|
285
|
+
} else if (attributeValueLowercase === 'strict') {
|
|
286
|
+
// 2. If cookie-av's attribute-value is a case-insensitive match for
|
|
287
|
+
// "Strict", append an attribute to the cookie-attribute-list with
|
|
288
|
+
// an attribute-name of "SameSite" and an attribute-value of
|
|
289
|
+
// "Strict".
|
|
290
|
+
cookieAttributeList.sameSite = 'Strict'
|
|
291
|
+
} else if (attributeValueLowercase === 'lax') {
|
|
292
|
+
// 3. If cookie-av's attribute-value is a case-insensitive match for
|
|
293
|
+
// "Lax", append an attribute to the cookie-attribute-list with an
|
|
294
|
+
// attribute-name of "SameSite" and an attribute-value of "Lax".
|
|
295
|
+
cookieAttributeList.sameSite = 'Lax'
|
|
292
296
|
}
|
|
293
|
-
|
|
294
|
-
// 4. If cookie-av's attribute-value is a case-insensitive match for
|
|
295
|
-
// "Lax", set enforcement to "Lax".
|
|
296
|
-
if (attributeValueLowercase.includes('lax')) {
|
|
297
|
-
enforcement = 'Lax'
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// 5. Append an attribute to the cookie-attribute-list with an
|
|
301
|
-
// attribute-name of "SameSite" and an attribute-value of
|
|
302
|
-
// enforcement.
|
|
303
|
-
cookieAttributeList.sameSite = enforcement
|
|
304
297
|
} else {
|
|
305
298
|
cookieAttributeList.unparsed ??= []
|
|
306
299
|
|
|
@@ -20,6 +20,11 @@ const { closeWebSocketConnection } = require('./connection')
|
|
|
20
20
|
const { PerMessageDeflate } = require('./permessage-deflate')
|
|
21
21
|
const { MessageSizeExceededError } = require('../../core/errors')
|
|
22
22
|
|
|
23
|
+
function failWebsocketConnectionWithCode (ws, code, reason) {
|
|
24
|
+
closeWebSocketConnection(ws, code, reason, Buffer.byteLength(reason))
|
|
25
|
+
failWebsocketConnection(ws, reason)
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
// This code was influenced by ws released under the MIT license.
|
|
24
29
|
// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
|
25
30
|
// Copyright (c) 2013 Arnout Kazemier and contributors
|
|
@@ -39,19 +44,23 @@ class ByteParser extends Writable {
|
|
|
39
44
|
/** @type {Map<string, PerMessageDeflate>} */
|
|
40
45
|
#extensions
|
|
41
46
|
|
|
47
|
+
/** @type {number} */
|
|
48
|
+
#maxFragments
|
|
49
|
+
|
|
42
50
|
/** @type {number} */
|
|
43
51
|
#maxPayloadSize
|
|
44
52
|
|
|
45
53
|
/**
|
|
46
54
|
* @param {import('./websocket').WebSocket} ws
|
|
47
55
|
* @param {Map<string, string>|null} extensions
|
|
48
|
-
* @param {{ maxPayloadSize?: number }} [options]
|
|
56
|
+
* @param {{ maxFragments?: number, maxPayloadSize?: number }} [options]
|
|
49
57
|
*/
|
|
50
58
|
constructor (ws, extensions, options = {}) {
|
|
51
59
|
super()
|
|
52
60
|
|
|
53
61
|
this.ws = ws
|
|
54
62
|
this.#extensions = extensions == null ? new Map() : extensions
|
|
63
|
+
this.#maxFragments = options.maxFragments ?? 0
|
|
55
64
|
this.#maxPayloadSize = options.maxPayloadSize ?? 0
|
|
56
65
|
|
|
57
66
|
if (this.#extensions.has('permessage-deflate')) {
|
|
@@ -75,9 +84,9 @@ class ByteParser extends Writable {
|
|
|
75
84
|
if (
|
|
76
85
|
this.#maxPayloadSize > 0 &&
|
|
77
86
|
!isControlFrame(this.#info.opcode) &&
|
|
78
|
-
this.#info.payloadLength > this.#maxPayloadSize
|
|
87
|
+
this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize
|
|
79
88
|
) {
|
|
80
|
-
|
|
89
|
+
failWebsocketConnectionWithCode(this.ws, 1009, 'Payload size exceeds maximum allowed size')
|
|
81
90
|
return false
|
|
82
91
|
}
|
|
83
92
|
|
|
@@ -242,10 +251,12 @@ class ByteParser extends Writable {
|
|
|
242
251
|
this.#state = parserStates.INFO
|
|
243
252
|
} else {
|
|
244
253
|
if (!this.#info.compressed) {
|
|
245
|
-
this.writeFragments(body)
|
|
254
|
+
if (!this.writeFragments(body)) {
|
|
255
|
+
return
|
|
256
|
+
}
|
|
246
257
|
|
|
247
258
|
if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
|
|
248
|
-
|
|
259
|
+
failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message)
|
|
249
260
|
return
|
|
250
261
|
}
|
|
251
262
|
|
|
@@ -264,14 +275,17 @@ class ByteParser extends Writable {
|
|
|
264
275
|
this.#info.fin,
|
|
265
276
|
(error, data) => {
|
|
266
277
|
if (error) {
|
|
267
|
-
|
|
278
|
+
const code = error instanceof MessageSizeExceededError ? 1009 : 1007
|
|
279
|
+
failWebsocketConnectionWithCode(this.ws, code, error.message)
|
|
268
280
|
return
|
|
269
281
|
}
|
|
270
282
|
|
|
271
|
-
this.writeFragments(data)
|
|
283
|
+
if (!this.writeFragments(data)) {
|
|
284
|
+
return
|
|
285
|
+
}
|
|
272
286
|
|
|
273
287
|
if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
|
|
274
|
-
|
|
288
|
+
failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message)
|
|
275
289
|
return
|
|
276
290
|
}
|
|
277
291
|
|
|
@@ -341,8 +355,17 @@ class ByteParser extends Writable {
|
|
|
341
355
|
}
|
|
342
356
|
|
|
343
357
|
writeFragments (fragment) {
|
|
358
|
+
if (
|
|
359
|
+
this.#maxFragments > 0 &&
|
|
360
|
+
this.#fragments.length === this.#maxFragments
|
|
361
|
+
) {
|
|
362
|
+
failWebsocketConnectionWithCode(this.ws, 1008, 'Too many message fragments')
|
|
363
|
+
return false
|
|
364
|
+
}
|
|
365
|
+
|
|
344
366
|
this.#fragmentsBytes += fragment.length
|
|
345
367
|
this.#fragments.push(fragment)
|
|
368
|
+
return true
|
|
346
369
|
}
|
|
347
370
|
|
|
348
371
|
consumeFragments () {
|
|
@@ -435,9 +435,12 @@ class WebSocket extends EventTarget {
|
|
|
435
435
|
// once this happens, the connection is open
|
|
436
436
|
this[kResponse] = response
|
|
437
437
|
|
|
438
|
-
const
|
|
438
|
+
const webSocketOptions = this[kController]?.dispatcher?.webSocketOptions
|
|
439
|
+
const maxFragments = webSocketOptions?.maxFragments
|
|
440
|
+
const maxPayloadSize = webSocketOptions?.maxPayloadSize
|
|
439
441
|
|
|
440
442
|
const parser = new ByteParser(this, parsedExtensions, {
|
|
443
|
+
maxFragments,
|
|
441
444
|
maxPayloadSize
|
|
442
445
|
})
|
|
443
446
|
parser.on('drain', onParserDrain)
|
package/package.json
CHANGED
package/types/client.d.ts
CHANGED
|
@@ -106,6 +106,12 @@ export declare namespace Client {
|
|
|
106
106
|
bytesRead?: number
|
|
107
107
|
}
|
|
108
108
|
export interface WebSocketOptions {
|
|
109
|
+
/**
|
|
110
|
+
* Maximum number of fragments in a message.
|
|
111
|
+
* Set to 0 to disable the limit.
|
|
112
|
+
* @default 131072
|
|
113
|
+
*/
|
|
114
|
+
maxFragments?: number;
|
|
109
115
|
/**
|
|
110
116
|
* Maximum allowed payload size in bytes for WebSocket messages.
|
|
111
117
|
* Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages.
|