From 63df2bc1b2c4b4598201e71a16863ef8a458f318 Mon Sep 17 00:00:00 2001 From: prettydiff Date: Sun, 3 Sep 2023 20:12:31 -0400 Subject: [PATCH 01/15] websocket: proposal for a new core module This is a proposal to add websockets as a new module for Node.js. This implementation attempts to adhere to RFC 6455. If differences are identified I will supply the necessary changes. This implementation does not attempt to parse connection header values, such as extensions or sub-protocols values. This proposal focuses exclusively on Node's APIs from its core modules and then supplies additional options as necessary to populate callbacks and RFC 6455 connection header values. Some notes about performance. Everybody that writes a websocket library wants to measure performance in terms of message sent speed, which seems to be a red herring. Message send speed is trivial compared to message receive speed. Message send speed appears to be a memory bound operation. Using this approach to websockets I send at about 180,000 messages per second on my old desktop computer with slow DDR3 memory. On my newer laptop with faster DDR4 memory I can send at about 460,000 - 480,000 messages per second. This speed is largely irrelevant though because at around 460,000 messages garbage collection kicks in and message send speed slows to a crawl at around 5,000 per second. Message receive speed is much slower and far more dependent upon the speed of the CPU. On my old desktop I can receive messages at a speed of around 18,000 messages per second while on my laptop its a bit slower at around 12,000-15,000 messages per second because the desktop has a more powerful CPU. The speed penalty on message reception appears to be due to analysis for message fragmentation. This approach accounts for 4 types of message fragmentation. --- doc/api/websocket.md | 242 +++++++++++++ lib/websocket.js | 844 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1086 insertions(+) create mode 100644 doc/api/websocket.md create mode 100644 lib/websocket.js diff --git a/doc/api/websocket.md b/doc/api/websocket.md new file mode 100644 index 00000000000000..944914ac5f2587 --- /dev/null +++ b/doc/api/websocket.md @@ -0,0 +1,242 @@ +# WebSocket + + + +> Stability: 1 - Experimental + + + + + +The `node:websocket` library allows for the creation of RFC 6455 conformant +WebSocket client sockets and servers both secure and insecure. + +This module supports, as currently written, asynchronous execution with +callbacks only. + +## Callback example + +### Client TLS Socket example +```javascript +const options = { + authentication: 'auth-string', + callbackOpen: function(err, socket) { + console.log("client callbackOpen"); + if (err === null) { + socket.messageSend("hello from client"); + } + }, + masking: false, + id: 'id-string', + messageHandler: function(message){ + console.log("message received at client"); + console.log(message.toString()); + }, + 'proxy-authorization': 'proxy-auth', + subProtocol: 'sub-proto', + type: 'type-string', + secure: true, + socketOptions: { + host: host, + port: port, + rejectUnauthorized: false + } +}; +websocket.clientConnect(options); +``` + +### Client Net Socket example +```javascript +const options = { + authentication: 'auth-string', + callbackOpen: function(err, socket) { + console.log("client callbackOpen"); + if (err === null) { + socket.messageSend("hello from client"); + } + }, + masking: false, + id: 'id-string', + messageHandler: function(message){ + console.log("message received at client"); + console.log(message.toString()); + }, + 'proxy-authorization': 'proxy-auth', + subProtocol: 'sub-proto', + type: 'type-string', + secure: false, + socketOptions: { + host: host, + port: port + } +}; +websocket.clientConnect(options); +``` + +### Server TLS example +```javascript +const options = { + callbackConnect: function(headerValues, socket, ready) { + console.log(headerValues); + ready(); + }, + callbackListener: function(server) { + console.log("server callbackListener"); + }, + callbackOpen: function(err, socket) { + console.log("server callbackOpen"); + }, + messageHandler: function(message) { + console.log("message received at server"); + console.log(message.toString()); + }, + listenerOptions: { + host: host, + port: port + }, + secure: true, + serverOptions: { + ca: caCertificate, + cert: domainCertificate, + key: domainKey + } +}; +const server = ws.server(options); +``` + +### Server Net example +```javascript +const options = { + callbackConnect: function(headerValues, socket, ready) { + console.log(headerValues); + ready(); + }, + callbackListener: function(server) { + console.log("server callbackListener"); + }, + callbackOpen: function(err, socket) { + console.log("server callbackOpen"); + }, + messageHandler: function(message) { + console.log("message received at server"); + console.log(message.toString()); + }, + listenerOptions: { + host: host, + port: port + }, + secure: false, + serverOptions: {} +}; +const server = ws.server(options); +``` + +## API + + + +The websocket module exists to provide primitive WebSocket protocol support as +defined by [RFC 6455](https://www.rfc-editor.org/rfc/rfc6455). + +### Class: `clientConnect` + + + +The `clientConnect` class extends class +[net.Socket](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#class-netsocket) +or +[tls.TLSSocket](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#class-tlstlssocket) +to create a WebSocket socket and connects it to a WebSocket server. + +* `options` {object} + * `callbackOpen` {Function} If present will execute upon completion of + WebSocket connection handshake. Receives two arguments: `err` + {NodeJS.ErrnoException} and `socket` {websocketClient}. + **Default:** `undefined` + * `extensions` {string} Any additional instructions, identifiers, or custom + descriptive data. + * `masking` {boolean} Whether to apply RFC 6455 message masking. + **Default:** `true` + * `messageHandler` {Function} Received messages are passed into this function + for custom processing. When this function is absent received messages are + discarded. Receives one argument: `message` {Buffer}. + * `proxy-authorization` {string} Any identifier required by proxy + authorization mechanism. + * `secure` {boolean} Whether to create a TLS based socket or Net based + socket. **Default:** `true` + * `socketOptions` {object} See + [net.connect](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#netconnect) + or + [tls.connect](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#tlsconnectoptions-callback). + * `subProtocol` {string} Any one or more RFC 6455 identified sub-protocols. +* Returns {websocketClient} + +### Class: `getAddress` + + + +A convenience utility for attaining addressing data from any network socket. + +* `socket` {websocketClient} +* Returns {object} of form: + * `local` + * `address` {string} An IP address. + * `port` {number} A port. + * `remote` + * `address` {string} An IP address. + * `port` {number} A port. + +### `magicString` + + + +* {string} A static string required by RFC 6455 to internally create and prove + the connection handshake. + +### Class: `server` + + + +The `server` class extends class +[net.Server](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#class-netserver) +or +[tls.Server](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#class-tlsserver) +to create a WebSocket server listening for connecting sockets. Any socket that +fails to complete the handshake within 5 seconds of connecting to the server +will be destroyed. + +* `options` {object} + * `callbackConnect` {Function} A callback to execute when a socket connects + to the server, but before the handshake completes. This function provides a + means to apply authentication or additional description before completing + the handshake and allowing messaging. Receives 3 arguments: `headerValues` + {object}, `socket` {websocketClient}, `ready` {Function}. The third + argument must be called by the callbackConnect function in order for the + handshake to complete. + * `callbackListener` {Function} A callback that executes once the server + starts listening for incoming socket connections. Provides 1 argument: + `server` {Server}. + * `callbackOpen` {Function} If present will execute upon completion of + WebSocket connection handshake. Receives two arguments: `err` + {NodeJS.ErrnoException} and `socket` {websocketClient}. + * `messageHandler` {Function} Received messages are passed into this function + for custom processing. When this function is absent received messages are + discarded. Receives one argument: `message` {Buffer}. + * `listenerOptions` {object} See + [net.server.listen](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#serverlistenoptions-callback). + * `secure` Whether to create a net.Server or a tls.TLSServer. **Default:** + true + * `serverOptions` {object} See + [net.createServer](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#netcreateserveroptions-connectionlistener) + or + [tls.createServer](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#tlscreateserveroptions-secureconnectionlistener). \ No newline at end of file diff --git a/lib/websocket.js b/lib/websocket.js new file mode 100644 index 00000000000000..c16d8810b92b68 --- /dev/null +++ b/lib/websocket.js @@ -0,0 +1,844 @@ + +const crypto = require('node:crypto'); +const net = require('node:net'); +const os = require('node:os'); +const tls = require('node:tls'); + + +/** + * ```typescript + * interface extensions { + * // A callback that fires once the connection handshake completes. + * callbackOpen?: ( + * err:NodeJS.ErrnoException, + * socket:Socket|TLSSocket + * ) => void; + * // Extensions are the place to provide custom identifiers, security + * // tokens, and other descriptors. + * extensions?: string; + * // eliminates use of message masking as required by default for + * // client-side sockets. + * // Default: `true` + * masking?: boolean; + * // Arbitrary unique identifier of user's choosing. + * messageHandler?: (message:Buffer) => void; + * // If connecting through a proxy that requires an authentication token. + * proxy-authorization?: string; + * // A comma separated list of one or more known subprotocol values per + * // RFC 6455 11.2. + * subProtocol?: string; + * } + * interface clientOptions extends extensions { + * // Whether to create a Socket or TLSSocket type socket. Defaults to + * // `true` + * secure?: boolean; + * // Required options to create a Node socket. + * socketOptions: net.NetConnectOpts | tls.ConnectionOptions; + * } + * (options:clientOptions) => Socket | TLSSocket; + * ``` */ + +// creates and connects a websocket client +function clientConnect(options) { + const socket = (options.secure === false) ? + net.connect(options.socketOptions) : + tls.connect(options.socketOptions); + // RFC 6455 1.7 says the only relation to HTTP is that a valid handshake + // be sent and received in the form of a HTTP 1.1 GET header, so this + // application will send the conforming header text and otherwise avoid + // the overhead of HTTP, which will greatly boost execution performance. + function handlerReady() { + // response from server + socket.once('data', function(responseData) { + const hash = crypto.createHash('sha1'); + const responseString = responseData.toString(); + const response = responseString.replace(/\r\n/g, '\n').split('\n'); + const len = response.length; + function callbackCheck() { + if (typeof options.callbackOpen === 'function') { + options.callbackOpen(errorObject( + 'ECONNABORTED', + 'WebSocket server handshake response returned no ' + + 'Sec-WebSocket-Accept header or one which is malformed.', + socket, + 'websocket.clientConnect' + ), null); + } + } + // check if response contains required HTTP header + if (len > 1 && responseString.includes('HTTP/1.1 101 Switching Protocols')) { + let index = 0; + let key = ''; + let colon = 0; + do { + colon = response[index].indexOf(':'); + // find the header dealing with the handshake + if (response[index].slice(0, colon).toLowerCase() === 'sec-websocket-accept') { + key = response[index].slice(colon + 1).replace(/^\s+/, '').replace(/\s+$/, ''); + break; + } + index = index + 1; + } while (index < len); + hash.update(nonceSource + websocket.magicString); + const digest = hash.digest('hex');//console.log(digest);console.log(key); + // validate handshake + if (digest === key) { + delete options.secure; + delete options.socketOptions; + socketExtend(socket, options); + } else { + callbackCheck(); + } + } else { + callbackCheck(); + } + }); + const addresses = getAddress(socket); + const resourceName = (typeof options.socketOptions.host === 'string') ? + (function() { + const host = options.socketOptions.host; + const scheme = (/^\w+:\/\//); + if (typeof host !== 'string') { + return '/'; + } + if (scheme.test(host) === false) { + return '/'; + } + host.replace(scheme, ''); + if (host.indexOf('/') < 0 || host.indexOf('/') === host.length - 1) { + return '/'; + } + return `/${host.slice(host.indexOf('/') + 1)}`; + }()) : + '/'; + const nonceSource = Buffer.from(Date.now().toString() + os.hostname()).toString('base64'); + const header = [ + `GET ${resourceName} HTTP/1.1`, + (addresses.remote.address.indexOf(':') > -1) ? + // RFC 6455 4.1. request host be defined according to RFC 3986 (URI) + // however it also requires the socket be already established and open + // at layer 4 (TCP/TLS) and layer 4 cares only for IP address as URI + // is layer 7. For sanity ip/port is fully sufficient if derived from + // the established socket + `Host: [${addresses.remote.address}]:${addresses.remote.port}` : + `Host: ${addresses.remote.address}:${addresses.remote.port}`, + 'Upgrade: websocket', + 'Connection: Upgrade', + 'Sec-WebSocket-Version: 13', + `Sec-WebSocket-Key: ${nonceSource}`, + `User-Agent: Node.js--${process.version}--${os.version()}--${os.release()}` + ]; + function headingCheck(heading) { + if (typeof options[heading] === 'string') { + if (heading === 'extensions') { + header.push(`Sec-WebSocket-Extensions: ${options.extensions}`); + } else if (heading === 'subProtocol') { + header.push(`Sec-WebSocket-Protocol: ${options[heading]}`); + } else { + header.push(`Proxy-Authorization: ${options[heading]}`); + } + } + } + headingCheck('extensions'); + headingCheck('proxy-authorization'); + headingCheck('subProtocol'); + options.role = 'client'; + + header.push(''); + header.push(''); + // last use of socket.write before its hidden in socket extensions + socket.write(header.join('\r\n')); + } + + // prevents a crashing server from breaking the client node instance + socket.on('error', function() {}); + + // wait for layer 4 socket connection + socket.once('ready', handlerReady); + return socket; +} + +// a uniform convenience function for generating error objects +function errorObject(code, message, socket, syscall) { + const addresses = getAddress(socket).remote; + return new Error({ + address: addresses.address, + code: code, + errno: 0, + message: message, + port: addresses.port, + syscall: syscall + }); +} + +// a handy utility to conveniently gather a socket's connection identity +function getAddress(socket) { + function parse(input) { + if (input === undefined) { + return 'undefined, possibly due to socket closing'; + } + if (input.indexOf('::ffff:') === 0) { + return input.replace('::ffff:', ''); + } + if (input.indexOf(':') > 0 && input.indexOf('.') > 0) { + return input.slice(0, input.lastIndexOf(':')); + } + return input; + } + return { + local: { + address: parse(socket.localAddress), + port: socket.localPort + }, + remote: { + address: parse(socket.remoteAddress), + port: socket.remotePort + } + }; +} + +// send a message payload in conformance to RFC 6455 +function messageSend(message, opcode, fragmentSize) { + const socket = this; + function writeFrame() { + function writeCallback() { + socket.websocket.queue.splice(0, 1); + if (socket.websocket.queue.length > 0) { + writeFrame(); + } else { + socket.websocket.status = 'open'; + } + }; + if (socket.websocket.status === 'open') { + socket.websocket.status = 'pending'; + } + if (socket.internalWrite(socket.websocket.queue[0]) === true) { + writeCallback(); + } else { + socket.once('drain', writeCallback); + } + }; + function mask(body) { + const mask = Buffer.alloc(4); + const rand = Buffer.from(Math.random().toString()); + mask[0] = rand[4]; + mask[1] = rand[5]; + mask[2] = rand[6]; + mask[3] = rand[7]; + // RFC 6455, 5.3. Client-to-Server Masking + // j = i MOD 4 + // transformed-octet-i = original-octet-i XOR masking-key-octet-j + body.forEach(function(value, index) { + body[index] = value ^ mask[index % 4]; + }); + return [body, mask]; + } + // OPCODES + // ## Messages + // 0 - continuation - fragments of a message payload following an initial + // fragment + // 1 - text message + // 2 - binary message + // 3-7 - reserved for future use + // + // ## Control Frames + // 8 - close, the remote is destroying the socket + // 9 - ping, a connectivity health check + // a - pong, a response to a ping + // b-f - reserved for future use + // + // ## Notes + // * Message frame fragments must be transmitted in order and not interleaved + // with other messages. + // * Message types may be supplied as buffer or socketData types, but will + // always be transmitted as buffers. + // * Control frames are always granted priority and may occur between fragments + // of a single message. + // * Control frames will always be supplied as buffer data types. + // + // ## Masking + // * All traffic coming from the browser will be websocket masked. + // * I have not tested if the browsers will process masked data as they + // shouldn't according to RFC 6455. + // * This application supports both masked and unmasked transmission so long + // as the mask bit is set and a 32bit mask key is supplied. + // * Mask bit is set as payload length (up to 127) + 128 assigned to frame + // header second byte. + // * Mask key is first 4 bytes following payload length bytes (if any). + if (typeof opcode !== 'number') { + opcode = 1; + } else { + opcode = Math.floor(opcode); + if (opcode < 0 || opcode > 15) { + opcode = 1; + } + } + if (opcode === 1 && Buffer.isBuffer(message) === true) { + opcode = 2; + } + if (typeof fragmentSize !== 'number' || fragmentSize < 1) { + fragmentSize = 0; + } + if (opcode === 1 || opcode === 2 || opcode === 3 || opcode === 4 || opcode === 5 || opcode === 6 || opcode === 7) { + let maskKey = null; + function fragmentation(first) { + let finish = false; + const frameBody = (function() { + if (fragmentSize < 1 || len === fragmentSize) { + finish = true; + if (socket.websocket.masking === true) { + const masked = mask(dataPackage); + maskKey = masked[1]; + return masked[0]; + } + return dataPackage; + } + const fragment = dataPackage.subarray(0, fragmentSize); + dataPackage = dataPackage.subarray(fragmentSize); + len = dataPackage.length; + if (len < fragmentSize) { + finish = true; + } + if (socket.websocket.masking === true) { + const masked = mask(fragment); + maskKey = masked[1]; + return masked[0]; + } + return fragment; + }()); + const size = frameBody.length; + const frameHeader = (function() { + // frame 0 is: + // * 128 bits for fin, 0 for unfinished plus opcode + // * opcode 0 - continuation of fragments + // * opcode 1 - text (total payload must be UTF8 and probably not contain hidden + // control characters) + // * opcode 2 - supposed to be binary, really anything that isn't 100& UTF8 text + // ** for fragmented data only first data frame gets a data opcode, others + // receive 0 (continuity) + const frame = (size < 126) ? + (socket.websocket.masking === true) ? + Buffer.alloc(6) : + Buffer.alloc(2) : + (size < 65536) ? + (socket.websocket.masking === true) ? + Buffer.alloc(8) : + Buffer.alloc(4) : + (socket.websocket.masking === true) ? + Buffer.alloc(14) : + Buffer.alloc(10); + frame[0] = (finish === true) ? + (first === true) ? + 128 + opcode : + 128 : + (first === true) ? + opcode : + 0; + // frame 1 is mask bit + length flag + frame[1] = (size < 126) ? + (socket.websocket.masking === true) ? + size + 128 : + size : + (size < 65536) ? + (socket.websocket.masking === true) ? + 254 : + 126 : + (socket.websocket.masking === true) ? + 255 : + 127; + // write payload length followed by mask key + if (size > 125) { + if (size < 65536) { + frame.writeUInt16BE(size, 2); + if (socket.websocket.masking === true) { + frame[6] = maskKey[0]; + frame[7] = maskKey[1]; + frame[8] = maskKey[2]; + frame[9] = maskKey[3]; + } + } else { + frame.writeUIntBE(size, 4, 6); + if (socket.websocket.masking === true) { + frame[10] = maskKey[0]; + frame[11] = maskKey[1]; + frame[12] = maskKey[2]; + frame[13] = maskKey[3]; + } + } + } else if (socket.websocket.masking === true) { + frame[2] = maskKey[0]; + frame[3] = maskKey[1]; + frame[4] = maskKey[2]; + frame[5] = maskKey[3]; + } + return frame; + }()); + socket.websocket.queue.push(Buffer.concat([frameHeader, frameBody])); + if (finish === true) { + if (socket.websocket.status === 'open') { + writeFrame(); + } + } else { + fragmentation(false); + } + }; + let dataPackage = (Buffer.isBuffer === true) ? + message : + Buffer.from(message); + let len = dataPackage.length; + fragmentation(true); + } else if (opcode === 8 || opcode === 9 || opcode === 10 || opcode === 11 || opcode === 12 || opcode === 13 || opcode === 14 || opcode === 15) { + let frameBody = message.subarray(0, 125); + let frameHeader = null; + if (socket.websocket.masking === true) { + const masked = mask(frameBody); + frameHeader = Buffer.alloc(6); + // opcode + fin bit, rsv bits set to 0 + frameHeader[0] = 128 + opcode; + // set the mask bit + frameHeader[1] = 128 + frameBody.length; + // set the mask key + frameHeader[2] = masked[1][0]; + frameHeader[3] = masked[1][1]; + frameHeader[4] = masked[1][2]; + frameHeader[5] = masked[1][3]; + // assign the masked payload + frameBody = masked[0]; + } else { + frameHeader = Buffer.alloc(2); + // opcode + fin bit, rsv bits set to 0 + frameHeader[0] = 128 + opcode; + frameHeader[1] = frameBody.length; + } + // control frames send immediately, out of sequence + socket.websocket.queue.unshift(Buffer.concat([frameHeader, frameBody])); + + if (socket.websocket.status === 'open') { + writeFrame(); + } + } +} + +// arbitrary ping function, which may be called by any means at any time +function ping(ttl, callback) { + const socket = this; + function errorObject(code, message) { + const err = new Error(); + err.code = code; + err.message = `${message} Socket type ${config.socket.type} and name ${name}.`; + return err; + } + if (socket.websocket.status !== 'open' && socket.websocket.status !== 'pending') { + callback(errorObject( + 'ECONNABORTED', + 'Ping error on websocket without \'open\' or \'pending\' status.', + socket, + 'websocket.ping' + ), null); + } else { + const nameSlice = socket.hash.slice(0, 125); + // send ping + transmit_ws.queue(Buffer.from(nameSlice), config.socket, 9); + socket.pong = { + callback: callback, + start: process.hrtime.bigint(), + timeOut: setTimeout(function() { + callback(socket.websocket.pong.timeOutMessage, null); + delete socket.websocket.pong; + }, ttl), + timeOutMessage: errorObject('ETIMEDOUT', 'Ping timeout on websocket.'), + ttl: BigInt(ttl * 1e6) + }; + } +} + +// processes incoming messages +function receive(socket) { + function processor(buf) { + // RFC 6455, 5.2. Base Framing Protocol + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-------+-+-------------+-------------------------------+ + // |F|R|R|R| opcode|M| Payload len | Extended payload length | + // |I|S|S|S| (4) |A| (7) | (16/64) | + // |N|V|V|V| |S| | (if payload len==126/127) | + // | |1|2|3| |K| | | + // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + // | Extended payload length continued, if payload len == 127 | + // + - - - - - - - - - - - - - - - +-------------------------------+ + // | |Masking-key, if MASK set to 1 | + // +-------------------------------+-------------------------------+ + // | Masking-key (continued) | Payload Data | + // +-------------------------------- - - - - - - - - - - - - - - - + + // : Payload Data continued ... : + // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // | Payload Data continued ... | + // +---------------------------------------------------------------+ + + function unmask(input) { + if (frame.mask === true) { + // RFC 6455, 5.3. Client-to-Server Masking + // j = i MOD 4 + // transformed-octet-i = original-octet-i XOR masking-key-octet-j + input.forEach(function(value, index) { + input[index] = value ^ frame.maskKey[index % 4]; + }); + } + return input; + }; + // identify payload extended length + const extended = function(input) { + const mask = (input[1] > 127), + len = (mask === true) ? + input[1] - 128 : + input[1], + keyOffset = (mask === true) ? + 4 : + 0; + if (len < 126) { + return { + lengthExtended: len, + lengthShort: len, + mask: mask, + startByte: 2 + keyOffset + }; + } + if (len < 127) { + return { + lengthExtended: input.subarray(2, 4).readUInt16BE(0), + lengthShort: len, + mask: mask, + startByte: 4 + keyOffset + }; + } + return { + lengthExtended: input.subarray(4, 10).readUIntBE(0, 6), + lengthShort: len, + mask: mask, + startByte: 10 + keyOffset + }; + }; + // populates data from the incoming network buffer with no assumptions of + // completeness + const data = (function() { + if (buf !== null && buf !== undefined) { + socket.websocket.frame = Buffer.concat([socket.websocket.frame, buf]); + } + if (socket.websocket.frame.length < 2) { + return null; + } + return socket.websocket.frame; + }()); + // interprets the frame header from Buffer to an object + const frame = (function() { + if (data === null) { + return null; + } + // bit string - convert byte number (0 - 255) to 8 bits + const bits0 = data[0].toString(2).padStart(8, '0'); + const meta = extended(data); + return { + fin: (data[0] > 127), + rsv1: (bits0.charAt(1) === '1'), + rsv2: (bits0.charAt(2) === '1'), + rsv3: (bits0.charAt(3) === '1'), + opcode: ( + (Number(bits0.charAt(4)) * 8) + + (Number(bits0.charAt(5)) * 4) + + (Number(bits0.charAt(6)) * 2) + + Number(bits0.charAt(7)) + ), + mask: meta.mask, + len: meta.lengthShort, + extended: meta.lengthExtended, + maskKey: (meta.mask === true) ? + data.subarray(meta.startByte - 4, meta.startByte) : + null, + startByte: meta.startByte + }; + }()); + const payload = (function() { + // Payload processing must contend with these 4 constraints: + // 1. Message Fragmentation - RFC6455 allows messages to be fragmented from a + // single transmission into multiple transmission + // frames independently sent and received. + // 2. Header Separation - Firefox sends frame headers separated from frame + // bodies. + // 3. Node Concatenation - If Node.js receives message frames too quickly the + // various binary buffers are concatenated into a + // single deliverable to the processing application. + // 4. TLS Max Packet Size - TLS forces a maximum payload size of 65536 bytes. + if (frame === null) { + return null; + } + let complete = null; + const size = frame.extended + frame.startByte; + const len = socket.websocket.frame.length; + if (len < size) { + return null; + } + complete = unmask(socket.websocket.frame.subarray(frame.startByte, size)); + socket.websocket.frame = socket.websocket.frame.subarray(size); + + return complete; + }()); + + if (payload === null) { + return; + } + + if (frame.opcode === 8) { + // socket close + data[0] = 136; + data[1] = (data[1] > 127) ? + data[1] - 128 : + data[1]; + const payload = Buffer.concat([data.subarray(0, 2), unmask(data.subarray(2))]); + socket.write(payload); + socket.off('data', processor); + } else if (frame.opcode === 9) { + // respond to 'ping' as 'pong' + socket.send(data.subarray(frame.startByte), socket, 10); + } else if (frame.opcode === 10) { + // pong + const payloadString = payload.toString(); + const pong = socket.websocket.pong[payloadString]; + const time = process.hrtime.bigint(); + if (pong !== undefined) { + if (time < pong.start + pong.ttl) { + clearTimeout(pong.timeOut); + pong.callback(null, time - pong.start); + } + delete socket.websocket.pong[payloadString]; + } + } else { + const segment = Buffer.concat([socket.websocket.fragment, payload]); + // this block may include frame.opcode === 0 - a continuation frame + socket.websocket.frameExtended = frame.extended; + if (frame.fin === true) { + if (typeof socket.websocket.messageHandler === 'function') { + socket.websocket.messageHandler(segment.subarray(0, socket.websocket.frameExtended)); + } + socket.websocket.fragment = segment.subarray(socket.websocket.frameExtended); + } else { + socket.websocket.fragment = segment; + } + } + if (socket.websocket.frame.length > 2) { + processor(null); + } + }; + socket.on('data', processor); +} + +/** + * ```typescript + * interface serverOptions { + * // Optional callback for when a socket connects to the server, before + * // completion of the handshake. + * // Insert custom socket extensions or authentication logic here. + * // The callbackConnect function must call `extend()` in order to + * // complete the connection handshake. + * callbackConnect?: ( + * headerValues:headerValues, + * socket:Socket|TLSSocket, + * ready:() => void, + * ) => void; + * // Optional callback for when server begins to listen for sockets. + * callbackListener?: (server:Server) => void; + * // Optional callback that fires once the connection handshake completes. + * callbackOpen?: (err:NodeJS.ErrnoException, socket:Socket|TLSSocket) => void; + * // A handler to receive processed messages. If not a function incoming + * // messages are ignored. + * messageHandler?: (message:Buffer) => void; + * // Options for the server's listener event emitter. + * listenerOptions: ListenerOptions; + * // Whether to invoke tls.createServer or net.createServer. + * secure?: boolean; + * // Node core options object for net.createServer or tls.createServer. + * serverOptions?: ServerOpts | TlsOptions; + * } + * (options:serverOptions) => Server; + * ``` */ +// creates a websocket server +function server(options) { + function connection(socket) { + // prevents a closing socket from crashing the server + socket.on('error', function () {}); + // socket handshake must be processed within 5 seconds of connection + // this is a security precaution to prevent overloading servers + const deathDelay = setTimeout(function() { + socket.destroy(); + }, 5000); + // the first data must be the HTTP handshake, otherwise destroy + socket.once('data', function(data) { + // we expect data to be the handshake payload in HTTP form + const headerValues = { + authentication: '', + id: '', + key: '', + subprotocol: '', + type: '', + userAgent: '', + }; + const dataString = data.toString(); + const lowString = dataString.toLowerCase(); + const headerList = dataString.split('\r\n'); + const flags = { + complete: false, + extensions: (lowString.includes('\r\nsec-websocket-extensions:')) ? + false : + true, + key: false, + subprotocol: (lowString.includes('\r\nsec-webSocket-protocol:')) ? + false : + true, + userAgent: (lowString.includes('\r\nuser-agent:')) ? + false : + true, + }; + function complete() { + if ( + flags.authentication === true && + flags.extensions === true && + flags.subprotocol === true && + flags.userAgent === true && + flags.complete === false + ) { + function extend() { + // send the handshake response + const headers = [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade', + `Sec-WebSocket-Accept: ${headerValues.key}`, + `Sec-WebSocket-Protocol: ${headerValues + .subprotocol.split(',')[0] + .replace(/^\s+/, '') + .replace(/\s+$/, '') + }`, + ]; + headers.push(''); + headers.push(''); + socket.write(headers.join('\r\n')); + + // extend the socket + socketExtend(socket, { + callbackOpen: options.callbackOpen, + extensions: headerValues.extensions, + messageHandler: options.messageHandler, + 'proxy-authorization': '', + 'role': 'server', + subprotocol: headerValues.subprotocol, + userAgent: headerValues.userAgent, + }); + } + flags.complete = true; + clearTimeout(deathDelay); + if (typeof options.callbackConnect === 'function') { + options.callbackConnect(headerValues, socket, extend); + } else { + extend(); + } + } + } + headerList.forEach(function(header) { + const index = header.indexOf(':'); + const lowHeader = header.toLowerCase().slice(0, index); + const value = header.slice(index + 1).replace(/^\s+/, ''); + if (lowHeader === 'sec-websocket-key') { + const hash = crypto.createHash('sha1'); + const key = value + websocket.magicString; + hash.update(key); + headerValues.key = hash.digest('hex'); + flags.key = true; + } else if (lowHeader === 'sec-webSocket-protocol') { + headerValues.subprotocol = value; + flags.subprotocol = true; + } else if (lowHeader === 'sec-websocket-extensions') { + headerValues.extensions = value; + flags.extensions = true; + } else if (lowHeader === 'user-agent') { + headerValues.userAgent = value; + flags.userAgent = true; + } + complete(); + }); + if (flags.complete === false) { + socket.destroy(); + clearTimeout(deathDelay); + } + }); + } + const serverInstance = (options.secure === false) ? + net.createServer(options.serverOptions) : + tls.createServer(options.serverOptions, connection); + if (options.secure === false) { + serverInstance.on('connection', connection); + } + serverInstance.listen(options.listenerOptions, function () { + if (typeof options.callbackListener === 'function') { + options.callbackListener(serverInstance); + } + }); + return serverInstance; +} + +// extends a net Socket type with additional features specific for WebSocket +function socketExtend(socket, extensions) { + function stringExtensions(name) { + socket.websocket[name] = (typeof extensions[name] === 'string') ? + extensions[name] : + ''; + } + socket.websocket = ( + extensions === null || + extensions === undefined || + typeof extensions !== 'object' + ) ? + {} : + extensions; + if (typeof socket.websocket.messageHandler === 'function') { + receive(socket); + } + // arbitrary ping utility + socket.ping = ping; + // send a message + socket.messageSend = messageSend; + socket.setKeepAlive(true, 0); + stringExtensions('extensions'); + stringExtensions('proxy-authorization'); + stringExtensions('role'); + stringExtensions('userAgent'); + socket.websocket.masking = (typeof extensions.masking === 'boolean') ? + extensions.masking : + extensions.role === 'client' ? + true : + false; + // storehouse of complete received data frames + // a frame is a message fragment considering for continuation frames + socket.websocket.fragment = Buffer.from([]); + // stores pieces of frames for assembly into complete frames + socket.websocket.frame = Buffer.from([]); + // stores the payload size of current received message payload + socket.websocket.frameExtended = 0; + // stores termination times and callbacks for pong handling + socket.websocket.pong = {}; + // stores messages for transmit in order, + // because websocket protocol cannot intermix messages + socket.websocket.queue = []; + socket.websocket.status = 'open'; + // hides the generic socket.write method to encourage use of messageSend + socket.internalWrite = socket.write; + socket.write = messageSend; + if (typeof extensions.callbackOpen === 'function') { + extensions.callbackOpen(null, socket); + } +} + +module.exports = websocket = { + clientConnect, + getAddress, + magicString: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + server: server +}; \ No newline at end of file From b32c6b68d93e175cbebfe7a8a993c935218e2cbe Mon Sep 17 00:00:00 2001 From: prettydiff Date: Sun, 3 Sep 2023 21:54:05 -0400 Subject: [PATCH 02/15] fixing lint style violations and adding further data to the documentation. --- doc/api/websocket.md | 74 +++++++++++-- lib/websocket.js | 250 ++++++++++++++++++++++++------------------- 2 files changed, 207 insertions(+), 117 deletions(-) diff --git a/doc/api/websocket.md b/doc/api/websocket.md index 944914ac5f2587..5639222297d2a8 100644 --- a/doc/api/websocket.md +++ b/doc/api/websocket.md @@ -152,7 +152,7 @@ or [tls.TLSSocket](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#class-tlstlssocket) to create a WebSocket socket and connects it to a WebSocket server. -* `options` {object} +* `options` {Object} * `callbackOpen` {Function} If present will execute upon completion of WebSocket connection handshake. Receives two arguments: `err` {NodeJS.ErrnoException} and `socket` {websocketClient}. @@ -168,7 +168,7 @@ to create a WebSocket socket and connects it to a WebSocket server. authorization mechanism. * `secure` {boolean} Whether to create a TLS based socket or Net based socket. **Default:** `true` - * `socketOptions` {object} See + * `socketOptions` {Object} See [net.connect](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#netconnect) or [tls.connect](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#tlsconnectoptions-callback). @@ -184,7 +184,7 @@ added: REPLACEME A convenience utility for attaining addressing data from any network socket. * `socket` {websocketClient} -* Returns {object} of form: +* Returns {Object} of form: * `local` * `address` {string} An IP address. * `port` {number} A port. @@ -215,12 +215,12 @@ to create a WebSocket server listening for connecting sockets. Any socket that fails to complete the handshake within 5 seconds of connecting to the server will be destroyed. -* `options` {object} +* `options` {Object} * `callbackConnect` {Function} A callback to execute when a socket connects to the server, but before the handshake completes. This function provides a means to apply authentication or additional description before completing the handshake and allowing messaging. Receives 3 arguments: `headerValues` - {object}, `socket` {websocketClient}, `ready` {Function}. The third + {Object}, `socket` {websocketClient}, `ready` {Function}. The third argument must be called by the callbackConnect function in order for the handshake to complete. * `callbackListener` {Function} A callback that executes once the server @@ -232,11 +232,69 @@ will be destroyed. * `messageHandler` {Function} Received messages are passed into this function for custom processing. When this function is absent received messages are discarded. Receives one argument: `message` {Buffer}. - * `listenerOptions` {object} See + * `listenerOptions` {Object} See [net.server.listen](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#serverlistenoptions-callback). * `secure` Whether to create a net.Server or a tls.TLSServer. **Default:** true - * `serverOptions` {object} See + * `serverOptions` {Object} See [net.createServer](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#netcreateserveroptions-connectionlistener) or - [tls.createServer](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#tlscreateserveroptions-secureconnectionlistener). \ No newline at end of file + [tls.createServer](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#tlscreateserveroptions-secureconnectionlistener). + +## Common Objects + +### websocketClient + + + +The `websocketClient` object inherits from either +[net.Socket](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#class-netsocket) +or +[tls.TLSSocket](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#class-tlstlssocket) +with these additional object properties: + +* `ping` {Function} Performs an arbitrary connection test that a user may call + at their liberty. +* `websocket` {Object} + * `callbackOpen` {Function} If present will execute upon completion of + WebSocket connection handshake. Receives two arguments: `err` + {NodeJS.ErrnoException} and `socket` {websocketClient}. + * `extensions` {string} Any additional instructions, identifiers, or custom + descriptive data. + * `fragment` {Buffer} Stores complete message frames sufficient to assemble a + complete message. + * `frame` {Buffer} Stores pieces of a frame sufficient to assemble a complete + frame. + * `frameExtended` {number} Stores the extended length value of the current + message. + * `masking` {boolean} Determines whether to mask messages before sending them. + Defaults to `true` for client roles and `false` for server roles, but default + behavior can be changes with options. + * `messageHandler` {Function} Received messages are passed into this function + for custom processing. When this function is absent received messages are + discarded. Receives one argument: `message` {Buffer}. + * `pong` Stores an object with metadata sufficient to test connectivity + initiated as a ping from the remote end. + * `proxy-authorization` Any identifier required by proxy authorization + mechanism. + * `queue` {Buffer[]} Stores messages in order so that they are sent one at a + time exactly in the order sent. + * `role` {'client'|'server'} Whether the socket is instantiated as a client + or server connection. + * `status` {'closed'|'open'|'pending'} Current transfer status of the socket. + * `closed` - Socket is not destroyed but is no longer receiving or + transmitting. + * `open` - Socket is available to send and receive messages. + * `pending` - Socket can receive messages, but is halted from sending + messages. This typically occurs because the socket is writing a message and + others are stacked up in queue. + * `subprotocol`: {string} Any sub-protocols defined by the client. + * `userAgent` {string} User agent identifier populated by the client. +* `write` {Function} Sends WebSocket messages. + * `message` {Buffer|string} The message to send. + * `opcode` {Integer} an RFC 6455 message code. **Default:** 1 if the message is + text or 2 if the message is a Buffer. + * `fragmentSize` Determines the size of message fragmentation. **Default:** 0, + which means no message fragmentation. \ No newline at end of file diff --git a/lib/websocket.js b/lib/websocket.js index c16d8810b92b68..939829ee800068 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -1,9 +1,25 @@ - +'use strict'; +const { Buffer } = require('node:buffer'); const crypto = require('node:crypto'); const net = require('node:net'); const os = require('node:os'); const tls = require('node:tls'); +const { + BigInt, + DateNow, + MathFloor, + MathRandom, + Number, +} = primordials; +const { + SystemError, +} = require('internal/errors'); +const { + clearTimeout, + setTimeout, +} = require('timers'); +const websocket = {}; /** * ```typescript @@ -36,7 +52,8 @@ const tls = require('node:tls'); * socketOptions: net.NetConnectOpts | tls.ConnectionOptions; * } * (options:clientOptions) => Socket | TLSSocket; - * ``` */ + * ``` + */ // creates and connects a websocket client function clientConnect(options) { @@ -61,18 +78,18 @@ function clientConnect(options) { 'WebSocket server handshake response returned no ' + 'Sec-WebSocket-Accept header or one which is malformed.', socket, - 'websocket.clientConnect' + 'websocket.clientConnect', ), null); } } - // check if response contains required HTTP header + // Check if response contains required HTTP header if (len > 1 && responseString.includes('HTTP/1.1 101 Switching Protocols')) { let index = 0; let key = ''; let colon = 0; do { colon = response[index].indexOf(':'); - // find the header dealing with the handshake + // Find the header dealing with the handshake if (response[index].slice(0, colon).toLowerCase() === 'sec-websocket-accept') { key = response[index].slice(colon + 1).replace(/^\s+/, '').replace(/\s+$/, ''); break; @@ -80,8 +97,8 @@ function clientConnect(options) { index = index + 1; } while (index < len); hash.update(nonceSource + websocket.magicString); - const digest = hash.digest('hex');//console.log(digest);console.log(key); - // validate handshake + const digest = hash.digest('hex'); + // Validate handshake if (digest === key) { delete options.secure; delete options.socketOptions; @@ -111,11 +128,11 @@ function clientConnect(options) { return `/${host.slice(host.indexOf('/') + 1)}`; }()) : '/'; - const nonceSource = Buffer.from(Date.now().toString() + os.hostname()).toString('base64'); + const nonceSource = Buffer.from(DateNow().toString() + os.hostname()).toString('base64'); const header = [ `GET ${resourceName} HTTP/1.1`, (addresses.remote.address.indexOf(':') > -1) ? - // RFC 6455 4.1. request host be defined according to RFC 3986 (URI) + // RFC 6455 4.1 request host be defined according to RFC 3986 (URI) // however it also requires the socket be already established and open // at layer 4 (TCP/TLS) and layer 4 cares only for IP address as URI // is layer 7. For sanity ip/port is fully sufficient if derived from @@ -126,7 +143,7 @@ function clientConnect(options) { 'Connection: Upgrade', 'Sec-WebSocket-Version: 13', `Sec-WebSocket-Key: ${nonceSource}`, - `User-Agent: Node.js--${process.version}--${os.version()}--${os.release()}` + `User-Agent: Node.js--${process.version}--${os.version()}--${os.release()}`, ]; function headingCheck(heading) { if (typeof options[heading] === 'string') { @@ -146,32 +163,32 @@ function clientConnect(options) { header.push(''); header.push(''); - // last use of socket.write before its hidden in socket extensions + // Last use of socket.write before its hidden in socket extensions socket.write(header.join('\r\n')); } - // prevents a crashing server from breaking the client node instance + // Prevents a crashing server from breaking the client node instance socket.on('error', function() {}); - // wait for layer 4 socket connection + // Wait for layer 4 socket connection socket.once('ready', handlerReady); return socket; } -// a uniform convenience function for generating error objects +// A uniform convenience function for generating error objects function errorObject(code, message, socket, syscall) { const addresses = getAddress(socket).remote; - return new Error({ + return new SystemError({ address: addresses.address, code: code, errno: 0, message: message, port: addresses.port, - syscall: syscall + syscall: syscall, }); } -// a handy utility to conveniently gather a socket's connection identity +// A handy utility to conveniently gather a socket's connection identity function getAddress(socket) { function parse(input) { if (input === undefined) { @@ -188,12 +205,12 @@ function getAddress(socket) { return { local: { address: parse(socket.localAddress), - port: socket.localPort + port: socket.localPort, }, remote: { address: parse(socket.remoteAddress), - port: socket.remotePort - } + port: socket.remotePort, + }, }; } @@ -208,7 +225,7 @@ function messageSend(message, opcode, fragmentSize) { } else { socket.websocket.status = 'open'; } - }; + } if (socket.websocket.status === 'open') { socket.websocket.status = 'pending'; } @@ -217,10 +234,10 @@ function messageSend(message, opcode, fragmentSize) { } else { socket.once('drain', writeCallback); } - }; + } function mask(body) { const mask = Buffer.alloc(4); - const rand = Buffer.from(Math.random().toString()); + const rand = Buffer.from(MathRandom().toString()); mask[0] = rand[4]; mask[1] = rand[5]; mask[2] = rand[6]; @@ -268,7 +285,7 @@ function messageSend(message, opcode, fragmentSize) { if (typeof opcode !== 'number') { opcode = 1; } else { - opcode = Math.floor(opcode); + opcode = MathFloor(opcode); if (opcode < 0 || opcode > 15) { opcode = 1; } @@ -279,7 +296,15 @@ function messageSend(message, opcode, fragmentSize) { if (typeof fragmentSize !== 'number' || fragmentSize < 1) { fragmentSize = 0; } - if (opcode === 1 || opcode === 2 || opcode === 3 || opcode === 4 || opcode === 5 || opcode === 6 || opcode === 7) { + if ( + opcode === 1 || + opcode === 2 || + opcode === 3 || + opcode === 4 || + opcode === 5 || + opcode === 6 || + opcode === 7 + ) { let maskKey = null; function fragmentation(first) { let finish = false; @@ -308,7 +333,7 @@ function messageSend(message, opcode, fragmentSize) { }()); const size = frameBody.length; const frameHeader = (function() { - // frame 0 is: + // Frame 0 is: // * 128 bits for fin, 0 for unfinished plus opcode // * opcode 0 - continuation of fragments // * opcode 1 - text (total payload must be UTF8 and probably not contain hidden @@ -334,7 +359,7 @@ function messageSend(message, opcode, fragmentSize) { (first === true) ? opcode : 0; - // frame 1 is mask bit + length flag + // Frame 1 is mask bit + length flag frame[1] = (size < 126) ? (socket.websocket.masking === true) ? size + 128 : @@ -346,7 +371,7 @@ function messageSend(message, opcode, fragmentSize) { (socket.websocket.masking === true) ? 255 : 127; - // write payload length followed by mask key + // Write payload length followed by mask key if (size > 125) { if (size < 65536) { frame.writeUInt16BE(size, 2); @@ -381,23 +406,32 @@ function messageSend(message, opcode, fragmentSize) { } else { fragmentation(false); } - }; + } let dataPackage = (Buffer.isBuffer === true) ? message : Buffer.from(message); let len = dataPackage.length; fragmentation(true); - } else if (opcode === 8 || opcode === 9 || opcode === 10 || opcode === 11 || opcode === 12 || opcode === 13 || opcode === 14 || opcode === 15) { + } else if ( + opcode === 8 || + opcode === 9 || + opcode === 10 || + opcode === 11 || + opcode === 12 || + opcode === 13 || + opcode === 14 || + opcode === 15 + ) { let frameBody = message.subarray(0, 125); let frameHeader = null; if (socket.websocket.masking === true) { const masked = mask(frameBody); frameHeader = Buffer.alloc(6); - // opcode + fin bit, rsv bits set to 0 + // Opcode + fin bit, rsv bits set to 0 frameHeader[0] = 128 + opcode; - // set the mask bit + // Set the mask bit frameHeader[1] = 128 + frameBody.length; - // set the mask key + // Set the mask key frameHeader[2] = masked[1][0]; frameHeader[3] = masked[1][1]; frameHeader[4] = masked[1][2]; @@ -406,11 +440,11 @@ function messageSend(message, opcode, fragmentSize) { frameBody = masked[0]; } else { frameHeader = Buffer.alloc(2); - // opcode + fin bit, rsv bits set to 0 + // Opcode + fin bit, rsv bits set to 0 frameHeader[0] = 128 + opcode; frameHeader[1] = frameBody.length; } - // control frames send immediately, out of sequence + // Control frames send immediately, out of sequence socket.websocket.queue.unshift(Buffer.concat([frameHeader, frameBody])); if (socket.websocket.status === 'open') { @@ -419,15 +453,9 @@ function messageSend(message, opcode, fragmentSize) { } } -// arbitrary ping function, which may be called by any means at any time +// Arbitrary ping function, which may be called by any means at any time function ping(ttl, callback) { const socket = this; - function errorObject(code, message) { - const err = new Error(); - err.code = code; - err.message = `${message} Socket type ${config.socket.type} and name ${name}.`; - return err; - } if (socket.websocket.status !== 'open' && socket.websocket.status !== 'pending') { callback(errorObject( 'ECONNABORTED', @@ -437,22 +465,22 @@ function ping(ttl, callback) { ), null); } else { const nameSlice = socket.hash.slice(0, 125); - // send ping - transmit_ws.queue(Buffer.from(nameSlice), config.socket, 9); + // Send ping + socket.messageSend(Buffer.from(nameSlice), socket, 9); socket.pong = { - callback: callback, - start: process.hrtime.bigint(), - timeOut: setTimeout(function() { - callback(socket.websocket.pong.timeOutMessage, null); - delete socket.websocket.pong; - }, ttl), - timeOutMessage: errorObject('ETIMEDOUT', 'Ping timeout on websocket.'), - ttl: BigInt(ttl * 1e6) + callback: callback, + start: process.hrtime.bigint(), + timeOut: setTimeout(function() { + callback(socket.websocket.pong.timeOutMessage, null); + delete socket.websocket.pong; + }, ttl), + timeOutMessage: errorObject('ETIMEDOUT', 'Ping timeout on websocket.'), + ttl: BigInt(ttl * 1e6), }; } } -// processes incoming messages +// Processes incoming messages function receive(socket) { function processor(buf) { // RFC 6455, 5.2. Base Framing Protocol @@ -485,22 +513,22 @@ function receive(socket) { }); } return input; - }; - // identify payload extended length - const extended = function(input) { - const mask = (input[1] > 127), - len = (mask === true) ? - input[1] - 128 : - input[1], - keyOffset = (mask === true) ? - 4 : - 0; + } + // Identify payload extended length + function extended(input) { + const mask = (input[1] > 127); + const len = (mask === true) ? + input[1] - 128 : + input[1]; + const keyOffset = (mask === true) ? + 4 : + 0; if (len < 126) { return { lengthExtended: len, lengthShort: len, mask: mask, - startByte: 2 + keyOffset + startByte: 2 + keyOffset, }; } if (len < 127) { @@ -508,17 +536,17 @@ function receive(socket) { lengthExtended: input.subarray(2, 4).readUInt16BE(0), lengthShort: len, mask: mask, - startByte: 4 + keyOffset + startByte: 4 + keyOffset, }; } return { lengthExtended: input.subarray(4, 10).readUIntBE(0, 6), lengthShort: len, mask: mask, - startByte: 10 + keyOffset + startByte: 10 + keyOffset, }; - }; - // populates data from the incoming network buffer with no assumptions of + } + // Populates data from the incoming network buffer with no assumptions of // completeness const data = (function() { if (buf !== null && buf !== undefined) { @@ -529,12 +557,12 @@ function receive(socket) { } return socket.websocket.frame; }()); - // interprets the frame header from Buffer to an object + // Interprets the frame header from Buffer to an object const frame = (function() { if (data === null) { return null; } - // bit string - convert byte number (0 - 255) to 8 bits + // Bit string - convert byte number (0 - 255) to 8 bits const bits0 = data[0].toString(2).padStart(8, '0'); const meta = extended(data); return { @@ -554,7 +582,7 @@ function receive(socket) { maskKey: (meta.mask === true) ? data.subarray(meta.startByte - 4, meta.startByte) : null, - startByte: meta.startByte + startByte: meta.startByte, }; }()); const payload = (function() { @@ -588,7 +616,7 @@ function receive(socket) { } if (frame.opcode === 8) { - // socket close + // Socket close data[0] = 136; data[1] = (data[1] > 127) ? data[1] - 128 : @@ -597,10 +625,10 @@ function receive(socket) { socket.write(payload); socket.off('data', processor); } else if (frame.opcode === 9) { - // respond to 'ping' as 'pong' + // Respond to 'ping' as 'pong' socket.send(data.subarray(frame.startByte), socket, 10); } else if (frame.opcode === 10) { - // pong + // Pong const payloadString = payload.toString(); const pong = socket.websocket.pong[payloadString]; const time = process.hrtime.bigint(); @@ -613,7 +641,7 @@ function receive(socket) { } } else { const segment = Buffer.concat([socket.websocket.fragment, payload]); - // this block may include frame.opcode === 0 - a continuation frame + // This block may include frame.opcode === 0 - a continuation frame socket.websocket.frameExtended = frame.extended; if (frame.fin === true) { if (typeof socket.websocket.messageHandler === 'function') { @@ -627,7 +655,7 @@ function receive(socket) { if (socket.websocket.frame.length > 2) { processor(null); } - }; + } socket.on('data', processor); } @@ -648,6 +676,9 @@ function receive(socket) { * callbackListener?: (server:Server) => void; * // Optional callback that fires once the connection handshake completes. * callbackOpen?: (err:NodeJS.ErrnoException, socket:Socket|TLSSocket) => void; + * // Whether to apply RFC 6455 masking before sending messages. Defaults + * // to false. + * masking?: boolean; * // A handler to receive processed messages. If not a function incoming * // messages are ignored. * messageHandler?: (message:Buffer) => void; @@ -659,20 +690,21 @@ function receive(socket) { * serverOptions?: ServerOpts | TlsOptions; * } * (options:serverOptions) => Server; - * ``` */ -// creates a websocket server + * ``` + */ +// Creates a websocket server function server(options) { function connection(socket) { - // prevents a closing socket from crashing the server - socket.on('error', function () {}); - // socket handshake must be processed within 5 seconds of connection - // this is a security precaution to prevent overloading servers + // Prevents a closing socket from crashing the server + socket.on('error', function() {}); + // Socket handshake must be processed within 5 seconds of connection. + // This is a security precaution to prevent overloading servers. const deathDelay = setTimeout(function() { socket.destroy(); }, 5000); - // the first data must be the HTTP handshake, otherwise destroy + // The first data must be the HTTP handshake, otherwise destroy socket.once('data', function(data) { - // we expect data to be the handshake payload in HTTP form + // We expect data to be the handshake payload in HTTP form const headerValues = { authentication: '', id: '', @@ -706,7 +738,7 @@ function server(options) { flags.complete === false ) { function extend() { - // send the handshake response + // Send the handshake response const headers = [ 'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', @@ -722,15 +754,18 @@ function server(options) { headers.push(''); socket.write(headers.join('\r\n')); - // extend the socket + // Extend the socket socketExtend(socket, { - callbackOpen: options.callbackOpen, - extensions: headerValues.extensions, - messageHandler: options.messageHandler, + 'callbackOpen': options.callbackOpen, + 'extensions': headerValues.extensions, + 'masking': (typeof options.masking === 'boolean') ? + options.masking : + false, + 'messageHandler': options.messageHandler, 'proxy-authorization': '', 'role': 'server', - subprotocol: headerValues.subprotocol, - userAgent: headerValues.userAgent, + 'subprotocol': headerValues.subprotocol, + 'userAgent': headerValues.userAgent, }); } flags.complete = true; @@ -784,7 +819,7 @@ function server(options) { return serverInstance; } -// extends a net Socket type with additional features specific for WebSocket +// Extends a net Socket type with additional features specific for WebSocket function socketExtend(socket, extensions) { function stringExtensions(name) { socket.websocket[name] = (typeof extensions[name] === 'string') ? @@ -801,10 +836,8 @@ function socketExtend(socket, extensions) { if (typeof socket.websocket.messageHandler === 'function') { receive(socket); } - // arbitrary ping utility + // Arbitrary ping utility socket.ping = ping; - // send a message - socket.messageSend = messageSend; socket.setKeepAlive(true, 0); stringExtensions('extensions'); stringExtensions('proxy-authorization'); @@ -815,20 +848,20 @@ function socketExtend(socket, extensions) { extensions.role === 'client' ? true : false; - // storehouse of complete received data frames - // a frame is a message fragment considering for continuation frames + // Storehouse of complete received data frames + // A frame is a message fragment considering for continuation frames socket.websocket.fragment = Buffer.from([]); - // stores pieces of frames for assembly into complete frames + // Stores pieces of frames for assembly into complete frames socket.websocket.frame = Buffer.from([]); - // stores the payload size of current received message payload + // Stores the payload size of current received message payload socket.websocket.frameExtended = 0; - // stores termination times and callbacks for pong handling + // Stores termination times and callbacks for pong handling socket.websocket.pong = {}; - // stores messages for transmit in order, - // because websocket protocol cannot intermix messages + // Stores messages for transmit in order, + // Because websocket protocol cannot intermix messages socket.websocket.queue = []; socket.websocket.status = 'open'; - // hides the generic socket.write method to encourage use of messageSend + // Hides the generic socket.write method to encourage use of messageSend socket.internalWrite = socket.write; socket.write = messageSend; if (typeof extensions.callbackOpen === 'function') { @@ -836,9 +869,8 @@ function socketExtend(socket, extensions) { } } -module.exports = websocket = { - clientConnect, - getAddress, - magicString: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', - server: server -}; \ No newline at end of file +websocket.clientConnect = clientConnect; +websocket.getAddress = getAddress; +websocket.magicString = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; +websocket.server = server; +module.exports = websocket; From 2e8e57fc3200e00490f4559fa20ee69743ce7cc5 Mon Sep 17 00:00:00 2001 From: prettydiff Date: Sun, 3 Sep 2023 22:06:41 -0400 Subject: [PATCH 03/15] More lint style refinement --- lib/websocket.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 939829ee800068..9e84f941f0eda6 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -214,7 +214,7 @@ function getAddress(socket) { }; } -// send a message payload in conformance to RFC 6455 +// Send a message payload in conformance to RFC 6455 function messageSend(message, opcode, fragmentSize) { const socket = this; function writeFrame() { @@ -235,6 +235,7 @@ function messageSend(message, opcode, fragmentSize) { socket.once('drain', writeCallback); } } + function mask(body) { const mask = Buffer.alloc(4); const rand = Buffer.from(MathRandom().toString()); @@ -436,7 +437,7 @@ function messageSend(message, opcode, fragmentSize) { frameHeader[3] = masked[1][1]; frameHeader[4] = masked[1][2]; frameHeader[5] = masked[1][3]; - // assign the masked payload + // Assign the masked payload frameBody = masked[0]; } else { frameHeader = Buffer.alloc(2); @@ -461,7 +462,7 @@ function ping(ttl, callback) { 'ECONNABORTED', 'Ping error on websocket without \'open\' or \'pending\' status.', socket, - 'websocket.ping' + 'websocket.ping', ), null); } else { const nameSlice = socket.hash.slice(0, 125); @@ -514,6 +515,7 @@ function receive(socket) { } return input; } + // Identify payload extended length function extended(input) { const mask = (input[1] > 127); @@ -811,7 +813,7 @@ function server(options) { if (options.secure === false) { serverInstance.on('connection', connection); } - serverInstance.listen(options.listenerOptions, function () { + serverInstance.listen(options.listenerOptions, function() { if (typeof options.callbackListener === 'function') { options.callbackListener(serverInstance); } From 886f0e989b67c9f375730b33c9897f59f07cb2c5 Mon Sep 17 00:00:00 2001 From: prettydiff Date: Sun, 3 Sep 2023 22:20:06 -0400 Subject: [PATCH 04/15] attempting to fix automation checks regarding types in documentation --- doc/api/websocket.md | 10 +++++----- tools/doc/type-parser.mjs | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/api/websocket.md b/doc/api/websocket.md index 5639222297d2a8..9fbcb5f48bcf47 100644 --- a/doc/api/websocket.md +++ b/doc/api/websocket.md @@ -155,7 +155,7 @@ to create a WebSocket socket and connects it to a WebSocket server. * `options` {Object} * `callbackOpen` {Function} If present will execute upon completion of WebSocket connection handshake. Receives two arguments: `err` - {NodeJS.ErrnoException} and `socket` {websocketClient}. + {errors.Error} and `socket` {websocketClient}. **Default:** `undefined` * `extensions` {string} Any additional instructions, identifiers, or custom descriptive data. @@ -228,7 +228,7 @@ will be destroyed. `server` {Server}. * `callbackOpen` {Function} If present will execute upon completion of WebSocket connection handshake. Receives two arguments: `err` - {NodeJS.ErrnoException} and `socket` {websocketClient}. + {errors.Error} and `socket` {websocketClient}. * `messageHandler` {Function} Received messages are passed into this function for custom processing. When this function is absent received messages are discarded. Receives one argument: `message` {Buffer}. @@ -260,14 +260,14 @@ with these additional object properties: * `websocket` {Object} * `callbackOpen` {Function} If present will execute upon completion of WebSocket connection handshake. Receives two arguments: `err` - {NodeJS.ErrnoException} and `socket` {websocketClient}. + {errors.Error} and `socket` {websocketClient}. * `extensions` {string} Any additional instructions, identifiers, or custom descriptive data. * `fragment` {Buffer} Stores complete message frames sufficient to assemble a complete message. * `frame` {Buffer} Stores pieces of a frame sufficient to assemble a complete frame. - * `frameExtended` {number} Stores the extended length value of the current + * `frameExtended` {integer} Stores the extended length value of the current message. * `masking` {boolean} Determines whether to mask messages before sending them. Defaults to `true` for client roles and `false` for server roles, but default @@ -294,7 +294,7 @@ with these additional object properties: * `userAgent` {string} User agent identifier populated by the client. * `write` {Function} Sends WebSocket messages. * `message` {Buffer|string} The message to send. - * `opcode` {Integer} an RFC 6455 message code. **Default:** 1 if the message is + * `opcode` {integer} an RFC 6455 message code. **Default:** 1 if the message is text or 2 if the message is a Buffer. * `fragmentSize` Determines the size of message fragmentation. **Default:** 0, which means no message fragmentation. \ No newline at end of file diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index d4d75f3d7482d8..ffa1f78bd72f44 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -228,6 +228,8 @@ const customTypesMap = { 'vm.Script': 'vm.html#class-vmscript', 'vm.SourceTextModule': 'vm.html#class-vmsourcetextmodule', + 'websocketClient': 'websocket.html#websocketClient', + 'MessagePort': 'worker_threads.html#class-messageport', 'Worker': 'worker_threads.html#class-worker', From 2649ead795122a7f5ea5e62d9bce5eb8d89e4092 Mon Sep 17 00:00:00 2001 From: prettydiff Date: Sun, 3 Sep 2023 22:40:12 -0400 Subject: [PATCH 05/15] updating a documentation type instance to the correct value --- doc/api/websocket.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/websocket.md b/doc/api/websocket.md index 9fbcb5f48bcf47..c7ee1abfe1de05 100644 --- a/doc/api/websocket.md +++ b/doc/api/websocket.md @@ -225,7 +225,7 @@ will be destroyed. handshake to complete. * `callbackListener` {Function} A callback that executes once the server starts listening for incoming socket connections. Provides 1 argument: - `server` {Server}. + `server` {net.Server|tls.Server}. * `callbackOpen` {Function} If present will execute upon completion of WebSocket connection handshake. Receives two arguments: `err` {errors.Error} and `socket` {websocketClient}. From b1ae17ee7511887825c84ca2956358a0b6835a6e Mon Sep 17 00:00:00 2001 From: prettydiff Date: Sun, 3 Sep 2023 22:54:29 -0400 Subject: [PATCH 06/15] more documentation type annotation fixes --- doc/api/websocket.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/api/websocket.md b/doc/api/websocket.md index c7ee1abfe1de05..7fd3ac78274773 100644 --- a/doc/api/websocket.md +++ b/doc/api/websocket.md @@ -281,13 +281,13 @@ with these additional object properties: mechanism. * `queue` {Buffer[]} Stores messages in order so that they are sent one at a time exactly in the order sent. - * `role` {'client'|'server'} Whether the socket is instantiated as a client - or server connection. - * `status` {'closed'|'open'|'pending'} Current transfer status of the socket. - * `closed` - Socket is not destroyed but is no longer receiving or + * `role` {string} Whether the socket is instantiated as a `'client'` or + `'server'` connection. + * `status` {string} Current transfer status of the socket. + * `'closed'` - Socket is not destroyed but is no longer receiving or transmitting. - * `open` - Socket is available to send and receive messages. - * `pending` - Socket can receive messages, but is halted from sending + * `'open'` - Socket is available to send and receive messages. + * `'pending'` - Socket can receive messages, but is halted from sending messages. This typically occurs because the socket is writing a message and others are stacked up in queue. * `subprotocol`: {string} Any sub-protocols defined by the client. From 03596284d65c1707b971b82750258211e2b1611c Mon Sep 17 00:00:00 2001 From: prettydiff Date: Mon, 4 Sep 2023 10:48:21 -0400 Subject: [PATCH 07/15] working through CI failures --- lib/websocket.js | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 9e84f941f0eda6..3bc345ce127d2a 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -1,5 +1,5 @@ 'use strict'; -const { Buffer } = require('node:buffer'); +const { Buffer } = require('buffer'); const crypto = require('node:crypto'); const net = require('node:net'); const os = require('node:os'); @@ -12,7 +12,7 @@ const { Number, } = primordials; const { - SystemError, + errnoException, } = require('internal/errors'); const { clearTimeout, @@ -73,13 +73,7 @@ function clientConnect(options) { const len = response.length; function callbackCheck() { if (typeof options.callbackOpen === 'function') { - options.callbackOpen(errorObject( - 'ECONNABORTED', - 'WebSocket server handshake response returned no ' + - 'Sec-WebSocket-Accept header or one which is malformed.', - socket, - 'websocket.clientConnect', - ), null); + // REPLACEME - error handling here for handshake failure at client } } // Check if response contains required HTTP header @@ -175,19 +169,6 @@ function clientConnect(options) { return socket; } -// A uniform convenience function for generating error objects -function errorObject(code, message, socket, syscall) { - const addresses = getAddress(socket).remote; - return new SystemError({ - address: addresses.address, - code: code, - errno: 0, - message: message, - port: addresses.port, - syscall: syscall, - }); -} - // A handy utility to conveniently gather a socket's connection identity function getAddress(socket) { function parse(input) { @@ -458,12 +439,9 @@ function messageSend(message, opcode, fragmentSize) { function ping(ttl, callback) { const socket = this; if (socket.websocket.status !== 'open' && socket.websocket.status !== 'pending') { - callback(errorObject( - 'ECONNABORTED', - 'Ping error on websocket without \'open\' or \'pending\' status.', - socket, - 'websocket.ping', - ), null); + // REPLACEME - first null should be error object equivalent to write to a closed + // socket + callback(null, null); } else { const nameSlice = socket.hash.slice(0, 125); // Send ping @@ -475,7 +453,8 @@ function ping(ttl, callback) { callback(socket.websocket.pong.timeOutMessage, null); delete socket.websocket.pong; }, ttl), - timeOutMessage: errorObject('ETIMEDOUT', 'Ping timeout on websocket.'), + // REPLACEME - null value should be an error object for ping timeout + timeOutMessage: null, ttl: BigInt(ttl * 1e6), }; } From f8d2888f8866da4d3c0f4be9099466821d9dd4e1 Mon Sep 17 00:00:00 2001 From: prettydiff Date: Mon, 4 Sep 2023 11:00:48 -0400 Subject: [PATCH 08/15] removing an unused error library import --- lib/websocket.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 3bc345ce127d2a..3091ad4702cb3a 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -11,9 +11,6 @@ const { MathRandom, Number, } = primordials; -const { - errnoException, -} = require('internal/errors'); const { clearTimeout, setTimeout, From ab2d1a908b82d54ad4dc5b9f12798dd4f3d6ba00 Mon Sep 17 00:00:00 2001 From: prettydiff Date: Mon, 4 Sep 2023 11:11:26 -0400 Subject: [PATCH 09/15] updating documentation to comply with style rules --- doc/api/websocket.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/api/websocket.md b/doc/api/websocket.md index 7fd3ac78274773..a5a9ac5f875711 100644 --- a/doc/api/websocket.md +++ b/doc/api/websocket.md @@ -17,7 +17,7 @@ callbacks only. ## Callback example ### Client TLS Socket example -```javascript +```mjs const options = { authentication: 'auth-string', callbackOpen: function(err, socket) { @@ -46,7 +46,7 @@ websocket.clientConnect(options); ``` ### Client Net Socket example -```javascript +```mjs const options = { authentication: 'auth-string', callbackOpen: function(err, socket) { @@ -74,7 +74,7 @@ websocket.clientConnect(options); ``` ### Server TLS example -```javascript +```mjs const options = { callbackConnect: function(headerValues, socket, ready) { console.log(headerValues); @@ -105,7 +105,7 @@ const server = ws.server(options); ``` ### Server Net example -```javascript +```mjs const options = { callbackConnect: function(headerValues, socket, ready) { console.log(headerValues); @@ -270,8 +270,8 @@ with these additional object properties: * `frameExtended` {integer} Stores the extended length value of the current message. * `masking` {boolean} Determines whether to mask messages before sending them. - Defaults to `true` for client roles and `false` for server roles, but default - behavior can be changes with options. + Defaults to `true` for client roles and `false` for server roles, but + default behavior can be changes with options. * `messageHandler` {Function} Received messages are passed into this function for custom processing. When this function is absent received messages are discarded. Receives one argument: `message` {Buffer}. @@ -288,13 +288,13 @@ with these additional object properties: transmitting. * `'open'` - Socket is available to send and receive messages. * `'pending'` - Socket can receive messages, but is halted from sending - messages. This typically occurs because the socket is writing a message and - others are stacked up in queue. + messages. This typically occurs because the socket is writing a message + and others are stacked up in queue. * `subprotocol`: {string} Any sub-protocols defined by the client. * `userAgent` {string} User agent identifier populated by the client. * `write` {Function} Sends WebSocket messages. * `message` {Buffer|string} The message to send. - * `opcode` {integer} an RFC 6455 message code. **Default:** 1 if the message is - text or 2 if the message is a Buffer. + * `opcode` {integer} an RFC 6455 message code. **Default:** 1 if the message + is text or 2 if the message is a Buffer. * `fragmentSize` Determines the size of message fragmentation. **Default:** 0, - which means no message fragmentation. \ No newline at end of file + which means no message fragmentation. From 56f5bdbb4c8dabcf894941ce89ff830d2ae1e561 Mon Sep 17 00:00:00 2001 From: prettydiff Date: Mon, 4 Sep 2023 11:23:09 -0400 Subject: [PATCH 10/15] more style corrections for documentation --- doc/api/websocket.md | 598 +++++++++++++++++++++---------------------- 1 file changed, 298 insertions(+), 300 deletions(-) diff --git a/doc/api/websocket.md b/doc/api/websocket.md index a5a9ac5f875711..13232d07a3e420 100644 --- a/doc/api/websocket.md +++ b/doc/api/websocket.md @@ -1,300 +1,298 @@ -# WebSocket - - - -> Stability: 1 - Experimental - - - - - -The `node:websocket` library allows for the creation of RFC 6455 conformant -WebSocket client sockets and servers both secure and insecure. - -This module supports, as currently written, asynchronous execution with -callbacks only. - -## Callback example - -### Client TLS Socket example -```mjs -const options = { - authentication: 'auth-string', - callbackOpen: function(err, socket) { - console.log("client callbackOpen"); - if (err === null) { - socket.messageSend("hello from client"); - } - }, - masking: false, - id: 'id-string', - messageHandler: function(message){ - console.log("message received at client"); - console.log(message.toString()); - }, - 'proxy-authorization': 'proxy-auth', - subProtocol: 'sub-proto', - type: 'type-string', - secure: true, - socketOptions: { - host: host, - port: port, - rejectUnauthorized: false - } -}; -websocket.clientConnect(options); -``` - -### Client Net Socket example -```mjs -const options = { - authentication: 'auth-string', - callbackOpen: function(err, socket) { - console.log("client callbackOpen"); - if (err === null) { - socket.messageSend("hello from client"); - } - }, - masking: false, - id: 'id-string', - messageHandler: function(message){ - console.log("message received at client"); - console.log(message.toString()); - }, - 'proxy-authorization': 'proxy-auth', - subProtocol: 'sub-proto', - type: 'type-string', - secure: false, - socketOptions: { - host: host, - port: port - } -}; -websocket.clientConnect(options); -``` - -### Server TLS example -```mjs -const options = { - callbackConnect: function(headerValues, socket, ready) { - console.log(headerValues); - ready(); - }, - callbackListener: function(server) { - console.log("server callbackListener"); - }, - callbackOpen: function(err, socket) { - console.log("server callbackOpen"); - }, - messageHandler: function(message) { - console.log("message received at server"); - console.log(message.toString()); - }, - listenerOptions: { - host: host, - port: port - }, - secure: true, - serverOptions: { - ca: caCertificate, - cert: domainCertificate, - key: domainKey - } -}; -const server = ws.server(options); -``` - -### Server Net example -```mjs -const options = { - callbackConnect: function(headerValues, socket, ready) { - console.log(headerValues); - ready(); - }, - callbackListener: function(server) { - console.log("server callbackListener"); - }, - callbackOpen: function(err, socket) { - console.log("server callbackOpen"); - }, - messageHandler: function(message) { - console.log("message received at server"); - console.log(message.toString()); - }, - listenerOptions: { - host: host, - port: port - }, - secure: false, - serverOptions: {} -}; -const server = ws.server(options); -``` - -## API - - - -The websocket module exists to provide primitive WebSocket protocol support as -defined by [RFC 6455](https://www.rfc-editor.org/rfc/rfc6455). - -### Class: `clientConnect` - - - -The `clientConnect` class extends class -[net.Socket](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#class-netsocket) -or -[tls.TLSSocket](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#class-tlstlssocket) -to create a WebSocket socket and connects it to a WebSocket server. - -* `options` {Object} - * `callbackOpen` {Function} If present will execute upon completion of - WebSocket connection handshake. Receives two arguments: `err` - {errors.Error} and `socket` {websocketClient}. - **Default:** `undefined` - * `extensions` {string} Any additional instructions, identifiers, or custom - descriptive data. - * `masking` {boolean} Whether to apply RFC 6455 message masking. - **Default:** `true` - * `messageHandler` {Function} Received messages are passed into this function - for custom processing. When this function is absent received messages are - discarded. Receives one argument: `message` {Buffer}. - * `proxy-authorization` {string} Any identifier required by proxy - authorization mechanism. - * `secure` {boolean} Whether to create a TLS based socket or Net based - socket. **Default:** `true` - * `socketOptions` {Object} See - [net.connect](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#netconnect) - or - [tls.connect](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#tlsconnectoptions-callback). - * `subProtocol` {string} Any one or more RFC 6455 identified sub-protocols. -* Returns {websocketClient} - -### Class: `getAddress` - - - -A convenience utility for attaining addressing data from any network socket. - -* `socket` {websocketClient} -* Returns {Object} of form: - * `local` - * `address` {string} An IP address. - * `port` {number} A port. - * `remote` - * `address` {string} An IP address. - * `port` {number} A port. - -### `magicString` - - - -* {string} A static string required by RFC 6455 to internally create and prove - the connection handshake. - -### Class: `server` - - - -The `server` class extends class -[net.Server](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#class-netserver) -or -[tls.Server](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#class-tlsserver) -to create a WebSocket server listening for connecting sockets. Any socket that -fails to complete the handshake within 5 seconds of connecting to the server -will be destroyed. - -* `options` {Object} - * `callbackConnect` {Function} A callback to execute when a socket connects - to the server, but before the handshake completes. This function provides a - means to apply authentication or additional description before completing - the handshake and allowing messaging. Receives 3 arguments: `headerValues` - {Object}, `socket` {websocketClient}, `ready` {Function}. The third - argument must be called by the callbackConnect function in order for the - handshake to complete. - * `callbackListener` {Function} A callback that executes once the server - starts listening for incoming socket connections. Provides 1 argument: - `server` {net.Server|tls.Server}. - * `callbackOpen` {Function} If present will execute upon completion of - WebSocket connection handshake. Receives two arguments: `err` - {errors.Error} and `socket` {websocketClient}. - * `messageHandler` {Function} Received messages are passed into this function - for custom processing. When this function is absent received messages are - discarded. Receives one argument: `message` {Buffer}. - * `listenerOptions` {Object} See - [net.server.listen](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#serverlistenoptions-callback). - * `secure` Whether to create a net.Server or a tls.TLSServer. **Default:** - true - * `serverOptions` {Object} See - [net.createServer](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#netcreateserveroptions-connectionlistener) - or - [tls.createServer](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#tlscreateserveroptions-secureconnectionlistener). - -## Common Objects - -### websocketClient - - - -The `websocketClient` object inherits from either -[net.Socket](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#class-netsocket) -or -[tls.TLSSocket](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#class-tlstlssocket) -with these additional object properties: - -* `ping` {Function} Performs an arbitrary connection test that a user may call - at their liberty. -* `websocket` {Object} - * `callbackOpen` {Function} If present will execute upon completion of - WebSocket connection handshake. Receives two arguments: `err` - {errors.Error} and `socket` {websocketClient}. - * `extensions` {string} Any additional instructions, identifiers, or custom - descriptive data. - * `fragment` {Buffer} Stores complete message frames sufficient to assemble a - complete message. - * `frame` {Buffer} Stores pieces of a frame sufficient to assemble a complete - frame. - * `frameExtended` {integer} Stores the extended length value of the current - message. - * `masking` {boolean} Determines whether to mask messages before sending them. - Defaults to `true` for client roles and `false` for server roles, but - default behavior can be changes with options. - * `messageHandler` {Function} Received messages are passed into this function - for custom processing. When this function is absent received messages are - discarded. Receives one argument: `message` {Buffer}. - * `pong` Stores an object with metadata sufficient to test connectivity - initiated as a ping from the remote end. - * `proxy-authorization` Any identifier required by proxy authorization - mechanism. - * `queue` {Buffer[]} Stores messages in order so that they are sent one at a - time exactly in the order sent. - * `role` {string} Whether the socket is instantiated as a `'client'` or - `'server'` connection. - * `status` {string} Current transfer status of the socket. - * `'closed'` - Socket is not destroyed but is no longer receiving or - transmitting. - * `'open'` - Socket is available to send and receive messages. - * `'pending'` - Socket can receive messages, but is halted from sending - messages. This typically occurs because the socket is writing a message - and others are stacked up in queue. - * `subprotocol`: {string} Any sub-protocols defined by the client. - * `userAgent` {string} User agent identifier populated by the client. -* `write` {Function} Sends WebSocket messages. - * `message` {Buffer|string} The message to send. - * `opcode` {integer} an RFC 6455 message code. **Default:** 1 if the message - is text or 2 if the message is a Buffer. - * `fragmentSize` Determines the size of message fragmentation. **Default:** 0, - which means no message fragmentation. +# WebSocket + + + +> Stability: 1 - Experimental + + + + + +The `node:websocket` library allows for the creation of RFC 6455 conformant +WebSocket client sockets and servers both secure and insecure. + +This module supports, as currently written, asynchronous execution with +callbacks only. + +## Callback example + +### Client TLS Socket example +```mjs +const options = { + callbackOpen: function(err, socket) { + console.log('client callbackOpen'); + if (err === null) { + socket.messageSend('hello from client'); + } + }, + extensions: 'extensions here', + masking: false, + messageHandler: function(message){ + console.log('message received at client'); + console.log(message.toString()); + }, + 'proxy-authorization': 'proxy-auth', + subProtocol: 'sub-proto', + type: 'type-string', + secure: true, + socketOptions: { + host: host, + port: port, + rejectUnauthorized: false, + }, +}; +websocket.clientConnect(options); +``` + +### Client Net Socket example +```mjs +const options = { + callbackOpen: function(err, socket) { + console.log('client callbackOpen'); + if (err === null) { + socket.messageSend('hello from client'); + } + }, + extensions: 'extensions here', + masking: false, + messageHandler: function(message){ + console.log('message received at client'); + console.log(message.toString()); + }, + 'proxy-authorization': 'proxy-auth', + subProtocol: 'sub-proto', + type: 'type-string', + secure: false, + socketOptions: { + host: host, + port: port, + }, +}; +websocket.clientConnect(options); +``` + +### Server TLS example +```mjs +const options = { + callbackConnect: function(headerValues, socket, ready) { + console.log(headerValues); + ready(); + }, + callbackListener: function(server) { + console.log('server callbackListener'); + }, + callbackOpen: function(err, socket) { + console.log('server callbackOpen'); + }, + messageHandler: function(message) { + console.log('message received at server'); + console.log(message.toString()); + }, + listenerOptions: { + host: host, + port: port, + }, + secure: true, + serverOptions: { + ca: caCertificate, + cert: domainCertificate, + key: domainKey, + }, +}; +const server = ws.server(options); +``` + +### Server Net example +```mjs +const options = { + callbackConnect: function(headerValues, socket, ready) { + console.log(headerValues); + ready(); + }, + callbackListener: function(server) { + console.log('server callbackListener'); + }, + callbackOpen: function(err, socket) { + console.log('server callbackOpen'); + }, + messageHandler: function(message) { + console.log('message received at server'); + console.log(message.toString()); + }, + listenerOptions: { + host: host, + port: port, + }, + secure: false, + serverOptions: {}, +}; +const server = ws.server(options); +``` + +## API + + + +The websocket module exists to provide primitive WebSocket protocol support as +defined by [RFC 6455](https://www.rfc-editor.org/rfc/rfc6455). + +### Class: `clientConnect` + + + +The `clientConnect` class extends class +[net.Socket](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#class-netsocket) +or +[tls.TLSSocket](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#class-tlstlssocket) +to create a WebSocket socket and connects it to a WebSocket server. + +* `options` {Object} + * `callbackOpen` {Function} If present will execute upon completion of + WebSocket connection handshake. Receives two arguments: `err` + {errors.Error} and `socket` {websocketClient}. + **Default:** `undefined` + * `extensions` {string} Any additional instructions, identifiers, or custom + descriptive data. + * `masking` {boolean} Whether to apply RFC 6455 message masking. + **Default:** `true` + * `messageHandler` {Function} Received messages are passed into this function + for custom processing. When this function is absent received messages are + discarded. Receives one argument: `message` {Buffer}. + * `proxy-authorization` {string} Any identifier required by proxy + authorization mechanism. + * `secure` {boolean} Whether to create a TLS based socket or Net based + socket. **Default:** `true` + * `socketOptions` {Object} See + [net.connect](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#netconnect) + or + [tls.connect](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#tlsconnectoptions-callback). + * `subProtocol` {string} Any one or more RFC 6455 identified sub-protocols. +* Returns {websocketClient} + +### Class: `getAddress` + + + +A convenience utility for attaining addressing data from any network socket. + +* `socket` {websocketClient} +* Returns {Object} of form: + * `local` + * `address` {string} An IP address. + * `port` {number} A port. + * `remote` + * `address` {string} An IP address. + * `port` {number} A port. + +### `magicString` + + + +* {string} A static string required by RFC 6455 to internally create and prove + the connection handshake. + +### Class: `server` + + + +The `server` class extends class +[net.Server](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#class-netserver) +or +[tls.Server](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#class-tlsserver) +to create a WebSocket server listening for connecting sockets. Any socket that +fails to complete the handshake within 5 seconds of connecting to the server +will be destroyed. + +* `options` {Object} + * `callbackConnect` {Function} A callback to execute when a socket connects + to the server, but before the handshake completes. This function provides a + means to apply authentication or additional description before completing + the handshake and allowing messaging. Receives 3 arguments: `headerValues` + {Object}, `socket` {websocketClient}, `ready` {Function}. The third + argument must be called by the callbackConnect function in order for the + handshake to complete. + * `callbackListener` {Function} A callback that executes once the server + starts listening for incoming socket connections. Provides 1 argument: + `server` {net.Server|tls.Server}. + * `callbackOpen` {Function} If present will execute upon completion of + WebSocket connection handshake. Receives two arguments: `err` + {errors.Error} and `socket` {websocketClient}. + * `messageHandler` {Function} Received messages are passed into this function + for custom processing. When this function is absent received messages are + discarded. Receives one argument: `message` {Buffer}. + * `listenerOptions` {Object} See + [net.server.listen](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#serverlistenoptions-callback). + * `secure` Whether to create a net.Server or a tls.TLSServer. **Default:** + true + * `serverOptions` {Object} See + [net.createServer](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#netcreateserveroptions-connectionlistener) + or + [tls.createServer](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#tlscreateserveroptions-secureconnectionlistener). + +## Common Objects + +### websocketClient + + + +The `websocketClient` object inherits from either +[net.Socket](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#class-netsocket) +or +[tls.TLSSocket](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#class-tlstlssocket) +with these additional object properties: + +* `ping` {Function} Performs an arbitrary connection test that a user may call + at their liberty. +* `websocket` {Object} + * `callbackOpen` {Function} If present will execute upon completion of + WebSocket connection handshake. Receives two arguments: `err` + {errors.Error} and `socket` {websocketClient}. + * `extensions` {string} Any additional instructions, identifiers, or custom + descriptive data. + * `fragment` {Buffer} Stores complete message frames sufficient to assemble a + complete message. + * `frame` {Buffer} Stores pieces of a frame sufficient to assemble a complete + frame. + * `frameExtended` {integer} Stores the extended length value of the current + message. + * `masking` {boolean} Determines whether to mask messages before sending them. + Defaults to `true` for client roles and `false` for server roles, but + default behavior can be changes with options. + * `messageHandler` {Function} Received messages are passed into this function + for custom processing. When this function is absent received messages are + discarded. Receives one argument: `message` {Buffer}. + * `pong` Stores an object with metadata sufficient to test connectivity + initiated as a ping from the remote end. + * `proxy-authorization` Any identifier required by proxy authorization + mechanism. + * `queue` {Buffer[]} Stores messages in order so that they are sent one at a + time exactly in the order sent. + * `role` {string} Whether the socket is instantiated as a `'client'` or + `'server'` connection. + * `status` {string} Current transfer status of the socket. + * `'closed'` - Socket is not destroyed but is no longer receiving or + transmitting. + * `'open'` - Socket is available to send and receive messages. + * `'pending'` - Socket can receive messages, but is halted from sending + messages. This typically occurs because the socket is writing a message + and others are stacked up in queue. + * `subprotocol`: {string} Any sub-protocols defined by the client. + * `userAgent` {string} User agent identifier populated by the client. +* `write` {Function} Sends WebSocket messages. + * `message` {Buffer|string} The message to send. + * `opcode` {integer} an RFC 6455 message code. **Default:** 1 if the message + is text or 2 if the message is a Buffer. + * `fragmentSize` Determines the size of message fragmentation. **Default:** 0, + which means no message fragmentation. From 7bda7b190ec95a36f8195f8ba0bdd19b4ffdece4 Mon Sep 17 00:00:00 2001 From: prettydiff Date: Mon, 4 Sep 2023 11:32:29 -0400 Subject: [PATCH 11/15] more style corrections for documentation --- doc/api/websocket.md | 56 ++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/doc/api/websocket.md b/doc/api/websocket.md index 13232d07a3e420..8838ac529b9cff 100644 --- a/doc/api/websocket.md +++ b/doc/api/websocket.md @@ -19,23 +19,23 @@ callbacks only. ### Client TLS Socket example ```mjs const options = { - callbackOpen: function(err, socket) { + 'callbackOpen': function(err, socket) { console.log('client callbackOpen'); if (err === null) { socket.messageSend('hello from client'); } }, - extensions: 'extensions here', - masking: false, - messageHandler: function(message){ + 'extensions': 'extensions here', + 'masking': false, + 'messageHandler': function(message) { console.log('message received at client'); console.log(message.toString()); }, 'proxy-authorization': 'proxy-auth', - subProtocol: 'sub-proto', - type: 'type-string', - secure: true, - socketOptions: { + 'subProtocol': 'sub-proto', + 'type': 'type-string', + 'secure': true, + 'socketOptions': { host: host, port: port, rejectUnauthorized: false, @@ -47,23 +47,23 @@ websocket.clientConnect(options); ### Client Net Socket example ```mjs const options = { - callbackOpen: function(err, socket) { + 'callbackOpen': function(err, socket) { console.log('client callbackOpen'); if (err === null) { socket.messageSend('hello from client'); } }, - extensions: 'extensions here', - masking: false, - messageHandler: function(message){ + 'extensions': 'extensions here', + 'masking': false, + 'messageHandler': function(message) { console.log('message received at client'); console.log(message.toString()); }, 'proxy-authorization': 'proxy-auth', - subProtocol: 'sub-proto', - type: 'type-string', - secure: false, - socketOptions: { + 'subProtocol': 'sub-proto', + 'type': 'type-string', + 'secure': false, + 'socketOptions': { host: host, port: port, }, @@ -79,18 +79,18 @@ const options = { ready(); }, callbackListener: function(server) { - console.log('server callbackListener'); + console.log('server callbackListener'); }, callbackOpen: function(err, socket) { - console.log('server callbackOpen'); + console.log('server callbackOpen'); }, messageHandler: function(message) { - console.log('message received at server'); - console.log(message.toString()); + console.log('message received at server'); + console.log(message.toString()); }, listenerOptions: { - host: host, - port: port, + host: host, + port: port, }, secure: true, serverOptions: { @@ -110,18 +110,18 @@ const options = { ready(); }, callbackListener: function(server) { - console.log('server callbackListener'); + console.log('server callbackListener'); }, callbackOpen: function(err, socket) { - console.log('server callbackOpen'); + console.log('server callbackOpen'); }, messageHandler: function(message) { - console.log('message received at server'); - console.log(message.toString()); + console.log('message received at server'); + console.log(message.toString()); }, listenerOptions: { - host: host, - port: port, + host: host, + port: port, }, secure: false, serverOptions: {}, From dfba03f9bc37190d13c6b7600e5ac9e5fa09221e Mon Sep 17 00:00:00 2001 From: prettydiff Date: Mon, 4 Sep 2023 11:41:10 -0400 Subject: [PATCH 12/15] more style corrections for documentation --- doc/api/websocket.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/websocket.md b/doc/api/websocket.md index 8838ac529b9cff..719802dde2f002 100644 --- a/doc/api/websocket.md +++ b/doc/api/websocket.md @@ -170,7 +170,7 @@ to create a WebSocket socket and connects it to a WebSocket server. [net.connect](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#netconnect) or [tls.connect](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#tlsconnectoptions-callback). - * `subProtocol` {string} Any one or more RFC 6455 identified sub-protocols. + * `subProtocol` {string} Any one or more RFC 6455 identified sub-protocols. * Returns {websocketClient} ### Class: `getAddress` From 142570cd5cbe54abbeff862468b9a7c8ef3c2247 Mon Sep 17 00:00:00 2001 From: prettydiff Date: Tue, 5 Sep 2023 05:15:09 -0400 Subject: [PATCH 13/15] updates to the closing handshake --- doc/api/websocket.md | 10 ++++++---- lib/websocket.js | 33 +++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/doc/api/websocket.md b/doc/api/websocket.md index 719802dde2f002..e522da5e1261dd 100644 --- a/doc/api/websocket.md +++ b/doc/api/websocket.md @@ -252,7 +252,7 @@ The `websocketClient` object inherits from either or [tls.TLSSocket](https://nodejs.org/dist/latest-v20.x/docs/api/tls.html#class-tlstlssocket) with these additional object properties: - +* `close` {Function} A convenience method to initiate the closing process. * `ping` {Function} Performs an arbitrary connection test that a user may call at their liberty. * `websocket` {Object} @@ -282,10 +282,12 @@ with these additional object properties: * `role` {string} Whether the socket is instantiated as a `'client'` or `'server'` connection. * `status` {string} Current transfer status of the socket. - * `'closed'` - Socket is not destroyed but is no longer receiving or + * `'CLOSED'` - Socket is not destroyed but is no longer receiving or transmitting. - * `'open'` - Socket is available to send and receive messages. - * `'pending'` - Socket can receive messages, but is halted from sending + * `'CLOSING'` - Socket has sent a *close* type control frame and is + awaiting a response to complete its closing handshake. + * `'OPEN'` - Socket is available to send and receive messages. + * `'PENDING'` - Socket can receive messages, but is halted from sending messages. This typically occurs because the socket is writing a message and others are stacked up in queue. * `subprotocol`: {string} Any sub-protocols defined by the client. diff --git a/lib/websocket.js b/lib/websocket.js index 3091ad4702cb3a..d34a1d473778a9 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -52,7 +52,7 @@ const websocket = {}; * ``` */ -// creates and connects a websocket client +// Creates and connects a websocket client function clientConnect(options) { const socket = (options.secure === false) ? net.connect(options.socketOptions) : @@ -195,17 +195,20 @@ function getAddress(socket) { // Send a message payload in conformance to RFC 6455 function messageSend(message, opcode, fragmentSize) { const socket = this; + if (socket.websocket.status === 'CLOSED' || socket.websocket.status === 'CLOSING') { + return; + } function writeFrame() { function writeCallback() { socket.websocket.queue.splice(0, 1); if (socket.websocket.queue.length > 0) { writeFrame(); - } else { - socket.websocket.status = 'open'; + } else if (socket.websocket.status === 'PENDING') { + socket.websocket.status = 'OPEN'; } } - if (socket.websocket.status === 'open') { - socket.websocket.status = 'pending'; + if (socket.websocket.status === 'OPEN') { + socket.websocket.status = 'PENDING'; } if (socket.internalWrite(socket.websocket.queue[0]) === true) { writeCallback(); @@ -379,7 +382,7 @@ function messageSend(message, opcode, fragmentSize) { }()); socket.websocket.queue.push(Buffer.concat([frameHeader, frameBody])); if (finish === true) { - if (socket.websocket.status === 'open') { + if (socket.websocket.status === 'OPEN') { writeFrame(); } } else { @@ -403,6 +406,9 @@ function messageSend(message, opcode, fragmentSize) { ) { let frameBody = message.subarray(0, 125); let frameHeader = null; + if (opcode === 8) { + socket.websocket.status = 'CLOSING'; + } if (socket.websocket.masking === true) { const masked = mask(frameBody); frameHeader = Buffer.alloc(6); @@ -426,7 +432,7 @@ function messageSend(message, opcode, fragmentSize) { // Control frames send immediately, out of sequence socket.websocket.queue.unshift(Buffer.concat([frameHeader, frameBody])); - if (socket.websocket.status === 'open') { + if (socket.websocket.status === 'OPEN') { writeFrame(); } } @@ -435,7 +441,7 @@ function messageSend(message, opcode, fragmentSize) { // Arbitrary ping function, which may be called by any means at any time function ping(ttl, callback) { const socket = this; - if (socket.websocket.status !== 'open' && socket.websocket.status !== 'pending') { + if (socket.websocket.status !== 'OPEN' && socket.websocket.status !== 'PENDING') { // REPLACEME - first null should be error object equivalent to write to a closed // socket callback(null, null); @@ -600,8 +606,12 @@ function receive(socket) { data[1] - 128 : data[1]; const payload = Buffer.concat([data.subarray(0, 2), unmask(data.subarray(2))]); - socket.write(payload); + if (socket.websocket.status !== 'CLOSING') { + socket.write(payload, 8, 0); + } + socket.websocket.status = 'CLOSED'; socket.off('data', processor); + socket.destroy(); } else if (frame.opcode === 9) { // Respond to 'ping' as 'pong' socket.send(data.subarray(frame.startByte), socket, 10); @@ -814,6 +824,9 @@ function socketExtend(socket, extensions) { if (typeof socket.websocket.messageHandler === 'function') { receive(socket); } + socket.close = function() { + this.write('CLOSE', 8, 0); + } // Arbitrary ping utility socket.ping = ping; socket.setKeepAlive(true, 0); @@ -838,7 +851,7 @@ function socketExtend(socket, extensions) { // Stores messages for transmit in order, // Because websocket protocol cannot intermix messages socket.websocket.queue = []; - socket.websocket.status = 'open'; + socket.websocket.status = 'OPEN'; // Hides the generic socket.write method to encourage use of messageSend socket.internalWrite = socket.write; socket.write = messageSend; From 92f75c16081673c4725b00c6e28c308b2b388ca7 Mon Sep 17 00:00:00 2001 From: prettydiff Date: Tue, 5 Sep 2023 07:46:45 -0400 Subject: [PATCH 14/15] adding sanity checks to messageSend --- lib/websocket.js | 444 ++++++++++++++++++++++++----------------------- 1 file changed, 223 insertions(+), 221 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index d34a1d473778a9..61b0c9f829d77a 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -195,245 +195,247 @@ function getAddress(socket) { // Send a message payload in conformance to RFC 6455 function messageSend(message, opcode, fragmentSize) { const socket = this; - if (socket.websocket.status === 'CLOSED' || socket.websocket.status === 'CLOSING') { - return; - } - function writeFrame() { - function writeCallback() { - socket.websocket.queue.splice(0, 1); - if (socket.websocket.queue.length > 0) { - writeFrame(); - } else if (socket.websocket.status === 'PENDING') { - socket.websocket.status = 'OPEN'; + if (socket.websocket.status !== "OPEN" && socket.websocket.status !== "PENDING") { + // REPLACEME - emit error on socket here + } else if (typeof message !== 'string' && Buffer.isBuffer(message) === false) { + // REPLACEME - emit error on socket here + } else { + function writeFrame() { + function writeCallback() { + socket.websocket.queue.splice(0, 1); + if (socket.websocket.queue.length > 0) { + writeFrame(); + } else if (socket.websocket.status === 'PENDING') { + socket.websocket.status = 'OPEN'; + } + } + if (socket.websocket.status === 'OPEN') { + socket.websocket.status = 'PENDING'; + } + if (socket.internalWrite(socket.websocket.queue[0]) === true) { + writeCallback(); + } else { + socket.once('drain', writeCallback); } } - if (socket.websocket.status === 'OPEN') { - socket.websocket.status = 'PENDING'; + + function mask(body) { + const mask = Buffer.alloc(4); + const rand = Buffer.from(MathRandom().toString()); + mask[0] = rand[4]; + mask[1] = rand[5]; + mask[2] = rand[6]; + mask[3] = rand[7]; + // RFC 6455, 5.3. Client-to-Server Masking + // j = i MOD 4 + // transformed-octet-i = original-octet-i XOR masking-key-octet-j + body.forEach(function(value, index) { + body[index] = value ^ mask[index % 4]; + }); + return [body, mask]; } - if (socket.internalWrite(socket.websocket.queue[0]) === true) { - writeCallback(); + // OPCODES + // ## Messages + // 0 - continuation - fragments of a message payload following an initial + // fragment + // 1 - text message + // 2 - binary message + // 3-7 - reserved for future use + // + // ## Control Frames + // 8 - close, the remote is destroying the socket + // 9 - ping, a connectivity health check + // a - pong, a response to a ping + // b-f - reserved for future use + // + // ## Notes + // * Message frame fragments must be transmitted in order and not interleaved + // with other messages. + // * Message types may be supplied as buffer or socketData types, but will + // always be transmitted as buffers. + // * Control frames are always granted priority and may occur between fragments + // of a single message. + // * Control frames will always be supplied as buffer data types. + // + // ## Masking + // * All traffic coming from the browser will be websocket masked. + // * I have not tested if the browsers will process masked data as they + // shouldn't according to RFC 6455. + // * This application supports both masked and unmasked transmission so long + // as the mask bit is set and a 32bit mask key is supplied. + // * Mask bit is set as payload length (up to 127) + 128 assigned to frame + // header second byte. + // * Mask key is first 4 bytes following payload length bytes (if any). + if (typeof opcode !== 'number') { + opcode = (Buffer.isBuffer(message)) ? + 2 : + 1; } else { - socket.once('drain', writeCallback); + opcode = MathFloor(opcode); + if (opcode < 1 || opcode > 15) { + opcode = 1; + } } - } - - function mask(body) { - const mask = Buffer.alloc(4); - const rand = Buffer.from(MathRandom().toString()); - mask[0] = rand[4]; - mask[1] = rand[5]; - mask[2] = rand[6]; - mask[3] = rand[7]; - // RFC 6455, 5.3. Client-to-Server Masking - // j = i MOD 4 - // transformed-octet-i = original-octet-i XOR masking-key-octet-j - body.forEach(function(value, index) { - body[index] = value ^ mask[index % 4]; - }); - return [body, mask]; - } - // OPCODES - // ## Messages - // 0 - continuation - fragments of a message payload following an initial - // fragment - // 1 - text message - // 2 - binary message - // 3-7 - reserved for future use - // - // ## Control Frames - // 8 - close, the remote is destroying the socket - // 9 - ping, a connectivity health check - // a - pong, a response to a ping - // b-f - reserved for future use - // - // ## Notes - // * Message frame fragments must be transmitted in order and not interleaved - // with other messages. - // * Message types may be supplied as buffer or socketData types, but will - // always be transmitted as buffers. - // * Control frames are always granted priority and may occur between fragments - // of a single message. - // * Control frames will always be supplied as buffer data types. - // - // ## Masking - // * All traffic coming from the browser will be websocket masked. - // * I have not tested if the browsers will process masked data as they - // shouldn't according to RFC 6455. - // * This application supports both masked and unmasked transmission so long - // as the mask bit is set and a 32bit mask key is supplied. - // * Mask bit is set as payload length (up to 127) + 128 assigned to frame - // header second byte. - // * Mask key is first 4 bytes following payload length bytes (if any). - if (typeof opcode !== 'number') { - opcode = 1; - } else { - opcode = MathFloor(opcode); - if (opcode < 0 || opcode > 15) { - opcode = 1; + if (typeof fragmentSize !== 'number' || fragmentSize < 1) { + fragmentSize = 0; } - } - if (opcode === 1 && Buffer.isBuffer(message) === true) { - opcode = 2; - } - if (typeof fragmentSize !== 'number' || fragmentSize < 1) { - fragmentSize = 0; - } - if ( - opcode === 1 || - opcode === 2 || - opcode === 3 || - opcode === 4 || - opcode === 5 || - opcode === 6 || - opcode === 7 - ) { - let maskKey = null; - function fragmentation(first) { - let finish = false; - const frameBody = (function() { - if (fragmentSize < 1 || len === fragmentSize) { - finish = true; + if ( + opcode === 1 || + opcode === 2 || + opcode === 3 || + opcode === 4 || + opcode === 5 || + opcode === 6 || + opcode === 7 + ) { + let maskKey = null; + function fragmentation(first) { + let finish = false; + const frameBody = (function() { + if (fragmentSize < 1 || len === fragmentSize) { + finish = true; + if (socket.websocket.masking === true) { + const masked = mask(dataPackage); + maskKey = masked[1]; + return masked[0]; + } + return dataPackage; + } + const fragment = dataPackage.subarray(0, fragmentSize); + dataPackage = dataPackage.subarray(fragmentSize); + len = dataPackage.length; + if (len < fragmentSize) { + finish = true; + } if (socket.websocket.masking === true) { - const masked = mask(dataPackage); + const masked = mask(fragment); maskKey = masked[1]; return masked[0]; } - return dataPackage; - } - const fragment = dataPackage.subarray(0, fragmentSize); - dataPackage = dataPackage.subarray(fragmentSize); - len = dataPackage.length; - if (len < fragmentSize) { - finish = true; - } - if (socket.websocket.masking === true) { - const masked = mask(fragment); - maskKey = masked[1]; - return masked[0]; - } - return fragment; - }()); - const size = frameBody.length; - const frameHeader = (function() { - // Frame 0 is: - // * 128 bits for fin, 0 for unfinished plus opcode - // * opcode 0 - continuation of fragments - // * opcode 1 - text (total payload must be UTF8 and probably not contain hidden - // control characters) - // * opcode 2 - supposed to be binary, really anything that isn't 100& UTF8 text - // ** for fragmented data only first data frame gets a data opcode, others - // receive 0 (continuity) - const frame = (size < 126) ? - (socket.websocket.masking === true) ? - Buffer.alloc(6) : - Buffer.alloc(2) : - (size < 65536) ? - (socket.websocket.masking === true) ? - Buffer.alloc(8) : - Buffer.alloc(4) : - (socket.websocket.masking === true) ? - Buffer.alloc(14) : - Buffer.alloc(10); - frame[0] = (finish === true) ? - (first === true) ? - 128 + opcode : - 128 : - (first === true) ? - opcode : - 0; - // Frame 1 is mask bit + length flag - frame[1] = (size < 126) ? - (socket.websocket.masking === true) ? - size + 128 : - size : - (size < 65536) ? + return fragment; + }()); + const size = frameBody.length; + const frameHeader = (function() { + // Frame 0 is: + // * 128 bits for fin, 0 for unfinished plus opcode + // * opcode 0 - continuation of fragments + // * opcode 1 - text (total payload must be UTF8 and probably not contain hidden + // control characters) + // * opcode 2 - supposed to be binary, really anything that isn't 100& UTF8 text + // ** for fragmented data only first data frame gets a data opcode, others + // receive 0 (continuity) + const frame = (size < 126) ? (socket.websocket.masking === true) ? - 254 : - 126 : + Buffer.alloc(6) : + Buffer.alloc(2) : + (size < 65536) ? + (socket.websocket.masking === true) ? + Buffer.alloc(8) : + Buffer.alloc(4) : + (socket.websocket.masking === true) ? + Buffer.alloc(14) : + Buffer.alloc(10); + frame[0] = (finish === true) ? + (first === true) ? + 128 + opcode : + 128 : + (first === true) ? + opcode : + 0; + // Frame 1 is mask bit + length flag + frame[1] = (size < 126) ? (socket.websocket.masking === true) ? - 255 : - 127; - // Write payload length followed by mask key - if (size > 125) { - if (size < 65536) { - frame.writeUInt16BE(size, 2); - if (socket.websocket.masking === true) { - frame[6] = maskKey[0]; - frame[7] = maskKey[1]; - frame[8] = maskKey[2]; - frame[9] = maskKey[3]; - } - } else { - frame.writeUIntBE(size, 4, 6); - if (socket.websocket.masking === true) { - frame[10] = maskKey[0]; - frame[11] = maskKey[1]; - frame[12] = maskKey[2]; - frame[13] = maskKey[3]; + size + 128 : + size : + (size < 65536) ? + (socket.websocket.masking === true) ? + 254 : + 126 : + (socket.websocket.masking === true) ? + 255 : + 127; + // Write payload length followed by mask key + if (size > 125) { + if (size < 65536) { + frame.writeUInt16BE(size, 2); + if (socket.websocket.masking === true) { + frame[6] = maskKey[0]; + frame[7] = maskKey[1]; + frame[8] = maskKey[2]; + frame[9] = maskKey[3]; + } + } else { + frame.writeUIntBE(size, 4, 6); + if (socket.websocket.masking === true) { + frame[10] = maskKey[0]; + frame[11] = maskKey[1]; + frame[12] = maskKey[2]; + frame[13] = maskKey[3]; + } } + } else if (socket.websocket.masking === true) { + frame[2] = maskKey[0]; + frame[3] = maskKey[1]; + frame[4] = maskKey[2]; + frame[5] = maskKey[3]; } - } else if (socket.websocket.masking === true) { - frame[2] = maskKey[0]; - frame[3] = maskKey[1]; - frame[4] = maskKey[2]; - frame[5] = maskKey[3]; - } - return frame; - }()); - socket.websocket.queue.push(Buffer.concat([frameHeader, frameBody])); - if (finish === true) { - if (socket.websocket.status === 'OPEN') { - writeFrame(); + return frame; + }()); + socket.websocket.queue.push(Buffer.concat([frameHeader, frameBody])); + if (finish === true) { + if (socket.websocket.status === 'OPEN') { + writeFrame(); + } + } else { + fragmentation(false); } + } + let dataPackage = (Buffer.isBuffer === true) ? + message : + Buffer.from(message); + let len = dataPackage.length; + fragmentation(true); + } else if ( + opcode === 8 || + opcode === 9 || + opcode === 10 || + opcode === 11 || + opcode === 12 || + opcode === 13 || + opcode === 14 || + opcode === 15 + ) { + let frameBody = message.subarray(0, 125); + let frameHeader = null; + if (opcode === 8) { + socket.websocket.status = 'CLOSING'; + } + if (socket.websocket.masking === true) { + const masked = mask(frameBody); + frameHeader = Buffer.alloc(6); + // Opcode + fin bit, rsv bits set to 0 + frameHeader[0] = 128 + opcode; + // Set the mask bit + frameHeader[1] = 128 + frameBody.length; + // Set the mask key + frameHeader[2] = masked[1][0]; + frameHeader[3] = masked[1][1]; + frameHeader[4] = masked[1][2]; + frameHeader[5] = masked[1][3]; + // Assign the masked payload + frameBody = masked[0]; } else { - fragmentation(false); + frameHeader = Buffer.alloc(2); + // Opcode + fin bit, rsv bits set to 0 + frameHeader[0] = 128 + opcode; + frameHeader[1] = frameBody.length; } - } - let dataPackage = (Buffer.isBuffer === true) ? - message : - Buffer.from(message); - let len = dataPackage.length; - fragmentation(true); - } else if ( - opcode === 8 || - opcode === 9 || - opcode === 10 || - opcode === 11 || - opcode === 12 || - opcode === 13 || - opcode === 14 || - opcode === 15 - ) { - let frameBody = message.subarray(0, 125); - let frameHeader = null; - if (opcode === 8) { - socket.websocket.status = 'CLOSING'; - } - if (socket.websocket.masking === true) { - const masked = mask(frameBody); - frameHeader = Buffer.alloc(6); - // Opcode + fin bit, rsv bits set to 0 - frameHeader[0] = 128 + opcode; - // Set the mask bit - frameHeader[1] = 128 + frameBody.length; - // Set the mask key - frameHeader[2] = masked[1][0]; - frameHeader[3] = masked[1][1]; - frameHeader[4] = masked[1][2]; - frameHeader[5] = masked[1][3]; - // Assign the masked payload - frameBody = masked[0]; - } else { - frameHeader = Buffer.alloc(2); - // Opcode + fin bit, rsv bits set to 0 - frameHeader[0] = 128 + opcode; - frameHeader[1] = frameBody.length; - } - // Control frames send immediately, out of sequence - socket.websocket.queue.unshift(Buffer.concat([frameHeader, frameBody])); + // Control frames send immediately, out of sequence + socket.websocket.queue.unshift(Buffer.concat([frameHeader, frameBody])); - if (socket.websocket.status === 'OPEN') { - writeFrame(); + if (socket.websocket.status === 'OPEN') { + writeFrame(); + } } } } From 93b0938bedb170bc90c264ee2a6fb789f08b52fd Mon Sep 17 00:00:00 2001 From: prettydiff Date: Tue, 5 Sep 2023 09:34:36 -0400 Subject: [PATCH 15/15] allowing support for a custom header and server customization of messageHandler --- doc/api/websocket.md | 38 +++++++++++++++++++++++++++++-- lib/websocket.js | 54 ++++++++++++++++++++++++++++---------------- 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/doc/api/websocket.md b/doc/api/websocket.md index e522da5e1261dd..2bdd60becf9d8d 100644 --- a/doc/api/websocket.md +++ b/doc/api/websocket.md @@ -217,10 +217,10 @@ will be destroyed. * `callbackConnect` {Function} A callback to execute when a socket connects to the server, but before the handshake completes. This function provides a means to apply authentication or additional description before completing - the handshake and allowing messaging. Receives 3 arguments: `headerValues` + the handshake and allowing messaging. Receives 3 arguments: `connectOptions` {Object}, `socket` {websocketClient}, `ready` {Function}. The third argument must be called by the callbackConnect function in order for the - handshake to complete. + handshake to complete and a `connectOptions` object must be passed into it. * `callbackListener` {Function} A callback that executes once the server starts listening for incoming socket connections. Provides 1 argument: `server` {net.Server|tls.Server}. @@ -241,6 +241,40 @@ will be destroyed. ## Common Objects +### connectOptions + + + +This object is used internally to extend a `Socket` object type into a +`websocketClient` type based upon client options or headers in the handshake at +the server. This is also passed into the `callbackConnect` function on the +server to allow custom modification and authentication for a `websocketClient` +socket before the handshake completes. + +* `callbackOpen` {Function} The callback to execute once the handshake + completes. +* `extensions` {string} Value of the optional extensions HTTP header used in + the handshake process. +* `masking` {boolean} Whether to forcefully impose message masking, + forcefully prevent message masking, or leave to the default. By default + all sockets with role *client* will perform message masking and all sockets + with role *server* will not. +* `messageHandler` {Function} The function to process received messages. On + the client side this function is defined as an option passed into + `clientConnect`. On the server side it is also defined as an option passed + into the `server` method but can be redefined in the `callbackConnect` + callback per socket. +* `proxy-authorization` {string} An optional value to define a security token + to proxies that require such. +* `role` {string} A read only value of *client* or *server* that cannot be + customized or manually populated. +* `subprotocol` {string} An subprotocol value passed into `clientConnect` or + received on the server as a header in the handshake. +* `userAgent` {string} A user agent identifier populated by the client for the + server. + ### websocketClient