Spaces:
Runtime error
Runtime error
| const { webidl } = require('../fetch/webidl') | |
| const { DOMException } = require('../fetch/constants') | |
| const { URLSerializer } = require('../fetch/dataURL') | |
| const { getGlobalOrigin } = require('../fetch/global') | |
| const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = require('./constants') | |
| const { | |
| kWebSocketURL, | |
| kReadyState, | |
| kController, | |
| kBinaryType, | |
| kResponse, | |
| kSentClose, | |
| kByteParser | |
| } = require('./symbols') | |
| const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = require('./util') | |
| const { establishWebSocketConnection } = require('./connection') | |
| const { WebsocketFrameSend } = require('./frame') | |
| const { ByteParser } = require('./receiver') | |
| const { kEnumerableProperty, isBlobLike } = require('../core/util') | |
| const { getGlobalDispatcher } = require('../global') | |
| const { types } = require('util') | |
| let experimentalWarned = false | |
| // https://websockets.spec.whatwg.org/#interface-definition | |
| class WebSocket extends EventTarget { | |
| #events = { | |
| open: null, | |
| error: null, | |
| close: null, | |
| message: null | |
| } | |
| #bufferedAmount = 0 | |
| #protocol = '' | |
| #extensions = '' | |
| /** | |
| * @param {string} url | |
| * @param {string|string[]} protocols | |
| */ | |
| constructor (url, protocols = []) { | |
| super() | |
| webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket constructor' }) | |
| if (!experimentalWarned) { | |
| experimentalWarned = true | |
| process.emitWarning('WebSockets are experimental, expect them to change at any time.', { | |
| code: 'UNDICI-WS' | |
| }) | |
| } | |
| const options = webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'](protocols) | |
| url = webidl.converters.USVString(url) | |
| protocols = options.protocols | |
| // 1. Let baseURL be this's relevant settings object's API base URL. | |
| const baseURL = getGlobalOrigin() | |
| // 1. Let urlRecord be the result of applying the URL parser to url with baseURL. | |
| let urlRecord | |
| try { | |
| urlRecord = new URL(url, baseURL) | |
| } catch (e) { | |
| // 3. If urlRecord is failure, then throw a "SyntaxError" DOMException. | |
| throw new DOMException(e, 'SyntaxError') | |
| } | |
| // 4. If urlRecord’s scheme is "http", then set urlRecord’s scheme to "ws". | |
| if (urlRecord.protocol === 'http:') { | |
| urlRecord.protocol = 'ws:' | |
| } else if (urlRecord.protocol === 'https:') { | |
| // 5. Otherwise, if urlRecord’s scheme is "https", set urlRecord’s scheme to "wss". | |
| urlRecord.protocol = 'wss:' | |
| } | |
| // 6. If urlRecord’s scheme is not "ws" or "wss", then throw a "SyntaxError" DOMException. | |
| if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') { | |
| throw new DOMException( | |
| `Expected a ws: or wss: protocol, got ${urlRecord.protocol}`, | |
| 'SyntaxError' | |
| ) | |
| } | |
| // 7. If urlRecord’s fragment is non-null, then throw a "SyntaxError" | |
| // DOMException. | |
| if (urlRecord.hash || urlRecord.href.endsWith('#')) { | |
| throw new DOMException('Got fragment', 'SyntaxError') | |
| } | |
| // 8. If protocols is a string, set protocols to a sequence consisting | |
| // of just that string. | |
| if (typeof protocols === 'string') { | |
| protocols = [protocols] | |
| } | |
| // 9. If any of the values in protocols occur more than once or otherwise | |
| // fail to match the requirements for elements that comprise the value | |
| // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket | |
| // protocol, then throw a "SyntaxError" DOMException. | |
| if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) { | |
| throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') | |
| } | |
| if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) { | |
| throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') | |
| } | |
| // 10. Set this's url to urlRecord. | |
| this[kWebSocketURL] = new URL(urlRecord.href) | |
| // 11. Let client be this's relevant settings object. | |
| // 12. Run this step in parallel: | |
| // 1. Establish a WebSocket connection given urlRecord, protocols, | |
| // and client. | |
| this[kController] = establishWebSocketConnection( | |
| urlRecord, | |
| protocols, | |
| this, | |
| (response) => this.#onConnectionEstablished(response), | |
| options | |
| ) | |
| // Each WebSocket object has an associated ready state, which is a | |
| // number representing the state of the connection. Initially it must | |
| // be CONNECTING (0). | |
| this[kReadyState] = WebSocket.CONNECTING | |
| // The extensions attribute must initially return the empty string. | |
| // The protocol attribute must initially return the empty string. | |
| // Each WebSocket object has an associated binary type, which is a | |
| // BinaryType. Initially it must be "blob". | |
| this[kBinaryType] = 'blob' | |
| } | |
| /** | |
| * @see https://websockets.spec.whatwg.org/#dom-websocket-close | |
| * @param {number|undefined} code | |
| * @param {string|undefined} reason | |
| */ | |
| close (code = undefined, reason = undefined) { | |
| webidl.brandCheck(this, WebSocket) | |
| if (code !== undefined) { | |
| code = webidl.converters['unsigned short'](code, { clamp: true }) | |
| } | |
| if (reason !== undefined) { | |
| reason = webidl.converters.USVString(reason) | |
| } | |
| // 1. If code is present, but is neither an integer equal to 1000 nor an | |
| // integer in the range 3000 to 4999, inclusive, throw an | |
| // "InvalidAccessError" DOMException. | |
| if (code !== undefined) { | |
| if (code !== 1000 && (code < 3000 || code > 4999)) { | |
| throw new DOMException('invalid code', 'InvalidAccessError') | |
| } | |
| } | |
| let reasonByteLength = 0 | |
| // 2. If reason is present, then run these substeps: | |
| if (reason !== undefined) { | |
| // 1. Let reasonBytes be the result of encoding reason. | |
| // 2. If reasonBytes is longer than 123 bytes, then throw a | |
| // "SyntaxError" DOMException. | |
| reasonByteLength = Buffer.byteLength(reason) | |
| if (reasonByteLength > 123) { | |
| throw new DOMException( | |
| `Reason must be less than 123 bytes; received ${reasonByteLength}`, | |
| 'SyntaxError' | |
| ) | |
| } | |
| } | |
| // 3. Run the first matching steps from the following list: | |
| if (this[kReadyState] === WebSocket.CLOSING || this[kReadyState] === WebSocket.CLOSED) { | |
| // If this's ready state is CLOSING (2) or CLOSED (3) | |
| // Do nothing. | |
| } else if (!isEstablished(this)) { | |
| // If the WebSocket connection is not yet established | |
| // Fail the WebSocket connection and set this's ready state | |
| // to CLOSING (2). | |
| failWebsocketConnection(this, 'Connection was closed before it was established.') | |
| this[kReadyState] = WebSocket.CLOSING | |
| } else if (!isClosing(this)) { | |
| // If the WebSocket closing handshake has not yet been started | |
| // Start the WebSocket closing handshake and set this's ready | |
| // state to CLOSING (2). | |
| // - If neither code nor reason is present, the WebSocket Close | |
| // message must not have a body. | |
| // - If code is present, then the status code to use in the | |
| // WebSocket Close message must be the integer given by code. | |
| // - If reason is also present, then reasonBytes must be | |
| // provided in the Close message after the status code. | |
| const frame = new WebsocketFrameSend() | |
| // If neither code nor reason is present, the WebSocket Close | |
| // message must not have a body. | |
| // If code is present, then the status code to use in the | |
| // WebSocket Close message must be the integer given by code. | |
| if (code !== undefined && reason === undefined) { | |
| frame.frameData = Buffer.allocUnsafe(2) | |
| frame.frameData.writeUInt16BE(code, 0) | |
| } else if (code !== undefined && reason !== undefined) { | |
| // If reason is also present, then reasonBytes must be | |
| // provided in the Close message after the status code. | |
| frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength) | |
| frame.frameData.writeUInt16BE(code, 0) | |
| // the body MAY contain UTF-8-encoded data with value /reason/ | |
| frame.frameData.write(reason, 2, 'utf-8') | |
| } else { | |
| frame.frameData = emptyBuffer | |
| } | |
| /** @type {import('stream').Duplex} */ | |
| const socket = this[kResponse].socket | |
| socket.write(frame.createFrame(opcodes.CLOSE), (err) => { | |
| if (!err) { | |
| this[kSentClose] = true | |
| } | |
| }) | |
| // Upon either sending or receiving a Close control frame, it is said | |
| // that _The WebSocket Closing Handshake is Started_ and that the | |
| // WebSocket connection is in the CLOSING state. | |
| this[kReadyState] = states.CLOSING | |
| } else { | |
| // Otherwise | |
| // Set this's ready state to CLOSING (2). | |
| this[kReadyState] = WebSocket.CLOSING | |
| } | |
| } | |
| /** | |
| * @see https://websockets.spec.whatwg.org/#dom-websocket-send | |
| * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data | |
| */ | |
| send (data) { | |
| webidl.brandCheck(this, WebSocket) | |
| webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket.send' }) | |
| data = webidl.converters.WebSocketSendData(data) | |
| // 1. If this's ready state is CONNECTING, then throw an | |
| // "InvalidStateError" DOMException. | |
| if (this[kReadyState] === WebSocket.CONNECTING) { | |
| throw new DOMException('Sent before connected.', 'InvalidStateError') | |
| } | |
| // 2. Run the appropriate set of steps from the following list: | |
| // https://datatracker.ietf.org/doc/html/rfc6455#section-6.1 | |
| // https://datatracker.ietf.org/doc/html/rfc6455#section-5.2 | |
| if (!isEstablished(this) || isClosing(this)) { | |
| return | |
| } | |
| /** @type {import('stream').Duplex} */ | |
| const socket = this[kResponse].socket | |
| // If data is a string | |
| if (typeof data === 'string') { | |
| // If the WebSocket connection is established and the WebSocket | |
| // closing handshake has not yet started, then the user agent | |
| // must send a WebSocket Message comprised of the data argument | |
| // using a text frame opcode; if the data cannot be sent, e.g. | |
| // because it would need to be buffered but the buffer is full, | |
| // the user agent must flag the WebSocket as full and then close | |
| // the WebSocket connection. Any invocation of this method with a | |
| // string argument that does not throw an exception must increase | |
| // the bufferedAmount attribute by the number of bytes needed to | |
| // express the argument as UTF-8. | |
| const value = Buffer.from(data) | |
| const frame = new WebsocketFrameSend(value) | |
| const buffer = frame.createFrame(opcodes.TEXT) | |
| this.#bufferedAmount += value.byteLength | |
| socket.write(buffer, () => { | |
| this.#bufferedAmount -= value.byteLength | |
| }) | |
| } else if (types.isArrayBuffer(data)) { | |
| // If the WebSocket connection is established, and the WebSocket | |
| // closing handshake has not yet started, then the user agent must | |
| // send a WebSocket Message comprised of data using a binary frame | |
| // opcode; if the data cannot be sent, e.g. because it would need | |
| // to be buffered but the buffer is full, the user agent must flag | |
| // the WebSocket as full and then close the WebSocket connection. | |
| // The data to be sent is the data stored in the buffer described | |
| // by the ArrayBuffer object. Any invocation of this method with an | |
| // ArrayBuffer argument that does not throw an exception must | |
| // increase the bufferedAmount attribute by the length of the | |
| // ArrayBuffer in bytes. | |
| const value = Buffer.from(data) | |
| const frame = new WebsocketFrameSend(value) | |
| const buffer = frame.createFrame(opcodes.BINARY) | |
| this.#bufferedAmount += value.byteLength | |
| socket.write(buffer, () => { | |
| this.#bufferedAmount -= value.byteLength | |
| }) | |
| } else if (ArrayBuffer.isView(data)) { | |
| // If the WebSocket connection is established, and the WebSocket | |
| // closing handshake has not yet started, then the user agent must | |
| // send a WebSocket Message comprised of data using a binary frame | |
| // opcode; if the data cannot be sent, e.g. because it would need to | |
| // be buffered but the buffer is full, the user agent must flag the | |
| // WebSocket as full and then close the WebSocket connection. The | |
| // data to be sent is the data stored in the section of the buffer | |
| // described by the ArrayBuffer object that data references. Any | |
| // invocation of this method with this kind of argument that does | |
| // not throw an exception must increase the bufferedAmount attribute | |
| // by the length of data’s buffer in bytes. | |
| const ab = Buffer.from(data, data.byteOffset, data.byteLength) | |
| const frame = new WebsocketFrameSend(ab) | |
| const buffer = frame.createFrame(opcodes.BINARY) | |
| this.#bufferedAmount += ab.byteLength | |
| socket.write(buffer, () => { | |
| this.#bufferedAmount -= ab.byteLength | |
| }) | |
| } else if (isBlobLike(data)) { | |
| // If the WebSocket connection is established, and the WebSocket | |
| // closing handshake has not yet started, then the user agent must | |
| // send a WebSocket Message comprised of data using a binary frame | |
| // opcode; if the data cannot be sent, e.g. because it would need to | |
| // be buffered but the buffer is full, the user agent must flag the | |
| // WebSocket as full and then close the WebSocket connection. The data | |
| // to be sent is the raw data represented by the Blob object. Any | |
| // invocation of this method with a Blob argument that does not throw | |
| // an exception must increase the bufferedAmount attribute by the size | |
| // of the Blob object’s raw data, in bytes. | |
| const frame = new WebsocketFrameSend() | |
| data.arrayBuffer().then((ab) => { | |
| const value = Buffer.from(ab) | |
| frame.frameData = value | |
| const buffer = frame.createFrame(opcodes.BINARY) | |
| this.#bufferedAmount += value.byteLength | |
| socket.write(buffer, () => { | |
| this.#bufferedAmount -= value.byteLength | |
| }) | |
| }) | |
| } | |
| } | |
| get readyState () { | |
| webidl.brandCheck(this, WebSocket) | |
| // The readyState getter steps are to return this's ready state. | |
| return this[kReadyState] | |
| } | |
| get bufferedAmount () { | |
| webidl.brandCheck(this, WebSocket) | |
| return this.#bufferedAmount | |
| } | |
| get url () { | |
| webidl.brandCheck(this, WebSocket) | |
| // The url getter steps are to return this's url, serialized. | |
| return URLSerializer(this[kWebSocketURL]) | |
| } | |
| get extensions () { | |
| webidl.brandCheck(this, WebSocket) | |
| return this.#extensions | |
| } | |
| get protocol () { | |
| webidl.brandCheck(this, WebSocket) | |
| return this.#protocol | |
| } | |
| get onopen () { | |
| webidl.brandCheck(this, WebSocket) | |
| return this.#events.open | |
| } | |
| set onopen (fn) { | |
| webidl.brandCheck(this, WebSocket) | |
| if (this.#events.open) { | |
| this.removeEventListener('open', this.#events.open) | |
| } | |
| if (typeof fn === 'function') { | |
| this.#events.open = fn | |
| this.addEventListener('open', fn) | |
| } else { | |
| this.#events.open = null | |
| } | |
| } | |
| get onerror () { | |
| webidl.brandCheck(this, WebSocket) | |
| return this.#events.error | |
| } | |
| set onerror (fn) { | |
| webidl.brandCheck(this, WebSocket) | |
| if (this.#events.error) { | |
| this.removeEventListener('error', this.#events.error) | |
| } | |
| if (typeof fn === 'function') { | |
| this.#events.error = fn | |
| this.addEventListener('error', fn) | |
| } else { | |
| this.#events.error = null | |
| } | |
| } | |
| get onclose () { | |
| webidl.brandCheck(this, WebSocket) | |
| return this.#events.close | |
| } | |
| set onclose (fn) { | |
| webidl.brandCheck(this, WebSocket) | |
| if (this.#events.close) { | |
| this.removeEventListener('close', this.#events.close) | |
| } | |
| if (typeof fn === 'function') { | |
| this.#events.close = fn | |
| this.addEventListener('close', fn) | |
| } else { | |
| this.#events.close = null | |
| } | |
| } | |
| get onmessage () { | |
| webidl.brandCheck(this, WebSocket) | |
| return this.#events.message | |
| } | |
| set onmessage (fn) { | |
| webidl.brandCheck(this, WebSocket) | |
| if (this.#events.message) { | |
| this.removeEventListener('message', this.#events.message) | |
| } | |
| if (typeof fn === 'function') { | |
| this.#events.message = fn | |
| this.addEventListener('message', fn) | |
| } else { | |
| this.#events.message = null | |
| } | |
| } | |
| get binaryType () { | |
| webidl.brandCheck(this, WebSocket) | |
| return this[kBinaryType] | |
| } | |
| set binaryType (type) { | |
| webidl.brandCheck(this, WebSocket) | |
| if (type !== 'blob' && type !== 'arraybuffer') { | |
| this[kBinaryType] = 'blob' | |
| } else { | |
| this[kBinaryType] = type | |
| } | |
| } | |
| /** | |
| * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol | |
| */ | |
| #onConnectionEstablished (response) { | |
| // processResponse is called when the "response’s header list has been received and initialized." | |
| // once this happens, the connection is open | |
| this[kResponse] = response | |
| const parser = new ByteParser(this) | |
| parser.on('drain', function onParserDrain () { | |
| this.ws[kResponse].socket.resume() | |
| }) | |
| response.socket.ws = this | |
| this[kByteParser] = parser | |
| // 1. Change the ready state to OPEN (1). | |
| this[kReadyState] = states.OPEN | |
| // 2. Change the extensions attribute’s value to the extensions in use, if | |
| // it is not the null value. | |
| // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 | |
| const extensions = response.headersList.get('sec-websocket-extensions') | |
| if (extensions !== null) { | |
| this.#extensions = extensions | |
| } | |
| // 3. Change the protocol attribute’s value to the subprotocol in use, if | |
| // it is not the null value. | |
| // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9 | |
| const protocol = response.headersList.get('sec-websocket-protocol') | |
| if (protocol !== null) { | |
| this.#protocol = protocol | |
| } | |
| // 4. Fire an event named open at the WebSocket object. | |
| fireEvent('open', this) | |
| } | |
| } | |
| // https://websockets.spec.whatwg.org/#dom-websocket-connecting | |
| WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING | |
| // https://websockets.spec.whatwg.org/#dom-websocket-open | |
| WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN | |
| // https://websockets.spec.whatwg.org/#dom-websocket-closing | |
| WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING | |
| // https://websockets.spec.whatwg.org/#dom-websocket-closed | |
| WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED | |
| Object.defineProperties(WebSocket.prototype, { | |
| CONNECTING: staticPropertyDescriptors, | |
| OPEN: staticPropertyDescriptors, | |
| CLOSING: staticPropertyDescriptors, | |
| CLOSED: staticPropertyDescriptors, | |
| url: kEnumerableProperty, | |
| readyState: kEnumerableProperty, | |
| bufferedAmount: kEnumerableProperty, | |
| onopen: kEnumerableProperty, | |
| onerror: kEnumerableProperty, | |
| onclose: kEnumerableProperty, | |
| close: kEnumerableProperty, | |
| onmessage: kEnumerableProperty, | |
| binaryType: kEnumerableProperty, | |
| send: kEnumerableProperty, | |
| extensions: kEnumerableProperty, | |
| protocol: kEnumerableProperty, | |
| [Symbol.toStringTag]: { | |
| value: 'WebSocket', | |
| writable: false, | |
| enumerable: false, | |
| configurable: true | |
| } | |
| }) | |
| Object.defineProperties(WebSocket, { | |
| CONNECTING: staticPropertyDescriptors, | |
| OPEN: staticPropertyDescriptors, | |
| CLOSING: staticPropertyDescriptors, | |
| CLOSED: staticPropertyDescriptors | |
| }) | |
| webidl.converters['sequence<DOMString>'] = webidl.sequenceConverter( | |
| webidl.converters.DOMString | |
| ) | |
| webidl.converters['DOMString or sequence<DOMString>'] = function (V) { | |
| if (webidl.util.Type(V) === 'Object' && Symbol.iterator in V) { | |
| return webidl.converters['sequence<DOMString>'](V) | |
| } | |
| return webidl.converters.DOMString(V) | |
| } | |
| // This implements the propsal made in https://github.com/whatwg/websockets/issues/42 | |
| webidl.converters.WebSocketInit = webidl.dictionaryConverter([ | |
| { | |
| key: 'protocols', | |
| converter: webidl.converters['DOMString or sequence<DOMString>'], | |
| get defaultValue () { | |
| return [] | |
| } | |
| }, | |
| { | |
| key: 'dispatcher', | |
| converter: (V) => V, | |
| get defaultValue () { | |
| return getGlobalDispatcher() | |
| } | |
| }, | |
| { | |
| key: 'headers', | |
| converter: webidl.nullableConverter(webidl.converters.HeadersInit) | |
| } | |
| ]) | |
| webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'] = function (V) { | |
| if (webidl.util.Type(V) === 'Object' && !(Symbol.iterator in V)) { | |
| return webidl.converters.WebSocketInit(V) | |
| } | |
| return { protocols: webidl.converters['DOMString or sequence<DOMString>'](V) } | |
| } | |
| webidl.converters.WebSocketSendData = function (V) { | |
| if (webidl.util.Type(V) === 'Object') { | |
| if (isBlobLike(V)) { | |
| return webidl.converters.Blob(V, { strict: false }) | |
| } | |
| if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) { | |
| return webidl.converters.BufferSource(V) | |
| } | |
| } | |
| return webidl.converters.USVString(V) | |
| } | |
| module.exports = { | |
| WebSocket | |
| } | |