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 | |
} | |