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

separate whatwg websocket logic from rfc 6455 #3396

Merged
merged 5 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading