Skip to content

Commit

Permalink
separate whatwg websocket logic from rfc 6455 (#3396)
Browse files Browse the repository at this point in the history
* separate whatwg websocket logic from rfc 6455

* fixup

* remove symbols

* move parser events

* remove symbols
  • Loading branch information
KhafraDev committed Jul 8, 2024
1 parent ed3ad9f commit 7436fee
Show file tree
Hide file tree
Showing 5 changed files with 346 additions and 336 deletions.
190 changes: 21 additions & 169 deletions lib/web/websocket/connection.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
'use strict'

const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
const {
kReadyState,
kSentClose,
kByteParser,
kReceivedClose,
kResponse
} = require('./symbols')
const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished, parseExtensions } = require('./util')
const { uid } = require('./constants')
const { failWebsocketConnection, parseExtensions } = require('./util')
const { channels } = require('../../core/diagnostics')
const { CloseEvent } = require('./events')
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')

/** @type {import('crypto')} */
let crypto
Expand All @@ -30,11 +21,10 @@ try {
* @see https://websockets.spec.whatwg.org/#concept-websocket-establish
* @param {URL} url
* @param {string|string[]} protocols
* @param {import('./websocket').WebSocket} ws
* @param {(response: any, extensions: string[] | undefined) => void} onEstablish
* @param {import('./websocket').Handler} handler
* @param {Partial<import('../../types/websocket').WebSocketInit>} options
*/
function establishWebSocketConnection (url, protocols, client, ws, onEstablish, 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
Expand Down Expand Up @@ -107,7 +97,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
// 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(ws, 'Received network error or non-101 status code.')
failWebsocketConnection(handler, 'Received network error or non-101 status code.')
return
}

Expand All @@ -116,7 +106,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
// 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(ws, 'Server did not respond with sent protocols.')
failWebsocketConnection(handler, 'Server did not respond with sent protocols.')
return
}

Expand All @@ -131,7 +121,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
// insensitive match for the value "websocket", the client MUST
// _Fail the WebSocket Connection_.
if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') {
failWebsocketConnection(ws, 'Server did not set Upgrade header to "websocket".')
failWebsocketConnection(handler, 'Server did not set Upgrade header to "websocket".')
return
}

Expand All @@ -140,7 +130,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
// ASCII case-insensitive match for the value "Upgrade", the client
// MUST _Fail the WebSocket Connection_.
if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') {
failWebsocketConnection(ws, 'Server did not set Connection header to "upgrade".')
failWebsocketConnection(handler, 'Server did not set Connection header to "upgrade".')
return
}

Expand All @@ -154,7 +144,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
const secWSAccept = response.headersList.get('Sec-WebSocket-Accept')
const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64')
if (secWSAccept !== digest) {
failWebsocketConnection(ws, 'Incorrect hash received in Sec-WebSocket-Accept header.')
failWebsocketConnection(handler, 'Incorrect hash received in Sec-WebSocket-Accept header.')
return
}

Expand All @@ -172,7 +162,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
extensions = parseExtensions(secExtension)

if (!extensions.has('permessage-deflate')) {
failWebsocketConnection(ws, 'Sec-WebSocket-Extensions header does not match.')
failWebsocketConnection(handler, 'Sec-WebSocket-Extensions header does not match.')
return
}
}
Expand All @@ -193,14 +183,14 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
// the selected subprotocol values in its response for the connection to
// be established.
if (!requestProtocols.includes(secProtocol)) {
failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
failWebsocketConnection(handler, 'Protocol was not set in the opening handshake.')
return
}
}

response.socket.on('data', onSocketData)
response.socket.on('close', onSocketClose)
response.socket.on('error', onSocketError)
response.socket.on('data', handler.onSocketData)
response.socket.on('close', handler.onSocketClose)
response.socket.on('error', handler.onSocketError)

if (channels.open.hasSubscribers) {
channels.open.publish({
Expand All @@ -210,159 +200,21 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
})
}

onEstablish(response, extensions)
handler.onConnectionEstablished(response, extensions)
}
})

return controller
}

function closeWebSocketConnection (ws, code, reason, reasonByteLength) {
if (isClosing(ws) || isClosed(ws)) {
// If this's ready state is CLOSING (2) or CLOSED (3)
// Do nothing.
} else if (!isEstablished(ws)) {
// If the WebSocket connection is not yet established
// Fail the WebSocket connection and set this's ready state
// to CLOSING (2).
failWebsocketConnection(ws, 'Connection was closed before it was established.')
ws[kReadyState] = states.CLOSING
} else if (ws[kSentClose] === sentCloseFrameState.NOT_SENT) {
// If the WebSocket closing handshake has not yet been started
// Start the WebSocket closing handshake and set this's ready
// state to CLOSING (2).
// - If neither code nor reason is present, the WebSocket Close
// message must not have a body.
// - If code is present, then the status code to use in the
// WebSocket Close message must be the integer given by code.
// - If reason is also present, then reasonBytes must be
// provided in the Close message after the status code.

ws[kSentClose] = 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 !== undefined && reason === undefined) {
frame.frameData = Buffer.allocUnsafe(2)
frame.frameData.writeUInt16BE(code, 0)
} else if (code !== undefined && reason !== undefined) {
// If reason is also present, then reasonBytes must be
// provided in the Close message after the status code.
frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength)
frame.frameData.writeUInt16BE(code, 0)
// the body MAY contain UTF-8-encoded data with value /reason/
frame.frameData.write(reason, 2, 'utf-8')
} else {
frame.frameData = emptyBuffer
}

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

socket.write(frame.createFrame(opcodes.CLOSE))

ws[kSentClose] = 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.
ws[kReadyState] = states.CLOSING
} else {
// Otherwise
// Set this's ready state to CLOSING (2).
ws[kReadyState] = states.CLOSING
}
}

/**
* @param {Buffer} chunk
*/
function onSocketData (chunk) {
if (!this.ws[kByteParser].write(chunk)) {
this.pause()
}
}

/**
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4
* @param {import('./websocket').Handler} handler
* @param {number} code
* @param {any} reason
* @param {number} reasonByteLength
*/
function onSocketClose () {
const { ws } = this
const { [kResponse]: response } = ws

response.socket.off('data', onSocketData)
response.socket.off('close', onSocketClose)
response.socket.off('error', onSocketError)

// 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 = ws[kSentClose] === sentCloseFrameState.SENT && ws[kReceivedClose]

let code = 1005
let reason = ''

const result = ws[kByteParser].closingInfo

if (result && !result.error) {
code = result.code ?? 1005
reason = result.reason
} else if (!ws[kReceivedClose]) {
// 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
}

// 1. Change the ready state to CLOSED (3).
ws[kReadyState] = 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.
// TODO

// 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', ws, (type, init) => new CloseEvent(type, init), {
wasClean, code, reason
})

if (channels.close.hasSubscribers) {
channels.close.publish({
websocket: ws,
code,
reason
})
}
}

function onSocketError (error) {
const { ws } = this

ws[kReadyState] = states.CLOSING

if (channels.socketError.hasSubscribers) {
channels.socketError.publish(error)
}

this.destroy()
function closeWebSocketConnection (handler, code, reason, reasonByteLength) {
handler.onClose(code, reason, reasonByteLength)
}

module.exports = {
Expand Down
Loading

0 comments on commit 7436fee

Please sign in to comment.