| 'use strict' |
| |
| const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = require('./symbols') |
| const { states, opcodes } = require('./constants') |
| const { MessageEvent, ErrorEvent } = require('./events') |
| |
| /* globals Blob */ |
| |
| /** |
| * @param {import('./websocket').WebSocket} ws |
| */ |
| function isEstablished (ws) { |
| // If the server's response is validated as provided for above, it is |
| // said that _The WebSocket Connection is Established_ and that the |
| // WebSocket Connection is in the OPEN state. |
| return ws[kReadyState] === states.OPEN |
| } |
| |
| /** |
| * @param {import('./websocket').WebSocket} ws |
| */ |
| function isClosing (ws) { |
| // 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. |
| return ws[kReadyState] === states.CLOSING |
| } |
| |
| /** |
| * @param {import('./websocket').WebSocket} ws |
| */ |
| function isClosed (ws) { |
| return ws[kReadyState] === states.CLOSED |
| } |
| |
| /** |
| * @see https://dom.spec.whatwg.org/#concept-event-fire |
| * @param {string} e |
| * @param {EventTarget} target |
| * @param {EventInit | undefined} eventInitDict |
| */ |
| function fireEvent (e, target, eventConstructor = Event, eventInitDict) { |
| // 1. If eventConstructor is not given, then let eventConstructor be Event. |
| |
| // 2. Let event be the result of creating an event given eventConstructor, |
| // in the relevant realm of target. |
| // 3. Initialize event’s type attribute to e. |
| const event = new eventConstructor(e, eventInitDict) // eslint-disable-line new-cap |
| |
| // 4. Initialize any other IDL attributes of event as described in the |
| // invocation of this algorithm. |
| |
| // 5. Return the result of dispatching event at target, with legacy target |
| // override flag set if set. |
| target.dispatchEvent(event) |
| } |
| |
| /** |
| * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol |
| * @param {import('./websocket').WebSocket} ws |
| * @param {number} type Opcode |
| * @param {Buffer} data application data |
| */ |
| function websocketMessageReceived (ws, type, data) { |
| // 1. If ready state is not OPEN (1), then return. |
| if (ws[kReadyState] !== states.OPEN) { |
| return |
| } |
| |
| // 2. Let dataForEvent be determined by switching on type and binary type: |
| let dataForEvent |
| |
| if (type === opcodes.TEXT) { |
| // -> type indicates that the data is Text |
| // a new DOMString containing data |
| try { |
| dataForEvent = new TextDecoder('utf-8', { fatal: true }).decode(data) |
| } catch { |
| failWebsocketConnection(ws, 'Received invalid UTF-8 in text frame.') |
| return |
| } |
| } else if (type === opcodes.BINARY) { |
| if (ws[kBinaryType] === 'blob') { |
| // -> type indicates that the data is Binary and binary type is "blob" |
| // a new Blob object, created in the relevant Realm of the WebSocket |
| // object, that represents data as its raw data |
| dataForEvent = new Blob([data]) |
| } else { |
| // -> type indicates that the data is Binary and binary type is "arraybuffer" |
| // a new ArrayBuffer object, created in the relevant Realm of the |
| // WebSocket object, whose contents are data |
| dataForEvent = new Uint8Array(data).buffer |
| } |
| } |
| |
| // 3. Fire an event named message at the WebSocket object, using MessageEvent, |
| // with the origin attribute initialized to the serialization of the WebSocket |
| // object’s url's origin, and the data attribute initialized to dataForEvent. |
| fireEvent('message', ws, MessageEvent, { |
| origin: ws[kWebSocketURL].origin, |
| data: dataForEvent |
| }) |
| } |
| |
| /** |
| * @see https://datatracker.ietf.org/doc/html/rfc6455 |
| * @see https://datatracker.ietf.org/doc/html/rfc2616 |
| * @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407 |
| * @param {string} protocol |
| */ |
| function isValidSubprotocol (protocol) { |
| // If present, this value indicates one |
| // or more comma-separated subprotocol the client wishes to speak, |
| // ordered by preference. The elements that comprise this value |
| // MUST be non-empty strings with characters in the range U+0021 to |
| // U+007E not including separator characters as defined in |
| // [RFC2616] and MUST all be unique strings. |
| if (protocol.length === 0) { |
| return false |
| } |
| |
| for (const char of protocol) { |
| const code = char.charCodeAt(0) |
| |
| if ( |
| code < 0x21 || |
| code > 0x7E || |
| char === '(' || |
| char === ')' || |
| char === '<' || |
| char === '>' || |
| char === '@' || |
| char === ',' || |
| char === ';' || |
| char === ':' || |
| char === '\\' || |
| char === '"' || |
| char === '/' || |
| char === '[' || |
| char === ']' || |
| char === '?' || |
| char === '=' || |
| char === '{' || |
| char === '}' || |
| code === 32 || // SP |
| code === 9 // HT |
| ) { |
| return false |
| } |
| } |
| |
| return true |
| } |
| |
| /** |
| * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4 |
| * @param {number} code |
| */ |
| function isValidStatusCode (code) { |
| if (code >= 1000 && code < 1015) { |
| return ( |
| code !== 1004 && // reserved |
| code !== 1005 && // "MUST NOT be set as a status code" |
| code !== 1006 // "MUST NOT be set as a status code" |
| ) |
| } |
| |
| return code >= 3000 && code <= 4999 |
| } |
| |
| /** |
| * @param {import('./websocket').WebSocket} ws |
| * @param {string|undefined} reason |
| */ |
| function failWebsocketConnection (ws, reason) { |
| const { [kController]: controller, [kResponse]: response } = ws |
| |
| controller.abort() |
| |
| if (response?.socket && !response.socket.destroyed) { |
| response.socket.destroy() |
| } |
| |
| if (reason) { |
| fireEvent('error', ws, ErrorEvent, { |
| error: new Error(reason) |
| }) |
| } |
| } |
| |
| module.exports = { |
| isEstablished, |
| isClosing, |
| isClosed, |
| fireEvent, |
| isValidSubprotocol, |
| isValidStatusCode, |
| failWebsocketConnection, |
| websocketMessageReceived |
| } |