Skip to content

Commit

Permalink
separate whatwg websocket logic from rfc 6455
Browse files Browse the repository at this point in the history
  • Loading branch information
KhafraDev committed Jul 7, 2024
1 parent ed3ad9f commit de89fde
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 164 deletions.
93 changes: 20 additions & 73 deletions lib/web/websocket/connection.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
'use strict'

const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
const { uid, states, sentCloseFrameState } = require('./constants')
const {
kReadyState,
kSentClose,
kByteParser,
kReceivedClose,
kResponse
} = require('./symbols')
const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished, parseExtensions } = require('./util')
const { fireEvent, 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 @@ -31,10 +30,10 @@ try {
* @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, ws, 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 +106,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 +115,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 +130,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 +139,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 +153,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 +171,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,7 +192,7 @@ 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
}
}
Expand All @@ -210,73 +209,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 {import('./websocket').Handler} handler
* @param {number} code
* @param {any} reason
* @param {number} reasonByteLength
*/
function closeWebSocketConnection (handler, code, reason, reasonByteLength) {
handler.onClose(code, reason, reasonByteLength)
}

/**
Expand Down
38 changes: 21 additions & 17 deletions lib/web/websocket/receiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,14 @@ class ByteParser extends Writable {
/** @type {Map<string, PerMessageDeflate>} */
#extensions

constructor (ws, extensions) {
/** @type {import('./websocket').Handler} */
#handler

constructor (ws, handler, extensions) {
super()

this.ws = ws
this.#handler = handler
this.#extensions = extensions == null ? new Map() : extensions

if (this.#extensions.has('permessage-deflate')) {
Expand Down Expand Up @@ -86,12 +90,12 @@ class ByteParser extends Writable {
const rsv3 = buffer[0] & 0x10

if (!isValidOpcode(opcode)) {
failWebsocketConnection(this.ws, 'Invalid opcode received')
failWebsocketConnection(this.#handler, 'Invalid opcode received')
return callback()
}

if (masked) {
failWebsocketConnection(this.ws, 'Frame cannot be masked')
failWebsocketConnection(this.#handler, 'Frame cannot be masked')
return callback()
}

Expand All @@ -105,43 +109,43 @@ class ByteParser extends Writable {
// 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.ws, 'Expected RSV1 to be clear.')
failWebsocketConnection(this.#handler, 'Expected RSV1 to be clear.')
return
}

if (rsv2 !== 0 || rsv3 !== 0) {
failWebsocketConnection(this.ws, 'RSV1, RSV2, RSV3 must be clear')
failWebsocketConnection(this.#handler, 'RSV1, RSV2, RSV3 must be clear')
return
}

if (fragmented && !isTextBinaryFrame(opcode)) {
// Only text and binary frames can be fragmented
failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.')
failWebsocketConnection(this.#handler, '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.ws, 'Expected continuation frame')
failWebsocketConnection(this.#handler, 'Expected continuation frame')
return
}

if (this.#info.fragmented && fragmented) {
// A fragmented frame can't be fragmented itself
failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.')
failWebsocketConnection(this.#handler, '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.ws, 'Control frame either too large or fragmented')
failWebsocketConnection(this.#handler, 'Control frame either too large or fragmented')
return
}

if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) {
failWebsocketConnection(this.ws, 'Unexpected continuation frame')
failWebsocketConnection(this.#handler, 'Unexpected continuation frame')
return
}

Expand Down Expand Up @@ -187,7 +191,7 @@ class ByteParser extends Writable {
// 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.ws, 'Received payload length > 2^31 bytes.')
failWebsocketConnection(this.#handler, 'Received payload length > 2^31 bytes.')
return
}

Expand Down Expand Up @@ -215,15 +219,15 @@ class ByteParser extends Writable {
// parsing continuation frames, not here.
if (!this.#info.fragmented && this.#info.fin) {
const fullMessage = Buffer.concat(this.#fragments)
websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
websocketMessageReceived(this.#handler, this.#info.binaryType, fullMessage)
this.#fragments.length = 0
}

this.#state = parserStates.INFO
} else {
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
if (error) {
closeWebSocketConnection(this.ws, 1007, error.message, error.message.length)
closeWebSocketConnection(this.#handler, 1007, error.message, error.message.length)
return
}

Expand All @@ -236,7 +240,7 @@ class ByteParser extends Writable {
return
}

websocketMessageReceived(this.ws, this.#info.binaryType, Buffer.concat(this.#fragments))
websocketMessageReceived(this.#handler, this.#info.binaryType, Buffer.concat(this.#fragments))

this.#loop = true
this.#state = parserStates.INFO
Expand Down Expand Up @@ -339,7 +343,7 @@ class ByteParser extends Writable {

if (opcode === opcodes.CLOSE) {
if (payloadLength === 1) {
failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.')
failWebsocketConnection(this.#handler, 'Received close frame with a 1-byte body.')
return false
}

Expand All @@ -348,8 +352,8 @@ class ByteParser extends Writable {
if (this.#info.closeInfo.error) {
const { code, reason } = this.#info.closeInfo

closeWebSocketConnection(this.ws, code, reason, reason.length)
failWebsocketConnection(this.ws, reason)
closeWebSocketConnection(this.#handler, code, reason, reason.length)
failWebsocketConnection(this.#handler, reason)
return false
}

Expand Down
Loading

0 comments on commit de89fde

Please sign in to comment.