source: node_modules/undici/lib/websocket/websocket.js@ d24f17c

main
Last change on this file since d24f17c was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 20.5 KB
RevLine 
[d24f17c]1'use strict'
2
3const { webidl } = require('../fetch/webidl')
4const { DOMException } = require('../fetch/constants')
5const { URLSerializer } = require('../fetch/dataURL')
6const { getGlobalOrigin } = require('../fetch/global')
7const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = require('./constants')
8const {
9 kWebSocketURL,
10 kReadyState,
11 kController,
12 kBinaryType,
13 kResponse,
14 kSentClose,
15 kByteParser
16} = require('./symbols')
17const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = require('./util')
18const { establishWebSocketConnection } = require('./connection')
19const { WebsocketFrameSend } = require('./frame')
20const { ByteParser } = require('./receiver')
21const { kEnumerableProperty, isBlobLike } = require('../core/util')
22const { getGlobalDispatcher } = require('../global')
23const { types } = require('util')
24
25let experimentalWarned = false
26
27// https://websockets.spec.whatwg.org/#interface-definition
28class WebSocket extends EventTarget {
29 #events = {
30 open: null,
31 error: null,
32 close: null,
33 message: null
34 }
35
36 #bufferedAmount = 0
37 #protocol = ''
38 #extensions = ''
39
40 /**
41 * @param {string} url
42 * @param {string|string[]} protocols
43 */
44 constructor (url, protocols = []) {
45 super()
46
47 webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket constructor' })
48
49 if (!experimentalWarned) {
50 experimentalWarned = true
51 process.emitWarning('WebSockets are experimental, expect them to change at any time.', {
52 code: 'UNDICI-WS'
53 })
54 }
55
56 const options = webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'](protocols)
57
58 url = webidl.converters.USVString(url)
59 protocols = options.protocols
60
61 // 1. Let baseURL be this's relevant settings object's API base URL.
62 const baseURL = getGlobalOrigin()
63
64 // 1. Let urlRecord be the result of applying the URL parser to url with baseURL.
65 let urlRecord
66
67 try {
68 urlRecord = new URL(url, baseURL)
69 } catch (e) {
70 // 3. If urlRecord is failure, then throw a "SyntaxError" DOMException.
71 throw new DOMException(e, 'SyntaxError')
72 }
73
74 // 4. If urlRecord’s scheme is "http", then set urlRecord’s scheme to "ws".
75 if (urlRecord.protocol === 'http:') {
76 urlRecord.protocol = 'ws:'
77 } else if (urlRecord.protocol === 'https:') {
78 // 5. Otherwise, if urlRecord’s scheme is "https", set urlRecord’s scheme to "wss".
79 urlRecord.protocol = 'wss:'
80 }
81
82 // 6. If urlRecord’s scheme is not "ws" or "wss", then throw a "SyntaxError" DOMException.
83 if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') {
84 throw new DOMException(
85 `Expected a ws: or wss: protocol, got ${urlRecord.protocol}`,
86 'SyntaxError'
87 )
88 }
89
90 // 7. If urlRecord’s fragment is non-null, then throw a "SyntaxError"
91 // DOMException.
92 if (urlRecord.hash || urlRecord.href.endsWith('#')) {
93 throw new DOMException('Got fragment', 'SyntaxError')
94 }
95
96 // 8. If protocols is a string, set protocols to a sequence consisting
97 // of just that string.
98 if (typeof protocols === 'string') {
99 protocols = [protocols]
100 }
101
102 // 9. If any of the values in protocols occur more than once or otherwise
103 // fail to match the requirements for elements that comprise the value
104 // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket
105 // protocol, then throw a "SyntaxError" DOMException.
106 if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) {
107 throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
108 }
109
110 if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) {
111 throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
112 }
113
114 // 10. Set this's url to urlRecord.
115 this[kWebSocketURL] = new URL(urlRecord.href)
116
117 // 11. Let client be this's relevant settings object.
118
119 // 12. Run this step in parallel:
120
121 // 1. Establish a WebSocket connection given urlRecord, protocols,
122 // and client.
123 this[kController] = establishWebSocketConnection(
124 urlRecord,
125 protocols,
126 this,
127 (response) => this.#onConnectionEstablished(response),
128 options
129 )
130
131 // Each WebSocket object has an associated ready state, which is a
132 // number representing the state of the connection. Initially it must
133 // be CONNECTING (0).
134 this[kReadyState] = WebSocket.CONNECTING
135
136 // The extensions attribute must initially return the empty string.
137
138 // The protocol attribute must initially return the empty string.
139
140 // Each WebSocket object has an associated binary type, which is a
141 // BinaryType. Initially it must be "blob".
142 this[kBinaryType] = 'blob'
143 }
144
145 /**
146 * @see https://websockets.spec.whatwg.org/#dom-websocket-close
147 * @param {number|undefined} code
148 * @param {string|undefined} reason
149 */
150 close (code = undefined, reason = undefined) {
151 webidl.brandCheck(this, WebSocket)
152
153 if (code !== undefined) {
154 code = webidl.converters['unsigned short'](code, { clamp: true })
155 }
156
157 if (reason !== undefined) {
158 reason = webidl.converters.USVString(reason)
159 }
160
161 // 1. If code is present, but is neither an integer equal to 1000 nor an
162 // integer in the range 3000 to 4999, inclusive, throw an
163 // "InvalidAccessError" DOMException.
164 if (code !== undefined) {
165 if (code !== 1000 && (code < 3000 || code > 4999)) {
166 throw new DOMException('invalid code', 'InvalidAccessError')
167 }
168 }
169
170 let reasonByteLength = 0
171
172 // 2. If reason is present, then run these substeps:
173 if (reason !== undefined) {
174 // 1. Let reasonBytes be the result of encoding reason.
175 // 2. If reasonBytes is longer than 123 bytes, then throw a
176 // "SyntaxError" DOMException.
177 reasonByteLength = Buffer.byteLength(reason)
178
179 if (reasonByteLength > 123) {
180 throw new DOMException(
181 `Reason must be less than 123 bytes; received ${reasonByteLength}`,
182 'SyntaxError'
183 )
184 }
185 }
186
187 // 3. Run the first matching steps from the following list:
188 if (this[kReadyState] === WebSocket.CLOSING || this[kReadyState] === WebSocket.CLOSED) {
189 // If this's ready state is CLOSING (2) or CLOSED (3)
190 // Do nothing.
191 } else if (!isEstablished(this)) {
192 // If the WebSocket connection is not yet established
193 // Fail the WebSocket connection and set this's ready state
194 // to CLOSING (2).
195 failWebsocketConnection(this, 'Connection was closed before it was established.')
196 this[kReadyState] = WebSocket.CLOSING
197 } else if (!isClosing(this)) {
198 // If the WebSocket closing handshake has not yet been started
199 // Start the WebSocket closing handshake and set this's ready
200 // state to CLOSING (2).
201 // - If neither code nor reason is present, the WebSocket Close
202 // message must not have a body.
203 // - If code is present, then the status code to use in the
204 // WebSocket Close message must be the integer given by code.
205 // - If reason is also present, then reasonBytes must be
206 // provided in the Close message after the status code.
207
208 const frame = new WebsocketFrameSend()
209
210 // If neither code nor reason is present, the WebSocket Close
211 // message must not have a body.
212
213 // If code is present, then the status code to use in the
214 // WebSocket Close message must be the integer given by code.
215 if (code !== undefined && reason === undefined) {
216 frame.frameData = Buffer.allocUnsafe(2)
217 frame.frameData.writeUInt16BE(code, 0)
218 } else if (code !== undefined && reason !== undefined) {
219 // If reason is also present, then reasonBytes must be
220 // provided in the Close message after the status code.
221 frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength)
222 frame.frameData.writeUInt16BE(code, 0)
223 // the body MAY contain UTF-8-encoded data with value /reason/
224 frame.frameData.write(reason, 2, 'utf-8')
225 } else {
226 frame.frameData = emptyBuffer
227 }
228
229 /** @type {import('stream').Duplex} */
230 const socket = this[kResponse].socket
231
232 socket.write(frame.createFrame(opcodes.CLOSE), (err) => {
233 if (!err) {
234 this[kSentClose] = true
235 }
236 })
237
238 // Upon either sending or receiving a Close control frame, it is said
239 // that _The WebSocket Closing Handshake is Started_ and that the
240 // WebSocket connection is in the CLOSING state.
241 this[kReadyState] = states.CLOSING
242 } else {
243 // Otherwise
244 // Set this's ready state to CLOSING (2).
245 this[kReadyState] = WebSocket.CLOSING
246 }
247 }
248
249 /**
250 * @see https://websockets.spec.whatwg.org/#dom-websocket-send
251 * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data
252 */
253 send (data) {
254 webidl.brandCheck(this, WebSocket)
255
256 webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket.send' })
257
258 data = webidl.converters.WebSocketSendData(data)
259
260 // 1. If this's ready state is CONNECTING, then throw an
261 // "InvalidStateError" DOMException.
262 if (this[kReadyState] === WebSocket.CONNECTING) {
263 throw new DOMException('Sent before connected.', 'InvalidStateError')
264 }
265
266 // 2. Run the appropriate set of steps from the following list:
267 // https://datatracker.ietf.org/doc/html/rfc6455#section-6.1
268 // https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
269
270 if (!isEstablished(this) || isClosing(this)) {
271 return
272 }
273
274 /** @type {import('stream').Duplex} */
275 const socket = this[kResponse].socket
276
277 // If data is a string
278 if (typeof data === 'string') {
279 // If the WebSocket connection is established and the WebSocket
280 // closing handshake has not yet started, then the user agent
281 // must send a WebSocket Message comprised of the data argument
282 // using a text frame opcode; if the data cannot be sent, e.g.
283 // because it would need to be buffered but the buffer is full,
284 // the user agent must flag the WebSocket as full and then close
285 // the WebSocket connection. Any invocation of this method with a
286 // string argument that does not throw an exception must increase
287 // the bufferedAmount attribute by the number of bytes needed to
288 // express the argument as UTF-8.
289
290 const value = Buffer.from(data)
291 const frame = new WebsocketFrameSend(value)
292 const buffer = frame.createFrame(opcodes.TEXT)
293
294 this.#bufferedAmount += value.byteLength
295 socket.write(buffer, () => {
296 this.#bufferedAmount -= value.byteLength
297 })
298 } else if (types.isArrayBuffer(data)) {
299 // If the WebSocket connection is established, and the WebSocket
300 // closing handshake has not yet started, then the user agent must
301 // send a WebSocket Message comprised of data using a binary frame
302 // opcode; if the data cannot be sent, e.g. because it would need
303 // to be buffered but the buffer is full, the user agent must flag
304 // the WebSocket as full and then close the WebSocket connection.
305 // The data to be sent is the data stored in the buffer described
306 // by the ArrayBuffer object. Any invocation of this method with an
307 // ArrayBuffer argument that does not throw an exception must
308 // increase the bufferedAmount attribute by the length of the
309 // ArrayBuffer in bytes.
310
311 const value = Buffer.from(data)
312 const frame = new WebsocketFrameSend(value)
313 const buffer = frame.createFrame(opcodes.BINARY)
314
315 this.#bufferedAmount += value.byteLength
316 socket.write(buffer, () => {
317 this.#bufferedAmount -= value.byteLength
318 })
319 } else if (ArrayBuffer.isView(data)) {
320 // If the WebSocket connection is established, and the WebSocket
321 // closing handshake has not yet started, then the user agent must
322 // send a WebSocket Message comprised of data using a binary frame
323 // opcode; if the data cannot be sent, e.g. because it would need to
324 // be buffered but the buffer is full, the user agent must flag the
325 // WebSocket as full and then close the WebSocket connection. The
326 // data to be sent is the data stored in the section of the buffer
327 // described by the ArrayBuffer object that data references. Any
328 // invocation of this method with this kind of argument that does
329 // not throw an exception must increase the bufferedAmount attribute
330 // by the length of data’s buffer in bytes.
331
332 const ab = Buffer.from(data, data.byteOffset, data.byteLength)
333
334 const frame = new WebsocketFrameSend(ab)
335 const buffer = frame.createFrame(opcodes.BINARY)
336
337 this.#bufferedAmount += ab.byteLength
338 socket.write(buffer, () => {
339 this.#bufferedAmount -= ab.byteLength
340 })
341 } else if (isBlobLike(data)) {
342 // If the WebSocket connection is established, and the WebSocket
343 // closing handshake has not yet started, then the user agent must
344 // send a WebSocket Message comprised of data using a binary frame
345 // opcode; if the data cannot be sent, e.g. because it would need to
346 // be buffered but the buffer is full, the user agent must flag the
347 // WebSocket as full and then close the WebSocket connection. The data
348 // to be sent is the raw data represented by the Blob object. Any
349 // invocation of this method with a Blob argument that does not throw
350 // an exception must increase the bufferedAmount attribute by the size
351 // of the Blob object’s raw data, in bytes.
352
353 const frame = new WebsocketFrameSend()
354
355 data.arrayBuffer().then((ab) => {
356 const value = Buffer.from(ab)
357 frame.frameData = value
358 const buffer = frame.createFrame(opcodes.BINARY)
359
360 this.#bufferedAmount += value.byteLength
361 socket.write(buffer, () => {
362 this.#bufferedAmount -= value.byteLength
363 })
364 })
365 }
366 }
367
368 get readyState () {
369 webidl.brandCheck(this, WebSocket)
370
371 // The readyState getter steps are to return this's ready state.
372 return this[kReadyState]
373 }
374
375 get bufferedAmount () {
376 webidl.brandCheck(this, WebSocket)
377
378 return this.#bufferedAmount
379 }
380
381 get url () {
382 webidl.brandCheck(this, WebSocket)
383
384 // The url getter steps are to return this's url, serialized.
385 return URLSerializer(this[kWebSocketURL])
386 }
387
388 get extensions () {
389 webidl.brandCheck(this, WebSocket)
390
391 return this.#extensions
392 }
393
394 get protocol () {
395 webidl.brandCheck(this, WebSocket)
396
397 return this.#protocol
398 }
399
400 get onopen () {
401 webidl.brandCheck(this, WebSocket)
402
403 return this.#events.open
404 }
405
406 set onopen (fn) {
407 webidl.brandCheck(this, WebSocket)
408
409 if (this.#events.open) {
410 this.removeEventListener('open', this.#events.open)
411 }
412
413 if (typeof fn === 'function') {
414 this.#events.open = fn
415 this.addEventListener('open', fn)
416 } else {
417 this.#events.open = null
418 }
419 }
420
421 get onerror () {
422 webidl.brandCheck(this, WebSocket)
423
424 return this.#events.error
425 }
426
427 set onerror (fn) {
428 webidl.brandCheck(this, WebSocket)
429
430 if (this.#events.error) {
431 this.removeEventListener('error', this.#events.error)
432 }
433
434 if (typeof fn === 'function') {
435 this.#events.error = fn
436 this.addEventListener('error', fn)
437 } else {
438 this.#events.error = null
439 }
440 }
441
442 get onclose () {
443 webidl.brandCheck(this, WebSocket)
444
445 return this.#events.close
446 }
447
448 set onclose (fn) {
449 webidl.brandCheck(this, WebSocket)
450
451 if (this.#events.close) {
452 this.removeEventListener('close', this.#events.close)
453 }
454
455 if (typeof fn === 'function') {
456 this.#events.close = fn
457 this.addEventListener('close', fn)
458 } else {
459 this.#events.close = null
460 }
461 }
462
463 get onmessage () {
464 webidl.brandCheck(this, WebSocket)
465
466 return this.#events.message
467 }
468
469 set onmessage (fn) {
470 webidl.brandCheck(this, WebSocket)
471
472 if (this.#events.message) {
473 this.removeEventListener('message', this.#events.message)
474 }
475
476 if (typeof fn === 'function') {
477 this.#events.message = fn
478 this.addEventListener('message', fn)
479 } else {
480 this.#events.message = null
481 }
482 }
483
484 get binaryType () {
485 webidl.brandCheck(this, WebSocket)
486
487 return this[kBinaryType]
488 }
489
490 set binaryType (type) {
491 webidl.brandCheck(this, WebSocket)
492
493 if (type !== 'blob' && type !== 'arraybuffer') {
494 this[kBinaryType] = 'blob'
495 } else {
496 this[kBinaryType] = type
497 }
498 }
499
500 /**
501 * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
502 */
503 #onConnectionEstablished (response) {
504 // processResponse is called when the "response’s header list has been received and initialized."
505 // once this happens, the connection is open
506 this[kResponse] = response
507
508 const parser = new ByteParser(this)
509 parser.on('drain', function onParserDrain () {
510 this.ws[kResponse].socket.resume()
511 })
512
513 response.socket.ws = this
514 this[kByteParser] = parser
515
516 // 1. Change the ready state to OPEN (1).
517 this[kReadyState] = states.OPEN
518
519 // 2. Change the extensions attribute’s value to the extensions in use, if
520 // it is not the null value.
521 // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
522 const extensions = response.headersList.get('sec-websocket-extensions')
523
524 if (extensions !== null) {
525 this.#extensions = extensions
526 }
527
528 // 3. Change the protocol attribute’s value to the subprotocol in use, if
529 // it is not the null value.
530 // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9
531 const protocol = response.headersList.get('sec-websocket-protocol')
532
533 if (protocol !== null) {
534 this.#protocol = protocol
535 }
536
537 // 4. Fire an event named open at the WebSocket object.
538 fireEvent('open', this)
539 }
540}
541
542// https://websockets.spec.whatwg.org/#dom-websocket-connecting
543WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING
544// https://websockets.spec.whatwg.org/#dom-websocket-open
545WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN
546// https://websockets.spec.whatwg.org/#dom-websocket-closing
547WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING
548// https://websockets.spec.whatwg.org/#dom-websocket-closed
549WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED
550
551Object.defineProperties(WebSocket.prototype, {
552 CONNECTING: staticPropertyDescriptors,
553 OPEN: staticPropertyDescriptors,
554 CLOSING: staticPropertyDescriptors,
555 CLOSED: staticPropertyDescriptors,
556 url: kEnumerableProperty,
557 readyState: kEnumerableProperty,
558 bufferedAmount: kEnumerableProperty,
559 onopen: kEnumerableProperty,
560 onerror: kEnumerableProperty,
561 onclose: kEnumerableProperty,
562 close: kEnumerableProperty,
563 onmessage: kEnumerableProperty,
564 binaryType: kEnumerableProperty,
565 send: kEnumerableProperty,
566 extensions: kEnumerableProperty,
567 protocol: kEnumerableProperty,
568 [Symbol.toStringTag]: {
569 value: 'WebSocket',
570 writable: false,
571 enumerable: false,
572 configurable: true
573 }
574})
575
576Object.defineProperties(WebSocket, {
577 CONNECTING: staticPropertyDescriptors,
578 OPEN: staticPropertyDescriptors,
579 CLOSING: staticPropertyDescriptors,
580 CLOSED: staticPropertyDescriptors
581})
582
583webidl.converters['sequence<DOMString>'] = webidl.sequenceConverter(
584 webidl.converters.DOMString
585)
586
587webidl.converters['DOMString or sequence<DOMString>'] = function (V) {
588 if (webidl.util.Type(V) === 'Object' && Symbol.iterator in V) {
589 return webidl.converters['sequence<DOMString>'](V)
590 }
591
592 return webidl.converters.DOMString(V)
593}
594
595// This implements the propsal made in https://github.com/whatwg/websockets/issues/42
596webidl.converters.WebSocketInit = webidl.dictionaryConverter([
597 {
598 key: 'protocols',
599 converter: webidl.converters['DOMString or sequence<DOMString>'],
600 get defaultValue () {
601 return []
602 }
603 },
604 {
605 key: 'dispatcher',
606 converter: (V) => V,
607 get defaultValue () {
608 return getGlobalDispatcher()
609 }
610 },
611 {
612 key: 'headers',
613 converter: webidl.nullableConverter(webidl.converters.HeadersInit)
614 }
615])
616
617webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'] = function (V) {
618 if (webidl.util.Type(V) === 'Object' && !(Symbol.iterator in V)) {
619 return webidl.converters.WebSocketInit(V)
620 }
621
622 return { protocols: webidl.converters['DOMString or sequence<DOMString>'](V) }
623}
624
625webidl.converters.WebSocketSendData = function (V) {
626 if (webidl.util.Type(V) === 'Object') {
627 if (isBlobLike(V)) {
628 return webidl.converters.Blob(V, { strict: false })
629 }
630
631 if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) {
632 return webidl.converters.BufferSource(V)
633 }
634 }
635
636 return webidl.converters.USVString(V)
637}
638
639module.exports = {
640 WebSocket
641}
Note: See TracBrowser for help on using the repository browser.