初始化
This commit is contained in:
318
node_modules/undici/lib/web/websocket/connection.js
generated
vendored
Normal file
318
node_modules/undici/lib/web/websocket/connection.js
generated
vendored
Normal file
@@ -0,0 +1,318 @@
|
||||
'use strict'
|
||||
|
||||
const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
|
||||
const { parseExtensions, isClosed, isClosing, isEstablished, validateCloseCodeAndReason } = require('./util')
|
||||
const { makeRequest } = require('../fetch/request')
|
||||
const { fetching } = require('../fetch/index')
|
||||
const { Headers, getHeadersList } = require('../fetch/headers')
|
||||
const { getDecodeSplit } = require('../fetch/util')
|
||||
const { WebsocketFrameSend } = require('./frame')
|
||||
const assert = require('node:assert')
|
||||
|
||||
/** @type {import('crypto')} */
|
||||
let crypto
|
||||
try {
|
||||
crypto = require('node:crypto')
|
||||
/* c8 ignore next 3 */
|
||||
} catch {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://websockets.spec.whatwg.org/#concept-websocket-establish
|
||||
* @param {URL} url
|
||||
* @param {string|string[]} protocols
|
||||
* @param {import('./websocket').Handler} handler
|
||||
* @param {Partial<import('../../../types/websocket').WebSocketInit>} options
|
||||
*/
|
||||
function establishWebSocketConnection (url, protocols, client, handler, options) {
|
||||
// 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s
|
||||
// scheme is "ws", and to "https" otherwise.
|
||||
const requestURL = url
|
||||
|
||||
requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:'
|
||||
|
||||
// 2. Let request be a new request, whose URL is requestURL, client is client,
|
||||
// service-workers mode is "none", referrer is "no-referrer", mode is
|
||||
// "websocket", credentials mode is "include", cache mode is "no-store" ,
|
||||
// and redirect mode is "error".
|
||||
const request = makeRequest({
|
||||
urlList: [requestURL],
|
||||
client,
|
||||
serviceWorkers: 'none',
|
||||
referrer: 'no-referrer',
|
||||
mode: 'websocket',
|
||||
credentials: 'include',
|
||||
cache: 'no-store',
|
||||
redirect: 'error'
|
||||
})
|
||||
|
||||
// Note: undici extension, allow setting custom headers.
|
||||
if (options.headers) {
|
||||
const headersList = getHeadersList(new Headers(options.headers))
|
||||
|
||||
request.headersList = headersList
|
||||
}
|
||||
|
||||
// 3. Append (`Upgrade`, `websocket`) to request’s header list.
|
||||
// 4. Append (`Connection`, `Upgrade`) to request’s header list.
|
||||
// Note: both of these are handled by undici currently.
|
||||
// https://github.com/nodejs/undici/blob/68c269c4144c446f3f1220951338daef4a6b5ec4/lib/client.js#L1397
|
||||
|
||||
// 5. Let keyValue be a nonce consisting of a randomly selected
|
||||
// 16-byte value that has been forgiving-base64-encoded and
|
||||
// isomorphic encoded.
|
||||
const keyValue = crypto.randomBytes(16).toString('base64')
|
||||
|
||||
// 6. Append (`Sec-WebSocket-Key`, keyValue) to request’s
|
||||
// header list.
|
||||
request.headersList.append('sec-websocket-key', keyValue, true)
|
||||
|
||||
// 7. Append (`Sec-WebSocket-Version`, `13`) to request’s
|
||||
// header list.
|
||||
request.headersList.append('sec-websocket-version', '13', true)
|
||||
|
||||
// 8. For each protocol in protocols, combine
|
||||
// (`Sec-WebSocket-Protocol`, protocol) in request’s header
|
||||
// list.
|
||||
for (const protocol of protocols) {
|
||||
request.headersList.append('sec-websocket-protocol', protocol, true)
|
||||
}
|
||||
|
||||
// 9. Let permessageDeflate be a user-agent defined
|
||||
// "permessage-deflate" extension header value.
|
||||
// https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673
|
||||
const permessageDeflate = 'permessage-deflate; client_max_window_bits'
|
||||
|
||||
// 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to
|
||||
// request’s header list.
|
||||
request.headersList.append('sec-websocket-extensions', permessageDeflate, true)
|
||||
|
||||
// 11. Fetch request with useParallelQueue set to true, and
|
||||
// processResponse given response being these steps:
|
||||
const controller = fetching({
|
||||
request,
|
||||
useParallelQueue: true,
|
||||
dispatcher: options.dispatcher,
|
||||
processResponse (response) {
|
||||
if (response.type === 'error') {
|
||||
// If the WebSocket connection could not be established, it is also said
|
||||
// that _The WebSocket Connection is Closed_, but not _cleanly_.
|
||||
handler.readyState = states.CLOSED
|
||||
}
|
||||
|
||||
// 1. If response is a network error or its status is not 101,
|
||||
// fail the WebSocket connection.
|
||||
if (response.type === 'error' || response.status !== 101) {
|
||||
failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. If protocols is not the empty list and extracting header
|
||||
// list values given `Sec-WebSocket-Protocol` and response’s
|
||||
// header list results in null, failure, or the empty byte
|
||||
// sequence, then fail the WebSocket connection.
|
||||
if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) {
|
||||
failWebsocketConnection(handler, 1002, 'Server did not respond with sent protocols.')
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Follow the requirements stated step 2 to step 6, inclusive,
|
||||
// of the last set of steps in section 4.1 of The WebSocket
|
||||
// Protocol to validate response. This either results in fail
|
||||
// the WebSocket connection or the WebSocket connection is
|
||||
// established.
|
||||
|
||||
// 2. If the response lacks an |Upgrade| header field or the |Upgrade|
|
||||
// header field contains a value that is not an ASCII case-
|
||||
// insensitive match for the value "websocket", the client MUST
|
||||
// _Fail the WebSocket Connection_.
|
||||
if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') {
|
||||
failWebsocketConnection(handler, 1002, 'Server did not set Upgrade header to "websocket".')
|
||||
return
|
||||
}
|
||||
|
||||
// 3. If the response lacks a |Connection| header field or the
|
||||
// |Connection| header field doesn't contain a token that is an
|
||||
// ASCII case-insensitive match for the value "Upgrade", the client
|
||||
// MUST _Fail the WebSocket Connection_.
|
||||
if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') {
|
||||
failWebsocketConnection(handler, 1002, 'Server did not set Connection header to "upgrade".')
|
||||
return
|
||||
}
|
||||
|
||||
// 4. If the response lacks a |Sec-WebSocket-Accept| header field or
|
||||
// the |Sec-WebSocket-Accept| contains a value other than the
|
||||
// base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket-
|
||||
// Key| (as a string, not base64-decoded) with the string "258EAFA5-
|
||||
// E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and
|
||||
// trailing whitespace, the client MUST _Fail the WebSocket
|
||||
// Connection_.
|
||||
const secWSAccept = response.headersList.get('Sec-WebSocket-Accept')
|
||||
const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64')
|
||||
if (secWSAccept !== digest) {
|
||||
failWebsocketConnection(handler, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.')
|
||||
return
|
||||
}
|
||||
|
||||
// 5. If the response includes a |Sec-WebSocket-Extensions| header
|
||||
// field and this header field indicates the use of an extension
|
||||
// that was not present in the client's handshake (the server has
|
||||
// indicated an extension not requested by the client), the client
|
||||
// MUST _Fail the WebSocket Connection_. (The parsing of this
|
||||
// header field to determine which extensions are requested is
|
||||
// discussed in Section 9.1.)
|
||||
const secExtension = response.headersList.get('Sec-WebSocket-Extensions')
|
||||
let extensions
|
||||
|
||||
if (secExtension !== null) {
|
||||
extensions = parseExtensions(secExtension)
|
||||
|
||||
if (!extensions.has('permessage-deflate')) {
|
||||
failWebsocketConnection(handler, 1002, 'Sec-WebSocket-Extensions header does not match.')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 6. If the response includes a |Sec-WebSocket-Protocol| header field
|
||||
// and this header field indicates the use of a subprotocol that was
|
||||
// not present in the client's handshake (the server has indicated a
|
||||
// subprotocol not requested by the client), the client MUST _Fail
|
||||
// the WebSocket Connection_.
|
||||
const secProtocol = response.headersList.get('Sec-WebSocket-Protocol')
|
||||
|
||||
if (secProtocol !== null) {
|
||||
const requestProtocols = getDecodeSplit('sec-websocket-protocol', request.headersList)
|
||||
|
||||
// The client can request that the server use a specific subprotocol by
|
||||
// including the |Sec-WebSocket-Protocol| field in its handshake. If it
|
||||
// is specified, the server needs to include the same field and one of
|
||||
// the selected subprotocol values in its response for the connection to
|
||||
// be established.
|
||||
if (!requestProtocols.includes(secProtocol)) {
|
||||
failWebsocketConnection(handler, 1002, 'Protocol was not set in the opening handshake.')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.socket.on('data', handler.onSocketData)
|
||||
response.socket.on('close', handler.onSocketClose)
|
||||
response.socket.on('error', handler.onSocketError)
|
||||
|
||||
handler.wasEverConnected = true
|
||||
handler.onConnectionEstablished(response, extensions)
|
||||
}
|
||||
})
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://whatpr.org/websockets/48.html#close-the-websocket
|
||||
* @param {import('./websocket').Handler} object
|
||||
* @param {number} [code=null]
|
||||
* @param {string} [reason='']
|
||||
*/
|
||||
function closeWebSocketConnection (object, code, reason, validate = false) {
|
||||
// 1. If code was not supplied, let code be null.
|
||||
code ??= null
|
||||
|
||||
// 2. If reason was not supplied, let reason be the empty string.
|
||||
reason ??= ''
|
||||
|
||||
// 3. Validate close code and reason with code and reason.
|
||||
if (validate) validateCloseCodeAndReason(code, reason)
|
||||
|
||||
// 4. Run the first matching steps from the following list:
|
||||
// - If object’s ready state is CLOSING (2) or CLOSED (3)
|
||||
// - If the WebSocket connection is not yet established [WSP]
|
||||
// - If the WebSocket closing handshake has not yet been started [WSP]
|
||||
// - Otherwise
|
||||
if (isClosed(object.readyState) || isClosing(object.readyState)) {
|
||||
// Do nothing.
|
||||
} else if (!isEstablished(object.readyState)) {
|
||||
// Fail the WebSocket connection and set object’s ready state to CLOSING (2). [WSP]
|
||||
failWebsocketConnection(object)
|
||||
object.readyState = states.CLOSING
|
||||
} else if (!object.closeState.has(sentCloseFrameState.SENT) && !object.closeState.has(sentCloseFrameState.RECEIVED)) {
|
||||
// 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.
|
||||
|
||||
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 is null and reason is the empty string, the WebSocket Close frame must not have a body.
|
||||
// If reason is non-empty but code is null, then set code to 1000 ("Normal Closure").
|
||||
if (reason.length !== 0 && code === null) {
|
||||
code = 1000
|
||||
}
|
||||
|
||||
// If code is set, then the status code to use in the WebSocket Close frame must be the integer given by code.
|
||||
assert(code === null || Number.isInteger(code))
|
||||
|
||||
if (code === null && reason.length === 0) {
|
||||
frame.frameData = emptyBuffer
|
||||
} else if (code !== null && reason === null) {
|
||||
frame.frameData = Buffer.allocUnsafe(2)
|
||||
frame.frameData.writeUInt16BE(code, 0)
|
||||
} else if (code !== null && reason !== null) {
|
||||
// If reason is also present, then reasonBytes must be
|
||||
// provided in the Close message after the status code.
|
||||
frame.frameData = Buffer.allocUnsafe(2 + Buffer.byteLength(reason))
|
||||
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
|
||||
}
|
||||
|
||||
object.socket.write(frame.createFrame(opcodes.CLOSE))
|
||||
|
||||
object.closeState.add(sentCloseFrameState.SENT)
|
||||
|
||||
// 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.
|
||||
object.readyState = states.CLOSING
|
||||
} else {
|
||||
// Set object’s ready state to CLOSING (2).
|
||||
object.readyState = states.CLOSING
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./websocket').Handler} handler
|
||||
* @param {number} code
|
||||
* @param {string|undefined} reason
|
||||
* @param {unknown} cause
|
||||
* @returns {void}
|
||||
*/
|
||||
function failWebsocketConnection (handler, code, reason, cause) {
|
||||
// If _The WebSocket Connection is Established_ prior to the point where
|
||||
// the endpoint is required to _Fail the WebSocket Connection_, the
|
||||
// endpoint SHOULD send a Close frame with an appropriate status code
|
||||
// (Section 7.4) before proceeding to _Close the WebSocket Connection_.
|
||||
if (isEstablished(handler.readyState)) {
|
||||
closeWebSocketConnection(handler, code, reason, false)
|
||||
}
|
||||
|
||||
handler.controller.abort()
|
||||
|
||||
if (!handler.socket) {
|
||||
// If the connection was not established, we must still emit an 'error' and 'close' events
|
||||
handler.onSocketClose()
|
||||
} else if (handler.socket.destroyed === false) {
|
||||
handler.socket.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
establishWebSocketConnection,
|
||||
failWebsocketConnection,
|
||||
closeWebSocketConnection
|
||||
}
|
||||
126
node_modules/undici/lib/web/websocket/constants.js
generated
vendored
Normal file
126
node_modules/undici/lib/web/websocket/constants.js
generated
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* This is a Globally Unique Identifier unique used to validate that the
|
||||
* endpoint accepts websocket connections.
|
||||
* @see https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3
|
||||
* @type {'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'}
|
||||
*/
|
||||
const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
||||
|
||||
/**
|
||||
* @type {PropertyDescriptor}
|
||||
*/
|
||||
const staticPropertyDescriptors = {
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
configurable: false
|
||||
}
|
||||
|
||||
/**
|
||||
* The states of the WebSocket connection.
|
||||
*
|
||||
* @readonly
|
||||
* @enum
|
||||
* @property {0} CONNECTING
|
||||
* @property {1} OPEN
|
||||
* @property {2} CLOSING
|
||||
* @property {3} CLOSED
|
||||
*/
|
||||
const states = {
|
||||
CONNECTING: 0,
|
||||
OPEN: 1,
|
||||
CLOSING: 2,
|
||||
CLOSED: 3
|
||||
}
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @enum
|
||||
* @property {0} NOT_SENT
|
||||
* @property {1} PROCESSING
|
||||
* @property {2} SENT
|
||||
*/
|
||||
const sentCloseFrameState = {
|
||||
SENT: 1,
|
||||
RECEIVED: 2
|
||||
}
|
||||
|
||||
/**
|
||||
* The WebSocket opcodes.
|
||||
*
|
||||
* @readonly
|
||||
* @enum
|
||||
* @property {0x0} CONTINUATION
|
||||
* @property {0x1} TEXT
|
||||
* @property {0x2} BINARY
|
||||
* @property {0x8} CLOSE
|
||||
* @property {0x9} PING
|
||||
* @property {0xA} PONG
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
|
||||
*/
|
||||
const opcodes = {
|
||||
CONTINUATION: 0x0,
|
||||
TEXT: 0x1,
|
||||
BINARY: 0x2,
|
||||
CLOSE: 0x8,
|
||||
PING: 0x9,
|
||||
PONG: 0xA
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum value for an unsigned 16-bit integer.
|
||||
*
|
||||
* @type {65535} 2 ** 16 - 1
|
||||
*/
|
||||
const maxUnsigned16Bit = 65535
|
||||
|
||||
/**
|
||||
* The states of the parser.
|
||||
*
|
||||
* @readonly
|
||||
* @enum
|
||||
* @property {0} INFO
|
||||
* @property {2} PAYLOADLENGTH_16
|
||||
* @property {3} PAYLOADLENGTH_64
|
||||
* @property {4} READ_DATA
|
||||
*/
|
||||
const parserStates = {
|
||||
INFO: 0,
|
||||
PAYLOADLENGTH_16: 2,
|
||||
PAYLOADLENGTH_64: 3,
|
||||
READ_DATA: 4
|
||||
}
|
||||
|
||||
/**
|
||||
* An empty buffer.
|
||||
*
|
||||
* @type {Buffer}
|
||||
*/
|
||||
const emptyBuffer = Buffer.allocUnsafe(0)
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @property {1} text
|
||||
* @property {2} typedArray
|
||||
* @property {3} arrayBuffer
|
||||
* @property {4} blob
|
||||
*/
|
||||
const sendHints = {
|
||||
text: 1,
|
||||
typedArray: 2,
|
||||
arrayBuffer: 3,
|
||||
blob: 4
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
uid,
|
||||
sentCloseFrameState,
|
||||
staticPropertyDescriptors,
|
||||
states,
|
||||
opcodes,
|
||||
maxUnsigned16Bit,
|
||||
parserStates,
|
||||
emptyBuffer,
|
||||
sendHints
|
||||
}
|
||||
331
node_modules/undici/lib/web/websocket/events.js
generated
vendored
Normal file
331
node_modules/undici/lib/web/websocket/events.js
generated
vendored
Normal file
@@ -0,0 +1,331 @@
|
||||
'use strict'
|
||||
|
||||
const { webidl } = require('../webidl')
|
||||
const { kEnumerableProperty } = require('../../core/util')
|
||||
const { kConstruct } = require('../../core/symbols')
|
||||
|
||||
/**
|
||||
* @see https://html.spec.whatwg.org/multipage/comms.html#messageevent
|
||||
*/
|
||||
class MessageEvent extends Event {
|
||||
#eventInit
|
||||
|
||||
constructor (type, eventInitDict = {}) {
|
||||
if (type === kConstruct) {
|
||||
super(arguments[1], arguments[2])
|
||||
webidl.util.markAsUncloneable(this)
|
||||
return
|
||||
}
|
||||
|
||||
const prefix = 'MessageEvent constructor'
|
||||
webidl.argumentLengthCheck(arguments, 1, prefix)
|
||||
|
||||
type = webidl.converters.DOMString(type, prefix, 'type')
|
||||
eventInitDict = webidl.converters.MessageEventInit(eventInitDict, prefix, 'eventInitDict')
|
||||
|
||||
super(type, eventInitDict)
|
||||
|
||||
this.#eventInit = eventInitDict
|
||||
webidl.util.markAsUncloneable(this)
|
||||
}
|
||||
|
||||
get data () {
|
||||
webidl.brandCheck(this, MessageEvent)
|
||||
|
||||
return this.#eventInit.data
|
||||
}
|
||||
|
||||
get origin () {
|
||||
webidl.brandCheck(this, MessageEvent)
|
||||
|
||||
return this.#eventInit.origin
|
||||
}
|
||||
|
||||
get lastEventId () {
|
||||
webidl.brandCheck(this, MessageEvent)
|
||||
|
||||
return this.#eventInit.lastEventId
|
||||
}
|
||||
|
||||
get source () {
|
||||
webidl.brandCheck(this, MessageEvent)
|
||||
|
||||
return this.#eventInit.source
|
||||
}
|
||||
|
||||
get ports () {
|
||||
webidl.brandCheck(this, MessageEvent)
|
||||
|
||||
if (!Object.isFrozen(this.#eventInit.ports)) {
|
||||
Object.freeze(this.#eventInit.ports)
|
||||
}
|
||||
|
||||
return this.#eventInit.ports
|
||||
}
|
||||
|
||||
initMessageEvent (
|
||||
type,
|
||||
bubbles = false,
|
||||
cancelable = false,
|
||||
data = null,
|
||||
origin = '',
|
||||
lastEventId = '',
|
||||
source = null,
|
||||
ports = []
|
||||
) {
|
||||
webidl.brandCheck(this, MessageEvent)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, 'MessageEvent.initMessageEvent')
|
||||
|
||||
return new MessageEvent(type, {
|
||||
bubbles, cancelable, data, origin, lastEventId, source, ports
|
||||
})
|
||||
}
|
||||
|
||||
static createFastMessageEvent (type, init) {
|
||||
const messageEvent = new MessageEvent(kConstruct, type, init)
|
||||
messageEvent.#eventInit = init
|
||||
messageEvent.#eventInit.data ??= null
|
||||
messageEvent.#eventInit.origin ??= ''
|
||||
messageEvent.#eventInit.lastEventId ??= ''
|
||||
messageEvent.#eventInit.source ??= null
|
||||
messageEvent.#eventInit.ports ??= []
|
||||
return messageEvent
|
||||
}
|
||||
}
|
||||
|
||||
const { createFastMessageEvent } = MessageEvent
|
||||
delete MessageEvent.createFastMessageEvent
|
||||
|
||||
/**
|
||||
* @see https://websockets.spec.whatwg.org/#the-closeevent-interface
|
||||
*/
|
||||
class CloseEvent extends Event {
|
||||
#eventInit
|
||||
|
||||
constructor (type, eventInitDict = {}) {
|
||||
const prefix = 'CloseEvent constructor'
|
||||
webidl.argumentLengthCheck(arguments, 1, prefix)
|
||||
|
||||
type = webidl.converters.DOMString(type, prefix, 'type')
|
||||
eventInitDict = webidl.converters.CloseEventInit(eventInitDict)
|
||||
|
||||
super(type, eventInitDict)
|
||||
|
||||
this.#eventInit = eventInitDict
|
||||
webidl.util.markAsUncloneable(this)
|
||||
}
|
||||
|
||||
get wasClean () {
|
||||
webidl.brandCheck(this, CloseEvent)
|
||||
|
||||
return this.#eventInit.wasClean
|
||||
}
|
||||
|
||||
get code () {
|
||||
webidl.brandCheck(this, CloseEvent)
|
||||
|
||||
return this.#eventInit.code
|
||||
}
|
||||
|
||||
get reason () {
|
||||
webidl.brandCheck(this, CloseEvent)
|
||||
|
||||
return this.#eventInit.reason
|
||||
}
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface
|
||||
class ErrorEvent extends Event {
|
||||
#eventInit
|
||||
|
||||
constructor (type, eventInitDict) {
|
||||
const prefix = 'ErrorEvent constructor'
|
||||
webidl.argumentLengthCheck(arguments, 1, prefix)
|
||||
|
||||
super(type, eventInitDict)
|
||||
webidl.util.markAsUncloneable(this)
|
||||
|
||||
type = webidl.converters.DOMString(type, prefix, 'type')
|
||||
eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {})
|
||||
|
||||
this.#eventInit = eventInitDict
|
||||
}
|
||||
|
||||
get message () {
|
||||
webidl.brandCheck(this, ErrorEvent)
|
||||
|
||||
return this.#eventInit.message
|
||||
}
|
||||
|
||||
get filename () {
|
||||
webidl.brandCheck(this, ErrorEvent)
|
||||
|
||||
return this.#eventInit.filename
|
||||
}
|
||||
|
||||
get lineno () {
|
||||
webidl.brandCheck(this, ErrorEvent)
|
||||
|
||||
return this.#eventInit.lineno
|
||||
}
|
||||
|
||||
get colno () {
|
||||
webidl.brandCheck(this, ErrorEvent)
|
||||
|
||||
return this.#eventInit.colno
|
||||
}
|
||||
|
||||
get error () {
|
||||
webidl.brandCheck(this, ErrorEvent)
|
||||
|
||||
return this.#eventInit.error
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(MessageEvent.prototype, {
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'MessageEvent',
|
||||
configurable: true
|
||||
},
|
||||
data: kEnumerableProperty,
|
||||
origin: kEnumerableProperty,
|
||||
lastEventId: kEnumerableProperty,
|
||||
source: kEnumerableProperty,
|
||||
ports: kEnumerableProperty,
|
||||
initMessageEvent: kEnumerableProperty
|
||||
})
|
||||
|
||||
Object.defineProperties(CloseEvent.prototype, {
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'CloseEvent',
|
||||
configurable: true
|
||||
},
|
||||
reason: kEnumerableProperty,
|
||||
code: kEnumerableProperty,
|
||||
wasClean: kEnumerableProperty
|
||||
})
|
||||
|
||||
Object.defineProperties(ErrorEvent.prototype, {
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'ErrorEvent',
|
||||
configurable: true
|
||||
},
|
||||
message: kEnumerableProperty,
|
||||
filename: kEnumerableProperty,
|
||||
lineno: kEnumerableProperty,
|
||||
colno: kEnumerableProperty,
|
||||
error: kEnumerableProperty
|
||||
})
|
||||
|
||||
webidl.converters.MessagePort = webidl.interfaceConverter(
|
||||
webidl.is.MessagePort,
|
||||
'MessagePort'
|
||||
)
|
||||
|
||||
webidl.converters['sequence<MessagePort>'] = webidl.sequenceConverter(
|
||||
webidl.converters.MessagePort
|
||||
)
|
||||
|
||||
const eventInit = [
|
||||
{
|
||||
key: 'bubbles',
|
||||
converter: webidl.converters.boolean,
|
||||
defaultValue: () => false
|
||||
},
|
||||
{
|
||||
key: 'cancelable',
|
||||
converter: webidl.converters.boolean,
|
||||
defaultValue: () => false
|
||||
},
|
||||
{
|
||||
key: 'composed',
|
||||
converter: webidl.converters.boolean,
|
||||
defaultValue: () => false
|
||||
}
|
||||
]
|
||||
|
||||
webidl.converters.MessageEventInit = webidl.dictionaryConverter([
|
||||
...eventInit,
|
||||
{
|
||||
key: 'data',
|
||||
converter: webidl.converters.any,
|
||||
defaultValue: () => null
|
||||
},
|
||||
{
|
||||
key: 'origin',
|
||||
converter: webidl.converters.USVString,
|
||||
defaultValue: () => ''
|
||||
},
|
||||
{
|
||||
key: 'lastEventId',
|
||||
converter: webidl.converters.DOMString,
|
||||
defaultValue: () => ''
|
||||
},
|
||||
{
|
||||
key: 'source',
|
||||
// Node doesn't implement WindowProxy or ServiceWorker, so the only
|
||||
// valid value for source is a MessagePort.
|
||||
converter: webidl.nullableConverter(webidl.converters.MessagePort),
|
||||
defaultValue: () => null
|
||||
},
|
||||
{
|
||||
key: 'ports',
|
||||
converter: webidl.converters['sequence<MessagePort>'],
|
||||
defaultValue: () => []
|
||||
}
|
||||
])
|
||||
|
||||
webidl.converters.CloseEventInit = webidl.dictionaryConverter([
|
||||
...eventInit,
|
||||
{
|
||||
key: 'wasClean',
|
||||
converter: webidl.converters.boolean,
|
||||
defaultValue: () => false
|
||||
},
|
||||
{
|
||||
key: 'code',
|
||||
converter: webidl.converters['unsigned short'],
|
||||
defaultValue: () => 0
|
||||
},
|
||||
{
|
||||
key: 'reason',
|
||||
converter: webidl.converters.USVString,
|
||||
defaultValue: () => ''
|
||||
}
|
||||
])
|
||||
|
||||
webidl.converters.ErrorEventInit = webidl.dictionaryConverter([
|
||||
...eventInit,
|
||||
{
|
||||
key: 'message',
|
||||
converter: webidl.converters.DOMString,
|
||||
defaultValue: () => ''
|
||||
},
|
||||
{
|
||||
key: 'filename',
|
||||
converter: webidl.converters.USVString,
|
||||
defaultValue: () => ''
|
||||
},
|
||||
{
|
||||
key: 'lineno',
|
||||
converter: webidl.converters['unsigned long'],
|
||||
defaultValue: () => 0
|
||||
},
|
||||
{
|
||||
key: 'colno',
|
||||
converter: webidl.converters['unsigned long'],
|
||||
defaultValue: () => 0
|
||||
},
|
||||
{
|
||||
key: 'error',
|
||||
converter: webidl.converters.any
|
||||
}
|
||||
])
|
||||
|
||||
module.exports = {
|
||||
MessageEvent,
|
||||
CloseEvent,
|
||||
ErrorEvent,
|
||||
createFastMessageEvent
|
||||
}
|
||||
139
node_modules/undici/lib/web/websocket/frame.js
generated
vendored
Normal file
139
node_modules/undici/lib/web/websocket/frame.js
generated
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
'use strict'
|
||||
|
||||
const { maxUnsigned16Bit, opcodes } = require('./constants')
|
||||
|
||||
const BUFFER_SIZE = 8 * 1024
|
||||
|
||||
/** @type {import('crypto')} */
|
||||
let crypto
|
||||
let buffer = null
|
||||
let bufIdx = BUFFER_SIZE
|
||||
|
||||
try {
|
||||
crypto = require('node:crypto')
|
||||
/* c8 ignore next 3 */
|
||||
} catch {
|
||||
crypto = {
|
||||
// not full compatibility, but minimum.
|
||||
randomFillSync: function randomFillSync (buffer, _offset, _size) {
|
||||
for (let i = 0; i < buffer.length; ++i) {
|
||||
buffer[i] = Math.random() * 255 | 0
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateMask () {
|
||||
if (bufIdx === BUFFER_SIZE) {
|
||||
bufIdx = 0
|
||||
crypto.randomFillSync((buffer ??= Buffer.allocUnsafeSlow(BUFFER_SIZE)), 0, BUFFER_SIZE)
|
||||
}
|
||||
return [buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++]]
|
||||
}
|
||||
|
||||
class WebsocketFrameSend {
|
||||
/**
|
||||
* @param {Buffer|undefined} data
|
||||
*/
|
||||
constructor (data) {
|
||||
this.frameData = data
|
||||
}
|
||||
|
||||
createFrame (opcode) {
|
||||
const frameData = this.frameData
|
||||
const maskKey = generateMask()
|
||||
const bodyLength = frameData?.byteLength ?? 0
|
||||
|
||||
/** @type {number} */
|
||||
let payloadLength = bodyLength // 0-125
|
||||
let offset = 6
|
||||
|
||||
if (bodyLength > maxUnsigned16Bit) {
|
||||
offset += 8 // payload length is next 8 bytes
|
||||
payloadLength = 127
|
||||
} else if (bodyLength > 125) {
|
||||
offset += 2 // payload length is next 2 bytes
|
||||
payloadLength = 126
|
||||
}
|
||||
|
||||
const buffer = Buffer.allocUnsafe(bodyLength + offset)
|
||||
|
||||
// Clear first 2 bytes, everything else is overwritten
|
||||
buffer[0] = buffer[1] = 0
|
||||
buffer[0] |= 0x80 // FIN
|
||||
buffer[0] = (buffer[0] & 0xF0) + opcode // opcode
|
||||
|
||||
/*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */
|
||||
buffer[offset - 4] = maskKey[0]
|
||||
buffer[offset - 3] = maskKey[1]
|
||||
buffer[offset - 2] = maskKey[2]
|
||||
buffer[offset - 1] = maskKey[3]
|
||||
|
||||
buffer[1] = payloadLength
|
||||
|
||||
if (payloadLength === 126) {
|
||||
buffer.writeUInt16BE(bodyLength, 2)
|
||||
} else if (payloadLength === 127) {
|
||||
// Clear extended payload length
|
||||
buffer[2] = buffer[3] = 0
|
||||
buffer.writeUIntBE(bodyLength, 4, 6)
|
||||
}
|
||||
|
||||
buffer[1] |= 0x80 // MASK
|
||||
|
||||
// mask body
|
||||
for (let i = 0; i < bodyLength; ++i) {
|
||||
buffer[offset + i] = frameData[i] ^ maskKey[i & 3]
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} buffer
|
||||
*/
|
||||
static createFastTextFrame (buffer) {
|
||||
const maskKey = generateMask()
|
||||
|
||||
const bodyLength = buffer.length
|
||||
|
||||
// mask body
|
||||
for (let i = 0; i < bodyLength; ++i) {
|
||||
buffer[i] ^= maskKey[i & 3]
|
||||
}
|
||||
|
||||
let payloadLength = bodyLength
|
||||
let offset = 6
|
||||
|
||||
if (bodyLength > maxUnsigned16Bit) {
|
||||
offset += 8 // payload length is next 8 bytes
|
||||
payloadLength = 127
|
||||
} else if (bodyLength > 125) {
|
||||
offset += 2 // payload length is next 2 bytes
|
||||
payloadLength = 126
|
||||
}
|
||||
const head = Buffer.allocUnsafeSlow(offset)
|
||||
|
||||
head[0] = 0x80 /* FIN */ | opcodes.TEXT /* opcode TEXT */
|
||||
head[1] = payloadLength | 0x80 /* MASK */
|
||||
head[offset - 4] = maskKey[0]
|
||||
head[offset - 3] = maskKey[1]
|
||||
head[offset - 2] = maskKey[2]
|
||||
head[offset - 1] = maskKey[3]
|
||||
|
||||
if (payloadLength === 126) {
|
||||
head.writeUInt16BE(bodyLength, 2)
|
||||
} else if (payloadLength === 127) {
|
||||
head[2] = head[3] = 0
|
||||
head.writeUIntBE(bodyLength, 4, 6)
|
||||
}
|
||||
|
||||
return [head, buffer]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
WebsocketFrameSend,
|
||||
generateMask // for benchmark
|
||||
}
|
||||
70
node_modules/undici/lib/web/websocket/permessage-deflate.js
generated
vendored
Normal file
70
node_modules/undici/lib/web/websocket/permessage-deflate.js
generated
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
'use strict'
|
||||
|
||||
const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib')
|
||||
const { isValidClientWindowBits } = require('./util')
|
||||
|
||||
const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
|
||||
const kBuffer = Symbol('kBuffer')
|
||||
const kLength = Symbol('kLength')
|
||||
|
||||
class PerMessageDeflate {
|
||||
/** @type {import('node:zlib').InflateRaw} */
|
||||
#inflate
|
||||
|
||||
#options = {}
|
||||
|
||||
constructor (extensions) {
|
||||
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
|
||||
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
|
||||
}
|
||||
|
||||
decompress (chunk, fin, callback) {
|
||||
// An endpoint uses the following algorithm to decompress a message.
|
||||
// 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
|
||||
// payload of the message.
|
||||
// 2. Decompress the resulting data using DEFLATE.
|
||||
|
||||
if (!this.#inflate) {
|
||||
let windowBits = Z_DEFAULT_WINDOWBITS
|
||||
|
||||
if (this.#options.serverMaxWindowBits) { // empty values default to Z_DEFAULT_WINDOWBITS
|
||||
if (!isValidClientWindowBits(this.#options.serverMaxWindowBits)) {
|
||||
callback(new Error('Invalid server_max_window_bits'))
|
||||
return
|
||||
}
|
||||
|
||||
windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
|
||||
}
|
||||
|
||||
this.#inflate = createInflateRaw({ windowBits })
|
||||
this.#inflate[kBuffer] = []
|
||||
this.#inflate[kLength] = 0
|
||||
|
||||
this.#inflate.on('data', (data) => {
|
||||
this.#inflate[kBuffer].push(data)
|
||||
this.#inflate[kLength] += data.length
|
||||
})
|
||||
|
||||
this.#inflate.on('error', (err) => {
|
||||
this.#inflate = null
|
||||
callback(err)
|
||||
})
|
||||
}
|
||||
|
||||
this.#inflate.write(chunk)
|
||||
if (fin) {
|
||||
this.#inflate.write(tail)
|
||||
}
|
||||
|
||||
this.#inflate.flush(() => {
|
||||
const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
|
||||
|
||||
this.#inflate[kBuffer].length = 0
|
||||
this.#inflate[kLength] = 0
|
||||
|
||||
callback(null, full)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { PerMessageDeflate }
|
||||
444
node_modules/undici/lib/web/websocket/receiver.js
generated
vendored
Normal file
444
node_modules/undici/lib/web/websocket/receiver.js
generated
vendored
Normal file
@@ -0,0 +1,444 @@
|
||||
'use strict'
|
||||
|
||||
const { Writable } = require('node:stream')
|
||||
const assert = require('node:assert')
|
||||
const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
|
||||
const {
|
||||
isValidStatusCode,
|
||||
isValidOpcode,
|
||||
websocketMessageReceived,
|
||||
utf8Decode,
|
||||
isControlFrame,
|
||||
isTextBinaryFrame,
|
||||
isContinuationFrame
|
||||
} = require('./util')
|
||||
const { failWebsocketConnection } = require('./connection')
|
||||
const { WebsocketFrameSend } = require('./frame')
|
||||
const { PerMessageDeflate } = require('./permessage-deflate')
|
||||
|
||||
// This code was influenced by ws released under the MIT license.
|
||||
// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
// Copyright (c) 2013 Arnout Kazemier and contributors
|
||||
// Copyright (c) 2016 Luigi Pinca and contributors
|
||||
|
||||
class ByteParser extends Writable {
|
||||
#buffers = []
|
||||
#fragmentsBytes = 0
|
||||
#byteOffset = 0
|
||||
#loop = false
|
||||
|
||||
#state = parserStates.INFO
|
||||
|
||||
#info = {}
|
||||
#fragments = []
|
||||
|
||||
/** @type {Map<string, PerMessageDeflate>} */
|
||||
#extensions
|
||||
|
||||
/** @type {import('./websocket').Handler} */
|
||||
#handler
|
||||
|
||||
constructor (handler, extensions) {
|
||||
super()
|
||||
|
||||
this.#handler = handler
|
||||
this.#extensions = extensions == null ? new Map() : extensions
|
||||
|
||||
if (this.#extensions.has('permessage-deflate')) {
|
||||
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} chunk
|
||||
* @param {() => void} callback
|
||||
*/
|
||||
_write (chunk, _, callback) {
|
||||
this.#buffers.push(chunk)
|
||||
this.#byteOffset += chunk.length
|
||||
this.#loop = true
|
||||
|
||||
this.run(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs whenever a new chunk is received.
|
||||
* Callback is called whenever there are no more chunks buffering,
|
||||
* or not enough bytes are buffered to parse.
|
||||
*/
|
||||
run (callback) {
|
||||
while (this.#loop) {
|
||||
if (this.#state === parserStates.INFO) {
|
||||
// If there aren't enough bytes to parse the payload length, etc.
|
||||
if (this.#byteOffset < 2) {
|
||||
return callback()
|
||||
}
|
||||
|
||||
const buffer = this.consume(2)
|
||||
const fin = (buffer[0] & 0x80) !== 0
|
||||
const opcode = buffer[0] & 0x0F
|
||||
const masked = (buffer[1] & 0x80) === 0x80
|
||||
|
||||
const fragmented = !fin && opcode !== opcodes.CONTINUATION
|
||||
const payloadLength = buffer[1] & 0x7F
|
||||
|
||||
const rsv1 = buffer[0] & 0x40
|
||||
const rsv2 = buffer[0] & 0x20
|
||||
const rsv3 = buffer[0] & 0x10
|
||||
|
||||
if (!isValidOpcode(opcode)) {
|
||||
failWebsocketConnection(this.#handler, 1002, 'Invalid opcode received')
|
||||
return callback()
|
||||
}
|
||||
|
||||
if (masked) {
|
||||
failWebsocketConnection(this.#handler, 1002, 'Frame cannot be masked')
|
||||
return callback()
|
||||
}
|
||||
|
||||
// MUST be 0 unless an extension is negotiated that defines meanings
|
||||
// for non-zero values. If a nonzero value is received and none of
|
||||
// the negotiated extensions defines the meaning of such a nonzero
|
||||
// value, the receiving endpoint MUST _Fail the WebSocket
|
||||
// Connection_.
|
||||
// This document allocates the RSV1 bit of the WebSocket header for
|
||||
// PMCEs and calls the bit the "Per-Message Compressed" bit. On a
|
||||
// WebSocket connection where a PMCE is in use, this bit indicates
|
||||
// whether a message is compressed or not.
|
||||
if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) {
|
||||
failWebsocketConnection(this.#handler, 1002, 'Expected RSV1 to be clear.')
|
||||
return
|
||||
}
|
||||
|
||||
if (rsv2 !== 0 || rsv3 !== 0) {
|
||||
failWebsocketConnection(this.#handler, 1002, 'RSV1, RSV2, RSV3 must be clear')
|
||||
return
|
||||
}
|
||||
|
||||
if (fragmented && !isTextBinaryFrame(opcode)) {
|
||||
// Only text and binary frames can be fragmented
|
||||
failWebsocketConnection(this.#handler, 1002, 'Invalid frame type was fragmented.')
|
||||
return
|
||||
}
|
||||
|
||||
// If we are already parsing a text/binary frame and do not receive either
|
||||
// a continuation frame or close frame, fail the connection.
|
||||
if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) {
|
||||
failWebsocketConnection(this.#handler, 1002, 'Expected continuation frame')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.#info.fragmented && fragmented) {
|
||||
// A fragmented frame can't be fragmented itself
|
||||
failWebsocketConnection(this.#handler, 1002, 'Fragmented frame exceeded 125 bytes.')
|
||||
return
|
||||
}
|
||||
|
||||
// "All control frames MUST have a payload length of 125 bytes or less
|
||||
// and MUST NOT be fragmented."
|
||||
if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) {
|
||||
failWebsocketConnection(this.#handler, 1002, 'Control frame either too large or fragmented')
|
||||
return
|
||||
}
|
||||
|
||||
if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) {
|
||||
failWebsocketConnection(this.#handler, 1002, 'Unexpected continuation frame')
|
||||
return
|
||||
}
|
||||
|
||||
if (payloadLength <= 125) {
|
||||
this.#info.payloadLength = payloadLength
|
||||
this.#state = parserStates.READ_DATA
|
||||
} else if (payloadLength === 126) {
|
||||
this.#state = parserStates.PAYLOADLENGTH_16
|
||||
} else if (payloadLength === 127) {
|
||||
this.#state = parserStates.PAYLOADLENGTH_64
|
||||
}
|
||||
|
||||
if (isTextBinaryFrame(opcode)) {
|
||||
this.#info.binaryType = opcode
|
||||
this.#info.compressed = rsv1 !== 0
|
||||
}
|
||||
|
||||
this.#info.opcode = opcode
|
||||
this.#info.masked = masked
|
||||
this.#info.fin = fin
|
||||
this.#info.fragmented = fragmented
|
||||
} else if (this.#state === parserStates.PAYLOADLENGTH_16) {
|
||||
if (this.#byteOffset < 2) {
|
||||
return callback()
|
||||
}
|
||||
|
||||
const buffer = this.consume(2)
|
||||
|
||||
this.#info.payloadLength = buffer.readUInt16BE(0)
|
||||
this.#state = parserStates.READ_DATA
|
||||
} else if (this.#state === parserStates.PAYLOADLENGTH_64) {
|
||||
if (this.#byteOffset < 8) {
|
||||
return callback()
|
||||
}
|
||||
|
||||
const buffer = this.consume(8)
|
||||
const upper = buffer.readUInt32BE(0)
|
||||
|
||||
// 2^31 is the maximum bytes an arraybuffer can contain
|
||||
// on 32-bit systems. Although, on 64-bit systems, this is
|
||||
// 2^53-1 bytes.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
|
||||
if (upper > 2 ** 31 - 1) {
|
||||
failWebsocketConnection(this.#handler, 1009, 'Received payload length > 2^31 bytes.')
|
||||
return
|
||||
}
|
||||
|
||||
const lower = buffer.readUInt32BE(4)
|
||||
|
||||
this.#info.payloadLength = (upper << 8) + lower
|
||||
this.#state = parserStates.READ_DATA
|
||||
} else if (this.#state === parserStates.READ_DATA) {
|
||||
if (this.#byteOffset < this.#info.payloadLength) {
|
||||
return callback()
|
||||
}
|
||||
|
||||
const body = this.consume(this.#info.payloadLength)
|
||||
|
||||
if (isControlFrame(this.#info.opcode)) {
|
||||
this.#loop = this.parseControlFrame(body)
|
||||
this.#state = parserStates.INFO
|
||||
} else {
|
||||
if (!this.#info.compressed) {
|
||||
this.writeFragments(body)
|
||||
|
||||
// If the frame is not fragmented, a message has been received.
|
||||
// If the frame is fragmented, it will terminate with a fin bit set
|
||||
// and an opcode of 0 (continuation), therefore we handle that when
|
||||
// parsing continuation frames, not here.
|
||||
if (!this.#info.fragmented && this.#info.fin) {
|
||||
websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
|
||||
}
|
||||
|
||||
this.#state = parserStates.INFO
|
||||
} else {
|
||||
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
|
||||
if (error) {
|
||||
failWebsocketConnection(this.#handler, 1007, error.message)
|
||||
return
|
||||
}
|
||||
|
||||
this.writeFragments(data)
|
||||
|
||||
if (!this.#info.fin) {
|
||||
this.#state = parserStates.INFO
|
||||
this.#loop = true
|
||||
this.run(callback)
|
||||
return
|
||||
}
|
||||
|
||||
websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
|
||||
|
||||
this.#loop = true
|
||||
this.#state = parserStates.INFO
|
||||
this.run(callback)
|
||||
})
|
||||
|
||||
this.#loop = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take n bytes from the buffered Buffers
|
||||
* @param {number} n
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
consume (n) {
|
||||
if (n > this.#byteOffset) {
|
||||
throw new Error('Called consume() before buffers satiated.')
|
||||
} else if (n === 0) {
|
||||
return emptyBuffer
|
||||
}
|
||||
|
||||
this.#byteOffset -= n
|
||||
|
||||
const first = this.#buffers[0]
|
||||
|
||||
if (first.length > n) {
|
||||
// replace with remaining buffer
|
||||
this.#buffers[0] = first.subarray(n, first.length)
|
||||
return first.subarray(0, n)
|
||||
} else if (first.length === n) {
|
||||
// prefect match
|
||||
return this.#buffers.shift()
|
||||
} else {
|
||||
let offset = 0
|
||||
// If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
|
||||
const buffer = Buffer.allocUnsafeSlow(n)
|
||||
while (offset !== n) {
|
||||
const next = this.#buffers[0]
|
||||
const length = next.length
|
||||
|
||||
if (length + offset === n) {
|
||||
buffer.set(this.#buffers.shift(), offset)
|
||||
break
|
||||
} else if (length + offset > n) {
|
||||
buffer.set(next.subarray(0, n - offset), offset)
|
||||
this.#buffers[0] = next.subarray(n - offset)
|
||||
break
|
||||
} else {
|
||||
buffer.set(this.#buffers.shift(), offset)
|
||||
offset += length
|
||||
}
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
|
||||
writeFragments (fragment) {
|
||||
this.#fragmentsBytes += fragment.length
|
||||
this.#fragments.push(fragment)
|
||||
}
|
||||
|
||||
consumeFragments () {
|
||||
const fragments = this.#fragments
|
||||
|
||||
if (fragments.length === 1) {
|
||||
// single fragment
|
||||
this.#fragmentsBytes = 0
|
||||
return fragments.shift()
|
||||
}
|
||||
|
||||
let offset = 0
|
||||
// If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
|
||||
const output = Buffer.allocUnsafeSlow(this.#fragmentsBytes)
|
||||
|
||||
for (let i = 0; i < fragments.length; ++i) {
|
||||
const buffer = fragments[i]
|
||||
output.set(buffer, offset)
|
||||
offset += buffer.length
|
||||
}
|
||||
|
||||
this.#fragments = []
|
||||
this.#fragmentsBytes = 0
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
parseCloseBody (data) {
|
||||
assert(data.length !== 1)
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
|
||||
/** @type {number|undefined} */
|
||||
let code
|
||||
|
||||
if (data.length >= 2) {
|
||||
// _The WebSocket Connection Close Code_ is
|
||||
// defined as the status code (Section 7.4) contained in the first Close
|
||||
// control frame received by the application
|
||||
code = data.readUInt16BE(0)
|
||||
}
|
||||
|
||||
if (code !== undefined && !isValidStatusCode(code)) {
|
||||
return { code: 1002, reason: 'Invalid status code', error: true }
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6
|
||||
/** @type {Buffer} */
|
||||
let reason = data.subarray(2)
|
||||
|
||||
// Remove BOM
|
||||
if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) {
|
||||
reason = reason.subarray(3)
|
||||
}
|
||||
|
||||
try {
|
||||
reason = utf8Decode(reason)
|
||||
} catch {
|
||||
return { code: 1007, reason: 'Invalid UTF-8', error: true }
|
||||
}
|
||||
|
||||
return { code, reason, error: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses control frames.
|
||||
* @param {Buffer} body
|
||||
*/
|
||||
parseControlFrame (body) {
|
||||
const { opcode, payloadLength } = this.#info
|
||||
|
||||
if (opcode === opcodes.CLOSE) {
|
||||
if (payloadLength === 1) {
|
||||
failWebsocketConnection(this.#handler, 1002, 'Received close frame with a 1-byte body.')
|
||||
return false
|
||||
}
|
||||
|
||||
this.#info.closeInfo = this.parseCloseBody(body)
|
||||
|
||||
if (this.#info.closeInfo.error) {
|
||||
const { code, reason } = this.#info.closeInfo
|
||||
|
||||
failWebsocketConnection(this.#handler, code, reason)
|
||||
return false
|
||||
}
|
||||
|
||||
// Upon receiving such a frame, the other peer sends a
|
||||
// Close frame in response, if it hasn't already sent one.
|
||||
if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
|
||||
// If an endpoint receives a Close frame and did not previously send a
|
||||
// Close frame, the endpoint MUST send a Close frame in response. (When
|
||||
// sending a Close frame in response, the endpoint typically echos the
|
||||
// status code it received.)
|
||||
let body = emptyBuffer
|
||||
if (this.#info.closeInfo.code) {
|
||||
body = Buffer.allocUnsafe(2)
|
||||
body.writeUInt16BE(this.#info.closeInfo.code, 0)
|
||||
}
|
||||
const closeFrame = new WebsocketFrameSend(body)
|
||||
|
||||
this.#handler.socket.write(closeFrame.createFrame(opcodes.CLOSE))
|
||||
this.#handler.closeState.add(sentCloseFrameState.SENT)
|
||||
}
|
||||
|
||||
// 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.#handler.readyState = states.CLOSING
|
||||
this.#handler.closeState.add(sentCloseFrameState.RECEIVED)
|
||||
|
||||
return false
|
||||
} else if (opcode === opcodes.PING) {
|
||||
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
|
||||
// response, unless it already received a Close frame.
|
||||
// A Pong frame sent in response to a Ping frame must have identical
|
||||
// "Application data"
|
||||
|
||||
if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
|
||||
const frame = new WebsocketFrameSend(body)
|
||||
|
||||
this.#handler.socket.write(frame.createFrame(opcodes.PONG))
|
||||
|
||||
this.#handler.onPing(body)
|
||||
}
|
||||
} else if (opcode === opcodes.PONG) {
|
||||
// A Pong frame MAY be sent unsolicited. This serves as a
|
||||
// unidirectional heartbeat. A response to an unsolicited Pong frame is
|
||||
// not expected.
|
||||
this.#handler.onPong(body)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
get closingInfo () {
|
||||
return this.#info.closeInfo
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ByteParser
|
||||
}
|
||||
109
node_modules/undici/lib/web/websocket/sender.js
generated
vendored
Normal file
109
node_modules/undici/lib/web/websocket/sender.js
generated
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
'use strict'
|
||||
|
||||
const { WebsocketFrameSend } = require('./frame')
|
||||
const { opcodes, sendHints } = require('./constants')
|
||||
const FixedQueue = require('../../dispatcher/fixed-queue')
|
||||
|
||||
/**
|
||||
* @typedef {object} SendQueueNode
|
||||
* @property {Promise<void> | null} promise
|
||||
* @property {((...args: any[]) => any)} callback
|
||||
* @property {Buffer | null} frame
|
||||
*/
|
||||
|
||||
class SendQueue {
|
||||
/**
|
||||
* @type {FixedQueue}
|
||||
*/
|
||||
#queue = new FixedQueue()
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
#running = false
|
||||
|
||||
/** @type {import('node:net').Socket} */
|
||||
#socket
|
||||
|
||||
constructor (socket) {
|
||||
this.#socket = socket
|
||||
}
|
||||
|
||||
add (item, cb, hint) {
|
||||
if (hint !== sendHints.blob) {
|
||||
if (!this.#running) {
|
||||
// TODO(@tsctx): support fast-path for string on running
|
||||
if (hint === sendHints.text) {
|
||||
// special fast-path for string
|
||||
const { 0: head, 1: body } = WebsocketFrameSend.createFastTextFrame(item)
|
||||
this.#socket.cork()
|
||||
this.#socket.write(head)
|
||||
this.#socket.write(body, cb)
|
||||
this.#socket.uncork()
|
||||
} else {
|
||||
// direct writing
|
||||
this.#socket.write(createFrame(item, hint), cb)
|
||||
}
|
||||
} else {
|
||||
/** @type {SendQueueNode} */
|
||||
const node = {
|
||||
promise: null,
|
||||
callback: cb,
|
||||
frame: createFrame(item, hint)
|
||||
}
|
||||
this.#queue.push(node)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/** @type {SendQueueNode} */
|
||||
const node = {
|
||||
promise: item.arrayBuffer().then((ab) => {
|
||||
node.promise = null
|
||||
node.frame = createFrame(ab, hint)
|
||||
}),
|
||||
callback: cb,
|
||||
frame: null
|
||||
}
|
||||
|
||||
this.#queue.push(node)
|
||||
|
||||
if (!this.#running) {
|
||||
this.#run()
|
||||
}
|
||||
}
|
||||
|
||||
async #run () {
|
||||
this.#running = true
|
||||
const queue = this.#queue
|
||||
while (!queue.isEmpty()) {
|
||||
const node = queue.shift()
|
||||
// wait pending promise
|
||||
if (node.promise !== null) {
|
||||
await node.promise
|
||||
}
|
||||
// write
|
||||
this.#socket.write(node.frame, node.callback)
|
||||
// cleanup
|
||||
node.callback = node.frame = null
|
||||
}
|
||||
this.#running = false
|
||||
}
|
||||
}
|
||||
|
||||
function createFrame (data, hint) {
|
||||
return new WebsocketFrameSend(toBuffer(data, hint)).createFrame(hint === sendHints.text ? opcodes.TEXT : opcodes.BINARY)
|
||||
}
|
||||
|
||||
function toBuffer (data, hint) {
|
||||
switch (hint) {
|
||||
case sendHints.text:
|
||||
case sendHints.typedArray:
|
||||
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
||||
case sendHints.arrayBuffer:
|
||||
case sendHints.blob:
|
||||
return new Uint8Array(data)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SendQueue }
|
||||
104
node_modules/undici/lib/web/websocket/stream/websocketerror.js
generated
vendored
Normal file
104
node_modules/undici/lib/web/websocket/stream/websocketerror.js
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
'use strict'
|
||||
|
||||
const { webidl } = require('../../webidl')
|
||||
const { validateCloseCodeAndReason } = require('../util')
|
||||
const { kConstruct } = require('../../../core/symbols')
|
||||
const { kEnumerableProperty } = require('../../../core/util')
|
||||
|
||||
function createInheritableDOMException () {
|
||||
// https://github.com/nodejs/node/issues/59677
|
||||
class Test extends DOMException {
|
||||
get reason () {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
if (new Test().reason !== undefined) {
|
||||
return DOMException
|
||||
}
|
||||
|
||||
return new Proxy(DOMException, {
|
||||
construct (target, args, newTarget) {
|
||||
const instance = Reflect.construct(target, args, target)
|
||||
Object.setPrototypeOf(instance, newTarget.prototype)
|
||||
return instance
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
class WebSocketError extends createInheritableDOMException() {
|
||||
#closeCode
|
||||
#reason
|
||||
|
||||
constructor (message = '', init = undefined) {
|
||||
message = webidl.converters.DOMString(message, 'WebSocketError', 'message')
|
||||
|
||||
// 1. Set this 's name to " WebSocketError ".
|
||||
// 2. Set this 's message to message .
|
||||
super(message, 'WebSocketError')
|
||||
|
||||
if (init === kConstruct) {
|
||||
return
|
||||
} else if (init !== null) {
|
||||
init = webidl.converters.WebSocketCloseInfo(init)
|
||||
}
|
||||
|
||||
// 3. Let code be init [" closeCode "] if it exists , or null otherwise.
|
||||
let code = init.closeCode ?? null
|
||||
|
||||
// 4. Let reason be init [" reason "] if it exists , or the empty string otherwise.
|
||||
const reason = init.reason ?? ''
|
||||
|
||||
// 5. Validate close code and reason with code and reason .
|
||||
validateCloseCodeAndReason(code, reason)
|
||||
|
||||
// 6. If reason is non-empty, but code is not set, then set code to 1000 ("Normal Closure").
|
||||
if (reason.length !== 0 && code === null) {
|
||||
code = 1000
|
||||
}
|
||||
|
||||
// 7. Set this 's closeCode to code .
|
||||
this.#closeCode = code
|
||||
|
||||
// 8. Set this 's reason to reason .
|
||||
this.#reason = reason
|
||||
}
|
||||
|
||||
get closeCode () {
|
||||
return this.#closeCode
|
||||
}
|
||||
|
||||
get reason () {
|
||||
return this.#reason
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {number|null} code
|
||||
* @param {string} reason
|
||||
*/
|
||||
static createUnvalidatedWebSocketError (message, code, reason) {
|
||||
const error = new WebSocketError(message, kConstruct)
|
||||
error.#closeCode = code
|
||||
error.#reason = reason
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
const { createUnvalidatedWebSocketError } = WebSocketError
|
||||
delete WebSocketError.createUnvalidatedWebSocketError
|
||||
|
||||
Object.defineProperties(WebSocketError.prototype, {
|
||||
closeCode: kEnumerableProperty,
|
||||
reason: kEnumerableProperty,
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'WebSocketError',
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
}
|
||||
})
|
||||
|
||||
webidl.is.WebSocketError = webidl.util.MakeTypeAssertion(WebSocketError)
|
||||
|
||||
module.exports = { WebSocketError, createUnvalidatedWebSocketError }
|
||||
497
node_modules/undici/lib/web/websocket/stream/websocketstream.js
generated
vendored
Normal file
497
node_modules/undici/lib/web/websocket/stream/websocketstream.js
generated
vendored
Normal file
@@ -0,0 +1,497 @@
|
||||
'use strict'
|
||||
|
||||
const { createDeferredPromise } = require('../../../util/promise')
|
||||
const { environmentSettingsObject } = require('../../fetch/util')
|
||||
const { states, opcodes, sentCloseFrameState } = require('../constants')
|
||||
const { webidl } = require('../../webidl')
|
||||
const { getURLRecord, isValidSubprotocol, isEstablished, utf8Decode } = require('../util')
|
||||
const { establishWebSocketConnection, failWebsocketConnection, closeWebSocketConnection } = require('../connection')
|
||||
const { channels } = require('../../../core/diagnostics')
|
||||
const { WebsocketFrameSend } = require('../frame')
|
||||
const { ByteParser } = require('../receiver')
|
||||
const { WebSocketError, createUnvalidatedWebSocketError } = require('./websocketerror')
|
||||
const { utf8DecodeBytes } = require('../../fetch/util')
|
||||
const { kEnumerableProperty } = require('../../../core/util')
|
||||
|
||||
let emittedExperimentalWarning = false
|
||||
|
||||
class WebSocketStream {
|
||||
// Each WebSocketStream object has an associated url , which is a URL record .
|
||||
/** @type {URL} */
|
||||
#url
|
||||
|
||||
// Each WebSocketStream object has an associated opened promise , which is a promise.
|
||||
/** @type {import('../../../util/promise').DeferredPromise} */
|
||||
#openedPromise
|
||||
|
||||
// Each WebSocketStream object has an associated closed promise , which is a promise.
|
||||
/** @type {import('../../../util/promise').DeferredPromise} */
|
||||
#closedPromise
|
||||
|
||||
// Each WebSocketStream object has an associated readable stream , which is a ReadableStream .
|
||||
/** @type {ReadableStream} */
|
||||
#readableStream
|
||||
/** @type {ReadableStreamDefaultController} */
|
||||
#readableStreamController
|
||||
|
||||
// Each WebSocketStream object has an associated writable stream , which is a WritableStream .
|
||||
/** @type {WritableStream} */
|
||||
#writableStream
|
||||
|
||||
// Each WebSocketStream object has an associated boolean handshake aborted , which is initially false.
|
||||
#handshakeAborted = false
|
||||
|
||||
/** @type {import('../websocket').Handler} */
|
||||
#handler = {
|
||||
// https://whatpr.org/websockets/48/7b748d3...d5570f3.html#feedback-to-websocket-stream-from-the-protocol
|
||||
onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
|
||||
onMessage: (opcode, data) => this.#onMessage(opcode, data),
|
||||
onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
|
||||
onParserDrain: () => this.#handler.socket.resume(),
|
||||
onSocketData: (chunk) => {
|
||||
if (!this.#parser.write(chunk)) {
|
||||
this.#handler.socket.pause()
|
||||
}
|
||||
},
|
||||
onSocketError: (err) => {
|
||||
this.#handler.readyState = states.CLOSING
|
||||
|
||||
if (channels.socketError.hasSubscribers) {
|
||||
channels.socketError.publish(err)
|
||||
}
|
||||
|
||||
this.#handler.socket.destroy()
|
||||
},
|
||||
onSocketClose: () => this.#onSocketClose(),
|
||||
onPing: () => {},
|
||||
onPong: () => {},
|
||||
|
||||
readyState: states.CONNECTING,
|
||||
socket: null,
|
||||
closeState: new Set(),
|
||||
controller: null,
|
||||
wasEverConnected: false
|
||||
}
|
||||
|
||||
/** @type {import('../receiver').ByteParser} */
|
||||
#parser
|
||||
|
||||
constructor (url, options = undefined) {
|
||||
if (!emittedExperimentalWarning) {
|
||||
process.emitWarning('WebSocketStream is experimental! Expect it to change at any time.', {
|
||||
code: 'UNDICI-WSS'
|
||||
})
|
||||
emittedExperimentalWarning = true
|
||||
}
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, 'WebSocket')
|
||||
|
||||
url = webidl.converters.USVString(url)
|
||||
if (options !== null) {
|
||||
options = webidl.converters.WebSocketStreamOptions(options)
|
||||
}
|
||||
|
||||
// 1. Let baseURL be this 's relevant settings object 's API base URL .
|
||||
const baseURL = environmentSettingsObject.settingsObject.baseUrl
|
||||
|
||||
// 2. Let urlRecord be the result of getting a URL record given url and baseURL .
|
||||
const urlRecord = getURLRecord(url, baseURL)
|
||||
|
||||
// 3. Let protocols be options [" protocols "] if it exists , otherwise an empty sequence.
|
||||
const protocols = options.protocols
|
||||
|
||||
// 4. 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 . [WSP]
|
||||
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')
|
||||
}
|
||||
|
||||
// 5. Set this 's url to urlRecord .
|
||||
this.#url = urlRecord.toString()
|
||||
|
||||
// 6. Set this 's opened promise and closed promise to new promises.
|
||||
this.#openedPromise = createDeferredPromise()
|
||||
this.#closedPromise = createDeferredPromise()
|
||||
|
||||
// 7. Apply backpressure to the WebSocket.
|
||||
// TODO
|
||||
|
||||
// 8. If options [" signal "] exists ,
|
||||
if (options.signal != null) {
|
||||
// 8.1. Let signal be options [" signal "].
|
||||
const signal = options.signal
|
||||
|
||||
// 8.2. If signal is aborted , then reject this 's opened promise and closed promise with signal ’s abort reason
|
||||
// and return.
|
||||
if (signal.aborted) {
|
||||
this.#openedPromise.reject(signal.reason)
|
||||
this.#closedPromise.reject(signal.reason)
|
||||
return
|
||||
}
|
||||
|
||||
// 8.3. Add the following abort steps to signal :
|
||||
signal.addEventListener('abort', () => {
|
||||
// 8.3.1. If the WebSocket connection is not yet established : [WSP]
|
||||
if (!isEstablished(this.#handler.readyState)) {
|
||||
// 8.3.1.1. Fail the WebSocket connection .
|
||||
failWebsocketConnection(this.#handler)
|
||||
|
||||
// Set this 's ready state to CLOSING .
|
||||
this.#handler.readyState = states.CLOSING
|
||||
|
||||
// Reject this 's opened promise and closed promise with signal ’s abort reason .
|
||||
this.#openedPromise.reject(signal.reason)
|
||||
this.#closedPromise.reject(signal.reason)
|
||||
|
||||
// Set this 's handshake aborted to true.
|
||||
this.#handshakeAborted = true
|
||||
}
|
||||
}, { once: true })
|
||||
}
|
||||
|
||||
// 9. Let client be this 's relevant settings object .
|
||||
const client = environmentSettingsObject.settingsObject
|
||||
|
||||
// 10. Run this step in parallel :
|
||||
// 10.1. Establish a WebSocket connection given urlRecord , protocols , and client . [FETCH]
|
||||
this.#handler.controller = establishWebSocketConnection(
|
||||
urlRecord,
|
||||
protocols,
|
||||
client,
|
||||
this.#handler,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
// The url getter steps are to return this 's url , serialized .
|
||||
get url () {
|
||||
return this.#url.toString()
|
||||
}
|
||||
|
||||
// The opened getter steps are to return this 's opened promise .
|
||||
get opened () {
|
||||
return this.#openedPromise.promise
|
||||
}
|
||||
|
||||
// The closed getter steps are to return this 's closed promise .
|
||||
get closed () {
|
||||
return this.#closedPromise.promise
|
||||
}
|
||||
|
||||
// The close( closeInfo ) method steps are:
|
||||
close (closeInfo = undefined) {
|
||||
if (closeInfo !== null) {
|
||||
closeInfo = webidl.converters.WebSocketCloseInfo(closeInfo)
|
||||
}
|
||||
|
||||
// 1. Let code be closeInfo [" closeCode "] if present, or null otherwise.
|
||||
const code = closeInfo.closeCode ?? null
|
||||
|
||||
// 2. Let reason be closeInfo [" reason "].
|
||||
const reason = closeInfo.reason
|
||||
|
||||
// 3. Close the WebSocket with this , code , and reason .
|
||||
closeWebSocketConnection(this.#handler, code, reason, true)
|
||||
}
|
||||
|
||||
#write (chunk) {
|
||||
// See /websockets/stream/tentative/write.any.html
|
||||
chunk = webidl.converters.WebSocketStreamWrite(chunk)
|
||||
|
||||
// 1. Let promise be a new promise created in stream ’s relevant realm .
|
||||
const promise = createDeferredPromise()
|
||||
|
||||
// 2. Let data be null.
|
||||
let data = null
|
||||
|
||||
// 3. Let opcode be null.
|
||||
let opcode = null
|
||||
|
||||
// 4. If chunk is a BufferSource ,
|
||||
if (webidl.is.BufferSource(chunk)) {
|
||||
// 4.1. Set data to a copy of the bytes given chunk .
|
||||
data = new Uint8Array(ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) : chunk.slice())
|
||||
|
||||
// 4.2. Set opcode to a binary frame opcode.
|
||||
opcode = opcodes.BINARY
|
||||
} else {
|
||||
// 5. Otherwise,
|
||||
|
||||
// 5.1. Let string be the result of converting chunk to an IDL USVString .
|
||||
// If this throws an exception, return a promise rejected with the exception.
|
||||
let string
|
||||
|
||||
try {
|
||||
string = webidl.converters.DOMString(chunk)
|
||||
} catch (e) {
|
||||
promise.reject(e)
|
||||
return promise.promise
|
||||
}
|
||||
|
||||
// 5.2. Set data to the result of UTF-8 encoding string .
|
||||
data = new TextEncoder().encode(string)
|
||||
|
||||
// 5.3. Set opcode to a text frame opcode.
|
||||
opcode = opcodes.TEXT
|
||||
}
|
||||
|
||||
// 6. In parallel,
|
||||
// 6.1. Wait until there is sufficient buffer space in stream to send the message.
|
||||
|
||||
// 6.2. If the closing handshake has not yet started , Send a WebSocket Message to stream comprised of data using opcode .
|
||||
if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
|
||||
const frame = new WebsocketFrameSend(data)
|
||||
|
||||
this.#handler.socket.write(frame.createFrame(opcode), () => {
|
||||
promise.resolve(undefined)
|
||||
})
|
||||
}
|
||||
|
||||
// 6.3. Queue a global task on the WebSocket task source given stream ’s relevant global object to resolve promise with undefined.
|
||||
return promise.promise
|
||||
}
|
||||
|
||||
/** @type {import('../websocket').Handler['onConnectionEstablished']} */
|
||||
#onConnectionEstablished (response, parsedExtensions) {
|
||||
this.#handler.socket = response.socket
|
||||
|
||||
const parser = new ByteParser(this.#handler, parsedExtensions)
|
||||
parser.on('drain', () => this.#handler.onParserDrain())
|
||||
parser.on('error', (err) => this.#handler.onParserError(err))
|
||||
|
||||
this.#parser = parser
|
||||
|
||||
// 1. Change stream ’s ready state to OPEN (1).
|
||||
this.#handler.readyState = states.OPEN
|
||||
|
||||
// 2. Set stream ’s was ever connected to true.
|
||||
// This is done in the opening handshake.
|
||||
|
||||
// 3. Let extensions be the extensions in use .
|
||||
const extensions = parsedExtensions ?? ''
|
||||
|
||||
// 4. Let protocol be the subprotocol in use .
|
||||
const protocol = response.headersList.get('sec-websocket-protocol') ?? ''
|
||||
|
||||
// 5. Let pullAlgorithm be an action that pulls bytes from stream .
|
||||
// 6. Let cancelAlgorithm be an action that cancels stream with reason , given reason .
|
||||
// 7. Let readable be a new ReadableStream .
|
||||
// 8. Set up readable with pullAlgorithm and cancelAlgorithm .
|
||||
const readable = new ReadableStream({
|
||||
start: (controller) => {
|
||||
this.#readableStreamController = controller
|
||||
},
|
||||
pull (controller) {
|
||||
let chunk
|
||||
while (controller.desiredSize > 0 && (chunk = response.socket.read()) !== null) {
|
||||
controller.enqueue(chunk)
|
||||
}
|
||||
},
|
||||
cancel: (reason) => this.#cancel(reason)
|
||||
})
|
||||
|
||||
// 9. Let writeAlgorithm be an action that writes chunk to stream , given chunk .
|
||||
// 10. Let closeAlgorithm be an action that closes stream .
|
||||
// 11. Let abortAlgorithm be an action that aborts stream with reason , given reason .
|
||||
// 12. Let writable be a new WritableStream .
|
||||
// 13. Set up writable with writeAlgorithm , closeAlgorithm , and abortAlgorithm .
|
||||
const writable = new WritableStream({
|
||||
write: (chunk) => this.#write(chunk),
|
||||
close: () => closeWebSocketConnection(this.#handler, null, null),
|
||||
abort: (reason) => this.#closeUsingReason(reason)
|
||||
})
|
||||
|
||||
// Set stream ’s readable stream to readable .
|
||||
this.#readableStream = readable
|
||||
|
||||
// Set stream ’s writable stream to writable .
|
||||
this.#writableStream = writable
|
||||
|
||||
// Resolve stream ’s opened promise with WebSocketOpenInfo «[ " extensions " → extensions , " protocol " → protocol , " readable " → readable , " writable " → writable ]».
|
||||
this.#openedPromise.resolve({
|
||||
extensions,
|
||||
protocol,
|
||||
readable,
|
||||
writable
|
||||
})
|
||||
}
|
||||
|
||||
/** @type {import('../websocket').Handler['onMessage']} */
|
||||
#onMessage (type, data) {
|
||||
// 1. If stream’s ready state is not OPEN (1), then return.
|
||||
if (this.#handler.readyState !== states.OPEN) {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Let chunk be determined by switching on type:
|
||||
// - type indicates that the data is Text
|
||||
// a new DOMString containing data
|
||||
// - type indicates that the data is Binary
|
||||
// a new Uint8Array object, created in the relevant Realm of the
|
||||
// WebSocketStream object, whose contents are data
|
||||
let chunk
|
||||
|
||||
if (type === opcodes.TEXT) {
|
||||
try {
|
||||
chunk = utf8Decode(data)
|
||||
} catch {
|
||||
failWebsocketConnection(this.#handler, 'Received invalid UTF-8 in text frame.')
|
||||
return
|
||||
}
|
||||
} else if (type === opcodes.BINARY) {
|
||||
chunk = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
||||
}
|
||||
|
||||
// 3. Enqueue chunk into stream’s readable stream.
|
||||
this.#readableStreamController.enqueue(chunk)
|
||||
|
||||
// 4. Apply backpressure to the WebSocket.
|
||||
}
|
||||
|
||||
/** @type {import('../websocket').Handler['onSocketClose']} */
|
||||
#onSocketClose () {
|
||||
const wasClean =
|
||||
this.#handler.closeState.has(sentCloseFrameState.SENT) &&
|
||||
this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
|
||||
|
||||
// 1. Change the ready state to CLOSED (3).
|
||||
this.#handler.readyState = states.CLOSED
|
||||
|
||||
// 2. If stream ’s handshake aborted is true, then return.
|
||||
if (this.#handshakeAborted) {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. If stream ’s was ever connected is false, then reject stream ’s opened promise with a new WebSocketError.
|
||||
if (!this.#handler.wasEverConnected) {
|
||||
this.#openedPromise.reject(new WebSocketError('Socket never opened'))
|
||||
}
|
||||
|
||||
const result = this.#parser.closingInfo
|
||||
|
||||
// 4. Let code be the WebSocket connection close code .
|
||||
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
|
||||
// If this Close control frame contains no status code, _The WebSocket
|
||||
// Connection Close Code_ is considered to be 1005. If _The WebSocket
|
||||
// Connection is Closed_ and no Close control frame was received by the
|
||||
// endpoint (such as could occur if the underlying transport connection
|
||||
// is lost), _The WebSocket Connection Close Code_ is considered to be
|
||||
// 1006.
|
||||
let code = result?.code ?? 1005
|
||||
|
||||
if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
|
||||
code = 1006
|
||||
}
|
||||
|
||||
// 5. Let reason be the result of applying UTF-8 decode without BOM to the WebSocket connection close reason .
|
||||
const reason = result?.reason == null ? '' : utf8DecodeBytes(Buffer.from(result.reason))
|
||||
|
||||
// 6. If the connection was closed cleanly ,
|
||||
if (wasClean) {
|
||||
// 6.1. Close stream ’s readable stream .
|
||||
this.#readableStreamController.close()
|
||||
|
||||
// 6.2. Error stream ’s writable stream with an " InvalidStateError " DOMException indicating that a closed WebSocketStream cannot be written to.
|
||||
if (!this.#writableStream.locked) {
|
||||
this.#writableStream.abort(new DOMException('A closed WebSocketStream cannot be written to', 'InvalidStateError'))
|
||||
}
|
||||
|
||||
// 6.3. Resolve stream ’s closed promise with WebSocketCloseInfo «[ " closeCode " → code , " reason " → reason ]».
|
||||
this.#closedPromise.resolve({
|
||||
closeCode: code,
|
||||
reason
|
||||
})
|
||||
} else {
|
||||
// 7. Otherwise,
|
||||
|
||||
// 7.1. Let error be a new WebSocketError whose closeCode is code and reason is reason .
|
||||
const error = createUnvalidatedWebSocketError('unclean close', code, reason)
|
||||
|
||||
// 7.2. Error stream ’s readable stream with error .
|
||||
this.#readableStreamController.error(error)
|
||||
|
||||
// 7.3. Error stream ’s writable stream with error .
|
||||
this.#writableStream.abort(error)
|
||||
|
||||
// 7.4. Reject stream ’s closed promise with error .
|
||||
this.#closedPromise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
#closeUsingReason (reason) {
|
||||
// 1. Let code be null.
|
||||
let code = null
|
||||
|
||||
// 2. Let reasonString be the empty string.
|
||||
let reasonString = ''
|
||||
|
||||
// 3. If reason implements WebSocketError ,
|
||||
if (webidl.is.WebSocketError(reason)) {
|
||||
// 3.1. Set code to reason ’s closeCode .
|
||||
code = reason.closeCode
|
||||
|
||||
// 3.2. Set reasonString to reason ’s reason .
|
||||
reasonString = reason.reason
|
||||
}
|
||||
|
||||
// 4. Close the WebSocket with stream , code , and reasonString . If this throws an exception,
|
||||
// discard code and reasonString and close the WebSocket with stream .
|
||||
closeWebSocketConnection(this.#handler, code, reasonString)
|
||||
}
|
||||
|
||||
// To cancel a WebSocketStream stream given reason , close using reason giving stream and reason .
|
||||
#cancel (reason) {
|
||||
this.#closeUsingReason(reason)
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(WebSocketStream.prototype, {
|
||||
url: kEnumerableProperty,
|
||||
opened: kEnumerableProperty,
|
||||
closed: kEnumerableProperty,
|
||||
close: kEnumerableProperty,
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'WebSocketStream',
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
}
|
||||
})
|
||||
|
||||
webidl.converters.WebSocketStreamOptions = webidl.dictionaryConverter([
|
||||
{
|
||||
key: 'protocols',
|
||||
converter: webidl.sequenceConverter(webidl.converters.USVString),
|
||||
defaultValue: () => []
|
||||
},
|
||||
{
|
||||
key: 'signal',
|
||||
converter: webidl.nullableConverter(webidl.converters.AbortSignal),
|
||||
defaultValue: () => null
|
||||
}
|
||||
])
|
||||
|
||||
webidl.converters.WebSocketCloseInfo = webidl.dictionaryConverter([
|
||||
{
|
||||
key: 'closeCode',
|
||||
converter: (V) => webidl.converters['unsigned short'](V, webidl.attributes.EnforceRange)
|
||||
},
|
||||
{
|
||||
key: 'reason',
|
||||
converter: webidl.converters.USVString,
|
||||
defaultValue: () => ''
|
||||
}
|
||||
])
|
||||
|
||||
webidl.converters.WebSocketStreamWrite = function (V) {
|
||||
if (typeof V === 'string') {
|
||||
return webidl.converters.USVString(V)
|
||||
}
|
||||
|
||||
return webidl.converters.BufferSource(V)
|
||||
}
|
||||
|
||||
module.exports = { WebSocketStream }
|
||||
338
node_modules/undici/lib/web/websocket/util.js
generated
vendored
Normal file
338
node_modules/undici/lib/web/websocket/util.js
generated
vendored
Normal file
@@ -0,0 +1,338 @@
|
||||
'use strict'
|
||||
|
||||
const { states, opcodes } = require('./constants')
|
||||
const { isUtf8 } = require('node:buffer')
|
||||
const { collectASequenceOfCodePointsFast, removeHTTPWhitespace } = require('../fetch/data-url')
|
||||
|
||||
/**
|
||||
* @param {number} readyState
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isConnecting (readyState) {
|
||||
// If the WebSocket connection is not yet established, and the connection
|
||||
// is not yet closed, then the WebSocket connection is in the CONNECTING state.
|
||||
return readyState === states.CONNECTING
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} readyState
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isEstablished (readyState) {
|
||||
// If the server's response is validated as provided for above, it is
|
||||
// said that _The WebSocket Connection is Established_ and that the
|
||||
// WebSocket Connection is in the OPEN state.
|
||||
return readyState === states.OPEN
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} readyState
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isClosing (readyState) {
|
||||
// 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.
|
||||
return readyState === states.CLOSING
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} readyState
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isClosed (readyState) {
|
||||
return readyState === states.CLOSED
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://dom.spec.whatwg.org/#concept-event-fire
|
||||
* @param {string} e
|
||||
* @param {EventTarget} target
|
||||
* @param {(...args: ConstructorParameters<typeof Event>) => Event} eventFactory
|
||||
* @param {EventInit | undefined} eventInitDict
|
||||
* @returns {void}
|
||||
*/
|
||||
function fireEvent (e, target, eventFactory = (type, init) => new Event(type, init), eventInitDict = {}) {
|
||||
// 1. If eventConstructor is not given, then let eventConstructor be Event.
|
||||
|
||||
// 2. Let event be the result of creating an event given eventConstructor,
|
||||
// in the relevant realm of target.
|
||||
// 3. Initialize event’s type attribute to e.
|
||||
const event = eventFactory(e, eventInitDict)
|
||||
|
||||
// 4. Initialize any other IDL attributes of event as described in the
|
||||
// invocation of this algorithm.
|
||||
|
||||
// 5. Return the result of dispatching event at target, with legacy target
|
||||
// override flag set if set.
|
||||
target.dispatchEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
|
||||
* @param {import('./websocket').Handler} handler
|
||||
* @param {number} type Opcode
|
||||
* @param {Buffer} data application data
|
||||
* @returns {void}
|
||||
*/
|
||||
function websocketMessageReceived (handler, type, data) {
|
||||
handler.onMessage(type, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} buffer
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
function toArrayBuffer (buffer) {
|
||||
if (buffer.byteLength === buffer.buffer.byteLength) {
|
||||
return buffer.buffer
|
||||
}
|
||||
return new Uint8Array(buffer).buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6455
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc2616
|
||||
* @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407
|
||||
* @param {string} protocol
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidSubprotocol (protocol) {
|
||||
// If present, this value indicates one
|
||||
// or more comma-separated subprotocol the client wishes to speak,
|
||||
// ordered by preference. The elements that comprise this value
|
||||
// MUST be non-empty strings with characters in the range U+0021 to
|
||||
// U+007E not including separator characters as defined in
|
||||
// [RFC2616] and MUST all be unique strings.
|
||||
if (protocol.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < protocol.length; ++i) {
|
||||
const code = protocol.charCodeAt(i)
|
||||
|
||||
if (
|
||||
code < 0x21 || // CTL, contains SP (0x20) and HT (0x09)
|
||||
code > 0x7E ||
|
||||
code === 0x22 || // "
|
||||
code === 0x28 || // (
|
||||
code === 0x29 || // )
|
||||
code === 0x2C || // ,
|
||||
code === 0x2F || // /
|
||||
code === 0x3A || // :
|
||||
code === 0x3B || // ;
|
||||
code === 0x3C || // <
|
||||
code === 0x3D || // =
|
||||
code === 0x3E || // >
|
||||
code === 0x3F || // ?
|
||||
code === 0x40 || // @
|
||||
code === 0x5B || // [
|
||||
code === 0x5C || // \
|
||||
code === 0x5D || // ]
|
||||
code === 0x7B || // {
|
||||
code === 0x7D // }
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4
|
||||
* @param {number} code
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidStatusCode (code) {
|
||||
if (code >= 1000 && code < 1015) {
|
||||
return (
|
||||
code !== 1004 && // reserved
|
||||
code !== 1005 && // "MUST NOT be set as a status code"
|
||||
code !== 1006 // "MUST NOT be set as a status code"
|
||||
)
|
||||
}
|
||||
|
||||
return code >= 3000 && code <= 4999
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5
|
||||
* @param {number} opcode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isControlFrame (opcode) {
|
||||
return (
|
||||
opcode === opcodes.CLOSE ||
|
||||
opcode === opcodes.PING ||
|
||||
opcode === opcodes.PONG
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} opcode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isContinuationFrame (opcode) {
|
||||
return opcode === opcodes.CONTINUATION
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} opcode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isTextBinaryFrame (opcode) {
|
||||
return opcode === opcodes.TEXT || opcode === opcodes.BINARY
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} opcode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidOpcode (opcode) {
|
||||
return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a Sec-WebSocket-Extensions header value.
|
||||
* @param {string} extensions
|
||||
* @returns {Map<string, string>}
|
||||
*/
|
||||
// TODO(@Uzlopak, @KhafraDev): make compliant https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
|
||||
function parseExtensions (extensions) {
|
||||
const position = { position: 0 }
|
||||
const extensionList = new Map()
|
||||
|
||||
while (position.position < extensions.length) {
|
||||
const pair = collectASequenceOfCodePointsFast(';', extensions, position)
|
||||
const [name, value = ''] = pair.split('=', 2)
|
||||
|
||||
extensionList.set(
|
||||
removeHTTPWhitespace(name, true, false),
|
||||
removeHTTPWhitespace(value, false, true)
|
||||
)
|
||||
|
||||
position.position++
|
||||
}
|
||||
|
||||
return extensionList
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.rfc-editor.org/rfc/rfc7692#section-7.1.2.2
|
||||
* @description "client-max-window-bits = 1*DIGIT"
|
||||
* @param {string} value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidClientWindowBits (value) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const byte = value.charCodeAt(i)
|
||||
|
||||
if (byte < 0x30 || byte > 0x39) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://whatpr.org/websockets/48/7b748d3...d5570f3.html#get-a-url-record
|
||||
* @param {string} url
|
||||
* @param {string} [baseURL]
|
||||
*/
|
||||
function getURLRecord (url, baseURL) {
|
||||
// 1. Let urlRecord be the result of applying the URL parser to url with baseURL .
|
||||
// 2. If urlRecord is failure, then throw a " SyntaxError " DOMException .
|
||||
let urlRecord
|
||||
|
||||
try {
|
||||
urlRecord = new URL(url, baseURL)
|
||||
} catch (e) {
|
||||
throw new DOMException(e, 'SyntaxError')
|
||||
}
|
||||
|
||||
// 3. If urlRecord ’s scheme is " http ", then set urlRecord ’s scheme to " ws ".
|
||||
// 4. Otherwise, if urlRecord ’s scheme is " https ", set urlRecord ’s scheme to " wss ".
|
||||
if (urlRecord.protocol === 'http:') {
|
||||
urlRecord.protocol = 'ws:'
|
||||
} else if (urlRecord.protocol === 'https:') {
|
||||
urlRecord.protocol = 'wss:'
|
||||
}
|
||||
|
||||
// 5. 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: url', 'SyntaxError')
|
||||
}
|
||||
|
||||
// If urlRecord ’s fragment is non-null, then throw a " SyntaxError " DOMException .
|
||||
if (urlRecord.hash.length || urlRecord.href.endsWith('#')) {
|
||||
throw new DOMException('hash', 'SyntaxError')
|
||||
}
|
||||
|
||||
// Return urlRecord .
|
||||
return urlRecord
|
||||
}
|
||||
|
||||
// https://whatpr.org/websockets/48.html#validate-close-code-and-reason
|
||||
function validateCloseCodeAndReason (code, reason) {
|
||||
// 1. If code is not null, but is neither an integer equal to
|
||||
// 1000 nor an integer in the range 3000 to 4999, inclusive,
|
||||
// throw an "InvalidAccessError" DOMException.
|
||||
if (code !== null) {
|
||||
if (code !== 1000 && (code < 3000 || code > 4999)) {
|
||||
throw new DOMException('invalid code', 'InvalidAccessError')
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If reason is not null, then:
|
||||
if (reason !== null) {
|
||||
// 2.1. Let reasonBytes be the result of UTF-8 encoding reason.
|
||||
// 2.2. If reasonBytes is longer than 123 bytes, then throw a
|
||||
// "SyntaxError" DOMException.
|
||||
const reasonBytesLength = Buffer.byteLength(reason)
|
||||
|
||||
if (reasonBytesLength > 123) {
|
||||
throw new DOMException(`Reason must be less than 123 bytes; received ${reasonBytesLength}`, 'SyntaxError')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Buffer to utf-8, even on platforms without icu.
|
||||
* @type {(buffer: Buffer) => string}
|
||||
*/
|
||||
const utf8Decode = (() => {
|
||||
if (typeof process.versions.icu === 'string') {
|
||||
const fatalDecoder = new TextDecoder('utf-8', { fatal: true })
|
||||
return fatalDecoder.decode.bind(fatalDecoder)
|
||||
}
|
||||
return function (buffer) {
|
||||
if (isUtf8(buffer)) {
|
||||
return buffer.toString('utf-8')
|
||||
}
|
||||
throw new TypeError('Invalid utf-8 received.')
|
||||
}
|
||||
})()
|
||||
|
||||
module.exports = {
|
||||
isConnecting,
|
||||
isEstablished,
|
||||
isClosing,
|
||||
isClosed,
|
||||
fireEvent,
|
||||
isValidSubprotocol,
|
||||
isValidStatusCode,
|
||||
websocketMessageReceived,
|
||||
utf8Decode,
|
||||
isControlFrame,
|
||||
isContinuationFrame,
|
||||
isTextBinaryFrame,
|
||||
isValidOpcode,
|
||||
parseExtensions,
|
||||
isValidClientWindowBits,
|
||||
toArrayBuffer,
|
||||
getURLRecord,
|
||||
validateCloseCodeAndReason
|
||||
}
|
||||
739
node_modules/undici/lib/web/websocket/websocket.js
generated
vendored
Normal file
739
node_modules/undici/lib/web/websocket/websocket.js
generated
vendored
Normal file
@@ -0,0 +1,739 @@
|
||||
'use strict'
|
||||
|
||||
const { isArrayBuffer } = require('node:util/types')
|
||||
const { webidl } = require('../webidl')
|
||||
const { URLSerializer } = require('../fetch/data-url')
|
||||
const { environmentSettingsObject } = require('../fetch/util')
|
||||
const { staticPropertyDescriptors, states, sentCloseFrameState, sendHints, opcodes } = require('./constants')
|
||||
const {
|
||||
isConnecting,
|
||||
isEstablished,
|
||||
isClosing,
|
||||
isClosed,
|
||||
isValidSubprotocol,
|
||||
fireEvent,
|
||||
utf8Decode,
|
||||
toArrayBuffer,
|
||||
getURLRecord
|
||||
} = require('./util')
|
||||
const { establishWebSocketConnection, closeWebSocketConnection, failWebsocketConnection } = require('./connection')
|
||||
const { ByteParser } = require('./receiver')
|
||||
const { kEnumerableProperty } = require('../../core/util')
|
||||
const { getGlobalDispatcher } = require('../../global')
|
||||
const { ErrorEvent, CloseEvent, createFastMessageEvent } = require('./events')
|
||||
const { SendQueue } = require('./sender')
|
||||
const { WebsocketFrameSend } = require('./frame')
|
||||
const { channels } = require('../../core/diagnostics')
|
||||
|
||||
/**
|
||||
* @typedef {object} Handler
|
||||
* @property {(response: any, extensions?: string[]) => void} onConnectionEstablished
|
||||
* @property {(opcode: number, data: Buffer) => void} onMessage
|
||||
* @property {(error: Error) => void} onParserError
|
||||
* @property {() => void} onParserDrain
|
||||
* @property {(chunk: Buffer) => void} onSocketData
|
||||
* @property {(err: Error) => void} onSocketError
|
||||
* @property {() => void} onSocketClose
|
||||
* @property {(body: Buffer) => void} onPing
|
||||
* @property {(body: Buffer) => void} onPong
|
||||
*
|
||||
* @property {number} readyState
|
||||
* @property {import('stream').Duplex} socket
|
||||
* @property {Set<number>} closeState
|
||||
* @property {import('../fetch/index').Fetch} controller
|
||||
* @property {boolean} [wasEverConnected=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 = ''
|
||||
|
||||
/** @type {SendQueue} */
|
||||
#sendQueue
|
||||
|
||||
/** @type {Handler} */
|
||||
#handler = {
|
||||
onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
|
||||
onMessage: (opcode, data) => this.#onMessage(opcode, data),
|
||||
onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
|
||||
onParserDrain: () => this.#onParserDrain(),
|
||||
onSocketData: (chunk) => {
|
||||
if (!this.#parser.write(chunk)) {
|
||||
this.#handler.socket.pause()
|
||||
}
|
||||
},
|
||||
onSocketError: (err) => {
|
||||
this.#handler.readyState = states.CLOSING
|
||||
|
||||
if (channels.socketError.hasSubscribers) {
|
||||
channels.socketError.publish(err)
|
||||
}
|
||||
|
||||
this.#handler.socket.destroy()
|
||||
},
|
||||
onSocketClose: () => this.#onSocketClose(),
|
||||
onPing: (body) => {
|
||||
if (channels.ping.hasSubscribers) {
|
||||
channels.ping.publish({
|
||||
payload: body,
|
||||
websocket: this
|
||||
})
|
||||
}
|
||||
},
|
||||
onPong: (body) => {
|
||||
if (channels.pong.hasSubscribers) {
|
||||
channels.pong.publish({
|
||||
payload: body,
|
||||
websocket: this
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
readyState: states.CONNECTING,
|
||||
socket: null,
|
||||
closeState: new Set(),
|
||||
controller: null,
|
||||
wasEverConnected: false
|
||||
}
|
||||
|
||||
#url
|
||||
#binaryType
|
||||
/** @type {import('./receiver').ByteParser} */
|
||||
#parser
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {string|string[]} protocols
|
||||
*/
|
||||
constructor (url, protocols = []) {
|
||||
super()
|
||||
|
||||
webidl.util.markAsUncloneable(this)
|
||||
|
||||
const prefix = 'WebSocket constructor'
|
||||
webidl.argumentLengthCheck(arguments, 1, prefix)
|
||||
|
||||
const options = webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'](protocols, prefix, 'options')
|
||||
|
||||
url = webidl.converters.USVString(url)
|
||||
protocols = options.protocols
|
||||
|
||||
// 1. Let baseURL be this's relevant settings object's API base URL.
|
||||
const baseURL = environmentSettingsObject.settingsObject.baseUrl
|
||||
|
||||
// 2. Let urlRecord be the result of getting a URL record given url and baseURL.
|
||||
const urlRecord = getURLRecord(url, baseURL)
|
||||
|
||||
// 3. If protocols is a string, set protocols to a sequence consisting
|
||||
// of just that string.
|
||||
if (typeof protocols === 'string') {
|
||||
protocols = [protocols]
|
||||
}
|
||||
|
||||
// 4. 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')
|
||||
}
|
||||
|
||||
// 5. Set this's url to urlRecord.
|
||||
this.#url = new URL(urlRecord.href)
|
||||
|
||||
// 6. Let client be this's relevant settings object.
|
||||
const client = environmentSettingsObject.settingsObject
|
||||
|
||||
// 7. Run this step in parallel:
|
||||
// 7.1. Establish a WebSocket connection given urlRecord, protocols,
|
||||
// and client.
|
||||
this.#handler.controller = establishWebSocketConnection(
|
||||
urlRecord,
|
||||
protocols,
|
||||
client,
|
||||
this.#handler,
|
||||
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.#handler.readyState = 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.#binaryType = '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)
|
||||
|
||||
const prefix = 'WebSocket.close'
|
||||
|
||||
if (code !== undefined) {
|
||||
code = webidl.converters['unsigned short'](code, prefix, 'code', webidl.attributes.Clamp)
|
||||
}
|
||||
|
||||
if (reason !== undefined) {
|
||||
reason = webidl.converters.USVString(reason)
|
||||
}
|
||||
|
||||
// 1. If code is the special value "missing", then set code to null.
|
||||
code ??= null
|
||||
|
||||
// 2. If reason is the special value "missing", then set reason to the empty string.
|
||||
reason ??= ''
|
||||
|
||||
// 3. Close the WebSocket with this, code, and reason.
|
||||
closeWebSocketConnection(this.#handler, code, reason, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://websockets.spec.whatwg.org/#dom-websocket-send
|
||||
* @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data
|
||||
*/
|
||||
send (data) {
|
||||
webidl.brandCheck(this, WebSocket)
|
||||
|
||||
const prefix = 'WebSocket.send'
|
||||
webidl.argumentLengthCheck(arguments, 1, prefix)
|
||||
|
||||
data = webidl.converters.WebSocketSendData(data, prefix, 'data')
|
||||
|
||||
// 1. If this's ready state is CONNECTING, then throw an
|
||||
// "InvalidStateError" DOMException.
|
||||
if (isConnecting(this.#handler.readyState)) {
|
||||
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.#handler.readyState) || isClosing(this.#handler.readyState)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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 buffer = Buffer.from(data)
|
||||
|
||||
this.#bufferedAmount += buffer.byteLength
|
||||
this.#sendQueue.add(buffer, () => {
|
||||
this.#bufferedAmount -= buffer.byteLength
|
||||
}, sendHints.text)
|
||||
} else if (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.
|
||||
|
||||
this.#bufferedAmount += data.byteLength
|
||||
this.#sendQueue.add(data, () => {
|
||||
this.#bufferedAmount -= data.byteLength
|
||||
}, sendHints.arrayBuffer)
|
||||
} 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.
|
||||
|
||||
this.#bufferedAmount += data.byteLength
|
||||
this.#sendQueue.add(data, () => {
|
||||
this.#bufferedAmount -= data.byteLength
|
||||
}, sendHints.typedArray)
|
||||
} else if (webidl.is.Blob(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.
|
||||
|
||||
this.#bufferedAmount += data.size
|
||||
this.#sendQueue.add(data, () => {
|
||||
this.#bufferedAmount -= data.size
|
||||
}, sendHints.blob)
|
||||
}
|
||||
}
|
||||
|
||||
get readyState () {
|
||||
webidl.brandCheck(this, WebSocket)
|
||||
|
||||
// The readyState getter steps are to return this's ready state.
|
||||
return this.#handler.readyState
|
||||
}
|
||||
|
||||
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.#url)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const listener = webidl.converters.EventHandlerNonNull(fn)
|
||||
|
||||
if (listener !== null) {
|
||||
this.addEventListener('open', listener)
|
||||
this.#events.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)
|
||||
}
|
||||
|
||||
const listener = webidl.converters.EventHandlerNonNull(fn)
|
||||
|
||||
if (listener !== null) {
|
||||
this.addEventListener('error', listener)
|
||||
this.#events.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)
|
||||
}
|
||||
|
||||
const listener = webidl.converters.EventHandlerNonNull(fn)
|
||||
|
||||
if (listener !== null) {
|
||||
this.addEventListener('close', listener)
|
||||
this.#events.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)
|
||||
}
|
||||
|
||||
const listener = webidl.converters.EventHandlerNonNull(fn)
|
||||
|
||||
if (listener !== null) {
|
||||
this.addEventListener('message', listener)
|
||||
this.#events.message = fn
|
||||
} else {
|
||||
this.#events.message = null
|
||||
}
|
||||
}
|
||||
|
||||
get binaryType () {
|
||||
webidl.brandCheck(this, WebSocket)
|
||||
|
||||
return this.#binaryType
|
||||
}
|
||||
|
||||
set binaryType (type) {
|
||||
webidl.brandCheck(this, WebSocket)
|
||||
|
||||
if (type !== 'blob' && type !== 'arraybuffer') {
|
||||
this.#binaryType = 'blob'
|
||||
} else {
|
||||
this.#binaryType = type
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
|
||||
*/
|
||||
#onConnectionEstablished (response, parsedExtensions) {
|
||||
// processResponse is called when the "response’s header list has been received and initialized."
|
||||
// once this happens, the connection is open
|
||||
this.#handler.socket = response.socket
|
||||
|
||||
const parser = new ByteParser(this.#handler, parsedExtensions)
|
||||
parser.on('drain', () => this.#handler.onParserDrain())
|
||||
parser.on('error', (err) => this.#handler.onParserError(err))
|
||||
|
||||
this.#parser = parser
|
||||
this.#sendQueue = new SendQueue(response.socket)
|
||||
|
||||
// 1. Change the ready state to OPEN (1).
|
||||
this.#handler.readyState = 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)
|
||||
|
||||
if (channels.open.hasSubscribers) {
|
||||
// Convert headers to a plain object for the event
|
||||
const headers = response.headersList.entries
|
||||
channels.open.publish({
|
||||
address: response.socket.address(),
|
||||
protocol: this.#protocol,
|
||||
extensions: this.#extensions,
|
||||
websocket: this,
|
||||
handshakeResponse: {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#onMessage (type, data) {
|
||||
// 1. If ready state is not OPEN (1), then return.
|
||||
if (this.#handler.readyState !== states.OPEN) {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Let dataForEvent be determined by switching on type and binary type:
|
||||
let dataForEvent
|
||||
|
||||
if (type === opcodes.TEXT) {
|
||||
// -> type indicates that the data is Text
|
||||
// a new DOMString containing data
|
||||
try {
|
||||
dataForEvent = utf8Decode(data)
|
||||
} catch {
|
||||
failWebsocketConnection(this.#handler, 1007, 'Received invalid UTF-8 in text frame.')
|
||||
return
|
||||
}
|
||||
} else if (type === opcodes.BINARY) {
|
||||
if (this.#binaryType === 'blob') {
|
||||
// -> type indicates that the data is Binary and binary type is "blob"
|
||||
// a new Blob object, created in the relevant Realm of the WebSocket
|
||||
// object, that represents data as its raw data
|
||||
dataForEvent = new Blob([data])
|
||||
} else {
|
||||
// -> type indicates that the data is Binary and binary type is "arraybuffer"
|
||||
// a new ArrayBuffer object, created in the relevant Realm of the
|
||||
// WebSocket object, whose contents are data
|
||||
dataForEvent = toArrayBuffer(data)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fire an event named message at the WebSocket object, using MessageEvent,
|
||||
// with the origin attribute initialized to the serialization of the WebSocket
|
||||
// object’s url's origin, and the data attribute initialized to dataForEvent.
|
||||
fireEvent('message', this, createFastMessageEvent, {
|
||||
origin: this.#url.origin,
|
||||
data: dataForEvent
|
||||
})
|
||||
}
|
||||
|
||||
#onParserDrain () {
|
||||
this.#handler.socket.resume()
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4
|
||||
*/
|
||||
#onSocketClose () {
|
||||
// If the TCP connection was closed after the
|
||||
// WebSocket closing handshake was completed, the WebSocket connection
|
||||
// is said to have been closed _cleanly_.
|
||||
const wasClean =
|
||||
this.#handler.closeState.has(sentCloseFrameState.SENT) &&
|
||||
this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
|
||||
|
||||
let code = 1005
|
||||
let reason = ''
|
||||
|
||||
const result = this.#parser?.closingInfo
|
||||
|
||||
if (result && !result.error) {
|
||||
code = result.code ?? 1005
|
||||
reason = result.reason
|
||||
}
|
||||
|
||||
// 1. Change the ready state to CLOSED (3).
|
||||
this.#handler.readyState = states.CLOSED
|
||||
|
||||
// 2. If the user agent was required to fail the WebSocket
|
||||
// connection, or if the WebSocket connection was closed
|
||||
// after being flagged as full, fire an event named error
|
||||
// at the WebSocket object.
|
||||
if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
|
||||
// If _The WebSocket
|
||||
// Connection is Closed_ and no Close control frame was received by the
|
||||
// endpoint (such as could occur if the underlying transport connection
|
||||
// is lost), _The WebSocket Connection Close Code_ is considered to be
|
||||
// 1006.
|
||||
code = 1006
|
||||
|
||||
fireEvent('error', this, (type, init) => new ErrorEvent(type, init), {
|
||||
error: new TypeError(reason)
|
||||
})
|
||||
}
|
||||
|
||||
// 3. Fire an event named close at the WebSocket object,
|
||||
// using CloseEvent, with the wasClean attribute
|
||||
// initialized to true if the connection closed cleanly
|
||||
// and false otherwise, the code attribute initialized to
|
||||
// the WebSocket connection close code, and the reason
|
||||
// attribute initialized to the result of applying UTF-8
|
||||
// decode without BOM to the WebSocket connection close
|
||||
// reason.
|
||||
// TODO: process.nextTick
|
||||
fireEvent('close', this, (type, init) => new CloseEvent(type, init), {
|
||||
wasClean, code, reason
|
||||
})
|
||||
|
||||
if (channels.close.hasSubscribers) {
|
||||
channels.close.publish({
|
||||
websocket: this,
|
||||
code,
|
||||
reason
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {WebSocket} ws
|
||||
* @param {Buffer|undefined} buffer
|
||||
*/
|
||||
static ping (ws, buffer) {
|
||||
if (Buffer.isBuffer(buffer)) {
|
||||
if (buffer.length > 125) {
|
||||
throw new TypeError('A PING frame cannot have a body larger than 125 bytes.')
|
||||
}
|
||||
} else if (buffer !== undefined) {
|
||||
throw new TypeError('Expected buffer payload')
|
||||
}
|
||||
|
||||
// An endpoint MAY send a Ping frame any time after the connection is
|
||||
// established and before the connection is closed.
|
||||
const readyState = ws.#handler.readyState
|
||||
|
||||
if (isEstablished(readyState) && !isClosing(readyState) && !isClosed(readyState)) {
|
||||
const frame = new WebsocketFrameSend(buffer)
|
||||
ws.#handler.socket.write(frame.createFrame(opcodes.PING))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { ping } = WebSocket
|
||||
Reflect.deleteProperty(WebSocket, 'ping')
|
||||
|
||||
// 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, prefix, argument) {
|
||||
if (webidl.util.Type(V) === webidl.util.Types.OBJECT && Symbol.iterator in V) {
|
||||
return webidl.converters['sequence<DOMString>'](V)
|
||||
}
|
||||
|
||||
return webidl.converters.DOMString(V, prefix, argument)
|
||||
}
|
||||
|
||||
// This implements the proposal made in https://github.com/whatwg/websockets/issues/42
|
||||
webidl.converters.WebSocketInit = webidl.dictionaryConverter([
|
||||
{
|
||||
key: 'protocols',
|
||||
converter: webidl.converters['DOMString or sequence<DOMString>'],
|
||||
defaultValue: () => []
|
||||
},
|
||||
{
|
||||
key: 'dispatcher',
|
||||
converter: webidl.converters.any,
|
||||
defaultValue: () => getGlobalDispatcher()
|
||||
},
|
||||
{
|
||||
key: 'headers',
|
||||
converter: webidl.nullableConverter(webidl.converters.HeadersInit)
|
||||
}
|
||||
])
|
||||
|
||||
webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'] = function (V) {
|
||||
if (webidl.util.Type(V) === webidl.util.Types.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) === webidl.util.Types.OBJECT) {
|
||||
if (webidl.is.Blob(V)) {
|
||||
return V
|
||||
}
|
||||
|
||||
if (webidl.is.BufferSource(V)) {
|
||||
return V
|
||||
}
|
||||
}
|
||||
|
||||
return webidl.converters.USVString(V)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
WebSocket,
|
||||
ping
|
||||
}
|
||||
Reference in New Issue
Block a user