Skip to content

Commit

Permalink
feat: implement WebSocketStream (#3560)
Browse files Browse the repository at this point in the history
* start

fixup

fixup

fixup

* fixup

* fixup

* fixup

* run wpts

* fix 49 errors

* fix 49 errors

* fix 7 errors

* fix 12 failures

* fix 10 failures

* fixup

* fix 9 failures

* fix 3 failures

* fix 3 failures

* fix rest of failures

* fixup

* mark timing test as flaky

* spell

* fixup

* fixup

* run the CI again! woo!
  • Loading branch information
KhafraDev authored Sep 26, 2024
1 parent e6e87c1 commit cfab33f
Show file tree
Hide file tree
Showing 9 changed files with 819 additions and 146 deletions.
86 changes: 78 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')

/** @type {import('crypto')} */
let crypto
Expand Down Expand Up @@ -214,13 +216,81 @@ 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=null]
* @param {string} [reason='']
*/
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.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
}
}

module.exports = {
Expand Down
4 changes: 2 additions & 2 deletions 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)
return
}

Expand Down Expand Up @@ -356,7 +356,7 @@ class ByteParser extends Writable {

// 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)) {
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
Expand Down
81 changes: 81 additions & 0 deletions lib/web/websocket/stream/websocketerror.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use strict'

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

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

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

module.exports = { WebSocketError, createUnvalidatedWebSocketError }
Loading

0 comments on commit cfab33f

Please sign in to comment.