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.
@@ -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 with request.
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
- // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list.
262
- if (request.responseTainting === 'cors' || request.mode === 'websocket') {
263
- if (serializedOrigin) {
264
- request.headersList.append('origin', serializedOrigin, true)
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 request’s current URL’s scheme is not "https", then set serializedOrigin to `null`.
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 origin, then set serializedOrigin to `null`.
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
- if (serializedOrigin) {
294
- // 2. Append (`Origin`, serializedOrigin) to request’s header list.
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)[kHeadersList]
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), (err) => {
264
- if (!err) {
265
- ws[kSentClose] = sentCloseFrameState.SENT
266
- }
267
- })
263
+ socket.write(frame.createFrame(opcodes.CLOSE))
268
264
 
269
- ws[kSentClose] = sentCloseFrameState.PROCESSING
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 { isValidStatusCode, failWebsocketConnection, websocketMessageReceived, utf8Decode } = require('./util')
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 { CloseEvent } = require('./events')
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 (true) {
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
- this.#info.fin = (buffer[0] & 0x80) !== 0
58
- this.#info.opcode = buffer[0] & 0x0F
59
- this.#info.masked = (buffer[1] & 0x80) === 0x80
72
+ const fragmented = !fin && opcode !== opcodes.CONTINUATION
73
+ const payloadLength = buffer[1] & 0x7F
60
74
 
61
- if (this.#info.masked) {
62
- failWebsocketConnection(this.ws, 'Frame cannot be masked')
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
- // If we receive a fragmented message, we use the type of the first
67
- // frame to parse the full message as binary/text, when it's terminated
68
- this.#info.originalOpcode ??= this.#info.opcode
84
+ if (masked) {
85
+ failWebsocketConnection(this.ws, 'Frame cannot be masked')
86
+ return callback()
87
+ }
69
88
 
70
- this.#info.fragmented = !this.#info.fin && this.#info.opcode !== opcodes.CONTINUATION
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 (this.#info.fragmented && this.#info.opcode !== opcodes.BINARY && this.#info.opcode !== opcodes.TEXT) {
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
- const payloadLength = buffer[1] & 0x7F
79
-
80
- if (payloadLength <= 125) {
81
- this.#info.payloadLength = payloadLength
82
- this.#state = parserStates.READ_DATA
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 && payloadLength > 125) {
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
- } else if (
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
- } else if (this.#info.opcode === opcodes.PING) {
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
- const body = this.consume(payloadLength)
125
+ if (isContinuationFrame(opcode) && this.#fragments.length === 0) {
126
+ failWebsocketConnection(this.ws, 'Unexpected continuation frame')
127
+ return
128
+ }
184
129
 
185
- if (channels.pong.hasSubscribers) {
186
- channels.pong.publish({
187
- payload: body
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
- if (this.#byteOffset > 0) {
192
- continue
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 maxinimum bytes an arraybuffer can contain
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
- } else if (this.#byteOffset >= this.#info.payloadLength) {
235
- // If the server sent multiple frames in a single chunk
182
+ }
236
183
 
237
- const body = this.consume(this.#info.payloadLength)
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 unfragmented, or a fragmented frame was terminated,
242
- // a message was received
243
- if (!this.#info.fragmented || (this.#info.fin && this.#info.opcode === opcodes.CONTINUATION)) {
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
- if (this.#byteOffset === 0 && this.#info.payloadLength !== 0) {
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|null}
210
+ * @returns {Buffer}
267
211
  */
268
212
  consume (n) {
269
213
  if (n > this.#byteOffset) {
270
- return null
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 propsal made in https://github.com/whatwg/websockets/issues/42
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
- fireEvent('error', this, () => new ErrorEvent('error', { error: err, message: err.reason }))
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, err.code)
609
+ closeWebSocketConnection(this, code)
598
610
  }
599
611
 
600
612
  module.exports = {