undici 7.0.0-alpha.1 → 7.0.0-alpha.10
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 +24 -38
- package/docs/docs/api/Agent.md +14 -14
- package/docs/docs/api/BalancedPool.md +16 -16
- package/docs/docs/api/CacheStore.md +131 -0
- package/docs/docs/api/Client.md +12 -12
- package/docs/docs/api/Debug.md +1 -1
- package/docs/docs/api/Dispatcher.md +98 -193
- package/docs/docs/api/EnvHttpProxyAgent.md +12 -12
- package/docs/docs/api/MockAgent.md +5 -3
- package/docs/docs/api/MockClient.md +5 -5
- package/docs/docs/api/MockPool.md +4 -3
- package/docs/docs/api/Pool.md +15 -15
- package/docs/docs/api/PoolStats.md +1 -1
- package/docs/docs/api/ProxyAgent.md +3 -3
- package/docs/docs/api/RedirectHandler.md +1 -1
- package/docs/docs/api/RetryAgent.md +1 -1
- package/docs/docs/api/RetryHandler.md +4 -4
- package/docs/docs/api/WebSocket.md +46 -4
- package/docs/docs/api/api-lifecycle.md +11 -11
- package/docs/docs/best-practices/mocking-request.md +2 -2
- package/docs/docs/best-practices/proxy.md +1 -1
- package/index.d.ts +1 -1
- package/index.js +23 -3
- package/lib/api/abort-signal.js +2 -0
- package/lib/api/api-pipeline.js +4 -2
- package/lib/api/api-request.js +6 -4
- package/lib/api/api-stream.js +3 -1
- package/lib/api/api-upgrade.js +2 -2
- package/lib/api/readable.js +200 -47
- package/lib/api/util.js +2 -0
- package/lib/cache/memory-cache-store.js +177 -0
- package/lib/cache/sqlite-cache-store.js +446 -0
- package/lib/core/connect.js +54 -22
- package/lib/core/constants.js +35 -10
- package/lib/core/diagnostics.js +122 -128
- package/lib/core/errors.js +2 -2
- package/lib/core/request.js +6 -6
- package/lib/core/symbols.js +2 -0
- package/lib/core/tree.js +4 -2
- package/lib/core/util.js +238 -40
- package/lib/dispatcher/client-h1.js +405 -142
- package/lib/dispatcher/client-h2.js +212 -109
- package/lib/dispatcher/client.js +24 -7
- package/lib/dispatcher/dispatcher-base.js +4 -1
- package/lib/dispatcher/dispatcher.js +4 -0
- package/lib/dispatcher/fixed-queue.js +91 -49
- package/lib/dispatcher/pool-base.js +3 -3
- package/lib/dispatcher/pool-stats.js +2 -0
- package/lib/dispatcher/proxy-agent.js +3 -1
- package/lib/handler/cache-handler.js +393 -0
- package/lib/handler/cache-revalidation-handler.js +124 -0
- package/lib/handler/decorator-handler.js +3 -0
- package/lib/handler/redirect-handler.js +45 -59
- package/lib/handler/retry-handler.js +68 -109
- package/lib/handler/unwrap-handler.js +96 -0
- package/lib/handler/wrap-handler.js +98 -0
- package/lib/interceptor/cache.js +350 -0
- package/lib/interceptor/dns.js +375 -0
- package/lib/interceptor/response-error.js +15 -7
- package/lib/mock/mock-agent.js +5 -8
- package/lib/mock/mock-client.js +7 -2
- package/lib/mock/mock-errors.js +3 -1
- package/lib/mock/mock-interceptor.js +8 -6
- package/lib/mock/mock-pool.js +7 -2
- package/lib/mock/mock-symbols.js +2 -1
- package/lib/mock/mock-utils.js +33 -5
- package/lib/util/cache.js +360 -0
- package/lib/util/timers.js +50 -6
- package/lib/web/cache/cache.js +25 -21
- package/lib/web/cache/cachestorage.js +3 -1
- package/lib/web/cookies/index.js +18 -5
- package/lib/web/cookies/parse.js +6 -1
- package/lib/web/eventsource/eventsource.js +2 -0
- package/lib/web/fetch/body.js +43 -39
- package/lib/web/fetch/constants.js +45 -29
- package/lib/web/fetch/data-url.js +2 -2
- package/lib/web/fetch/formdata-parser.js +84 -46
- package/lib/web/fetch/formdata.js +42 -20
- package/lib/web/fetch/headers.js +119 -85
- package/lib/web/fetch/index.js +69 -65
- package/lib/web/fetch/request.js +132 -55
- package/lib/web/fetch/response.js +81 -36
- package/lib/web/fetch/util.js +274 -103
- package/lib/web/fetch/webidl.js +54 -18
- package/lib/web/websocket/connection.js +92 -15
- package/lib/web/websocket/constants.js +69 -9
- package/lib/web/websocket/events.js +8 -2
- package/lib/web/websocket/receiver.js +20 -26
- package/lib/web/websocket/stream/websocketerror.js +83 -0
- package/lib/web/websocket/stream/websocketstream.js +485 -0
- package/lib/web/websocket/util.js +115 -10
- package/lib/web/websocket/websocket.js +47 -170
- package/package.json +15 -11
- package/types/agent.d.ts +1 -1
- package/types/cache-interceptor.d.ts +172 -0
- package/types/cookies.d.ts +2 -0
- package/types/dispatcher.d.ts +29 -4
- package/types/env-http-proxy-agent.d.ts +1 -1
- package/types/fetch.d.ts +9 -8
- package/types/handlers.d.ts +4 -4
- package/types/index.d.ts +3 -1
- package/types/interceptors.d.ts +18 -1
- package/types/mock-agent.d.ts +4 -1
- package/types/mock-client.d.ts +1 -1
- package/types/mock-pool.d.ts +1 -1
- package/types/proxy-agent.d.ts +1 -1
- package/types/readable.d.ts +10 -7
- package/types/retry-handler.d.ts +3 -3
- package/types/webidl.d.ts +30 -4
- package/types/websocket.d.ts +33 -0
- package/lib/mock/pluralizer.js +0 -29
- package/lib/web/cache/symbols.js +0 -5
- package/lib/web/fetch/symbols.js +0 -8
package/lib/web/fetch/webidl.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { types, inspect } = require('node:util')
|
|
4
|
+
const { markAsUncloneable } = require('node:worker_threads')
|
|
4
5
|
const { toUSVString } = require('../../core/util')
|
|
5
6
|
|
|
6
7
|
const UNDEFINED = 1
|
|
@@ -12,11 +13,15 @@ const BIGINT = 6
|
|
|
12
13
|
const NULL = 7
|
|
13
14
|
const OBJECT = 8 // function and object
|
|
14
15
|
|
|
16
|
+
const FunctionPrototypeSymbolHasInstance = Function.call.bind(Function.prototype[Symbol.hasInstance])
|
|
17
|
+
|
|
15
18
|
/** @type {import('../../../types/webidl').Webidl} */
|
|
16
|
-
const webidl = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const webidl = {
|
|
20
|
+
converters: {},
|
|
21
|
+
util: {},
|
|
22
|
+
errors: {},
|
|
23
|
+
is: {}
|
|
24
|
+
}
|
|
20
25
|
|
|
21
26
|
webidl.errors.exception = function (message) {
|
|
22
27
|
return new TypeError(`${message.header}: ${message.message}`)
|
|
@@ -43,18 +48,22 @@ webidl.errors.invalidArgument = function (context) {
|
|
|
43
48
|
|
|
44
49
|
// https://webidl.spec.whatwg.org/#implements
|
|
45
50
|
webidl.brandCheck = function (V, I) {
|
|
46
|
-
if (!(V
|
|
51
|
+
if (!FunctionPrototypeSymbolHasInstance(I, V)) {
|
|
47
52
|
const err = new TypeError('Illegal invocation')
|
|
48
53
|
err.code = 'ERR_INVALID_THIS' // node compat.
|
|
49
54
|
throw err
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
webidl.brandCheckMultiple = function (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
webidl.brandCheckMultiple = function (List) {
|
|
59
|
+
const prototypes = List.map((c) => webidl.util.MakeTypeAssertion(c))
|
|
60
|
+
|
|
61
|
+
return (V) => {
|
|
62
|
+
if (prototypes.every(typeCheck => !typeCheck(V))) {
|
|
63
|
+
const err = new TypeError('Illegal invocation')
|
|
64
|
+
err.code = 'ERR_INVALID_THIS' // node compat.
|
|
65
|
+
throw err
|
|
66
|
+
}
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
69
|
|
|
@@ -75,6 +84,10 @@ webidl.illegalConstructor = function () {
|
|
|
75
84
|
})
|
|
76
85
|
}
|
|
77
86
|
|
|
87
|
+
webidl.util.MakeTypeAssertion = function (I) {
|
|
88
|
+
return (O) => FunctionPrototypeSymbolHasInstance(I, O)
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
|
|
79
92
|
webidl.util.Type = function (V) {
|
|
80
93
|
switch (typeof V) {
|
|
@@ -119,6 +132,8 @@ webidl.util.TypeValueToString = function (o) {
|
|
|
119
132
|
}
|
|
120
133
|
}
|
|
121
134
|
|
|
135
|
+
webidl.util.markAsUncloneable = markAsUncloneable || (() => {})
|
|
136
|
+
|
|
122
137
|
// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
|
|
123
138
|
webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) {
|
|
124
139
|
let upperBound
|
|
@@ -330,12 +345,14 @@ webidl.recordConverter = function (keyConverter, valueConverter) {
|
|
|
330
345
|
const keys = [...Object.getOwnPropertyNames(O), ...Object.getOwnPropertySymbols(O)]
|
|
331
346
|
|
|
332
347
|
for (const key of keys) {
|
|
348
|
+
const keyName = webidl.util.Stringify(key)
|
|
349
|
+
|
|
333
350
|
// 1. Let typedKey be key converted to an IDL value of type K.
|
|
334
|
-
const typedKey = keyConverter(key, prefix, argument)
|
|
351
|
+
const typedKey = keyConverter(key, prefix, `Key ${keyName} in ${argument}`)
|
|
335
352
|
|
|
336
353
|
// 2. Let value be ? Get(O, key).
|
|
337
354
|
// 3. Let typedValue be value converted to an IDL value of type V.
|
|
338
|
-
const typedValue = valueConverter(O[key], prefix, argument)
|
|
355
|
+
const typedValue = valueConverter(O[key], prefix, `${argument}[${keyName}]`)
|
|
339
356
|
|
|
340
357
|
// 4. Set result[typedKey] to typedValue.
|
|
341
358
|
result[typedKey] = typedValue
|
|
@@ -372,12 +389,12 @@ webidl.recordConverter = function (keyConverter, valueConverter) {
|
|
|
372
389
|
}
|
|
373
390
|
}
|
|
374
391
|
|
|
375
|
-
webidl.interfaceConverter = function (
|
|
392
|
+
webidl.interfaceConverter = function (TypeCheck, name) {
|
|
376
393
|
return (V, prefix, argument) => {
|
|
377
|
-
if (!(V
|
|
394
|
+
if (!TypeCheck(V)) {
|
|
378
395
|
throw webidl.errors.exception({
|
|
379
396
|
header: prefix,
|
|
380
|
-
message: `Expected ${argument} ("${webidl.util.Stringify(V)}") to be an instance of ${
|
|
397
|
+
message: `Expected ${argument} ("${webidl.util.Stringify(V)}") to be an instance of ${name}.`
|
|
381
398
|
})
|
|
382
399
|
}
|
|
383
400
|
|
|
@@ -451,6 +468,14 @@ webidl.nullableConverter = function (converter) {
|
|
|
451
468
|
}
|
|
452
469
|
}
|
|
453
470
|
|
|
471
|
+
webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream)
|
|
472
|
+
webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob)
|
|
473
|
+
webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams)
|
|
474
|
+
webidl.is.File = webidl.util.MakeTypeAssertion(globalThis.File ?? require('node:buffer').File)
|
|
475
|
+
webidl.is.URL = webidl.util.MakeTypeAssertion(URL)
|
|
476
|
+
webidl.is.AbortSignal = webidl.util.MakeTypeAssertion(AbortSignal)
|
|
477
|
+
webidl.is.MessagePort = webidl.util.MakeTypeAssertion(MessagePort)
|
|
478
|
+
|
|
454
479
|
// https://webidl.spec.whatwg.org/#es-DOMString
|
|
455
480
|
webidl.converters.DOMString = function (V, prefix, argument, opts) {
|
|
456
481
|
// 1. If V is null and the conversion is to an IDL type
|
|
@@ -478,8 +503,14 @@ webidl.converters.DOMString = function (V, prefix, argument, opts) {
|
|
|
478
503
|
// https://webidl.spec.whatwg.org/#es-ByteString
|
|
479
504
|
webidl.converters.ByteString = function (V, prefix, argument) {
|
|
480
505
|
// 1. Let x be ? ToString(V).
|
|
481
|
-
|
|
482
|
-
|
|
506
|
+
if (typeof V === 'symbol') {
|
|
507
|
+
throw webidl.errors.exception({
|
|
508
|
+
header: prefix,
|
|
509
|
+
message: `${argument} is a symbol, which cannot be converted to a ByteString.`
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const x = String(V)
|
|
483
514
|
|
|
484
515
|
// 2. If the value of any element of x is greater than
|
|
485
516
|
// 255, then throw a TypeError.
|
|
@@ -697,7 +728,12 @@ webidl.converters['record<ByteString, ByteString>'] = webidl.recordConverter(
|
|
|
697
728
|
webidl.converters.ByteString
|
|
698
729
|
)
|
|
699
730
|
|
|
700
|
-
webidl.converters.Blob = webidl.interfaceConverter(Blob)
|
|
731
|
+
webidl.converters.Blob = webidl.interfaceConverter(webidl.is.Blob, 'Blob')
|
|
732
|
+
|
|
733
|
+
webidl.converters.AbortSignal = webidl.interfaceConverter(
|
|
734
|
+
webidl.is.AbortSignal,
|
|
735
|
+
'AbortSignal'
|
|
736
|
+
)
|
|
701
737
|
|
|
702
738
|
module.exports = {
|
|
703
739
|
webidl
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { uid } = require('./constants')
|
|
4
|
-
const { failWebsocketConnection, parseExtensions } = require('./util')
|
|
3
|
+
const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
|
|
4
|
+
const { failWebsocketConnection, parseExtensions, isClosed, isClosing, isEstablished, validateCloseCodeAndReason } = require('./util')
|
|
5
5
|
const { channels } = require('../../core/diagnostics')
|
|
6
6
|
const { makeRequest } = require('../fetch/request')
|
|
7
7
|
const { fetching } = require('../fetch/index')
|
|
8
8
|
const { Headers, getHeadersList } = require('../fetch/headers')
|
|
9
9
|
const { getDecodeSplit } = require('../fetch/util')
|
|
10
|
+
const { WebsocketFrameSend } = require('./frame')
|
|
11
|
+
const assert = require('node:assert')
|
|
10
12
|
|
|
11
13
|
/** @type {import('crypto')} */
|
|
12
14
|
let crypto
|
|
@@ -94,10 +96,16 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
94
96
|
useParallelQueue: true,
|
|
95
97
|
dispatcher: options.dispatcher,
|
|
96
98
|
processResponse (response) {
|
|
99
|
+
if (response.type === 'error') {
|
|
100
|
+
// If the WebSocket connection could not be established, it is also said
|
|
101
|
+
// that _The WebSocket Connection is Closed_, but not _cleanly_.
|
|
102
|
+
handler.readyState = states.CLOSED
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
// 1. If response is a network error or its status is not 101,
|
|
98
106
|
// fail the WebSocket connection.
|
|
99
107
|
if (response.type === 'error' || response.status !== 101) {
|
|
100
|
-
failWebsocketConnection(handler, 'Received network error or non-101 status code.')
|
|
108
|
+
failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.')
|
|
101
109
|
return
|
|
102
110
|
}
|
|
103
111
|
|
|
@@ -106,7 +114,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
106
114
|
// header list results in null, failure, or the empty byte
|
|
107
115
|
// sequence, then fail the WebSocket connection.
|
|
108
116
|
if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) {
|
|
109
|
-
failWebsocketConnection(handler, 'Server did not respond with sent protocols.')
|
|
117
|
+
failWebsocketConnection(handler, 1002, 'Server did not respond with sent protocols.')
|
|
110
118
|
return
|
|
111
119
|
}
|
|
112
120
|
|
|
@@ -121,7 +129,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
121
129
|
// insensitive match for the value "websocket", the client MUST
|
|
122
130
|
// _Fail the WebSocket Connection_.
|
|
123
131
|
if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') {
|
|
124
|
-
failWebsocketConnection(handler, 'Server did not set Upgrade header to "websocket".')
|
|
132
|
+
failWebsocketConnection(handler, 1002, 'Server did not set Upgrade header to "websocket".')
|
|
125
133
|
return
|
|
126
134
|
}
|
|
127
135
|
|
|
@@ -130,7 +138,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
130
138
|
// ASCII case-insensitive match for the value "Upgrade", the client
|
|
131
139
|
// MUST _Fail the WebSocket Connection_.
|
|
132
140
|
if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') {
|
|
133
|
-
failWebsocketConnection(handler, 'Server did not set Connection header to "upgrade".')
|
|
141
|
+
failWebsocketConnection(handler, 1002, 'Server did not set Connection header to "upgrade".')
|
|
134
142
|
return
|
|
135
143
|
}
|
|
136
144
|
|
|
@@ -144,7 +152,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
144
152
|
const secWSAccept = response.headersList.get('Sec-WebSocket-Accept')
|
|
145
153
|
const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64')
|
|
146
154
|
if (secWSAccept !== digest) {
|
|
147
|
-
failWebsocketConnection(handler, 'Incorrect hash received in Sec-WebSocket-Accept header.')
|
|
155
|
+
failWebsocketConnection(handler, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.')
|
|
148
156
|
return
|
|
149
157
|
}
|
|
150
158
|
|
|
@@ -162,7 +170,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
162
170
|
extensions = parseExtensions(secExtension)
|
|
163
171
|
|
|
164
172
|
if (!extensions.has('permessage-deflate')) {
|
|
165
|
-
failWebsocketConnection(handler, 'Sec-WebSocket-Extensions header does not match.')
|
|
173
|
+
failWebsocketConnection(handler, 1002, 'Sec-WebSocket-Extensions header does not match.')
|
|
166
174
|
return
|
|
167
175
|
}
|
|
168
176
|
}
|
|
@@ -183,7 +191,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
183
191
|
// the selected subprotocol values in its response for the connection to
|
|
184
192
|
// be established.
|
|
185
193
|
if (!requestProtocols.includes(secProtocol)) {
|
|
186
|
-
failWebsocketConnection(handler, 'Protocol was not set in the opening handshake.')
|
|
194
|
+
failWebsocketConnection(handler, 1002, 'Protocol was not set in the opening handshake.')
|
|
187
195
|
return
|
|
188
196
|
}
|
|
189
197
|
}
|
|
@@ -200,6 +208,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
200
208
|
})
|
|
201
209
|
}
|
|
202
210
|
|
|
211
|
+
handler.wasEverConnected = true
|
|
203
212
|
handler.onConnectionEstablished(response, extensions)
|
|
204
213
|
}
|
|
205
214
|
})
|
|
@@ -208,13 +217,81 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
|
|
|
208
217
|
}
|
|
209
218
|
|
|
210
219
|
/**
|
|
211
|
-
* @
|
|
212
|
-
* @param {
|
|
213
|
-
* @param {
|
|
214
|
-
* @param {
|
|
220
|
+
* @see https://whatpr.org/websockets/48.html#close-the-websocket
|
|
221
|
+
* @param {import('./websocket').Handler} object
|
|
222
|
+
* @param {number} [code=null]
|
|
223
|
+
* @param {string} [reason='']
|
|
215
224
|
*/
|
|
216
|
-
function closeWebSocketConnection (
|
|
217
|
-
|
|
225
|
+
function closeWebSocketConnection (object, code, reason, validate = false) {
|
|
226
|
+
// 1. If code was not supplied, let code be null.
|
|
227
|
+
code ??= null
|
|
228
|
+
|
|
229
|
+
// 2. If reason was not supplied, let reason be the empty string.
|
|
230
|
+
reason ??= ''
|
|
231
|
+
|
|
232
|
+
// 3. Validate close code and reason with code and reason.
|
|
233
|
+
if (validate) validateCloseCodeAndReason(code, reason)
|
|
234
|
+
|
|
235
|
+
// 4. Run the first matching steps from the following list:
|
|
236
|
+
// - If object’s ready state is CLOSING (2) or CLOSED (3)
|
|
237
|
+
// - If the WebSocket connection is not yet established [WSP]
|
|
238
|
+
// - If the WebSocket closing handshake has not yet been started [WSP]
|
|
239
|
+
// - Otherwise
|
|
240
|
+
if (isClosed(object.readyState) || isClosing(object.readyState)) {
|
|
241
|
+
// Do nothing.
|
|
242
|
+
} else if (!isEstablished(object.readyState)) {
|
|
243
|
+
// Fail the WebSocket connection and set object’s ready state to CLOSING (2). [WSP]
|
|
244
|
+
failWebsocketConnection(object)
|
|
245
|
+
object.readyState = states.CLOSING
|
|
246
|
+
} else if (!object.closeState.has(sentCloseFrameState.SENT) && !object.closeState.has(sentCloseFrameState.RECEIVED)) {
|
|
247
|
+
// Upon either sending or receiving a Close control frame, it is said
|
|
248
|
+
// that _The WebSocket Closing Handshake is Started_ and that the
|
|
249
|
+
// WebSocket connection is in the CLOSING state.
|
|
250
|
+
|
|
251
|
+
const frame = new WebsocketFrameSend()
|
|
252
|
+
|
|
253
|
+
// If neither code nor reason is present, the WebSocket Close
|
|
254
|
+
// message must not have a body.
|
|
255
|
+
|
|
256
|
+
// If code is present, then the status code to use in the
|
|
257
|
+
// WebSocket Close message must be the integer given by code.
|
|
258
|
+
// If code is null and reason is the empty string, the WebSocket Close frame must not have a body.
|
|
259
|
+
// If reason is non-empty but code is null, then set code to 1000 ("Normal Closure").
|
|
260
|
+
if (reason.length !== 0 && code === null) {
|
|
261
|
+
code = 1000
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// If code is set, then the status code to use in the WebSocket Close frame must be the integer given by code.
|
|
265
|
+
assert(code === null || Number.isInteger(code))
|
|
266
|
+
|
|
267
|
+
if (code === null && reason.length === 0) {
|
|
268
|
+
frame.frameData = emptyBuffer
|
|
269
|
+
} else if (code !== null && reason === null) {
|
|
270
|
+
frame.frameData = Buffer.allocUnsafe(2)
|
|
271
|
+
frame.frameData.writeUInt16BE(code, 0)
|
|
272
|
+
} else if (code !== null && reason !== null) {
|
|
273
|
+
// If reason is also present, then reasonBytes must be
|
|
274
|
+
// provided in the Close message after the status code.
|
|
275
|
+
frame.frameData = Buffer.allocUnsafe(2 + Buffer.byteLength(reason))
|
|
276
|
+
frame.frameData.writeUInt16BE(code, 0)
|
|
277
|
+
// the body MAY contain UTF-8-encoded data with value /reason/
|
|
278
|
+
frame.frameData.write(reason, 2, 'utf-8')
|
|
279
|
+
} else {
|
|
280
|
+
frame.frameData = emptyBuffer
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
object.socket.write(frame.createFrame(opcodes.CLOSE))
|
|
284
|
+
|
|
285
|
+
object.closeState.add(sentCloseFrameState.SENT)
|
|
286
|
+
|
|
287
|
+
// Upon either sending or receiving a Close control frame, it is said
|
|
288
|
+
// that _The WebSocket Closing Handshake is Started_ and that the
|
|
289
|
+
// WebSocket connection is in the CLOSING state.
|
|
290
|
+
object.readyState = states.CLOSING
|
|
291
|
+
} else {
|
|
292
|
+
// Set object’s ready state to CLOSING (2).
|
|
293
|
+
object.readyState = states.CLOSING
|
|
294
|
+
}
|
|
218
295
|
}
|
|
219
296
|
|
|
220
297
|
module.exports = {
|
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* This is a Globally Unique Identifier unique used to validate that the
|
|
5
|
+
* endpoint accepts websocket connections.
|
|
6
|
+
* @see https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3
|
|
7
|
+
* @type {'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'}
|
|
8
|
+
*/
|
|
7
9
|
const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
8
10
|
|
|
9
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* @type {PropertyDescriptor}
|
|
13
|
+
*/
|
|
10
14
|
const staticPropertyDescriptors = {
|
|
11
15
|
enumerable: true,
|
|
12
16
|
writable: false,
|
|
13
17
|
configurable: false
|
|
14
18
|
}
|
|
15
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The states of the WebSocket connection.
|
|
22
|
+
*
|
|
23
|
+
* @readonly
|
|
24
|
+
* @enum
|
|
25
|
+
* @property {0} CONNECTING
|
|
26
|
+
* @property {1} OPEN
|
|
27
|
+
* @property {2} CLOSING
|
|
28
|
+
* @property {3} CLOSED
|
|
29
|
+
*/
|
|
16
30
|
const states = {
|
|
17
31
|
CONNECTING: 0,
|
|
18
32
|
OPEN: 1,
|
|
@@ -20,12 +34,31 @@ const states = {
|
|
|
20
34
|
CLOSED: 3
|
|
21
35
|
}
|
|
22
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @readonly
|
|
39
|
+
* @enum
|
|
40
|
+
* @property {0} NOT_SENT
|
|
41
|
+
* @property {1} PROCESSING
|
|
42
|
+
* @property {2} SENT
|
|
43
|
+
*/
|
|
23
44
|
const sentCloseFrameState = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
SENT: 2
|
|
45
|
+
SENT: 1,
|
|
46
|
+
RECEIVED: 2
|
|
27
47
|
}
|
|
28
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The WebSocket opcodes.
|
|
51
|
+
*
|
|
52
|
+
* @readonly
|
|
53
|
+
* @enum
|
|
54
|
+
* @property {0x0} CONTINUATION
|
|
55
|
+
* @property {0x1} TEXT
|
|
56
|
+
* @property {0x2} BINARY
|
|
57
|
+
* @property {0x8} CLOSE
|
|
58
|
+
* @property {0x9} PING
|
|
59
|
+
* @property {0xA} PONG
|
|
60
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
|
|
61
|
+
*/
|
|
29
62
|
const opcodes = {
|
|
30
63
|
CONTINUATION: 0x0,
|
|
31
64
|
TEXT: 0x1,
|
|
@@ -35,8 +68,23 @@ const opcodes = {
|
|
|
35
68
|
PONG: 0xA
|
|
36
69
|
}
|
|
37
70
|
|
|
38
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The maximum value for an unsigned 16-bit integer.
|
|
73
|
+
*
|
|
74
|
+
* @type {65535} 2 ** 16 - 1
|
|
75
|
+
*/
|
|
76
|
+
const maxUnsigned16Bit = 65535
|
|
39
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The states of the parser.
|
|
80
|
+
*
|
|
81
|
+
* @readonly
|
|
82
|
+
* @enum
|
|
83
|
+
* @property {0} INFO
|
|
84
|
+
* @property {2} PAYLOADLENGTH_16
|
|
85
|
+
* @property {3} PAYLOADLENGTH_64
|
|
86
|
+
* @property {4} READ_DATA
|
|
87
|
+
*/
|
|
40
88
|
const parserStates = {
|
|
41
89
|
INFO: 0,
|
|
42
90
|
PAYLOADLENGTH_16: 2,
|
|
@@ -44,8 +92,20 @@ const parserStates = {
|
|
|
44
92
|
READ_DATA: 4
|
|
45
93
|
}
|
|
46
94
|
|
|
95
|
+
/**
|
|
96
|
+
* An empty buffer.
|
|
97
|
+
*
|
|
98
|
+
* @type {Buffer}
|
|
99
|
+
*/
|
|
47
100
|
const emptyBuffer = Buffer.allocUnsafe(0)
|
|
48
101
|
|
|
102
|
+
/**
|
|
103
|
+
* @readonly
|
|
104
|
+
* @property {1} text
|
|
105
|
+
* @property {2} typedArray
|
|
106
|
+
* @property {3} arrayBuffer
|
|
107
|
+
* @property {4} blob
|
|
108
|
+
*/
|
|
49
109
|
const sendHints = {
|
|
50
110
|
text: 1,
|
|
51
111
|
typedArray: 2,
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const { webidl } = require('../fetch/webidl')
|
|
4
4
|
const { kEnumerableProperty } = require('../../core/util')
|
|
5
5
|
const { kConstruct } = require('../../core/symbols')
|
|
6
|
-
const { MessagePort } = require('node:worker_threads')
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* @see https://html.spec.whatwg.org/multipage/comms.html#messageevent
|
|
@@ -14,6 +13,7 @@ class MessageEvent extends Event {
|
|
|
14
13
|
constructor (type, eventInitDict = {}) {
|
|
15
14
|
if (type === kConstruct) {
|
|
16
15
|
super(arguments[1], arguments[2])
|
|
16
|
+
webidl.util.markAsUncloneable(this)
|
|
17
17
|
return
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -26,6 +26,7 @@ class MessageEvent extends Event {
|
|
|
26
26
|
super(type, eventInitDict)
|
|
27
27
|
|
|
28
28
|
this.#eventInit = eventInitDict
|
|
29
|
+
webidl.util.markAsUncloneable(this)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
get data () {
|
|
@@ -112,6 +113,7 @@ class CloseEvent extends Event {
|
|
|
112
113
|
super(type, eventInitDict)
|
|
113
114
|
|
|
114
115
|
this.#eventInit = eventInitDict
|
|
116
|
+
webidl.util.markAsUncloneable(this)
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
get wasClean () {
|
|
@@ -142,6 +144,7 @@ class ErrorEvent extends Event {
|
|
|
142
144
|
webidl.argumentLengthCheck(arguments, 1, prefix)
|
|
143
145
|
|
|
144
146
|
super(type, eventInitDict)
|
|
147
|
+
webidl.util.markAsUncloneable(this)
|
|
145
148
|
|
|
146
149
|
type = webidl.converters.DOMString(type, prefix, 'type')
|
|
147
150
|
eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {})
|
|
@@ -215,7 +218,10 @@ Object.defineProperties(ErrorEvent.prototype, {
|
|
|
215
218
|
error: kEnumerableProperty
|
|
216
219
|
})
|
|
217
220
|
|
|
218
|
-
webidl.converters.MessagePort = webidl.interfaceConverter(
|
|
221
|
+
webidl.converters.MessagePort = webidl.interfaceConverter(
|
|
222
|
+
webidl.is.MessagePort,
|
|
223
|
+
'MessagePort'
|
|
224
|
+
)
|
|
219
225
|
|
|
220
226
|
webidl.converters['sequence<MessagePort>'] = webidl.sequenceConverter(
|
|
221
227
|
webidl.converters.MessagePort
|
|
@@ -15,7 +15,6 @@ const {
|
|
|
15
15
|
isContinuationFrame
|
|
16
16
|
} = require('./util')
|
|
17
17
|
const { WebsocketFrameSend } = require('./frame')
|
|
18
|
-
const { closeWebSocketConnection } = require('./connection')
|
|
19
18
|
const { PerMessageDeflate } = require('./permessage-deflate')
|
|
20
19
|
|
|
21
20
|
// This code was influenced by ws released under the MIT license.
|
|
@@ -88,12 +87,12 @@ class ByteParser extends Writable {
|
|
|
88
87
|
const rsv3 = buffer[0] & 0x10
|
|
89
88
|
|
|
90
89
|
if (!isValidOpcode(opcode)) {
|
|
91
|
-
failWebsocketConnection(this.#handler, 'Invalid opcode received')
|
|
90
|
+
failWebsocketConnection(this.#handler, 1002, 'Invalid opcode received')
|
|
92
91
|
return callback()
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
if (masked) {
|
|
96
|
-
failWebsocketConnection(this.#handler, 'Frame cannot be masked')
|
|
95
|
+
failWebsocketConnection(this.#handler, 1002, 'Frame cannot be masked')
|
|
97
96
|
return callback()
|
|
98
97
|
}
|
|
99
98
|
|
|
@@ -107,43 +106,43 @@ class ByteParser extends Writable {
|
|
|
107
106
|
// WebSocket connection where a PMCE is in use, this bit indicates
|
|
108
107
|
// whether a message is compressed or not.
|
|
109
108
|
if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) {
|
|
110
|
-
failWebsocketConnection(this.#handler, 'Expected RSV1 to be clear.')
|
|
109
|
+
failWebsocketConnection(this.#handler, 1002, 'Expected RSV1 to be clear.')
|
|
111
110
|
return
|
|
112
111
|
}
|
|
113
112
|
|
|
114
113
|
if (rsv2 !== 0 || rsv3 !== 0) {
|
|
115
|
-
failWebsocketConnection(this.#handler, 'RSV1, RSV2, RSV3 must be clear')
|
|
114
|
+
failWebsocketConnection(this.#handler, 1002, 'RSV1, RSV2, RSV3 must be clear')
|
|
116
115
|
return
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
if (fragmented && !isTextBinaryFrame(opcode)) {
|
|
120
119
|
// Only text and binary frames can be fragmented
|
|
121
|
-
failWebsocketConnection(this.#handler, 'Invalid frame type was fragmented.')
|
|
120
|
+
failWebsocketConnection(this.#handler, 1002, 'Invalid frame type was fragmented.')
|
|
122
121
|
return
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
// If we are already parsing a text/binary frame and do not receive either
|
|
126
125
|
// a continuation frame or close frame, fail the connection.
|
|
127
126
|
if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) {
|
|
128
|
-
failWebsocketConnection(this.#handler, 'Expected continuation frame')
|
|
127
|
+
failWebsocketConnection(this.#handler, 1002, 'Expected continuation frame')
|
|
129
128
|
return
|
|
130
129
|
}
|
|
131
130
|
|
|
132
131
|
if (this.#info.fragmented && fragmented) {
|
|
133
132
|
// A fragmented frame can't be fragmented itself
|
|
134
|
-
failWebsocketConnection(this.#handler, 'Fragmented frame exceeded 125 bytes.')
|
|
133
|
+
failWebsocketConnection(this.#handler, 1002, 'Fragmented frame exceeded 125 bytes.')
|
|
135
134
|
return
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
// "All control frames MUST have a payload length of 125 bytes or less
|
|
139
138
|
// and MUST NOT be fragmented."
|
|
140
139
|
if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) {
|
|
141
|
-
failWebsocketConnection(this.#handler, 'Control frame either too large or fragmented')
|
|
140
|
+
failWebsocketConnection(this.#handler, 1002, 'Control frame either too large or fragmented')
|
|
142
141
|
return
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) {
|
|
146
|
-
failWebsocketConnection(this.#handler, 'Unexpected continuation frame')
|
|
145
|
+
failWebsocketConnection(this.#handler, 1002, 'Unexpected continuation frame')
|
|
147
146
|
return
|
|
148
147
|
}
|
|
149
148
|
|
|
@@ -189,7 +188,7 @@ class ByteParser extends Writable {
|
|
|
189
188
|
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
|
|
190
189
|
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
|
|
191
190
|
if (upper > 2 ** 31 - 1) {
|
|
192
|
-
failWebsocketConnection(this.#handler, 'Received payload length > 2^31 bytes.')
|
|
191
|
+
failWebsocketConnection(this.#handler, 1009, 'Received payload length > 2^31 bytes.')
|
|
193
192
|
return
|
|
194
193
|
}
|
|
195
194
|
|
|
@@ -225,7 +224,7 @@ class ByteParser extends Writable {
|
|
|
225
224
|
} else {
|
|
226
225
|
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
|
|
227
226
|
if (error) {
|
|
228
|
-
|
|
227
|
+
failWebsocketConnection(this.#handler, 1007, error.message)
|
|
229
228
|
return
|
|
230
229
|
}
|
|
231
230
|
|
|
@@ -341,7 +340,7 @@ class ByteParser extends Writable {
|
|
|
341
340
|
|
|
342
341
|
if (opcode === opcodes.CLOSE) {
|
|
343
342
|
if (payloadLength === 1) {
|
|
344
|
-
failWebsocketConnection(this.#handler, 'Received close frame with a 1-byte body.')
|
|
343
|
+
failWebsocketConnection(this.#handler, 1002, 'Received close frame with a 1-byte body.')
|
|
345
344
|
return false
|
|
346
345
|
}
|
|
347
346
|
|
|
@@ -350,12 +349,13 @@ class ByteParser extends Writable {
|
|
|
350
349
|
if (this.#info.closeInfo.error) {
|
|
351
350
|
const { code, reason } = this.#info.closeInfo
|
|
352
351
|
|
|
353
|
-
|
|
354
|
-
failWebsocketConnection(this.#handler, reason)
|
|
352
|
+
failWebsocketConnection(this.#handler, code, reason)
|
|
355
353
|
return false
|
|
356
354
|
}
|
|
357
355
|
|
|
358
|
-
|
|
356
|
+
// Upon receiving such a frame, the other peer sends a
|
|
357
|
+
// Close frame in response, if it hasn't already sent one.
|
|
358
|
+
if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
|
|
359
359
|
// If an endpoint receives a Close frame and did not previously send a
|
|
360
360
|
// Close frame, the endpoint MUST send a Close frame in response. (When
|
|
361
361
|
// sending a Close frame in response, the endpoint typically echos the
|
|
@@ -367,21 +367,15 @@ class ByteParser extends Writable {
|
|
|
367
367
|
}
|
|
368
368
|
const closeFrame = new WebsocketFrameSend(body)
|
|
369
369
|
|
|
370
|
-
this.#handler.socket.write(
|
|
371
|
-
|
|
372
|
-
(err) => {
|
|
373
|
-
if (!err) {
|
|
374
|
-
this.#handler.closeState = sentCloseFrameState.SENT
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
)
|
|
370
|
+
this.#handler.socket.write(closeFrame.createFrame(opcodes.CLOSE))
|
|
371
|
+
this.#handler.closeState.add(sentCloseFrameState.SENT)
|
|
378
372
|
}
|
|
379
373
|
|
|
380
374
|
// Upon either sending or receiving a Close control frame, it is said
|
|
381
375
|
// that _The WebSocket Closing Handshake is Started_ and that the
|
|
382
376
|
// WebSocket connection is in the CLOSING state.
|
|
383
377
|
this.#handler.readyState = states.CLOSING
|
|
384
|
-
this.#handler.
|
|
378
|
+
this.#handler.closeState.add(sentCloseFrameState.RECEIVED)
|
|
385
379
|
|
|
386
380
|
return false
|
|
387
381
|
} else if (opcode === opcodes.PING) {
|
|
@@ -390,7 +384,7 @@ class ByteParser extends Writable {
|
|
|
390
384
|
// A Pong frame sent in response to a Ping frame must have identical
|
|
391
385
|
// "Application data"
|
|
392
386
|
|
|
393
|
-
if (!this.#handler.
|
|
387
|
+
if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
|
|
394
388
|
const frame = new WebsocketFrameSend(body)
|
|
395
389
|
|
|
396
390
|
this.#handler.socket.write(frame.createFrame(opcodes.PONG))
|