undici 6.26.0 → 6.28.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/core/request.js +12 -1
- package/lib/dispatcher/client-h1.js +83 -3
- package/lib/dispatcher/dispatcher-base.js +1 -0
- package/lib/handler/retry-handler.js +34 -0
- package/lib/web/cookies/parse.js +16 -23
- package/lib/web/cookies/util.js +79 -9
- 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`.
|
package/lib/core/request.js
CHANGED
|
@@ -350,7 +350,13 @@ function processHeader (request, key, val) {
|
|
|
350
350
|
} else if (typeof val[i] === 'object') {
|
|
351
351
|
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
352
352
|
} else {
|
|
353
|
-
|
|
353
|
+
// Coerce primitives (and reject unsafe coercions such as functions
|
|
354
|
+
// with a crafted toString/Symbol.toPrimitive).
|
|
355
|
+
const str = `${val[i]}`
|
|
356
|
+
if (!isValidHeaderValue(str)) {
|
|
357
|
+
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
358
|
+
}
|
|
359
|
+
arr.push(str)
|
|
354
360
|
}
|
|
355
361
|
}
|
|
356
362
|
val = arr
|
|
@@ -361,7 +367,12 @@ function processHeader (request, key, val) {
|
|
|
361
367
|
} else if (val === null) {
|
|
362
368
|
val = ''
|
|
363
369
|
} else {
|
|
370
|
+
// Coerce primitives (and reject unsafe coercions such as functions
|
|
371
|
+
// with a crafted toString/Symbol.toPrimitive).
|
|
364
372
|
val = `${val}`
|
|
373
|
+
if (!isValidHeaderValue(val)) {
|
|
374
|
+
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
375
|
+
}
|
|
365
376
|
}
|
|
366
377
|
|
|
367
378
|
if (headerName === 'host') {
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
RequestContentLengthMismatchError,
|
|
11
11
|
ResponseContentLengthMismatchError,
|
|
12
12
|
RequestAbortedError,
|
|
13
|
+
InvalidArgumentError,
|
|
13
14
|
HeadersTimeoutError,
|
|
14
15
|
HeadersOverflowError,
|
|
15
16
|
SocketError,
|
|
@@ -57,6 +58,9 @@ const EMPTY_BUF = Buffer.alloc(0)
|
|
|
57
58
|
const FastBuffer = Buffer[Symbol.species]
|
|
58
59
|
const addListener = util.addListener
|
|
59
60
|
const removeAllListeners = util.removeAllListeners
|
|
61
|
+
const kIdleSocketValidation = Symbol('kIdleSocketValidation')
|
|
62
|
+
const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout')
|
|
63
|
+
const kSocketUsed = Symbol('kSocketUsed')
|
|
60
64
|
|
|
61
65
|
let extractBody
|
|
62
66
|
|
|
@@ -371,6 +375,11 @@ class Parser {
|
|
|
371
375
|
return -1
|
|
372
376
|
}
|
|
373
377
|
|
|
378
|
+
if (client[kRunning] === 0) {
|
|
379
|
+
util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
|
|
380
|
+
return -1
|
|
381
|
+
}
|
|
382
|
+
|
|
374
383
|
const request = client[kQueue][client[kRunningIdx]]
|
|
375
384
|
if (!request) {
|
|
376
385
|
return -1
|
|
@@ -474,6 +483,11 @@ class Parser {
|
|
|
474
483
|
return -1
|
|
475
484
|
}
|
|
476
485
|
|
|
486
|
+
if (client[kRunning] === 0) {
|
|
487
|
+
util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
|
|
488
|
+
return -1
|
|
489
|
+
}
|
|
490
|
+
|
|
477
491
|
const request = client[kQueue][client[kRunningIdx]]
|
|
478
492
|
|
|
479
493
|
/* istanbul ignore next: difficult to make a test case for */
|
|
@@ -647,6 +661,7 @@ class Parser {
|
|
|
647
661
|
request.onComplete(headers)
|
|
648
662
|
|
|
649
663
|
client[kQueue][client[kRunningIdx]++] = null
|
|
664
|
+
socket[kSocketUsed] = true
|
|
650
665
|
|
|
651
666
|
if (socket[kWriting]) {
|
|
652
667
|
assert(client[kRunning] === 0)
|
|
@@ -705,6 +720,9 @@ async function connectH1 (client, socket) {
|
|
|
705
720
|
socket[kWriting] = false
|
|
706
721
|
socket[kReset] = false
|
|
707
722
|
socket[kBlocking] = false
|
|
723
|
+
socket[kIdleSocketValidation] = 0
|
|
724
|
+
socket[kIdleSocketValidationTimeout] = null
|
|
725
|
+
socket[kSocketUsed] = false
|
|
708
726
|
socket[kParser] = new Parser(client, socket, llhttpInstance)
|
|
709
727
|
|
|
710
728
|
addListener(socket, 'error', function (err) {
|
|
@@ -751,6 +769,8 @@ async function connectH1 (client, socket) {
|
|
|
751
769
|
const client = this[kClient]
|
|
752
770
|
const parser = this[kParser]
|
|
753
771
|
|
|
772
|
+
clearIdleSocketValidation(this)
|
|
773
|
+
|
|
754
774
|
if (parser) {
|
|
755
775
|
if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
|
|
756
776
|
this[kError] = parser.finish() || this[kError]
|
|
@@ -816,7 +836,7 @@ async function connectH1 (client, socket) {
|
|
|
816
836
|
return socket.destroyed
|
|
817
837
|
},
|
|
818
838
|
busy (request) {
|
|
819
|
-
if (socket[kWriting] || socket[kReset] || socket[kBlocking]) {
|
|
839
|
+
if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) {
|
|
820
840
|
return true
|
|
821
841
|
}
|
|
822
842
|
|
|
@@ -854,6 +874,31 @@ async function connectH1 (client, socket) {
|
|
|
854
874
|
}
|
|
855
875
|
}
|
|
856
876
|
|
|
877
|
+
function clearIdleSocketValidation (socket) {
|
|
878
|
+
if (socket[kIdleSocketValidationTimeout]) {
|
|
879
|
+
clearTimeout(socket[kIdleSocketValidationTimeout])
|
|
880
|
+
socket[kIdleSocketValidationTimeout] = null
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
socket[kIdleSocketValidation] = 0
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function scheduleIdleSocketValidation (client, socket) {
|
|
887
|
+
socket[kIdleSocketValidation] = 1
|
|
888
|
+
socket[kIdleSocketValidationTimeout] = setTimeout(() => {
|
|
889
|
+
socket[kIdleSocketValidationTimeout] = null
|
|
890
|
+
socket[kIdleSocketValidation] = 2
|
|
891
|
+
|
|
892
|
+
if (client[kSocket] === socket && !socket.destroyed) {
|
|
893
|
+
client[kResume]()
|
|
894
|
+
}
|
|
895
|
+
}, 0)
|
|
896
|
+
socket[kIdleSocketValidationTimeout].unref?.()
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* @param {import('./client.js')} client
|
|
901
|
+
*/
|
|
857
902
|
function resumeH1 (client) {
|
|
858
903
|
const socket = client[kSocket]
|
|
859
904
|
|
|
@@ -868,6 +913,32 @@ function resumeH1 (client) {
|
|
|
868
913
|
socket[kNoRef] = false
|
|
869
914
|
}
|
|
870
915
|
|
|
916
|
+
if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) {
|
|
917
|
+
if (socket[kIdleSocketValidation] === 0) {
|
|
918
|
+
scheduleIdleSocketValidation(client, socket)
|
|
919
|
+
socket[kParser].readMore()
|
|
920
|
+
if (socket.destroyed) {
|
|
921
|
+
return
|
|
922
|
+
}
|
|
923
|
+
return
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (socket[kIdleSocketValidation] === 1) {
|
|
927
|
+
socket[kParser].readMore()
|
|
928
|
+
if (socket.destroyed) {
|
|
929
|
+
return
|
|
930
|
+
}
|
|
931
|
+
return
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (client[kRunning] === 0) {
|
|
936
|
+
socket[kParser].readMore()
|
|
937
|
+
if (socket.destroyed) {
|
|
938
|
+
return
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
871
942
|
if (client[kSize] === 0) {
|
|
872
943
|
if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
|
|
873
944
|
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
|
|
@@ -923,8 +994,16 @@ function writeH1 (client, request) {
|
|
|
923
994
|
}
|
|
924
995
|
body = bodyStream.stream
|
|
925
996
|
contentLength = bodyStream.length
|
|
926
|
-
} else if (util.isBlobLike(body) && request.contentType == null
|
|
927
|
-
|
|
997
|
+
} else if (util.isBlobLike(body) && request.contentType == null) {
|
|
998
|
+
const contentType = body.type
|
|
999
|
+
if (contentType) {
|
|
1000
|
+
const contentTypeValue = `${contentType}`
|
|
1001
|
+
if (!util.isValidHeaderValue(contentTypeValue)) {
|
|
1002
|
+
util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
|
|
1003
|
+
return false
|
|
1004
|
+
}
|
|
1005
|
+
headers.push('content-type', contentTypeValue)
|
|
1006
|
+
}
|
|
928
1007
|
}
|
|
929
1008
|
|
|
930
1009
|
if (body && typeof body.read === 'function') {
|
|
@@ -961,6 +1040,7 @@ function writeH1 (client, request) {
|
|
|
961
1040
|
}
|
|
962
1041
|
|
|
963
1042
|
const socket = client[kSocket]
|
|
1043
|
+
clearIdleSocketValidation(socket)
|
|
964
1044
|
|
|
965
1045
|
const abort = (err) => {
|
|
966
1046
|
if (request.aborted || request.completed) {
|
|
@@ -15,6 +15,28 @@ function calculateRetryAfterHeader (retryAfter) {
|
|
|
15
15
|
return new Date(retryAfter).getTime() - current
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
|
|
19
|
+
const contentLength = headers['content-length']
|
|
20
|
+
if (contentLength == null) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const length = Number(contentLength)
|
|
29
|
+
const expectedLength = range.end - range.start + 1
|
|
30
|
+
if (!Number.isFinite(length) || length !== expectedLength) {
|
|
31
|
+
return new RequestRetryError('Content-Length mismatch', statusCode, {
|
|
32
|
+
headers,
|
|
33
|
+
data: { count: retryCount }
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
class RetryHandler {
|
|
19
41
|
constructor (opts, handlers) {
|
|
20
42
|
const { retryOptions, ...dispatchOpts } = opts
|
|
@@ -229,6 +251,12 @@ class RetryHandler {
|
|
|
229
251
|
return false
|
|
230
252
|
}
|
|
231
253
|
|
|
254
|
+
const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
|
|
255
|
+
if (contentLengthError != null) {
|
|
256
|
+
this.abort(contentLengthError)
|
|
257
|
+
return false
|
|
258
|
+
}
|
|
259
|
+
|
|
232
260
|
const { start, size, end = size - 1 } = contentRange
|
|
233
261
|
|
|
234
262
|
assert(this.start === start, 'content-range mismatch')
|
|
@@ -252,6 +280,12 @@ class RetryHandler {
|
|
|
252
280
|
)
|
|
253
281
|
}
|
|
254
282
|
|
|
283
|
+
const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
|
|
284
|
+
if (contentLengthError != null) {
|
|
285
|
+
this.abort(contentLengthError)
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
|
|
255
289
|
const { start, size, end = size - 1 } = range
|
|
256
290
|
assert(
|
|
257
291
|
start != null && Number.isFinite(start),
|
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
|
|
package/lib/web/cookies/util.js
CHANGED
|
@@ -105,7 +105,7 @@ function validateCookiePath (path) {
|
|
|
105
105
|
|
|
106
106
|
if (
|
|
107
107
|
code < 0x20 || // exclude CTLs (0-31)
|
|
108
|
-
code
|
|
108
|
+
code > 0x7E || // exclude DEL and non-ascii
|
|
109
109
|
code === 0x3B // ;
|
|
110
110
|
) {
|
|
111
111
|
throw new Error('Invalid cookie path')
|
|
@@ -114,16 +114,80 @@ function validateCookiePath (path) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
117
|
+
* <let-dig> ::= <letter> | <digit>
|
|
118
|
+
*
|
|
119
|
+
* <letter> ::= any one of the 52 alphabetic characters A through Z in
|
|
120
|
+
* upper case and a through z in lower case
|
|
121
|
+
*
|
|
122
|
+
* <digit> ::= any one of the ten digits 0 through 9r
|
|
123
|
+
*
|
|
124
|
+
* @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
|
|
125
|
+
* @param {number} code
|
|
126
|
+
*/
|
|
127
|
+
function isLetterOrDigit (code) {
|
|
128
|
+
return (
|
|
129
|
+
(code >= 0x30 && code <= 0x39) || // 0-9
|
|
130
|
+
(code >= 0x41 && code <= 0x5A) || // A-Z
|
|
131
|
+
(code >= 0x61 && code <= 0x7A) // a-z
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validates a cookie domain against the "preferred name syntax".
|
|
137
|
+
*
|
|
138
|
+
* <domain> ::= <subdomain> | " "
|
|
139
|
+
* <subdomain> ::= <label> | <subdomain> "." <label>
|
|
140
|
+
* <label> ::= <let-dig> [ [ <ldh-str> ] <let-dig> ]
|
|
141
|
+
* <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
|
|
142
|
+
* <let-dig-hyp> ::= <let-dig> | "-"
|
|
143
|
+
*
|
|
144
|
+
* @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
|
|
145
|
+
* @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
|
|
146
|
+
* @see https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4
|
|
119
147
|
* @param {string} domain
|
|
120
148
|
*/
|
|
121
149
|
function validateCookieDomain (domain) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
150
|
+
// <domain> ::= <subdomain> | " "
|
|
151
|
+
if (domain === ' ') {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (domain.length > 255) {
|
|
156
|
+
throw new Error('Invalid cookie domain')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let labelLength = 0
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < domain.length; ++i) {
|
|
162
|
+
const code = domain.charCodeAt(i)
|
|
163
|
+
|
|
164
|
+
if (code === 0x2E) {
|
|
165
|
+
if (labelLength === 0) {
|
|
166
|
+
throw new Error('Invalid cookie domain')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (domain.charCodeAt(i - 1) === 0x2D) { // "-"
|
|
170
|
+
throw new Error('Invalid cookie domain')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
labelLength = 0
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (labelLength === 0 && !isLetterOrDigit(code)) {
|
|
178
|
+
throw new Error('Invalid cookie domain')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!isLetterOrDigit(code) && code !== 0x2D) { // "-"
|
|
182
|
+
throw new Error('Invalid cookie domain')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (++labelLength > 63) {
|
|
186
|
+
throw new Error('Invalid cookie domain')
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (labelLength === 0 || domain.charCodeAt(domain.length - 1) === 0x2D) { // "-"
|
|
127
191
|
throw new Error('Invalid cookie domain')
|
|
128
192
|
}
|
|
129
193
|
}
|
|
@@ -266,7 +330,13 @@ function stringify (cookie) {
|
|
|
266
330
|
|
|
267
331
|
const [key, ...value] = part.split('=')
|
|
268
332
|
|
|
269
|
-
|
|
333
|
+
const trimmedKey = key.trim()
|
|
334
|
+
const joinedValue = value.join('=')
|
|
335
|
+
|
|
336
|
+
validateCookieName(trimmedKey)
|
|
337
|
+
validateCookieValue(joinedValue)
|
|
338
|
+
|
|
339
|
+
out.push(`${trimmedKey}=${joinedValue}`)
|
|
270
340
|
}
|
|
271
341
|
|
|
272
342
|
return out.join('; ')
|
|
@@ -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.
|