undici 6.24.1 → 6.26.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 +2 -0
- package/lib/dispatcher/agent.js +2 -2
- package/lib/dispatcher/client-h1.js +68 -22
- package/lib/dispatcher/client.js +3 -2
- package/lib/dispatcher/dispatcher-base.js +9 -1
- package/lib/dispatcher/pool-base.js +2 -2
- package/lib/dispatcher/pool.js +2 -2
- package/lib/web/websocket/permessage-deflate.js +13 -31
- package/lib/web/websocket/receiver.js +87 -24
- package/lib/web/websocket/websocket.js +5 -1
- package/package.json +1 -1
- package/types/client.d.ts +11 -0
package/docs/docs/api/Client.md
CHANGED
|
@@ -26,6 +26,8 @@ Returns: `Client`
|
|
|
26
26
|
* **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.
|
|
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
|
+
* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
|
|
30
|
+
* **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.
|
|
29
31
|
* **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.
|
|
30
32
|
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
|
|
31
33
|
* **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.
|
package/lib/dispatcher/agent.js
CHANGED
|
@@ -24,8 +24,6 @@ function defaultFactory (origin, opts) {
|
|
|
24
24
|
|
|
25
25
|
class Agent extends DispatcherBase {
|
|
26
26
|
constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) {
|
|
27
|
-
super()
|
|
28
|
-
|
|
29
27
|
if (typeof factory !== 'function') {
|
|
30
28
|
throw new InvalidArgumentError('factory must be a function.')
|
|
31
29
|
}
|
|
@@ -38,6 +36,8 @@ class Agent extends DispatcherBase {
|
|
|
38
36
|
throw new InvalidArgumentError('maxRedirections must be a positive number')
|
|
39
37
|
}
|
|
40
38
|
|
|
39
|
+
super(options)
|
|
40
|
+
|
|
41
41
|
if (connect && typeof connect !== 'function') {
|
|
42
42
|
connect = { ...connect }
|
|
43
43
|
}
|
|
@@ -279,29 +279,71 @@ class Parser {
|
|
|
279
279
|
|
|
280
280
|
const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr
|
|
281
281
|
|
|
282
|
-
if (ret
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
|
|
293
|
-
message =
|
|
294
|
-
'Response does not match the HTTP/1.1 protocol (' +
|
|
295
|
-
Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
|
|
296
|
-
')'
|
|
282
|
+
if (ret !== constants.ERROR.OK) {
|
|
283
|
+
const body = data.subarray(offset)
|
|
284
|
+
|
|
285
|
+
if (ret === constants.ERROR.PAUSED_UPGRADE) {
|
|
286
|
+
this.onUpgrade(body)
|
|
287
|
+
} else if (ret === constants.ERROR.PAUSED) {
|
|
288
|
+
this.paused = true
|
|
289
|
+
socket.unshift(body)
|
|
290
|
+
} else {
|
|
291
|
+
throw this.createError(ret, body)
|
|
297
292
|
}
|
|
298
|
-
throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset))
|
|
299
293
|
}
|
|
300
294
|
} catch (err) {
|
|
301
295
|
util.destroy(socket, err)
|
|
302
296
|
}
|
|
303
297
|
}
|
|
304
298
|
|
|
299
|
+
finish () {
|
|
300
|
+
assert(currentParser === null)
|
|
301
|
+
assert(this.ptr != null)
|
|
302
|
+
assert(!this.paused)
|
|
303
|
+
|
|
304
|
+
const { llhttp } = this
|
|
305
|
+
|
|
306
|
+
let ret
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
currentParser = this
|
|
310
|
+
ret = llhttp.llhttp_finish(this.ptr)
|
|
311
|
+
} finally {
|
|
312
|
+
currentParser = null
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (ret === constants.ERROR.OK) {
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) {
|
|
320
|
+
this.paused = true
|
|
321
|
+
return null
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return this.createError(ret, EMPTY_BUF)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
createError (ret, data) {
|
|
328
|
+
const { llhttp, contentLength, bytesRead } = this
|
|
329
|
+
|
|
330
|
+
if (contentLength && bytesRead !== parseInt(contentLength, 10)) {
|
|
331
|
+
return new ResponseContentLengthMismatchError()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const ptr = llhttp.llhttp_get_error_reason(this.ptr)
|
|
335
|
+
let message = ''
|
|
336
|
+
if (ptr) {
|
|
337
|
+
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
|
|
338
|
+
message =
|
|
339
|
+
'Response does not match the HTTP/1.1 protocol (' +
|
|
340
|
+
Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
|
|
341
|
+
')'
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return new HTTPParserError(message, constants.ERROR[ret], data)
|
|
345
|
+
}
|
|
346
|
+
|
|
305
347
|
destroy () {
|
|
306
348
|
assert(this.ptr != null)
|
|
307
349
|
assert(currentParser == null)
|
|
@@ -673,8 +715,11 @@ async function connectH1 (client, socket) {
|
|
|
673
715
|
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
|
|
674
716
|
// to the user.
|
|
675
717
|
if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
|
|
676
|
-
|
|
677
|
-
|
|
718
|
+
const parserErr = parser.finish()
|
|
719
|
+
if (parserErr) {
|
|
720
|
+
this[kError] = parserErr
|
|
721
|
+
this[kClient][kOnError](parserErr)
|
|
722
|
+
}
|
|
678
723
|
return
|
|
679
724
|
}
|
|
680
725
|
|
|
@@ -693,8 +738,10 @@ async function connectH1 (client, socket) {
|
|
|
693
738
|
const parser = this[kParser]
|
|
694
739
|
|
|
695
740
|
if (parser.statusCode && !parser.shouldKeepAlive) {
|
|
696
|
-
|
|
697
|
-
|
|
741
|
+
const parserErr = parser.finish()
|
|
742
|
+
if (parserErr) {
|
|
743
|
+
util.destroy(this, parserErr)
|
|
744
|
+
}
|
|
698
745
|
return
|
|
699
746
|
}
|
|
700
747
|
|
|
@@ -706,8 +753,7 @@ async function connectH1 (client, socket) {
|
|
|
706
753
|
|
|
707
754
|
if (parser) {
|
|
708
755
|
if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
|
|
709
|
-
|
|
710
|
-
parser.onMessageComplete()
|
|
756
|
+
this[kError] = parser.finish() || this[kError]
|
|
711
757
|
}
|
|
712
758
|
|
|
713
759
|
this[kParser].destroy()
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -106,9 +106,10 @@ class Client extends DispatcherBase {
|
|
|
106
106
|
autoSelectFamilyAttemptTimeout,
|
|
107
107
|
// h2
|
|
108
108
|
maxConcurrentStreams,
|
|
109
|
-
allowH2
|
|
109
|
+
allowH2,
|
|
110
|
+
webSocket
|
|
110
111
|
} = {}) {
|
|
111
|
-
super()
|
|
112
|
+
super({ webSocket })
|
|
112
113
|
|
|
113
114
|
if (keepAlive !== undefined) {
|
|
114
115
|
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
|
|
@@ -11,15 +11,23 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch, kInterceptors } = requ
|
|
|
11
11
|
const kOnDestroyed = Symbol('onDestroyed')
|
|
12
12
|
const kOnClosed = Symbol('onClosed')
|
|
13
13
|
const kInterceptedDispatch = Symbol('Intercepted Dispatch')
|
|
14
|
+
const kWebSocketOptions = Symbol('webSocketOptions')
|
|
14
15
|
|
|
15
16
|
class DispatcherBase extends Dispatcher {
|
|
16
|
-
constructor () {
|
|
17
|
+
constructor (opts) {
|
|
17
18
|
super()
|
|
18
19
|
|
|
19
20
|
this[kDestroyed] = false
|
|
20
21
|
this[kOnDestroyed] = null
|
|
21
22
|
this[kClosed] = false
|
|
22
23
|
this[kOnClosed] = []
|
|
24
|
+
this[kWebSocketOptions] = opts?.webSocket ?? {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get webSocketOptions () {
|
|
28
|
+
return {
|
|
29
|
+
maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024
|
|
30
|
+
}
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
get destroyed () {
|
|
@@ -19,8 +19,8 @@ const kRemoveClient = Symbol('remove client')
|
|
|
19
19
|
const kStats = Symbol('stats')
|
|
20
20
|
|
|
21
21
|
class PoolBase extends DispatcherBase {
|
|
22
|
-
constructor () {
|
|
23
|
-
super()
|
|
22
|
+
constructor (opts) {
|
|
23
|
+
super(opts)
|
|
24
24
|
|
|
25
25
|
this[kQueue] = new FixedQueue()
|
|
26
26
|
this[kClients] = []
|
package/lib/dispatcher/pool.js
CHANGED
|
@@ -37,8 +37,6 @@ class Pool extends PoolBase {
|
|
|
37
37
|
allowH2,
|
|
38
38
|
...options
|
|
39
39
|
} = {}) {
|
|
40
|
-
super()
|
|
41
|
-
|
|
42
40
|
if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
|
|
43
41
|
throw new InvalidArgumentError('invalid connections')
|
|
44
42
|
}
|
|
@@ -63,6 +61,8 @@ class Pool extends PoolBase {
|
|
|
63
61
|
})
|
|
64
62
|
}
|
|
65
63
|
|
|
64
|
+
super(options)
|
|
65
|
+
|
|
66
66
|
this[kInterceptors] = options.interceptors?.Pool && Array.isArray(options.interceptors.Pool)
|
|
67
67
|
? options.interceptors.Pool
|
|
68
68
|
: []
|
|
@@ -8,40 +8,35 @@ const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
|
|
|
8
8
|
const kBuffer = Symbol('kBuffer')
|
|
9
9
|
const kLength = Symbol('kLength')
|
|
10
10
|
|
|
11
|
-
// Default maximum decompressed message size: 4 MB
|
|
12
|
-
const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
|
|
13
|
-
|
|
14
11
|
class PerMessageDeflate {
|
|
15
12
|
/** @type {import('node:zlib').InflateRaw} */
|
|
16
13
|
#inflate
|
|
17
14
|
|
|
18
15
|
#options = {}
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
#aborted = false
|
|
22
|
-
|
|
23
|
-
/** @type {Function|null} */
|
|
24
|
-
#currentCallback = null
|
|
17
|
+
#maxPayloadSize = 0
|
|
25
18
|
|
|
26
19
|
/**
|
|
27
20
|
* @param {Map<string, string>} extensions
|
|
28
21
|
*/
|
|
29
|
-
constructor (extensions) {
|
|
22
|
+
constructor (extensions, options) {
|
|
30
23
|
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
|
|
31
24
|
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
|
|
25
|
+
|
|
26
|
+
this.#maxPayloadSize = options.maxPayloadSize
|
|
32
27
|
}
|
|
33
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Decompress a compressed payload.
|
|
31
|
+
* @param {Buffer} chunk Compressed data
|
|
32
|
+
* @param {boolean} fin Final fragment flag
|
|
33
|
+
* @param {Function} callback Callback function
|
|
34
|
+
*/
|
|
34
35
|
decompress (chunk, fin, callback) {
|
|
35
36
|
// An endpoint uses the following algorithm to decompress a message.
|
|
36
37
|
// 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
|
|
37
38
|
// payload of the message.
|
|
38
39
|
// 2. Decompress the resulting data using DEFLATE.
|
|
39
|
-
|
|
40
|
-
if (this.#aborted) {
|
|
41
|
-
callback(new MessageSizeExceededError())
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
40
|
if (!this.#inflate) {
|
|
46
41
|
let windowBits = Z_DEFAULT_WINDOWBITS
|
|
47
42
|
|
|
@@ -64,23 +59,12 @@ class PerMessageDeflate {
|
|
|
64
59
|
this.#inflate[kLength] = 0
|
|
65
60
|
|
|
66
61
|
this.#inflate.on('data', (data) => {
|
|
67
|
-
if (this.#aborted) {
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
|
|
71
62
|
this.#inflate[kLength] += data.length
|
|
72
63
|
|
|
73
|
-
if (this.#inflate[kLength] >
|
|
74
|
-
|
|
64
|
+
if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
|
|
65
|
+
callback(new MessageSizeExceededError())
|
|
75
66
|
this.#inflate.removeAllListeners()
|
|
76
|
-
this.#inflate.destroy()
|
|
77
67
|
this.#inflate = null
|
|
78
|
-
|
|
79
|
-
if (this.#currentCallback) {
|
|
80
|
-
const cb = this.#currentCallback
|
|
81
|
-
this.#currentCallback = null
|
|
82
|
-
cb(new MessageSizeExceededError())
|
|
83
|
-
}
|
|
84
68
|
return
|
|
85
69
|
}
|
|
86
70
|
|
|
@@ -93,14 +77,13 @@ class PerMessageDeflate {
|
|
|
93
77
|
})
|
|
94
78
|
}
|
|
95
79
|
|
|
96
|
-
this.#currentCallback = callback
|
|
97
80
|
this.#inflate.write(chunk)
|
|
98
81
|
if (fin) {
|
|
99
82
|
this.#inflate.write(tail)
|
|
100
83
|
}
|
|
101
84
|
|
|
102
85
|
this.#inflate.flush(() => {
|
|
103
|
-
if (
|
|
86
|
+
if (!this.#inflate) {
|
|
104
87
|
return
|
|
105
88
|
}
|
|
106
89
|
|
|
@@ -108,7 +91,6 @@ class PerMessageDeflate {
|
|
|
108
91
|
|
|
109
92
|
this.#inflate[kBuffer].length = 0
|
|
110
93
|
this.#inflate[kLength] = 0
|
|
111
|
-
this.#currentCallback = null
|
|
112
94
|
|
|
113
95
|
callback(null, full)
|
|
114
96
|
})
|
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
const { WebsocketFrameSend } = require('./frame')
|
|
19
19
|
const { closeWebSocketConnection } = require('./connection')
|
|
20
20
|
const { PerMessageDeflate } = require('./permessage-deflate')
|
|
21
|
+
const { MessageSizeExceededError } = require('../../core/errors')
|
|
21
22
|
|
|
22
23
|
// This code was influenced by ws released under the MIT license.
|
|
23
24
|
// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
|
@@ -26,6 +27,7 @@ const { PerMessageDeflate } = require('./permessage-deflate')
|
|
|
26
27
|
|
|
27
28
|
class ByteParser extends Writable {
|
|
28
29
|
#buffers = []
|
|
30
|
+
#fragmentsBytes = 0
|
|
29
31
|
#byteOffset = 0
|
|
30
32
|
#loop = false
|
|
31
33
|
|
|
@@ -37,18 +39,23 @@ class ByteParser extends Writable {
|
|
|
37
39
|
/** @type {Map<string, PerMessageDeflate>} */
|
|
38
40
|
#extensions
|
|
39
41
|
|
|
42
|
+
/** @type {number} */
|
|
43
|
+
#maxPayloadSize
|
|
44
|
+
|
|
40
45
|
/**
|
|
41
46
|
* @param {import('./websocket').WebSocket} ws
|
|
42
47
|
* @param {Map<string, string>|null} extensions
|
|
48
|
+
* @param {{ maxPayloadSize?: number }} [options]
|
|
43
49
|
*/
|
|
44
|
-
constructor (ws, extensions) {
|
|
50
|
+
constructor (ws, extensions, options = {}) {
|
|
45
51
|
super()
|
|
46
52
|
|
|
47
53
|
this.ws = ws
|
|
48
54
|
this.#extensions = extensions == null ? new Map() : extensions
|
|
55
|
+
this.#maxPayloadSize = options.maxPayloadSize ?? 0
|
|
49
56
|
|
|
50
57
|
if (this.#extensions.has('permessage-deflate')) {
|
|
51
|
-
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
|
|
58
|
+
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options))
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
|
|
@@ -64,6 +71,19 @@ class ByteParser extends Writable {
|
|
|
64
71
|
this.run(callback)
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
#validatePayloadLength () {
|
|
75
|
+
if (
|
|
76
|
+
this.#maxPayloadSize > 0 &&
|
|
77
|
+
!isControlFrame(this.#info.opcode) &&
|
|
78
|
+
this.#info.payloadLength > this.#maxPayloadSize
|
|
79
|
+
) {
|
|
80
|
+
failWebsocketConnection(this.ws, 'Payload size exceeds maximum allowed size')
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
/**
|
|
68
88
|
* Runs whenever a new chunk is received.
|
|
69
89
|
* Callback is called whenever there are no more chunks buffering,
|
|
@@ -152,6 +172,10 @@ class ByteParser extends Writable {
|
|
|
152
172
|
if (payloadLength <= 125) {
|
|
153
173
|
this.#info.payloadLength = payloadLength
|
|
154
174
|
this.#state = parserStates.READ_DATA
|
|
175
|
+
|
|
176
|
+
if (!this.#validatePayloadLength()) {
|
|
177
|
+
return
|
|
178
|
+
}
|
|
155
179
|
} else if (payloadLength === 126) {
|
|
156
180
|
this.#state = parserStates.PAYLOADLENGTH_16
|
|
157
181
|
} else if (payloadLength === 127) {
|
|
@@ -176,6 +200,10 @@ class ByteParser extends Writable {
|
|
|
176
200
|
|
|
177
201
|
this.#info.payloadLength = buffer.readUInt16BE(0)
|
|
178
202
|
this.#state = parserStates.READ_DATA
|
|
203
|
+
|
|
204
|
+
if (!this.#validatePayloadLength()) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
179
207
|
} else if (this.#state === parserStates.PAYLOADLENGTH_64) {
|
|
180
208
|
if (this.#byteOffset < 8) {
|
|
181
209
|
return callback()
|
|
@@ -198,6 +226,10 @@ class ByteParser extends Writable {
|
|
|
198
226
|
|
|
199
227
|
this.#info.payloadLength = lower
|
|
200
228
|
this.#state = parserStates.READ_DATA
|
|
229
|
+
|
|
230
|
+
if (!this.#validatePayloadLength()) {
|
|
231
|
+
return
|
|
232
|
+
}
|
|
201
233
|
} else if (this.#state === parserStates.READ_DATA) {
|
|
202
234
|
if (this.#byteOffset < this.#info.payloadLength) {
|
|
203
235
|
return callback()
|
|
@@ -210,42 +242,53 @@ class ByteParser extends Writable {
|
|
|
210
242
|
this.#state = parserStates.INFO
|
|
211
243
|
} else {
|
|
212
244
|
if (!this.#info.compressed) {
|
|
213
|
-
this
|
|
245
|
+
this.writeFragments(body)
|
|
246
|
+
|
|
247
|
+
if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
|
|
248
|
+
failWebsocketConnection(this.ws, new MessageSizeExceededError().message)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
214
251
|
|
|
215
252
|
// If the frame is not fragmented, a message has been received.
|
|
216
253
|
// If the frame is fragmented, it will terminate with a fin bit set
|
|
217
254
|
// and an opcode of 0 (continuation), therefore we handle that when
|
|
218
255
|
// parsing continuation frames, not here.
|
|
219
256
|
if (!this.#info.fragmented && this.#info.fin) {
|
|
220
|
-
|
|
221
|
-
websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
|
|
222
|
-
this.#fragments.length = 0
|
|
257
|
+
websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments())
|
|
223
258
|
}
|
|
224
259
|
|
|
225
260
|
this.#state = parserStates.INFO
|
|
226
261
|
} else {
|
|
227
|
-
this.#extensions.get('permessage-deflate').decompress(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
262
|
+
this.#extensions.get('permessage-deflate').decompress(
|
|
263
|
+
body,
|
|
264
|
+
this.#info.fin,
|
|
265
|
+
(error, data) => {
|
|
266
|
+
if (error) {
|
|
267
|
+
failWebsocketConnection(this.ws, error.message)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.writeFragments(data)
|
|
272
|
+
|
|
273
|
+
if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
|
|
274
|
+
failWebsocketConnection(this.ws, new MessageSizeExceededError().message)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!this.#info.fin) {
|
|
279
|
+
this.#state = parserStates.INFO
|
|
280
|
+
this.#loop = true
|
|
281
|
+
this.run(callback)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments())
|
|
232
286
|
|
|
233
|
-
this.#fragments.push(data)
|
|
234
|
-
|
|
235
|
-
if (!this.#info.fin) {
|
|
236
|
-
this.#state = parserStates.INFO
|
|
237
287
|
this.#loop = true
|
|
288
|
+
this.#state = parserStates.INFO
|
|
238
289
|
this.run(callback)
|
|
239
|
-
return
|
|
240
290
|
}
|
|
241
|
-
|
|
242
|
-
websocketMessageReceived(this.ws, this.#info.binaryType, Buffer.concat(this.#fragments))
|
|
243
|
-
|
|
244
|
-
this.#loop = true
|
|
245
|
-
this.#state = parserStates.INFO
|
|
246
|
-
this.#fragments.length = 0
|
|
247
|
-
this.run(callback)
|
|
248
|
-
})
|
|
291
|
+
)
|
|
249
292
|
|
|
250
293
|
this.#loop = false
|
|
251
294
|
break
|
|
@@ -297,6 +340,26 @@ class ByteParser extends Writable {
|
|
|
297
340
|
return buffer
|
|
298
341
|
}
|
|
299
342
|
|
|
343
|
+
writeFragments (fragment) {
|
|
344
|
+
this.#fragmentsBytes += fragment.length
|
|
345
|
+
this.#fragments.push(fragment)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
consumeFragments () {
|
|
349
|
+
const fragments = this.#fragments
|
|
350
|
+
|
|
351
|
+
if (fragments.length === 1) {
|
|
352
|
+
this.#fragmentsBytes = 0
|
|
353
|
+
return fragments.shift()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const output = Buffer.concat(fragments, this.#fragmentsBytes)
|
|
357
|
+
this.#fragments = []
|
|
358
|
+
this.#fragmentsBytes = 0
|
|
359
|
+
|
|
360
|
+
return output
|
|
361
|
+
}
|
|
362
|
+
|
|
300
363
|
parseCloseBody (data) {
|
|
301
364
|
assert(data.length !== 1)
|
|
302
365
|
|
|
@@ -435,7 +435,11 @@ class WebSocket extends EventTarget {
|
|
|
435
435
|
// once this happens, the connection is open
|
|
436
436
|
this[kResponse] = response
|
|
437
437
|
|
|
438
|
-
const
|
|
438
|
+
const maxPayloadSize = this[kController]?.dispatcher?.webSocketOptions?.maxPayloadSize
|
|
439
|
+
|
|
440
|
+
const parser = new ByteParser(this, parsedExtensions, {
|
|
441
|
+
maxPayloadSize
|
|
442
|
+
})
|
|
439
443
|
parser.on('drain', onParserDrain)
|
|
440
444
|
parser.on('error', onParserError.bind(this))
|
|
441
445
|
|
package/package.json
CHANGED
package/types/client.d.ts
CHANGED
|
@@ -78,6 +78,8 @@ export declare namespace Client {
|
|
|
78
78
|
localAddress?: string;
|
|
79
79
|
/** Max response body size in bytes, -1 is disabled */
|
|
80
80
|
maxResponseSize?: number;
|
|
81
|
+
/** WebSocket-specific options */
|
|
82
|
+
webSocket?: Client.WebSocketOptions;
|
|
81
83
|
/** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */
|
|
82
84
|
autoSelectFamily?: boolean;
|
|
83
85
|
/** 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,15 @@ export declare namespace Client {
|
|
|
103
105
|
bytesWritten?: number
|
|
104
106
|
bytesRead?: number
|
|
105
107
|
}
|
|
108
|
+
export interface WebSocketOptions {
|
|
109
|
+
/**
|
|
110
|
+
* Maximum allowed payload size in bytes for WebSocket messages.
|
|
111
|
+
* Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages.
|
|
112
|
+
* Set to 0 to disable the limit.
|
|
113
|
+
* @default 134217728 (128 MB)
|
|
114
|
+
*/
|
|
115
|
+
maxPayloadSize?: number;
|
|
116
|
+
}
|
|
106
117
|
}
|
|
107
118
|
|
|
108
119
|
export default Client;
|