Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement WebSocketStream #3560

Merged
merged 22 commits into from
Sep 26, 2024
Merged
85 changes: 77 additions & 8 deletions lib/web/websocket/connection.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
'use strict'

const { uid, states } = require('./constants')
const { failWebsocketConnection, parseExtensions } = require('./util')
const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
const { failWebsocketConnection, parseExtensions, isClosed, isClosing, isEstablished, validateCloseCodeAndReason } = require('./util')
const { channels } = require('../../core/diagnostics')
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')
KhafraDev marked this conversation as resolved.
Show resolved Hide resolved

/** @type {import('crypto')} */
let crypto
Expand Down Expand Up @@ -214,13 +216,80 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
}

/**
* @param {import('./websocket').Handler} handler
* @param {number} code
* @param {any} reason
* @param {number} reasonByteLength
* @see https://whatpr.org/websockets/48.html#close-the-websocket
* @param {import('./websocket').Handler} object
* @param {number} [code]
KhafraDev marked this conversation as resolved.
Show resolved Hide resolved
* @param {string} [reason]
KhafraDev marked this conversation as resolved.
Show resolved Hide resolved
*/
function closeWebSocketConnection (handler, code, reason, reasonByteLength) {
handler.onClose(code, reason, reasonByteLength)
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 === sentCloseFrameState.NOT_SENT) {
// Start the WebSocket closing handshake and set object’s ready state to CLOSING (2). [WSP]
object.closeState = sentCloseFrameState.PROCESSING

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

KhafraDev marked this conversation as resolved.
Show resolved Hide resolved
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 = 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
}
}

module.exports = {
Expand Down
2 changes: 1 addition & 1 deletion lib/web/websocket/receiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ class ByteParser extends Writable {
} else {
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
if (error) {
closeWebSocketConnection(this.#handler, 1007, error.message, error.message.length)
closeWebSocketConnection(this.#handler, 1007, error.message)
KhafraDev marked this conversation as resolved.
Show resolved Hide resolved
return
}

Expand Down
69 changes: 69 additions & 0 deletions lib/web/websocket/stream/websocketerror.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use strict'

const { webidl } = require('../../fetch/webidl')
const { validateCloseCodeAndReason } = require('../util')
const { kConstruct } = require('../../../core/symbols')

class WebSocketError extends DOMException {
#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')
Uzlopak marked this conversation as resolved.
Show resolved Hide resolved

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
KhafraDev marked this conversation as resolved.
Show resolved Hide resolved
*/
static createUnvalidatedWebSocketError (message, code, reason) {
Uzlopak marked this conversation as resolved.
Show resolved Hide resolved
const error = new WebSocketError(message, kConstruct)
error.#closeCode = code
error.#reason = reason
return error
}
}

const { createUnvalidatedWebSocketError } = WebSocketError
delete WebSocketError.createUnvalidatedWebSocketError

module.exports = { WebSocketError, createUnvalidatedWebSocketError }
Loading
Loading