undici 6.17.0 → 6.18.1
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/README.md +10 -10
- package/lib/web/fetch/data-url.js +1 -0
- package/lib/web/fetch/headers.js +11 -4
- package/lib/web/websocket/connection.js +18 -9
- package/lib/web/websocket/constants.js +9 -1
- package/lib/web/websocket/permessage-deflate.js +70 -0
- package/lib/web/websocket/receiver.js +63 -16
- package/lib/web/websocket/sender.js +104 -0
- package/lib/web/websocket/util.js +46 -1
- package/lib/web/websocket/websocket.js +28 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ This section documents our most commonly used API methods. Additional APIs are d
|
|
|
123
123
|
Arguments:
|
|
124
124
|
|
|
125
125
|
* **url** `string | URL | UrlObject`
|
|
126
|
-
* **options** [`RequestOptions`](./docs/api/Dispatcher.md#parameter-requestoptions)
|
|
126
|
+
* **options** [`RequestOptions`](./docs/docs/api/Dispatcher.md#parameter-requestoptions)
|
|
127
127
|
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
|
128
128
|
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
|
|
129
129
|
* **maxRedirections** `Integer` - Default: `0`
|
|
@@ -132,14 +132,14 @@ Returns a promise with the result of the `Dispatcher.request` method.
|
|
|
132
132
|
|
|
133
133
|
Calls `options.dispatcher.request(options)`.
|
|
134
134
|
|
|
135
|
-
See [Dispatcher.request](./docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples.
|
|
135
|
+
See [Dispatcher.request](./docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples.
|
|
136
136
|
|
|
137
137
|
### `undici.stream([url, options, ]factory): Promise`
|
|
138
138
|
|
|
139
139
|
Arguments:
|
|
140
140
|
|
|
141
141
|
* **url** `string | URL | UrlObject`
|
|
142
|
-
* **options** [`StreamOptions`](./docs/api/Dispatcher.md#parameter-streamoptions)
|
|
142
|
+
* **options** [`StreamOptions`](./docs/docs/api/Dispatcher.md#parameter-streamoptions)
|
|
143
143
|
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
|
144
144
|
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
|
|
145
145
|
* **maxRedirections** `Integer` - Default: `0`
|
|
@@ -149,14 +149,14 @@ Returns a promise with the result of the `Dispatcher.stream` method.
|
|
|
149
149
|
|
|
150
150
|
Calls `options.dispatcher.stream(options, factory)`.
|
|
151
151
|
|
|
152
|
-
See [Dispatcher.stream](./docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
|
|
152
|
+
See [Dispatcher.stream](./docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
|
|
153
153
|
|
|
154
154
|
### `undici.pipeline([url, options, ]handler): Duplex`
|
|
155
155
|
|
|
156
156
|
Arguments:
|
|
157
157
|
|
|
158
158
|
* **url** `string | URL | UrlObject`
|
|
159
|
-
* **options** [`PipelineOptions`](./docs/api/Dispatcher.md#parameter-pipelineoptions)
|
|
159
|
+
* **options** [`PipelineOptions`](./docs/docs/api/Dispatcher.md#parameter-pipelineoptions)
|
|
160
160
|
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
|
161
161
|
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
|
|
162
162
|
* **maxRedirections** `Integer` - Default: `0`
|
|
@@ -166,7 +166,7 @@ Returns: `stream.Duplex`
|
|
|
166
166
|
|
|
167
167
|
Calls `options.dispatch.pipeline(options, handler)`.
|
|
168
168
|
|
|
169
|
-
See [Dispatcher.pipeline](./docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
|
|
169
|
+
See [Dispatcher.pipeline](./docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
|
|
170
170
|
|
|
171
171
|
### `undici.connect([url, options]): Promise`
|
|
172
172
|
|
|
@@ -175,7 +175,7 @@ Starts two-way communications with the requested resource using [HTTP CONNECT](h
|
|
|
175
175
|
Arguments:
|
|
176
176
|
|
|
177
177
|
* **url** `string | URL | UrlObject`
|
|
178
|
-
* **options** [`ConnectOptions`](./docs/api/Dispatcher.md#parameter-connectoptions)
|
|
178
|
+
* **options** [`ConnectOptions`](./docs/docs/api/Dispatcher.md#parameter-connectoptions)
|
|
179
179
|
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
|
180
180
|
* **maxRedirections** `Integer` - Default: `0`
|
|
181
181
|
* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional)
|
|
@@ -184,7 +184,7 @@ Returns a promise with the result of the `Dispatcher.connect` method.
|
|
|
184
184
|
|
|
185
185
|
Calls `options.dispatch.connect(options)`.
|
|
186
186
|
|
|
187
|
-
See [Dispatcher.connect](./docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
|
|
187
|
+
See [Dispatcher.connect](./docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
|
|
188
188
|
|
|
189
189
|
### `undici.fetch(input[, init]): Promise`
|
|
190
190
|
|
|
@@ -335,7 +335,7 @@ Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](h
|
|
|
335
335
|
Arguments:
|
|
336
336
|
|
|
337
337
|
* **url** `string | URL | UrlObject`
|
|
338
|
-
* **options** [`UpgradeOptions`](./docs/api/Dispatcher.md#parameter-upgradeoptions)
|
|
338
|
+
* **options** [`UpgradeOptions`](./docs/docs/api/Dispatcher.md#parameter-upgradeoptions)
|
|
339
339
|
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
|
340
340
|
* **maxRedirections** `Integer` - Default: `0`
|
|
341
341
|
* **callback** `(error: Error | null, data: UpgradeData) => void` (optional)
|
|
@@ -344,7 +344,7 @@ Returns a promise with the result of the `Dispatcher.upgrade` method.
|
|
|
344
344
|
|
|
345
345
|
Calls `options.dispatcher.upgrade(options)`.
|
|
346
346
|
|
|
347
|
-
See [Dispatcher.upgrade](./docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
|
|
347
|
+
See [Dispatcher.upgrade](./docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
|
|
348
348
|
|
|
349
349
|
### `undici.setGlobalDispatcher(dispatcher)`
|
|
350
350
|
|
package/lib/web/fetch/headers.js
CHANGED
|
@@ -626,10 +626,6 @@ Reflect.deleteProperty(Headers, 'setHeadersGuard')
|
|
|
626
626
|
Reflect.deleteProperty(Headers, 'getHeadersList')
|
|
627
627
|
Reflect.deleteProperty(Headers, 'setHeadersList')
|
|
628
628
|
|
|
629
|
-
Object.defineProperty(Headers.prototype, util.inspect.custom, {
|
|
630
|
-
enumerable: false
|
|
631
|
-
})
|
|
632
|
-
|
|
633
629
|
iteratorMixin('Headers', Headers, kHeadersSortedMap, 0, 1)
|
|
634
630
|
|
|
635
631
|
Object.defineProperties(Headers.prototype, {
|
|
@@ -642,6 +638,17 @@ Object.defineProperties(Headers.prototype, {
|
|
|
642
638
|
[Symbol.toStringTag]: {
|
|
643
639
|
value: 'Headers',
|
|
644
640
|
configurable: true
|
|
641
|
+
},
|
|
642
|
+
[util.inspect.custom]: {
|
|
643
|
+
enumerable: false
|
|
644
|
+
},
|
|
645
|
+
// Compatibility for global headers
|
|
646
|
+
[Symbol('headers list')]: {
|
|
647
|
+
configurable: false,
|
|
648
|
+
enumerable: false,
|
|
649
|
+
get: function () {
|
|
650
|
+
return getHeadersList(this)
|
|
651
|
+
}
|
|
645
652
|
}
|
|
646
653
|
})
|
|
647
654
|
|
|
@@ -8,7 +8,7 @@ const {
|
|
|
8
8
|
kReceivedClose,
|
|
9
9
|
kResponse
|
|
10
10
|
} = require('./symbols')
|
|
11
|
-
const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished } = require('./util')
|
|
11
|
+
const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished, parseExtensions } = require('./util')
|
|
12
12
|
const { channels } = require('../../core/diagnostics')
|
|
13
13
|
const { CloseEvent } = require('./events')
|
|
14
14
|
const { makeRequest } = require('../fetch/request')
|
|
@@ -31,7 +31,7 @@ try {
|
|
|
31
31
|
* @param {URL} url
|
|
32
32
|
* @param {string|string[]} protocols
|
|
33
33
|
* @param {import('./websocket').WebSocket} ws
|
|
34
|
-
* @param {(response: any) => void} onEstablish
|
|
34
|
+
* @param {(response: any, extensions: string[] | undefined) => void} onEstablish
|
|
35
35
|
* @param {Partial<import('../../types/websocket').WebSocketInit>} options
|
|
36
36
|
*/
|
|
37
37
|
function establishWebSocketConnection (url, protocols, client, ws, onEstablish, options) {
|
|
@@ -91,12 +91,11 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
|
|
|
91
91
|
// 9. Let permessageDeflate be a user-agent defined
|
|
92
92
|
// "permessage-deflate" extension header value.
|
|
93
93
|
// https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673
|
|
94
|
-
|
|
95
|
-
const permessageDeflate = '' // 'permessage-deflate; 15'
|
|
94
|
+
const permessageDeflate = 'permessage-deflate; client_max_window_bits'
|
|
96
95
|
|
|
97
96
|
// 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to
|
|
98
97
|
// request’s header list.
|
|
99
|
-
|
|
98
|
+
request.headersList.append('sec-websocket-extensions', permessageDeflate)
|
|
100
99
|
|
|
101
100
|
// 11. Fetch request with useParallelQueue set to true, and
|
|
102
101
|
// processResponse given response being these steps:
|
|
@@ -167,10 +166,15 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
|
|
|
167
166
|
// header field to determine which extensions are requested is
|
|
168
167
|
// discussed in Section 9.1.)
|
|
169
168
|
const secExtension = response.headersList.get('Sec-WebSocket-Extensions')
|
|
169
|
+
let extensions
|
|
170
170
|
|
|
171
|
-
if (secExtension !== null
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
if (secExtension !== null) {
|
|
172
|
+
extensions = parseExtensions(secExtension)
|
|
173
|
+
|
|
174
|
+
if (!extensions.has('permessage-deflate')) {
|
|
175
|
+
failWebsocketConnection(ws, 'Sec-WebSocket-Extensions header does not match.')
|
|
176
|
+
return
|
|
177
|
+
}
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
// 6. If the response includes a |Sec-WebSocket-Protocol| header field
|
|
@@ -206,7 +210,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
|
|
|
206
210
|
})
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
onEstablish(response)
|
|
213
|
+
onEstablish(response, extensions)
|
|
210
214
|
}
|
|
211
215
|
})
|
|
212
216
|
|
|
@@ -290,6 +294,11 @@ function onSocketData (chunk) {
|
|
|
290
294
|
*/
|
|
291
295
|
function onSocketClose () {
|
|
292
296
|
const { ws } = this
|
|
297
|
+
const { [kResponse]: response } = ws
|
|
298
|
+
|
|
299
|
+
response.socket.off('data', onSocketData)
|
|
300
|
+
response.socket.off('close', onSocketClose)
|
|
301
|
+
response.socket.off('error', onSocketError)
|
|
293
302
|
|
|
294
303
|
// If the TCP connection was closed after the
|
|
295
304
|
// WebSocket closing handshake was completed, the WebSocket connection
|
|
@@ -46,6 +46,13 @@ const parserStates = {
|
|
|
46
46
|
|
|
47
47
|
const emptyBuffer = Buffer.allocUnsafe(0)
|
|
48
48
|
|
|
49
|
+
const sendHints = {
|
|
50
|
+
string: 1,
|
|
51
|
+
typedArray: 2,
|
|
52
|
+
arrayBuffer: 3,
|
|
53
|
+
blob: 4
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
module.exports = {
|
|
50
57
|
uid,
|
|
51
58
|
sentCloseFrameState,
|
|
@@ -54,5 +61,6 @@ module.exports = {
|
|
|
54
61
|
opcodes,
|
|
55
62
|
maxUnsigned16Bit,
|
|
56
63
|
parserStates,
|
|
57
|
-
emptyBuffer
|
|
64
|
+
emptyBuffer,
|
|
65
|
+
sendHints
|
|
58
66
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib')
|
|
4
|
+
const { isValidClientWindowBits } = require('./util')
|
|
5
|
+
|
|
6
|
+
const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
|
|
7
|
+
const kBuffer = Symbol('kBuffer')
|
|
8
|
+
const kLength = Symbol('kLength')
|
|
9
|
+
|
|
10
|
+
class PerMessageDeflate {
|
|
11
|
+
/** @type {import('node:zlib').InflateRaw} */
|
|
12
|
+
#inflate
|
|
13
|
+
|
|
14
|
+
#options = {}
|
|
15
|
+
|
|
16
|
+
constructor (extensions) {
|
|
17
|
+
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
|
|
18
|
+
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
decompress (chunk, fin, callback) {
|
|
22
|
+
// An endpoint uses the following algorithm to decompress a message.
|
|
23
|
+
// 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
|
|
24
|
+
// payload of the message.
|
|
25
|
+
// 2. Decompress the resulting data using DEFLATE.
|
|
26
|
+
|
|
27
|
+
if (!this.#inflate) {
|
|
28
|
+
let windowBits = Z_DEFAULT_WINDOWBITS
|
|
29
|
+
|
|
30
|
+
if (this.#options.serverMaxWindowBits) { // empty values default to Z_DEFAULT_WINDOWBITS
|
|
31
|
+
if (!isValidClientWindowBits(this.#options.serverMaxWindowBits)) {
|
|
32
|
+
callback(new Error('Invalid server_max_window_bits'))
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.#inflate = createInflateRaw({ windowBits })
|
|
40
|
+
this.#inflate[kBuffer] = []
|
|
41
|
+
this.#inflate[kLength] = 0
|
|
42
|
+
|
|
43
|
+
this.#inflate.on('data', (data) => {
|
|
44
|
+
this.#inflate[kBuffer].push(data)
|
|
45
|
+
this.#inflate[kLength] += data.length
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
this.#inflate.on('error', (err) => {
|
|
49
|
+
this.#inflate = null
|
|
50
|
+
callback(err)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.#inflate.write(chunk)
|
|
55
|
+
if (fin) {
|
|
56
|
+
this.#inflate.write(tail)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.#inflate.flush(() => {
|
|
60
|
+
const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
|
|
61
|
+
|
|
62
|
+
this.#inflate[kBuffer].length = 0
|
|
63
|
+
this.#inflate[kLength] = 0
|
|
64
|
+
|
|
65
|
+
callback(null, full)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { PerMessageDeflate }
|
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
} = require('./util')
|
|
18
18
|
const { WebsocketFrameSend } = require('./frame')
|
|
19
19
|
const { closeWebSocketConnection } = require('./connection')
|
|
20
|
+
const { PerMessageDeflate } = require('./permessage-deflate')
|
|
20
21
|
|
|
21
22
|
// This code was influenced by ws released under the MIT license.
|
|
22
23
|
// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
|
@@ -33,10 +34,18 @@ class ByteParser extends Writable {
|
|
|
33
34
|
#info = {}
|
|
34
35
|
#fragments = []
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
/** @type {Map<string, PerMessageDeflate>} */
|
|
38
|
+
#extensions
|
|
39
|
+
|
|
40
|
+
constructor (ws, extensions) {
|
|
37
41
|
super()
|
|
38
42
|
|
|
39
43
|
this.ws = ws
|
|
44
|
+
this.#extensions = extensions == null ? new Map() : extensions
|
|
45
|
+
|
|
46
|
+
if (this.#extensions.has('permessage-deflate')) {
|
|
47
|
+
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
|
|
48
|
+
}
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
/**
|
|
@@ -91,7 +100,16 @@ class ByteParser extends Writable {
|
|
|
91
100
|
// the negotiated extensions defines the meaning of such a nonzero
|
|
92
101
|
// value, the receiving endpoint MUST _Fail the WebSocket
|
|
93
102
|
// Connection_.
|
|
94
|
-
|
|
103
|
+
// This document allocates the RSV1 bit of the WebSocket header for
|
|
104
|
+
// PMCEs and calls the bit the "Per-Message Compressed" bit. On a
|
|
105
|
+
// WebSocket connection where a PMCE is in use, this bit indicates
|
|
106
|
+
// whether a message is compressed or not.
|
|
107
|
+
if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) {
|
|
108
|
+
failWebsocketConnection(this.ws, 'Expected RSV1 to be clear.')
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (rsv2 !== 0 || rsv3 !== 0) {
|
|
95
113
|
failWebsocketConnection(this.ws, 'RSV1, RSV2, RSV3 must be clear')
|
|
96
114
|
return
|
|
97
115
|
}
|
|
@@ -122,7 +140,7 @@ class ByteParser extends Writable {
|
|
|
122
140
|
return
|
|
123
141
|
}
|
|
124
142
|
|
|
125
|
-
if (isContinuationFrame(opcode) && this.#fragments.length === 0) {
|
|
143
|
+
if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) {
|
|
126
144
|
failWebsocketConnection(this.ws, 'Unexpected continuation frame')
|
|
127
145
|
return
|
|
128
146
|
}
|
|
@@ -138,6 +156,7 @@ class ByteParser extends Writable {
|
|
|
138
156
|
|
|
139
157
|
if (isTextBinaryFrame(opcode)) {
|
|
140
158
|
this.#info.binaryType = opcode
|
|
159
|
+
this.#info.compressed = rsv1 !== 0
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
this.#info.opcode = opcode
|
|
@@ -185,21 +204,50 @@ class ByteParser extends Writable {
|
|
|
185
204
|
|
|
186
205
|
if (isControlFrame(this.#info.opcode)) {
|
|
187
206
|
this.#loop = this.parseControlFrame(body)
|
|
207
|
+
this.#state = parserStates.INFO
|
|
188
208
|
} else {
|
|
189
|
-
this.#
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
209
|
+
if (!this.#info.compressed) {
|
|
210
|
+
this.#fragments.push(body)
|
|
211
|
+
|
|
212
|
+
// If the frame is not fragmented, a message has been received.
|
|
213
|
+
// If the frame is fragmented, it will terminate with a fin bit set
|
|
214
|
+
// and an opcode of 0 (continuation), therefore we handle that when
|
|
215
|
+
// parsing continuation frames, not here.
|
|
216
|
+
if (!this.#info.fragmented && this.#info.fin) {
|
|
217
|
+
const fullMessage = Buffer.concat(this.#fragments)
|
|
218
|
+
websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
|
|
219
|
+
this.#fragments.length = 0
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this.#state = parserStates.INFO
|
|
223
|
+
} else {
|
|
224
|
+
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
|
|
225
|
+
if (error) {
|
|
226
|
+
closeWebSocketConnection(this.ws, 1007, error.message, error.message.length)
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.#fragments.push(data)
|
|
231
|
+
|
|
232
|
+
if (!this.#info.fin) {
|
|
233
|
+
this.#state = parserStates.INFO
|
|
234
|
+
this.#loop = true
|
|
235
|
+
this.run(callback)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
websocketMessageReceived(this.ws, this.#info.binaryType, Buffer.concat(this.#fragments))
|
|
240
|
+
|
|
241
|
+
this.#loop = true
|
|
242
|
+
this.#state = parserStates.INFO
|
|
243
|
+
this.#fragments.length = 0
|
|
244
|
+
this.run(callback)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
this.#loop = false
|
|
248
|
+
break
|
|
199
249
|
}
|
|
200
250
|
}
|
|
201
|
-
|
|
202
|
-
this.#state = parserStates.INFO
|
|
203
251
|
}
|
|
204
252
|
}
|
|
205
253
|
}
|
|
@@ -333,7 +381,6 @@ class ByteParser extends Writable {
|
|
|
333
381
|
this.ws[kReadyState] = states.CLOSING
|
|
334
382
|
this.ws[kReceivedClose] = true
|
|
335
383
|
|
|
336
|
-
this.end()
|
|
337
384
|
return false
|
|
338
385
|
} else if (opcode === opcodes.PING) {
|
|
339
386
|
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { WebsocketFrameSend } = require('./frame')
|
|
4
|
+
const { opcodes, sendHints } = require('./constants')
|
|
5
|
+
const FixedQueue = require('../../dispatcher/fixed-queue')
|
|
6
|
+
|
|
7
|
+
/** @type {typeof Uint8Array} */
|
|
8
|
+
const FastBuffer = Buffer[Symbol.species]
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} SendQueueNode
|
|
12
|
+
* @property {Promise<void> | null} promise
|
|
13
|
+
* @property {((...args: any[]) => any)} callback
|
|
14
|
+
* @property {Buffer | null} frame
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
class SendQueue {
|
|
18
|
+
/**
|
|
19
|
+
* @type {FixedQueue}
|
|
20
|
+
*/
|
|
21
|
+
#queue = new FixedQueue()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @type {boolean}
|
|
25
|
+
*/
|
|
26
|
+
#running = false
|
|
27
|
+
|
|
28
|
+
/** @type {import('node:net').Socket} */
|
|
29
|
+
#socket
|
|
30
|
+
|
|
31
|
+
constructor (socket) {
|
|
32
|
+
this.#socket = socket
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
add (item, cb, hint) {
|
|
36
|
+
if (hint !== sendHints.blob) {
|
|
37
|
+
const frame = createFrame(item, hint)
|
|
38
|
+
if (!this.#running) {
|
|
39
|
+
// fast-path
|
|
40
|
+
this.#socket.write(frame, cb)
|
|
41
|
+
} else {
|
|
42
|
+
/** @type {SendQueueNode} */
|
|
43
|
+
const node = {
|
|
44
|
+
promise: null,
|
|
45
|
+
callback: cb,
|
|
46
|
+
frame
|
|
47
|
+
}
|
|
48
|
+
this.#queue.push(node)
|
|
49
|
+
}
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @type {SendQueueNode} */
|
|
54
|
+
const node = {
|
|
55
|
+
promise: item.arrayBuffer().then((ab) => {
|
|
56
|
+
node.promise = null
|
|
57
|
+
node.frame = createFrame(ab, hint)
|
|
58
|
+
}),
|
|
59
|
+
callback: cb,
|
|
60
|
+
frame: null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.#queue.push(node)
|
|
64
|
+
|
|
65
|
+
if (!this.#running) {
|
|
66
|
+
this.#run()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async #run () {
|
|
71
|
+
this.#running = true
|
|
72
|
+
const queue = this.#queue
|
|
73
|
+
while (!queue.isEmpty()) {
|
|
74
|
+
const node = queue.shift()
|
|
75
|
+
// wait pending promise
|
|
76
|
+
if (node.promise !== null) {
|
|
77
|
+
await node.promise
|
|
78
|
+
}
|
|
79
|
+
// write
|
|
80
|
+
this.#socket.write(node.frame, node.callback)
|
|
81
|
+
// cleanup
|
|
82
|
+
node.callback = node.frame = null
|
|
83
|
+
}
|
|
84
|
+
this.#running = false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createFrame (data, hint) {
|
|
89
|
+
return new WebsocketFrameSend(toBuffer(data, hint)).createFrame(hint === sendHints.string ? opcodes.TEXT : opcodes.BINARY)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toBuffer (data, hint) {
|
|
93
|
+
switch (hint) {
|
|
94
|
+
case sendHints.string:
|
|
95
|
+
return Buffer.from(data)
|
|
96
|
+
case sendHints.arrayBuffer:
|
|
97
|
+
case sendHints.blob:
|
|
98
|
+
return new FastBuffer(data)
|
|
99
|
+
case sendHints.typedArray:
|
|
100
|
+
return new FastBuffer(data.buffer, data.byteOffset, data.byteLength)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { SendQueue }
|
|
@@ -4,6 +4,7 @@ const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = requ
|
|
|
4
4
|
const { states, opcodes } = require('./constants')
|
|
5
5
|
const { ErrorEvent, createFastMessageEvent } = require('./events')
|
|
6
6
|
const { isUtf8 } = require('node:buffer')
|
|
7
|
+
const { collectASequenceOfCodePointsFast, removeHTTPWhitespace } = require('../fetch/data-url')
|
|
7
8
|
|
|
8
9
|
/* globals Blob */
|
|
9
10
|
|
|
@@ -234,6 +235,48 @@ function isValidOpcode (opcode) {
|
|
|
234
235
|
return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
|
|
235
236
|
}
|
|
236
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Parses a Sec-WebSocket-Extensions header value.
|
|
240
|
+
* @param {string} extensions
|
|
241
|
+
* @returns {Map<string, string>}
|
|
242
|
+
*/
|
|
243
|
+
// TODO(@Uzlopak, @KhafraDev): make compliant https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
|
|
244
|
+
function parseExtensions (extensions) {
|
|
245
|
+
const position = { position: 0 }
|
|
246
|
+
const extensionList = new Map()
|
|
247
|
+
|
|
248
|
+
while (position.position < extensions.length) {
|
|
249
|
+
const pair = collectASequenceOfCodePointsFast(';', extensions, position)
|
|
250
|
+
const [name, value = ''] = pair.split('=')
|
|
251
|
+
|
|
252
|
+
extensionList.set(
|
|
253
|
+
removeHTTPWhitespace(name, true, false),
|
|
254
|
+
removeHTTPWhitespace(value, false, true)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
position.position++
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return extensionList
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @see https://www.rfc-editor.org/rfc/rfc7692#section-7.1.2.2
|
|
265
|
+
* @description "client-max-window-bits = 1*DIGIT"
|
|
266
|
+
* @param {string} value
|
|
267
|
+
*/
|
|
268
|
+
function isValidClientWindowBits (value) {
|
|
269
|
+
for (let i = 0; i < value.length; i++) {
|
|
270
|
+
const byte = value.charCodeAt(i)
|
|
271
|
+
|
|
272
|
+
if (byte < 0x30 || byte > 0x39) {
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return true
|
|
278
|
+
}
|
|
279
|
+
|
|
237
280
|
// https://nodejs.org/api/intl.html#detecting-internationalization-support
|
|
238
281
|
const hasIntl = typeof process.versions.icu === 'string'
|
|
239
282
|
const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undefined
|
|
@@ -265,5 +308,7 @@ module.exports = {
|
|
|
265
308
|
isControlFrame,
|
|
266
309
|
isContinuationFrame,
|
|
267
310
|
isTextBinaryFrame,
|
|
268
|
-
isValidOpcode
|
|
311
|
+
isValidOpcode,
|
|
312
|
+
parseExtensions,
|
|
313
|
+
isValidClientWindowBits
|
|
269
314
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { webidl } = require('../fetch/webidl')
|
|
4
4
|
const { URLSerializer } = require('../fetch/data-url')
|
|
5
5
|
const { environmentSettingsObject } = require('../fetch/util')
|
|
6
|
-
const { staticPropertyDescriptors, states, sentCloseFrameState,
|
|
6
|
+
const { staticPropertyDescriptors, states, sentCloseFrameState, sendHints } = require('./constants')
|
|
7
7
|
const {
|
|
8
8
|
kWebSocketURL,
|
|
9
9
|
kReadyState,
|
|
@@ -21,17 +21,15 @@ const {
|
|
|
21
21
|
fireEvent
|
|
22
22
|
} = require('./util')
|
|
23
23
|
const { establishWebSocketConnection, closeWebSocketConnection } = require('./connection')
|
|
24
|
-
const { WebsocketFrameSend } = require('./frame')
|
|
25
24
|
const { ByteParser } = require('./receiver')
|
|
26
25
|
const { kEnumerableProperty, isBlobLike } = require('../../core/util')
|
|
27
26
|
const { getGlobalDispatcher } = require('../../global')
|
|
28
27
|
const { types } = require('node:util')
|
|
29
28
|
const { ErrorEvent, CloseEvent } = require('./events')
|
|
29
|
+
const { SendQueue } = require('./sender')
|
|
30
30
|
|
|
31
31
|
let experimentalWarned = false
|
|
32
32
|
|
|
33
|
-
const FastBuffer = Buffer[Symbol.species]
|
|
34
|
-
|
|
35
33
|
// https://websockets.spec.whatwg.org/#interface-definition
|
|
36
34
|
class WebSocket extends EventTarget {
|
|
37
35
|
#events = {
|
|
@@ -45,6 +43,9 @@ class WebSocket extends EventTarget {
|
|
|
45
43
|
#protocol = ''
|
|
46
44
|
#extensions = ''
|
|
47
45
|
|
|
46
|
+
/** @type {SendQueue} */
|
|
47
|
+
#sendQueue
|
|
48
|
+
|
|
48
49
|
/**
|
|
49
50
|
* @param {string} url
|
|
50
51
|
* @param {string|string[]} protocols
|
|
@@ -135,7 +136,7 @@ class WebSocket extends EventTarget {
|
|
|
135
136
|
protocols,
|
|
136
137
|
client,
|
|
137
138
|
this,
|
|
138
|
-
(response) => this.#onConnectionEstablished(response),
|
|
139
|
+
(response, extensions) => this.#onConnectionEstablished(response, extensions),
|
|
139
140
|
options
|
|
140
141
|
)
|
|
141
142
|
|
|
@@ -229,9 +230,6 @@ class WebSocket extends EventTarget {
|
|
|
229
230
|
return
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
/** @type {import('stream').Duplex} */
|
|
233
|
-
const socket = this[kResponse].socket
|
|
234
|
-
|
|
235
233
|
// If data is a string
|
|
236
234
|
if (typeof data === 'string') {
|
|
237
235
|
// If the WebSocket connection is established and the WebSocket
|
|
@@ -245,14 +243,12 @@ class WebSocket extends EventTarget {
|
|
|
245
243
|
// the bufferedAmount attribute by the number of bytes needed to
|
|
246
244
|
// express the argument as UTF-8.
|
|
247
245
|
|
|
248
|
-
const
|
|
249
|
-
const frame = new WebsocketFrameSend(value)
|
|
250
|
-
const buffer = frame.createFrame(opcodes.TEXT)
|
|
246
|
+
const length = Buffer.byteLength(data)
|
|
251
247
|
|
|
252
|
-
this.#bufferedAmount +=
|
|
253
|
-
|
|
254
|
-
this.#bufferedAmount -=
|
|
255
|
-
})
|
|
248
|
+
this.#bufferedAmount += length
|
|
249
|
+
this.#sendQueue.add(data, () => {
|
|
250
|
+
this.#bufferedAmount -= length
|
|
251
|
+
}, sendHints.string)
|
|
256
252
|
} else if (types.isArrayBuffer(data)) {
|
|
257
253
|
// If the WebSocket connection is established, and the WebSocket
|
|
258
254
|
// closing handshake has not yet started, then the user agent must
|
|
@@ -266,14 +262,10 @@ class WebSocket extends EventTarget {
|
|
|
266
262
|
// increase the bufferedAmount attribute by the length of the
|
|
267
263
|
// ArrayBuffer in bytes.
|
|
268
264
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
this.#bufferedAmount += value.byteLength
|
|
274
|
-
socket.write(buffer, () => {
|
|
275
|
-
this.#bufferedAmount -= value.byteLength
|
|
276
|
-
})
|
|
265
|
+
this.#bufferedAmount += data.byteLength
|
|
266
|
+
this.#sendQueue.add(data, () => {
|
|
267
|
+
this.#bufferedAmount -= data.byteLength
|
|
268
|
+
}, sendHints.arrayBuffer)
|
|
277
269
|
} else if (ArrayBuffer.isView(data)) {
|
|
278
270
|
// If the WebSocket connection is established, and the WebSocket
|
|
279
271
|
// closing handshake has not yet started, then the user agent must
|
|
@@ -287,15 +279,10 @@ class WebSocket extends EventTarget {
|
|
|
287
279
|
// not throw an exception must increase the bufferedAmount attribute
|
|
288
280
|
// by the length of data’s buffer in bytes.
|
|
289
281
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
this.#bufferedAmount += ab.byteLength
|
|
296
|
-
socket.write(buffer, () => {
|
|
297
|
-
this.#bufferedAmount -= ab.byteLength
|
|
298
|
-
})
|
|
282
|
+
this.#bufferedAmount += data.byteLength
|
|
283
|
+
this.#sendQueue.add(data, () => {
|
|
284
|
+
this.#bufferedAmount -= data.byteLength
|
|
285
|
+
}, sendHints.typedArray)
|
|
299
286
|
} else if (isBlobLike(data)) {
|
|
300
287
|
// If the WebSocket connection is established, and the WebSocket
|
|
301
288
|
// closing handshake has not yet started, then the user agent must
|
|
@@ -308,18 +295,10 @@ class WebSocket extends EventTarget {
|
|
|
308
295
|
// an exception must increase the bufferedAmount attribute by the size
|
|
309
296
|
// of the Blob object’s raw data, in bytes.
|
|
310
297
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
frame.frameData = value
|
|
316
|
-
const buffer = frame.createFrame(opcodes.BINARY)
|
|
317
|
-
|
|
318
|
-
this.#bufferedAmount += value.byteLength
|
|
319
|
-
socket.write(buffer, () => {
|
|
320
|
-
this.#bufferedAmount -= value.byteLength
|
|
321
|
-
})
|
|
322
|
-
})
|
|
298
|
+
this.#bufferedAmount += data.size
|
|
299
|
+
this.#sendQueue.add(data, () => {
|
|
300
|
+
this.#bufferedAmount -= data.size
|
|
301
|
+
}, sendHints.blob)
|
|
323
302
|
}
|
|
324
303
|
}
|
|
325
304
|
|
|
@@ -458,18 +437,20 @@ class WebSocket extends EventTarget {
|
|
|
458
437
|
/**
|
|
459
438
|
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
|
|
460
439
|
*/
|
|
461
|
-
#onConnectionEstablished (response) {
|
|
440
|
+
#onConnectionEstablished (response, parsedExtensions) {
|
|
462
441
|
// processResponse is called when the "response’s header list has been received and initialized."
|
|
463
442
|
// once this happens, the connection is open
|
|
464
443
|
this[kResponse] = response
|
|
465
444
|
|
|
466
|
-
const parser = new ByteParser(this)
|
|
445
|
+
const parser = new ByteParser(this, parsedExtensions)
|
|
467
446
|
parser.on('drain', onParserDrain)
|
|
468
447
|
parser.on('error', onParserError.bind(this))
|
|
469
448
|
|
|
470
449
|
response.socket.ws = this
|
|
471
450
|
this[kByteParser] = parser
|
|
472
451
|
|
|
452
|
+
this.#sendQueue = new SendQueue(response.socket)
|
|
453
|
+
|
|
473
454
|
// 1. Change the ready state to OPEN (1).
|
|
474
455
|
this[kReadyState] = states.OPEN
|
|
475
456
|
|
|
@@ -558,7 +539,7 @@ webidl.converters.WebSocketInit = webidl.dictionaryConverter([
|
|
|
558
539
|
},
|
|
559
540
|
{
|
|
560
541
|
key: 'dispatcher',
|
|
561
|
-
converter:
|
|
542
|
+
converter: webidl.converters.any,
|
|
562
543
|
defaultValue: () => getGlobalDispatcher()
|
|
563
544
|
},
|
|
564
545
|
{
|