Skip to content

Commit

Permalink
implement WebSocketStream
Browse files Browse the repository at this point in the history
enumerable

implement some more

implement close

finish open

run WPTs

update wpts and fix 9/19 failures

fix most remaining failures

fix remaining test failures

fix writing

fix: implement writing completely

fixup
  • Loading branch information
KhafraDev committed Mar 6, 2024
1 parent 03a2d43 commit 1abf223
Show file tree
Hide file tree
Showing 12 changed files with 756 additions and 66 deletions.
5 changes: 5 additions & 0 deletions lib/web/fetch/webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const webidl = {}
webidl.converters = {}
webidl.util = {}
webidl.errors = {}
webidl.is = {}

webidl.errors.exception = function (message) {
return new TypeError(`${message.header}: ${message.message}`)
Expand Down Expand Up @@ -675,6 +676,10 @@ webidl.converters['record<ByteString, ByteString>'] = webidl.recordConverter(
webidl.converters.ByteString
)

webidl.is.BufferSource = function (V) {
return ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)
}

module.exports = {
webidl
}
3 changes: 2 additions & 1 deletion lib/web/websocket/symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ module.exports = {
kBinaryType: Symbol('binary type'),
kSentClose: Symbol('sent close'),
kReceivedClose: Symbol('received close'),
kByteParser: Symbol('byte parser')
kByteParser: Symbol('byte parser'),
kPromises: Symbol('kPromises')
}
167 changes: 164 additions & 3 deletions lib/web/websocket/util.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use strict'

const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = require('./symbols')
const { states, opcodes } = require('./constants')
const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL, kSentClose, kPromises } = require('./symbols')
const { states, opcodes, emptyBuffer } = require('./constants')
const { MessageEvent, ErrorEvent } = require('./events')
const { WebsocketFrameSend } = require('./frame')

/* globals Blob */

