undici 6.16.0 → 6.17.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/Dispatcher.md +32 -0
- package/index-fetch.js +1 -1
- package/index.js +3 -2
- package/lib/api/api-request.js +10 -1
- package/lib/core/connect.js +1 -1
- package/lib/core/symbols.js +0 -1
- package/lib/dispatcher/client-h1.js +1 -1
- package/lib/interceptor/dump.js +123 -0
- package/lib/web/cookies/util.js +5 -3
- package/lib/web/fetch/body.js +9 -0
- package/lib/web/fetch/headers.js +59 -31
- package/lib/web/fetch/index.js +1 -2
- package/lib/web/fetch/request.js +11 -11
- package/lib/web/fetch/response.js +8 -8
- package/lib/web/fetch/symbols.js +0 -1
- package/lib/web/fetch/util.js +20 -12
- package/lib/web/websocket/connection.js +6 -10
- package/lib/web/websocket/receiver.js +177 -146
- package/lib/web/websocket/util.js +29 -1
- package/lib/web/websocket/websocket.js +17 -5
- package/package.json +4 -2
- package/types/dispatcher.d.ts +3 -3
- package/types/index.d.ts +5 -0
- package/types/interceptors.d.ts +8 -2
package/lib/web/fetch/util.js
CHANGED
|
@@ -255,16 +255,23 @@ function appendFetchMetadata (httpRequest) {
|
|
|
255
255
|
|
|
256
256
|
// https://fetch.spec.whatwg.org/#append-a-request-origin-header
|
|
257
257
|
function appendRequestOriginHeader (request) {
|
|
258
|
-
// 1. Let serializedOrigin be the result of byte-serializing a request origin
|
|
258
|
+
// 1. Let serializedOrigin be the result of byte-serializing a request origin
|
|
259
|
+
// with request.
|
|
260
|
+
// TODO: implement "byte-serializing a request origin"
|
|
259
261
|
let serializedOrigin = request.origin
|
|
260
262
|
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
// "'client' is changed to an origin during fetching."
|
|
264
|
+
// This doesn't happen in undici (in most cases) because undici, by default,
|
|
265
|
+
// has no concept of origin.
|
|
266
|
+
if (serializedOrigin === 'client') {
|
|
267
|
+
return
|
|
268
|
+
}
|
|
266
269
|
|
|
270
|
+
// 2. If request’s response tainting is "cors" or request’s mode is "websocket",
|
|
271
|
+
// then append (`Origin`, serializedOrigin) to request’s header list.
|
|
267
272
|
// 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
|
|
273
|
+
if (request.responseTainting === 'cors' || request.mode === 'websocket') {
|
|
274
|
+
request.headersList.append('origin', serializedOrigin, true)
|
|
268
275
|
} else if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
269
276
|
// 1. Switch on request’s referrer policy:
|
|
270
277
|
switch (request.referrerPolicy) {
|
|
@@ -275,13 +282,16 @@ function appendRequestOriginHeader (request) {
|
|
|
275
282
|
case 'no-referrer-when-downgrade':
|
|
276
283
|
case 'strict-origin':
|
|
277
284
|
case 'strict-origin-when-cross-origin':
|
|
278
|
-
// If request’s origin is a tuple origin, its scheme is "https", and
|
|
285
|
+
// If request’s origin is a tuple origin, its scheme is "https", and
|
|
286
|
+
// request’s current URL’s scheme is not "https", then set
|
|
287
|
+
// serializedOrigin to `null`.
|
|
279
288
|
if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) {
|
|
280
289
|
serializedOrigin = null
|
|
281
290
|
}
|
|
282
291
|
break
|
|
283
292
|
case 'same-origin':
|
|
284
|
-
// If request’s origin is not same origin with request’s current URL’s
|
|
293
|
+
// If request’s origin is not same origin with request’s current URL’s
|
|
294
|
+
// origin, then set serializedOrigin to `null`.
|
|
285
295
|
if (!sameOrigin(request, requestCurrentURL(request))) {
|
|
286
296
|
serializedOrigin = null
|
|
287
297
|
}
|
|
@@ -290,10 +300,8 @@ function appendRequestOriginHeader (request) {
|
|
|
290
300
|
// Do nothing.
|
|
291
301
|
}
|
|
292
302
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
request.headersList.append('origin', serializedOrigin, true)
|
|
296
|
-
}
|
|
303
|
+
// 2. Append (`Origin`, serializedOrigin) to request’s header list.
|
|
304
|
+
request.headersList.append('origin', serializedOrigin, true)
|
|
297
305
|
}
|
|
298
306
|
}
|
|
299
307
|
|
|
@@ -13,9 +13,8 @@ const { channels } = require('../../core/diagnostics')
|
|
|
13
13
|
const { CloseEvent } = require('./events')
|
|
14
14
|
const { makeRequest } = require('../fetch/request')
|
|
15
15
|
const { fetching } = require('../fetch/index')
|
|
16
|
-
const { Headers } = require('../fetch/headers')
|
|
16
|
+
const { Headers, getHeadersList } = require('../fetch/headers')
|
|
17
17
|
const { getDecodeSplit } = require('../fetch/util')
|
|
18
|
-
const { kHeadersList } = require('../../core/symbols')
|
|
19
18
|
const { WebsocketFrameSend } = require('./frame')
|
|
20
19
|
|
|
21
20
|
/** @type {import('crypto')} */
|
|
@@ -35,7 +34,7 @@ try {
|
|
|
35
34
|
* @param {(response: any) => void} onEstablish
|
|
36
35
|
* @param {Partial<import('../../types/websocket').WebSocketInit>} options
|
|
37
36
|
*/
|
|
38
|
-
function establishWebSocketConnection (url, protocols, ws, onEstablish, options) {
|
|
37
|
+
function establishWebSocketConnection (url, protocols, client, ws, onEstablish, options) {
|
|
39
38
|
// 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s
|
|
40
39
|
// scheme is "ws", and to "https" otherwise.
|
|
41
40
|
const requestURL = url
|
|
@@ -48,6 +47,7 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish, options)
|
|
|
48
47
|
// and redirect mode is "error".
|
|
49
48
|
const request = makeRequest({
|
|
50
49
|
urlList: [requestURL],
|
|
50
|
+
client,
|
|
51
51
|
serviceWorkers: 'none',
|
|
52
52
|
referrer: 'no-referrer',
|
|
53
53
|
mode: 'websocket',
|
|
@@ -58,7 +58,7 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish, options)
|
|
|
58
58
|
|
|
59
59
|
// Note: undici extension, allow setting custom headers.
|
|
60
60
|
if (options.headers) {
|
|
61
|
-
const headersList = new Headers(options.headers)
|
|
61
|
+
const headersList = getHeadersList(new Headers(options.headers))
|
|
62
62
|
|
|
63
63
|
request.headersList = headersList
|
|
64
64
|
}
|
|
@@ -260,13 +260,9 @@ function closeWebSocketConnection (ws, code, reason, reasonByteLength) {
|
|
|
260
260
|
/** @type {import('stream').Duplex} */
|
|
261
261
|
const socket = ws[kResponse].socket
|
|
262
262
|
|
|
263
|
-
socket.write(frame.createFrame(opcodes.CLOSE)
|
|
264
|
-
if (!err) {
|
|
265
|
-
ws[kSentClose] = sentCloseFrameState.SENT
|
|
266
|
-
}
|
|
267
|
-
})
|
|
263
|
+
socket.write(frame.createFrame(opcodes.CLOSE))
|
|
268
264
|
|
|
269
|
-
ws[kSentClose] = sentCloseFrameState.
|
|
265
|
+
ws[kSentClose] = sentCloseFrameState.SENT
|
|
270
266
|
|
|
271
267
|
// Upon either sending or receiving a Close control frame, it is said
|
|
272
268
|
// that _The WebSocket Closing Handshake is Started_ and that the
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Writable } = require('node:stream')
|
|
4
|
+
const assert = require('node:assert')
|
|
4
5
|
const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
|
|
5
6
|
const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols')
|
|
6
7
|
const { channels } = require('../../core/diagnostics')
|
|
7
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
isValidStatusCode,
|
|
10
|
+
isValidOpcode,
|
|
11
|
+
failWebsocketConnection,
|
|
12
|
+
websocketMessageReceived,
|
|
13
|
+
utf8Decode,
|
|
14
|
+
isControlFrame,
|
|
15
|
+
isTextBinaryFrame,
|
|
16
|
+
isContinuationFrame
|
|
17
|
+
} = require('./util')
|
|
8
18
|
const { WebsocketFrameSend } = require('./frame')
|
|
9
|
-
const {
|
|
19
|
+
const { closeWebSocketConnection } = require('./connection')
|
|
10
20
|
|
|
11
21
|
// This code was influenced by ws released under the MIT license.
|
|
12
22
|
// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
|
@@ -16,6 +26,7 @@ const { CloseEvent } = require('./events')
|
|
|
16
26
|
class ByteParser extends Writable {
|
|
17
27
|
#buffers = []
|
|
18
28
|
#byteOffset = 0
|
|
29
|
+
#loop = false
|
|
19
30
|
|
|
20
31
|
#state = parserStates.INFO
|
|
21
32
|
|
|
@@ -35,6 +46,7 @@ class ByteParser extends Writable {
|
|
|
35
46
|
_write (chunk, _, callback) {
|
|
36
47
|
this.#buffers.push(chunk)
|
|
37
48
|
this.#byteOffset += chunk.length
|
|
49
|
+
this.#loop = true
|
|
38
50
|
|
|
39
51
|
this.run(callback)
|
|
40
52
|
}
|
|
@@ -45,7 +57,7 @@ class ByteParser extends Writable {
|
|
|
45
57
|
* or not enough bytes are buffered to parse.
|
|
46
58
|
*/
|
|
47
59
|
run (callback) {
|
|
48
|
-
while (
|
|
60
|
+
while (this.#loop) {
|
|
49
61
|
if (this.#state === parserStates.INFO) {
|
|
50
62
|
// If there aren't enough bytes to parse the payload length, etc.
|
|
51
63
|
if (this.#byteOffset < 2) {
|
|
@@ -53,148 +65,85 @@ class ByteParser extends Writable {
|
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
const buffer = this.consume(2)
|
|
68
|
+
const fin = (buffer[0] & 0x80) !== 0
|
|
69
|
+
const opcode = buffer[0] & 0x0F
|
|
70
|
+
const masked = (buffer[1] & 0x80) === 0x80
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
this.#info.masked = (buffer[1] & 0x80) === 0x80
|
|
72
|
+
const fragmented = !fin && opcode !== opcodes.CONTINUATION
|
|
73
|
+
const payloadLength = buffer[1] & 0x7F
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
const rsv1 = buffer[0] & 0x40
|
|
76
|
+
const rsv2 = buffer[0] & 0x20
|
|
77
|
+
const rsv3 = buffer[0] & 0x10
|
|
78
|
+
|
|
79
|
+
if (!isValidOpcode(opcode)) {
|
|
80
|
+
failWebsocketConnection(this.ws, 'Invalid opcode received')
|
|
63
81
|
return callback()
|
|
64
82
|
}
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
if (masked) {
|
|
85
|
+
failWebsocketConnection(this.ws, 'Frame cannot be masked')
|
|
86
|
+
return callback()
|
|
87
|
+
}
|
|
69
88
|
|
|
70
|
-
|
|
89
|
+
// MUST be 0 unless an extension is negotiated that defines meanings
|
|
90
|
+
// for non-zero values. If a nonzero value is received and none of
|
|
91
|
+
// the negotiated extensions defines the meaning of such a nonzero
|
|
92
|
+
// value, the receiving endpoint MUST _Fail the WebSocket
|
|
93
|
+
// Connection_.
|
|
94
|
+
if (rsv1 !== 0 || rsv2 !== 0 || rsv3 !== 0) {
|
|
95
|
+
failWebsocketConnection(this.ws, 'RSV1, RSV2, RSV3 must be clear')
|
|
96
|
+
return
|
|
97
|
+
}
|
|
71
98
|
|
|
72
|
-
if (
|
|
99
|
+
if (fragmented && !isTextBinaryFrame(opcode)) {
|
|
73
100
|
// Only text and binary frames can be fragmented
|
|
74
101
|
failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.')
|
|
75
102
|
return
|
|
76
103
|
}
|
|
77
104
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
this
|
|
82
|
-
|
|
83
|
-
} else if (payloadLength === 126) {
|
|
84
|
-
this.#state = parserStates.PAYLOADLENGTH_16
|
|
85
|
-
} else if (payloadLength === 127) {
|
|
86
|
-
this.#state = parserStates.PAYLOADLENGTH_64
|
|
105
|
+
// If we are already parsing a text/binary frame and do not receive either
|
|
106
|
+
// a continuation frame or close frame, fail the connection.
|
|
107
|
+
if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) {
|
|
108
|
+
failWebsocketConnection(this.ws, 'Expected continuation frame')
|
|
109
|
+
return
|
|
87
110
|
}
|
|
88
111
|
|
|
89
|
-
if (this.#info.fragmented &&
|
|
112
|
+
if (this.#info.fragmented && fragmented) {
|
|
90
113
|
// A fragmented frame can't be fragmented itself
|
|
91
114
|
failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.')
|
|
92
115
|
return
|
|
93
|
-
}
|
|
94
|
-
(this.#info.opcode === opcodes.PING ||
|
|
95
|
-
this.#info.opcode === opcodes.PONG ||
|
|
96
|
-
this.#info.opcode === opcodes.CLOSE) &&
|
|
97
|
-
payloadLength > 125
|
|
98
|
-
) {
|
|
99
|
-
// Control frames can have a payload length of 125 bytes MAX
|
|
100
|
-
failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.')
|
|
101
|
-
return
|
|
102
|
-
} else if (this.#info.opcode === opcodes.CLOSE) {
|
|
103
|
-
if (payloadLength === 1) {
|
|
104
|
-
failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.')
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const body = this.consume(payloadLength)
|
|
109
|
-
|
|
110
|
-
this.#info.closeInfo = this.parseCloseBody(body)
|
|
111
|
-
|
|
112
|
-
if (this.#info.closeInfo.error) {
|
|
113
|
-
const { code, reason } = this.#info.closeInfo
|
|
114
|
-
|
|
115
|
-
callback(new CloseEvent('close', { wasClean: false, reason, code }))
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (this.ws[kSentClose] !== sentCloseFrameState.SENT) {
|
|
120
|
-
// If an endpoint receives a Close frame and did not previously send a
|
|
121
|
-
// Close frame, the endpoint MUST send a Close frame in response. (When
|
|
122
|
-
// sending a Close frame in response, the endpoint typically echos the
|
|
123
|
-
// status code it received.)
|
|
124
|
-
let body = emptyBuffer
|
|
125
|
-
if (this.#info.closeInfo.code) {
|
|
126
|
-
body = Buffer.allocUnsafe(2)
|
|
127
|
-
body.writeUInt16BE(this.#info.closeInfo.code, 0)
|
|
128
|
-
}
|
|
129
|
-
const closeFrame = new WebsocketFrameSend(body)
|
|
130
|
-
|
|
131
|
-
this.ws[kResponse].socket.write(
|
|
132
|
-
closeFrame.createFrame(opcodes.CLOSE),
|
|
133
|
-
(err) => {
|
|
134
|
-
if (!err) {
|
|
135
|
-
this.ws[kSentClose] = sentCloseFrameState.SENT
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Upon either sending or receiving a Close control frame, it is said
|
|
142
|
-
// that _The WebSocket Closing Handshake is Started_ and that the
|
|
143
|
-
// WebSocket connection is in the CLOSING state.
|
|
144
|
-
this.ws[kReadyState] = states.CLOSING
|
|
145
|
-
this.ws[kReceivedClose] = true
|
|
146
|
-
|
|
147
|
-
this.end()
|
|
116
|
+
}
|
|
148
117
|
|
|
118
|
+
// "All control frames MUST have a payload length of 125 bytes or less
|
|
119
|
+
// and MUST NOT be fragmented."
|
|
120
|
+
if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) {
|
|
121
|
+
failWebsocketConnection(this.ws, 'Control frame either too large or fragmented')
|
|
149
122
|
return
|
|
150
|
-
}
|
|
151
|
-
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
|
|
152
|
-
// response, unless it already received a Close frame.
|
|
153
|
-
// A Pong frame sent in response to a Ping frame must have identical
|
|
154
|
-
// "Application data"
|
|
155
|
-
|
|
156
|
-
const body = this.consume(payloadLength)
|
|
157
|
-
|
|
158
|
-
if (!this.ws[kReceivedClose]) {
|
|
159
|
-
const frame = new WebsocketFrameSend(body)
|
|
160
|
-
|
|
161
|
-
this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG))
|
|
162
|
-
|
|
163
|
-
if (channels.ping.hasSubscribers) {
|
|
164
|
-
channels.ping.publish({
|
|
165
|
-
payload: body
|
|
166
|
-
})
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
this.#state = parserStates.INFO
|
|
171
|
-
|
|
172
|
-
if (this.#byteOffset > 0) {
|
|
173
|
-
continue
|
|
174
|
-
} else {
|
|
175
|
-
callback()
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
} else if (this.#info.opcode === opcodes.PONG) {
|
|
179
|
-
// A Pong frame MAY be sent unsolicited. This serves as a
|
|
180
|
-
// unidirectional heartbeat. A response to an unsolicited Pong frame is
|
|
181
|
-
// not expected.
|
|
123
|
+
}
|
|
182
124
|
|
|
183
|
-
|
|
125
|
+
if (isContinuationFrame(opcode) && this.#fragments.length === 0) {
|
|
126
|
+
failWebsocketConnection(this.ws, 'Unexpected continuation frame')
|
|
127
|
+
return
|
|
128
|
+
}
|
|
184
129
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
130
|
+
if (payloadLength <= 125) {
|
|
131
|
+
this.#info.payloadLength = payloadLength
|
|
132
|
+
this.#state = parserStates.READ_DATA
|
|
133
|
+
} else if (payloadLength === 126) {
|
|
134
|
+
this.#state = parserStates.PAYLOADLENGTH_16
|
|
135
|
+
} else if (payloadLength === 127) {
|
|
136
|
+
this.#state = parserStates.PAYLOADLENGTH_64
|
|
137
|
+
}
|
|
190
138
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
} else {
|
|
194
|
-
callback()
|
|
195
|
-
return
|
|
196
|
-
}
|
|
139
|
+
if (isTextBinaryFrame(opcode)) {
|
|
140
|
+
this.#info.binaryType = opcode
|
|
197
141
|
}
|
|
142
|
+
|
|
143
|
+
this.#info.opcode = opcode
|
|
144
|
+
this.#info.masked = masked
|
|
145
|
+
this.#info.fin = fin
|
|
146
|
+
this.#info.fragmented = fragmented
|
|
198
147
|
} else if (this.#state === parserStates.PAYLOADLENGTH_16) {
|
|
199
148
|
if (this.#byteOffset < 2) {
|
|
200
149
|
return callback()
|
|
@@ -212,7 +161,7 @@ class ByteParser extends Writable {
|
|
|
212
161
|
const buffer = this.consume(8)
|
|
213
162
|
const upper = buffer.readUInt32BE(0)
|
|
214
163
|
|
|
215
|
-
// 2^31 is the
|
|
164
|
+
// 2^31 is the maximum bytes an arraybuffer can contain
|
|
216
165
|
// on 32-bit systems. Although, on 64-bit systems, this is
|
|
217
166
|
// 2^53-1 bytes.
|
|
218
167
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
|
|
@@ -229,33 +178,28 @@ class ByteParser extends Writable {
|
|
|
229
178
|
this.#state = parserStates.READ_DATA
|
|
230
179
|
} else if (this.#state === parserStates.READ_DATA) {
|
|
231
180
|
if (this.#byteOffset < this.#info.payloadLength) {
|
|
232
|
-
// If there is still more data in this chunk that needs to be read
|
|
233
181
|
return callback()
|
|
234
|
-
}
|
|
235
|
-
// If the server sent multiple frames in a single chunk
|
|
182
|
+
}
|
|
236
183
|
|
|
237
|
-
|
|
184
|
+
const body = this.consume(this.#info.payloadLength)
|
|
238
185
|
|
|
186
|
+
if (isControlFrame(this.#info.opcode)) {
|
|
187
|
+
this.#loop = this.parseControlFrame(body)
|
|
188
|
+
} else {
|
|
239
189
|
this.#fragments.push(body)
|
|
240
190
|
|
|
241
|
-
// If the frame is
|
|
242
|
-
// a
|
|
243
|
-
|
|
191
|
+
// If the frame is not fragmented, a message has been received.
|
|
192
|
+
// If the frame is fragmented, it will terminate with a fin bit set
|
|
193
|
+
// and an opcode of 0 (continuation), therefore we handle that when
|
|
194
|
+
// parsing continuation frames, not here.
|
|
195
|
+
if (!this.#info.fragmented && this.#info.fin) {
|
|
244
196
|
const fullMessage = Buffer.concat(this.#fragments)
|
|
245
|
-
|
|
246
|
-
websocketMessageReceived(this.ws, this.#info.originalOpcode, fullMessage)
|
|
247
|
-
|
|
248
|
-
this.#info = {}
|
|
197
|
+
websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
|
|
249
198
|
this.#fragments.length = 0
|
|
250
199
|
}
|
|
251
|
-
|
|
252
|
-
this.#state = parserStates.INFO
|
|
253
200
|
}
|
|
254
|
-
}
|
|
255
201
|
|
|
256
|
-
|
|
257
|
-
callback()
|
|
258
|
-
break
|
|
202
|
+
this.#state = parserStates.INFO
|
|
259
203
|
}
|
|
260
204
|
}
|
|
261
205
|
}
|
|
@@ -263,11 +207,11 @@ class ByteParser extends Writable {
|
|
|
263
207
|
/**
|
|
264
208
|
* Take n bytes from the buffered Buffers
|
|
265
209
|
* @param {number} n
|
|
266
|
-
* @returns {Buffer
|
|
210
|
+
* @returns {Buffer}
|
|
267
211
|
*/
|
|
268
212
|
consume (n) {
|
|
269
213
|
if (n > this.#byteOffset) {
|
|
270
|
-
|
|
214
|
+
throw new Error('Called consume() before buffers satiated.')
|
|
271
215
|
} else if (n === 0) {
|
|
272
216
|
return emptyBuffer
|
|
273
217
|
}
|
|
@@ -303,6 +247,8 @@ class ByteParser extends Writable {
|
|
|
303
247
|
}
|
|
304
248
|
|
|
305
249
|
parseCloseBody (data) {
|
|
250
|
+
assert(data.length !== 1)
|
|
251
|
+
|
|
306
252
|
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
|
|
307
253
|
/** @type {number|undefined} */
|
|
308
254
|
let code
|
|
@@ -314,6 +260,10 @@ class ByteParser extends Writable {
|
|
|
314
260
|
code = data.readUInt16BE(0)
|
|
315
261
|
}
|
|
316
262
|
|
|
263
|
+
if (code !== undefined && !isValidStatusCode(code)) {
|
|
264
|
+
return { code: 1002, reason: 'Invalid status code', error: true }
|
|
265
|
+
}
|
|
266
|
+
|
|
317
267
|
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6
|
|
318
268
|
/** @type {Buffer} */
|
|
319
269
|
let reason = data.subarray(2)
|
|
@@ -323,10 +273,6 @@ class ByteParser extends Writable {
|
|
|
323
273
|
reason = reason.subarray(3)
|
|
324
274
|
}
|
|
325
275
|
|
|
326
|
-
if (code !== undefined && !isValidStatusCode(code)) {
|
|
327
|
-
return { code: 1002, reason: 'Invalid status code', error: true }
|
|
328
|
-
}
|
|
329
|
-
|
|
330
276
|
try {
|
|
331
277
|
reason = utf8Decode(reason)
|
|
332
278
|
} catch {
|
|
@@ -336,6 +282,91 @@ class ByteParser extends Writable {
|
|
|
336
282
|
return { code, reason, error: false }
|
|
337
283
|
}
|
|
338
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Parses control frames.
|
|
287
|
+
* @param {Buffer} body
|
|
288
|
+
*/
|
|
289
|
+
parseControlFrame (body) {
|
|
290
|
+
const { opcode, payloadLength } = this.#info
|
|
291
|
+
|
|
292
|
+
if (opcode === opcodes.CLOSE) {
|
|
293
|
+
if (payloadLength === 1) {
|
|
294
|
+
failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.')
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.#info.closeInfo = this.parseCloseBody(body)
|
|
299
|
+
|
|
300
|
+
if (this.#info.closeInfo.error) {
|
|
301
|
+
const { code, reason } = this.#info.closeInfo
|
|
302
|
+
|
|
303
|
+
closeWebSocketConnection(this.ws, code, reason, reason.length)
|
|
304
|
+
failWebsocketConnection(this.ws, reason)
|
|
305
|
+
return false
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (this.ws[kSentClose] !== sentCloseFrameState.SENT) {
|
|
309
|
+
// If an endpoint receives a Close frame and did not previously send a
|
|
310
|
+
// Close frame, the endpoint MUST send a Close frame in response. (When
|
|
311
|
+
// sending a Close frame in response, the endpoint typically echos the
|
|
312
|
+
// status code it received.)
|
|
313
|
+
let body = emptyBuffer
|
|
314
|
+
if (this.#info.closeInfo.code) {
|
|
315
|
+
body = Buffer.allocUnsafe(2)
|
|
316
|
+
body.writeUInt16BE(this.#info.closeInfo.code, 0)
|
|
317
|
+
}
|
|
318
|
+
const closeFrame = new WebsocketFrameSend(body)
|
|
319
|
+
|
|
320
|
+
this.ws[kResponse].socket.write(
|
|
321
|
+
closeFrame.createFrame(opcodes.CLOSE),
|
|
322
|
+
(err) => {
|
|
323
|
+
if (!err) {
|
|
324
|
+
this.ws[kSentClose] = sentCloseFrameState.SENT
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Upon either sending or receiving a Close control frame, it is said
|
|
331
|
+
// that _The WebSocket Closing Handshake is Started_ and that the
|
|
332
|
+
// WebSocket connection is in the CLOSING state.
|
|
333
|
+
this.ws[kReadyState] = states.CLOSING
|
|
334
|
+
this.ws[kReceivedClose] = true
|
|
335
|
+
|
|
336
|
+
this.end()
|
|
337
|
+
return false
|
|
338
|
+
} else if (opcode === opcodes.PING) {
|
|
339
|
+
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
|
|
340
|
+
// response, unless it already received a Close frame.
|
|
341
|
+
// A Pong frame sent in response to a Ping frame must have identical
|
|
342
|
+
// "Application data"
|
|
343
|
+
|
|
344
|
+
if (!this.ws[kReceivedClose]) {
|
|
345
|
+
const frame = new WebsocketFrameSend(body)
|
|
346
|
+
|
|
347
|
+
this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG))
|
|
348
|
+
|
|
349
|
+
if (channels.ping.hasSubscribers) {
|
|
350
|
+
channels.ping.publish({
|
|
351
|
+
payload: body
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} else if (opcode === opcodes.PONG) {
|
|
356
|
+
// A Pong frame MAY be sent unsolicited. This serves as a
|
|
357
|
+
// unidirectional heartbeat. A response to an unsolicited Pong frame is
|
|
358
|
+
// not expected.
|
|
359
|
+
|
|
360
|
+
if (channels.pong.hasSubscribers) {
|
|
361
|
+
channels.pong.publish({
|
|
362
|
+
payload: body
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return true
|
|
368
|
+
}
|
|
369
|
+
|
|
339
370
|
get closingInfo () {
|
|
340
371
|
return this.#info.closeInfo
|
|
341
372
|
}
|
|
@@ -210,6 +210,30 @@ function failWebsocketConnection (ws, reason) {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5
|
|
215
|
+
* @param {number} opcode
|
|
216
|
+
*/
|
|
217
|
+
function isControlFrame (opcode) {
|
|
218
|
+
return (
|
|
219
|
+
opcode === opcodes.CLOSE ||
|
|
220
|
+
opcode === opcodes.PING ||
|
|
221
|
+
opcode === opcodes.PONG
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isContinuationFrame (opcode) {
|
|
226
|
+
return opcode === opcodes.CONTINUATION
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isTextBinaryFrame (opcode) {
|
|
230
|
+
return opcode === opcodes.TEXT || opcode === opcodes.BINARY
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isValidOpcode (opcode) {
|
|
234
|
+
return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
|
|
235
|
+
}
|
|
236
|
+
|
|
213
237
|
// https://nodejs.org/api/intl.html#detecting-internationalization-support
|
|
214
238
|
const hasIntl = typeof process.versions.icu === 'string'
|
|
215
239
|
const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undefined
|
|
@@ -237,5 +261,9 @@ module.exports = {
|
|
|
237
261
|
isValidStatusCode,
|
|
238
262
|
failWebsocketConnection,
|
|
239
263
|
websocketMessageReceived,
|
|
240
|
-
utf8Decode
|
|
264
|
+
utf8Decode,
|
|
265
|
+
isControlFrame,
|
|
266
|
+
isContinuationFrame,
|
|
267
|
+
isTextBinaryFrame,
|
|
268
|
+
isValidOpcode
|
|
241
269
|
}
|
|
@@ -26,7 +26,7 @@ const { ByteParser } = require('./receiver')
|
|
|
26
26
|
const { kEnumerableProperty, isBlobLike } = require('../../core/util')
|
|
27
27
|
const { getGlobalDispatcher } = require('../../global')
|
|
28
28
|
const { types } = require('node:util')
|
|
29
|
-
const { ErrorEvent } = require('./events')
|
|
29
|
+
const { ErrorEvent, CloseEvent } = require('./events')
|
|
30
30
|
|
|
31
31
|
let experimentalWarned = false
|
|
32
32
|
|
|
@@ -124,6 +124,7 @@ class WebSocket extends EventTarget {
|
|
|
124
124
|
this[kWebSocketURL] = new URL(urlRecord.href)
|
|
125
125
|
|
|
126
126
|
// 11. Let client be this's relevant settings object.
|
|
127
|
+
const client = environmentSettingsObject.settingsObject
|
|
127
128
|
|
|
128
129
|
// 12. Run this step in parallel:
|
|
129
130
|
|
|
@@ -132,6 +133,7 @@ class WebSocket extends EventTarget {
|
|
|
132
133
|
this[kController] = establishWebSocketConnection(
|
|
133
134
|
urlRecord,
|
|
134
135
|
protocols,
|
|
136
|
+
client,
|
|
135
137
|
this,
|
|
136
138
|
(response) => this.#onConnectionEstablished(response),
|
|
137
139
|
options
|
|
@@ -285,7 +287,7 @@ class WebSocket extends EventTarget {
|
|
|
285
287
|
// not throw an exception must increase the bufferedAmount attribute
|
|
286
288
|
// by the length of data’s buffer in bytes.
|
|
287
289
|
|
|
288
|
-
const ab = new FastBuffer(data, data.byteOffset, data.byteLength)
|
|
290
|
+
const ab = new FastBuffer(data.buffer, data.byteOffset, data.byteLength)
|
|
289
291
|
|
|
290
292
|
const frame = new WebsocketFrameSend(ab)
|
|
291
293
|
const buffer = frame.createFrame(opcodes.BINARY)
|
|
@@ -547,7 +549,7 @@ webidl.converters['DOMString or sequence<DOMString>'] = function (V, prefix, arg
|
|
|
547
549
|
return webidl.converters.DOMString(V, prefix, argument)
|
|
548
550
|
}
|
|
549
551
|
|
|
550
|
-
// This implements the
|
|
552
|
+
// This implements the proposal made in https://github.com/whatwg/websockets/issues/42
|
|
551
553
|
webidl.converters.WebSocketInit = webidl.dictionaryConverter([
|
|
552
554
|
{
|
|
553
555
|
key: 'protocols',
|
|
@@ -592,9 +594,19 @@ function onParserDrain () {
|
|
|
592
594
|
}
|
|
593
595
|
|
|
594
596
|
function onParserError (err) {
|
|
595
|
-
|
|
597
|
+
let message
|
|
598
|
+
let code
|
|
599
|
+
|
|
600
|
+
if (err instanceof CloseEvent) {
|
|
601
|
+
message = err.reason
|
|
602
|
+
code = err.code
|
|
603
|
+
} else {
|
|
604
|
+
message = err.message
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
fireEvent('error', this, () => new ErrorEvent('error', { error: err, message }))
|
|
596
608
|
|
|
597
|
-
closeWebSocketConnection(this,
|
|
609
|
+
closeWebSocketConnection(this, code)
|
|
598
610
|
}
|
|
599
611
|
|
|
600
612
|
module.exports = {
|