Expand Down Expand Up @@ -53,6 +54,9 @@ function isClosed (ws) {
* @param {EventInit | undefined} eventInitDict
*/
function fireEvent (e, target, eventConstructor = Event, eventInitDict = {}) {
// TODO: remove this
if (!target.dispatchEvent) return

// 1. If eventConstructor is not given, then let eventConstructor be Event.

// 2. Let event be the result of creating an event given eventConstructor,
Expand All @@ -77,6 +81,9 @@ const textDecoder = new TextDecoder('utf-8', { fatal: true })
* @param {Buffer} data application data
*/
function websocketMessageReceived (ws, type, data) {
// TODO: remove
if (!ws.dispatchEvent) return

// 1. If ready state is not OPEN (1), then return.
if (ws[kReadyState] !== states.OPEN) {
return
Expand Down Expand Up @@ -181,6 +188,9 @@ function isValidStatusCode (code) {
return code >= 3000 && code <= 4999
}

/** @type {import('./stream/websocketerror').WebSocketError} */
let WebSocketError

/**
* @param {import('./websocket').WebSocket} ws
* @param {string|undefined} reason
Expand All @@ -200,6 +210,154 @@ function failWebsocketConnection (ws, reason) {
error: new Error(reason)
})
}

if (ws[kPromises]) {
WebSocketError ??= require('./stream/websocketerror').WebSocketError

const error = new WebSocketError('Connection closed.', { reason })

ws[kPromises].opened.reject(error)
ws[kPromises].closed.reject(error)
}
}

/**
* @see https://whatpr.org/websockets/48/7b748d3...7b81f79.html#close-the-websocket
* @param {import('./websocket').WebSocket|import('./stream/websocketstream').WebSocketStream} object
* @param {number|null} code
* @param {string} reason
*/
function closeWebSocket (object, code = null, reason = '') {
// 1. If code was not supplied, let code be null.
// 2. If reason was not supplied, let reason be the empty string.

// 3. Validate close code and reason with code and reason .
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 (object[kReadyState] === states.CLOSING || object[kReadyState] === states.CLOSED) {
// Do nothing.
// I like this step.
} else if (!isEstablished(object)) {
// Fail the WebSocket connection and set object ’s ready state to CLOSING (2). [WSP]
// TODO: with reason?
failWebsocketConnection(object, reason)
object[kReadyState] = states.CLOSING
} else if (!isClosing(object)) {
// Start the WebSocket closing handshake and set object ’s ready state to CLOSING (2). [WSP]
// - 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 code is set, then the status code to use in the WebSocket Close frame must be the integer given by code . [WSP]
// - If reason is non-empty, then reason , encoded as UTF-8 , must be provided in the Close frame after the status code. [WSP]

if (reason.length && code === null) {
code = 1000
}

const frame = new WebsocketFrameSend()

if (code !== null && reason.length === 0) {
frame.frameData = Buffer.allocUnsafe(2)
frame.frameData.writeUInt16BE(code, 0)
} else if (code !== null && reason.length) {
frame.frameData = Buffer.allocUnsafe(2 + Buffer.byteLength(reason))
frame.frameData.writeUInt16BE(code, 0)
frame.frameData.write(reason, 2, 'utf-8')
} else {
frame.frameData = emptyBuffer
}

/** @type {import('stream').Duplex} */
const socket = object[kResponse].socket

socket.write(frame.createFrame(opcodes.CLOSE), (err) => {
if (!err) {
object[kSentClose] = true
}
})

// Upon either sending or receiving a Close control frame, it is said
// that _The WebSocket Closing Handshake is Started_ and that the
// WebSocket connection is in the CLOSING state.
object[kReadyState] = states.CLOSING
} else {
// Set object ’s ready state to CLOSING (2).
object[kReadyState] = states.CLOSING
}
}

/**
* @see https://whatpr.org/websockets/48/7b748d3...7b81f79.html#validate-close-code-and-reason
* @param {number|null} code
* @param {any} 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 && code !== 1000 && (code < 3000 || code > 4999)) {
throw new DOMException('Invalid code', 'InvalidAccessError')
}

// 2. If reason is not null, then:
// TODO: reason can't be null here?
if (reason) {
// 2.1. Let reasonBytes be the result of UTF-8 encoding reason .
const reasonBytes = new TextEncoder().encode(reason)

// 2.2. If reasonBytes is longer than 123 bytes, then throw a " SyntaxError " DOMException .
if (reasonBytes.length > 123) {
throw new DOMException(
`Reason must be less than 123 bytes; received ${Buffer.byteLength(reasonBytes)}`,
'SyntaxError'
)
}
}
}

/**
* @see https://whatpr.org/websockets/48/7b748d3...7b81f79.html#get-a-url-record
* @param {URL|string} url
* @param {URL|string|undefined} baseURL
*/
function getURLRecord (url, baseURL) {
// 1. Let urlRecord be the result of applying the URL parser to url with baseURL .
/** @type {URL} */
let urlRecord

try {
urlRecord = new URL(url, baseURL)
} catch (e) {
// 2. If urlRecord is failure, then throw a " SyntaxError " DOMException .
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: protocol, got ${urlRecord.protocol}`,
'SyntaxError'
)
}

// 6. If urlRecord ’s fragment is non-null, then throw a " SyntaxError " DOMException .
if (urlRecord.hash || urlRecord.href.endsWith('#')) {
throw new DOMException('Got fragment', 'SyntaxError')
}

// 7. Return urlRecord .
return urlRecord
}

module.exports = {
Expand All @@ -211,5 +369,8 @@ module.exports = {
isValidSubprotocol,
isValidStatusCode,
failWebsocketConnection,
websocketMessageReceived
websocketMessageReceived,
closeWebSocket,
validateCloseCodeAndReason,
getURLRecord
}
76 changes: 76 additions & 0 deletions lib/websocket/stream/websocketerror.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use strict'

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

/**
* @see https://whatpr.org/websockets/48/7b748d3...7b81f79.html#websocketerror
*/
class WebSocketError extends DOMException {
/** @type {string} */
#reason
/** @type {number|null} */
#closeCode = null

/**
* @param {string} message
* @param {import('./websocketstream').WebSocketCloseInfo} init
*/
constructor (message = '', init = {}) {
super(message, 'WebSocketError')

message = webidl.converters.DOMString(message)
init = webidl.converters.WebSocketCloseInfo(init)

// 1. Set this 's name to " WebSocketError ".
// 2. Set this 's message to message .

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

Object.defineProperties(WebSocketError.prototype, {
closeCode: kEnumerableProperty,
reason: kEnumerableProperty
})

webidl.converters.WebSocketCloseInfo = webidl.dictionaryConverter([
{
converter: webidl.nullableConverter((V) => webidl.converters['unsigned short'](V, { enforceRange: true })),
key: 'closeCode'
},
{
converter: webidl.converters.USVString,
key: 'reason',
defaultValue: ''
}
])

module.exports = {
WebSocketError
}

0 comments on commit 1abf223

Please sign in to comment.