From 02ecf45a29ccee55b3d09521f29689769b66e5be Mon Sep 17 00:00:00 2001 From: Dmitry Kirilyuk Date: Thu, 22 Oct 2020 12:50:15 +0300 Subject: [PATCH 001/328] [minor] Specify optional parameters in JSDoc (#1799) --- lib/event-target.js | 8 ++-- lib/limiter.js | 4 +- lib/permessage-deflate.js | 34 +++++++++-------- lib/receiver.js | 8 ++-- lib/sender.js | 79 +++++++++++++++++++++++---------------- lib/stream.js | 2 +- lib/websocket-server.js | 31 +++++++-------- lib/websocket.js | 63 +++++++++++++++++-------------- 8 files changed, 125 insertions(+), 104 deletions(-) diff --git a/lib/event-target.js b/lib/event-target.js index d38e1f2d7..bc1a385fe 100644 --- a/lib/event-target.js +++ b/lib/event-target.js @@ -111,11 +111,11 @@ const EventTarget = { * * @param {String} type A string representing the event type to listen for * @param {Function} listener The listener to add - * @param {Object} options An options object specifies characteristics about + * @param {Object} [options] An options object specifies characteristics about * the event listener - * @param {Boolean} options.once A `Boolean`` indicating that the listener - * should be invoked at most once after being added. If `true`, the - * listener would be automatically removed when invoked. + * @param {Boolean} [options.once=false] A `Boolean`` indicating that the + * listener should be invoked at most once after being added. If `true`, + * the listener would be automatically removed when invoked. * @public */ addEventListener(type, listener, options) { diff --git a/lib/limiter.js b/lib/limiter.js index c5e600958..3fd35784e 100644 --- a/lib/limiter.js +++ b/lib/limiter.js @@ -11,8 +11,8 @@ class Limiter { /** * Creates a new `Limiter`. * - * @param {Number} concurrency The maximum number of jobs allowed to run - * concurrently + * @param {Number} [concurrency=Infinity] The maximum number of jobs allowed + * to run concurrently */ constructor(concurrency) { this[kDone] = () => { diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index 7bb7c98d7..7d7209b9e 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -29,24 +29,26 @@ class PerMessageDeflate { /** * Creates a PerMessageDeflate instance. * - * @param {Object} options Configuration options - * @param {Boolean} options.serverNoContextTakeover Request/accept disabling - * of server context takeover - * @param {Boolean} options.clientNoContextTakeover Advertise/acknowledge - * disabling of client context takeover - * @param {(Boolean|Number)} options.serverMaxWindowBits Request/confirm the + * @param {Object} [options] Configuration options + * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept + * disabling of server context takeover + * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ + * acknowledge disabling of client context takeover + * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the * use of a custom server window size - * @param {(Boolean|Number)} options.clientMaxWindowBits Advertise support + * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support * for, or request, a custom client window size - * @param {Object} options.zlibDeflateOptions Options to pass to zlib on deflate - * @param {Object} options.zlibInflateOptions Options to pass to zlib on inflate - * @param {Number} options.threshold Size (in bytes) below which messages - * should not be compressed - * @param {Number} options.concurrencyLimit The number of concurrent calls to - * zlib - * @param {Boolean} isServer Create the instance in either server or client - * mode - * @param {Number} maxPayload The maximum allowed message length + * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on + * deflate + * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on + * inflate + * @param {Number} [options.threshold=1024] Size (in bytes) below which + * messages should not be compressed + * @param {Number} [options.concurrencyLimit=10] The number of concurrent + * calls to zlib + * @param {Boolean} [isServer=false] Create the instance in either server or + * client mode + * @param {Number} [maxPayload=0] The maximum allowed message length */ constructor(options, isServer, maxPayload) { this._maxPayload = maxPayload | 0; diff --git a/lib/receiver.js b/lib/receiver.js index d762393ee..15bf4b6e8 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -28,11 +28,11 @@ class Receiver extends Writable { /** * Creates a Receiver instance. * - * @param {String} binaryType The type for binary data - * @param {Object} extensions An object containing the negotiated extensions - * @param {Boolean} isServer Specifies whether to operate in client or server + * @param {String} [binaryType=nodebuffer] The type for binary data + * @param {Object} [extensions] An object containing the negotiated extensions + * @param {Boolean} [isServer=false] Specifies whether to operate in client or server * mode - * @param {Number} maxPayload The maximum allowed message length + * @param {Number} [maxPayload=0] The maximum allowed message length */ constructor(binaryType, extensions, isServer, maxPayload) { super(); diff --git a/lib/sender.js b/lib/sender.js index 3ea2fc147..ad71e1950 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -17,7 +17,7 @@ class Sender { * Creates a Sender instance. * * @param {net.Socket} socket The connection socket - * @param {Object} extensions An object containing the negotiated extensions + * @param {Object} [extensions] An object containing the negotiated extensions */ constructor(socket, extensions) { this._extensions = extensions || {}; @@ -37,10 +37,14 @@ class Sender { * @param {Buffer} data The data to frame * @param {Object} options Options object * @param {Number} options.opcode The opcode - * @param {Boolean} options.readOnly Specifies whether `data` can be modified - * @param {Boolean} options.fin Specifies whether or not to set the FIN bit - * @param {Boolean} options.mask Specifies whether or not to mask `data` - * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit * @return {Buffer[]} The framed data as a list of `Buffer` instances * @public */ @@ -93,10 +97,10 @@ class Sender { /** * Sends a close message to the other peer. * - * @param {(Number|undefined)} code The status code component of the body - * @param {String} data The message component of the body - * @param {Boolean} mask Specifies whether or not to mask the message - * @param {Function} cb Callback + * @param {Number} [code] The status code component of the body + * @param {String} [data] The message component of the body + * @param {Boolean} [mask=false] Specifies whether or not to mask the message + * @param {Function} [cb] Callback * @public */ close(code, data, mask, cb) { @@ -132,8 +136,8 @@ class Sender { * Frames and sends a close message. * * @param {Buffer} data The message to send - * @param {Boolean} mask Specifies whether or not to mask `data` - * @param {Function} cb Callback + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback * @private */ doClose(data, mask, cb) { @@ -153,8 +157,8 @@ class Sender { * Sends a ping message to the other peer. * * @param {*} data The message to send - * @param {Boolean} mask Specifies whether or not to mask `data` - * @param {Function} cb Callback + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback * @public */ ping(data, mask, cb) { @@ -175,9 +179,9 @@ class Sender { * Frames and sends a ping message. * * @param {Buffer} data The message to send - * @param {Boolean} mask Specifies whether or not to mask `data` - * @param {Boolean} readOnly Specifies whether `data` can be modified - * @param {Function} cb Callback + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified + * @param {Function} [cb] Callback * @private */ doPing(data, mask, readOnly, cb) { @@ -197,8 +201,8 @@ class Sender { * Sends a pong message to the other peer. * * @param {*} data The message to send - * @param {Boolean} mask Specifies whether or not to mask `data` - * @param {Function} cb Callback + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback * @public */ pong(data, mask, cb) { @@ -219,9 +223,9 @@ class Sender { * Frames and sends a pong message. * * @param {Buffer} data The message to send - * @param {Boolean} mask Specifies whether or not to mask `data` - * @param {Boolean} readOnly Specifies whether `data` can be modified - * @param {Function} cb Callback + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified + * @param {Function} [cb] Callback * @private */ doPong(data, mask, readOnly, cb) { @@ -242,11 +246,15 @@ class Sender { * * @param {*} data The message to send * @param {Object} options Options object - * @param {Boolean} options.compress Specifies whether or not to compress `data` - * @param {Boolean} options.binary Specifies whether `data` is binary or text - * @param {Boolean} options.fin Specifies whether the fragment is the last one - * @param {Boolean} options.mask Specifies whether or not to mask `data` - * @param {Function} cb Callback + * @param {Boolean} [options.compress=false] Specifies whether or not to + * compress `data` + * @param {Boolean} [options.binary=false] Specifies whether `data` is binary + * or text + * @param {Boolean} [options.fin=false] Specifies whether the fragment is the + * last one + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Function} [cb] Callback * @public */ send(data, options, cb) { @@ -300,14 +308,19 @@ class Sender { * Dispatches a data message. * * @param {Buffer} data The message to send - * @param {Boolean} compress Specifies whether or not to compress `data` + * @param {Boolean} [compress=false] Specifies whether or not to compress + * `data` * @param {Object} options Options object * @param {Number} options.opcode The opcode - * @param {Boolean} options.readOnly Specifies whether `data` can be modified - * @param {Boolean} options.fin Specifies whether or not to set the FIN bit - * @param {Boolean} options.mask Specifies whether or not to mask `data` - * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit - * @param {Function} cb Callback + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit + * @param {Function} [cb] Callback * @private */ dispatch(data, compress, options, cb) { @@ -374,7 +387,7 @@ class Sender { * Sends a frame. * * @param {Buffer[]} list The frame to send - * @param {Function} cb Callback + * @param {Function} [cb] Callback * @private */ sendFrame(list, cb) { diff --git a/lib/stream.js b/lib/stream.js index eb03a4993..604cf366b 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -42,7 +42,7 @@ function duplexOnError(err) { * Wraps a `WebSocket` in a duplex stream. * * @param {WebSocket} ws The `WebSocket` to wrap - * @param {Object} options The options for the `Duplex` constructor + * @param {Object} [options] The options for the `Duplex` constructor * @return {stream.Duplex} The duplex stream * @public */ diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 0d3f39592..b3922c82e 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -21,21 +21,22 @@ class WebSocketServer extends EventEmitter { * Create a `WebSocketServer` instance. * * @param {Object} options Configuration options - * @param {Number} options.backlog The maximum length of the queue of pending - * connections - * @param {Boolean} options.clientTracking Specifies whether or not to track - * clients - * @param {Function} options.handleProtocols A hook to handle protocols - * @param {String} options.host The hostname where to bind the server - * @param {Number} options.maxPayload The maximum allowed message size - * @param {Boolean} options.noServer Enable no server mode - * @param {String} options.path Accept only connections matching this path - * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable + * @param {Number} [options.backlog=511] The maximum length of the queue of + * pending connections + * @param {Boolean} [options.clientTracking=true] Specifies whether or not to + * track clients + * @param {Function} [options.handleProtocols] A hook to handle protocols + * @param {String} [options.host] The hostname where to bind the server + * @param {Number} [options.maxPayload=104857600] The maximum allowed message + * size + * @param {Boolean} [options.noServer=false] Enable no server mode + * @param {String} [options.path] Accept only connections matching this path + * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable * permessage-deflate - * @param {Number} options.port The port where to bind the server - * @param {http.Server} options.server A pre-created HTTP/S server to use - * @param {Function} options.verifyClient A hook to reject connections - * @param {Function} callback A listener for the `listening` event + * @param {Number} [options.port] The port where to bind the server + * @param {http.Server} [options.server] A pre-created HTTP/S server to use + * @param {Function} [options.verifyClient] A hook to reject connections + * @param {Function} [callback] A listener for the `listening` event */ constructor(options, callback) { super(); @@ -119,7 +120,7 @@ class WebSocketServer extends EventEmitter { /** * Close the server. * - * @param {Function} cb Callback + * @param {Function} [cb] Callback * @public */ close(cb) { diff --git a/lib/websocket.js b/lib/websocket.js index ce6f34caf..83e35d37b 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -37,8 +37,8 @@ class WebSocket extends EventEmitter { * Create a new `WebSocket`. * * @param {(String|url.URL)} address The URL to which to connect - * @param {(String|String[])} protocols The subprotocols - * @param {Object} options Connection options + * @param {(String|String[])} [protocols] The subprotocols + * @param {Object} [options] Connection options */ constructor(address, protocols, options) { super(); @@ -131,7 +131,7 @@ class WebSocket extends EventEmitter { * * @param {net.Socket} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream - * @param {Number} maxPayload The maximum allowed message size + * @param {Number} [maxPayload=0] The maximum allowed message size * @private */ setSocket(socket, head, maxPayload) { @@ -206,8 +206,8 @@ class WebSocket extends EventEmitter { * - - - - -|fin|<---------------------+ * +---+ * - * @param {Number} code Status code explaining why the connection is closing - * @param {String} data A string explaining why the connection is closing + * @param {Number} [code] Status code explaining why the connection is closing + * @param {String} [data] A string explaining why the connection is closing * @public */ close(code, data) { @@ -246,9 +246,9 @@ class WebSocket extends EventEmitter { /** * Send a ping. * - * @param {*} data The data to send - * @param {Boolean} mask Indicates whether or not to mask `data` - * @param {Function} cb Callback which is executed when the ping is sent + * @param {*} [data] The data to send + * @param {Boolean} [mask] Indicates whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when the ping is sent * @public */ ping(data, mask, cb) { @@ -278,9 +278,9 @@ class WebSocket extends EventEmitter { /** * Send a pong. * - * @param {*} data The data to send - * @param {Boolean} mask Indicates whether or not to mask `data` - * @param {Function} cb Callback which is executed when the pong is sent + * @param {*} [data] The data to send + * @param {Boolean} [mask] Indicates whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when the pong is sent * @public */ pong(data, mask, cb) { @@ -311,13 +311,15 @@ class WebSocket extends EventEmitter { * Send a data message. * * @param {*} data The message to send - * @param {Object} options Options object - * @param {Boolean} options.compress Specifies whether or not to compress + * @param {Object} [options] Options object + * @param {Boolean} [options.compress] Specifies whether or not to compress * `data` - * @param {Boolean} options.binary Specifies whether `data` is binary or text - * @param {Boolean} options.fin Specifies whether the fragment is the last one - * @param {Boolean} options.mask Specifies whether or not to mask `data` - * @param {Function} cb Callback which is executed when data is written out + * @param {Boolean} [options.binary] Specifies whether `data` is binary or + * text + * @param {Boolean} [options.fin=true] Specifies whether the fragment is the + * last one + * @param {Boolean} [options.mask] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when data is written out * @public */ send(data, options, cb) { @@ -424,19 +426,22 @@ module.exports = WebSocket; * * @param {WebSocket} websocket The client to initialize * @param {(String|url.URL)} address The URL to which to connect - * @param {String} protocols The subprotocols - * @param {Object} options Connection options - * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable + * @param {String} [protocols] The subprotocols + * @param {Object} [options] Connection options + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable * permessage-deflate - * @param {Number} options.handshakeTimeout Timeout in milliseconds for the + * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the * handshake request - * @param {Number} options.protocolVersion Value of the `Sec-WebSocket-Version` - * header - * @param {String} options.origin Value of the `Origin` or + * @param {Number} [options.protocolVersion=13] Value of the + * `Sec-WebSocket-Version` header + * @param {String} [options.origin] Value of the `Origin` or * `Sec-WebSocket-Origin` header - * @param {Number} options.maxPayload The maximum allowed message size - * @param {Boolean} options.followRedirects Whether or not to follow redirects - * @param {Number} options.maxRedirects The maximum number of redirects allowed + * @param {Number} [options.maxPayload=104857600] The maximum allowed message + * size + * @param {Boolean} [options.followRedirects=false] Whether or not to follow + * redirects + * @param {Number} [options.maxRedirects=10] The maximum number of redirects + * allowed * @private */ function initAsClient(websocket, address, protocols, options) { @@ -704,8 +709,8 @@ function abortHandshake(websocket, stream, message) { * when the `readyState` attribute is `CLOSING` or `CLOSED`. * * @param {WebSocket} websocket The WebSocket instance - * @param {*} data The data to send - * @param {Function} cb Callback + * @param {*} [data] The data to send + * @param {Function} [cb] Callback * @private */ function sendAfterClose(websocket, data, cb) { From 237960e915b3f41b8c7aabfa5c938171976f2978 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 22 Oct 2020 12:37:37 +0200 Subject: [PATCH 002/328] [codestyle] Use 80 characters per line in JSDoc comments --- lib/event-target.js | 21 ++++++++++++++------- lib/receiver.js | 4 ++-- lib/websocket-server.js | 3 ++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/event-target.js b/lib/event-target.js index bc1a385fe..a6fbe72b7 100644 --- a/lib/event-target.js +++ b/lib/event-target.js @@ -10,7 +10,8 @@ class Event { * Create a new `Event`. * * @param {String} type The name of the event - * @param {Object} target A reference to the target to which the event was dispatched + * @param {Object} target A reference to the target to which the event was + * dispatched */ constructor(type, target) { this.target = target; @@ -29,7 +30,8 @@ class MessageEvent extends Event { * Create a new `MessageEvent`. * * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data - * @param {WebSocket} target A reference to the target to which the event was dispatched + * @param {WebSocket} target A reference to the target to which the event was + * dispatched */ constructor(data, target) { super('message', target); @@ -48,9 +50,12 @@ class CloseEvent extends Event { /** * Create a new `CloseEvent`. * - * @param {Number} code The status code explaining why the connection is being closed - * @param {String} reason A human-readable string explaining why the connection is closing - * @param {WebSocket} target A reference to the target to which the event was dispatched + * @param {Number} code The status code explaining why the connection is being + * closed + * @param {String} reason A human-readable string explaining why the + * connection is closing + * @param {WebSocket} target A reference to the target to which the event was + * dispatched */ constructor(code, reason, target) { super('close', target); @@ -71,7 +76,8 @@ class OpenEvent extends Event { /** * Create a new `OpenEvent`. * - * @param {WebSocket} target A reference to the target to which the event was dispatched + * @param {WebSocket} target A reference to the target to which the event was + * dispatched */ constructor(target) { super('open', target); @@ -89,7 +95,8 @@ class ErrorEvent extends Event { * Create a new `ErrorEvent`. * * @param {Object} error The error that generated this event - * @param {WebSocket} target A reference to the target to which the event was dispatched + * @param {WebSocket} target A reference to the target to which the event was + * dispatched */ constructor(error, target) { super('error', target); diff --git a/lib/receiver.js b/lib/receiver.js index 15bf4b6e8..65a5ab45f 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -30,8 +30,8 @@ class Receiver extends Writable { * * @param {String} [binaryType=nodebuffer] The type for binary data * @param {Object} [extensions] An object containing the negotiated extensions - * @param {Boolean} [isServer=false] Specifies whether to operate in client or server - * mode + * @param {Boolean} [isServer=false] Specifies whether to operate in client or + * server mode * @param {Number} [maxPayload=0] The maximum allowed message length */ constructor(binaryType, extensions, isServer, maxPayload) { diff --git a/lib/websocket-server.js b/lib/websocket-server.js index b3922c82e..c6a68ad0e 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -339,7 +339,8 @@ module.exports = WebSocketServer; * * @param {EventEmitter} server The event emitter * @param {Object.} map The listeners to add - * @return {Function} A function that will remove the added listeners when called + * @return {Function} A function that will remove the added listeners when + * called * @private */ function addListeners(server, map) { From 572c81f375e7ba771af0db24860f5c08c644bf2b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 5 Nov 2020 20:53:09 +0100 Subject: [PATCH 003/328] [ci] Test on node 15 --- .travis.yml | 1 + appveyor.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index c87ee06ef..07c6e235d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: node_js node_js: + - '15' - '14' - '12' - '10' diff --git a/appveyor.yml b/appveyor.yml index 2217103dd..5daff04e1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,6 @@ environment: matrix: + - nodejs_version: '15' - nodejs_version: '14' - nodejs_version: '12' - nodejs_version: '10' From 7d39f19ee2b87e5c4eaec34c13a88de651533bdc Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 6 Nov 2020 20:02:30 +0100 Subject: [PATCH 004/328] [minor] Pass the request object to `server.handleUpgrade()` callback Fixes #1813 --- doc/ws.md | 8 +++++--- lib/websocket-server.js | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 84e348d9f..8e5a0d2a0 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -154,7 +154,7 @@ emitted independently. ### Event: 'connection' -- `socket` {WebSocket} +- `websocket` {WebSocket} - `request` {http.IncomingMessage} Emitted when the handshake is complete. `request` is the http GET request sent @@ -211,8 +211,10 @@ when the HTTP server is passed via the `server` option, this method is called automatically. When operating in "noServer" mode, this method must be called manually. -If the upgrade is successful, the `callback` is called with a `WebSocket` object -as parameter. +If the upgrade is successful, the `callback` is called with two arguments: + +- `websocket` {WebSocket} A `WebSocket` object. +- `request` {http.IncomingMessage} The client HTTP GET request. ### server.shouldHandle(request) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index c6a68ad0e..84d1dc9b3 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -83,13 +83,13 @@ class WebSocketServer extends EventEmitter { } if (this._server) { + const emitConnection = this.emit.bind(this, 'connection'); + this._removeListeners = addListeners(this._server, { listening: this.emit.bind(this, 'listening'), error: this.emit.bind(this, 'error'), upgrade: (req, socket, head) => { - this.handleUpgrade(req, socket, head, (ws) => { - this.emit('connection', ws, req); - }); + this.handleUpgrade(req, socket, head, emitConnection); } }); } @@ -327,7 +327,7 @@ class WebSocketServer extends EventEmitter { ws.on('close', () => this.clients.delete(ws)); } - cb(ws); + cb(ws, req); } } From eabed8fcc3694893e603460008994e3f4bcbaf42 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 7 Nov 2020 08:21:53 +0100 Subject: [PATCH 005/328] [fix] Make read-only properties read-only Fixes #1814 --- lib/websocket-server.js | 2 +- lib/websocket.js | 61 ++++++++++++++++++++++++++++------------- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 84d1dc9b3..be481a0f0 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -299,7 +299,7 @@ class WebSocketServer extends EventEmitter { if (protocol) { headers.push(`Sec-WebSocket-Protocol: ${protocol}`); - ws.protocol = protocol; + ws._protocol = protocol; } } diff --git a/lib/websocket.js b/lib/websocket.js index 83e35d37b..fa994ff91 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -43,16 +43,15 @@ class WebSocket extends EventEmitter { constructor(address, protocols, options) { super(); - this.readyState = WebSocket.CONNECTING; - this.protocol = ''; - this._binaryType = BINARY_TYPES[0]; + this._closeCode = 1006; this._closeFrameReceived = false; this._closeFrameSent = false; this._closeMessage = ''; this._closeTimer = null; - this._closeCode = 1006; this._extensions = {}; + this._protocol = ''; + this._readyState = WebSocket.CONNECTING; this._receiver = null; this._sender = null; this._socket = null; @@ -126,6 +125,27 @@ class WebSocket extends EventEmitter { return Object.keys(this._extensions).join(); } + /** + * @type {String} + */ + get protocol() { + return this._protocol; + } + + /** + * @type {Number} + */ + get readyState() { + return this._readyState; + } + + /** + * @type {String} + */ + get url() { + return this._url; + } + /** * Set up the socket and the internal resources. * @@ -166,7 +186,7 @@ class WebSocket extends EventEmitter { socket.on('end', socketOnEnd); socket.on('error', socketOnError); - this.readyState = WebSocket.OPEN; + this._readyState = WebSocket.OPEN; this.emit('open'); } @@ -177,7 +197,7 @@ class WebSocket extends EventEmitter { */ emitClose() { if (!this._socket) { - this.readyState = WebSocket.CLOSED; + this._readyState = WebSocket.CLOSED; this.emit('close', this._closeCode, this._closeMessage); return; } @@ -187,7 +207,7 @@ class WebSocket extends EventEmitter { } this._receiver.removeAllListeners(); - this.readyState = WebSocket.CLOSED; + this._readyState = WebSocket.CLOSED; this.emit('close', this._closeCode, this._closeMessage); } @@ -222,7 +242,7 @@ class WebSocket extends EventEmitter { return; } - this.readyState = WebSocket.CLOSING; + this._readyState = WebSocket.CLOSING; this._sender.close(code, data, !this._isServer, (err) => { // // This error is handled by the `'error'` listener on the socket. We only @@ -367,14 +387,17 @@ class WebSocket extends EventEmitter { } if (this._socket) { - this.readyState = WebSocket.CLOSING; + this._readyState = WebSocket.CLOSING; this._socket.destroy(); } } } readyStates.forEach((readyState, i) => { - WebSocket[readyState] = i; + Object.defineProperty(WebSocket, readyState, { + enumerable: true, + value: i + }); }); // @@ -474,10 +497,10 @@ function initAsClient(websocket, address, protocols, options) { if (address instanceof URL) { parsedUrl = address; - websocket.url = address.href; + websocket._url = address.href; } else { parsedUrl = new URL(address); - websocket.url = address; + websocket._url = address; } const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; @@ -552,7 +575,7 @@ function initAsClient(websocket, address, protocols, options) { if (websocket._req.aborted) return; req = websocket._req = null; - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; websocket.emit('error', err); websocket.emitClose(); }); @@ -623,7 +646,7 @@ function initAsClient(websocket, address, protocols, options) { return; } - if (serverProt) websocket.protocol = serverProt; + if (serverProt) websocket._protocol = serverProt; if (perMessageDeflate) { try { @@ -688,7 +711,7 @@ function tlsConnect(options) { * @private */ function abortHandshake(websocket, stream, message) { - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; const err = new Error(message); Error.captureStackTrace(err, abortHandshake); @@ -777,7 +800,7 @@ function receiverOnError(err) { websocket._socket.removeListener('data', socketOnData); - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; websocket._closeCode = err[kStatusCode]; websocket.emit('error', err); websocket._socket.destroy(); @@ -836,7 +859,7 @@ function socketOnClose() { this.removeListener('close', socketOnClose); this.removeListener('end', socketOnEnd); - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; // // The close frame might not have been received or the `'end'` event emitted, @@ -887,7 +910,7 @@ function socketOnData(chunk) { function socketOnEnd() { const websocket = this[kWebSocket]; - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; websocket._receiver.end(); this.end(); } @@ -904,7 +927,7 @@ function socketOnError() { this.on('error', NOOP); if (websocket) { - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; this.destroy(); } } From 2069e684707f078d8f5f4f4a3f026ea89c491418 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 7 Nov 2020 08:56:02 +0100 Subject: [PATCH 006/328] [fix] Fix the enumerability of some properties Make the `CONNECTING`, `OPEN`, `CLOSING`, `CLOSED`, `binaryType`, `bufferedAmount`, `extensions`, `onclose`, `onerror`, `onmessage`, `onopen`, `protocol`, `readyState`, and `url` properties enumerable. --- lib/websocket.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index fa994ff91..09fb6710b 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -74,19 +74,6 @@ class WebSocket extends EventEmitter { } } - get CONNECTING() { - return WebSocket.CONNECTING; - } - get CLOSING() { - return WebSocket.CLOSING; - } - get CLOSED() { - return WebSocket.CLOSED; - } - get OPEN() { - return WebSocket.OPEN; - } - /** * This deviates from the WHATWG interface since ws doesn't support the * required default "blob" type (instead we define a custom "nodebuffer" @@ -394,10 +381,21 @@ class WebSocket extends EventEmitter { } readyStates.forEach((readyState, i) => { - Object.defineProperty(WebSocket, readyState, { - enumerable: true, - value: i - }); + const descriptor = { enumerable: true, value: i }; + + Object.defineProperty(WebSocket.prototype, readyState, descriptor); + Object.defineProperty(WebSocket, readyState, descriptor); +}); + +[ + 'binaryType', + 'bufferedAmount', + 'extensions', + 'protocol', + 'readyState', + 'url' +].forEach((property) => { + Object.defineProperty(WebSocket.prototype, property, { enumerable: true }); }); // @@ -406,6 +404,8 @@ readyStates.forEach((readyState, i) => { // ['open', 'error', 'close', 'message'].forEach((method) => { Object.defineProperty(WebSocket.prototype, `on${method}`, { + configurable: true, + enumerable: true, /** * Return the listener of the event. * From 3f185bf34a03dc4c94c0ff92b21d1a7d4c784623 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 7 Nov 2020 09:41:03 +0100 Subject: [PATCH 007/328] [minor] Use the public `binaryType` property --- lib/websocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/websocket.js b/lib/websocket.js index 09fb6710b..5386c8ec9 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -143,7 +143,7 @@ class WebSocket extends EventEmitter { */ setSocket(socket, head, maxPayload) { const receiver = new Receiver( - this._binaryType, + this.binaryType, this._extensions, this._isServer, maxPayload From eb36a63183bfaeb130eb288d8e1374533cd7dfbe Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 8 Nov 2020 07:41:21 +0100 Subject: [PATCH 008/328] [dist] 7.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f45e9d347..3ab0e2f7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.3.1", + "version": "7.4.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 3d5066a7cad9fe3176002916aeda720a7b5ee419 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 8 Nov 2020 09:07:08 +0100 Subject: [PATCH 009/328] [test] Check configurability and enumerability of WebSocket properties --- test/websocket.test.js | 113 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 11 deletions(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index 8defba689..a9a66e739 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -100,21 +100,28 @@ describe('WebSocket', () => { Object.keys(readyStates).forEach((state) => { describe(`\`${state}\``, () => { it('is enumerable property of class', () => { - const propertyDescripter = Object.getOwnPropertyDescriptor( - WebSocket, - state - ); + const descriptor = Object.getOwnPropertyDescriptor(WebSocket, state); - assert.strictEqual(propertyDescripter.value, readyStates[state]); - assert.strictEqual(propertyDescripter.enumerable, true); + assert.deepStrictEqual(descriptor, { + configurable: false, + enumerable: true, + value: readyStates[state], + writable: false + }); }); - it('is property of instance', () => { - const ws = new WebSocket('ws://localhost', { - agent: new CustomAgent() - }); + it('is enumerable property of prototype', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + state + ); - assert.strictEqual(ws[state], readyStates[state]); + assert.deepStrictEqual(descriptor, { + configurable: false, + enumerable: true, + value: readyStates[state], + writable: false + }); }); }); }); @@ -122,6 +129,18 @@ describe('WebSocket', () => { describe('Attributes', () => { describe('`binaryType`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'binaryType' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set !== undefined); + }); + it("defaults to 'nodebuffer'", () => { const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() @@ -153,6 +172,18 @@ describe('WebSocket', () => { }); describe('`bufferedAmount`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'bufferedAmount' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + it('defaults to zero', () => { const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() @@ -225,6 +256,18 @@ describe('WebSocket', () => { }); describe('`extensions`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'bufferedAmount' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + it('exposes the negotiated extensions names (1/2)', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); @@ -269,6 +312,18 @@ describe('WebSocket', () => { }); describe('`protocol`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'protocol' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + it('exposes the subprotocol selected by the server', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const port = wss.address().port; @@ -290,6 +345,18 @@ describe('WebSocket', () => { }); describe('`readyState`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'readyState' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + it('defaults to `CONNECTING`', () => { const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() @@ -339,6 +406,18 @@ describe('WebSocket', () => { }); describe('`url`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'url' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + it('exposes the server url', () => { const url = 'ws://localhost'; const ws = new WebSocket(url, { agent: new CustomAgent() }); @@ -1729,6 +1808,18 @@ describe('WebSocket', () => { describe('WHATWG API emulation', () => { it('supports the `on{close,error,message,open}` attributes', () => { + for (const property of ['onclose', 'onerror', 'onmessage', 'onopen']) { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + property + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set !== undefined); + } + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); assert.strictEqual(ws.onmessage, undefined); From 38d6ab3b0605e245e7177e056a767a100f617e4f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 4 Dec 2020 20:28:49 +0100 Subject: [PATCH 010/328] [fix] Handle cases where the `'error'` event is emitted multiple times The `'error'` event can be emitted multiple times by the `http.ClientRequest` object in Node.js < 13. Handle the case properly. Fixes #1819 --- lib/websocket.js | 2 +- test/websocket.test.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/websocket.js b/lib/websocket.js index 5386c8ec9..739d7e714 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -572,7 +572,7 @@ function initAsClient(websocket, address, protocols, options) { } req.on('error', (err) => { - if (websocket._req.aborted) return; + if (req === null || req.aborted) return; req = websocket._req = null; websocket._readyState = WebSocket.CLOSING; diff --git a/test/websocket.test.js b/test/websocket.test.js index a9a66e739..ebd8a3d3d 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2242,6 +2242,39 @@ describe('WebSocket', () => { const ws = new WebSocket('wss://127.0.0.1', { servername: '' }); }); + + it("works around a double 'error' event bug in Node.js", function (done) { + // + // The `minVersion` and `maxVersion` options are not supported in + // Node.js < 10.16.0. + // + if (process.versions.modules < 64) return this.skip(); + + // + // The `'error'` event can be emitted multiple times by the + // `http.ClientRequest` object in Node.js < 13. This test reproduces the + // issue in Node.js 12. + // + const server = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem'), + minVersion: 'TLSv1.2' + }); + const wss = new WebSocket.Server({ server }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + maxVersion: 'TLSv1.1', + rejectUnauthorized: false + }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + server.close(done); + wss.close(); + }); + }); + }); }); describe('Request headers', () => { From c171962844e1862cadff27804700e00e2f2adbf5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 4 Dec 2020 21:41:31 +0100 Subject: [PATCH 011/328] [dist] 7.4.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ab0e2f7a..43d657045 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.0", + "version": "7.4.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From a2c0d447af711ca245cb534159fa7c4d9ae67e64 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 28 Dec 2020 21:42:32 +0100 Subject: [PATCH 012/328] [minor] Silence deprecation warning Fixes #1829 --- lib/websocket.js | 2 +- test/websocket.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 739d7e714..0e2a83d06 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -695,7 +695,7 @@ function tlsConnect(options) { options.path = undefined; if (!options.servername && options.servername !== '') { - options.servername = options.host; + options.servername = net.isIP(options.host) ? '' : options.host; } return tls.connect(options); diff --git a/test/websocket.test.js b/test/websocket.test.js index ebd8a3d3d..acc94c81f 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2123,7 +2123,7 @@ describe('WebSocket', () => { }); server.listen(0, () => { - const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + const ws = new WebSocket(`wss://127.0.0.1:${server.address().port}`, { rejectUnauthorized: false }); }); From 48a2349d229e1814ad6bcd2eb25e4e04e8b67aef Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 29 Dec 2020 21:09:51 +0100 Subject: [PATCH 013/328] [pkg] Update eslint-config-prettier to version 7.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 43d657045..905f10114 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "bufferutil": "^4.0.1", "coveralls": "^3.0.3", "eslint": "^7.2.0", - "eslint-config-prettier": "^6.0.0", + "eslint-config-prettier": "^7.1.0", "eslint-plugin-prettier": "^3.0.1", "mocha": "^7.0.0", "nyc": "^15.0.0", From d1a8af4ddb1b24a4ee23acf66decb0ed0e0d8862 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 29 Dec 2020 21:06:39 +0100 Subject: [PATCH 014/328] [dist] 7.4.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 905f10114..0d7a8b743 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.1", + "version": "7.4.2", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 2079ca5e373738e7783d2010f03432f287695e0f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 26 Jan 2021 16:49:10 +0100 Subject: [PATCH 015/328] [test] Increase code coverage --- test/websocket-server.test.js | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index f41993f3f..71646e5a4 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -320,6 +320,7 @@ describe('WebSocketServer', () => { const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); assert.strictEqual(wss.shouldHandle({ url: '/foo' }), true); + assert.strictEqual(wss.shouldHandle({ url: '/foo?bar=baz' }), true); }); it("returns false when the path doesn't match", () => { @@ -546,6 +547,41 @@ describe('WebSocketServer', () => { }); }); + it('handles unsupported extensions', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Sec-WebSocket-Extensions': 'foo; bar' + } + }); + + req.on('upgrade', (res, socket, head) => { + if (head.length) socket.unshift(head); + + socket.once('data', (chunk) => { + assert.strictEqual(chunk[0], 0x88); + wss.close(done); + }); + }); + } + ); + + wss.on('connection', (ws) => { + assert.strictEqual(ws.extensions, ''); + ws.close(); + }); + }); + describe('`verifyClient`', () => { it('can reject client synchronously', (done) => { const wss = new WebSocket.Server( From 2789887c4c3769721c371a0edf3caa6c6933f114 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 27 Jan 2021 08:53:40 +0100 Subject: [PATCH 016/328] [minor] Use `request.socket` instead of `request.connection` `request.connection` is deprecated. --- doc/ws.md | 4 ++-- lib/websocket-server.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 8e5a0d2a0..4c1b52aba 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -86,8 +86,8 @@ is provided with a single argument then that is: - `info` {Object} - `origin` {String} The value in the Origin header indicated by the client. - `req` {http.IncomingMessage} The client HTTP GET request. - - `secure` {Boolean} `true` if `req.connection.authorized` or - `req.connection.encrypted` is set. + - `secure` {Boolean} `true` if `req.socket.authorized` or + `req.socket.encrypted` is set. The return value (`Boolean`) of the function determines whether or not to accept the handshake. diff --git a/lib/websocket-server.js b/lib/websocket-server.js index be481a0f0..b99ad050a 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -225,7 +225,7 @@ class WebSocketServer extends EventEmitter { const info = { origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], - secure: !!(req.connection.authorized || req.connection.encrypted), + secure: !!(req.socket.authorized || req.socket.encrypted), req }; From 4e9607bb259dc3747881c2c22c3f65127d018a16 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 2 Feb 2021 19:18:21 +0100 Subject: [PATCH 017/328] [perf] Reset compressor/decompressor instead of re-initialize (#1840) --- lib/permessage-deflate.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index 7d7209b9e..74bf14a23 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -376,12 +376,11 @@ class PerMessageDeflate { this._inflate[kTotalLength] ); + this._inflate[kTotalLength] = 0; + this._inflate[kBuffers] = []; + if (fin && this.params[`${endpoint}_no_context_takeover`]) { - this._inflate.close(); - this._inflate = null; - } else { - this._inflate[kTotalLength] = 0; - this._inflate[kBuffers] = []; + this._inflate.reset(); } callback(null, data); @@ -448,12 +447,11 @@ class PerMessageDeflate { // this._deflate[kCallback] = null; + this._deflate[kTotalLength] = 0; + this._deflate[kBuffers] = []; + if (fin && this.params[`${endpoint}_no_context_takeover`]) { - this._deflate.close(); - this._deflate = null; - } else { - this._deflate[kTotalLength] = 0; - this._deflate[kBuffers] = []; + this._deflate.reset(); } callback(null, data); From 223194e5af389d1ab8019010cd54baccb79f0916 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 2 Feb 2021 20:16:11 +0100 Subject: [PATCH 018/328] [dist] 7.4.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d7a8b743..17cafb7d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.2", + "version": "7.4.3", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 99338f7ec6a869dbdd48ae0bcf56ca5d9aaa3f90 Mon Sep 17 00:00:00 2001 From: Ryan Christian <33403762+rschristian@users.noreply.github.com> Date: Sun, 7 Feb 2021 01:38:09 -0600 Subject: [PATCH 019/328] [doc] Fix `data` argument type (#1843) --- doc/ws.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 4c1b52aba..2da2f6440 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -417,7 +417,8 @@ receives an `OpenEvent` named "open". ### websocket.ping([data[, mask]][, callback]) -- `data` {Any} The data to send in the ping frame. +- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The + data to send in the ping frame. - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults to `true` when `websocket` is not a server client. - `callback` {Function} An optional callback which is invoked when the ping @@ -427,7 +428,8 @@ Send a ping. ### websocket.pong([data[, mask]][, callback]) -- `data` {Any} The data to send in the pong frame. +- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The + data to send in the pong frame. - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults to `true` when `websocket` is not a server client. - `callback` {Function} An optional callback which is invoked when the pong @@ -456,7 +458,8 @@ Removes an event listener emulating the `EventTarget` interface. ### websocket.send(data[, options][, callback]) -- `data` {Any} The data to send. +- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The + data to send. - `options` {Object} - `compress` {Boolean} Specifies whether `data` should be compressed or not. Defaults to `true` when permessage-deflate is enabled. From 77370e00ca75b2f88c35be7202fbe641abab5ee7 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 24 Feb 2021 20:18:51 +0100 Subject: [PATCH 020/328] [pkg] Update eslint-config-prettier to version 8.1.0 --- .eslintrc.yaml | 5 +---- package.json | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 70f32c8d4..45998d206 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -5,16 +5,13 @@ env: node: true extends: - eslint:recommended - - prettier + - plugin:prettier/recommended parserOptions: ecmaVersion: 9 -plugins: - - prettier rules: no-console: off no-var: error prefer-const: error - prettier/prettier: error quotes: - error - single diff --git a/package.json b/package.json index 17cafb7d8..6a86ac0b0 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "bufferutil": "^4.0.1", "coveralls": "^3.0.3", "eslint": "^7.2.0", - "eslint-config-prettier": "^7.1.0", + "eslint-config-prettier": "^8.1.0", "eslint-plugin-prettier": "^3.0.1", "mocha": "^7.0.0", "nyc": "^15.0.0", From 489a295be632feea34266c9966a16d5453f123dc Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 26 Feb 2021 17:42:21 +0100 Subject: [PATCH 021/328] [ci] Use GitHub Actions (#1853) --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++++++++++++++++++ .travis.yml | 17 ---------------- README.md | 2 +- package.json | 3 +-- 4 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..ceb1490f3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + - push + - pull_request + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + node: + - 8 + - 10 + - 12 + - 14 + - 15 + os: + - macOS-latest + - ubuntu-latest + - windows-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + - run: npm install + - run: npm run lint + if: matrix.node == 14 && matrix.os == 'ubuntu-latest' + - run: npm test + - uses: coverallsapp/github-action@v1.1.2 + with: + flag-name: Node.js ${{ matrix.node }} on ${{ matrix.os }} + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel: true + coverage: + needs: test + runs-on: ubuntu-latest + steps: + - uses: coverallsapp/github-action@v1.1.2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 07c6e235d..000000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: node_js -node_js: - - '15' - - '14' - - '12' - - '10' - - '8' -os: - - linux - - osx - - windows -script: - - if [ "${TRAVIS_NODE_VERSION}" == "14" ] && [ "${TRAVIS_OS_NAME}" == linux ]; - then npm run lint; fi - - npm test -after_success: - - nyc report --reporter=text-lcov | coveralls diff --git a/README.md b/README.md index f36a354bb..6aea136b1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ws: a Node.js WebSocket library [![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws) -[![Build](https://img.shields.io/travis/websockets/ws/master.svg?logo=travis)](https://travis-ci.com/websockets/ws) +[![Build](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=build&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) [![Windows x86 Build](https://img.shields.io/appveyor/ci/lpinca/ws/master.svg?logo=appveyor)](https://ci.appveyor.com/project/lpinca/ws) [![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg)](https://coveralls.io/github/websockets/ws) diff --git a/package.json b/package.json index 6a86ac0b0..659e50b91 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "lib/*.js" ], "scripts": { - "test": "nyc --reporter=html --reporter=text mocha --throw-deprecation test/*.test.js", + "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js", "integration": "mocha --throw-deprecation test/*.integration.js", "lint": "eslint --ignore-path .gitignore . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\"" }, @@ -45,7 +45,6 @@ "devDependencies": { "benchmark": "^2.1.4", "bufferutil": "^4.0.1", - "coveralls": "^3.0.3", "eslint": "^7.2.0", "eslint-config-prettier": "^8.1.0", "eslint-plugin-prettier": "^3.0.1", From cbff929b810529f64a88e4b7b8f25d19023dc912 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 3 Mar 2021 09:30:15 +0100 Subject: [PATCH 022/328] [doc] Improve `websocket.terminate()` documentation Fixes #1858 --- doc/ws.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index 2da2f6440..de62e6756 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -476,7 +476,7 @@ Send `data` through the connection. ### websocket.terminate() -Forcibly close the connection. +Forcibly close the connection. Internally this calls [socket.destroy()][]. ### websocket.url @@ -502,4 +502,5 @@ given `WebSocket`. https://nodejs.org/api/https.html#https_https_request_options_callback [permessage-deflate]: https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19 +[socket.destroy()]: https://nodejs.org/api/net.html#net_socket_destroy_error [zlib-options]: https://nodejs.org/api/zlib.html#zlib_class_options From 92774377166b9e9241982cada4e80331093021ae Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 6 Mar 2021 21:12:07 +0100 Subject: [PATCH 023/328] [fix] Recreate the inflate stream if it ends Refs: https://github.com/nodejs/node/issues/37612 --- lib/permessage-deflate.js | 13 +++++++++---- test/permessage-deflate.test.js | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index 74bf14a23..a8974b988 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -376,11 +376,16 @@ class PerMessageDeflate { this._inflate[kTotalLength] ); - this._inflate[kTotalLength] = 0; - this._inflate[kBuffers] = []; + if (this._inflate._readableState.endEmitted) { + this._inflate.close(); + this._inflate = null; + } else { + this._inflate[kTotalLength] = 0; + this._inflate[kBuffers] = []; - if (fin && this.params[`${endpoint}_no_context_takeover`]) { - this._inflate.reset(); + if (fin && this.params[`${endpoint}_no_context_takeover`]) { + this._inflate.reset(); + } } callback(null, data); diff --git a/test/permessage-deflate.test.js b/test/permessage-deflate.test.js index 09681d96f..a547762ca 100644 --- a/test/permessage-deflate.test.js +++ b/test/permessage-deflate.test.js @@ -631,5 +631,26 @@ describe('PerMessageDeflate', () => { process.nextTick(() => perMessageDeflate.cleanup()); }); + + it('recreates the inflate stream if it ends', (done) => { + const perMessageDeflate = new PerMessageDeflate(); + const extensions = extension.parse( + 'permessage-deflate; client_no_context_takeover; ' + + 'server_no_context_takeover' + ); + const buf = Buffer.from('33343236313533b7000000', 'hex'); + const expected = Buffer.from('12345678'); + + perMessageDeflate.accept(extensions['permessage-deflate']); + + perMessageDeflate.decompress(buf, true, (err, data) => { + assert.ok(data.equals(expected)); + + perMessageDeflate.decompress(buf, true, (err, data) => { + assert.ok(data.equals(expected)); + done(); + }); + }); + }); }); }); From a74dd2ee88ca87e1e0af7062331996bc35f311a6 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 6 Mar 2021 21:29:14 +0100 Subject: [PATCH 024/328] [dist] 7.4.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 659e50b91..66cba8372 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.3", + "version": "7.4.4", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From d75a62ed661af25244e4825bec4813688886e3bd Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 7 Mar 2021 06:54:46 +0100 Subject: [PATCH 025/328] [ci] Include commit SHA in `flag-name` --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceb1490f3..21f0e8396 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,8 @@ jobs: - run: npm test - uses: coverallsapp/github-action@v1.1.2 with: - flag-name: Node.js ${{ matrix.node }} on ${{ matrix.os }} + flag-name: + ${{github.sha}} - Node.js ${{ matrix.node }} on ${{ matrix.os }} github-token: ${{ secrets.GITHUB_TOKEN }} parallel: true coverage: From 114de9e33668075f0af88dc440f1ebd813161e72 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 7 Mar 2021 08:44:53 +0100 Subject: [PATCH 026/328] [ci] Use a unique ID instead of commit SHA --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21f0e8396..76326adeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,10 +28,16 @@ jobs: - run: npm run lint if: matrix.node == 14 && matrix.os == 'ubuntu-latest' - run: npm test + - run: + echo ::set-output name=job_id::$(node -e + "console.log(crypto.randomBytes(16).toString('hex'))") + id: get_job_id + shell: bash - uses: coverallsapp/github-action@v1.1.2 with: flag-name: - ${{github.sha}} - Node.js ${{ matrix.node }} on ${{ matrix.os }} + ${{ steps.get_job_id.outputs.job_id }} (Node.js ${{ matrix.node }} + on ${{ matrix.os }}) github-token: ${{ secrets.GITHUB_TOKEN }} parallel: true coverage: From 23ba6b2922f521f2b656891a997ab562b7139dd4 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 17 Apr 2021 16:23:19 +0200 Subject: [PATCH 027/328] [fix] Make UTF-8 validation work even if utf-8-validate is not installed Fixes #1868 --- README.md | 4 +- lib/validation.js | 100 ++++++++++++++++++++++++++++++++++------ test/validation.test.js | 52 +++++++++++++++++++++ 3 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 test/validation.test.js diff --git a/README.md b/README.md index 6aea136b1..9c6e5287c 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ can use one of the many wrappers available on npm, like npm install ws ``` -### Opt-in for performance and spec compliance +### Opt-in for performance There are 2 optional modules that can be installed along side with the ws module. These modules are binary addons which improve certain operations. @@ -67,7 +67,7 @@ necessarily need to have a C++ compiler installed on your machine. operations such as masking and unmasking the data payload of the WebSocket frames. - `npm install --save-optional utf-8-validate`: Allows to efficiently check if a - message contains valid UTF-8 as required by the spec. + message contains valid UTF-8. ## API docs diff --git a/lib/validation.js b/lib/validation.js index 32db5a570..d8693fdb9 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -1,16 +1,5 @@ 'use strict'; -try { - const isValidUTF8 = require('utf-8-validate'); - - exports.isValidUTF8 = - typeof isValidUTF8 === 'object' - ? isValidUTF8.Validation.isValidUTF8 // utf-8-validate@<3.0.0 - : isValidUTF8; -} catch (e) /* istanbul ignore next */ { - exports.isValidUTF8 = () => true; -} - /** * Checks if a status code is allowed in a close frame. * @@ -18,7 +7,7 @@ try { * @return {Boolean} `true` if the status code is valid, else `false` * @public */ -exports.isValidStatusCode = (code) => { +function isValidStatusCode(code) { return ( (code >= 1000 && code <= 1014 && @@ -27,4 +16,89 @@ exports.isValidStatusCode = (code) => { code !== 1006) || (code >= 3000 && code <= 4999) ); -}; +} + +/** + * Checks if a given buffer contains only correct UTF-8. + * Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by + * Markus Kuhn. + * + * @param {Buffer} buf The buffer to check + * @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false` + * @public + */ +function _isValidUTF8(buf) { + const len = buf.length; + let i = 0; + + while (i < len) { + if (buf[i] < 0x80) { + // 0xxxxxxx + i++; + } else if ((buf[i] & 0xe0) === 0xc0) { + // 110xxxxx 10xxxxxx + if ( + i + 1 === len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i] & 0xfe) === 0xc0 // Overlong + ) { + return false; + } else { + i += 2; + } + } else if ((buf[i] & 0xf0) === 0xe0) { + // 1110xxxx 10xxxxxx 10xxxxxx + if ( + i + 2 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong + (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) + ) { + return false; + } else { + i += 3; + } + } else if ((buf[i] & 0xf8) === 0xf0) { + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if ( + i + 3 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i + 3] & 0xc0) !== 0x80 || + (buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong + (buf[i] === 0xf4 && buf[i + 1] > 0x8f) || + buf[i] > 0xf4 // > U+10FFFF + ) { + return false; + } else { + i += 4; + } + } else { + return false; + } + } + + return true; +} + +try { + let isValidUTF8 = require('utf-8-validate'); + + /* istanbul ignore if */ + if (typeof isValidUTF8 === 'object') { + isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0 + } + + module.exports = { + isValidStatusCode, + isValidUTF8(buf) { + return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); + } + }; +} catch (e) /* istanbul ignore next */ { + module.exports = { + isValidStatusCode, + isValidUTF8: _isValidUTF8 + }; +} diff --git a/test/validation.test.js b/test/validation.test.js new file mode 100644 index 000000000..5718b12f0 --- /dev/null +++ b/test/validation.test.js @@ -0,0 +1,52 @@ +'use strict'; + +const assert = require('assert'); + +const { isValidUTF8 } = require('../lib/validation'); + +describe('extension', () => { + describe('isValidUTF8', () => { + it('returns false if it finds invalid bytes', () => { + assert.strictEqual(isValidUTF8(Buffer.from([0xf8])), false); + }); + + it('returns false for overlong encodings', () => { + assert.strictEqual(isValidUTF8(Buffer.from([0xc0, 0xa0])), false); + assert.strictEqual(isValidUTF8(Buffer.from([0xe0, 0x80, 0xa0])), false); + assert.strictEqual( + isValidUTF8(Buffer.from([0xf0, 0x80, 0x80, 0xa0])), + false + ); + }); + + it('returns false for code points in the range U+D800 - U+DFFF', () => { + for (let i = 0xa0; i < 0xc0; i++) { + for (let j = 0x80; j < 0xc0; j++) { + assert.strictEqual(isValidUTF8(Buffer.from([0xed, i, j])), false); + } + } + }); + + it('returns false for code points greater than U+10FFFF', () => { + assert.strictEqual( + isValidUTF8(Buffer.from([0xf4, 0x90, 0x80, 0x80])), + false + ); + assert.strictEqual( + isValidUTF8(Buffer.from([0xf5, 0x80, 0x80, 0x80])), + false + ); + }); + + it('returns true for a well-formed UTF-8 byte sequence', () => { + // prettier-ignore + const buf = Buffer.from([ + 0xe2, 0x82, 0xAC, // € + 0xf0, 0x90, 0x8c, 0x88, // 𐍈 + 0x24 // $ + ]); + + assert.strictEqual(isValidUTF8(buf), true); + }); + }); +}); From 67e25ff50230d131d76b1061ca0be5c991df161f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 18 Apr 2021 09:34:27 +0200 Subject: [PATCH 028/328] [fix] Fix case where `abortHandshake()` does not close the connection On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if called after the request completed. Fixes #1869 --- lib/websocket.js | 10 ++++++++ test/websocket.test.js | 52 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 0e2a83d06..539238190 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -718,6 +718,16 @@ function abortHandshake(websocket, stream, message) { if (stream.setHeader) { stream.abort(); + + if (stream.socket && !stream.socket.destroyed) { + // + // On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if + // called after the request completed. See + // https://github.com/websockets/ws/issues/1869. + // + stream.socket.destroy(); + } + stream.once('abort', websocket.emitClose.bind(websocket)); websocket.emit('error', err); } else { diff --git a/test/websocket.test.js b/test/websocket.test.js index acc94c81f..8bfc9e151 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1444,7 +1444,7 @@ describe('WebSocket', () => { }); describe('#close', () => { - it('closes the connection if called while connecting (1/2)', (done) => { + it('closes the connection if called while connecting (1/3)', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); @@ -1461,7 +1461,7 @@ describe('WebSocket', () => { }); }); - it('closes the connection if called while connecting (2/2)', (done) => { + it('closes the connection if called while connecting (2/3)', (done) => { const wss = new WebSocket.Server( { verifyClient: (info, cb) => setTimeout(cb, 300, true), @@ -1484,6 +1484,54 @@ describe('WebSocket', () => { ); }); + it('closes the connection if called while connecting (3/3)', (done) => { + const server = http.createServer(); + + server.listen(0, function () { + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + ws.on('close', () => { + server.close(done); + }); + }); + + ws.on('unexpected-response', (req, res) => { + assert.strictEqual(res.statusCode, 502); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual(Buffer.concat(chunks).toString(), 'foo'); + ws.close(); + }); + }); + }); + + server.on('upgrade', (req, socket) => { + socket.on('end', socket.end); + + socket.write( + `HTTP/1.1 502 ${http.STATUS_CODES[502]}\r\n` + + 'Connection: keep-alive\r\n' + + 'Content-type: text/html\r\n' + + 'Content-Length: 3\r\n' + + '\r\n' + + 'foo' + ); + }); + }); + it('can be called from an error listener while connecting', (done) => { const ws = new WebSocket('ws://localhost:1337'); From f67271079755e79a1ac2b40f3f4efb94ca024539 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 18 Apr 2021 10:00:59 +0200 Subject: [PATCH 029/328] [dist] 7.4.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66cba8372..c2d63afe1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.4", + "version": "7.4.5", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 587c201bfc22c460658ca304d23477fc7ebd2a60 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 23 Apr 2021 20:23:23 +0200 Subject: [PATCH 030/328] [ci] Do not test on node 15 --- .github/workflows/ci.yml | 1 - appveyor.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76326adeb..4ae3b6aa4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,6 @@ jobs: - 10 - 12 - 14 - - 15 os: - macOS-latest - ubuntu-latest diff --git a/appveyor.yml b/appveyor.yml index 5daff04e1..2217103dd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,5 @@ environment: matrix: - - nodejs_version: '15' - nodejs_version: '14' - nodejs_version: '12' - nodejs_version: '10' From fc7e27d12ad0af90ce05302afc85c292024000b4 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 23 Apr 2021 20:24:19 +0200 Subject: [PATCH 031/328] [ci] Test on node 16 --- .github/workflows/ci.yml | 3 ++- appveyor.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ae3b6aa4..158a50e32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ jobs: - 10 - 12 - 14 + - 16 os: - macOS-latest - ubuntu-latest @@ -25,7 +26,7 @@ jobs: node-version: ${{ matrix.node }} - run: npm install - run: npm run lint - if: matrix.node == 14 && matrix.os == 'ubuntu-latest' + if: matrix.node == 16 && matrix.os == 'ubuntu-latest' - run: npm test - run: echo ::set-output name=job_id::$(node -e diff --git a/appveyor.yml b/appveyor.yml index 2217103dd..f4c05fbf4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,6 @@ environment: matrix: + - nodejs_version: '16' - nodejs_version: '14' - nodejs_version: '12' - nodejs_version: '10' From 8c914d18b86a7d1408884d18eeadae0fa41b0bb5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 4 May 2021 12:18:24 +0200 Subject: [PATCH 032/328] [minor] Fix nits --- lib/validation.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/validation.js b/lib/validation.js index d8693fdb9..169ac6f06 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -32,7 +32,7 @@ function _isValidUTF8(buf) { let i = 0; while (i < len) { - if (buf[i] < 0x80) { + if ((buf[i] & 0x80) === 0) { // 0xxxxxxx i++; } else if ((buf[i] & 0xe0) === 0xc0) { @@ -43,9 +43,9 @@ function _isValidUTF8(buf) { (buf[i] & 0xfe) === 0xc0 // Overlong ) { return false; - } else { - i += 2; } + + i += 2; } else if ((buf[i] & 0xf0) === 0xe0) { // 1110xxxx 10xxxxxx 10xxxxxx if ( @@ -56,9 +56,9 @@ function _isValidUTF8(buf) { (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) ) { return false; - } else { - i += 3; } + + i += 3; } else if ((buf[i] & 0xf8) === 0xf0) { // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx if ( @@ -71,9 +71,9 @@ function _isValidUTF8(buf) { buf[i] > 0xf4 // > U+10FFFF ) { return false; - } else { - i += 4; } + + i += 4; } else { return false; } From 32e3a8439b7c8273b44fe1adb5682f529e34d0ba Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 25 May 2021 15:54:54 +0200 Subject: [PATCH 033/328] [security] Remove reference to Node Security Project The Node Security Platform service no longer exists. New security advisories will be published to GitHub Security Advisories. --- SECURITY.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 258ff59fd..eebb2a552 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -25,8 +25,9 @@ following methods: Once we have acknowledged receipt of your report and confirmed the bug ourselves we will work with you to fix the vulnerability and publicly acknowledge your -responsible disclosure, if you wish. In addition to that we will report all -vulnerabilities to the [Node Security Project](https://nodesecurity.io/). +responsible disclosure, if you wish. In addition to that we will create and +publish a security advisory to +[GitHub Security Advisories](https://github.com/websockets/ws/security/advisories). ## History From 990306d1446faf346c76452409a4c11455690514 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 25 May 2021 16:48:37 +0200 Subject: [PATCH 034/328] [lint] Fix prettier error --- lib/websocket.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 539238190..83b471d94 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -654,9 +654,8 @@ function initAsClient(websocket, address, protocols, options) { if (extensions[PerMessageDeflate.extensionName]) { perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); - websocket._extensions[ - PerMessageDeflate.extensionName - ] = perMessageDeflate; + websocket._extensions[PerMessageDeflate.extensionName] = + perMessageDeflate; } } catch (err) { abortHandshake( From 00c425ec77993773d823f018f64a5c44e17023ff Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 25 May 2021 11:00:58 +0200 Subject: [PATCH 035/328] [security] Fix ReDoS vulnerability A specially crafted value of the `Sec-Websocket-Protocol` header could be used to significantly slow down a ws server. PoC and fix were sent privately by Robert McLaughlin from University of California, Santa Barbara. --- lib/websocket-server.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index b99ad050a..3c3bbe0b0 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -286,7 +286,7 @@ class WebSocketServer extends EventEmitter { let protocol = req.headers['sec-websocket-protocol']; if (protocol) { - protocol = protocol.trim().split(/ *, */); + protocol = protocol.split(',').map(trim); // // Optionally call external protocol selection handler. @@ -404,3 +404,15 @@ function abortHandshake(socket, code, message, headers) { socket.removeListener('error', socketOnError); socket.destroy(); } + +/** + * Remove whitespace characters from both ends of a string. + * + * @param {String} str The string + * @return {String} A new string representing `str` stripped of whitespace + * characters from both its beginning and end + * @private + */ +function trim(str) { + return str.trim(); +} From f5297f7090f6a628832a730187c5b3a06a247f00 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 25 May 2021 18:11:07 +0200 Subject: [PATCH 036/328] [dist] 7.4.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c2d63afe1..2ab6e3769 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.5", + "version": "7.4.6", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From c05d51f167e2464a3e8cf1888d60ac1da9b38197 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 25 May 2021 18:33:07 +0200 Subject: [PATCH 037/328] [security] Add ReDoS vulnerability to SECURITY.md --- SECURITY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index eebb2a552..9e3f475d4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -35,3 +35,5 @@ publish a security advisory to [Buffer vulnerability](https://github.com/websockets/ws/releases/tag/1.0.1) - 08 Nov 2017: [DoS vulnerability](https://github.com/websockets/ws/releases/tag/3.3.1) +- 25 May 2021: + [ReDoS in `Sec-Websocket-Protocol` header](https://github.com/websockets/ws/releases/tag/7.4.6) From 2f2b3e8f8417c799fd579ced1a3e89f9a18fbb1c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 26 May 2021 21:20:45 +0200 Subject: [PATCH 038/328] [test] Update certificates and private keys Fixes #1890 --- test/fixtures/agent1-cert.pem | 24 ++++++++++-------------- test/fixtures/agent1-key.pem | 20 +++++--------------- test/fixtures/ca1-cert.pem | 23 ++++++++++------------- test/fixtures/ca1-key.pem | 22 +++++----------------- test/fixtures/certificate.pem | 21 ++++++++++----------- test/fixtures/key.pem | 20 +++++--------------- test/fixtures/request.pem | 11 ----------- 7 files changed, 45 insertions(+), 96 deletions(-) delete mode 100644 test/fixtures/request.pem diff --git a/test/fixtures/agent1-cert.pem b/test/fixtures/agent1-cert.pem index cccb9fb4d..0e20560b8 100644 --- a/test/fixtures/agent1-cert.pem +++ b/test/fixtures/agent1-cert.pem @@ -1,16 +1,12 @@ -----BEGIN CERTIFICATE----- -MIICbjCCAdcCCQCVvok5oeLpqzANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV -UzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQKEwZKb3llbnQxEDAO -BgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqGSIb3DQEJARYRcnlA -dGlueWNsb3Vkcy5vcmcwHhcNMTMwMzA4MDAzMDIyWhcNNDAwNzIzMDAzMDIyWjB9 -MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQK -EwZKb3llbnQxEDAOBgNVBAsTB05vZGUuanMxDzANBgNVBAMTBmFnZW50MTEgMB4G -CSqGSIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcwgZ8wDQYJKoZIhvcNAQEBBQAD -gY0AMIGJAoGBAL6GwKosYb0Yc3Qo0OtQVlCJ4208Idw11ij+t2W5sfYbCil5tyQo -jnhGM1CJhEXynQpXXwjKJuIeTQCkeUibTyFKa0bs8+li2FiGoKYbb4G81ovnqkmE -2iDVb8Gw3rrM4zeZ0ZdFnjMsAZac8h6+C4sB/pS9BiMOo6qTl15RQlcJAgMBAAEw -DQYJKoZIhvcNAQEFBQADgYEAOtmLo8DwTPnI4wfQbQ3hWlTS/9itww6IsxH2ODt9 -ggB7wi7N3uAdIWRZ54ke0NEAO5CW1xNTwsWcxQbiHrDOqX1vfVCjIenI76jVEEap -/Ay53ydHNBKdsKkib61Me14Mu0bA3lUul57VXwmH4NUEFB3w973Q60PschUhOEXj -7DY= +MIIBtzCCAV0CCQDDIX2dKuKP0zAKBggqhkjOPQQDAjBhMQswCQYDVQQGEwJJVDEQ +MA4GA1UECAwHUGVydWdpYTEQMA4GA1UEBwwHRm9saWdubzETMBEGA1UECgwKd2Vi +c29ja2V0czELMAkGA1UECwwCd3MxDDAKBgNVBAMMA2NhMTAgFw0yMTA1MjYxOTE3 +NDJaGA8yMTIxMDUwMjE5MTc0MlowZDELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB1Bl +cnVnaWExEDAOBgNVBAcMB0ZvbGlnbm8xEzARBgNVBAoMCndlYnNvY2tldHMxCzAJ +BgNVBAsMAndzMQ8wDQYDVQQDDAZhZ2VudDEwWTATBgcqhkjOPQIBBggqhkjOPQMB +BwNCAATwHlNS2b13TMhBTSWBXAn6TEPxrsvG93ZZyUlmrEMOXSMX2hI7sv660YNj ++eGyE2CV33XsQxV3TUqi51fUjIu8MAoGCCqGSM49BAMCA0gAMEUCIQCxsqBre+Do +jnfg6XmCaB0fywNzcDlvdoVNuNAWfVNrSAIgDQmbM0mXZaSAkf4sgtKdXnpE3vrb +MElb457Bi3B+rkE= -----END CERTIFICATE----- diff --git a/test/fixtures/agent1-key.pem b/test/fixtures/agent1-key.pem index cbd5f0c26..e034f57fc 100644 --- a/test/fixtures/agent1-key.pem +++ b/test/fixtures/agent1-key.pem @@ -1,15 +1,5 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQC+hsCqLGG9GHN0KNDrUFZQieNtPCHcNdYo/rdlubH2Gwopebck -KI54RjNQiYRF8p0KV18IyibiHk0ApHlIm08hSmtG7PPpYthYhqCmG2+BvNaL56pJ -hNog1W/BsN66zOM3mdGXRZ4zLAGWnPIevguLAf6UvQYjDqOqk5deUUJXCQIDAQAB -AoGANu/CBA+SCyVOvRK70u4yRTzNMAUjukxnuSBhH1rg/pajYnwvG6T6F6IeT72n -P0gKkh3JUE6B0bds+p9yPUZTFUXghxjcF33wlIY44H6gFE4K5WutsFJ9c450wtuu -8rXZTsIg7lAXWjTFVmdtOEPetcGlO2Hpi1O7ZzkzHgB2w9ECQQDksCCYx78or1zY -ZSokm8jmpIjG3VLKdvI9HAoJRN40ldnwFoigrFa1AHwsFtWNe8bKyVRPDoLDUjpB -dkPWgweVAkEA1UfgqguQ2KIkbtp9nDBionu3QaajksrRHwIa8vdfRfLxszfHk2fh -NGY3dkRZF8HUAbzYLrd9poVhCBAEjWekpQJASOM6AHfpnXYHCZF01SYx6hEW5wsz -kARJQODm8f1ZNTlttO/5q/xBxn7ZFNRSTD3fJlL05B2j380ddC/Vf1FT4QJAP1BC -GliqnBSuGhZUWYxni3KMeTm9rzL0F29pjpzutHYlWB2D6ndY/FQnvL0XcZ0Bka58 -womIDGnl3x3aLBwLXQJBAJv6h5CHbXHx7VyDJAcNfppAqZGcEaiVg8yf2F33iWy2 -FLthhJucx7df7SO2aw5h06bRDRAhb9br0R9/3mLr7RE= ------END RSA PRIVATE KEY----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKVGskK0UR86WwMo5H0+hNAFGRBYsEevK3ye4y1YberVoAoGCCqGSM49 +AwEHoUQDQgAE8B5TUtm9d0zIQU0lgVwJ+kxD8a7Lxvd2WclJZqxDDl0jF9oSO7L+ +utGDY/nhshNgld917EMVd01KoudX1IyLvA== +-----END EC PRIVATE KEY----- diff --git a/test/fixtures/ca1-cert.pem b/test/fixtures/ca1-cert.pem index 1d0c0d688..0f1658821 100644 --- a/test/fixtures/ca1-cert.pem +++ b/test/fixtures/ca1-cert.pem @@ -1,15 +1,12 @@ -----BEGIN CERTIFICATE----- -MIICazCCAdQCCQC9/g69HtxXRzANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV -UzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQKEwZKb3llbnQxEDAO -BgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqGSIb3DQEJARYRcnlA -dGlueWNsb3Vkcy5vcmcwHhcNMTMwMzA4MDAzMDIyWhcNNDAwNzIzMDAzMDIyWjB6 -MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQK -EwZKb3llbnQxEDAOBgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqG -SIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAKxr1mARUcv7zaqx5y4AxJPK6c1jdbSg7StcL4vg8klaPAlfNO6o+/Cl -w5CdQD3ukaVUwUOJ4T/+b3Xf7785XcWBC33GdjVQkfbHATJYcka7j7JDw3qev5Jk -1rAbRw48hF6rYlSGcx1mccAjoLoa3I8jgxCNAYHIjUQXgdmU893rAgMBAAEwDQYJ -KoZIhvcNAQEFBQADgYEAis05yxjCtJRuv8uX/DK6TX/j9C9Lzp1rKDNFTaTZ0iRw -KCw1EcNx4OXSj9gNblW4PWxpDvygrt1AmH9h2cb8K859NSHa9JOBFw6MA5C2A4Sj -NQfNATqUl4T6cdORlcDEZwHtT8b6D4A6Er31G/eJF4Sen0TUFpjdjd+l9RBjHlo= +MIIBtTCCAVoCCQCXqK2FegDgiDAKBggqhkjOPQQDAjBhMQswCQYDVQQGEwJJVDEQ +MA4GA1UECAwHUGVydWdpYTEQMA4GA1UEBwwHRm9saWdubzETMBEGA1UECgwKd2Vi +c29ja2V0czELMAkGA1UECwwCd3MxDDAKBgNVBAMMA2NhMTAgFw0yMTA1MjYxOTA1 +MjdaGA8yMTIxMDUwMjE5MDUyN1owYTELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB1Bl +cnVnaWExEDAOBgNVBAcMB0ZvbGlnbm8xEzARBgNVBAoMCndlYnNvY2tldHMxCzAJ +BgNVBAsMAndzMQwwCgYDVQQDDANjYTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC +AASHE75QDQN6XNo/711YSbckaa8r4lt0hGkgtADaBFT9Qn9gcm5omapePZT76Ff9 +rwjMcS+YPXS7J7bk+QHLihJMMAoGCCqGSM49BAMCA0kAMEYCIQCUMdUih+sE0ZTu +ORlcKiM8DKyiKkGU4Ty+dslz6nVJjAIhAMcSy0SBsBDgsai1s9aCmAGJXCijNb6g +vfWaatgq+ma2 -----END CERTIFICATE----- diff --git a/test/fixtures/ca1-key.pem b/test/fixtures/ca1-key.pem index df1495083..a9352fb6a 100644 --- a/test/fixtures/ca1-key.pem +++ b/test/fixtures/ca1-key.pem @@ -1,17 +1,5 @@ ------BEGIN ENCRYPTED PRIVATE KEY----- -MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIFeWxJE1BrRECAggA -MBQGCCqGSIb3DQMHBAgu9PlMSQ+BOASCAoDEZN2tX0xWo/N+Jg+PrvCrFDk3P+3x -5xG/PEDjtMCAWPBEwbnaYHDzYmhNcAmxzGqEHGMDiWYs46LbO560VS3uMvFbEWPo -KYYVb13vkxl2poXdonCb5cHZA5GUYzTIVVJFptl4LHwBczHoMHtA4FqAhKlYvlWw -EOrdLB8XcwMmGPFabbbGxno0+EWWM27uNjlogfoxj35mQqSW4rOlhZ460XjOB1Zx -LjXMuZeONojkGYQRG5EUMchBoctQpCOM6cAi9r1B9BvtFCBpDV1c1zEZBzTEUd8o -kLn6tjLmY+QpTdylFjEWc7U3ppLY/pkoTBv4r85a2sEMWqkhSJboLaTboWzDJcU3 -Ke61pMpovt/3yCUd3TKgwduVwwQtDVTlBe0p66aN9QVj3CrFy/bKAGO3vxlli24H -aIjZf+OVoBY21ESlW3jLvNlBf7Ezf///2E7j4SCDLyZSFMTpFoAG/jDRyvi+wTKX -Kh485Bptnip6DCSuoH4u2SkOqwz3gJS/6s02YKe4m311QT4Pzne5/FwOFaS/HhQg -Xvyh2/d00OgJ0Y0PYQsHILPRgTUCKUXvj1O58opn3fxSacsPxIXwj6Z4FYAjUTaV -2B85k1lpant/JJEilDqMjqzx4pHZ/Z3Uto1lSM1JZs9SNL/0UR+6F0TXZTULVU9V -w8jYzz4sPr7LEyrrTbzmjQgnQFVbhAN/eKgRZK/SpLjxpmBV5MfpbPKsPUZqT4UC -4nXa8a/NYUQ9e+QKK8enq9E599c2W442W7Z1uFRZTWReMx/lF8wwA6G8zOPG0bdj -d+T5Gegzd5mvRiXMBklCo8RLxOOvgxun1n3PY4a63aH6mqBhdfhiLp5j ------END ENCRYPTED PRIVATE KEY----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIAa/Onpk27cLkqzje69Bac8yG+LTBXIPWT8yGlyjEFbboAoGCCqGSM49 +AwEHoUQDQgAEhxO+UA0DelzaP+9dWEm3JGmvK+JbdIRpILQA2gRU/UJ/YHJuaJmq +Xj2U++hX/a8IzHEvmD10uye25PkBy4oSTA== +-----END EC PRIVATE KEY----- diff --git a/test/fixtures/certificate.pem b/test/fixtures/certificate.pem index 0efc2ef5b..538553ee0 100644 --- a/test/fixtures/certificate.pem +++ b/test/fixtures/certificate.pem @@ -1,13 +1,12 @@ -----BEGIN CERTIFICATE----- -MIICATCCAWoCCQDPufXH86n2QzANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJu -bzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMB4XDTEyMDEwMTE0NDQwMFoXDTIwMDMxOTE0NDQwMFowRTELMAkG -A1UEBhMCbm8xEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 -IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtrQ7 -+r//2iV/B6F+4boH0XqFn7alcV9lpjvAmwRXNKnxAoa0f97AjYPGNLKrjpkNXXhB -JROIdbRbZnCNeC5fzX1a+JCo7KStzBXuGSZr27TtFmcV4H+9gIRIcNHtZmJLnxbJ -sIhkGR8yVYdmJZe4eT5ldk1zoB1adgPF1hZhCBMCAwEAATANBgkqhkiG9w0BAQUF -AAOBgQCeWBEHYJ4mCB5McwSSUox0T+/mJ4W48L/ZUE4LtRhHasU9hiW92xZkTa7E -QLcoJKQiWfiLX2ysAro0NX4+V8iqLziMqvswnPzz5nezaOLE/9U/QvH3l8qqNkXu -rNbsW1h/IO6FV8avWFYVFoutUwOaZ809k7iMh2F2JMgXQ5EymQ== +MIIBujCCAWACCQDjKdAMt3mZhDAKBggqhkjOPQQDAjBkMQswCQYDVQQGEwJJVDEQ +MA4GA1UECAwHUGVydWdpYTEQMA4GA1UEBwwHRm9saWdubzETMBEGA1UECgwKd2Vi +c29ja2V0czELMAkGA1UECwwCd3MxDzANBgNVBAMMBnNlcnZlcjAgFw0yMTA1MjYx +OTEwMjlaGA8yMTIxMDUwMjE5MTAyOVowZDELMAkGA1UEBhMCSVQxEDAOBgNVBAgM +B1BlcnVnaWExEDAOBgNVBAcMB0ZvbGlnbm8xEzARBgNVBAoMCndlYnNvY2tldHMx +CzAJBgNVBAsMAndzMQ8wDQYDVQQDDAZzZXJ2ZXIwWTATBgcqhkjOPQIBBggqhkjO +PQMBBwNCAAQKhyRhdSVOecbJU4O5XkB/iGodbnCOqmchs4TXmE3Prv5SrNDhODDv +rOWTXwR3/HrrdNfOzPdb54amu8POwpohMAoGCCqGSM49BAMCA0gAMEUCIHMRUSPl +8FGkDLl8KF1A+SbT2ds3zUOLdYvj30Z2SKSVAiEA84U/R1ly9wf5Rzv93sTHI99o +KScsr/PHN8rT2pop5pk= -----END CERTIFICATE----- diff --git a/test/fixtures/key.pem b/test/fixtures/key.pem index 176fe320b..05bfdb71e 100644 --- a/test/fixtures/key.pem +++ b/test/fixtures/key.pem @@ -1,15 +1,5 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQC2tDv6v//aJX8HoX7hugfReoWftqVxX2WmO8CbBFc0qfEChrR/ -3sCNg8Y0squOmQ1deEElE4h1tFtmcI14Ll/NfVr4kKjspK3MFe4ZJmvbtO0WZxXg -f72AhEhw0e1mYkufFsmwiGQZHzJVh2Yll7h5PmV2TXOgHVp2A8XWFmEIEwIDAQAB -AoGAAlVY8sHi/aE+9xT77twWX3mGHV0SzdjfDnly40fx6S1Gc7bOtVdd9DC7pk6l -3ENeJVR02IlgU8iC5lMHq4JEHPE272jtPrLlrpWLTGmHEqoVFv9AITPqUDLhB9Kk -Hjl7h8NYBKbr2JHKICr3DIPKOT+RnXVb1PD4EORbJ3ooYmkCQQDfknUnVxPgxUGs -ouABw1WJIOVgcCY/IFt4Ihf6VWTsxBgzTJKxn3HtgvE0oqTH7V480XoH0QxHhjLq -DrgobWU9AkEA0TRJ8/ouXGnFEPAXjWr9GdPQRZ1Use2MrFjneH2+Sxc0CmYtwwqL -Kr5kS6mqJrxprJeluSjBd+3/ElxURrEXjwJAUvmlN1OPEhXDmRHd92mKnlkyKEeX -OkiFCiIFKih1S5Y/sRJTQ0781nyJjtJqO7UyC3pnQu1oFEePL+UEniRztQJAMfav -AtnpYKDSM+1jcp7uu9BemYGtzKDTTAYfoiNF42EzSJiGrWJDQn4eLgPjY0T0aAf/ -yGz3Z9ErbhMm/Ysl+QJBAL4kBxRT8gM4ByJw4sdOvSeCCANFq8fhbgm8pGWlCPb5 -JGmX3/GHFM8x2tbWMGpyZP1DLtiNEFz7eCGktWK5rqE= ------END RSA PRIVATE KEY----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIjLz7YEWIrsGem2+YV8eJhHhetsjYIrjuqJLbdG7B3zoAoGCCqGSM49 +AwEHoUQDQgAECockYXUlTnnGyVODuV5Af4hqHW5wjqpnIbOE15hNz67+UqzQ4Tgw +76zlk18Ed/x663TXzsz3W+eGprvDzsKaIQ== +-----END EC PRIVATE KEY----- diff --git a/test/fixtures/request.pem b/test/fixtures/request.pem deleted file mode 100644 index 51bc7f625..000000000 --- a/test/fixtures/request.pem +++ /dev/null @@ -1,11 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIBhDCB7gIBADBFMQswCQYDVQQGEwJubzETMBEGA1UECAwKU29tZS1TdGF0ZTEh -MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB -AQUAA4GNADCBiQKBgQC2tDv6v//aJX8HoX7hugfReoWftqVxX2WmO8CbBFc0qfEC -hrR/3sCNg8Y0squOmQ1deEElE4h1tFtmcI14Ll/NfVr4kKjspK3MFe4ZJmvbtO0W -ZxXgf72AhEhw0e1mYkufFsmwiGQZHzJVh2Yll7h5PmV2TXOgHVp2A8XWFmEIEwID -AQABoAAwDQYJKoZIhvcNAQEFBQADgYEAjsUXEARgfxZNkMjuUcudgU2w4JXS0gGI -JQ0U1LmU0vMDSKwqndMlvCbKzEgPbJnGJDI8D4MeINCJHa5Ceyb8c+jaJYUcCabl -lQW5Psn3+eWp8ncKlIycDRj1Qk615XuXtV0fhkrgQM2ZCm9LaQ1O1Gd/CzLihLjF -W0MmgMKMMRk= ------END CERTIFICATE REQUEST----- From d18c677dbd88f38ab8312d341f2b0284e1648713 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 26 May 2021 21:27:29 +0200 Subject: [PATCH 039/328] [security] Update link to point to published security advisories --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 9e3f475d4..0baf19a63 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,7 +27,7 @@ Once we have acknowledged receipt of your report and confirmed the bug ourselves we will work with you to fix the vulnerability and publicly acknowledge your responsible disclosure, if you wish. In addition to that we will create and publish a security advisory to -[GitHub Security Advisories](https://github.com/websockets/ws/security/advisories). +[GitHub Security Advisories](https://github.com/websockets/ws/security/advisories?state=published). ## History From 262e45ac93622ffcf5ad0c33a2acf1eab481a501 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 27 May 2021 08:55:10 +0200 Subject: [PATCH 040/328] [test] Rename certificates and private keys files Rename `ca1-cert.pem` to `ca-certificate.pem`, `ca1-key.pem` to `ca-key.pem`, `agent1-cert.pem` to `client-certificate.pem`, and `agent1-key.pem` to `client-key.pem`. --- test/fixtures/{ca1-cert.pem => ca-certificate.pem} | 0 test/fixtures/{ca1-key.pem => ca-key.pem} | 0 test/fixtures/{agent1-cert.pem => client-certificate.pem} | 0 test/fixtures/{agent1-key.pem => client-key.pem} | 0 test/websocket.test.js | 6 +++--- 5 files changed, 3 insertions(+), 3 deletions(-) rename test/fixtures/{ca1-cert.pem => ca-certificate.pem} (100%) rename test/fixtures/{ca1-key.pem => ca-key.pem} (100%) rename test/fixtures/{agent1-cert.pem => client-certificate.pem} (100%) rename test/fixtures/{agent1-key.pem => client-key.pem} (100%) diff --git a/test/fixtures/ca1-cert.pem b/test/fixtures/ca-certificate.pem similarity index 100% rename from test/fixtures/ca1-cert.pem rename to test/fixtures/ca-certificate.pem diff --git a/test/fixtures/ca1-key.pem b/test/fixtures/ca-key.pem similarity index 100% rename from test/fixtures/ca1-key.pem rename to test/fixtures/ca-key.pem diff --git a/test/fixtures/agent1-cert.pem b/test/fixtures/client-certificate.pem similarity index 100% rename from test/fixtures/agent1-cert.pem rename to test/fixtures/client-certificate.pem diff --git a/test/fixtures/agent1-key.pem b/test/fixtures/client-key.pem similarity index 100% rename from test/fixtures/agent1-key.pem rename to test/fixtures/client-key.pem diff --git a/test/websocket.test.js b/test/websocket.test.js index 8bfc9e151..03ae90be9 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2180,7 +2180,7 @@ describe('WebSocket', () => { it('connects to secure websocket server with client side certificate', (done) => { const server = https.createServer({ cert: fs.readFileSync('test/fixtures/certificate.pem'), - ca: [fs.readFileSync('test/fixtures/ca1-cert.pem')], + ca: [fs.readFileSync('test/fixtures/ca-certificate.pem')], key: fs.readFileSync('test/fixtures/key.pem'), requestCert: true }); @@ -2202,8 +2202,8 @@ describe('WebSocket', () => { server.listen(0, () => { const ws = new WebSocket(`wss://localhost:${server.address().port}`, { - cert: fs.readFileSync('test/fixtures/agent1-cert.pem'), - key: fs.readFileSync('test/fixtures/agent1-key.pem'), + cert: fs.readFileSync('test/fixtures/client-certificate.pem'), + key: fs.readFileSync('test/fixtures/client-key.pem'), rejectUnauthorized: false }); }); From edff6bb01f1102ad2cc389ad25fce7a6aef40f72 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 28 May 2021 11:38:47 +0200 Subject: [PATCH 041/328] [test] Fix nit --- test/websocket.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index 03ae90be9..d7cf3aee8 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1487,7 +1487,7 @@ describe('WebSocket', () => { it('closes the connection if called while connecting (3/3)', (done) => { const server = http.createServer(); - server.listen(0, function () { + server.listen(0, () => { const ws = new WebSocket(`ws://localhost:${server.address().port}`); ws.on('open', () => done(new Error("Unexpected 'open' event"))); From 7ee31157d7b14bb94e0d0fd223a4a5508f4c39b9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 29 May 2021 21:21:01 +0200 Subject: [PATCH 042/328] [doc] Add logo to coverage badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c6e5287c..261ab95b0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws) [![Build](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=build&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) [![Windows x86 Build](https://img.shields.io/appveyor/ci/lpinca/ws/master.svg?logo=appveyor)](https://ci.appveyor.com/project/lpinca/ws) -[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg)](https://coveralls.io/github/websockets/ws) +[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws) ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and server implementation. From 03a707884c591d56ad69c4c1ddd34cab0449b1fe Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 31 May 2021 18:59:21 +0200 Subject: [PATCH 043/328] [doc] Remove unsafe regex from code snippet --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 261ab95b0..c16df2036 100644 --- a/README.md +++ b/README.md @@ -395,7 +395,7 @@ the `X-Forwarded-For` header. ```js wss.on('connection', function connection(ws, req) { - const ip = req.headers['x-forwarded-for'].split(/\s*,\s*/)[0]; + const ip = req.headers['x-forwarded-for'].split(',')[0].trim(); }); ``` From 05b8ccd639a91428d7440ad350b8d4301636b2e2 Mon Sep 17 00:00:00 2001 From: Mestery Date: Sat, 5 Jun 2021 21:02:44 +0200 Subject: [PATCH 044/328] [doc] Fix broken link (#1897) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c16df2036..1cb19d650 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ can use one of the many wrappers available on npm, like - [Protocol support](#protocol-support) - [Installing](#installing) - - [Opt-in for performance and spec compliance](#opt-in-for-performance-and-spec-compliance) + - [Opt-in for performance](#opt-in-for-performance) - [API docs](#api-docs) - [WebSocket compression](#websocket-compression) - [Usage examples](#usage-examples) From 8806aa9a836c3a616c9511adad159c65eeb153b0 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 8 Jun 2021 11:29:50 +0200 Subject: [PATCH 045/328] [fix] Close the connection cleanly when an error occurs Instead of destroying the socket, try to close the connection cleanly if an error (such as a data framing error) occurs after the opening handshake has completed. Also, to comply with the specification, use the 1006 status code if no close frame is received, even if the connection is closed due to an error. Fixes #1898 --- lib/websocket.js | 5 ++--- test/websocket.test.js | 23 +++++++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 83b471d94..40ab4e86a 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -808,11 +808,10 @@ function receiverOnError(err) { const websocket = this[kWebSocket]; websocket._socket.removeListener('data', socketOnData); + websocket._socket.resume(); - websocket._readyState = WebSocket.CLOSING; - websocket._closeCode = err[kStatusCode]; + websocket.close(err[kStatusCode]); websocket.emit('error', err); - websocket._socket.destroy(); } /** diff --git a/test/websocket.test.js b/test/websocket.test.js index d7cf3aee8..a0557980e 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -429,6 +429,7 @@ describe('WebSocket', () => { describe('Events', () => { it("emits an 'error' event if an error occurs", (done) => { + let clientCloseEventEmitted = false; const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); @@ -440,14 +441,21 @@ describe('WebSocket', () => { ); ws.on('close', (code, reason) => { - assert.strictEqual(code, 1002); + clientCloseEventEmitted = true; + assert.strictEqual(code, 1006); assert.strictEqual(reason, ''); - wss.close(done); }); }); }); wss.on('connection', (ws) => { + ws.on('close', (code, reason) => { + assert.ok(clientCloseEventEmitted); + assert.strictEqual(code, 1002); + assert.strictEqual(reason, ''); + wss.close(done); + }); + ws._socket.write(Buffer.from([0x85, 0x00])); }); }); @@ -1410,10 +1418,17 @@ describe('WebSocket', () => { }); it('honors the `mask` option', (done) => { + let serverClientCloseEventEmitted = false; const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); ws.on('open', () => ws.send('hi', { mask: false })); + ws.on('close', (code, reason) => { + assert.ok(serverClientCloseEventEmitted); + assert.strictEqual(code, 1002); + assert.strictEqual(reason, ''); + wss.close(done); + }); }); wss.on('connection', (ws) => { @@ -1434,9 +1449,9 @@ describe('WebSocket', () => { ); ws.on('close', (code, reason) => { - assert.strictEqual(code, 1002); + serverClientCloseEventEmitted = true; + assert.strictEqual(code, 1006); assert.strictEqual(reason, ''); - wss.close(done); }); }); }); From 074e6a8be7275a69a407f6c1fa2270c754d2834b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 9 Jun 2021 09:14:33 +0200 Subject: [PATCH 046/328] [fix] Don't call `ws.terminate()` unconditionally in `duplex._destroy()` Call `ws.terminate()` only if `duplex.destroy()` is called directly by the user and not indirectly by the listener of the `'error'` event of the `WebSocket` object. Calling `ws.terminate()` right after the `'error'` event is emitted on the `WebSocket` object, might prevent the close frame from being sent to the other peer. --- lib/stream.js | 14 +++++++++++++- test/create-websocket-stream.test.js | 9 ++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/stream.js b/lib/stream.js index 604cf366b..53706cc90 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -48,6 +48,7 @@ function duplexOnError(err) { */ function createWebSocketStream(ws, options) { let resumeOnReceiverDrain = true; + let terminateOnDestroy = true; function receiverOnDrain() { if (resumeOnReceiverDrain) ws._socket.resume(); @@ -81,6 +82,16 @@ function createWebSocketStream(ws, options) { ws.once('error', function error(err) { if (duplex.destroyed) return; + // Prevent `ws.terminate()` from being called by `duplex._destroy()`. + // + // - If the state of the `WebSocket` connection is `CONNECTING`, + // `ws.terminate()` is a noop as no socket was assigned. + // - Otherwise, the error was re-emitted from the listener of the `'error'` + // event of the `Receiver` object. The listener already closes the + // connection by calling `ws.close()`. This allows a close frame to be + // sent to the other peer. If `ws.terminate()` is called right after this, + // the close frame might not be sent. + terminateOnDestroy = false; duplex.destroy(err); }); @@ -108,7 +119,8 @@ function createWebSocketStream(ws, options) { if (!called) callback(err); process.nextTick(emitClose, duplex); }); - ws.terminate(); + + if (terminateOnDestroy) ws.terminate(); }; duplex._final = function (callback) { diff --git a/test/create-websocket-stream.test.js b/test/create-websocket-stream.test.js index 5da01bb18..ddccb56b2 100644 --- a/test/create-websocket-stream.test.js +++ b/test/create-websocket-stream.test.js @@ -203,6 +203,7 @@ describe('createWebSocketStream', () => { }); it('reemits errors', (done) => { + let duplexCloseEventEmitted = false; const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); const duplex = createWebSocketStream(ws); @@ -215,13 +216,19 @@ describe('createWebSocketStream', () => { ); duplex.on('close', () => { - wss.close(done); + duplexCloseEventEmitted = true; }); }); }); wss.on('connection', (ws) => { ws._socket.write(Buffer.from([0x85, 0x00])); + ws.on('close', (code, reason) => { + assert.ok(duplexCloseEventEmitted); + assert.strictEqual(code, 1002); + assert.strictEqual(reason, ''); + wss.close(done); + }); }); }); From c6e30806704cd1ff35282b85132bd29fca8acec8 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 15 Jun 2021 06:22:01 -0700 Subject: [PATCH 047/328] [minor] Attach error codes to all receiver errors (#1901) Fixes #1892 --- doc/ws.md | 66 ++++++++++++- lib/permessage-deflate.js | 1 + lib/receiver.js | 138 +++++++++++++++++++++++---- test/create-websocket-stream.test.js | 1 + test/receiver.test.js | 22 +++++ test/websocket.test.js | 1 + 6 files changed, 209 insertions(+), 20 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index de62e6756..7212a1c01 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -44,6 +44,18 @@ - [websocket.terminate()](#websocketterminate) - [websocket.url](#websocketurl) - [WebSocket.createWebSocketStream(websocket[, options])](#websocketcreatewebsocketstreamwebsocket-options) +- [WS Error Codes](#ws-error-codes) + - [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#wserrunsupporteddatapayloadlength) + - [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#wserrunsupporteddatapayloadlength) + - [WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH](#wserrinvalidcontrolpayloadlength) + - [WS_ERR_INVALID_UTF8](#wserrinvalidutf8) + - [WS_ERR_INVALID_OPCODE](#wserrinvalidopcode) + - [WS_ERR_INVALID_CLOSE_CODE](#wserrinvalidclosecode) + - [WS_ERR_UNEXPECTED_RSV_1](#wserrunexpectedrsv1) + - [WS_ERR_UNEXPECTED_RSV_2_3](#wserrunexpectedrsv23) + - [WS_ERR_EXPECTED_FIN](#wserrexpectedfin) + - [WS_ERR_EXPECTED_MASK](#wserrexpectedmask) + - [WS_ERR_UNEXPECTED_MASK](#wserrunexpectedmask) ## Class: WebSocket.Server @@ -298,7 +310,8 @@ human-readable string explaining why the connection has been closed. - `error` {Error} -Emitted when an error occurs. +Emitted when an error occurs. Errors may have a `.code` property, matching one +of the string values defined below under [WS Error Codes](#ws-error-codes). ### Event: 'message' @@ -493,6 +506,57 @@ The URL of the WebSocket server. Server clients don't have this attribute. Returns a `Duplex` stream that allows to use the Node.js streams API on top of a given `WebSocket`. +## WS Error Codes + +Errors emitted by the websocket may have a `.code` property, describing the +specific type of error that has occurred: + +### WS_ERR_UNSUPPORTED_MESSAGE_LENGTH + +A message was received with a length longer than the maximum supported length, +as configured by the `maxPayload` option. + +### WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH + +A data frame was received with a length longer the max supported length (2^53-1, +due to JavaScript language limitations). + +### WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH + +A control frame with an invalid payload length was received. + +### WS_ERR_INVALID_UTF8 + +A text or close frame was received containing invalid UTF-8 data. + +### WS_ERR_INVALID_OPCODE + +A WebSocket frame was received with an invalid opcode. + +### WS_ERR_INVALID_CLOSE_CODE + +A WebSocket close frame was received with an invalid close code. + +### WS_ERR_UNEXPECTED_RSV_1 + +A WebSocket frame was received with the RSV1 bit set unexpectedly. + +### WS_ERR_UNEXPECTED_RSV_2_3 + +A WebSocket frame was received with the RSV2 or RSV3 bit set unexpectedly. + +### WS_ERR_EXPECTED_FIN + +A WebSocket frame was received with the FIN bit not set when it was expected. + +### WS_ERR_EXPECTED_MASK + +An unmasked WebSocket frame was received by a WebSocket server. + +### WS_ERR_UNEXPECTED_MASK + +A masked WebSocket frame was received by a WebSocket client. + [concurrency-limit]: https://github.com/websockets/ws/issues/1202 [duplex-options]: https://nodejs.org/api/stream.html#stream_new_stream_duplex_options diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index a8974b988..ce9178429 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -495,6 +495,7 @@ function inflateOnData(chunk) { } this[kError] = new RangeError('Max payload size exceeded'); + this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; this[kError][kStatusCode] = 1009; this.removeListener('data', inflateOnData); this.reset(); diff --git a/lib/receiver.js b/lib/receiver.js index 65a5ab45f..7ed992214 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -168,14 +168,26 @@ class Receiver extends Writable { if ((buf[0] & 0x30) !== 0x00) { this._loop = false; - return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002); + return error( + RangeError, + 'RSV2 and RSV3 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_2_3' + ); } const compressed = (buf[0] & 0x40) === 0x40; if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } this._fin = (buf[0] & 0x80) === 0x80; @@ -185,31 +197,61 @@ class Receiver extends Writable { if (this._opcode === 0x00) { if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } if (!this._fragmented) { this._loop = false; - return error(RangeError, 'invalid opcode 0', true, 1002); + return error( + RangeError, + 'invalid opcode 0', + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._opcode = this._fragmented; } else if (this._opcode === 0x01 || this._opcode === 0x02) { if (this._fragmented) { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._compressed = compressed; } else if (this._opcode > 0x07 && this._opcode < 0x0b) { if (!this._fin) { this._loop = false; - return error(RangeError, 'FIN must be set', true, 1002); + return error( + RangeError, + 'FIN must be set', + true, + 1002, + 'WS_ERR_EXPECTED_FIN' + ); } if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } if (this._payloadLength > 0x7d) { @@ -218,12 +260,19 @@ class Receiver extends Writable { RangeError, `invalid payload length ${this._payloadLength}`, true, - 1002 + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' ); } } else { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } if (!this._fin && !this._fragmented) this._fragmented = this._opcode; @@ -232,11 +281,23 @@ class Receiver extends Writable { if (this._isServer) { if (!this._masked) { this._loop = false; - return error(RangeError, 'MASK must be set', true, 1002); + return error( + RangeError, + 'MASK must be set', + true, + 1002, + 'WS_ERR_EXPECTED_MASK' + ); } } else if (this._masked) { this._loop = false; - return error(RangeError, 'MASK must be clear', true, 1002); + return error( + RangeError, + 'MASK must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_MASK' + ); } if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; @@ -285,7 +346,8 @@ class Receiver extends Writable { RangeError, 'Unsupported WebSocket frame: payload length > 2^53 - 1', false, - 1009 + 1009, + 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' ); } @@ -304,7 +366,13 @@ class Receiver extends Writable { this._totalPayloadLength += this._payloadLength; if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { this._loop = false; - return error(RangeError, 'Max payload size exceeded', false, 1009); + return error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); } } @@ -384,7 +452,13 @@ class Receiver extends Writable { this._messageLength += buf.length; if (this._messageLength > this._maxPayload && this._maxPayload > 0) { return cb( - error(RangeError, 'Max payload size exceeded', false, 1009) + error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ) ); } @@ -431,7 +505,13 @@ class Receiver extends Writable { if (!isValidUTF8(buf)) { this._loop = false; - return error(Error, 'invalid UTF-8 sequence', true, 1007); + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } this.emit('message', buf.toString()); @@ -456,18 +536,36 @@ class Receiver extends Writable { this.emit('conclude', 1005, ''); this.end(); } else if (data.length === 1) { - return error(RangeError, 'invalid payload length 1', true, 1002); + return error( + RangeError, + 'invalid payload length 1', + true, + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' + ); } else { const code = data.readUInt16BE(0); if (!isValidStatusCode(code)) { - return error(RangeError, `invalid status code ${code}`, true, 1002); + return error( + RangeError, + `invalid status code ${code}`, + true, + 1002, + 'WS_ERR_INVALID_CLOSE_CODE' + ); } const buf = data.slice(2); if (!isValidUTF8(buf)) { - return error(Error, 'invalid UTF-8 sequence', true, 1007); + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } this.emit('conclude', code, buf.toString()); @@ -493,15 +591,17 @@ module.exports = Receiver; * @param {Boolean} prefix Specifies whether or not to add a default prefix to * `message` * @param {Number} statusCode The status code + * @param {String} errorCode The exposed error code * @return {(Error|RangeError)} The error * @private */ -function error(ErrorCtor, message, prefix, statusCode) { +function error(ErrorCtor, message, prefix, statusCode, errorCode) { const err = new ErrorCtor( prefix ? `Invalid WebSocket frame: ${message}` : message ); Error.captureStackTrace(err, error); + err.code = errorCode; err[kStatusCode] = statusCode; return err; } diff --git a/test/create-websocket-stream.test.js b/test/create-websocket-stream.test.js index ddccb56b2..bcd240974 100644 --- a/test/create-websocket-stream.test.js +++ b/test/create-websocket-stream.test.js @@ -210,6 +210,7 @@ describe('createWebSocketStream', () => { duplex.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid opcode 5' diff --git a/test/receiver.test.js b/test/receiver.test.js index a70cc8dbe..cd5770dfb 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -513,6 +513,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_1'); assert.strictEqual( err.message, 'Invalid WebSocket frame: RSV1 must be clear' @@ -534,6 +535,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_1'); assert.strictEqual( err.message, 'Invalid WebSocket frame: RSV1 must be clear' @@ -550,6 +552,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_2_3'); assert.strictEqual( err.message, 'Invalid WebSocket frame: RSV2 and RSV3 must be clear' @@ -566,6 +569,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_2_3'); assert.strictEqual( err.message, 'Invalid WebSocket frame: RSV2 and RSV3 must be clear' @@ -582,6 +586,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid opcode 0' @@ -598,6 +603,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid opcode 1' @@ -615,6 +621,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid opcode 2' @@ -632,6 +639,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_EXPECTED_FIN'); assert.strictEqual( err.message, 'Invalid WebSocket frame: FIN must be set' @@ -653,6 +661,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_1'); assert.strictEqual( err.message, 'Invalid WebSocket frame: RSV1 must be clear' @@ -669,6 +678,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_EXPECTED_FIN'); assert.strictEqual( err.message, 'Invalid WebSocket frame: FIN must be set' @@ -685,6 +695,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_EXPECTED_MASK'); assert.strictEqual( err.message, 'Invalid WebSocket frame: MASK must be set' @@ -701,6 +712,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_MASK'); assert.strictEqual( err.message, 'Invalid WebSocket frame: MASK must be clear' @@ -719,6 +731,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid payload length 126' @@ -735,6 +748,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH'); assert.strictEqual( err.message, 'Unsupported WebSocket frame: payload length > 2^53 - 1' @@ -756,6 +770,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof Error); + assert.strictEqual(err.code, 'WS_ERR_INVALID_UTF8'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid UTF-8 sequence' @@ -778,6 +793,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof Error); + assert.strictEqual(err.code, 'WS_ERR_INVALID_UTF8'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid UTF-8 sequence' @@ -799,6 +815,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid payload length 1' @@ -815,6 +832,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_CLOSE_CODE'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid status code 0' @@ -831,6 +849,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof Error); + assert.strictEqual(err.code, 'WS_ERR_INVALID_UTF8'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid UTF-8 sequence' @@ -860,6 +879,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'); assert.strictEqual(err.message, 'Max payload size exceeded'); assert.strictEqual(err[kStatusCode], 1009); done(); @@ -884,6 +904,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'); assert.strictEqual(err.message, 'Max payload size exceeded'); assert.strictEqual(err[kStatusCode], 1009); done(); @@ -913,6 +934,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'); assert.strictEqual(err.message, 'Max payload size exceeded'); assert.strictEqual(err[kStatusCode], 1009); done(); diff --git a/test/websocket.test.js b/test/websocket.test.js index a0557980e..03f984fcb 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -435,6 +435,7 @@ describe('WebSocket', () => { ws.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid opcode 5' From bb5d44b11880861f9fb0429e2c132f435a78198b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 15 Jun 2021 15:48:30 +0200 Subject: [PATCH 048/328] [doc] Sort error codes alphabetically --- doc/ws.md | 54 +++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 7212a1c01..3f041627f 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -45,17 +45,17 @@ - [websocket.url](#websocketurl) - [WebSocket.createWebSocketStream(websocket[, options])](#websocketcreatewebsocketstreamwebsocket-options) - [WS Error Codes](#ws-error-codes) - - [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#wserrunsupporteddatapayloadlength) - - [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#wserrunsupporteddatapayloadlength) + - [WS_ERR_EXPECTED_FIN](#wserrexpectedfin) + - [WS_ERR_EXPECTED_MASK](#wserrexpectedmask) + - [WS_ERR_INVALID_CLOSE_CODE](#wserrinvalidclosecode) - [WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH](#wserrinvalidcontrolpayloadlength) - - [WS_ERR_INVALID_UTF8](#wserrinvalidutf8) - [WS_ERR_INVALID_OPCODE](#wserrinvalidopcode) - - [WS_ERR_INVALID_CLOSE_CODE](#wserrinvalidclosecode) + - [WS_ERR_INVALID_UTF8](#wserrinvalidutf8) + - [WS_ERR_UNEXPECTED_MASK](#wserrunexpectedmask) - [WS_ERR_UNEXPECTED_RSV_1](#wserrunexpectedrsv1) - [WS_ERR_UNEXPECTED_RSV_2_3](#wserrunexpectedrsv23) - - [WS_ERR_EXPECTED_FIN](#wserrexpectedfin) - - [WS_ERR_EXPECTED_MASK](#wserrexpectedmask) - - [WS_ERR_UNEXPECTED_MASK](#wserrunexpectedmask) + - [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#wserrunsupporteddatapayloadlength) + - [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#wserrunsupporteddatapayloadlength) ## Class: WebSocket.Server @@ -511,31 +511,33 @@ given `WebSocket`. Errors emitted by the websocket may have a `.code` property, describing the specific type of error that has occurred: -### WS_ERR_UNSUPPORTED_MESSAGE_LENGTH +### WS_ERR_EXPECTED_FIN -A message was received with a length longer than the maximum supported length, -as configured by the `maxPayload` option. +A WebSocket frame was received with the FIN bit not set when it was expected. -### WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH +### WS_ERR_EXPECTED_MASK -A data frame was received with a length longer the max supported length (2^53-1, -due to JavaScript language limitations). +An unmasked WebSocket frame was received by a WebSocket server. -### WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH +### WS_ERR_INVALID_CLOSE_CODE -A control frame with an invalid payload length was received. +A WebSocket close frame was received with an invalid close code. -### WS_ERR_INVALID_UTF8 +### WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH -A text or close frame was received containing invalid UTF-8 data. +A control frame with an invalid payload length was received. ### WS_ERR_INVALID_OPCODE A WebSocket frame was received with an invalid opcode. -### WS_ERR_INVALID_CLOSE_CODE +### WS_ERR_INVALID_UTF8 -A WebSocket close frame was received with an invalid close code. +A text or close frame was received containing invalid UTF-8 data. + +### WS_ERR_UNEXPECTED_MASK + +A masked WebSocket frame was received by a WebSocket client. ### WS_ERR_UNEXPECTED_RSV_1 @@ -545,17 +547,15 @@ A WebSocket frame was received with the RSV1 bit set unexpectedly. A WebSocket frame was received with the RSV2 or RSV3 bit set unexpectedly. -### WS_ERR_EXPECTED_FIN - -A WebSocket frame was received with the FIN bit not set when it was expected. - -### WS_ERR_EXPECTED_MASK +### WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH -An unmasked WebSocket frame was received by a WebSocket server. +A data frame was received with a length longer the max supported length (2^53-1, +due to JavaScript language limitations). -### WS_ERR_UNEXPECTED_MASK +### WS_ERR_UNSUPPORTED_MESSAGE_LENGTH -A masked WebSocket frame was received by a WebSocket client. +A message was received with a length longer than the maximum supported length, +as configured by the `maxPayload` option. [concurrency-limit]: https://github.com/websockets/ws/issues/1202 [duplex-options]: From 6eea0d466b08a278c048092ee1cb06aee9f48cc9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 15 Jun 2021 15:50:18 +0200 Subject: [PATCH 049/328] [doc] Fix typo --- doc/ws.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 3f041627f..56681ade5 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -549,8 +549,8 @@ A WebSocket frame was received with the RSV2 or RSV3 bit set unexpectedly. ### WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH -A data frame was received with a length longer the max supported length (2^53-1, -due to JavaScript language limitations). +A data frame was received with a length longer than the max supported length +(2^53 - 1, due to JavaScript language limitations). ### WS_ERR_UNSUPPORTED_MESSAGE_LENGTH From 1d3f4cbb0ebb2519f6cc707e9f4344006d74ce03 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 15 Jun 2021 16:20:37 +0200 Subject: [PATCH 050/328] [doc] Fix anchor tags for error codes --- doc/ws.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 56681ade5..af0dfb325 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -45,17 +45,17 @@ - [websocket.url](#websocketurl) - [WebSocket.createWebSocketStream(websocket[, options])](#websocketcreatewebsocketstreamwebsocket-options) - [WS Error Codes](#ws-error-codes) - - [WS_ERR_EXPECTED_FIN](#wserrexpectedfin) - - [WS_ERR_EXPECTED_MASK](#wserrexpectedmask) - - [WS_ERR_INVALID_CLOSE_CODE](#wserrinvalidclosecode) - - [WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH](#wserrinvalidcontrolpayloadlength) - - [WS_ERR_INVALID_OPCODE](#wserrinvalidopcode) - - [WS_ERR_INVALID_UTF8](#wserrinvalidutf8) - - [WS_ERR_UNEXPECTED_MASK](#wserrunexpectedmask) - - [WS_ERR_UNEXPECTED_RSV_1](#wserrunexpectedrsv1) - - [WS_ERR_UNEXPECTED_RSV_2_3](#wserrunexpectedrsv23) - - [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#wserrunsupporteddatapayloadlength) - - [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#wserrunsupporteddatapayloadlength) + - [WS_ERR_EXPECTED_FIN](#ws_err_expected_fin) + - [WS_ERR_EXPECTED_MASK](#ws_err_expected_mask) + - [WS_ERR_INVALID_CLOSE_CODE](#ws_err_invalid_close_code) + - [WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH](#ws_err_invalid_control_payload_length) + - [WS_ERR_INVALID_OPCODE](#ws_err_invalid_opcode) + - [WS_ERR_INVALID_UTF8](#ws_err_invalid_utf8) + - [WS_ERR_UNEXPECTED_MASK](#ws_err_unexpected_mask) + - [WS_ERR_UNEXPECTED_RSV_1](#ws_err_unexpected_rsv_1) + - [WS_ERR_UNEXPECTED_RSV_2_3](#ws_err_unexpected_rsv_2_3) + - [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#ws_err_unsupported_data_payload_length) + - [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#ws_err_unsupported_message_length) ## Class: WebSocket.Server From e3f0c1720aab640fe78dc578907046fb84422ccd Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 16 Jun 2021 15:13:32 +0200 Subject: [PATCH 051/328] [dist] 7.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ab6e3769..9f38e4660 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.6", + "version": "7.5.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 145480a5b520ee951d848009d51069bfd7ed928c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 20 Jun 2021 19:48:58 +0200 Subject: [PATCH 052/328] [test] Fix repeated typo --- test/websocket-server.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 71646e5a4..ce6617ec6 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -471,7 +471,7 @@ describe('WebSocketServer', () => { }); }); - it('fails is the Sec-WebSocket-Version header is invalid (1/2)', (done) => { + it('fails if the Sec-WebSocket-Version header is invalid (1/2)', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const req = http.get({ port: wss.address().port, @@ -493,7 +493,7 @@ describe('WebSocketServer', () => { }); }); - it('fails is the Sec-WebSocket-Version header is invalid (2/2)', (done) => { + it('fails if the Sec-WebSocket-Version header is invalid (2/2)', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const req = http.get({ port: wss.address().port, @@ -516,7 +516,7 @@ describe('WebSocketServer', () => { }); }); - it('fails is the Sec-WebSocket-Extensions header is invalid', (done) => { + it('fails if the Sec-WebSocket-Extensions header is invalid', (done) => { const wss = new WebSocket.Server( { perMessageDeflate: true, From c3fdc994502cfb2f9a1274e78530a08609f5efb1 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 28 Jun 2021 11:16:11 +0200 Subject: [PATCH 053/328] [minor] Fix misleading comment --- lib/stream.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/stream.js b/lib/stream.js index 53706cc90..c3a793691 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -84,13 +84,13 @@ function createWebSocketStream(ws, options) { // Prevent `ws.terminate()` from being called by `duplex._destroy()`. // - // - If the state of the `WebSocket` connection is `CONNECTING`, - // `ws.terminate()` is a noop as no socket was assigned. - // - Otherwise, the error was re-emitted from the listener of the `'error'` + // - If the `'error'` event is emitted before the `'open'` event, then + // `ws.terminate()` is a noop as no socket is assigned. + // - Otherwise, the error is re-emitted by the listener of the `'error'` // event of the `Receiver` object. The listener already closes the // connection by calling `ws.close()`. This allows a close frame to be // sent to the other peer. If `ws.terminate()` is called right after this, - // the close frame might not be sent. + // then the close frame might not be sent. terminateOnDestroy = false; duplex.destroy(err); }); From b434b9f1653d6fda562c937f65b1f07f81c6aa1a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 25 Jun 2021 22:08:01 +0200 Subject: [PATCH 054/328] [fix] Fix close edge cases Ensure that `socket.end()` is called if an error occurs simultaneously on both peers. Refs: https://github.com/websockets/ws/pull/1902 --- lib/websocket.js | 16 +++- test/create-websocket-stream.test.js | 8 +- test/websocket.test.js | 136 +++++++++++++++++++++++++-- 3 files changed, 150 insertions(+), 10 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 40ab4e86a..280364a1b 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -225,7 +225,13 @@ class WebSocket extends EventEmitter { } if (this.readyState === WebSocket.CLOSING) { - if (this._closeFrameSent && this._closeFrameReceived) this._socket.end(); + if ( + this._closeFrameSent && + (this._closeFrameReceived || this._receiver._writableState.errorEmitted) + ) { + this._socket.end(); + } + return; } @@ -238,7 +244,13 @@ class WebSocket extends EventEmitter { if (err) return; this._closeFrameSent = true; - if (this._closeFrameReceived) this._socket.end(); + + if ( + this._closeFrameReceived || + this._receiver._writableState.errorEmitted + ) { + this._socket.end(); + } }); // diff --git a/test/create-websocket-stream.test.js b/test/create-websocket-stream.test.js index bcd240974..b96ac9b1b 100644 --- a/test/create-websocket-stream.test.js +++ b/test/create-websocket-stream.test.js @@ -204,6 +204,8 @@ describe('createWebSocketStream', () => { it('reemits errors', (done) => { let duplexCloseEventEmitted = false; + let serverClientCloseEventEmitted = false; + const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); const duplex = createWebSocketStream(ws); @@ -218,6 +220,7 @@ describe('createWebSocketStream', () => { duplex.on('close', () => { duplexCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); }); }); }); @@ -225,10 +228,11 @@ describe('createWebSocketStream', () => { wss.on('connection', (ws) => { ws._socket.write(Buffer.from([0x85, 0x00])); ws.on('close', (code, reason) => { - assert.ok(duplexCloseEventEmitted); assert.strictEqual(code, 1002); assert.strictEqual(reason, ''); - wss.close(done); + + serverClientCloseEventEmitted = true; + if (duplexCloseEventEmitted) wss.close(done); }); }); }); diff --git a/test/websocket.test.js b/test/websocket.test.js index 03f984fcb..21126b824 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -430,6 +430,8 @@ describe('WebSocket', () => { describe('Events', () => { it("emits an 'error' event if an error occurs", (done) => { let clientCloseEventEmitted = false; + let serverClientCloseEventEmitted = false; + const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); @@ -442,19 +444,22 @@ describe('WebSocket', () => { ); ws.on('close', (code, reason) => { - clientCloseEventEmitted = true; assert.strictEqual(code, 1006); assert.strictEqual(reason, ''); + + clientCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); }); }); }); wss.on('connection', (ws) => { ws.on('close', (code, reason) => { - assert.ok(clientCloseEventEmitted); assert.strictEqual(code, 1002); assert.strictEqual(reason, ''); - wss.close(done); + + serverClientCloseEventEmitted = true; + if (clientCloseEventEmitted) wss.close(done); }); ws._socket.write(Buffer.from([0x85, 0x00])); @@ -1419,16 +1424,19 @@ describe('WebSocket', () => { }); it('honors the `mask` option', (done) => { + let clientCloseEventEmitted = false; let serverClientCloseEventEmitted = false; + const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); ws.on('open', () => ws.send('hi', { mask: false })); ws.on('close', (code, reason) => { - assert.ok(serverClientCloseEventEmitted); assert.strictEqual(code, 1002); assert.strictEqual(reason, ''); - wss.close(done); + + clientCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); }); }); @@ -1450,9 +1458,11 @@ describe('WebSocket', () => { ); ws.on('close', (code, reason) => { - serverClientCloseEventEmitted = true; assert.strictEqual(code, 1006); assert.strictEqual(reason, ''); + + serverClientCloseEventEmitted = true; + if (clientCloseEventEmitted) wss.close(done); }); }); }); @@ -2760,4 +2770,118 @@ describe('WebSocket', () => { }); }); }); + + describe('Connection close edge cases', () => { + it('closes cleanly after simultaneous errors (1/2)', (done) => { + let clientCloseEventEmitted = false; + let serverClientCloseEventEmitted = false; + + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, ''); + + clientCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); + }); + }); + + ws.on('open', () => { + // Write an invalid frame in both directions to trigger simultaneous + // failure. + const chunk = Buffer.from([0x85, 0x00]); + + wss.clients.values().next().value._socket.write(chunk); + ws._socket.write(chunk); + }); + }); + + wss.on('connection', (ws) => { + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, ''); + + serverClientCloseEventEmitted = true; + if (clientCloseEventEmitted) wss.close(done); + }); + }); + }); + }); + + it('closes cleanly after simultaneous errors (2/2)', (done) => { + let clientCloseEventEmitted = false; + let serverClientCloseEventEmitted = false; + + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, ''); + + clientCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); + }); + }); + + ws.on('open', () => { + // Write an invalid frame in both directions and change the + // `readyState` to `WebSocket.CLOSING`. + const chunk = Buffer.from([0x85, 0x00]); + const serverWs = wss.clients.values().next().value; + + serverWs._socket.write(chunk); + serverWs.close(); + + ws._socket.write(chunk); + ws.close(); + }); + }); + + wss.on('connection', (ws) => { + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, ''); + + serverClientCloseEventEmitted = true; + if (clientCloseEventEmitted) wss.close(done); + }); + }); + }); + }); + }); }); From 2916006477bd50d5a7513640fcb610f7fd0dddda Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 22 Jun 2021 14:43:34 +0200 Subject: [PATCH 055/328] [test] Add more tests for `WebSocket.prototype.close()` --- test/websocket.test.js | 81 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/test/websocket.test.js b/test/websocket.test.js index 21126b824..5642d968d 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2638,6 +2638,87 @@ describe('WebSocket', () => { }); }); + it('handles a close frame received while compressing data', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws._receiver.on('conclude', () => { + assert.ok(ws._sender._deflating); + }); + + ws.send('foo'); + ws.send('bar'); + ws.send('baz'); + ws.send('qux'); + }); + } + ); + + wss.on('connection', (ws) => { + const messages = []; + + ws.on('message', (message) => { + messages.push(message); + }); + + ws.on('close', (code, reason) => { + assert.deepStrictEqual(messages, ['foo', 'bar', 'baz', 'qux']); + assert.strictEqual(code, 1000); + assert.strictEqual(reason, ''); + wss.close(done); + }); + + ws.close(1000); + }); + }); + + describe('#close', () => { + it('can be used while data is being decompressed', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const messages = []; + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws._socket.on('end', () => { + assert.strictEqual(ws._receiver._state, 5); + }); + }); + + ws.on('message', (message) => { + if (messages.push(message) > 1) return; + + ws.close(1000); + }); + + ws.on('close', (code, reason) => { + assert.deepStrictEqual(messages, ['', '', '', '']); + assert.strictEqual(code, 1000); + assert.strictEqual(reason, ''); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + const buf = Buffer.from('c10100c10100c10100c10100', 'hex'); + ws._socket.write(buf); + }); + }); + }); + describe('#send', () => { it('ignores the `compress` option if the extension is disabled', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { From 38c6c734daf8e15d5cd902ed3e47b8651fd1032c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 29 Jun 2021 06:53:49 +0200 Subject: [PATCH 056/328] [dist] 7.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f38e4660..1fb2075f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.5.0", + "version": "7.5.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From aca94c86e000675900b09729559e405f9207d154 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 3 Jul 2021 09:21:39 +0200 Subject: [PATCH 057/328] [fix] Abort the handshake if an unexpected extension is received Abort the handshake if the client receives a `Sec-WebSocket-Extensions` header but no extension was requested. Also abort the handshake if the server indicates an extension not requested by the client. --- lib/websocket.js | 52 +++++++++++++---- test/websocket.test.js | 126 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 13 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 280364a1b..50d85576a 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -660,22 +660,50 @@ function initAsClient(websocket, address, protocols, options) { if (serverProt) websocket._protocol = serverProt; - if (perMessageDeflate) { + const secWebSocketExtensions = res.headers['sec-websocket-extensions']; + + if (secWebSocketExtensions !== undefined) { + if (!perMessageDeflate) { + const message = + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested'; + abortHandshake(websocket, socket, message); + return; + } + + let extensions; + try { - const extensions = parse(res.headers['sec-websocket-extensions']); + extensions = parse(secWebSocketExtensions); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } + + const extensionNames = Object.keys(extensions); + + if (extensionNames.length) { + if ( + extensionNames.length !== 1 || + extensionNames[0] !== PerMessageDeflate.extensionName + ) { + const message = + 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } - if (extensions[PerMessageDeflate.extensionName]) { + try { perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); - websocket._extensions[PerMessageDeflate.extensionName] = - perMessageDeflate; + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; } - } catch (err) { - abortHandshake( - websocket, - socket, - 'Invalid Sec-WebSocket-Extensions header' - ); - return; + + websocket._extensions[PerMessageDeflate.extensionName] = + perMessageDeflate; } } diff --git a/test/websocket.test.js b/test/websocket.test.js index 5642d968d..a1a1cda43 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -663,7 +663,40 @@ describe('WebSocket', () => { }); }); - it('fails if the Sec-WebSocket-Extensions response header is invalid', (done) => { + it('fails if an unexpected Sec-WebSocket-Extensions header is received', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: foo\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + perMessageDeflate: false + }); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested' + ); + ws.on('close', () => done()); + }); + }); + + it('fails if the Sec-WebSocket-Extensions header is invalid (1/2)', (done) => { server.once('upgrade', (req, socket) => { const key = crypto .createHash('sha1') @@ -693,6 +726,97 @@ describe('WebSocket', () => { }); }); + it('fails if the Sec-WebSocket-Extensions header is invalid (2/2)', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: ' + + 'permessage-deflate; client_max_window_bits=7\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Invalid Sec-WebSocket-Extensions header' + ); + ws.on('close', () => done()); + }); + }); + + it('fails if an unexpected extension is received (1/2)', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: foo\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Server indicated an extension that was not requested' + ); + ws.on('close', () => done()); + }); + }); + + it('fails if an unexpected extension is received (2/2)', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: permessage-deflate,foo\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Server indicated an extension that was not requested' + ); + ws.on('close', () => done()); + }); + }); + it('fails if server sends a subprotocol when none was requested', (done) => { const wss = new WebSocket.Server({ server }); From 0ad1f9d6a48ed1b30bda09b958cb142c1e09cced Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 4 Jul 2021 07:22:35 +0200 Subject: [PATCH 058/328] [dist] 7.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1fb2075f3..85cea0fab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.5.1", + "version": "7.5.2", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From ecb9d9ea8f126416f2c07a2a8485b1d1e4ab3989 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 7 Jul 2021 13:07:17 +0200 Subject: [PATCH 059/328] [minor] Improve JSDoc-inferred types (#1912) Refs: https://github.com/websockets/ws/pull/1910 --- lib/receiver.js | 4 +- lib/sender.js | 6 +- lib/stream.js | 4 +- lib/websocket-server.js | 26 +++++--- lib/websocket.js | 141 +++++++++++++++++++++++++++++++++------- 5 files changed, 145 insertions(+), 36 deletions(-) diff --git a/lib/receiver.js b/lib/receiver.js index 7ed992214..1d2af76e1 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -22,7 +22,7 @@ const INFLATING = 5; /** * HyBi Receiver implementation. * - * @extends stream.Writable + * @extends Writable */ class Receiver extends Writable { /** @@ -586,7 +586,7 @@ module.exports = Receiver; /** * Builds an error object. * - * @param {(Error|RangeError)} ErrorCtor The error constructor + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor * @param {String} message The error message * @param {Boolean} prefix Specifies whether or not to add a default prefix to * `message` diff --git a/lib/sender.js b/lib/sender.js index ad71e1950..441171c57 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -1,5 +1,9 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls$" }] */ + 'use strict'; +const net = require('net'); +const tls = require('tls'); const { randomFillSync } = require('crypto'); const PerMessageDeflate = require('./permessage-deflate'); @@ -16,7 +20,7 @@ class Sender { /** * Creates a Sender instance. * - * @param {net.Socket} socket The connection socket + * @param {(net.Socket|tls.Socket)} socket The connection socket * @param {Object} [extensions] An object containing the negotiated extensions */ constructor(socket, extensions) { diff --git a/lib/stream.js b/lib/stream.js index c3a793691..b0896ff83 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -5,7 +5,7 @@ const { Duplex } = require('stream'); /** * Emits the `'close'` event on a stream. * - * @param {stream.Duplex} The stream. + * @param {Duplex} stream The stream. * @private */ function emitClose(stream) { @@ -43,7 +43,7 @@ function duplexOnError(err) { * * @param {WebSocket} ws The `WebSocket` to wrap * @param {Object} [options] The options for the `Duplex` constructor - * @return {stream.Duplex} The duplex stream + * @return {Duplex} The duplex stream * @public */ function createWebSocketStream(ws, options) { diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 3c3bbe0b0..1610e767a 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -1,8 +1,13 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */ + 'use strict'; const EventEmitter = require('events'); +const http = require('http'); +const https = require('https'); +const net = require('net'); +const tls = require('tls'); const { createHash } = require('crypto'); -const { createServer, STATUS_CODES } = require('http'); const PerMessageDeflate = require('./permessage-deflate'); const WebSocket = require('./websocket'); @@ -34,7 +39,8 @@ class WebSocketServer extends EventEmitter { * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable * permessage-deflate * @param {Number} [options.port] The port where to bind the server - * @param {http.Server} [options.server] A pre-created HTTP/S server to use + * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S + * server to use * @param {Function} [options.verifyClient] A hook to reject connections * @param {Function} [callback] A listener for the `listening` event */ @@ -63,8 +69,8 @@ class WebSocketServer extends EventEmitter { } if (options.port != null) { - this._server = createServer((req, res) => { - const body = STATUS_CODES[426]; + this._server = http.createServer((req, res) => { + const body = http.STATUS_CODES[426]; res.writeHead(426, { 'Content-Length': body.length, @@ -173,7 +179,8 @@ class WebSocketServer extends EventEmitter { * Handle a HTTP Upgrade request. * * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @public @@ -252,7 +259,8 @@ class WebSocketServer extends EventEmitter { * @param {String} key The value of the `Sec-WebSocket-Key` header * @param {Object} extensions The accepted extensions * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @throws {Error} If called more than once with the same socket @@ -375,7 +383,7 @@ function socketOnError() { /** * Close the connection when preconditions are not fulfilled. * - * @param {net.Socket} socket The socket of the upgrade request + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} [message] The HTTP response body * @param {Object} [headers] Additional HTTP response headers @@ -383,7 +391,7 @@ function socketOnError() { */ function abortHandshake(socket, code, message, headers) { if (socket.writable) { - message = message || STATUS_CODES[code]; + message = message || http.STATUS_CODES[code]; headers = { Connection: 'close', 'Content-Type': 'text/html', @@ -392,7 +400,7 @@ function abortHandshake(socket, code, message, headers) { }; socket.write( - `HTTP/1.1 ${code} ${STATUS_CODES[code]}\r\n` + + `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + Object.keys(headers) .map((h) => `${h}: ${headers[h]}`) .join('\r\n') + diff --git a/lib/websocket.js b/lib/websocket.js index 50d85576a..baf493458 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -36,7 +36,7 @@ class WebSocket extends EventEmitter { /** * Create a new `WebSocket`. * - * @param {(String|url.URL)} address The URL to which to connect + * @param {(String|URL)} address The URL to which to connect * @param {(String|String[])} [protocols] The subprotocols * @param {Object} [options] Connection options */ @@ -112,6 +112,50 @@ class WebSocket extends EventEmitter { return Object.keys(this._extensions).join(); } + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onclose() { + return undefined; + } + + /* istanbul ignore next */ + set onclose(listener) {} + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onerror() { + return null; + } + + /* istanbul ignore next */ + set onerror(listener) {} + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onopen() { + return undefined; + } + + /* istanbul ignore next */ + set onopen(listener) {} + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onmessage() { + return undefined; + } + + /* istanbul ignore next */ + set onmessage(listener) {} + /** * @type {String} */ @@ -136,7 +180,8 @@ class WebSocket extends EventEmitter { /** * Set up the socket and the internal resources. * - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Number} [maxPayload=0] The maximum allowed message size * @private @@ -392,11 +437,76 @@ class WebSocket extends EventEmitter { } } -readyStates.forEach((readyState, i) => { - const descriptor = { enumerable: true, value: i }; +/** + * @constant {Number} CONNECTING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); - Object.defineProperty(WebSocket.prototype, readyState, descriptor); - Object.defineProperty(WebSocket, readyState, descriptor); +/** + * @constant {Number} CONNECTING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') }); [ @@ -416,14 +526,7 @@ readyStates.forEach((readyState, i) => { // ['open', 'error', 'close', 'message'].forEach((method) => { Object.defineProperty(WebSocket.prototype, `on${method}`, { - configurable: true, enumerable: true, - /** - * Return the listener of the event. - * - * @return {(Function|undefined)} The event listener or `undefined` - * @public - */ get() { const listeners = this.listeners(method); for (let i = 0; i < listeners.length; i++) { @@ -432,12 +535,6 @@ readyStates.forEach((readyState, i) => { return undefined; }, - /** - * Add a listener for the event. - * - * @param {Function} listener The listener to add - * @public - */ set(listener) { const listeners = this.listeners(method); for (let i = 0; i < listeners.length; i++) { @@ -460,7 +557,7 @@ module.exports = WebSocket; * Initialize a WebSocket client. * * @param {WebSocket} websocket The client to initialize - * @param {(String|url.URL)} address The URL to which to connect + * @param {(String|URL)} address The URL to which to connect * @param {String} [protocols] The subprotocols * @param {Object} [options] Connection options * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable @@ -744,8 +841,8 @@ function tlsConnect(options) { * Abort the handshake and emit an error. * * @param {WebSocket} websocket The WebSocket instance - * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the - * socket to destroy + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to + * abort or the socket to destroy * @param {String} message The error message * @private */ From 66e58d279ffabe5108424c08ab71403aceddcad9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 8 Jul 2021 13:55:29 +0200 Subject: [PATCH 060/328] [fix] Make the `{noS,s}erver`, and `port` options mutually exclusive Remove ambiguity and prevent `WebSocketServer.prototype.address()` from throwing an error if the `noServer` option is used along with the `port` and/or `server` options. --- doc/ws.md | 4 ++-- lib/websocket-server.js | 9 +++++++-- test/websocket-server.test.js | 38 ++++++++++++++++++++++++++++++++--- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index af0dfb325..a3b1bff81 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -80,8 +80,8 @@ This class represents a WebSocket server. It extends the `EventEmitter`. - `maxPayload` {Number} The maximum allowed message size in bytes. - `callback` {Function} -Create a new server instance. One of `port`, `server` or `noServer` must be -provided or an error is thrown. An HTTP server is automatically created, +Create a new server instance. One and only one of `port`, `server` or `noServer` +must be provided or an error is thrown. An HTTP server is automatically created, started, and used if `port` is set. To use an external HTTP/S server instead, specify only `server` or `noServer`. In this case the HTTP/S server must be started manually. The "noServer" mode allows the WebSocket server to be diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 1610e767a..20276edb3 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -62,9 +62,14 @@ class WebSocketServer extends EventEmitter { ...options }; - if (options.port == null && !options.server && !options.noServer) { + if ( + (options.port == null && !options.server && !options.noServer) || + (options.port != null && (options.server || options.noServer)) || + (options.server && options.noServer) + ) { throw new TypeError( - 'One of the "port", "server", or "noServer" options must be specified' + 'One and only one of the "port", "server", or "noServer" options ' + + 'must be specified' ); } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index ce6617ec6..9b43284a1 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -18,12 +18,44 @@ const { NOOP } = require('../lib/constants'); describe('WebSocketServer', () => { describe('#ctor', () => { it('throws an error if no option object is passed', () => { - assert.throws(() => new WebSocket.Server()); + assert.throws( + () => new WebSocket.Server(), + new RegExp( + '^TypeError: One and only one of the "port", "server", or ' + + '"noServer" options must be specified$' + ) + ); }); describe('options', () => { - it('throws an error if no `port` or `server` option is specified', () => { - assert.throws(() => new WebSocket.Server({})); + it('throws an error if required options are not specified', () => { + assert.throws( + () => new WebSocket.Server({}), + new RegExp( + '^TypeError: One and only one of the "port", "server", or ' + + '"noServer" options must be specified$' + ) + ); + }); + + it('throws an error if mutually exclusive options are specified', () => { + const server = http.createServer(); + const variants = [ + { port: 0, noServer: true, server }, + { port: 0, noServer: true }, + { port: 0, server }, + { noServer: true, server } + ]; + + for (const options of variants) { + assert.throws( + () => new WebSocket.Server(options), + new RegExp( + '^TypeError: One and only one of the "port", "server", or ' + + '"noServer" options must be specified$' + ) + ); + } }); it('exposes options passed to constructor', (done) => { From ea63b29e81f95f7c5d38079487952b2eae94391e Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 8 Jul 2021 19:21:44 +0200 Subject: [PATCH 061/328] [minor] Fix typo --- lib/websocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/websocket.js b/lib/websocket.js index baf493458..7f1e3bcfa 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -128,7 +128,7 @@ class WebSocket extends EventEmitter { */ /* istanbul ignore next */ get onerror() { - return null; + return undefined; } /* istanbul ignore next */ From 5a5873048005cf5d25a2186fb9dc6db2a85096b0 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 9 Jul 2021 11:06:26 +0200 Subject: [PATCH 062/328] [fix] Emit the `'close'` event after the server is closed Ensure that `WebSocketServer.prototype.close()` does not emit a `'close'` event prematurely if called while the internal HTTP/S server is closing. --- lib/websocket-server.js | 16 ++++++++++++- test/websocket-server.test.js | 42 +++++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 20276edb3..e9ec2cbd7 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -16,6 +16,10 @@ const { GUID, kWebSocket } = require('./constants'); const keyRegex = /^[+/0-9A-Za-z]{22}==$/; +const RUNNING = 0; +const CLOSING = 1; +const CLOSED = 2; + /** * Class representing a WebSocket server. * @@ -108,6 +112,7 @@ class WebSocketServer extends EventEmitter { if (options.perMessageDeflate === true) options.perMessageDeflate = {}; if (options.clientTracking) this.clients = new Set(); this.options = options; + this._state = RUNNING; } /** @@ -137,6 +142,14 @@ class WebSocketServer extends EventEmitter { close(cb) { if (cb) this.once('close', cb); + if (this._state === CLOSED) { + process.nextTick(emitClose, this); + return; + } + + if (this._state === CLOSING) return; + this._state = CLOSING; + // // Terminate all associated clients. // @@ -154,7 +167,7 @@ class WebSocketServer extends EventEmitter { // Close the http server if it was internally created. // if (this.options.port != null) { - server.close(() => this.emit('close')); + server.close(emitClose.bind(undefined, this)); return; } } @@ -373,6 +386,7 @@ function addListeners(server, map) { * @private */ function emitClose(server) { + server._state = CLOSED; server.emit('close'); } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 9b43284a1..310c87c2e 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -278,11 +278,45 @@ describe('WebSocketServer', () => { }); }); - it("emits the 'close' event", (done) => { - const wss = new WebSocket.Server({ noServer: true }); + it("emits the 'close' event after the server closes", (done) => { + let serverCloseEventEmitted = false; + + const wss = new WebSocket.Server({ port: 0 }, () => { + net.createConnection({ port: wss.address().port }); + }); + + wss._server.on('connection', (socket) => { + wss.close(); + + // + // The server is closing. Ensure this does not emit a `'close'` + // event before the server is actually closed. + // + wss.close(); + + process.nextTick(() => { + socket.end(); + }); + }); - wss.on('close', done); - wss.close(); + wss._server.on('close', () => { + serverCloseEventEmitted = true; + }); + + wss.on('close', () => { + assert.ok(serverCloseEventEmitted); + done(); + }); + }); + + it("emits the 'close' event if the server is already closed", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + wss.close(() => { + assert.strictEqual(wss._state, 2); + wss.on('close', done); + wss.close(); + }); + }); }); }); From 772236a13ff2bd28291c911b7c25fbfe99580ed1 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 9 Jul 2021 12:34:22 +0200 Subject: [PATCH 063/328] [fix] Abort the handshake if the server is closing or closed Prevent WebSocket connections from being established after `WebSocketServer.prototype.close()` is called. --- lib/websocket-server.js | 2 ++ test/websocket-server.test.js | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index e9ec2cbd7..fe7fdf501 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -297,6 +297,8 @@ class WebSocketServer extends EventEmitter { ); } + if (this._state > RUNNING) return abortHandshake(socket, 503); + const digest = createHash('sha1') .update(key + GUID) .digest('base64'); diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 310c87c2e..90ceb5646 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -613,6 +613,28 @@ describe('WebSocketServer', () => { }); }); + it('fails if the WebSocket server is closing or closed', (done) => { + const server = http.createServer(); + const wss = new WebSocket.Server({ noServer: true }); + + server.on('upgrade', (req, socket, head) => { + wss.close(); + wss.handleUpgrade(req, socket, head, () => { + done(new Error('Unexpected callback invocation')); + }); + }); + + server.listen(0, () => { + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('unexpected-response', (req, res) => { + assert.strictEqual(res.statusCode, 503); + res.resume(); + server.close(done); + }); + }); + }); + it('handles unsupported extensions', (done) => { const wss = new WebSocket.Server( { From 4c1849a61e773fe0ce016f6eb59bc3877f09aeee Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 10 Jul 2021 07:26:57 +0200 Subject: [PATCH 064/328] [dist] 7.5.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 85cea0fab..d6dff1396 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.5.2", + "version": "7.5.3", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 0aecf0c95b41eaeeec9e265d5f1cb6c96e2f4d08 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 18 Jun 2021 21:21:01 +0200 Subject: [PATCH 065/328] [major] Validate subprotocol names Make the `WebSocket` constructor throw a `SyntaxError` if any of the subprotocol names are invalid or duplicated. --- lib/websocket.js | 49 ++++++++++++++++++++++++++++++------------ test/websocket.test.js | 22 +++++++++++++++---- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 7f1e3bcfa..37949de15 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -24,6 +24,7 @@ const { format, parse } = require('./extension'); const { toBuffer } = require('./buffer-util'); const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; +const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/; const protocolVersions = [8, 13]; const closeTimeout = 30 * 1000; @@ -61,11 +62,15 @@ class WebSocket extends EventEmitter { this._isServer = false; this._redirects = 0; - if (Array.isArray(protocols)) { - protocols = protocols.join(', '); - } else if (typeof protocols === 'object' && protocols !== null) { - options = protocols; - protocols = undefined; + if (protocols === undefined) { + protocols = []; + } else if (!Array.isArray(protocols)) { + if (typeof protocols === 'object' && protocols !== null) { + options = protocols; + protocols = []; + } else { + protocols = [protocols]; + } } initAsClient(this, address, protocols, options); @@ -558,7 +563,7 @@ module.exports = WebSocket; * * @param {WebSocket} websocket The client to initialize * @param {(String|URL)} address The URL to which to connect - * @param {String} [protocols] The subprotocols + * @param {Array} protocols The subprotocols * @param {Object} [options] Connection options * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable * permessage-deflate @@ -623,6 +628,7 @@ function initAsClient(websocket, address, protocols, options) { const defaultPort = isSecure ? 443 : 80; const key = randomBytes(16).toString('base64'); const get = isSecure ? https.get : http.get; + const protocolSet = new Set(); let perMessageDeflate; opts.createConnection = isSecure ? tlsConnect : netConnect; @@ -651,8 +657,22 @@ function initAsClient(websocket, address, protocols, options) { [PerMessageDeflate.extensionName]: perMessageDeflate.offer() }); } - if (protocols) { - opts.headers['Sec-WebSocket-Protocol'] = protocols; + if (protocols.length) { + for (const protocol of protocols) { + if ( + typeof protocol !== 'string' || + !subprotocolRegex.test(protocol) || + protocolSet.has(protocol) + ) { + throw new SyntaxError( + 'An invalid or duplicated subprotocol was specified' + ); + } + + protocolSet.add(protocol); + } + + opts.headers['Sec-WebSocket-Protocol'] = protocols.join(','); } if (opts.origin) { if (opts.protocolVersion < 13) { @@ -739,15 +759,16 @@ function initAsClient(websocket, address, protocols, options) { } const serverProt = res.headers['sec-websocket-protocol']; - const protList = (protocols || '').split(/, */); let protError; - if (!protocols && serverProt) { - protError = 'Server sent a subprotocol but none was requested'; - } else if (protocols && !serverProt) { + if (serverProt) { + if (!protocolSet.size) { + protError = 'Server sent a subprotocol but none was requested'; + } else if (!protocolSet.has(serverProt)) { + protError = 'Server sent an invalid subprotocol'; + } + } else if (protocolSet.size) { protError = 'Server sent no subprotocol'; - } else if (serverProt && !protList.includes(serverProt)) { - protError = 'Server sent an invalid subprotocol'; } if (protError) { diff --git a/test/websocket.test.js b/test/websocket.test.js index a1a1cda43..3301902f3 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -26,6 +26,15 @@ describe('WebSocket', () => { ); }); + it('throws an error if a subprotocol is invalid or duplicated', () => { + for (const subprotocol of [null, '', 'a,b', ['a', 'a']]) { + assert.throws( + () => new WebSocket('ws://localhost', subprotocol), + /^SyntaxError: An invalid or duplicated subprotocol was specified$/ + ); + } + }); + it('accepts `url.URL` objects as url', (done) => { const agent = new CustomAgent(); @@ -44,13 +53,18 @@ describe('WebSocket', () => { let count = 0; let ws; - agent.addRequest = () => count++; + agent.addRequest = (req) => { + assert.strictEqual( + req.getHeader('sec-websocket-protocol'), + undefined + ); + count++; + }; ws = new WebSocket('ws://localhost', undefined, { agent }); - ws = new WebSocket('ws://localhost', null, { agent }); ws = new WebSocket('ws://localhost', [], { agent }); - assert.strictEqual(count, 3); + assert.strictEqual(count, 2); }); it('accepts the `maxPayload` option', (done) => { @@ -651,7 +665,7 @@ describe('WebSocket', () => { server.once('upgrade', (req, socket) => socket.on('end', socket.end)); const port = server.address().port; - const ws = new WebSocket(`ws://localhost:${port}`, null, { + const ws = new WebSocket(`ws://localhost:${port}`, { handshakeTimeout: 100 }); From 1877ddeb9f79a6204a2fc9c85c85aec470657037 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 20 Jun 2021 11:50:02 +0200 Subject: [PATCH 066/328] [major] Validate the Sec-WebSocket-Protocol header Abort the handshake if the `Sec-WebSocket-Protocol` header is invalid. --- doc/ws.md | 2 +- lib/extension.js | 23 +-------- lib/subprotocol.js | 62 ++++++++++++++++++++++++ lib/validation.js | 29 ++++++++++- lib/websocket-server.js | 62 +++++++++++++----------- test/subprotocol.test.js | 91 +++++++++++++++++++++++++++++++++++ test/websocket-server.test.js | 26 +++++++++- 7 files changed, 240 insertions(+), 55 deletions(-) create mode 100644 lib/subprotocol.js create mode 100644 test/subprotocol.test.js diff --git a/doc/ws.md b/doc/ws.md index a3b1bff81..668d0d7c4 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -120,7 +120,7 @@ if `verifyClient` is provided with two arguments then those are: `handleProtocols` takes two arguments: -- `protocols` {Array} The list of WebSocket subprotocols indicated by the client +- `protocols` {Set} The list of WebSocket subprotocols indicated by the client in the `Sec-WebSocket-Protocol` header. - `request` {http.IncomingMessage} The client HTTP GET request. diff --git a/lib/extension.js b/lib/extension.js index 87a421329..0d833fee6 100644 --- a/lib/extension.js +++ b/lib/extension.js @@ -1,27 +1,6 @@ 'use strict'; -// -// Allowed token characters: -// -// '!', '#', '$', '%', '&', ''', '*', '+', '-', -// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' -// -// tokenChars[32] === 0 // ' ' -// tokenChars[33] === 1 // '!' -// tokenChars[34] === 0 // '"' -// ... -// -// prettier-ignore -const tokenChars = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 - 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 -]; +const { tokenChars } = require('./validation'); /** * Adds an offer to the map of extension offers or a parameter to the map of diff --git a/lib/subprotocol.js b/lib/subprotocol.js new file mode 100644 index 000000000..d4381e886 --- /dev/null +++ b/lib/subprotocol.js @@ -0,0 +1,62 @@ +'use strict'; + +const { tokenChars } = require('./validation'); + +/** + * Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names. + * + * @param {String} header The field value of the header + * @return {Set} The subprotocol names + * @public + */ +function parse(header) { + const protocols = new Set(); + let start = -1; + let end = -1; + let i = 0; + + for (i; i < header.length; i++) { + const code = header.charCodeAt(i); + + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x2c /* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + + const protocol = header.slice(start, end); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + + if (start === -1 || end !== -1) { + throw new SyntaxError('Unexpected end of input'); + } + + const protocol = header.slice(start, i); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + return protocols; +} + +module.exports = { parse }; diff --git a/lib/validation.js b/lib/validation.js index 169ac6f06..d09b1d39b 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -1,5 +1,28 @@ 'use strict'; +// +// Allowed token characters: +// +// '!', '#', '$', '%', '&', ''', '*', '+', '-', +// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' +// +// tokenChars[32] === 0 // ' ' +// tokenChars[33] === 1 // '!' +// tokenChars[34] === 0 // '"' +// ... +// +// prettier-ignore +const tokenChars = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 +]; + /** * Checks if a status code is allowed in a close frame. * @@ -94,11 +117,13 @@ try { isValidStatusCode, isValidUTF8(buf) { return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); - } + }, + tokenChars }; } catch (e) /* istanbul ignore next */ { module.exports = { isValidStatusCode, - isValidUTF8: _isValidUTF8 + isValidUTF8: _isValidUTF8, + tokenChars }; } diff --git a/lib/websocket-server.js b/lib/websocket-server.js index fe7fdf501..cb93aeeb3 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -9,9 +9,10 @@ const net = require('net'); const tls = require('tls'); const { createHash } = require('crypto'); +const extension = require('./extension'); const PerMessageDeflate = require('./permessage-deflate'); +const subprotocol = require('./subprotocol'); const WebSocket = require('./websocket'); -const { format, parse } = require('./extension'); const { GUID, kWebSocket } = require('./constants'); const keyRegex = /^[+/0-9A-Za-z]{22}==$/; @@ -208,7 +209,7 @@ class WebSocketServer extends EventEmitter { const key = req.headers['sec-websocket-key'] !== undefined - ? req.headers['sec-websocket-key'].trim() + ? req.headers['sec-websocket-key'] : false; const version = +req.headers['sec-websocket-version']; const extensions = {}; @@ -224,6 +225,17 @@ class WebSocketServer extends EventEmitter { return abortHandshake(socket, 400); } + const secWebSocketProtocol = req.headers['sec-websocket-protocol']; + let protocols = new Set(); + + if (secWebSocketProtocol !== undefined) { + try { + protocols = subprotocol.parse(secWebSocketProtocol); + } catch (err) { + return abortHandshake(socket, 400); + } + } + if (this.options.perMessageDeflate) { const perMessageDeflate = new PerMessageDeflate( this.options.perMessageDeflate, @@ -232,7 +244,7 @@ class WebSocketServer extends EventEmitter { ); try { - const offers = parse(req.headers['sec-websocket-extensions']); + const offers = extension.parse(req.headers['sec-websocket-extensions']); if (offers[PerMessageDeflate.extensionName]) { perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); @@ -260,7 +272,15 @@ class WebSocketServer extends EventEmitter { return abortHandshake(socket, code || 401, message, headers); } - this.completeUpgrade(key, extensions, req, socket, head, cb); + this.completeUpgrade( + extensions, + key, + protocols, + req, + socket, + head, + cb + ); }); return; } @@ -268,14 +288,15 @@ class WebSocketServer extends EventEmitter { if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); } - this.completeUpgrade(key, extensions, req, socket, head, cb); + this.completeUpgrade(extensions, key, protocols, req, socket, head, cb); } /** * Upgrade the connection to WebSocket. * - * @param {String} key The value of the `Sec-WebSocket-Key` header * @param {Object} extensions The accepted extensions + * @param {String} key The value of the `Sec-WebSocket-Key` header + * @param {Set} protocols The subprotocols * @param {http.IncomingMessage} req The request object * @param {(net.Socket|tls.Socket)} socket The network socket between the * server and client @@ -284,7 +305,7 @@ class WebSocketServer extends EventEmitter { * @throws {Error} If called more than once with the same socket * @private */ - completeUpgrade(key, extensions, req, socket, head, cb) { + completeUpgrade(extensions, key, protocols, req, socket, head, cb) { // // Destroy the socket if the client has already sent a FIN packet. // @@ -311,19 +332,14 @@ class WebSocketServer extends EventEmitter { ]; const ws = new WebSocket(null); - let protocol = req.headers['sec-websocket-protocol']; - - if (protocol) { - protocol = protocol.split(',').map(trim); + if (protocols.size) { // // Optionally call external protocol selection handler. // - if (this.options.handleProtocols) { - protocol = this.options.handleProtocols(protocol, req); - } else { - protocol = protocol[0]; - } + const protocol = this.options.handleProtocols + ? this.options.handleProtocols(protocols, req) + : protocols.values().next().value; if (protocol) { headers.push(`Sec-WebSocket-Protocol: ${protocol}`); @@ -333,7 +349,7 @@ class WebSocketServer extends EventEmitter { if (extensions[PerMessageDeflate.extensionName]) { const params = extensions[PerMessageDeflate.extensionName].params; - const value = format({ + const value = extension.format({ [PerMessageDeflate.extensionName]: [params] }); headers.push(`Sec-WebSocket-Extensions: ${value}`); @@ -433,15 +449,3 @@ function abortHandshake(socket, code, message, headers) { socket.removeListener('error', socketOnError); socket.destroy(); } - -/** - * Remove whitespace characters from both ends of a string. - * - * @param {String} str The string - * @return {String} A new string representing `str` stripped of whitespace - * characters from both its beginning and end - * @private - */ -function trim(str) { - return str.trim(); -} diff --git a/test/subprotocol.test.js b/test/subprotocol.test.js new file mode 100644 index 000000000..91dd5d69d --- /dev/null +++ b/test/subprotocol.test.js @@ -0,0 +1,91 @@ +'use strict'; + +const assert = require('assert'); + +const { parse } = require('../lib/subprotocol'); + +describe('subprotocol', () => { + describe('parse', () => { + it('parses a single subprotocol', () => { + assert.deepStrictEqual(parse('foo'), new Set(['foo'])); + }); + + it('parses multiple subprotocols', () => { + assert.deepStrictEqual( + parse('foo,bar,baz'), + new Set(['foo', 'bar', 'baz']) + ); + }); + + it('ignores the optional white spaces', () => { + const header = 'foo , bar\t, \tbaz\t , qux\t\t,norf'; + + assert.deepStrictEqual( + parse(header), + new Set(['foo', 'bar', 'baz', 'qux', 'norf']) + ); + }); + + it('throws an error if a subprotocol is empty', () => { + [ + [',', 0], + ['foo,,', 4], + ['foo, ,', 6] + ].forEach((element) => { + assert.throws( + () => parse(element[0]), + new RegExp( + `^SyntaxError: Unexpected character at index ${element[1]}$` + ) + ); + }); + }); + + it('throws an error if a subprotocol is duplicated', () => { + ['foo,foo,bar', 'foo,bar,foo'].forEach((header) => { + assert.throws( + () => parse(header), + /^SyntaxError: The "foo" subprotocol is duplicated$/ + ); + }); + }); + + it('throws an error if a white space is misplaced', () => { + [ + ['f oo', 2], + [' foo', 0] + ].forEach((element) => { + assert.throws( + () => parse(element[0]), + new RegExp( + `^SyntaxError: Unexpected character at index ${element[1]}$` + ) + ); + }); + }); + + it('throws an error if a subprotocol contains invalid characters', () => { + [ + ['f@o', 1], + ['f\\oo', 1], + ['foo,b@r', 5] + ].forEach((element) => { + assert.throws( + () => parse(element[0]), + new RegExp( + `^SyntaxError: Unexpected character at index ${element[1]}$` + ) + ); + }); + }); + + it('throws an error if the header value ends prematurely', () => { + ['foo ', 'foo, ', 'foo,bar ', 'foo,bar,'].forEach((header) => { + assert.throws( + () => parse(header), + /^SyntaxError: Unexpected end of input$/ + ); + }); + }); + }); +}); diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 90ceb5646..e0ab2114e 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -582,6 +582,30 @@ describe('WebSocketServer', () => { }); }); + it('fails is the Sec-WebSocket-Protocol header is invalid', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Sec-WebSocket-Protocol': 'foo;bar' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + wss.close(done); + }); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + it('fails if the Sec-WebSocket-Extensions header is invalid', (done) => { const wss = new WebSocket.Server( { @@ -920,7 +944,7 @@ describe('WebSocketServer', () => { const handleProtocols = (protocols, request) => { assert.ok(request instanceof http.IncomingMessage); assert.strictEqual(request.url, '/'); - return protocols.pop(); + return Array.from(protocols).pop(); }; const wss = new WebSocket.Server({ handleProtocols, port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`, [ From e814110ee6cd269a8982a529e38b55632c37ad28 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 22 Jun 2021 12:29:55 +0200 Subject: [PATCH 067/328] [major] Make the Sec-WebSocket-Extensions header parser stricter Make the parser throw an error if the header field value is empty or if it begins or ends with a white space. --- lib/extension.js | 13 +++++++------ lib/websocket-server.js | 11 ++++++++--- lib/websocket.js | 37 +++++++++++++++++-------------------- test/extension.test.js | 18 ++++++++++-------- 4 files changed, 42 insertions(+), 37 deletions(-) diff --git a/lib/extension.js b/lib/extension.js index 0d833fee6..3d7895c1b 100644 --- a/lib/extension.js +++ b/lib/extension.js @@ -26,9 +26,6 @@ function push(dest, name, elem) { */ function parse(header) { const offers = Object.create(null); - - if (header === undefined || header === '') return offers; - let params = Object.create(null); let mustUnescape = false; let isEscaping = false; @@ -36,16 +33,20 @@ function parse(header) { let extensionName; let paramName; let start = -1; + let code = -1; let end = -1; let i = 0; for (; i < header.length; i++) { - const code = header.charCodeAt(i); + code = header.charCodeAt(i); if (extensionName === undefined) { if (end === -1 && tokenChars[code] === 1) { if (start === -1) start = i; - } else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) { + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { if (end === -1 && start !== -1) end = i; } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { if (start === -1) { @@ -146,7 +147,7 @@ function parse(header) { } } - if (start === -1 || inQuotes) { + if (start === -1 || inQuotes || code === 0x20 || code === 0x09) { throw new SyntaxError('Unexpected end of input'); } diff --git a/lib/websocket-server.js b/lib/websocket-server.js index cb93aeeb3..e2f1af616 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -212,7 +212,6 @@ class WebSocketServer extends EventEmitter { ? req.headers['sec-websocket-key'] : false; const version = +req.headers['sec-websocket-version']; - const extensions = {}; if ( req.method !== 'GET' || @@ -236,7 +235,13 @@ class WebSocketServer extends EventEmitter { } } - if (this.options.perMessageDeflate) { + const secWebSocketExtensions = req.headers['sec-websocket-extensions']; + const extensions = {}; + + if ( + this.options.perMessageDeflate && + secWebSocketExtensions !== undefined + ) { const perMessageDeflate = new PerMessageDeflate( this.options.perMessageDeflate, true, @@ -244,7 +249,7 @@ class WebSocketServer extends EventEmitter { ); try { - const offers = extension.parse(req.headers['sec-websocket-extensions']); + const offers = extension.parse(secWebSocketExtensions); if (offers[PerMessageDeflate.extensionName]) { perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); diff --git a/lib/websocket.js b/lib/websocket.js index 37949de15..400a610d1 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -801,28 +801,25 @@ function initAsClient(websocket, address, protocols, options) { const extensionNames = Object.keys(extensions); - if (extensionNames.length) { - if ( - extensionNames.length !== 1 || - extensionNames[0] !== PerMessageDeflate.extensionName - ) { - const message = - 'Server indicated an extension that was not requested'; - abortHandshake(websocket, socket, message); - return; - } - - try { - perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); - } catch (err) { - const message = 'Invalid Sec-WebSocket-Extensions header'; - abortHandshake(websocket, socket, message); - return; - } + if ( + extensionNames.length !== 1 || + extensionNames[0] !== PerMessageDeflate.extensionName + ) { + const message = 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } - websocket._extensions[PerMessageDeflate.extensionName] = - perMessageDeflate; + try { + perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; } + + websocket._extensions[PerMessageDeflate.extensionName] = + perMessageDeflate; } websocket.setSocket(socket, head, opts.maxPayload); diff --git a/test/extension.test.js b/test/extension.test.js index 6cfbc1b23..a4b3e749d 100644 --- a/test/extension.test.js +++ b/test/extension.test.js @@ -6,11 +6,6 @@ const { format, parse } = require('../lib/extension'); describe('extension', () => { describe('parse', () => { - it('returns an empty object if the argument is `undefined`', () => { - assert.deepStrictEqual(parse(), { __proto__: null }); - assert.deepStrictEqual(parse(''), { __proto__: null }); - }); - it('parses a single extension', () => { assert.deepStrictEqual(parse('foo'), { foo: [{ __proto__: null }], @@ -73,7 +68,7 @@ describe('extension', () => { }); it('ignores the optional white spaces', () => { - const header = 'foo; bar\t; \tbaz=1\t ; bar="1"\t\t, \tqux\t ;norf '; + const header = 'foo; bar\t; \tbaz=1\t ; bar="1"\t\t, \tqux\t ;norf'; assert.deepStrictEqual(parse(header), { foo: [{ bar: [true, '1'], baz: ['1'], __proto__: null }], @@ -105,10 +100,12 @@ describe('extension', () => { it('throws an error if a white space is misplaced', () => { [ + [' foo', 0], ['f oo', 2], ['foo;ba r', 7], ['foo;bar =', 8], - ['foo;bar= ', 8] + ['foo;bar= ', 8], + ['foo;bar=ba z', 11] ].forEach((element) => { assert.throws( () => parse(element[0]), @@ -147,13 +144,18 @@ describe('extension', () => { it('throws an error if the header value ends prematurely', () => { [ + '', + 'foo ', + 'foo\t', 'foo, ', 'foo;', + 'foo;bar ', 'foo;bar,', 'foo;bar; ', 'foo;bar=', 'foo;bar="baz', - 'foo;bar="1\\' + 'foo;bar="1\\', + 'foo;bar="baz" ' ].forEach((header) => { assert.throws( () => parse(header), From 552b50679590eb97b92101574f9fafe73c09cf9a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 22 Jun 2021 15:40:24 +0200 Subject: [PATCH 068/328] [major] Drop support for Node.js < 10 --- .github/workflows/ci.yml | 1 - appveyor.yml | 1 - lib/buffer-util.js | 9 +++------ lib/permessage-deflate.js | 9 +-------- lib/validation.js | 7 +------ package.json | 2 +- 6 files changed, 6 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 158a50e32..29e75259f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,6 @@ jobs: strategy: matrix: node: - - 8 - 10 - 12 - 14 diff --git a/appveyor.yml b/appveyor.yml index f4c05fbf4..ff8520944 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,6 @@ environment: - nodejs_version: '14' - nodejs_version: '12' - nodejs_version: '10' - - nodejs_version: '8' platform: - x86 matrix: diff --git a/lib/buffer-util.js b/lib/buffer-util.js index 6fd84c311..1ba1d1beb 100644 --- a/lib/buffer-util.js +++ b/lib/buffer-util.js @@ -52,9 +52,7 @@ function _mask(source, mask, output, offset, length) { * @public */ function _unmask(buffer, mask) { - // Required until https://github.com/nodejs/node/issues/9006 is resolved. - const length = buffer.length; - for (let i = 0; i < length; i++) { + for (let i = 0; i < buffer.length; i++) { buffer[i] ^= mask[i & 3]; } } @@ -103,19 +101,18 @@ function toBuffer(data) { try { const bufferUtil = require('bufferutil'); - const bu = bufferUtil.BufferUtil || bufferUtil; module.exports = { concat, mask(source, mask, output, offset, length) { if (length < 48) _mask(source, mask, output, offset, length); - else bu.mask(source, mask, output, offset, length); + else bufferUtil.mask(source, mask, output, offset, length); }, toArrayBuffer, toBuffer, unmask(buffer, mask) { if (buffer.length < 32) _unmask(buffer, mask); - else bu.unmask(buffer, mask); + else bufferUtil.unmask(buffer, mask); } }; } catch (e) /* istanbul ignore next */ { diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index ce9178429..7fe9459ff 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -4,7 +4,7 @@ const zlib = require('zlib'); const bufferUtil = require('./buffer-util'); const Limiter = require('./limiter'); -const { kStatusCode, NOOP } = require('./constants'); +const { kStatusCode } = require('./constants'); const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); const kPerMessageDeflate = Symbol('permessage-deflate'); @@ -418,13 +418,6 @@ class PerMessageDeflate { this._deflate[kTotalLength] = 0; this._deflate[kBuffers] = []; - // - // An `'error'` event is emitted, only on Node.js < 10.0.0, if the - // `zlib.DeflateRaw` instance is closed while data is being processed. - // This can happen if `PerMessageDeflate#cleanup()` is called at the wrong - // time due to an abnormal WebSocket closure. - // - this._deflate.on('error', NOOP); this._deflate.on('data', deflateOnData); } diff --git a/lib/validation.js b/lib/validation.js index d09b1d39b..ed98c7591 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -106,12 +106,7 @@ function _isValidUTF8(buf) { } try { - let isValidUTF8 = require('utf-8-validate'); - - /* istanbul ignore if */ - if (typeof isValidUTF8 === 'object') { - isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0 - } + const isValidUTF8 = require('utf-8-validate'); module.exports = { isValidStatusCode, diff --git a/package.json b/package.json index d6dff1396..5318ff9e4 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "main": "index.js", "browser": "browser.js", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "files": [ "browser.js", From ebea038f82a8aa2ece7dc69d5e811e0d9deccfab Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 23 Jun 2021 11:40:58 +0200 Subject: [PATCH 069/328] [major] Throw an error if the connection URL is invalid Make the `WebSocket` constructor throw a `SyntaxError` if the URL contains a fragment identifier or if the URL's protocol is not one of `'ws:'`, `'wss:'`, or `'ws+unix:'`. --- lib/websocket.js | 24 +++++++++++++++++++----- test/websocket.test.js | 17 ++++++++++++++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 400a610d1..0915fa4c3 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -613,18 +613,32 @@ function initAsClient(websocket, address, protocols, options) { parsedUrl = address; websocket._url = address.href; } else { - parsedUrl = new URL(address); + try { + parsedUrl = new URL(address); + } catch (e) { + throw new SyntaxError(`Invalid URL: ${address}`); + } + websocket._url = address; } + const isSecure = parsedUrl.protocol === 'wss:'; const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; - if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) { - throw new Error(`Invalid URL: ${websocket.url}`); + if (parsedUrl.protocol !== 'ws:' && !isSecure && !isUnixSocket) { + throw new SyntaxError( + 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"' + ); + } + + if (isUnixSocket && !parsedUrl.pathname) { + throw new SyntaxError("The URL's pathname is empty"); + } + + if (parsedUrl.hash) { + throw new SyntaxError('The URL contains a fragment identifier'); } - const isSecure = - parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:'; const defaultPort = isSecure ? 443 : 80; const key = randomBytes(16).toString('base64'); const get = isSecure ? https.get : http.get; diff --git a/test/websocket.test.js b/test/websocket.test.js index 3301902f3..5c4058e8d 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -20,9 +20,24 @@ class CustomAgent extends http.Agent { describe('WebSocket', () => { describe('#ctor', () => { it('throws an error when using an invalid url', () => { + assert.throws( + () => new WebSocket('foo'), + /^SyntaxError: Invalid URL: foo$/ + ); + + assert.throws( + () => new WebSocket('https://echo.websocket.org'), + /^SyntaxError: The URL's protocol must be one of "ws:", "wss:", or "ws\+unix:"$/ + ); + assert.throws( () => new WebSocket('ws+unix:'), - /^Error: Invalid URL: ws\+unix:$/ + /^SyntaxError: The URL's pathname is empty$/ + ); + + assert.throws( + () => new WebSocket('wss://echo.websocket.org#foo'), + /^SyntaxError: The URL contains a fragment identifier$/ ); }); From e173423c180dc1e4e6ee8938d9e4376a7a8b9757 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 2 Jul 2021 17:43:32 +0200 Subject: [PATCH 070/328] [major] Do not decode `Buffer`s to strings Avoid decoding text messages and close reasons to strings. Pass them as `Buffer`s to the listeners of their respective events. Also, make listeners of the `'message'` event take a boolean argument to speficy whether or not the message is binary. Refs: https://github.com/websockets/ws/issues/1878 Refs: https://github.com/websockets/ws/issues/1804 --- README.md | 12 +- bench/speed.js | 4 +- doc/ws.md | 14 +- examples/ssl.js | 2 +- lib/event-target.js | 9 +- lib/receiver.js | 8 +- lib/sender.js | 11 +- lib/stream.js | 7 +- lib/websocket.js | 14 +- test/autobahn-server.js | 4 +- test/autobahn.js | 4 +- test/create-websocket-stream.test.js | 38 ++++- test/receiver.test.js | 186 ++++++++++++----------- test/websocket-server.test.js | 10 +- test/websocket.test.js | 212 ++++++++++++++++----------- 15 files changed, 326 insertions(+), 209 deletions(-) diff --git a/README.md b/README.md index 1cb19d650..fd75b6533 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,8 @@ ws.on('open', function open() { ws.send('something'); }); -ws.on('message', function incoming(data) { - console.log(data); +ws.on('message', function incoming(message) { + console.log('received: %s', message); }); ``` @@ -296,10 +296,10 @@ const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', function connection(ws) { - ws.on('message', function incoming(data) { + ws.on('message', function incoming(data, isBinary) { wss.clients.forEach(function each(client) { if (client.readyState === WebSocket.OPEN) { - client.send(data); + client.send(data, { binary: isBinary }); } }); }); @@ -315,10 +315,10 @@ const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', function connection(ws) { - ws.on('message', function incoming(data) { + ws.on('message', function incoming(data, isBinary) { wss.clients.forEach(function each(client) { if (client !== ws && client.readyState === WebSocket.OPEN) { - client.send(data); + client.send(data, { binary: isBinary }); } }); }); diff --git a/bench/speed.js b/bench/speed.js index 32ec0fb81..bef6a3067 100644 --- a/bench/speed.js +++ b/bench/speed.js @@ -19,7 +19,9 @@ if (cluster.isMaster) { }); wss.on('connection', (ws) => { - ws.on('message', (data) => ws.send(data)); + ws.on('message', (data, isBinary) => { + ws.send(data, { binary: isBinary }); + }); }); server.listen(path ? { path } : { port }, () => cluster.fork()); diff --git a/doc/ws.md b/doc/ws.md index 668d0d7c4..275a9ea28 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -300,11 +300,12 @@ it defaults to `/`. ### Event: 'close' - `code` {Number} -- `reason` {String} +- `reason` {Buffer} Emitted when the connection is closed. `code` is a numeric value indicating the status code explaining why the connection has been closed. `reason` is a -human-readable string explaining why the connection has been closed. +`Buffer` containing a human-readable string explaining why the connection has +been closed. ### Event: 'error' @@ -315,9 +316,11 @@ of the string values defined below under [WS Error Codes](#ws-error-codes). ### Event: 'message' -- `data` {String|Buffer|ArrayBuffer|Buffer[]} +- `data` {Buffer|ArrayBuffer|Buffer[]} +- `isBinary` {Boolean} -Emitted when a message is received from the server. +Emitted when a message is received. `data` is the message content. `isBinary` +specifies whether the message is binary or not. ### Event: 'open' @@ -389,8 +392,7 @@ following ways: - `code` {Number} A numeric value indicating the status code explaining why the connection is being closed. -- `reason` {String} A human-readable string explaining why the connection is - closing. +- `reason` {String|Buffer} The reason why the connection is closing. Initiate a closing handshake. diff --git a/examples/ssl.js b/examples/ssl.js index c4d5b0758..ad08632b1 100644 --- a/examples/ssl.js +++ b/examples/ssl.js @@ -14,7 +14,7 @@ const wss = new WebSocket.Server({ server }); wss.on('connection', function connection(ws) { ws.on('message', function message(msg) { - console.log(msg); + console.log(msg.toString()); }); }); diff --git a/lib/event-target.js b/lib/event-target.js index a6fbe72b7..cc4f3ba97 100644 --- a/lib/event-target.js +++ b/lib/event-target.js @@ -128,12 +128,15 @@ const EventTarget = { addEventListener(type, listener, options) { if (typeof listener !== 'function') return; - function onMessage(data) { - listener.call(this, new MessageEvent(data, this)); + function onMessage(data, isBinary) { + listener.call( + this, + new MessageEvent(isBinary ? data : data.toString(), this) + ); } function onClose(code, message) { - listener.call(this, new CloseEvent(code, message, this)); + listener.call(this, new CloseEvent(code, message.toString(), this)); } function onError(error) { diff --git a/lib/receiver.js b/lib/receiver.js index 1d2af76e1..d678d6afa 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -499,7 +499,7 @@ class Receiver extends Writable { data = fragments; } - this.emit('message', data); + this.emit('message', data, true); } else { const buf = concat(fragments, messageLength); @@ -514,7 +514,7 @@ class Receiver extends Writable { ); } - this.emit('message', buf.toString()); + this.emit('message', buf, false); } } @@ -533,7 +533,7 @@ class Receiver extends Writable { this._loop = false; if (data.length === 0) { - this.emit('conclude', 1005, ''); + this.emit('conclude', 1005, EMPTY_BUFFER); this.end(); } else if (data.length === 1) { return error( @@ -568,7 +568,7 @@ class Receiver extends Writable { ); } - this.emit('conclude', code, buf.toString()); + this.emit('conclude', code, buf); this.end(); } } else if (this._opcode === 0x09) { diff --git a/lib/sender.js b/lib/sender.js index 441171c57..4f46f4d88 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -102,7 +102,7 @@ class Sender { * Sends a close message to the other peer. * * @param {Number} [code] The status code component of the body - * @param {String} [data] The message component of the body + * @param {(String|Buffer)} [data] The message component of the body * @param {Boolean} [mask=false] Specifies whether or not to mask the message * @param {Function} [cb] Callback * @public @@ -114,7 +114,7 @@ class Sender { buf = EMPTY_BUFFER; } else if (typeof code !== 'number' || !isValidStatusCode(code)) { throw new TypeError('First argument must be a valid error code number'); - } else if (data === undefined || data === '') { + } else if (data === undefined || !data.length) { buf = Buffer.allocUnsafe(2); buf.writeUInt16BE(code, 0); } else { @@ -126,7 +126,12 @@ class Sender { buf = Buffer.allocUnsafe(2 + length); buf.writeUInt16BE(code, 0); - buf.write(data, 2); + + if (typeof data === 'string') { + buf.write(data, 2); + } else { + buf.set(data, 2); + } } if (this._deflating) { diff --git a/lib/stream.js b/lib/stream.js index b0896ff83..0f85ba55d 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -72,8 +72,11 @@ function createWebSocketStream(ws, options) { writableObjectMode: false }); - ws.on('message', function message(msg) { - if (!duplex.push(msg)) { + ws.on('message', function message(msg, isBinary) { + const data = + !isBinary && duplex._readableState.objectMode ? msg.toString() : msg; + + if (!duplex.push(data)) { resumeOnReceiverDrain = false; ws._socket.pause(); } diff --git a/lib/websocket.js b/lib/websocket.js index 0915fa4c3..c21a4ad68 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -48,7 +48,7 @@ class WebSocket extends EventEmitter { this._closeCode = 1006; this._closeFrameReceived = false; this._closeFrameSent = false; - this._closeMessage = ''; + this._closeMessage = EMPTY_BUFFER; this._closeTimer = null; this._extensions = {}; this._protocol = ''; @@ -264,7 +264,8 @@ class WebSocket extends EventEmitter { * +---+ * * @param {Number} [code] Status code explaining why the connection is closing - * @param {String} [data] A string explaining why the connection is closing + * @param {(String|Buffer)} [data] The reason why the connection is + * closing * @public */ close(code, data) { @@ -941,7 +942,7 @@ function sendAfterClose(websocket, data, cb) { * The listener of the `Receiver` `'conclude'` event. * * @param {Number} code The status code - * @param {String} reason The reason for closing + * @param {Buffer} reason The reason for closing * @private */ function receiverOnConclude(code, reason) { @@ -995,11 +996,12 @@ function receiverOnFinish() { /** * The listener of the `Receiver` `'message'` event. * - * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Boolean} isBinary Specifies whether the message is binary or not * @private */ -function receiverOnMessage(data) { - this[kWebSocket].emit('message', data); +function receiverOnMessage(data, isBinary) { + this[kWebSocket].emit('message', data, isBinary); } /** diff --git a/test/autobahn-server.js b/test/autobahn-server.js index 6e0be43ff..24ade1149 100644 --- a/test/autobahn-server.js +++ b/test/autobahn-server.js @@ -10,6 +10,8 @@ const wss = new WebSocket.Server({ port }, () => { }); wss.on('connection', (ws) => { - ws.on('message', (data) => ws.send(data)); + ws.on('message', (data, isBinary) => { + ws.send(data, { binary: isBinary }); + }); ws.on('error', (e) => console.error(e)); }); diff --git a/test/autobahn.js b/test/autobahn.js index cdda513a5..51532fc52 100644 --- a/test/autobahn.js +++ b/test/autobahn.js @@ -18,7 +18,9 @@ function nextTest() { ws = new WebSocket( `ws://localhost:9001/runCase?case=${currentTest}&agent=ws` ); - ws.on('message', (data) => ws.send(data)); + ws.on('message', (data, isBinary) => { + ws.send(data, { binary: isBinary }); + }); ws.on('close', () => { currentTest++; process.nextTick(nextTest); diff --git a/test/create-websocket-stream.test.js b/test/create-websocket-stream.test.js index b96ac9b1b..8aee1a18a 100644 --- a/test/create-websocket-stream.test.js +++ b/test/create-websocket-stream.test.js @@ -9,6 +9,7 @@ const { randomBytes } = require('crypto'); const createWebSocketStream = require('../lib/stream'); const Sender = require('../lib/sender'); const WebSocket = require('..'); +const { EMPTY_BUFFER } = require('../lib/constants'); describe('createWebSocketStream', () => { it('is exposed as a property of the `WebSocket` class', () => { @@ -58,11 +59,12 @@ describe('createWebSocketStream', () => { }); wss.on('connection', (ws) => { - ws.on('message', (message) => { + ws.on('message', (message, isBinary) => { ws.on('close', (code, reason) => { - assert.ok(message.equals(chunk)); + assert.deepStrictEqual(message, chunk); + assert.ok(isBinary); assert.strictEqual(code, 1005); - assert.strictEqual(reason, ''); + assert.strictEqual(reason, EMPTY_BUFFER); wss.close(done); }); }); @@ -229,7 +231,7 @@ describe('createWebSocketStream', () => { ws._socket.write(Buffer.from([0x85, 0x00])); ws.on('close', (code, reason) => { assert.strictEqual(code, 1002); - assert.strictEqual(reason, ''); + assert.deepStrictEqual(reason, EMPTY_BUFFER); serverClientCloseEventEmitted = true; if (duplexCloseEventEmitted) wss.close(done); @@ -538,5 +540,33 @@ describe('createWebSocketStream', () => { }); }); }); + + it('converts text messages to strings in readable object mode', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const events = []; + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + const duplex = createWebSocketStream(ws, { readableObjectMode: true }); + + duplex.on('data', (data) => { + events.push('data'); + assert.strictEqual(data, 'foo'); + }); + + duplex.on('end', () => { + events.push('end'); + duplex.end(); + }); + + duplex.on('close', () => { + assert.deepStrictEqual(events, ['data', 'end']); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.send('foo'); + ws.close(); + }); + }); }); }); diff --git a/test/receiver.test.js b/test/receiver.test.js index cd5770dfb..0736c6100 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -4,18 +4,17 @@ const assert = require('assert'); const crypto = require('crypto'); const PerMessageDeflate = require('../lib/permessage-deflate'); -const constants = require('../lib/constants'); const Receiver = require('../lib/receiver'); const Sender = require('../lib/sender'); - -const kStatusCode = constants.kStatusCode; +const { EMPTY_BUFFER, kStatusCode } = require('../lib/constants'); describe('Receiver', () => { it('parses an unmasked text message', (done) => { const receiver = new Receiver(); - receiver.on('message', (data) => { - assert.strictEqual(data, 'Hello'); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, Buffer.from('Hello')); + assert.ok(!isBinary); done(); }); @@ -27,7 +26,7 @@ describe('Receiver', () => { receiver.on('conclude', (code, data) => { assert.strictEqual(code, 1005); - assert.strictEqual(data, ''); + assert.strictEqual(data, EMPTY_BUFFER); done(); }); @@ -39,7 +38,7 @@ describe('Receiver', () => { receiver.on('conclude', (code, data) => { assert.strictEqual(code, 1000); - assert.strictEqual(data, 'DONE'); + assert.deepStrictEqual(data, Buffer.from('DONE')); done(); }); @@ -50,8 +49,9 @@ describe('Receiver', () => { it('parses a masked text message', (done) => { const receiver = new Receiver(undefined, {}, true); - receiver.on('message', (data) => { - assert.strictEqual(data, '5:::{"name":"echo"}'); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, Buffer.from('5:::{"name":"echo"}')); + assert.ok(!isBinary); done(); }); @@ -62,20 +62,21 @@ describe('Receiver', () => { it('parses a masked text message longer than 125 B', (done) => { const receiver = new Receiver(undefined, {}, true); - const msg = 'A'.repeat(200); + const msg = Buffer.from('A'.repeat(200)); - const list = Sender.frame(Buffer.from(msg), { + const list = Sender.frame(msg, { fin: true, rsv1: false, opcode: 0x01, mask: true, - readOnly: false + readOnly: true }); const frame = Buffer.concat(list); - receiver.on('message', (data) => { - assert.strictEqual(data, msg); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, msg); + assert.ok(!isBinary); done(); }); @@ -85,20 +86,21 @@ describe('Receiver', () => { it('parses a really long masked text message', (done) => { const receiver = new Receiver(undefined, {}, true); - const msg = 'A'.repeat(64 * 1024); + const msg = Buffer.from('A'.repeat(64 * 1024)); - const list = Sender.frame(Buffer.from(msg), { + const list = Sender.frame(msg, { fin: true, rsv1: false, opcode: 0x01, mask: true, - readOnly: false + readOnly: true }); const frame = Buffer.concat(list); - receiver.on('message', (data) => { - assert.strictEqual(data, msg); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, msg); + assert.ok(!isBinary); done(); }); @@ -107,30 +109,31 @@ describe('Receiver', () => { it('parses a 300 B fragmented masked text message', (done) => { const receiver = new Receiver(undefined, {}, true); - const msg = 'A'.repeat(300); + const msg = Buffer.from('A'.repeat(300)); - const fragment1 = msg.substr(0, 150); - const fragment2 = msg.substr(150); + const fragment1 = msg.slice(0, 150); + const fragment2 = msg.slice(150); - const options = { rsv1: false, mask: true, readOnly: false }; + const options = { rsv1: false, mask: true, readOnly: true }; const frame1 = Buffer.concat( - Sender.frame(Buffer.from(fragment1), { + Sender.frame(fragment1, { fin: false, opcode: 0x01, ...options }) ); const frame2 = Buffer.concat( - Sender.frame(Buffer.from(fragment2), { + Sender.frame(fragment2, { fin: true, opcode: 0x00, ...options }) ); - receiver.on('message', (data) => { - assert.strictEqual(data, msg); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, msg); + assert.ok(!isBinary); done(); }); @@ -140,20 +143,20 @@ describe('Receiver', () => { it('parses a ping message', (done) => { const receiver = new Receiver(undefined, {}, true); - const msg = 'Hello'; + const msg = Buffer.from('Hello'); - const list = Sender.frame(Buffer.from(msg), { + const list = Sender.frame(msg, { fin: true, rsv1: false, opcode: 0x09, mask: true, - readOnly: false + readOnly: true }); const frame = Buffer.concat(list); receiver.on('ping', (data) => { - assert.strictEqual(data.toString(), msg); + assert.deepStrictEqual(data, msg); done(); }); @@ -164,7 +167,7 @@ describe('Receiver', () => { const receiver = new Receiver(); receiver.on('ping', (data) => { - assert.ok(data.equals(Buffer.alloc(0))); + assert.strictEqual(data, EMPTY_BUFFER); done(); }); @@ -173,30 +176,30 @@ describe('Receiver', () => { it('parses a 300 B fragmented masked text message with a ping in the middle (1/2)', (done) => { const receiver = new Receiver(undefined, {}, true); - const msg = 'A'.repeat(300); - const pingMessage = 'Hello'; + const msg = Buffer.from('A'.repeat(300)); + const pingMessage = Buffer.from('Hello'); - const fragment1 = msg.substr(0, 150); - const fragment2 = msg.substr(150); + const fragment1 = msg.slice(0, 150); + const fragment2 = msg.slice(150); - const options = { rsv1: false, mask: true, readOnly: false }; + const options = { rsv1: false, mask: true, readOnly: true }; const frame1 = Buffer.concat( - Sender.frame(Buffer.from(fragment1), { + Sender.frame(fragment1, { fin: false, opcode: 0x01, ...options }) ); const frame2 = Buffer.concat( - Sender.frame(Buffer.from(pingMessage), { + Sender.frame(pingMessage, { fin: true, opcode: 0x09, ...options }) ); const frame3 = Buffer.concat( - Sender.frame(Buffer.from(fragment2), { + Sender.frame(fragment2, { fin: true, opcode: 0x00, ...options @@ -205,14 +208,15 @@ describe('Receiver', () => { let gotPing = false; - receiver.on('message', (data) => { - assert.strictEqual(data, msg); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, msg); + assert.ok(!isBinary); assert.ok(gotPing); done(); }); receiver.on('ping', (data) => { gotPing = true; - assert.strictEqual(data.toString(), pingMessage); + assert.ok(data.equals(pingMessage)); }); receiver.write(frame1); @@ -222,11 +226,11 @@ describe('Receiver', () => { it('parses a 300 B fragmented masked text message with a ping in the middle (2/2)', (done) => { const receiver = new Receiver(undefined, {}, true); - const msg = 'A'.repeat(300); - const pingMessage = 'Hello'; + const msg = Buffer.from('A'.repeat(300)); + const pingMessage = Buffer.from('Hello'); - const fragment1 = msg.substr(0, 150); - const fragment2 = msg.substr(150); + const fragment1 = msg.slice(0, 150); + const fragment2 = msg.slice(150); const options = { rsv1: false, mask: true, readOnly: false }; @@ -264,14 +268,15 @@ describe('Receiver', () => { let gotPing = false; - receiver.on('message', (data) => { - assert.strictEqual(data, msg); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, msg); + assert.ok(!isBinary); assert.ok(gotPing); done(); }); receiver.on('ping', (data) => { gotPing = true; - assert.strictEqual(data.toString(), pingMessage); + assert.ok(data.equals(pingMessage)); }); for (let i = 0; i < chunks.length; ++i) { @@ -293,8 +298,9 @@ describe('Receiver', () => { const frame = Buffer.concat(list); - receiver.on('message', (data) => { - assert.ok(data.equals(msg)); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, msg); + assert.ok(isBinary); done(); }); @@ -315,8 +321,9 @@ describe('Receiver', () => { const frame = Buffer.concat(list); - receiver.on('message', (data) => { - assert.ok(data.equals(msg)); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, msg); + assert.ok(isBinary); done(); }); @@ -337,8 +344,9 @@ describe('Receiver', () => { const frame = Buffer.concat(list); - receiver.on('message', (data) => { - assert.ok(data.equals(msg)); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, msg); + assert.ok(isBinary); done(); }); @@ -359,8 +367,9 @@ describe('Receiver', () => { const frame = Buffer.concat(list); - receiver.on('message', (data) => { - assert.ok(data.equals(msg)); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, msg); + assert.ok(isBinary); done(); }); @@ -376,8 +385,9 @@ describe('Receiver', () => { }); const buf = Buffer.from('Hello'); - receiver.on('message', (data) => { - assert.strictEqual(data, 'Hello'); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, buf); + assert.ok(!isBinary); done(); }); @@ -399,8 +409,9 @@ describe('Receiver', () => { const buf1 = Buffer.from('foo'); const buf2 = Buffer.from('bar'); - receiver.on('message', (data) => { - assert.strictEqual(data, 'foobar'); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, Buffer.concat([buf1, buf2])); + assert.ok(!isBinary); done(); }); @@ -430,8 +441,9 @@ describe('Receiver', () => { const receiver = new Receiver(); let counter = 0; - receiver.on('message', (data) => { - assert.strictEqual(data, ''); + receiver.on('message', (data, isBinary) => { + assert.strictEqual(data, EMPTY_BUFFER); + assert.ok(!isBinary); if (++counter === 20000) done(); }); @@ -441,9 +453,10 @@ describe('Receiver', () => { it('resets `totalPayloadLength` only on final frame (unfragmented)', (done) => { const receiver = new Receiver(undefined, {}, false, 10); - receiver.on('message', (data) => { + receiver.on('message', (data, isBinary) => { assert.strictEqual(receiver._totalPayloadLength, 0); - assert.strictEqual(data, 'Hello'); + assert.deepStrictEqual(data, Buffer.from('Hello')); + assert.ok(!isBinary); done(); }); @@ -454,9 +467,10 @@ describe('Receiver', () => { it('resets `totalPayloadLength` only on final frame (fragmented)', (done) => { const receiver = new Receiver(undefined, {}, false, 10); - receiver.on('message', (data) => { + receiver.on('message', (data, isBinary) => { assert.strictEqual(receiver._totalPayloadLength, 0); - assert.strictEqual(data, 'Hello'); + assert.deepStrictEqual(data, Buffer.from('Hello')); + assert.ok(!isBinary); done(); }); @@ -472,12 +486,13 @@ describe('Receiver', () => { receiver.on('ping', (buf) => { assert.strictEqual(receiver._totalPayloadLength, 2); - data = buf.toString(); + data = buf; }); - receiver.on('message', (buf) => { + receiver.on('message', (buf, isBinary) => { assert.strictEqual(receiver._totalPayloadLength, 0); - assert.strictEqual(data, ''); - assert.strictEqual(buf.toString(), 'Hello'); + assert.deepStrictEqual(data, EMPTY_BUFFER); + assert.deepStrictEqual(buf, Buffer.from('Hello')); + assert.ok(isBinary); done(); }); @@ -499,7 +514,12 @@ describe('Receiver', () => { receiver.on('conclude', push).on('message', push); receiver.on('finish', () => { - assert.deepStrictEqual(results, ['', 1005, '']); + assert.deepStrictEqual(results, [ + EMPTY_BUFFER, + false, + 1005, + EMPTY_BUFFER + ]); done(); }); @@ -964,9 +984,9 @@ describe('Receiver', () => { crypto.randomBytes(3) ]; - receiver.on('message', (data) => { - assert.ok(Buffer.isBuffer(data)); - assert.ok(data.equals(Buffer.concat(frags))); + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, Buffer.concat(frags)); + assert.ok(isBinary); done(); }); @@ -982,17 +1002,17 @@ describe('Receiver', () => { }); it("honors the 'arraybuffer' binary type", (done) => { - const receiver = new Receiver(); + const receiver = new Receiver('arraybuffer'); const frags = [ crypto.randomBytes(19221), crypto.randomBytes(954), crypto.randomBytes(623987) ]; - receiver._binaryType = 'arraybuffer'; - receiver.on('message', (data) => { + receiver.on('message', (data, isBinary) => { assert.ok(data instanceof ArrayBuffer); - assert.ok(Buffer.from(data).equals(Buffer.concat(frags))); + assert.deepStrictEqual(Buffer.from(data), Buffer.concat(frags)); + assert.ok(isBinary); done(); }); @@ -1008,7 +1028,7 @@ describe('Receiver', () => { }); it("honors the 'fragments' binary type", (done) => { - const receiver = new Receiver(); + const receiver = new Receiver('fragments'); const frags = [ crypto.randomBytes(17), crypto.randomBytes(419872), @@ -1017,9 +1037,9 @@ describe('Receiver', () => { crypto.randomBytes(1) ]; - receiver._binaryType = 'fragments'; - receiver.on('message', (data) => { + receiver.on('message', (data, isBinary) => { assert.deepStrictEqual(data, frags); + assert.ok(isBinary); done(); }); diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index e0ab2114e..5fc26780b 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -411,8 +411,9 @@ describe('WebSocketServer', () => { const ws = new WebSocket(`ws://localhost:${server.address().port}`); - ws.on('message', (message) => { - assert.strictEqual(message, 'hello'); + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from('hello')); + assert.ok(!isBinary); wss.close(); server.close(done); }); @@ -932,8 +933,9 @@ describe('WebSocketServer', () => { }); wss.on('connection', (ws) => { - ws.on('message', (data) => { - assert.strictEqual(data, 'Hello'); + ws.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, Buffer.from('Hello')); + assert.ok(!isBinary); wss.close(done); }); }); diff --git a/test/websocket.test.js b/test/websocket.test.js index 5c4058e8d..37768772e 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -11,7 +11,7 @@ const fs = require('fs'); const { URL } = require('url'); const WebSocket = require('..'); -const { GUID, NOOP } = require('../lib/constants'); +const { EMPTY_BUFFER, GUID, NOOP } = require('../lib/constants'); class CustomAgent extends http.Agent { addRequest() {} @@ -474,7 +474,7 @@ describe('WebSocket', () => { ws.on('close', (code, reason) => { assert.strictEqual(code, 1006); - assert.strictEqual(reason, ''); + assert.strictEqual(reason, EMPTY_BUFFER); clientCloseEventEmitted = true; if (serverClientCloseEventEmitted) wss.close(done); @@ -485,7 +485,7 @@ describe('WebSocket', () => { wss.on('connection', (ws) => { ws.on('close', (code, reason) => { assert.strictEqual(code, 1002); - assert.strictEqual(reason, ''); + assert.deepStrictEqual(reason, EMPTY_BUFFER); serverClientCloseEventEmitted = true; if (clientCloseEventEmitted) wss.close(done); @@ -505,8 +505,8 @@ describe('WebSocket', () => { assert.ok(err instanceof Error); assert.ok(codes.includes(err.code), `Unexpected code: ${err.code}`); ws.on('close', (code, message) => { - assert.strictEqual(message, ''); assert.strictEqual(code, 1006); + assert.strictEqual(message, EMPTY_BUFFER); wss.close(done); }); }); @@ -594,7 +594,7 @@ describe('WebSocket', () => { ws.on('close', (code, reason) => { assert.strictEqual(code, 1006); - assert.strictEqual(reason, ''); + assert.strictEqual(reason, EMPTY_BUFFER); done(); }); }); @@ -1401,15 +1401,19 @@ describe('WebSocket', () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.on('open', () => ws.send(array, { compress: false })); - ws.on('message', (msg) => { - assert.ok(msg.equals(Buffer.from(array.buffer))); + ws.on('open', () => ws.send(array)); + ws.on('message', (msg, isBinary) => { + assert.deepStrictEqual(msg, Buffer.from(array.buffer)); + assert.ok(isBinary); wss.close(done); }); }); wss.on('connection', (ws) => { - ws.on('message', (msg) => ws.send(msg, { compress: false })); + ws.on('message', (msg, isBinary) => { + assert.ok(isBinary); + ws.send(msg); + }); }); }); @@ -1418,14 +1422,17 @@ describe('WebSocket', () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); ws.on('open', () => ws.send('hi')); - ws.on('message', (message) => { - assert.strictEqual(message, 'hi'); + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from('hi')); + assert.ok(!isBinary); wss.close(done); }); }); wss.on('connection', (ws) => { - ws.on('message', (msg) => ws.send(msg)); + ws.on('message', (msg, isBinary) => { + ws.send(msg, { binary: isBinary }); + }); }); }); @@ -1440,8 +1447,9 @@ describe('WebSocket', () => { }); wss.on('connection', (ws) => { - ws.on('message', (msg) => { - assert.strictEqual(msg, 'fragmentfragment'); + ws.on('message', (msg, isBinary) => { + assert.deepStrictEqual(msg, Buffer.from('fragmentfragment')); + assert.ok(!isBinary); wss.close(done); }); }); @@ -1455,14 +1463,15 @@ describe('WebSocket', () => { }); wss.on('connection', (ws) => { - ws.on('message', (msg) => { - assert.strictEqual(msg, '0'); + ws.on('message', (msg, isBinary) => { + assert.deepStrictEqual(msg, Buffer.from('0')); + assert.ok(!isBinary); wss.close(done); }); }); }); - it('can send binary data as an array', (done) => { + it('can send a `TypedArray`', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const array = new Float32Array(6); @@ -1480,32 +1489,19 @@ describe('WebSocket', () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); ws.on('open', () => ws.send(partial)); - ws.on('message', (message) => { - assert.ok(message.equals(buf)); + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, buf); + assert.ok(isBinary); wss.close(done); }); }); wss.on('connection', (ws) => { - ws.on('message', (msg) => ws.send(msg)); - }); - }); - - it('can send binary data as a buffer', (done) => { - const wss = new WebSocket.Server({ port: 0 }, () => { - const buf = Buffer.from('foobar'); - const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - - ws.on('open', () => ws.send(buf)); - ws.on('message', (message) => { - assert.ok(message.equals(buf)); - wss.close(done); + ws.on('message', (msg, isBinary) => { + assert.ok(isBinary); + ws.send(msg); }); }); - - wss.on('connection', (ws) => { - ws.on('message', (msg) => ws.send(msg)); - }); }); it('can send an `ArrayBuffer`', (done) => { @@ -1526,7 +1522,10 @@ describe('WebSocket', () => { }); wss.on('connection', (ws) => { - ws.on('message', (msg) => ws.send(msg)); + ws.on('message', (msg, isBinary) => { + assert.ok(isBinary); + ws.send(msg); + }); }); }); @@ -1538,13 +1537,16 @@ describe('WebSocket', () => { ws.on('open', () => ws.send(buf)); ws.onmessage = (event) => { - assert.ok(event.data.equals(buf)); + assert.deepStrictEqual(event.data, buf); wss.close(done); }; }); wss.on('connection', (ws) => { - ws.on('message', (msg) => ws.send(msg)); + ws.on('message', (msg, isBinary) => { + assert.ok(isBinary); + ws.send(msg); + }); }); }); @@ -1569,8 +1571,9 @@ describe('WebSocket', () => { }); wss.on('connection', (ws) => { - ws.on('message', (message) => { - assert.ok(message.equals(Buffer.alloc(0))); + ws.on('message', (message, isBinary) => { + assert.strictEqual(message, EMPTY_BUFFER); + assert.ok(isBinary); wss.close(done); }); }); @@ -1586,7 +1589,7 @@ describe('WebSocket', () => { ws.on('open', () => ws.send('hi', { mask: false })); ws.on('close', (code, reason) => { assert.strictEqual(code, 1002); - assert.strictEqual(reason, ''); + assert.deepStrictEqual(reason, EMPTY_BUFFER); clientCloseEventEmitted = true; if (serverClientCloseEventEmitted) wss.close(done); @@ -1612,7 +1615,7 @@ describe('WebSocket', () => { ws.on('close', (code, reason) => { assert.strictEqual(code, 1006); - assert.strictEqual(reason, ''); + assert.strictEqual(reason, EMPTY_BUFFER); serverClientCloseEventEmitted = true; if (clientCloseEventEmitted) wss.close(done); @@ -1799,12 +1802,15 @@ describe('WebSocket', () => { wss.on('connection', (ws) => { ws._socket.once('data', (received) => { - assert.ok(received.slice(0, 2).equals(Buffer.from([0x88, 0x80]))); - assert.ok(sent.equals(Buffer.from([0x88, 0x00]))); + assert.deepStrictEqual( + received.slice(0, 2), + Buffer.from([0x88, 0x80]) + ); + assert.deepStrictEqual(sent, Buffer.from([0x88, 0x00])); ws.on('close', (code, reason) => { assert.strictEqual(code, 1005); - assert.strictEqual(reason, ''); + assert.strictEqual(reason, EMPTY_BUFFER); wss.close(done); }); }); @@ -1821,8 +1827,8 @@ describe('WebSocket', () => { wss.on('connection', (ws) => { ws.on('close', (code, message) => { - assert.strictEqual(message, ''); assert.strictEqual(code, 1000); + assert.deepStrictEqual(message, EMPTY_BUFFER); wss.close(done); }); }); @@ -1837,8 +1843,8 @@ describe('WebSocket', () => { wss.on('connection', (ws) => { ws.on('close', (code, message) => { - assert.strictEqual(message, 'some reason'); assert.strictEqual(code, 1000); + assert.deepStrictEqual(message, Buffer.from('some reason')); wss.close(done); }); }); @@ -1854,7 +1860,10 @@ describe('WebSocket', () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); const messages = []; - ws.on('message', (message) => messages.push(message)); + ws.on('message', (message, isBinary) => { + assert.ok(!isBinary); + messages.push(message.toString()); + }); ws.on('close', (code) => { assert.strictEqual(code, 1005); assert.deepStrictEqual(messages, ['foo', 'bar', 'baz']); @@ -1921,7 +1930,7 @@ describe('WebSocket', () => { ws.on('close', (code, reason) => { assert.strictEqual(code, 1000); - assert.strictEqual(reason, 'some reason'); + assert.deepStrictEqual(reason, Buffer.from('some reason')); wss.close(done); }); @@ -2084,7 +2093,9 @@ describe('WebSocket', () => { }); wss.on('connection', (ws) => { - ws.on('message', (msg) => ws.send(msg)); + ws.on('message', (msg, isBinary) => { + ws.send(msg, { binary: isBinary }); + }); }); }); @@ -2203,7 +2214,9 @@ describe('WebSocket', () => { }); wss.on('connection', (ws) => { - ws.on('message', (msg) => ws.send(msg)); + ws.on('message', (msg, isBinary) => { + ws.send(msg, { binary: isBinary }); + }); }); }); @@ -2331,7 +2344,10 @@ describe('WebSocket', () => { }); wss.on('connection', (ws) => { - ws.on('message', (msg) => ws.send(msg)); + ws.on('message', (msg, isBinary) => { + assert.ok(isBinary); + ws.send(msg); + }); }); }); }); @@ -2415,8 +2431,9 @@ describe('WebSocket', () => { const wss = new WebSocket.Server({ server }); wss.on('connection', (ws) => { - ws.on('message', (message) => { - assert.strictEqual(message, 'foobar'); + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from('foobar')); + assert.ok(!isBinary); server.close(done); wss.close(); }); @@ -2440,7 +2457,10 @@ describe('WebSocket', () => { const wss = new WebSocket.Server({ server }); wss.on('connection', (ws) => { - ws.on('message', (message) => ws.send(message)); + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + ws.send(message); + }); }); server.listen(0, () => { @@ -2449,8 +2469,9 @@ describe('WebSocket', () => { }); ws.on('open', () => ws.send(buf)); - ws.on('message', (message) => { - assert.ok(buf.equals(message)); + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, buf); + assert.ok(isBinary); server.close(done); wss.close(); @@ -2692,15 +2713,18 @@ describe('WebSocket', () => { }); ws.on('open', () => ws.send('hi', { compress: true })); - ws.on('message', (message) => { - assert.strictEqual(message, 'hi'); + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from('hi')); + assert.ok(!isBinary); wss.close(done); }); } ); wss.on('connection', (ws) => { - ws.on('message', (message) => ws.send(message, { compress: true })); + ws.on('message', (message, isBinary) => { + ws.send(message, { binary: isBinary, compress: true }); + }); }); }); @@ -2722,15 +2746,19 @@ describe('WebSocket', () => { }); ws.on('open', () => ws.send(array, { compress: true })); - ws.on('message', (message) => { - assert.ok(message.equals(Buffer.from(array.buffer))); + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from(array.buffer)); + assert.ok(isBinary); wss.close(done); }); } ); wss.on('connection', (ws) => { - ws.on('message', (message) => ws.send(message, { compress: true })); + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + ws.send(message, { compress: true }); + }); }); }); @@ -2752,15 +2780,19 @@ describe('WebSocket', () => { }); ws.on('open', () => ws.send(array.buffer, { compress: true })); - ws.on('message', (message) => { - assert.ok(message.equals(Buffer.from(array.buffer))); + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from(array.buffer)); + assert.ok(isBinary); wss.close(done); }); } ); wss.on('connection', (ws) => { - ws.on('message', (message) => ws.send(message, { compress: true })); + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + ws.send(message, { compress: true }); + }); }); }); @@ -2774,7 +2806,11 @@ describe('WebSocket', () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); const messages = []; - ws.on('message', (message) => messages.push(message)); + ws.on('message', (message, isBinary) => { + assert.ok(!isBinary); + messages.push(message.toString()); + }); + ws.on('close', (code) => { assert.strictEqual(code, 1006); assert.deepStrictEqual(messages, ['foo', 'bar', 'baz', 'qux']); @@ -2818,14 +2854,15 @@ describe('WebSocket', () => { wss.on('connection', (ws) => { const messages = []; - ws.on('message', (message) => { - messages.push(message); + ws.on('message', (message, isBinary) => { + assert.ok(!isBinary); + messages.push(message.toString()); }); ws.on('close', (code, reason) => { assert.deepStrictEqual(messages, ['foo', 'bar', 'baz', 'qux']); assert.strictEqual(code, 1000); - assert.strictEqual(reason, ''); + assert.deepStrictEqual(reason, EMPTY_BUFFER); wss.close(done); }); @@ -2850,8 +2887,10 @@ describe('WebSocket', () => { }); }); - ws.on('message', (message) => { - if (messages.push(message) > 1) return; + ws.on('message', (message, isBinary) => { + assert.ok(!isBinary); + + if (messages.push(message.toString()) > 1) return; ws.close(1000); }); @@ -2859,7 +2898,7 @@ describe('WebSocket', () => { ws.on('close', (code, reason) => { assert.deepStrictEqual(messages, ['', '', '', '']); assert.strictEqual(code, 1000); - assert.strictEqual(reason, ''); + assert.deepStrictEqual(reason, EMPTY_BUFFER); wss.close(done); }); } @@ -2880,14 +2919,17 @@ describe('WebSocket', () => { }); ws.on('open', () => ws.send('hi', { compress: true })); - ws.on('message', (message) => { - assert.strictEqual(message, 'hi'); + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from('hi')); + assert.ok(!isBinary); wss.close(done); }); }); wss.on('connection', (ws) => { - ws.on('message', (message) => ws.send(message, { compress: true })); + ws.on('message', (message, isBinary) => { + ws.send(message, { binary: isBinary, compress: true }); + }); }); }); @@ -2979,8 +3021,10 @@ describe('WebSocket', () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); const messages = []; - ws.on('message', (message) => { - if (messages.push(message) > 1) return; + ws.on('message', (message, isBinary) => { + assert.ok(!isBinary); + + if (messages.push(message.toString()) > 1) return; process.nextTick(() => { assert.strictEqual(ws._receiver._state, 5); @@ -2991,7 +3035,7 @@ describe('WebSocket', () => { ws.on('close', (code, reason) => { assert.deepStrictEqual(messages, ['', '', '', '']); assert.strictEqual(code, 1006); - assert.strictEqual(reason, ''); + assert.strictEqual(reason, EMPTY_BUFFER); wss.close(done); }); } @@ -3023,7 +3067,7 @@ describe('WebSocket', () => { ws.on('close', (code, reason) => { assert.strictEqual(code, 1006); - assert.strictEqual(reason, ''); + assert.strictEqual(reason, EMPTY_BUFFER); clientCloseEventEmitted = true; if (serverClientCloseEventEmitted) wss.close(done); @@ -3051,7 +3095,7 @@ describe('WebSocket', () => { ws.on('close', (code, reason) => { assert.strictEqual(code, 1006); - assert.strictEqual(reason, ''); + assert.strictEqual(reason, EMPTY_BUFFER); serverClientCloseEventEmitted = true; if (clientCloseEventEmitted) wss.close(done); @@ -3077,7 +3121,7 @@ describe('WebSocket', () => { ws.on('close', (code, reason) => { assert.strictEqual(code, 1006); - assert.strictEqual(reason, ''); + assert.strictEqual(reason, EMPTY_BUFFER); clientCloseEventEmitted = true; if (serverClientCloseEventEmitted) wss.close(done); @@ -3109,7 +3153,7 @@ describe('WebSocket', () => { ws.on('close', (code, reason) => { assert.strictEqual(code, 1006); - assert.strictEqual(reason, ''); + assert.strictEqual(reason, EMPTY_BUFFER); serverClientCloseEventEmitted = true; if (clientCloseEventEmitted) wss.close(done); From 78adf5f7737b10166c6f4903e6a5cdbbbb59934c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 6 Jul 2021 09:01:15 +0200 Subject: [PATCH 071/328] [major] Add an ES module wrapper Fixes #1886 --- .eslintrc.yaml | 3 +- README.md | 74 +++++++++++++++++++++++++------------------------- doc/ws.md | 8 +++--- package.json | 7 ++++- wrapper.mjs | 8 ++++++ 5 files changed, 57 insertions(+), 43 deletions(-) create mode 100644 wrapper.mjs diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 45998d206..f3d983b9c 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -7,7 +7,8 @@ extends: - eslint:recommended - plugin:prettier/recommended parserOptions: - ecmaVersion: 9 + ecmaVersion: latest + sourceType: module rules: no-console: off no-var: error diff --git a/README.md b/README.md index fd75b6533..a2ee9254e 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,9 @@ into the creation of [raw deflate/inflate streams][node-zlib-deflaterawdocs]. See [the docs][ws-server-options] for more options. ```js -const WebSocket = require('ws'); +import WebSocket, { WebSocketServer } from 'ws'; -const wss = new WebSocket.Server({ +const wss = new WebSocketServer({ port: 8080, perMessageDeflate: { zlibDeflateOptions: { @@ -129,7 +129,7 @@ server. To always disable the extension on the client set the `perMessageDeflate` option to `false`. ```js -const WebSocket = require('ws'); +import WebSocket from 'ws'; const ws = new WebSocket('ws://www.host.com/path', { perMessageDeflate: false @@ -141,7 +141,7 @@ const ws = new WebSocket('ws://www.host.com/path', { ### Sending and receiving text data ```js -const WebSocket = require('ws'); +import WebSocket from 'ws'; const ws = new WebSocket('ws://www.host.com/path'); @@ -157,7 +157,7 @@ ws.on('message', function incoming(message) { ### Sending binary data ```js -const WebSocket = require('ws'); +import WebSocket from 'ws'; const ws = new WebSocket('ws://www.host.com/path'); @@ -175,9 +175,9 @@ ws.on('open', function open() { ### Simple server ```js -const WebSocket = require('ws'); +import { WebSocketServer } from 'ws'; -const wss = new WebSocket.Server({ port: 8080 }); +const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { ws.on('message', function incoming(message) { @@ -191,15 +191,15 @@ wss.on('connection', function connection(ws) { ### External HTTP/S server ```js -const fs = require('fs'); -const https = require('https'); -const WebSocket = require('ws'); +import { createServer } from 'https'; +import { readFileSync } from 'fs'; +import { WebSocketServer } from 'ws'; -const server = https.createServer({ - cert: fs.readFileSync('/path/to/cert.pem'), - key: fs.readFileSync('/path/to/key.pem') +const server = createServer({ + cert: readFileSync('/path/to/cert.pem'), + key: readFileSync('/path/to/key.pem') }); -const wss = new WebSocket.Server({ server }); +const wss = new WebSocketServer({ server }); wss.on('connection', function connection(ws) { ws.on('message', function incoming(message) { @@ -215,13 +215,13 @@ server.listen(8080); ### Multiple servers sharing a single HTTP/S server ```js -const http = require('http'); -const WebSocket = require('ws'); -const url = require('url'); +import { createServer } from 'http'; +import { parse } from 'url'; +import { WebSocketServer } from 'ws'; -const server = http.createServer(); -const wss1 = new WebSocket.Server({ noServer: true }); -const wss2 = new WebSocket.Server({ noServer: true }); +const server = createServer(); +const wss1 = new WebSocketServer({ noServer: true }); +const wss2 = new WebSocketServer({ noServer: true }); wss1.on('connection', function connection(ws) { // ... @@ -232,7 +232,7 @@ wss2.on('connection', function connection(ws) { }); server.on('upgrade', function upgrade(request, socket, head) { - const pathname = url.parse(request.url).pathname; + const { pathname } = parse(request.url); if (pathname === '/foo') { wss1.handleUpgrade(request, socket, head, function done(ws) { @@ -253,11 +253,11 @@ server.listen(8080); ### Client authentication ```js -const http = require('http'); -const WebSocket = require('ws'); +import WebSocket from 'ws'; +import { createServer } from 'http'; -const server = http.createServer(); -const wss = new WebSocket.Server({ noServer: true }); +const server = createServer(); +const wss = new WebSocketServer({ noServer: true }); wss.on('connection', function connection(ws, request, client) { ws.on('message', function message(msg) { @@ -291,9 +291,9 @@ A client WebSocket broadcasting to all connected WebSocket clients, including itself. ```js -const WebSocket = require('ws'); +import WebSocket, { WebSocketServer } from 'ws'; -const wss = new WebSocket.Server({ port: 8080 }); +const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { ws.on('message', function incoming(data, isBinary) { @@ -310,9 +310,9 @@ A client WebSocket broadcasting to every other connected WebSocket clients, excluding itself. ```js -const WebSocket = require('ws'); +import WebSocket, { WebSocketServer } from 'ws'; -const wss = new WebSocket.Server({ port: 8080 }); +const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { ws.on('message', function incoming(data, isBinary) { @@ -328,7 +328,7 @@ wss.on('connection', function connection(ws) { ### echo.websocket.org demo ```js -const WebSocket = require('ws'); +import WebSocket from 'ws'; const ws = new WebSocket('wss://echo.websocket.org/', { origin: 'https://websocket.org' @@ -355,13 +355,13 @@ ws.on('message', function incoming(data) { ### Use the Node.js streams API ```js -const WebSocket = require('ws'); +import WebSocket, { createWebSocketStream } from 'ws'; const ws = new WebSocket('wss://echo.websocket.org/', { origin: 'https://websocket.org' }); -const duplex = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }); +const duplex = createWebSocketStream(ws, { encoding: 'utf8' }); duplex.pipe(process.stdout); process.stdin.pipe(duplex); @@ -381,9 +381,9 @@ Otherwise, see the test cases. The remote IP address can be obtained from the raw socket. ```js -const WebSocket = require('ws'); +import { WebSocketServer } from 'ws'; -const wss = new WebSocket.Server({ port: 8080 }); +const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws, req) { const ip = req.socket.remoteAddress; @@ -409,7 +409,7 @@ In these cases ping messages can be used as a means to verify that the remote endpoint is still responsive. ```js -const WebSocket = require('ws'); +import { WebSocketServer } from 'ws'; function noop() {} @@ -417,7 +417,7 @@ function heartbeat() { this.isAlive = true; } -const wss = new WebSocket.Server({ port: 8080 }); +const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { ws.isAlive = true; @@ -446,7 +446,7 @@ without knowing it. You might want to add a ping listener on your clients to prevent that. A simple implementation would be: ```js -const WebSocket = require('ws'); +import WebSocket from 'ws'; function heartbeat() { clearTimeout(this.pingTimeout); diff --git a/doc/ws.md b/doc/ws.md index 275a9ea28..6e0c6ef9f 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -2,8 +2,8 @@ ## Table of Contents -- [Class: WebSocket.Server](#class-websocketserver) - - [new WebSocket.Server(options[, callback])](#new-websocketserveroptions-callback) +- [Class: WebSocketServer](#class-websocketserver) + - [new WebSocketServer(options[, callback])](#new-websocketserveroptions-callback) - [Event: 'close'](#event-close) - [Event: 'connection'](#event-connection) - [Event: 'error'](#event-error) @@ -57,11 +57,11 @@ - [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#ws_err_unsupported_data_payload_length) - [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#ws_err_unsupported_message_length) -## Class: WebSocket.Server +## Class: WebSocketServer This class represents a WebSocket server. It extends the `EventEmitter`. -### new WebSocket.Server(options[, callback]) +### new WebSocketServer(options[, callback]) - `options` {Object} - `host` {String} The hostname where to bind the server. diff --git a/package.json b/package.json index 5318ff9e4..32e9b7b9e 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,10 @@ "author": "Einar Otto Stangvik (http://2x.io)", "license": "MIT", "main": "index.js", + "exports": { + "import": "./wrapper.mjs", + "require": "./index.js" + }, "browser": "browser.js", "engines": { "node": ">=10.0.0" @@ -23,7 +27,8 @@ "files": [ "browser.js", "index.js", - "lib/*.js" + "lib/*.js", + "wrapper.mjs" ], "scripts": { "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js", diff --git a/wrapper.mjs b/wrapper.mjs new file mode 100644 index 000000000..7245ad15d --- /dev/null +++ b/wrapper.mjs @@ -0,0 +1,8 @@ +import createWebSocketStream from './lib/stream.js'; +import Receiver from './lib/receiver.js'; +import Sender from './lib/sender.js'; +import WebSocket from './lib/websocket.js'; +import WebSocketServer from './lib/websocket-server.js'; + +export { createWebSocketStream, Receiver, Sender, WebSocket, WebSocketServer }; +export default WebSocket; From df7de574a07115e2321fdb5fc9b2d0fea55d27e8 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 12 Jul 2021 15:52:59 +0200 Subject: [PATCH 072/328] [major] Do not close existing connections When `WebSocketServer.prototype.close()` is called, stop accepting new connections but do not close the existing ones. If the HTTP/S server was created internally, then close it and emit the `'close'` event when it closes. Otherwise, if client tracking is enabled, then emit the `'close'` event when the number of connections goes down to zero. Otherwise, emit it in the next tick. Refs: https://github.com/websockets/ws/pull/1902 --- doc/ws.md | 11 +- lib/websocket-server.js | 55 +++++--- test/sender.test.js | 25 ++++ test/websocket-server.test.js | 100 +++++++++------ test/websocket.test.js | 234 ++++++++++++++++++++++------------ 5 files changed, 284 insertions(+), 141 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 6e0c6ef9f..390596270 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -207,9 +207,14 @@ added when the `clientTracking` is truthy. ### server.close([callback]) -Close the HTTP server if created internally, terminate all clients and call -callback when done. If an external HTTP server is used via the `server` or -`noServer` constructor options, it must be closed manually. +Prevent the server from accepting new connections and close the HTTP server if +created internally. If an external HTTP server is used via the `server` or +`noServer` constructor options, it must be closed manually. Existing connections +are not closed automatically. The server emits a `'close'` event when all +connections are closed unless an external HTTP server is used and client +tracking is disabled. In this case the `'close'` event is emitted in the next +tick. The optional callback is called when the `'close'` event occurs and +receives an `Error` if the server is already closed. ### server.handleUpgrade(request, socket, head, callback) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index e2f1af616..b0cba8315 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -111,7 +111,11 @@ class WebSocketServer extends EventEmitter { } if (options.perMessageDeflate === true) options.perMessageDeflate = {}; - if (options.clientTracking) this.clients = new Set(); + if (options.clientTracking) { + this.clients = new Set(); + this._shouldEmitClose = false; + } + this.options = options; this._state = RUNNING; } @@ -135,9 +139,10 @@ class WebSocketServer extends EventEmitter { } /** - * Close the server. + * Stop the server from accepting new connections and emit the `'close'` event + * when all existing connections are closed. * - * @param {Function} [cb] Callback + * @param {Function} [cb] A one-time listener for the `'close'` event * @public */ close(cb) { @@ -151,29 +156,35 @@ class WebSocketServer extends EventEmitter { if (this._state === CLOSING) return; this._state = CLOSING; - // - // Terminate all associated clients. - // - if (this.clients) { - for (const client of this.clients) client.terminate(); - } + if (this.options.noServer || this.options.server) { + if (this._server) { + this._removeListeners(); + this._removeListeners = this._server = null; + } - const server = this._server; + if (this.clients) { + if (!this.clients.size) { + process.nextTick(emitClose, this); + } else { + this._shouldEmitClose = true; + } + } else { + process.nextTick(emitClose, this); + } + } else { + const server = this._server; - if (server) { this._removeListeners(); this._removeListeners = this._server = null; // - // Close the http server if it was internally created. + // The HTTP/S server was created internally. Close it, and rely on its + // `'close'` event. // - if (this.options.port != null) { - server.close(emitClose.bind(undefined, this)); - return; - } + server.close(() => { + emitClose(this); + }); } - - process.nextTick(emitClose, this); } /** @@ -373,7 +384,13 @@ class WebSocketServer extends EventEmitter { if (this.clients) { this.clients.add(ws); - ws.on('close', () => this.clients.delete(ws)); + ws.on('close', () => { + this.clients.delete(ws); + + if (this._shouldEmitClose && !this.clients.size) { + process.nextTick(emitClose, this); + } + }); } cb(ws, req); diff --git a/test/sender.test.js b/test/sender.test.js index 58eca8fbf..1c43dc5a5 100644 --- a/test/sender.test.js +++ b/test/sender.test.js @@ -266,6 +266,31 @@ describe('Sender', () => { }); describe('#close', () => { + it('throws an error if the first argument is invalid', () => { + const mockSocket = new MockSocket(); + const sender = new Sender(mockSocket); + + assert.throws( + () => sender.close('error'), + /^TypeError: First argument must be a valid error code number$/ + ); + + assert.throws( + () => sender.close(1004), + /^TypeError: First argument must be a valid error code number$/ + ); + }); + + it('throws an error if the message is greater than 123 bytes', () => { + const mockSocket = new MockSocket(); + const sender = new Sender(mockSocket); + + assert.throws( + () => sender.close(1000, 'a'.repeat(124)), + /^RangeError: The message must not be greater than 123 bytes$/ + ); + }); + it('should consume all data before closing', (done) => { const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 5fc26780b..ece58cbea 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -75,6 +75,8 @@ describe('WebSocketServer', () => { }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', ws.close); } ); @@ -103,6 +105,8 @@ describe('WebSocketServer', () => { const port = 1337; const wss = new WebSocket.Server({ port }, () => { const ws = new WebSocket(`ws://localhost:${port}`); + + ws.on('open', ws.close); }); wss.on('connection', () => wss.close(done)); @@ -120,12 +124,14 @@ describe('WebSocketServer', () => { server.listen(0, () => { const wss = new WebSocket.Server({ server }); - const ws = new WebSocket(`ws://localhost:${server.address().port}`); wss.on('connection', () => { - wss.close(); server.close(done); }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', ws.close); }); }); @@ -169,7 +175,11 @@ describe('WebSocketServer', () => { assert.strictEqual(req.url, '/foo?bar=bar'); } else { assert.strictEqual(req.url, '/'); - wss.close(); + + for (const client of wss.clients) { + client.close(); + } + server.close(done); } }); @@ -209,30 +219,13 @@ describe('WebSocketServer', () => { }); describe('#close', () => { - it('does not throw when called twice', (done) => { + it('does not throw if called multiple times', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { + wss.on('close', done); + wss.close(); wss.close(); wss.close(); - - done(); - }); - }); - - it('closes all clients', (done) => { - let closes = 0; - const wss = new WebSocket.Server({ port: 0 }, () => { - const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.on('close', () => { - if (++closes === 2) done(); - }); - }); - - wss.on('connection', (ws) => { - ws.on('close', () => { - if (++closes === 2) done(); - }); - wss.close(); }); }); @@ -254,6 +247,8 @@ describe('WebSocketServer', () => { server.listen(0, () => { const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', ws.close); }); }); @@ -309,6 +304,16 @@ describe('WebSocketServer', () => { }); }); + it("emits the 'close' event if client tracking is disabled", (done) => { + const wss = new WebSocket.Server({ + noServer: true, + clientTracking: false + }); + + wss.on('close', done); + wss.close(); + }); + it("emits the 'close' event if the server is already closed", (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { wss.close(() => { @@ -324,7 +329,10 @@ describe('WebSocketServer', () => { it('returns a list of connected clients', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { assert.strictEqual(wss.clients.size, 0); + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', ws.close); }); wss.on('connection', () => { @@ -404,9 +412,10 @@ describe('WebSocketServer', () => { const wss = new WebSocket.Server({ noServer: true }); server.on('upgrade', (req, socket, head) => { - wss.handleUpgrade(req, socket, head, (client) => - client.send('hello') - ); + wss.handleUpgrade(req, socket, head, (ws) => { + ws.send('hello'); + ws.close(); + }); }); const ws = new WebSocket(`ws://localhost:${server.address().port}`); @@ -414,7 +423,6 @@ describe('WebSocketServer', () => { ws.on('message', (message, isBinary) => { assert.deepStrictEqual(message, Buffer.from('hello')); assert.ok(!isBinary); - wss.close(); server.close(done); }); }); @@ -683,6 +691,7 @@ describe('WebSocketServer', () => { socket.once('data', (chunk) => { assert.strictEqual(chunk[0], 0x88); + socket.destroy(); wss.close(done); }); }); @@ -742,7 +751,6 @@ describe('WebSocketServer', () => { }); wss.on('connection', () => { - wss.close(); server.close(done); }); @@ -751,6 +759,8 @@ describe('WebSocketServer', () => { headers: { Origin: 'https://example.com', foo: 'bar' }, rejectUnauthorized: false }); + + ws.on('open', ws.close); }); }); @@ -762,6 +772,8 @@ describe('WebSocketServer', () => { }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', ws.close); } ); @@ -959,6 +971,10 @@ describe('WebSocketServer', () => { wss.close(done); }); }); + + wss.on('connection', (ws) => { + ws.close(); + }); }); }); @@ -966,17 +982,19 @@ describe('WebSocketServer', () => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - wss.on('headers', (headers, request) => { - assert.deepStrictEqual(headers.slice(0, 3), [ - 'HTTP/1.1 101 Switching Protocols', - 'Upgrade: websocket', - 'Connection: Upgrade' - ]); - assert.ok(request instanceof http.IncomingMessage); - assert.strictEqual(request.url, '/'); + ws.on('open', ws.close); + }); - wss.on('connection', () => wss.close(done)); - }); + wss.on('headers', (headers, request) => { + assert.deepStrictEqual(headers.slice(0, 3), [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade' + ]); + assert.ok(request instanceof http.IncomingMessage); + assert.strictEqual(request.url, '/'); + + wss.on('connection', () => wss.close(done)); }); }); }); @@ -985,6 +1003,8 @@ describe('WebSocketServer', () => { it('is disabled by default', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', ws.close); }); wss.on('connection', (ws, req) => { @@ -1016,6 +1036,10 @@ describe('WebSocketServer', () => { }); } ); + + wss.on('connection', (ws) => { + ws.close(); + }); }); }); }); diff --git a/test/websocket.test.js b/test/websocket.test.js index 37768772e..feb1d8c50 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -105,6 +105,10 @@ describe('WebSocket', () => { }); } ); + + wss.on('connection', (ws) => { + ws.close(); + }); }); it('throws an error when using an invalid `protocolVersion`', () => { @@ -230,6 +234,10 @@ describe('WebSocket', () => { wss.close(done); }; }); + + wss.on('connection', (ws) => { + ws.close(); + }); }); it('takes into account the data in the sender queue', (done) => { @@ -258,6 +266,10 @@ describe('WebSocket', () => { }); } ); + + wss.on('connection', (ws) => { + ws.close(); + }); }); it('takes into account the data in the socket queue', (done) => { @@ -526,6 +538,10 @@ describe('WebSocket', () => { wss.close(done); }); }); + + wss.on('connection', (ws) => { + ws.close(); + }); }); it("emits a 'ping' event", (done) => { @@ -534,7 +550,10 @@ describe('WebSocket', () => { ws.on('ping', () => wss.close(done)); }); - wss.on('connection', (ws) => ws.ping()); + wss.on('connection', (ws) => { + ws.ping(); + ws.close(); + }); }); it("emits a 'pong' event", (done) => { @@ -543,7 +562,10 @@ describe('WebSocket', () => { ws.on('pong', () => wss.close(done)); }); - wss.on('connection', (ws) => ws.pong()); + wss.on('connection', (ws) => { + ws.pong(); + ws.close(); + }); }); }); @@ -977,7 +999,13 @@ describe('WebSocket', () => { const port = wss.address().port; const ws = new WebSocket(`ws://localhost:${port}/?token=qwerty`); - ws.on('open', () => wss.close(done)); + ws.on('open', () => { + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.close(); }); }); @@ -986,7 +1014,13 @@ describe('WebSocket', () => { const port = wss.address().port; const ws = new WebSocket(`ws://localhost:${port}?token=qwerty`); - ws.on('open', () => wss.close(done)); + ws.on('open', () => { + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.close(); }); }); }); @@ -1084,7 +1118,10 @@ describe('WebSocket', () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); ws.on('open', () => { - ws.ping(() => ws.ping()); + ws.ping(() => { + ws.ping(); + ws.close(); + }); }); }); @@ -1103,7 +1140,10 @@ describe('WebSocket', () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); ws.on('open', () => { - ws.ping('hi', () => ws.ping('hi', true)); + ws.ping('hi', () => { + ws.ping('hi', true); + ws.close(); + }); }); }); @@ -1120,7 +1160,10 @@ describe('WebSocket', () => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.on('open', () => ws.ping(0)); + ws.on('open', () => { + ws.ping(0); + ws.close(); + }); }); wss.on('connection', (ws) => { @@ -1144,6 +1187,10 @@ describe('WebSocket', () => { wss.close(done); }); }); + + wss.on('connection', (ws) => { + ws.close(); + }); }); }); @@ -1240,7 +1287,10 @@ describe('WebSocket', () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); ws.on('open', () => { - ws.pong(() => ws.pong()); + ws.pong(() => { + ws.pong(); + ws.close(); + }); }); }); @@ -1259,7 +1309,10 @@ describe('WebSocket', () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); ws.on('open', () => { - ws.pong('hi', () => ws.pong('hi', true)); + ws.pong('hi', () => { + ws.pong('hi', true); + ws.close(); + }); }); }); @@ -1276,7 +1329,10 @@ describe('WebSocket', () => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.on('open', () => ws.pong(0)); + ws.on('open', () => { + ws.pong(0); + ws.close(); + }); }); wss.on('connection', (ws) => { @@ -1300,6 +1356,10 @@ describe('WebSocket', () => { wss.close(done); }); }); + + wss.on('connection', (ws) => { + ws.close(); + }); }); }); @@ -1413,6 +1473,7 @@ describe('WebSocket', () => { ws.on('message', (msg, isBinary) => { assert.ok(isBinary); ws.send(msg); + ws.close(); }); }); }); @@ -1432,6 +1493,7 @@ describe('WebSocket', () => { wss.on('connection', (ws) => { ws.on('message', (msg, isBinary) => { ws.send(msg, { binary: isBinary }); + ws.close(); }); }); }); @@ -1443,6 +1505,7 @@ describe('WebSocket', () => { ws.on('open', () => { ws.send('fragment', { fin: false }); ws.send('fragment', { fin: true }); + ws.close(); }); }); @@ -1459,7 +1522,10 @@ describe('WebSocket', () => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.on('open', () => ws.send(0)); + ws.on('open', () => { + ws.send(0); + ws.close(); + }); }); wss.on('connection', (ws) => { @@ -1488,7 +1554,11 @@ describe('WebSocket', () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.on('open', () => ws.send(partial)); + ws.on('open', () => { + ws.send(partial); + ws.close(); + }); + ws.on('message', (message, isBinary) => { assert.deepStrictEqual(message, buf); assert.ok(isBinary); @@ -1514,7 +1584,11 @@ describe('WebSocket', () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.on('open', () => ws.send(array.buffer)); + ws.on('open', () => { + ws.send(array.buffer); + ws.close(); + }); + ws.onmessage = (event) => { assert.ok(event.data.equals(Buffer.from(array.buffer))); wss.close(done); @@ -1534,7 +1608,10 @@ describe('WebSocket', () => { const buf = Buffer.from('foobar'); const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.on('open', () => ws.send(buf)); + ws.on('open', () => { + ws.send(buf); + ws.close(); + }); ws.onmessage = (event) => { assert.deepStrictEqual(event.data, buf); @@ -1561,13 +1638,20 @@ describe('WebSocket', () => { }); }); }); + + wss.on('connection', (ws) => { + ws.close(); + }); }); it('works when the `data` argument is falsy', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.on('open', () => ws.send()); + ws.on('open', () => { + ws.send(); + ws.close(); + }); }); wss.on('connection', (ws) => { @@ -1743,51 +1827,6 @@ describe('WebSocket', () => { }); }); - it('throws an error if the first argument is invalid (1/2)', (done) => { - const wss = new WebSocket.Server({ port: 0 }, () => { - const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - - ws.on('open', () => { - assert.throws( - () => ws.close('error'), - /^TypeError: First argument must be a valid error code number$/ - ); - - wss.close(done); - }); - }); - }); - - it('throws an error if the first argument is invalid (2/2)', (done) => { - const wss = new WebSocket.Server({ port: 0 }, () => { - const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - - ws.on('open', () => { - assert.throws( - () => ws.close(1004), - /^TypeError: First argument must be a valid error code number$/ - ); - - wss.close(done); - }); - }); - }); - - it('throws an error if the message is greater than 123 bytes', (done) => { - const wss = new WebSocket.Server({ port: 0 }, () => { - const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - - ws.on('open', () => { - assert.throws( - () => ws.close(1000, 'a'.repeat(124)), - /^RangeError: The message must not be greater than 123 bytes$/ - ); - - wss.close(done); - }); - }); - }); - it('sends the close status code only when necessary', (done) => { let sent; const wss = new WebSocket.Server({ port: 0 }, () => { @@ -2206,7 +2245,11 @@ describe('WebSocket', () => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.addEventListener('open', () => ws.send('hi')); + ws.addEventListener('open', () => { + ws.send('hi'); + ws.close(); + }); + ws.addEventListener('message', (messageEvent) => { assert.strictEqual(messageEvent.data, 'hi'); wss.close(done); @@ -2262,7 +2305,7 @@ describe('WebSocket', () => { ws.addEventListener('message', (messageEvent) => { assert.strictEqual(messageEvent.type, 'message'); assert.strictEqual(messageEvent.target, ws); - wss.close(); + ws.close(); }); ws.addEventListener('close', (closeEvent) => { assert.strictEqual(closeEvent.type, 'close'); @@ -2275,7 +2318,7 @@ describe('WebSocket', () => { assert.strictEqual(errorEvent.target, ws); assert.strictEqual(errorEvent.error, err); - done(); + wss.close(done); }); }); @@ -2292,7 +2335,10 @@ describe('WebSocket', () => { }; }); - wss.on('connection', (ws) => ws.send(new Uint8Array(4096))); + wss.on('connection', (ws) => { + ws.send(new Uint8Array(4096)); + ws.close(); + }); }); it('ignores `binaryType` for text messages', (done) => { @@ -2307,7 +2353,10 @@ describe('WebSocket', () => { }; }); - wss.on('connection', (ws) => ws.send('foo')); + wss.on('connection', (ws) => { + ws.send('foo'); + ws.close(); + }); }); it('allows to update `binaryType` on the fly', (done) => { @@ -2337,7 +2386,10 @@ describe('WebSocket', () => { ws.onopen = () => { testType('nodebuffer', () => { testType('arraybuffer', () => { - testType('fragments', () => wss.close(done)); + testType('fragments', () => { + ws.close(); + wss.close(done); + }); }); }); }; @@ -2361,7 +2413,6 @@ describe('WebSocket', () => { const wss = new WebSocket.Server({ server }); wss.on('connection', () => { - wss.close(); server.close(done); }); @@ -2369,6 +2420,8 @@ describe('WebSocket', () => { const ws = new WebSocket(`wss://127.0.0.1:${server.address().port}`, { rejectUnauthorized: false }); + + ws.on('open', ws.close); }); }); @@ -2392,7 +2445,6 @@ describe('WebSocket', () => { wss.on('connection', () => { assert.ok(success); server.close(done); - wss.close(); }); server.listen(0, () => { @@ -2401,6 +2453,8 @@ describe('WebSocket', () => { key: fs.readFileSync('test/fixtures/client-key.pem'), rejectUnauthorized: false }); + + ws.on('open', ws.close); }); }); @@ -2435,7 +2489,6 @@ describe('WebSocket', () => { assert.deepStrictEqual(message, Buffer.from('foobar')); assert.ok(!isBinary); server.close(done); - wss.close(); }); }); @@ -2444,7 +2497,10 @@ describe('WebSocket', () => { rejectUnauthorized: false }); - ws.on('open', () => ws.send('foobar')); + ws.on('open', () => { + ws.send('foobar'); + ws.close(); + }); }); }); @@ -2460,6 +2516,7 @@ describe('WebSocket', () => { ws.on('message', (message, isBinary) => { assert.ok(isBinary); ws.send(message); + ws.close(); }); }); @@ -2474,7 +2531,6 @@ describe('WebSocket', () => { assert.ok(isBinary); server.close(done); - wss.close(); }); }); }).timeout(4000); @@ -2712,7 +2768,11 @@ describe('WebSocket', () => { perMessageDeflate: { threshold: 0 } }); - ws.on('open', () => ws.send('hi', { compress: true })); + ws.on('open', () => { + ws.send('hi', { compress: true }); + ws.close(); + }); + ws.on('message', (message, isBinary) => { assert.deepStrictEqual(message, Buffer.from('hi')); assert.ok(!isBinary); @@ -2745,7 +2805,11 @@ describe('WebSocket', () => { perMessageDeflate: { threshold: 0 } }); - ws.on('open', () => ws.send(array, { compress: true })); + ws.on('open', () => { + ws.send(array, { compress: true }); + ws.close(); + }); + ws.on('message', (message, isBinary) => { assert.deepStrictEqual(message, Buffer.from(array.buffer)); assert.ok(isBinary); @@ -2779,7 +2843,11 @@ describe('WebSocket', () => { perMessageDeflate: { threshold: 0 } }); - ws.on('open', () => ws.send(array.buffer, { compress: true })); + ws.on('open', () => { + ws.send(array.buffer, { compress: true }); + ws.close(); + }); + ws.on('message', (message, isBinary) => { assert.deepStrictEqual(message, Buffer.from(array.buffer)); assert.ok(isBinary); @@ -2918,7 +2986,11 @@ describe('WebSocket', () => { perMessageDeflate: false }); - ws.on('open', () => ws.send('hi', { compress: true })); + ws.on('open', () => { + ws.send('hi', { compress: true }); + ws.close(); + }); + ws.on('message', (message, isBinary) => { assert.deepStrictEqual(message, Buffer.from('hi')); assert.ok(!isBinary); @@ -2934,10 +3006,10 @@ describe('WebSocket', () => { }); it('calls the callback if the socket is closed prematurely', (done) => { + const called = []; const wss = new WebSocket.Server( { perMessageDeflate: true, port: 0 }, () => { - const called = []; const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { perMessageDeflate: { threshold: 0 } }); @@ -2966,15 +3038,15 @@ describe('WebSocket', () => { ); }); }); - - ws.on('close', () => { - assert.deepStrictEqual(called, [1, 2]); - wss.close(done); - }); } ); wss.on('connection', (ws) => { + ws.on('close', () => { + assert.deepStrictEqual(called, [1, 2]); + wss.close(done); + }); + ws._socket.end(); }); }); From abde9cfc21ce0f1cb7e2556aea70b423359364c7 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 12 Jul 2021 16:46:06 +0200 Subject: [PATCH 073/328] [major] Call the callback with an error if the server is closed Match the behavior of Node.js core `net.Server` and call the callback of `WebSocketServer.prototype.close()` with an error if the server is already closed. --- lib/websocket-server.js | 10 ++++++++-- test/websocket-server.test.js | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index b0cba8315..b147fe5d5 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -146,13 +146,19 @@ class WebSocketServer extends EventEmitter { * @public */ close(cb) { - if (cb) this.once('close', cb); - if (this._state === CLOSED) { + if (cb) { + this.once('close', () => { + cb(new Error('The server is not running')); + }); + } + process.nextTick(emitClose, this); return; } + if (cb) this.once('close', cb); + if (this._state === CLOSING) return; this._state = CLOSING; diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index ece58cbea..e3daf7e0b 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -315,11 +315,21 @@ describe('WebSocketServer', () => { }); it("emits the 'close' event if the server is already closed", (done) => { + let callbackCalled = false; const wss = new WebSocket.Server({ port: 0 }, () => { wss.close(() => { assert.strictEqual(wss._state, 2); - wss.on('close', done); - wss.close(); + + wss.on('close', () => { + callbackCalled = true; + }); + + wss.close((err) => { + assert.ok(callbackCalled); + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'The server is not running'); + done(); + }); }); }); }); From 64b3c71ee621737fcabb431d5113d78e1355af1c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 15 Jul 2021 11:17:02 +0200 Subject: [PATCH 074/328] [pkg] Update mocha to version 8.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 32e9b7b9e..74558562c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "eslint": "^7.2.0", "eslint-config-prettier": "^8.1.0", "eslint-plugin-prettier": "^3.0.1", - "mocha": "^7.0.0", + "mocha": "^8.4.0", "nyc": "^15.0.0", "prettier": "^2.0.5", "utf-8-validate": "^5.0.2" From 77a675c344e5c003cf0d0a4687a005cb75e4cc0f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 15 Jul 2021 11:25:55 +0200 Subject: [PATCH 075/328] [minor] Remove unneeded setters Refs: https://github.com/websockets/ws/commit/ecb9d9ea --- lib/websocket.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index c21a4ad68..cb8077de5 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -125,9 +125,6 @@ class WebSocket extends EventEmitter { return undefined; } - /* istanbul ignore next */ - set onclose(listener) {} - /** * @type {Function} */ @@ -136,9 +133,6 @@ class WebSocket extends EventEmitter { return undefined; } - /* istanbul ignore next */ - set onerror(listener) {} - /** * @type {Function} */ @@ -147,9 +141,6 @@ class WebSocket extends EventEmitter { return undefined; } - /* istanbul ignore next */ - set onopen(listener) {} - /** * @type {Function} */ @@ -158,9 +149,6 @@ class WebSocket extends EventEmitter { return undefined; } - /* istanbul ignore next */ - set onmessage(listener) {} - /** * @type {String} */ From 9558ed1c73fa1c77237efa90b058f5d1d5e44d71 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 16 Jul 2021 17:05:50 +0200 Subject: [PATCH 076/328] [major] Make `WebSocket#addEventListener()` ignore non standard events Make `WebSocket.prototype.addEventListener()` a noop if the `type` argument is not one of `'close'`, `'error'`, `'message'`, or `'open'`. --- doc/ws.md | 4 ++- lib/constants.js | 3 ++- lib/event-target.js | 26 ++++++++---------- lib/websocket.js | 18 +++++++------ test/websocket.test.js | 60 +++++++++++++++++++----------------------- 5 files changed, 53 insertions(+), 58 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 390596270..110980816 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -370,7 +370,9 @@ handshake. This allows you to read headers from the server, for example at most once after being added. If `true`, the listener would be automatically removed when invoked. -Register an event listener emulating the `EventTarget` interface. +Register an event listener emulating the `EventTarget` interface. This method +does nothing if `type` is not one of `'close'`, `'error'`, `'message'`, or +`'open'`. ### websocket.binaryType diff --git a/lib/constants.js b/lib/constants.js index 4082981f8..dce5dd21a 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -2,9 +2,10 @@ module.exports = { BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], + EMPTY_BUFFER: Buffer.alloc(0), GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + kListener: Symbol('kListener'), kStatusCode: Symbol('status-code'), kWebSocket: Symbol('websocket'), - EMPTY_BUFFER: Buffer.alloc(0), NOOP: () => {} }; diff --git a/lib/event-target.js b/lib/event-target.js index cc4f3ba97..6987d5b5c 100644 --- a/lib/event-target.js +++ b/lib/event-target.js @@ -1,5 +1,7 @@ 'use strict'; +const { kListener } = require('./constants'); + /** * Class representing an event. * @@ -126,8 +128,6 @@ const EventTarget = { * @public */ addEventListener(type, listener, options) { - if (typeof listener !== 'function') return; - function onMessage(data, isBinary) { listener.call( this, @@ -150,19 +150,17 @@ const EventTarget = { const method = options && options.once ? 'once' : 'on'; if (type === 'message') { - onMessage._listener = listener; + onMessage[kListener] = listener; this[method](type, onMessage); } else if (type === 'close') { - onClose._listener = listener; + onClose[kListener] = listener; this[method](type, onClose); } else if (type === 'error') { - onError._listener = listener; + onError[kListener] = listener; this[method](type, onError); } else if (type === 'open') { - onOpen._listener = listener; + onOpen[kListener] = listener; this[method](type, onOpen); - } else { - this[method](type, listener); } }, @@ -170,15 +168,13 @@ const EventTarget = { * Remove an event listener. * * @param {String} type A string representing the event type to remove - * @param {Function} listener The listener to remove + * @param {Function} handler The listener to remove * @public */ - removeEventListener(type, listener) { - const listeners = this.listeners(type); - - for (let i = 0; i < listeners.length; i++) { - if (listeners[i] === listener || listeners[i]._listener === listener) { - this.removeListener(type, listeners[i]); + removeEventListener(type, handler) { + for (const listener of this.listeners(type)) { + if (listener === handler || listener[kListener] === handler) { + this.removeListener(type, listener); } } } diff --git a/lib/websocket.js b/lib/websocket.js index cb8077de5..bf33b6ce8 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -15,6 +15,7 @@ const { BINARY_TYPES, EMPTY_BUFFER, GUID, + kListener, kStatusCode, kWebSocket, NOOP @@ -522,22 +523,23 @@ Object.defineProperty(WebSocket.prototype, 'CLOSED', { Object.defineProperty(WebSocket.prototype, `on${method}`, { enumerable: true, get() { - const listeners = this.listeners(method); - for (let i = 0; i < listeners.length; i++) { - if (listeners[i]._listener) return listeners[i]._listener; + for (const listener of this.listeners(method)) { + if (listener[kListener]) return listener[kListener]; } return undefined; }, - set(listener) { - const listeners = this.listeners(method); - for (let i = 0; i < listeners.length; i++) { + set(handler) { + for (const listener of this.listeners(method)) { // // Remove only the listeners added via `addEventListener`. // - if (listeners[i]._listener) this.removeListener(method, listeners[i]); + if (listener[kListener]) this.removeListener(method, listener); } - this.addEventListener(method, listener); + + if (typeof handler !== 'function') return; + + this.addEventListener(method, handler); } }); }); diff --git a/test/websocket.test.js b/test/websocket.test.js index feb1d8c50..e409e8fca 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -11,7 +11,7 @@ const fs = require('fs'); const { URL } = require('url'); const WebSocket = require('..'); -const { EMPTY_BUFFER, GUID, NOOP } = require('../lib/constants'); +const { EMPTY_BUFFER, GUID, kListener, NOOP } = require('../lib/constants'); class CustomAgent extends http.Agent { addRequest() {} @@ -2157,7 +2157,7 @@ describe('WebSocket', () => { assert.strictEqual(listeners.length, 2); assert.strictEqual(listeners[0], NOOP); - assert.strictEqual(listeners[1]._listener, NOOP); + assert.strictEqual(listeners[1][kListener], NOOP); ws.onclose = NOOP; @@ -2165,36 +2165,38 @@ describe('WebSocket', () => { assert.strictEqual(listeners.length, 2); assert.strictEqual(listeners[0], NOOP); - assert.strictEqual(listeners[1]._listener, NOOP); + assert.strictEqual(listeners[1][kListener], NOOP); }); - it('adds listeners for custom events with `addEventListener`', () => { + it('supports the `addEventListener` method', () => { + const events = []; const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); - ws.addEventListener('foo', NOOP); - assert.strictEqual(ws.listeners('foo')[0], NOOP); + ws.addEventListener('foo', () => {}); + assert.strictEqual(ws.listenerCount('foo'), 0); - // - // Fails silently when the `listener` is not a function. - // - ws.addEventListener('bar', {}); - assert.strictEqual(ws.listeners('bar').length, 0); - }); + ws.addEventListener('open', () => { + events.push('open'); + assert.strictEqual(ws.listenerCount('open'), 1); + }); - it('allows to add one time listeners with `addEventListener`', (done) => { - const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + assert.strictEqual(ws.listenerCount('open'), 1); ws.addEventListener( - 'foo', + 'message', () => { - assert.strictEqual(ws.listenerCount('foo'), 0); - done(); + events.push('message'); + assert.strictEqual(ws.listenerCount('message'), 0); }, { once: true } ); - assert.strictEqual(ws.listenerCount('foo'), 1); - ws.emit('foo'); + assert.strictEqual(ws.listenerCount('message'), 1); + + ws.emit('open'); + ws.emit('message', EMPTY_BUFFER, false); + + assert.deepStrictEqual(events, ['open', 'message']); }); it('supports the `removeEventListener` method', () => { @@ -2202,43 +2204,35 @@ describe('WebSocket', () => { ws.addEventListener('message', NOOP); ws.addEventListener('open', NOOP); - ws.addEventListener('foo', NOOP); - assert.strictEqual(ws.listeners('message')[0]._listener, NOOP); - assert.strictEqual(ws.listeners('open')[0]._listener, NOOP); - assert.strictEqual(ws.listeners('foo')[0], NOOP); + assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); + assert.strictEqual(ws.listeners('open')[0][kListener], NOOP); ws.removeEventListener('message', () => {}); - assert.strictEqual(ws.listeners('message')[0]._listener, NOOP); + assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); ws.removeEventListener('message', NOOP); ws.removeEventListener('open', NOOP); - ws.removeEventListener('foo', NOOP); assert.strictEqual(ws.listenerCount('message'), 0); assert.strictEqual(ws.listenerCount('open'), 0); - assert.strictEqual(ws.listenerCount('foo'), 0); ws.addEventListener('message', NOOP, { once: true }); ws.addEventListener('open', NOOP, { once: true }); - ws.addEventListener('foo', NOOP, { once: true }); - assert.strictEqual(ws.listeners('message')[0]._listener, NOOP); - assert.strictEqual(ws.listeners('open')[0]._listener, NOOP); - assert.strictEqual(ws.listeners('foo')[0], NOOP); + assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); + assert.strictEqual(ws.listeners('open')[0][kListener], NOOP); ws.removeEventListener('message', () => {}); - assert.strictEqual(ws.listeners('message')[0]._listener, NOOP); + assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); ws.removeEventListener('message', NOOP); ws.removeEventListener('open', NOOP); - ws.removeEventListener('foo', NOOP); assert.strictEqual(ws.listenerCount('message'), 0); assert.strictEqual(ws.listenerCount('open'), 0); - assert.strictEqual(ws.listenerCount('foo'), 0); }); it('wraps text data in a `MessageEvent`', (done) => { From ea95d9c49cbd90776134cf796998b5c74a338134 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 16 Jul 2021 17:20:52 +0200 Subject: [PATCH 077/328] [major] Ignore listeners not added with `WebSocket#addEventListener()` Make `WebSocket.prototype.removeEventListener()` only remove listeners added with `WebSocket.prototype.addEventListener()` and only one at time. --- doc/ws.md | 4 +++- lib/event-target.js | 3 ++- test/websocket.test.js | 24 ++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 110980816..d657ceeee 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -476,7 +476,9 @@ The current state of the connection. This is one of the ready state constants. - `type` {String} A string representing the event type to remove. - `listener` {Function} The listener to remove. -Removes an event listener emulating the `EventTarget` interface. +Removes an event listener emulating the `EventTarget` interface. This method +only removes listeners added with +[`websocket.addEventListener()`](#websocketaddeventlistenertype-listener-options). ### websocket.send(data[, options][, callback]) diff --git a/lib/event-target.js b/lib/event-target.js index 6987d5b5c..7230a2081 100644 --- a/lib/event-target.js +++ b/lib/event-target.js @@ -173,8 +173,9 @@ const EventTarget = { */ removeEventListener(type, handler) { for (const listener of this.listeners(type)) { - if (listener === handler || listener[kListener] === handler) { + if (listener[kListener] === handler) { this.removeListener(type, listener); + break; } } } diff --git a/test/websocket.test.js b/test/websocket.test.js index e409e8fca..1af00252b 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2233,6 +2233,30 @@ describe('WebSocket', () => { assert.strictEqual(ws.listenerCount('message'), 0); assert.strictEqual(ws.listenerCount('open'), 0); + + // Multiple listeners. + ws.addEventListener('message', NOOP); + ws.addEventListener('message', NOOP); + + assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); + assert.strictEqual(ws.listeners('message')[1][kListener], NOOP); + + ws.removeEventListener('message', NOOP); + + assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); + + ws.removeEventListener('message', NOOP); + + assert.strictEqual(ws.listenerCount('message'), 0); + + // Listeners not added with `websocket.addEventListener()`. + ws.on('message', NOOP); + + assert.deepStrictEqual(ws.listeners('message'), [NOOP]); + + ws.removeEventListener('message', NOOP); + + assert.deepStrictEqual(ws.listeners('message'), [NOOP]); }); it('wraps text data in a `MessageEvent`', (done) => { From 8c61563b5a5c73478e968ae92cea9fef92ef864c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 16 Jul 2021 18:44:34 +0200 Subject: [PATCH 078/328] [fix] Make listeners added via event handler properties independent Prevent the `onclose`, `onerror`, `onmessage`, and `onopen` getters and setters from returning or removing event listeners added with `WebSocket.prototype.addEventListener()`. Also prevent `WebSocket.prototype.removeEventListener()` from removing event listeners added with the `onclose`, `onerror`, `onmessage`, and `onopen` setters. Refs: https://github.com/websockets/ws/issues/1818 --- lib/constants.js | 1 + lib/event-target.js | 63 +++++++++++++++++++++--------------------- lib/websocket.js | 15 ++++++---- test/websocket.test.js | 47 +++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 37 deletions(-) diff --git a/lib/constants.js b/lib/constants.js index dce5dd21a..d691b30a1 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -4,6 +4,7 @@ module.exports = { BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], EMPTY_BUFFER: Buffer.alloc(0), GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + kForOnEventAttribute: Symbol('kIsForOnEventAttribute'), kListener: Symbol('kListener'), kStatusCode: Symbol('status-code'), kWebSocket: Symbol('websocket'), diff --git a/lib/event-target.js b/lib/event-target.js index 7230a2081..b135463d4 100644 --- a/lib/event-target.js +++ b/lib/event-target.js @@ -1,6 +1,6 @@ 'use strict'; -const { kListener } = require('./constants'); +const { kForOnEventAttribute, kListener } = require('./constants'); /** * Class representing an event. @@ -127,40 +127,41 @@ const EventTarget = { * the listener would be automatically removed when invoked. * @public */ - addEventListener(type, listener, options) { - function onMessage(data, isBinary) { - listener.call( - this, - new MessageEvent(isBinary ? data : data.toString(), this) - ); - } - - function onClose(code, message) { - listener.call(this, new CloseEvent(code, message.toString(), this)); - } - - function onError(error) { - listener.call(this, new ErrorEvent(error, this)); - } - - function onOpen() { - listener.call(this, new OpenEvent(this)); - } - - const method = options && options.once ? 'once' : 'on'; + addEventListener( + type, + listener, + options = { once: false, [kForOnEventAttribute]: false } + ) { + let wrapper; if (type === 'message') { - onMessage[kListener] = listener; - this[method](type, onMessage); + wrapper = function onMessage(data, isBinary) { + const event = new MessageEvent(isBinary ? data : data.toString(), this); + listener.call(this, event); + }; } else if (type === 'close') { - onClose[kListener] = listener; - this[method](type, onClose); + wrapper = function onClose(code, message) { + listener.call(this, new CloseEvent(code, message.toString(), this)); + }; } else if (type === 'error') { - onError[kListener] = listener; - this[method](type, onError); + wrapper = function onError(error) { + listener.call(this, new ErrorEvent(error, this)); + }; } else if (type === 'open') { - onOpen[kListener] = listener; - this[method](type, onOpen); + wrapper = function onOpen() { + listener.call(this, new OpenEvent(this)); + }; + } else { + return; + } + + wrapper[kForOnEventAttribute] = options[kForOnEventAttribute]; + wrapper[kListener] = listener; + + if (options.once) { + this.once(type, wrapper); + } else { + this.on(type, wrapper); } }, @@ -173,7 +174,7 @@ const EventTarget = { */ removeEventListener(type, handler) { for (const listener of this.listeners(type)) { - if (listener[kListener] === handler) { + if (listener[kListener] === handler && !listener[kForOnEventAttribute]) { this.removeListener(type, listener); break; } diff --git a/lib/websocket.js b/lib/websocket.js index bf33b6ce8..5b5ecd3cf 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -15,6 +15,7 @@ const { BINARY_TYPES, EMPTY_BUFFER, GUID, + kForOnEventAttribute, kListener, kStatusCode, kWebSocket, @@ -524,22 +525,24 @@ Object.defineProperty(WebSocket.prototype, 'CLOSED', { enumerable: true, get() { for (const listener of this.listeners(method)) { - if (listener[kListener]) return listener[kListener]; + if (listener[kForOnEventAttribute]) return listener[kListener]; } return undefined; }, set(handler) { for (const listener of this.listeners(method)) { - // - // Remove only the listeners added via `addEventListener`. - // - if (listener[kListener]) this.removeListener(method, listener); + if (listener[kForOnEventAttribute]) { + this.removeListener(method, listener); + break; + } } if (typeof handler !== 'function') return; - this.addEventListener(method, handler); + this.addEventListener(method, handler, { + [kForOnEventAttribute]: true + }); } }); }); diff --git a/test/websocket.test.js b/test/websocket.test.js index 1af00252b..3883d855a 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2111,6 +2111,11 @@ describe('WebSocket', () => { assert.strictEqual(ws.onclose, NOOP); assert.strictEqual(ws.onerror, NOOP); assert.strictEqual(ws.onopen, NOOP); + + ws.onmessage = 'foo'; + + assert.strictEqual(ws.onmessage, undefined); + assert.strictEqual(ws.listenerCount('onmessage'), 0); }); it('works like the `EventEmitter` interface', (done) => { @@ -2199,6 +2204,40 @@ describe('WebSocket', () => { assert.deepStrictEqual(events, ['open', 'message']); }); + it("doesn't return listeners added with `addEventListener`", () => { + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + ws.addEventListener('open', NOOP); + + const listeners = ws.listeners('open'); + + assert.strictEqual(listeners.length, 1); + assert.strictEqual(listeners[0][kListener], NOOP); + + assert.strictEqual(ws.onopen, undefined); + }); + + it("doesn't remove listeners added with `addEventListener`", () => { + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + ws.addEventListener('close', NOOP); + ws.onclose = NOOP; + + let listeners = ws.listeners('close'); + + assert.strictEqual(listeners.length, 2); + assert.strictEqual(listeners[0][kListener], NOOP); + assert.strictEqual(listeners[1][kListener], NOOP); + + ws.onclose = NOOP; + + listeners = ws.listeners('close'); + + assert.strictEqual(listeners.length, 2); + assert.strictEqual(listeners[0][kListener], NOOP); + assert.strictEqual(listeners[1][kListener], NOOP); + }); + it('supports the `removeEventListener` method', () => { const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); @@ -2257,6 +2296,14 @@ describe('WebSocket', () => { ws.removeEventListener('message', NOOP); assert.deepStrictEqual(ws.listeners('message'), [NOOP]); + + ws.onclose = NOOP; + + assert.strictEqual(ws.listeners('close')[0][kListener], NOOP); + + ws.removeEventListener('close', NOOP); + + assert.strictEqual(ws.listeners('close')[0][kListener], NOOP); }); it('wraps text data in a `MessageEvent`', (done) => { From 6756cf58081c34388d5a523447988aecdee5e2fa Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 16 Jul 2021 19:18:07 +0200 Subject: [PATCH 079/328] [fix] Return `null` if the event handler is not set Make the `onclose`, `onerror`, `onmessage`, and `onopen` getters return `null` instead of `undefined` if the event handler is not set. --- lib/websocket.js | 10 +++++----- test/websocket.test.js | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 5b5ecd3cf..45a643f22 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -124,7 +124,7 @@ class WebSocket extends EventEmitter { */ /* istanbul ignore next */ get onclose() { - return undefined; + return null; } /** @@ -132,7 +132,7 @@ class WebSocket extends EventEmitter { */ /* istanbul ignore next */ get onerror() { - return undefined; + return null; } /** @@ -140,7 +140,7 @@ class WebSocket extends EventEmitter { */ /* istanbul ignore next */ get onopen() { - return undefined; + return null; } /** @@ -148,7 +148,7 @@ class WebSocket extends EventEmitter { */ /* istanbul ignore next */ get onmessage() { - return undefined; + return null; } /** @@ -528,7 +528,7 @@ Object.defineProperty(WebSocket.prototype, 'CLOSED', { if (listener[kForOnEventAttribute]) return listener[kListener]; } - return undefined; + return null; }, set(handler) { for (const listener of this.listeners(method)) { diff --git a/test/websocket.test.js b/test/websocket.test.js index 3883d855a..0841a97ae 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2097,10 +2097,10 @@ describe('WebSocket', () => { const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); - assert.strictEqual(ws.onmessage, undefined); - assert.strictEqual(ws.onclose, undefined); - assert.strictEqual(ws.onerror, undefined); - assert.strictEqual(ws.onopen, undefined); + assert.strictEqual(ws.onmessage, null); + assert.strictEqual(ws.onclose, null); + assert.strictEqual(ws.onerror, null); + assert.strictEqual(ws.onopen, null); ws.onmessage = NOOP; ws.onerror = NOOP; @@ -2114,7 +2114,7 @@ describe('WebSocket', () => { ws.onmessage = 'foo'; - assert.strictEqual(ws.onmessage, undefined); + assert.strictEqual(ws.onmessage, null); assert.strictEqual(ws.listenerCount('onmessage'), 0); }); @@ -2149,7 +2149,7 @@ describe('WebSocket', () => { ws.on('open', NOOP); assert.deepStrictEqual(ws.listeners('open'), [NOOP]); - assert.strictEqual(ws.onopen, undefined); + assert.strictEqual(ws.onopen, null); }); it("doesn't remove listeners added with `on`", () => { @@ -2214,7 +2214,7 @@ describe('WebSocket', () => { assert.strictEqual(listeners.length, 1); assert.strictEqual(listeners[0][kListener], NOOP); - assert.strictEqual(ws.onopen, undefined); + assert.strictEqual(ws.onopen, null); }); it("doesn't remove listeners added with `addEventListener`", () => { From bd7febb685caaa240667cab8091d7af6128a62a9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 19 Jul 2021 09:48:40 +0200 Subject: [PATCH 080/328] [minor] Fix nits --- lib/event-target.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/event-target.js b/lib/event-target.js index b135463d4..4cf076477 100644 --- a/lib/event-target.js +++ b/lib/event-target.js @@ -122,16 +122,12 @@ const EventTarget = { * @param {Function} listener The listener to add * @param {Object} [options] An options object specifies characteristics about * the event listener - * @param {Boolean} [options.once=false] A `Boolean`` indicating that the + * @param {Boolean} [options.once=false] A `Boolean` indicating that the * listener should be invoked at most once after being added. If `true`, * the listener would be automatically removed when invoked. * @public */ - addEventListener( - type, - listener, - options = { once: false, [kForOnEventAttribute]: false } - ) { + addEventListener(type, listener, options = {}) { let wrapper; if (type === 'message') { @@ -155,7 +151,7 @@ const EventTarget = { return; } - wrapper[kForOnEventAttribute] = options[kForOnEventAttribute]; + wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; wrapper[kListener] = listener; if (options.once) { From 21e65004e0a0fc00f45bb428a8ec548f0a561cc0 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 20 Jul 2021 12:39:46 +0200 Subject: [PATCH 081/328] [major] Overhaul event classes - Remove non-standard `OpenEvent` class. - Make properties read-only. - Update constructor signatures to match the ones defined by the HTML standard. --- lib/event-target.js | 203 +++++++++++++++++++++--------- lib/websocket.js | 4 +- test/event-target.test.js | 253 ++++++++++++++++++++++++++++++++++++++ test/websocket.test.js | 61 +++++---- 4 files changed, 437 insertions(+), 84 deletions(-) create mode 100644 test/event-target.test.js diff --git a/lib/event-target.js b/lib/event-target.js index 4cf076477..d5abd83a0 100644 --- a/lib/event-target.js +++ b/lib/event-target.js @@ -2,112 +2,171 @@ const { kForOnEventAttribute, kListener } = require('./constants'); +const kCode = Symbol('kCode'); +const kData = Symbol('kData'); +const kError = Symbol('kError'); +const kMessage = Symbol('kMessage'); +const kReason = Symbol('kReason'); +const kTarget = Symbol('kTarget'); +const kType = Symbol('kType'); +const kWasClean = Symbol('kWasClean'); + /** * Class representing an event. - * - * @private */ class Event { /** * Create a new `Event`. * * @param {String} type The name of the event - * @param {Object} target A reference to the target to which the event was - * dispatched + * @throws {TypeError} If the `type` argument is not specified */ - constructor(type, target) { - this.target = target; - this.type = type; + constructor(type) { + this[kTarget] = null; + this[kType] = type; } -} -/** - * Class representing a message event. - * - * @extends Event - * @private - */ -class MessageEvent extends Event { /** - * Create a new `MessageEvent`. - * - * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @type {*} */ - constructor(data, target) { - super('message', target); + get target() { + return this[kTarget]; + } - this.data = data; + /** + * @type {String} + */ + get type() { + return this[kType]; } } +Object.defineProperty(Event.prototype, 'target', { enumerable: true }); +Object.defineProperty(Event.prototype, 'type', { enumerable: true }); + /** * Class representing a close event. * * @extends Event - * @private */ class CloseEvent extends Event { /** * Create a new `CloseEvent`. * - * @param {Number} code The status code explaining why the connection is being - * closed - * @param {String} reason A human-readable string explaining why the - * connection is closing - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {Number} [options.code=0] The status code explaining why the + * connection was closed + * @param {String} [options.reason=''] A human-readable string explaining why + * the connection was closed + * @param {Boolean} [options.wasClean=false] Indicates whether or not the + * connection was cleanly closed */ - constructor(code, reason, target) { - super('close', target); + constructor(type, options = {}) { + super(type); - this.wasClean = target._closeFrameReceived && target._closeFrameSent; - this.reason = reason; - this.code = code; + this[kCode] = options.code === undefined ? 0 : options.code; + this[kReason] = options.reason === undefined ? '' : options.reason; + this[kWasClean] = options.wasClean === undefined ? false : options.wasClean; + } + + /** + * @type {Number} + */ + get code() { + return this[kCode]; + } + + /** + * @type {String} + */ + get reason() { + return this[kReason]; + } + + /** + * @type {Boolean} + */ + get wasClean() { + return this[kWasClean]; } } +Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true }); + /** - * Class representing an open event. + * Class representing an error event. * * @extends Event - * @private */ -class OpenEvent extends Event { +class ErrorEvent extends Event { /** - * Create a new `OpenEvent`. + * Create a new `ErrorEvent`. * - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.error=null] The error that generated this event + * @param {String} [options.message=''] The error message + */ + constructor(type, options = {}) { + super(type); + + this[kError] = options.error === undefined ? null : options.error; + this[kMessage] = options.message === undefined ? '' : options.message; + } + + /** + * @type {*} + */ + get error() { + return this[kError]; + } + + /** + * @type {String} */ - constructor(target) { - super('open', target); + get message() { + return this[kMessage]; } } +Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true }); +Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true }); + /** - * Class representing an error event. + * Class representing a message event. * * @extends Event - * @private */ -class ErrorEvent extends Event { +class MessageEvent extends Event { /** - * Create a new `ErrorEvent`. + * Create a new `MessageEvent`. * - * @param {Object} error The error that generated this event - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.data=null] The message content */ - constructor(error, target) { - super('error', target); + constructor(type, options = {}) { + super(type); + + this[kData] = options.data === undefined ? null : options.data; + } - this.message = error.message; - this.error = error; + /** + * @type {*} + */ + get data() { + return this[kData]; } } +Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true }); + /** * This provides methods for emulating the `EventTarget` interface. It's not * meant to be used directly. @@ -132,20 +191,40 @@ const EventTarget = { if (type === 'message') { wrapper = function onMessage(data, isBinary) { - const event = new MessageEvent(isBinary ? data : data.toString(), this); + const event = new MessageEvent('message', { + data: isBinary ? data : data.toString() + }); + + event[kTarget] = this; listener.call(this, event); }; } else if (type === 'close') { wrapper = function onClose(code, message) { - listener.call(this, new CloseEvent(code, message.toString(), this)); + const event = new CloseEvent('close', { + code, + reason: message.toString(), + wasClean: this._closeFrameReceived && this._closeFrameSent + }); + + event[kTarget] = this; + listener.call(this, event); }; } else if (type === 'error') { wrapper = function onError(error) { - listener.call(this, new ErrorEvent(error, this)); + const event = new ErrorEvent('error', { + error, + message: error.message + }); + + event[kTarget] = this; + listener.call(this, event); }; } else if (type === 'open') { wrapper = function onOpen() { - listener.call(this, new OpenEvent(this)); + const event = new Event('open'); + + event[kTarget] = this; + listener.call(this, event); }; } else { return; @@ -178,4 +257,10 @@ const EventTarget = { } }; -module.exports = EventTarget; +module.exports = { + CloseEvent, + ErrorEvent, + Event, + EventTarget, + MessageEvent +}; diff --git a/lib/websocket.js b/lib/websocket.js index 45a643f22..3b2bacd28 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -21,7 +21,9 @@ const { kWebSocket, NOOP } = require('./constants'); -const { addEventListener, removeEventListener } = require('./event-target'); +const { + EventTarget: { addEventListener, removeEventListener } +} = require('./event-target'); const { format, parse } = require('./extension'); const { toBuffer } = require('./buffer-util'); diff --git a/test/event-target.test.js b/test/event-target.test.js new file mode 100644 index 000000000..5caaa5c27 --- /dev/null +++ b/test/event-target.test.js @@ -0,0 +1,253 @@ +'use strict'; + +const assert = require('assert'); + +const { + CloseEvent, + ErrorEvent, + Event, + MessageEvent +} = require('../lib/event-target'); + +describe('Event', () => { + describe('#ctor', () => { + it('takes a `type` argument', () => { + const event = new Event('foo'); + + assert.strictEqual(event.type, 'foo'); + }); + }); + + describe('Properties', () => { + describe('`target`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + Event.prototype, + 'target' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('defaults to `null`', () => { + const event = new Event('foo'); + + assert.strictEqual(event.target, null); + }); + }); + + describe('`type`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + Event.prototype, + 'type' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + }); + }); +}); + +describe('CloseEvent', () => { + it('inherits from `Event`', () => { + assert.ok(CloseEvent.prototype instanceof Event); + }); + + describe('#ctor', () => { + it('takes a `type` argument', () => { + const event = new CloseEvent('foo'); + + assert.strictEqual(event.type, 'foo'); + }); + + it('takes an optional `options` argument', () => { + const event = new CloseEvent('close', { + code: 1000, + reason: 'foo', + wasClean: true + }); + + assert.strictEqual(event.type, 'close'); + assert.strictEqual(event.code, 1000); + assert.strictEqual(event.reason, 'foo'); + assert.strictEqual(event.wasClean, true); + }); + }); + + describe('Properties', () => { + describe('`code`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + CloseEvent.prototype, + 'code' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('defaults to 0', () => { + const event = new CloseEvent('close'); + + assert.strictEqual(event.code, 0); + }); + }); + + describe('`reason`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + CloseEvent.prototype, + 'reason' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('defaults to an empty string', () => { + const event = new CloseEvent('close'); + + assert.strictEqual(event.reason, ''); + }); + }); + + describe('`wasClean`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + CloseEvent.prototype, + 'wasClean' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('defaults to false', () => { + const event = new CloseEvent('close'); + + assert.strictEqual(event.wasClean, false); + }); + }); + }); +}); + +describe('ErrorEvent', () => { + it('inherits from `Event`', () => { + assert.ok(ErrorEvent.prototype instanceof Event); + }); + + describe('#ctor', () => { + it('takes a `type` argument', () => { + const event = new ErrorEvent('foo'); + + assert.strictEqual(event.type, 'foo'); + }); + + it('takes an optional `options` argument', () => { + const error = new Error('Oops'); + const event = new ErrorEvent('error', { error, message: error.message }); + + assert.strictEqual(event.type, 'error'); + assert.strictEqual(event.error, error); + assert.strictEqual(event.message, error.message); + }); + }); + + describe('Properties', () => { + describe('`error`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + ErrorEvent.prototype, + 'error' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('defaults to `null`', () => { + const event = new ErrorEvent('error'); + + assert.strictEqual(event.error, null); + }); + }); + + describe('`message`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + ErrorEvent.prototype, + 'message' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('defaults to an empty string', () => { + const event = new ErrorEvent('error'); + + assert.strictEqual(event.message, ''); + }); + }); + }); +}); + +describe('MessageEvent', () => { + it('inherits from `Event`', () => { + assert.ok(MessageEvent.prototype instanceof Event); + }); + + describe('#ctor', () => { + it('takes a `type` argument', () => { + const event = new MessageEvent('foo'); + + assert.strictEqual(event.type, 'foo'); + }); + + it('takes an optional `options` argument', () => { + const event = new MessageEvent('message', { data: 'bar' }); + + assert.strictEqual(event.type, 'message'); + assert.strictEqual(event.data, 'bar'); + }); + }); + + describe('Properties', () => { + describe('`data`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + MessageEvent.prototype, + 'data' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('defaults to `null`', () => { + const event = new MessageEvent('message'); + + assert.strictEqual(event.data, null); + }); + }); + }); +}); diff --git a/test/websocket.test.js b/test/websocket.test.js index 0841a97ae..82a25bfce 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -11,6 +11,12 @@ const fs = require('fs'); const { URL } = require('url'); const WebSocket = require('..'); +const { + CloseEvent, + ErrorEvent, + Event, + MessageEvent +} = require('../lib/event-target'); const { EMPTY_BUFFER, GUID, kListener, NOOP } = require('../lib/constants'); class CustomAgent extends http.Agent { @@ -2315,8 +2321,9 @@ describe('WebSocket', () => { ws.close(); }); - ws.addEventListener('message', (messageEvent) => { - assert.strictEqual(messageEvent.data, 'hi'); + ws.addEventListener('message', (event) => { + assert.ok(event instanceof MessageEvent); + assert.strictEqual(event.data, 'hi'); wss.close(done); }); }); @@ -2332,10 +2339,11 @@ describe('WebSocket', () => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.addEventListener('close', (closeEvent) => { - assert.ok(closeEvent.wasClean); - assert.strictEqual(closeEvent.reason, ''); - assert.strictEqual(closeEvent.code, 1000); + ws.addEventListener('close', (event) => { + assert.ok(event instanceof CloseEvent); + assert.ok(event.wasClean); + assert.strictEqual(event.reason, ''); + assert.strictEqual(event.code, 1000); wss.close(done); }); }); @@ -2347,10 +2355,11 @@ describe('WebSocket', () => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.addEventListener('close', (closeEvent) => { - assert.ok(closeEvent.wasClean); - assert.strictEqual(closeEvent.reason, 'some daft reason'); - assert.strictEqual(closeEvent.code, 4000); + ws.addEventListener('close', (event) => { + assert.ok(event instanceof CloseEvent); + assert.ok(event.wasClean); + assert.strictEqual(event.reason, 'some daft reason'); + assert.strictEqual(event.code, 4000); wss.close(done); }); }); @@ -2363,25 +2372,29 @@ describe('WebSocket', () => { const err = new Error('forced'); const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.addEventListener('open', (openEvent) => { - assert.strictEqual(openEvent.type, 'open'); - assert.strictEqual(openEvent.target, ws); + ws.addEventListener('open', (event) => { + assert.ok(event instanceof Event); + assert.strictEqual(event.type, 'open'); + assert.strictEqual(event.target, ws); }); - ws.addEventListener('message', (messageEvent) => { - assert.strictEqual(messageEvent.type, 'message'); - assert.strictEqual(messageEvent.target, ws); + ws.addEventListener('message', (event) => { + assert.ok(event instanceof MessageEvent); + assert.strictEqual(event.type, 'message'); + assert.strictEqual(event.target, ws); ws.close(); }); - ws.addEventListener('close', (closeEvent) => { - assert.strictEqual(closeEvent.type, 'close'); - assert.strictEqual(closeEvent.target, ws); + ws.addEventListener('close', (event) => { + assert.ok(event instanceof CloseEvent); + assert.strictEqual(event.type, 'close'); + assert.strictEqual(event.target, ws); ws.emit('error', err); }); - ws.addEventListener('error', (errorEvent) => { - assert.strictEqual(errorEvent.message, 'forced'); - assert.strictEqual(errorEvent.type, 'error'); - assert.strictEqual(errorEvent.target, ws); - assert.strictEqual(errorEvent.error, err); + ws.addEventListener('error', (event) => { + assert.ok(event instanceof ErrorEvent); + assert.strictEqual(event.message, 'forced'); + assert.strictEqual(event.type, 'error'); + assert.strictEqual(event.target, ws); + assert.strictEqual(event.error, err); wss.close(done); }); From fc4024898c0da9b587c50a4a2d02d3ff88cddf06 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 28 Jul 2021 20:10:09 +0200 Subject: [PATCH 082/328] [dist] 8.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 74558562c..2334d02a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.5.3", + "version": "8.0.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 04e74a1f8c27f639c4c761e92208133f26429144 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 3 Aug 2021 08:40:48 +0200 Subject: [PATCH 083/328] [license] Fix license text --- LICENSE | 2 -- 1 file changed, 2 deletions(-) diff --git a/LICENSE b/LICENSE index a145cd1df..65ff176bf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,3 @@ -The MIT License (MIT) - Copyright (c) 2011 Einar Otto Stangvik Permission is hereby granted, free of charge, to any person obtaining a copy From 6a72da3e636ea658a22b3fe80acb07336683c3c3 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 6 Aug 2021 18:49:48 +0200 Subject: [PATCH 084/328] [fix] Do not rely on undocumented behavior Use the chunk returned by `socket.read()` to handle the buffered data instead of relying on a `'data'` event emitted after the `'close'` event. Refs: https://github.com/nodejs/node/pull/39639 --- lib/websocket.js | 17 ++++++--- test/websocket.test.js | 82 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 3b2bacd28..670921eec 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -1031,10 +1031,13 @@ function socketOnClose() { const websocket = this[kWebSocket]; this.removeListener('close', socketOnClose); + this.removeListener('data', socketOnData); this.removeListener('end', socketOnEnd); websocket._readyState = WebSocket.CLOSING; + let chunk; + // // The close frame might not have been received or the `'end'` event emitted, // for example, if the socket was destroyed due to an error. Ensure that the @@ -1042,13 +1045,19 @@ function socketOnClose() { // it. If the readable side of the socket is in flowing mode then there is no // buffered data as everything has been already written and `readable.read()` // will return `null`. If instead, the socket is paused, any possible buffered - // data will be read as a single chunk and emitted synchronously in a single - // `'data'` event. + // data will be read as a single chunk. // - websocket._socket.read(); + if ( + !this._readableState.endEmitted && + !websocket._closeFrameReceived && + !websocket._receiver._writableState.errorEmitted && + (chunk = websocket._socket.read()) !== null + ) { + websocket._receiver.write(chunk); + } + websocket._receiver.end(); - this.removeListener('data', socketOnData); this[kWebSocket] = undefined; clearTimeout(websocket._closeTimer); diff --git a/test/websocket.test.js b/test/websocket.test.js index 82a25bfce..e8e02443c 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -10,6 +10,7 @@ const tls = require('tls'); const fs = require('fs'); const { URL } = require('url'); +const Sender = require('../lib/sender'); const WebSocket = require('..'); const { CloseEvent, @@ -2942,15 +2943,21 @@ describe('WebSocket', () => { }); }); - it('consumes all received data when connection is closed abnormally', (done) => { + it('consumes all received data when connection is closed (1/2)', (done) => { const wss = new WebSocket.Server( { perMessageDeflate: { threshold: 0 }, port: 0 }, () => { - const ws = new WebSocket(`ws://localhost:${wss.address().port}`); const messages = []; + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws._socket.on('close', () => { + assert.strictEqual(ws._receiver._state, 5); + }); + }); ws.on('message', (message, isBinary) => { assert.ok(!isBinary); @@ -2973,6 +2980,77 @@ describe('WebSocket', () => { }); }); + it('consumes all received data when connection is closed (2/2)', (done) => { + const payload1 = Buffer.alloc(15 * 1024); + const payload2 = Buffer.alloc(1); + + const opts = { + fin: true, + opcode: 0x02, + mask: false, + readOnly: false + }; + + const list = [ + ...Sender.frame(payload1, { rsv1: false, ...opts }), + ...Sender.frame(payload2, { rsv1: true, ...opts }) + ]; + + for (let i = 0; i < 399; i++) { + list.push(list[list.length - 2], list[list.length - 1]); + } + + const data = Buffer.concat(list); + + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const messageLengths = []; + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws._socket.prependListener('close', () => { + assert.strictEqual(ws._receiver._state, 5); + assert.strictEqual(ws._socket._readableState.length, 3); + }); + + const push = ws._socket.push; + + ws._socket.push = (data) => { + ws._socket.push = push; + ws._socket.push(data); + ws.terminate(); + }; + + // This hack is used because there is no guarantee that more than + // 16 KiB will be sent as a single TCP packet. + push.call(ws._socket, data); + + wss.clients + .values() + .next() + .value.send(payload2, { compress: false }); + }); + + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + messageLengths.push(message.length); + }); + + ws.on('close', (code) => { + assert.strictEqual(code, 1006); + assert.strictEqual(messageLengths.length, 402); + assert.strictEqual(messageLengths[0], 15360); + assert.strictEqual(messageLengths[messageLengths.length - 1], 1); + wss.close(done); + }); + } + ); + }); + it('handles a close frame received while compressing data', (done) => { const wss = new WebSocket.Server( { From c95e695d35b1a469704035bc94fa2efee43d86cc Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 6 Aug 2021 21:15:41 +0200 Subject: [PATCH 085/328] [fix] Fix misleading error message Use the correct error message if the server sends an empty subprotocol name. --- lib/websocket.js | 2 +- test/websocket.test.js | 32 +++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 670921eec..63cc5b389 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -771,7 +771,7 @@ function initAsClient(websocket, address, protocols, options) { const serverProt = res.headers['sec-websocket-protocol']; let protError; - if (serverProt) { + if (serverProt !== undefined) { if (!protocolSet.size) { protError = 'Server sent a subprotocol but none was requested'; } else if (!protocolSet.has(serverProt)) { diff --git a/test/websocket.test.js b/test/websocket.test.js index e8e02443c..be25329f5 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -895,7 +895,7 @@ describe('WebSocket', () => { }); }); - it('fails if server sends an invalid subprotocol', (done) => { + it('fails if server sends an invalid subprotocol (1/2)', (done) => { const wss = new WebSocket.Server({ handleProtocols: () => 'baz', server @@ -914,6 +914,36 @@ describe('WebSocket', () => { }); }); + it('fails if server sends an invalid subprotocol (2/2)', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Protocol:\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, [ + 'foo', + 'bar' + ]); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Server sent an invalid subprotocol'); + ws.on('close', () => done()); + }); + }); + it('fails if server sends no subprotocol', (done) => { const wss = new WebSocket.Server({ handleProtocols() {}, From fd47c961fb99e7647a76a41d53784972959f4229 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 7 Aug 2021 08:13:03 +0200 Subject: [PATCH 086/328] [test] Move code block closer to where it is used --- test/websocket.test.js | 45 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index be25329f5..51da54f76 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -3011,27 +3011,6 @@ describe('WebSocket', () => { }); it('consumes all received data when connection is closed (2/2)', (done) => { - const payload1 = Buffer.alloc(15 * 1024); - const payload2 = Buffer.alloc(1); - - const opts = { - fin: true, - opcode: 0x02, - mask: false, - readOnly: false - }; - - const list = [ - ...Sender.frame(payload1, { rsv1: false, ...opts }), - ...Sender.frame(payload2, { rsv1: true, ...opts }) - ]; - - for (let i = 0; i < 399; i++) { - list.push(list[list.length - 2], list[list.length - 1]); - } - - const data = Buffer.concat(list); - const wss = new WebSocket.Server( { perMessageDeflate: true, @@ -3049,15 +3028,37 @@ describe('WebSocket', () => { const push = ws._socket.push; + // Override `ws._socket.push()` to know exactly when data is + // received and call `ws.terminate()` immediately after that without + // relying on a timer. ws._socket.push = (data) => { ws._socket.push = push; ws._socket.push(data); ws.terminate(); }; + const payload1 = Buffer.alloc(15 * 1024); + const payload2 = Buffer.alloc(1); + + const opts = { + fin: true, + opcode: 0x02, + mask: false, + readOnly: false + }; + + const list = [ + ...Sender.frame(payload1, { rsv1: false, ...opts }), + ...Sender.frame(payload2, { rsv1: true, ...opts }) + ]; + + for (let i = 0; i < 399; i++) { + list.push(list[list.length - 2], list[list.length - 1]); + } + // This hack is used because there is no guarantee that more than // 16 KiB will be sent as a single TCP packet. - push.call(ws._socket, data); + push.call(ws._socket, Buffer.concat(list)); wss.clients .values() From 7f0b5c42bbec7a25ee5c559c04e82c27b0953b70 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 9 Aug 2021 08:56:18 +0200 Subject: [PATCH 087/328] [example] Update uuid to version 8.3.2 --- examples/express-session-parse/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/express-session-parse/package.json b/examples/express-session-parse/package.json index f8cd22e30..406706ce8 100644 --- a/examples/express-session-parse/package.json +++ b/examples/express-session-parse/package.json @@ -6,6 +6,6 @@ "dependencies": { "express": "^4.16.4", "express-session": "^1.16.1", - "uuid": "^3.3.2" + "uuid": "^8.3.2" } } From 1e938f1a739b33d5936c2a02bcddcf14d18e2123 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 11 Aug 2021 08:23:37 +0200 Subject: [PATCH 088/328] [major] Use an options object instead of positional arguments Make the `Receiver` constructor take a single options object argument instead of multiple positional arguments. --- bench/parser.benchmark.js | 6 ++- lib/receiver.js | 22 +++++---- lib/websocket.js | 10 ++-- test/receiver.test.js | 98 +++++++++++++++++++++------------------ 4 files changed, 74 insertions(+), 62 deletions(-) diff --git a/bench/parser.benchmark.js b/bench/parser.benchmark.js index dd97701af..ee4954be4 100644 --- a/bench/parser.benchmark.js +++ b/bench/parser.benchmark.js @@ -36,7 +36,11 @@ const binaryFrame3 = createBinaryFrame(200 * 1024); const binaryFrame4 = createBinaryFrame(1024 * 1024); const suite = new benchmark.Suite(); -const receiver = new Receiver('nodebuffer', {}, true); +const receiver = new Receiver({ + binaryType: 'nodebuffer', + extensions: {}, + isServer: true +}); suite.add('ping frame (5 bytes payload)', { defer: true, diff --git a/lib/receiver.js b/lib/receiver.js index d678d6afa..2cbb48a7d 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -28,20 +28,22 @@ class Receiver extends Writable { /** * Creates a Receiver instance. * - * @param {String} [binaryType=nodebuffer] The type for binary data - * @param {Object} [extensions] An object containing the negotiated extensions - * @param {Boolean} [isServer=false] Specifies whether to operate in client or - * server mode - * @param {Number} [maxPayload=0] The maximum allowed message length + * @param {Object} [options] Options object + * @param {String} [options.binaryType=nodebuffer] The type for binary data + * @param {Object} [options.extensions] An object containing the negotiated + * extensions + * @param {Boolean} [options.isServer=false] Specifies whether to operate in + * client or server mode + * @param {Number} [options.maxPayload=0] The maximum allowed message length */ - constructor(binaryType, extensions, isServer, maxPayload) { + constructor(options = {}) { super(); - this._binaryType = binaryType || BINARY_TYPES[0]; + this._binaryType = options.binaryType || BINARY_TYPES[0]; + this._extensions = options.extensions || {}; + this._isServer = !!options.isServer; + this._maxPayload = options.maxPayload | 0; this[kWebSocket] = undefined; - this._extensions = extensions || {}; - this._isServer = !!isServer; - this._maxPayload = maxPayload | 0; this._bufferedBytes = 0; this._buffers = []; diff --git a/lib/websocket.js b/lib/websocket.js index 63cc5b389..2d7b5c43b 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -184,12 +184,12 @@ class WebSocket extends EventEmitter { * @private */ setSocket(socket, head, maxPayload) { - const receiver = new Receiver( - this.binaryType, - this._extensions, - this._isServer, + const receiver = new Receiver({ + binaryType: this.binaryType, + extensions: this._extensions, + isServer: this._isServer, maxPayload - ); + }); this._sender = new Sender(socket, this._extensions); this._receiver = receiver; diff --git a/test/receiver.test.js b/test/receiver.test.js index 0736c6100..4f03cea02 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -47,7 +47,7 @@ describe('Receiver', () => { }); it('parses a masked text message', (done) => { - const receiver = new Receiver(undefined, {}, true); + const receiver = new Receiver({ isServer: true }); receiver.on('message', (data, isBinary) => { assert.deepStrictEqual(data, Buffer.from('5:::{"name":"echo"}')); @@ -61,7 +61,7 @@ describe('Receiver', () => { }); it('parses a masked text message longer than 125 B', (done) => { - const receiver = new Receiver(undefined, {}, true); + const receiver = new Receiver({ isServer: true }); const msg = Buffer.from('A'.repeat(200)); const list = Sender.frame(msg, { @@ -85,7 +85,7 @@ describe('Receiver', () => { }); it('parses a really long masked text message', (done) => { - const receiver = new Receiver(undefined, {}, true); + const receiver = new Receiver({ isServer: true }); const msg = Buffer.from('A'.repeat(64 * 1024)); const list = Sender.frame(msg, { @@ -108,7 +108,7 @@ describe('Receiver', () => { }); it('parses a 300 B fragmented masked text message', (done) => { - const receiver = new Receiver(undefined, {}, true); + const receiver = new Receiver({ isServer: true }); const msg = Buffer.from('A'.repeat(300)); const fragment1 = msg.slice(0, 150); @@ -142,7 +142,7 @@ describe('Receiver', () => { }); it('parses a ping message', (done) => { - const receiver = new Receiver(undefined, {}, true); + const receiver = new Receiver({ isServer: true }); const msg = Buffer.from('Hello'); const list = Sender.frame(msg, { @@ -175,7 +175,7 @@ describe('Receiver', () => { }); it('parses a 300 B fragmented masked text message with a ping in the middle (1/2)', (done) => { - const receiver = new Receiver(undefined, {}, true); + const receiver = new Receiver({ isServer: true }); const msg = Buffer.from('A'.repeat(300)); const pingMessage = Buffer.from('Hello'); @@ -225,7 +225,7 @@ describe('Receiver', () => { }); it('parses a 300 B fragmented masked text message with a ping in the middle (2/2)', (done) => { - const receiver = new Receiver(undefined, {}, true); + const receiver = new Receiver({ isServer: true }); const msg = Buffer.from('A'.repeat(300)); const pingMessage = Buffer.from('Hello'); @@ -285,7 +285,7 @@ describe('Receiver', () => { }); it('parses a 100 B masked binary message', (done) => { - const receiver = new Receiver(undefined, {}, true); + const receiver = new Receiver({ isServer: true }); const msg = crypto.randomBytes(100); const list = Sender.frame(msg, { @@ -308,7 +308,7 @@ describe('Receiver', () => { }); it('parses a 256 B masked binary message', (done) => { - const receiver = new Receiver(undefined, {}, true); + const receiver = new Receiver({ isServer: true }); const msg = crypto.randomBytes(256); const list = Sender.frame(msg, { @@ -331,7 +331,7 @@ describe('Receiver', () => { }); it('parses a 200 KiB masked binary message', (done) => { - const receiver = new Receiver(undefined, {}, true); + const receiver = new Receiver({ isServer: true }); const msg = crypto.randomBytes(200 * 1024); const list = Sender.frame(msg, { @@ -380,8 +380,10 @@ describe('Receiver', () => { const perMessageDeflate = new PerMessageDeflate(); perMessageDeflate.accept([{}]); - const receiver = new Receiver(undefined, { - 'permessage-deflate': perMessageDeflate + const receiver = new Receiver({ + extensions: { + 'permessage-deflate': perMessageDeflate + } }); const buf = Buffer.from('Hello'); @@ -403,8 +405,10 @@ describe('Receiver', () => { const perMessageDeflate = new PerMessageDeflate(); perMessageDeflate.accept([{}]); - const receiver = new Receiver(undefined, { - 'permessage-deflate': perMessageDeflate + const receiver = new Receiver({ + extensions: { + 'permessage-deflate': perMessageDeflate + } }); const buf1 = Buffer.from('foo'); const buf2 = Buffer.from('bar'); @@ -451,7 +455,7 @@ describe('Receiver', () => { }); it('resets `totalPayloadLength` only on final frame (unfragmented)', (done) => { - const receiver = new Receiver(undefined, {}, false, 10); + const receiver = new Receiver({ maxPayload: 10 }); receiver.on('message', (data, isBinary) => { assert.strictEqual(receiver._totalPayloadLength, 0); @@ -465,7 +469,7 @@ describe('Receiver', () => { }); it('resets `totalPayloadLength` only on final frame (fragmented)', (done) => { - const receiver = new Receiver(undefined, {}, false, 10); + const receiver = new Receiver({ maxPayload: 10 }); receiver.on('message', (data, isBinary) => { assert.strictEqual(receiver._totalPayloadLength, 0); @@ -481,7 +485,7 @@ describe('Receiver', () => { }); it('resets `totalPayloadLength` only on final frame (fragmented + ping)', (done) => { - const receiver = new Receiver(undefined, {}, false, 10); + const receiver = new Receiver({ maxPayload: 10 }); let data; receiver.on('ping', (buf) => { @@ -506,8 +510,10 @@ describe('Receiver', () => { const perMessageDeflate = new PerMessageDeflate(); perMessageDeflate.accept([{}]); - const receiver = new Receiver(undefined, { - 'permessage-deflate': perMessageDeflate + const receiver = new Receiver({ + extensions: { + 'permessage-deflate': perMessageDeflate + } }); const results = []; const push = results.push.bind(results); @@ -549,8 +555,10 @@ describe('Receiver', () => { const perMessageDeflate = new PerMessageDeflate(); perMessageDeflate.accept([{}]); - const receiver = new Receiver(undefined, { - 'permessage-deflate': perMessageDeflate + const receiver = new Receiver({ + extensions: { + 'permessage-deflate': perMessageDeflate + } }); receiver.on('error', (err) => { @@ -675,8 +683,10 @@ describe('Receiver', () => { const perMessageDeflate = new PerMessageDeflate(); perMessageDeflate.accept([{}]); - const receiver = new Receiver(undefined, { - 'permessage-deflate': perMessageDeflate + const receiver = new Receiver({ + extensions: { + 'permessage-deflate': perMessageDeflate + } }); receiver.on('error', (err) => { @@ -711,7 +721,7 @@ describe('Receiver', () => { }); it('emits an error if a frame has the MASK bit off (server mode)', (done) => { - const receiver = new Receiver(undefined, {}, true); + const receiver = new Receiver({ isServer: true }); receiver.on('error', (err) => { assert.ok(err instanceof RangeError); @@ -728,7 +738,7 @@ describe('Receiver', () => { }); it('emits an error if a frame has the MASK bit on (client mode)', (done) => { - const receiver = new Receiver(undefined, {}, false); + const receiver = new Receiver(); receiver.on('error', (err) => { assert.ok(err instanceof RangeError); @@ -806,8 +816,10 @@ describe('Receiver', () => { const perMessageDeflate = new PerMessageDeflate(); perMessageDeflate.accept([{}]); - const receiver = new Receiver(undefined, { - 'permessage-deflate': perMessageDeflate + const receiver = new Receiver({ + extensions: { + 'permessage-deflate': perMessageDeflate + } }); const buf = Buffer.from([0xce, 0xba, 0xe1, 0xbd]); @@ -884,7 +896,7 @@ describe('Receiver', () => { }); it('emits an error if a frame payload length is bigger than `maxPayload`', (done) => { - const receiver = new Receiver(undefined, {}, true, 20 * 1024); + const receiver = new Receiver({ isServer: true, maxPayload: 20 * 1024 }); const msg = crypto.randomBytes(200 * 1024); const list = Sender.frame(msg, { @@ -912,14 +924,11 @@ describe('Receiver', () => { const perMessageDeflate = new PerMessageDeflate({}, false, 25); perMessageDeflate.accept([{}]); - const receiver = new Receiver( - undefined, - { - 'permessage-deflate': perMessageDeflate - }, - false, - 25 - ); + const receiver = new Receiver({ + extensions: { 'permessage-deflate': perMessageDeflate }, + isServer: false, + maxPayload: 25 + }); const buf = Buffer.from('A'.repeat(50)); receiver.on('error', (err) => { @@ -942,14 +951,11 @@ describe('Receiver', () => { const perMessageDeflate = new PerMessageDeflate({}, false, 25); perMessageDeflate.accept([{}]); - const receiver = new Receiver( - undefined, - { - 'permessage-deflate': perMessageDeflate - }, - false, - 25 - ); + const receiver = new Receiver({ + extensions: { 'permessage-deflate': perMessageDeflate }, + isServer: false, + maxPayload: 25 + }); const buf = Buffer.from('A'.repeat(15)); receiver.on('error', (err) => { @@ -1002,7 +1008,7 @@ describe('Receiver', () => { }); it("honors the 'arraybuffer' binary type", (done) => { - const receiver = new Receiver('arraybuffer'); + const receiver = new Receiver({ binaryType: 'arraybuffer' }); const frags = [ crypto.randomBytes(19221), crypto.randomBytes(954), @@ -1028,7 +1034,7 @@ describe('Receiver', () => { }); it("honors the 'fragments' binary type", (done) => { - const receiver = new Receiver('fragments'); + const receiver = new Receiver({ binaryType: 'fragments' }); const frags = [ crypto.randomBytes(17), crypto.randomBytes(419872), From 9bd3bd1251a4342511142a2cd32ce4a92b39a44c Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Wed, 11 Aug 2021 22:35:27 +0900 Subject: [PATCH 089/328] [minor] Fix typo (#1929) --- lib/receiver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/receiver.js b/lib/receiver.js index 2cbb48a7d..fe0703b2d 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -427,7 +427,7 @@ class Receiver extends Writable { if (data.length) { // - // This message is not compressed so its lenght is the sum of the payload + // This message is not compressed so its length is the sum of the payload // length of all fragments. // this._messageLength = this._totalPayloadLength; From d21c81034f53be7c5c292c5d2486eb3db41bc31a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 11 Aug 2021 15:48:52 +0200 Subject: [PATCH 090/328] [feature] Add ability to skip UTF-8 validation (#1928) Add the `skipUTF8Validation` option to skip UTF-8 validation for text and close messages. Refs: https://github.com/websockets/ws/issues/1878 Closes #1924 --- bench/parser.benchmark.js | 3 ++- doc/ws.md | 6 ++++++ lib/receiver.js | 7 +++++-- lib/websocket-server.js | 8 +++++++- lib/websocket.js | 18 ++++++++++++++---- test/receiver.test.js | 24 ++++++++++++++++++++++++ 6 files changed, 58 insertions(+), 8 deletions(-) diff --git a/bench/parser.benchmark.js b/bench/parser.benchmark.js index ee4954be4..a6e359d05 100644 --- a/bench/parser.benchmark.js +++ b/bench/parser.benchmark.js @@ -39,7 +39,8 @@ const suite = new benchmark.Suite(); const receiver = new Receiver({ binaryType: 'nodebuffer', extensions: {}, - isServer: true + isServer: true, + skipUTF8Validation: false }); suite.add('ping frame (5 bytes payload)', { diff --git a/doc/ws.md b/doc/ws.md index d657ceeee..22c712034 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -78,6 +78,9 @@ This class represents a WebSocket server. It extends the `EventEmitter`. - `clientTracking` {Boolean} Specifies whether or not to track clients. - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate. - `maxPayload` {Number} The maximum allowed message size in bytes. + - `skipUTF8Validation` {Boolean} Specifies whether or not to skip UTF-8 + validation for text and close messages. Defaults to `false`. Set to `true` + only if clients are trusted. - `callback` {Function} Create a new server instance. One and only one of `port`, `server` or `noServer` @@ -273,6 +276,9 @@ This class represents a WebSocket. It extends the `EventEmitter`. - `origin` {String} Value of the `Origin` or `Sec-WebSocket-Origin` header depending on the `protocolVersion`. - `maxPayload` {Number} The maximum allowed message size in bytes. + - `skipUTF8Validation` {Boolean} Specifies whether or not to skip UTF-8 + validation for text and close messages. Defaults to `false`. Set to `true` + only if the server is trusted. - Any other option allowed in [http.request()][] or [https.request()][]. Options given do not have any effect if parsed from the URL given with the `address` parameter. diff --git a/lib/receiver.js b/lib/receiver.js index fe0703b2d..e11e26618 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -35,6 +35,8 @@ class Receiver extends Writable { * @param {Boolean} [options.isServer=false] Specifies whether to operate in * client or server mode * @param {Number} [options.maxPayload=0] The maximum allowed message length + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages */ constructor(options = {}) { super(); @@ -43,6 +45,7 @@ class Receiver extends Writable { this._extensions = options.extensions || {}; this._isServer = !!options.isServer; this._maxPayload = options.maxPayload | 0; + this._skipUTF8Validation = !!options.skipUTF8Validation; this[kWebSocket] = undefined; this._bufferedBytes = 0; @@ -505,7 +508,7 @@ class Receiver extends Writable { } else { const buf = concat(fragments, messageLength); - if (!isValidUTF8(buf)) { + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { this._loop = false; return error( Error, @@ -560,7 +563,7 @@ class Receiver extends Writable { const buf = data.slice(2); - if (!isValidUTF8(buf)) { + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { return error( Error, 'invalid UTF-8 sequence', diff --git a/lib/websocket-server.js b/lib/websocket-server.js index b147fe5d5..3c7939f28 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -46,6 +46,8 @@ class WebSocketServer extends EventEmitter { * @param {Number} [options.port] The port where to bind the server * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S * server to use + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @param {Function} [options.verifyClient] A hook to reject connections * @param {Function} [callback] A listener for the `listening` event */ @@ -54,6 +56,7 @@ class WebSocketServer extends EventEmitter { options = { maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: false, handleProtocols: null, clientTracking: true, @@ -386,7 +389,10 @@ class WebSocketServer extends EventEmitter { socket.write(headers.concat('\r\n').join('\r\n')); socket.removeListener('error', socketOnError); - ws.setSocket(socket, head, this.options.maxPayload); + ws.setSocket(socket, head, { + maxPayload: this.options.maxPayload, + skipUTF8Validation: this.options.skipUTF8Validation + }); if (this.clients) { this.clients.add(ws); diff --git a/lib/websocket.js b/lib/websocket.js index 2d7b5c43b..8d8f913e4 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -180,15 +180,19 @@ class WebSocket extends EventEmitter { * @param {(net.Socket|tls.Socket)} socket The network socket between the * server and client * @param {Buffer} head The first packet of the upgraded stream - * @param {Number} [maxPayload=0] The maximum allowed message size + * @param {Object} options Options object + * @param {Number} [options.maxPayload=0] The maximum allowed message size + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ - setSocket(socket, head, maxPayload) { + setSocket(socket, head, options) { const receiver = new Receiver({ binaryType: this.binaryType, extensions: this._extensions, isServer: this._isServer, - maxPayload + maxPayload: options.maxPayload, + skipUTF8Validation: options.skipUTF8Validation }); this._sender = new Sender(socket, this._extensions); @@ -575,12 +579,15 @@ module.exports = WebSocket; * redirects * @param {Number} [options.maxRedirects=10] The maximum number of redirects * allowed + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ function initAsClient(websocket, address, protocols, options) { const opts = { protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: true, followRedirects: false, maxRedirects: 10, @@ -832,7 +839,10 @@ function initAsClient(websocket, address, protocols, options) { perMessageDeflate; } - websocket.setSocket(socket, head, opts.maxPayload); + websocket.setSocket(socket, head, { + maxPayload: opts.maxPayload, + skipUTF8Validation: opts.skipUTF8Validation + }); }); } diff --git a/test/receiver.test.js b/test/receiver.test.js index 4f03cea02..7ee35f740 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -1059,4 +1059,28 @@ describe('Receiver', () => { }).forEach((buf) => receiver.write(buf)); }); }); + + it('honors the `skipUTF8Validation` option (1/2)', (done) => { + const receiver = new Receiver({ skipUTF8Validation: true }); + + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, Buffer.from([0xf8])); + assert.ok(!isBinary); + done(); + }); + + receiver.write(Buffer.from([0x81, 0x01, 0xf8])); + }); + + it('honors the `skipUTF8Validation` option (2/2)', (done) => { + const receiver = new Receiver({ skipUTF8Validation: true }); + + receiver.on('conclude', (code, data) => { + assert.strictEqual(code, 1000); + assert.deepStrictEqual(data, Buffer.from([0xf8])); + done(); + }); + + receiver.write(Buffer.from([0x88, 0x03, 0x03, 0xe8, 0xf8])); + }); }); From 142f0911b550f85741297f68f33af0dc72a7f043 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 11 Aug 2021 19:28:53 +0200 Subject: [PATCH 091/328] [dist] 8.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2334d02a4..d163e70e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.0.0", + "version": "8.1.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From f38247e5e00b8d18e736a9240388dbcdb1a474c9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 12 Aug 2021 11:14:30 +0200 Subject: [PATCH 092/328] [doc] Sort options alphabetically --- doc/ws.md | 32 ++++++++++++++++---------------- lib/permessage-deflate.js | 16 ++++++++-------- lib/sender.js | 14 +++++++------- lib/websocket.js | 20 ++++++++++---------- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 22c712034..08d70cc7a 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -64,23 +64,23 @@ This class represents a WebSocket server. It extends the `EventEmitter`. ### new WebSocketServer(options[, callback]) - `options` {Object} - - `host` {String} The hostname where to bind the server. - - `port` {Number} The port where to bind the server. - `backlog` {Number} The maximum length of the queue of pending connections. - - `server` {http.Server|https.Server} A pre-created Node.js HTTP/S server. - - `verifyClient` {Function} A function which can be used to validate incoming - connections. See description below. (Usage is discouraged: see - [Issue #337](https://github.com/websockets/ws/issues/377#issuecomment-462152231)) + - `clientTracking` {Boolean} Specifies whether or not to track clients. - `handleProtocols` {Function} A function which can be used to handle the WebSocket subprotocols. See description below. - - `path` {String} Accept only connections matching this path. + - `host` {String} The hostname where to bind the server. + - `maxPayload` {Number} The maximum allowed message size in bytes. - `noServer` {Boolean} Enable no server mode. - - `clientTracking` {Boolean} Specifies whether or not to track clients. + - `path` {String} Accept only connections matching this path. - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate. - - `maxPayload` {Number} The maximum allowed message size in bytes. + - `port` {Number} The port where to bind the server. + - `server` {http.Server|https.Server} A pre-created Node.js HTTP/S server. - `skipUTF8Validation` {Boolean} Specifies whether or not to skip UTF-8 validation for text and close messages. Defaults to `false`. Set to `true` only if clients are trusted. + - `verifyClient` {Function} A function which can be used to validate incoming + connections. See description below. (Usage is discouraged: see + [Issue #337](https://github.com/websockets/ws/issues/377#issuecomment-462152231)) - `callback` {Function} Create a new server instance. One and only one of `port`, `server` or `noServer` @@ -269,13 +269,13 @@ This class represents a WebSocket. It extends the `EventEmitter`. `false`. - `handshakeTimeout` {Number} Timeout in milliseconds for the handshake request. This is reset after every redirection. + - `maxPayload` {Number} The maximum allowed message size in bytes. - `maxRedirects` {Number} The maximum number of redirects allowed. Defaults to 10. - - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate. - - `protocolVersion` {Number} Value of the `Sec-WebSocket-Version` header. - `origin` {String} Value of the `Origin` or `Sec-WebSocket-Origin` header depending on the `protocolVersion`. - - `maxPayload` {Number} The maximum allowed message size in bytes. + - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate. + - `protocolVersion` {Number} Value of the `Sec-WebSocket-Version` header. - `skipUTF8Validation` {Boolean} Specifies whether or not to skip UTF-8 validation for text and close messages. Defaults to `false`. Set to `true` only if the server is trusted. @@ -491,14 +491,14 @@ only removes listeners added with - `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The data to send. - `options` {Object} - - `compress` {Boolean} Specifies whether `data` should be compressed or not. - Defaults to `true` when permessage-deflate is enabled. - `binary` {Boolean} Specifies whether `data` should be sent as a binary or not. Default is autodetected. - - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults - to `true` when `websocket` is not a server client. + - `compress` {Boolean} Specifies whether `data` should be compressed or not. + Defaults to `true` when permessage-deflate is enabled. - `fin` {Boolean} Specifies whether `data` is the last fragment of a message or not. Defaults to `true`. + - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults + to `true` when `websocket` is not a server client. - `callback` {Function} An optional callback which is invoked when `data` is written out. diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index 7fe9459ff..9e6003d07 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -30,22 +30,22 @@ class PerMessageDeflate { * Creates a PerMessageDeflate instance. * * @param {Object} [options] Configuration options - * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept - * disabling of server context takeover + * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support + * for, or request, a custom client window size * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ * acknowledge disabling of client context takeover + * @param {Number} [options.concurrencyLimit=10] The number of concurrent + * calls to zlib * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the * use of a custom server window size - * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support - * for, or request, a custom client window size + * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept + * disabling of server context takeover + * @param {Number} [options.threshold=1024] Size (in bytes) below which + * messages should not be compressed * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on * deflate * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on * inflate - * @param {Number} [options.threshold=1024] Size (in bytes) below which - * messages should not be compressed - * @param {Number} [options.concurrencyLimit=10] The number of concurrent - * calls to zlib * @param {Boolean} [isServer=false] Create the instance in either server or * client mode * @param {Number} [maxPayload=0] The maximum allowed message length diff --git a/lib/sender.js b/lib/sender.js index 4f46f4d88..15df9d065 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -40,13 +40,13 @@ class Sender { * * @param {Buffer} data The data to frame * @param {Object} options Options object - * @param {Number} options.opcode The opcode - * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be - * modified * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit * @return {Buffer[]} The framed data as a list of `Buffer` instances @@ -255,10 +255,10 @@ class Sender { * * @param {*} data The message to send * @param {Object} options Options object - * @param {Boolean} [options.compress=false] Specifies whether or not to - * compress `data` * @param {Boolean} [options.binary=false] Specifies whether `data` is binary * or text + * @param {Boolean} [options.compress=false] Specifies whether or not to + * compress `data` * @param {Boolean} [options.fin=false] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask=false] Specifies whether or not to mask @@ -321,12 +321,12 @@ class Sender { * `data` * @param {Object} options Options object * @param {Number} options.opcode The opcode - * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be - * modified * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit * @param {Function} [cb] Callback diff --git a/lib/websocket.js b/lib/websocket.js index 8d8f913e4..f0b6eb531 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -378,10 +378,10 @@ class WebSocket extends EventEmitter { * * @param {*} data The message to send * @param {Object} [options] Options object - * @param {Boolean} [options.compress] Specifies whether or not to compress - * `data` * @param {Boolean} [options.binary] Specifies whether `data` is binary or * text + * @param {Boolean} [options.compress] Specifies whether or not to compress + * `data` * @param {Boolean} [options.fin=true] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask] Specifies whether or not to mask `data` @@ -565,20 +565,20 @@ module.exports = WebSocket; * @param {(String|URL)} address The URL to which to connect * @param {Array} protocols The subprotocols * @param {Object} [options] Connection options - * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable - * permessage-deflate + * @param {Boolean} [options.followRedirects=false] Whether or not to follow + * redirects * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the * handshake request - * @param {Number} [options.protocolVersion=13] Value of the - * `Sec-WebSocket-Version` header - * @param {String} [options.origin] Value of the `Origin` or - * `Sec-WebSocket-Origin` header * @param {Number} [options.maxPayload=104857600] The maximum allowed message * size - * @param {Boolean} [options.followRedirects=false] Whether or not to follow - * redirects * @param {Number} [options.maxRedirects=10] The maximum number of redirects * allowed + * @param {String} [options.origin] Value of the `Origin` or + * `Sec-WebSocket-Origin` header + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable + * permessage-deflate + * @param {Number} [options.protocolVersion=13] Value of the + * `Sec-WebSocket-Version` header * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or * not to skip UTF-8 validation for text and close messages * @private From c677aab97852f11bf146fc4e46fe5bdfd4c727d8 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 15 Aug 2021 20:17:18 +0200 Subject: [PATCH 093/328] [doc] Fix `createWebSocketStream()` documentation The ES module wrapper exports `createWebSocketStream()` as a named export and not as a static method of the `WebSocket` class. --- doc/ws.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 08d70cc7a..a4680702f 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -43,7 +43,7 @@ - [websocket.send(data[, options][, callback])](#websocketsenddata-options-callback) - [websocket.terminate()](#websocketterminate) - [websocket.url](#websocketurl) -- [WebSocket.createWebSocketStream(websocket[, options])](#websocketcreatewebsocketstreamwebsocket-options) +- [createWebSocketStream(websocket[, options])](#createwebsocketstreamwebsocket-options) - [WS Error Codes](#ws-error-codes) - [WS_ERR_EXPECTED_FIN](#ws_err_expected_fin) - [WS_ERR_EXPECTED_MASK](#ws_err_expected_mask) @@ -514,7 +514,7 @@ Forcibly close the connection. Internally this calls [socket.destroy()][]. The URL of the WebSocket server. Server clients don't have this attribute. -## WebSocket.createWebSocketStream(websocket[, options]) +## createWebSocketStream(websocket[, options]) - `websocket` {WebSocket} A `WebSocket` object. - `options` {Object} [Options][duplex-options] to pass to the `Duplex` From d5e3549a0350dd94103af88f6fb329d9a27ce193 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 17 Aug 2021 11:55:51 +0200 Subject: [PATCH 094/328] [minor] Add `WebSocket.WebSocket{,Server}` aliases (#1935) Add `WebSocket.WebSocket` as an alias for `WebSocket` and `WebSocket.WebSocketServer` as an alias for `WebSocket.Server` to fix name consistency and improve interoperability with the ES module wrapper. Refs: https://github.com/websockets/ws/issues/1877 Refs: https://github.com/websockets/ws/issues/1932 --- index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/index.js b/index.js index 722c78676..41edb3b81 100644 --- a/index.js +++ b/index.js @@ -7,4 +7,7 @@ WebSocket.Server = require('./lib/websocket-server'); WebSocket.Receiver = require('./lib/receiver'); WebSocket.Sender = require('./lib/sender'); +WebSocket.WebSocket = WebSocket; +WebSocket.WebSocketServer = WebSocket.Server; + module.exports = WebSocket; From 7647a8920b6a7ada107c28be68e4e82393dac893 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 18 Aug 2021 07:20:48 +0200 Subject: [PATCH 095/328] [dist] 8.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d163e70e1..e04602be6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.1.0", + "version": "8.2.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From ea6c054e975a715b83a8ca20e5af1bbcf80f90e5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 21 Aug 2021 15:50:20 +0200 Subject: [PATCH 096/328] [test] Reorganize some tests --- test/websocket.test.js | 214 ++++++++++++++++++++--------------------- 1 file changed, 107 insertions(+), 107 deletions(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index 51da54f76..888a57715 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2866,113 +2866,6 @@ describe('WebSocket', () => { }); }); - it('can send and receive text data', (done) => { - const wss = new WebSocket.Server( - { - perMessageDeflate: { threshold: 0 }, - port: 0 - }, - () => { - const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { - perMessageDeflate: { threshold: 0 } - }); - - ws.on('open', () => { - ws.send('hi', { compress: true }); - ws.close(); - }); - - ws.on('message', (message, isBinary) => { - assert.deepStrictEqual(message, Buffer.from('hi')); - assert.ok(!isBinary); - wss.close(done); - }); - } - ); - - wss.on('connection', (ws) => { - ws.on('message', (message, isBinary) => { - ws.send(message, { binary: isBinary, compress: true }); - }); - }); - }); - - it('can send and receive a `TypedArray`', (done) => { - const array = new Float32Array(5); - - for (let i = 0; i < array.length; i++) { - array[i] = i / 2; - } - - const wss = new WebSocket.Server( - { - perMessageDeflate: { threshold: 0 }, - port: 0 - }, - () => { - const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { - perMessageDeflate: { threshold: 0 } - }); - - ws.on('open', () => { - ws.send(array, { compress: true }); - ws.close(); - }); - - ws.on('message', (message, isBinary) => { - assert.deepStrictEqual(message, Buffer.from(array.buffer)); - assert.ok(isBinary); - wss.close(done); - }); - } - ); - - wss.on('connection', (ws) => { - ws.on('message', (message, isBinary) => { - assert.ok(isBinary); - ws.send(message, { compress: true }); - }); - }); - }); - - it('can send and receive an `ArrayBuffer`', (done) => { - const array = new Float32Array(5); - - for (let i = 0; i < array.length; i++) { - array[i] = i / 2; - } - - const wss = new WebSocket.Server( - { - perMessageDeflate: { threshold: 0 }, - port: 0 - }, - () => { - const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { - perMessageDeflate: { threshold: 0 } - }); - - ws.on('open', () => { - ws.send(array.buffer, { compress: true }); - ws.close(); - }); - - ws.on('message', (message, isBinary) => { - assert.deepStrictEqual(message, Buffer.from(array.buffer)); - assert.ok(isBinary); - wss.close(done); - }); - } - ); - - wss.on('connection', (ws) => { - ws.on('message', (message, isBinary) => { - assert.ok(isBinary); - ws.send(message, { compress: true }); - }); - }); - }); - it('consumes all received data when connection is closed (1/2)', (done) => { const wss = new WebSocket.Server( { @@ -3167,6 +3060,113 @@ describe('WebSocket', () => { }); describe('#send', () => { + it('can send text data', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: { threshold: 0 }, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws.send('hi', { compress: true }); + ws.close(); + }); + + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from('hi')); + assert.ok(!isBinary); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + ws.on('message', (message, isBinary) => { + ws.send(message, { binary: isBinary, compress: true }); + }); + }); + }); + + it('can send a `TypedArray`', (done) => { + const array = new Float32Array(5); + + for (let i = 0; i < array.length; i++) { + array[i] = i / 2; + } + + const wss = new WebSocket.Server( + { + perMessageDeflate: { threshold: 0 }, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws.send(array, { compress: true }); + ws.close(); + }); + + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from(array.buffer)); + assert.ok(isBinary); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + ws.send(message, { compress: true }); + }); + }); + }); + + it('can send an `ArrayBuffer`', (done) => { + const array = new Float32Array(5); + + for (let i = 0; i < array.length; i++) { + array[i] = i / 2; + } + + const wss = new WebSocket.Server( + { + perMessageDeflate: { threshold: 0 }, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws.send(array.buffer, { compress: true }); + ws.close(); + }); + + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from(array.buffer)); + assert.ok(isBinary); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + ws.send(message, { compress: true }); + }); + }); + }); + it('ignores the `compress` option if the extension is disabled', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { From 869c9892cd5f1f574fae3181231e462e1d4ab740 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 28 Aug 2021 16:42:49 +0200 Subject: [PATCH 097/328] [fix] Resume the socket in the next tick Ensure that `socket.resume()` is called after `socket.pause()`. Fixes #1940 --- lib/websocket.js | 22 +++++++++++++++-- test/websocket.test.js | 56 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index f0b6eb531..aac87296f 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -1,3 +1,5 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Readable$" }] */ + 'use strict'; const EventEmitter = require('events'); @@ -6,6 +8,7 @@ const http = require('http'); const net = require('net'); const tls = require('tls'); const { randomBytes, createHash } = require('crypto'); +const { Readable } = require('stream'); const { URL } = require('url'); const PerMessageDeflate = require('./permessage-deflate'); @@ -954,7 +957,7 @@ function receiverOnConclude(code, reason) { const websocket = this[kWebSocket]; websocket._socket.removeListener('data', socketOnData); - websocket._socket.resume(); + process.nextTick(resume, websocket._socket); websocket._closeFrameReceived = true; websocket._closeMessage = reason; @@ -983,7 +986,12 @@ function receiverOnError(err) { const websocket = this[kWebSocket]; websocket._socket.removeListener('data', socketOnData); - websocket._socket.resume(); + + // + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See + // https://github.com/websockets/ws/issues/1940. + // + process.nextTick(resume, websocket._socket); websocket.close(err[kStatusCode]); websocket.emit('error', err); @@ -1032,6 +1040,16 @@ function receiverOnPong(data) { this[kWebSocket].emit('pong', data); } +/** + * Resume a readable stream + * + * @param {Readable} stream The readable stream + * @private + */ +function resume(stream) { + stream.resume(); +} + /** * The listener of the `net.Socket` `'close'` event. * diff --git a/test/websocket.test.js b/test/websocket.test.js index 888a57715..4b85c7948 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -3308,7 +3308,7 @@ describe('WebSocket', () => { }); }); - describe('Connection close edge cases', () => { + describe('Connection close', () => { it('closes cleanly after simultaneous errors (1/2)', (done) => { let clientCloseEventEmitted = false; let serverClientCloseEventEmitted = false; @@ -3420,5 +3420,59 @@ describe('WebSocket', () => { }); }); }); + + it('resumes the socket when an error occurs', (done) => { + const maxPayload = 16 * 1024; + const wss = new WebSocket.Server({ maxPayload, port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + const list = [ + ...Sender.frame(Buffer.alloc(maxPayload + 1), { + fin: true, + opcode: 0x02, + mask: true, + readOnly: false + }) + ]; + + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'); + assert.strictEqual(err.message, 'Max payload size exceeded'); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, EMPTY_BUFFER); + wss.close(done); + }); + }); + + ws._socket.push(Buffer.concat(list)); + }); + }); + + it('resumes the socket when the close frame is received', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + const opts = { fin: true, mask: true, readOnly: false }; + const list = [ + ...Sender.frame(Buffer.alloc(16 * 1024), { opcode: 0x02, ...opts }), + ...Sender.frame(EMPTY_BUFFER, { opcode: 0x08, ...opts }) + ]; + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1005); + assert.strictEqual(reason, EMPTY_BUFFER); + wss.close(done); + }); + + ws._socket.push(Buffer.concat(list)); + }); + }); }); }); From cc7a7798b749cf263636abcba4ba19532161c3ea Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 28 Aug 2021 17:07:15 +0200 Subject: [PATCH 098/328] [dist] 8.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e04602be6..97424848c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.2.0", + "version": "8.2.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From ec9377ca745e51cf182c11550d4c39872e3d307f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 30 Aug 2021 20:20:45 +0200 Subject: [PATCH 099/328] [minor] Skip unnecessary operations if the socket is already closed There is no need to remove the already removed `socketOnData` listener, resume the socket, and call `websocket.close()` if the socket is already closed. --- lib/websocket.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index aac87296f..818f26914 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -956,13 +956,15 @@ function sendAfterClose(websocket, data, cb) { function receiverOnConclude(code, reason) { const websocket = this[kWebSocket]; - websocket._socket.removeListener('data', socketOnData); - process.nextTick(resume, websocket._socket); - websocket._closeFrameReceived = true; websocket._closeMessage = reason; websocket._closeCode = code; + if (websocket._socket[kWebSocket] === undefined) return; + + websocket._socket.removeListener('data', socketOnData); + process.nextTick(resume, websocket._socket); + if (code === 1005) websocket.close(); else websocket.close(code, reason); } @@ -985,15 +987,18 @@ function receiverOnDrain() { function receiverOnError(err) { const websocket = this[kWebSocket]; - websocket._socket.removeListener('data', socketOnData); + if (websocket._socket[kWebSocket] !== undefined) { + websocket._socket.removeListener('data', socketOnData); - // - // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See - // https://github.com/websockets/ws/issues/1940. - // - process.nextTick(resume, websocket._socket); + // + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See + // https://github.com/websockets/ws/issues/1940. + // + process.nextTick(resume, websocket._socket); + + websocket.close(err[kStatusCode]); + } - websocket.close(err[kStatusCode]); websocket.emit('error', err); } From 48c2cb36df1555745063633ced0a4a07d7f8c245 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 31 Aug 2021 08:49:52 +0200 Subject: [PATCH 100/328] [pkg] Update eslint-plugin-prettier to version 4.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97424848c..27aff6cac 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "bufferutil": "^4.0.1", "eslint": "^7.2.0", "eslint-config-prettier": "^8.1.0", - "eslint-plugin-prettier": "^3.0.1", + "eslint-plugin-prettier": "^4.0.0", "mocha": "^8.4.0", "nyc": "^15.0.0", "prettier": "^2.0.5", From 04c032c51b3219f76a3a48897027e38f4a9f5f50 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 31 Aug 2021 09:55:03 +0200 Subject: [PATCH 101/328] [ci] Use Github Actions for Windows x86 testing --- .github/workflows/ci.yml | 17 ++++++++++++++--- README.md | 1 - appveyor.yml | 18 ------------------ 3 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 appveyor.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29e75259f..52f5f9aa1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,9 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: + arch: + - x64 + - x86 node: - 10 - 12 @@ -18,14 +21,22 @@ jobs: - macOS-latest - ubuntu-latest - windows-latest + exclude: + - arch: x86 + os: macOS-latest + - arch: x86 + os: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: ${{ matrix.node }} + architecture: ${{ matrix.arch }} - run: npm install - run: npm run lint - if: matrix.node == 16 && matrix.os == 'ubuntu-latest' + if: + matrix.os == 'ubuntu-latest' && matrix.node == 16 && matrix.arch == + 'x64' - run: npm test - run: echo ::set-output name=job_id::$(node -e @@ -35,8 +46,8 @@ jobs: - uses: coverallsapp/github-action@v1.1.2 with: flag-name: - ${{ steps.get_job_id.outputs.job_id }} (Node.js ${{ matrix.node }} - on ${{ matrix.os }}) + ${{ steps.get_job_id.outputs.job_id }} (Node.js ${{ matrix.node }} - + $ {{ matrix.arch }} on ${{ matrix.os }}) github-token: ${{ secrets.GITHUB_TOKEN }} parallel: true coverage: diff --git a/README.md b/README.md index a2ee9254e..b00ec1e92 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws) [![Build](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=build&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) -[![Windows x86 Build](https://img.shields.io/appveyor/ci/lpinca/ws/master.svg?logo=appveyor)](https://ci.appveyor.com/project/lpinca/ws) [![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws) ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index ff8520944..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,18 +0,0 @@ -environment: - matrix: - - nodejs_version: '16' - - nodejs_version: '14' - - nodejs_version: '12' - - nodejs_version: '10' -platform: - - x86 -matrix: - fast_finish: true -install: - - ps: Install-Product node $env:nodejs_version $env:platform - - npm install -test_script: - - node --version - - npm --version - - npm test -build: off From 3039b6b9d2b290eca74f67e444dc7aa44e574f00 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 4 Sep 2021 20:44:38 +0200 Subject: [PATCH 102/328] [doc] Change label text to CI --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b00ec1e92..a0317031b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ws: a Node.js WebSocket library [![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws) -[![Build](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=build&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) +[![CI](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) [![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws) ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and From 72296e54cad6b105f901b82174f86837b4bcd414 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 8 Sep 2021 21:42:28 +0200 Subject: [PATCH 103/328] [dist] 8.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27aff6cac..1e26e491f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.2.1", + "version": "8.2.2", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From f871195c55cff39ac51020ea344ec6a2c4ea9f4e Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 9 Sep 2021 21:44:52 +0200 Subject: [PATCH 104/328] [doc] Update issue template --- .github/ISSUE_TEMPLATE/bug_report.yml | 48 +++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 1 + .github/issue_template.md | 41 ----------------------- 3 files changed, 49 insertions(+), 41 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/issue_template.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..9f437067f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,48 @@ +name: 'Bug report' +description: Create a bug report +body: + - type: markdown + attributes: + value: | + Thank you for reporting an issue. + + This issue tracker is for bugs and issues found in ws. + General support questions should be raised on a channel like Stack Overflow. + + Please fill in as much of the template below as you're able. + - type: checkboxes + attributes: + options: + - label: + I've searched for any related issues and avoided creating a + duplicate issue. + required: true + - type: textarea + attributes: + label: Description + description: + Description of the bug or feature, preferably a simple code snippet that + can be run directly without installing third-party dependencies. + - type: input + attributes: + label: ws version + - type: input + attributes: + label: Node.js Version + description: Output of `node -v`. + - type: textarea + attributes: + label: System + description: Output of `npx envinfo --system`. + - type: textarea + attributes: + label: Expected result + description: What you expected to happen. + - type: textarea + attributes: + label: Actual result + description: What actually happened. + - type: textarea + attributes: + label: Attachments + description: Logs, screenshots, screencast, etc. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..3ba13e0ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index 5c3ca6a01..000000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,41 +0,0 @@ - - -- [ ] I've searched for any related issues and avoided creating a duplicate - issue. - -#### Description - - - -#### Reproducible in: - -- version: -- Node.js version(s): -- OS version(s): - -#### Steps to reproduce: - -1. - -2. - -3. - -#### Expected result: - - - -#### Actual result: - - - -#### Attachments: - - From 5b85322f5490fdaed950ad6b8d7ad381077164df Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 11 Sep 2021 21:42:52 +0200 Subject: [PATCH 105/328] [ci] Update coverallsapp/github-action action to version 1.1.3 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52f5f9aa1..66ddf9cc4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: "console.log(crypto.randomBytes(16).toString('hex'))") id: get_job_id shell: bash - - uses: coverallsapp/github-action@v1.1.2 + - uses: coverallsapp/github-action@1.1.3 with: flag-name: ${{ steps.get_job_id.outputs.job_id }} (Node.js ${{ matrix.node }} - @@ -54,7 +54,7 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: coverallsapp/github-action@v1.1.2 + - uses: coverallsapp/github-action@1.1.3 with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true From 055949fd23b0d7f6a23ba9b9532b7834909df192 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 13 Sep 2021 20:29:42 +0200 Subject: [PATCH 106/328] [doc] Remove no longer needed noop function from code snippet --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index a0317031b..c6b71110e 100644 --- a/README.md +++ b/README.md @@ -410,8 +410,6 @@ endpoint is still responsive. ```js import { WebSocketServer } from 'ws'; -function noop() {} - function heartbeat() { this.isAlive = true; } @@ -428,7 +426,7 @@ const interval = setInterval(function ping() { if (ws.isAlive === false) return ws.terminate(); ws.isAlive = false; - ws.ping(noop); + ws.ping(); }); }, 30000); From 474aa3616fc9ebbb23369c7c3d3760f68d72b56e Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 28 Sep 2021 16:30:19 +0200 Subject: [PATCH 107/328] [doc] Improve `WebSocket#{p{i,o}ng,send}()` documentation Specify that an error is thrown if the ready state is `CONNECTING` and that if an error occurs, the callback is called with the error as its first argument. Fixes #1953 --- doc/ws.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index a4680702f..b9569497d 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -450,9 +450,10 @@ receives an `OpenEvent` named "open". - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults to `true` when `websocket` is not a server client. - `callback` {Function} An optional callback which is invoked when the ping - frame is written out. + frame is written out. If an error occurs, the callback is called with the + error as its first argument. -Send a ping. +Send a ping. This method throws an error if the ready state is `CONNECTING`. ### websocket.pong([data[, mask]][, callback]) @@ -461,9 +462,10 @@ Send a ping. - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults to `true` when `websocket` is not a server client. - `callback` {Function} An optional callback which is invoked when the pong - frame is written out. + frame is written out. If an error occurs, the callback is called with the + error as its first argument. -Send a pong. +Send a pong. This method throws an error if the ready state is `CONNECTING`. ### websocket.protocol @@ -500,9 +502,11 @@ only removes listeners added with - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults to `true` when `websocket` is not a server client. - `callback` {Function} An optional callback which is invoked when `data` is - written out. + written out. If an error occurs, the callback is called with the error as its + first argument. -Send `data` through the connection. +Send `data` through the connection. This method throws an error if the ready +state is `CONNECTING`. ### websocket.terminate() From 41ae56313be61a8c2344e714603eb6da56d5ea99 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 29 Sep 2021 16:41:32 +0200 Subject: [PATCH 108/328] [fix] Ignore the `threshold` option if context takeover is enabled When context takeover is enabled, compress messages even if their size is below the value of `threshold` option. Refs: https://github.com/websockets/ws/issues/1950 --- README.md | 2 +- doc/ws.md | 4 +- lib/permessage-deflate.js | 2 +- lib/sender.js | 10 +- test/permessage-deflate.test.js | 23 +-- test/sender.test.js | 260 +++++++++++++++++--------------- 6 files changed, 159 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index c6b71110e..b78b1f665 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ const wss = new WebSocketServer({ // Below options specified as default values. concurrencyLimit: 10, // Limits zlib concurrency for perf. threshold: 1024 // Size (in bytes) below which messages - // should not be compressed. + // should not be compressed if context takeover is disabled. } }); ``` diff --git a/doc/ws.md b/doc/ws.md index b9569497d..fc14e7e97 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -147,8 +147,8 @@ value). If an object is provided then that is extension parameters: zlib on deflate. - `zlibInflateOptions` {Object} [Additional options][zlib-options] to pass to zlib on inflate. -- `threshold` {Number} Payloads smaller than this will not be compressed. - Defaults to 1024 bytes. +- `threshold` {Number} Payloads smaller than this will not be compressed if + context takeover is disabled. Defaults to 1024 bytes. - `concurrencyLimit` {Number} The number of concurrent calls to zlib. Calls above this limit will be queued. Default 10. You usually won't need to touch this option. See [this issue][concurrency-limit] for more details. diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index 9e6003d07..504069719 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -41,7 +41,7 @@ class PerMessageDeflate { * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept * disabling of server context takeover * @param {Number} [options.threshold=1024] Size (in bytes) below which - * messages should not be compressed + * messages should not be compressed if context takeover is disabled * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on * deflate * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on diff --git a/lib/sender.js b/lib/sender.js index 15df9d065..c1e357c78 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -274,7 +274,15 @@ class Sender { if (this._firstFragment) { this._firstFragment = false; - if (rsv1 && perMessageDeflate) { + if ( + rsv1 && + perMessageDeflate && + perMessageDeflate.params[ + perMessageDeflate._isServer + ? 'server_no_context_takeover' + : 'client_no_context_takeover' + ] + ) { rsv1 = buf.length >= perMessageDeflate._threshold; } this._compress = rsv1; diff --git a/test/permessage-deflate.test.js b/test/permessage-deflate.test.js index a547762ca..a9c9bf165 100644 --- a/test/permessage-deflate.test.js +++ b/test/permessage-deflate.test.js @@ -344,7 +344,7 @@ describe('PerMessageDeflate', () => { describe('#compress and #decompress', () => { it('works with unfragmented messages', (done) => { - const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const perMessageDeflate = new PerMessageDeflate(); const buf = Buffer.from([1, 2, 3]); perMessageDeflate.accept([{}]); @@ -361,7 +361,7 @@ describe('PerMessageDeflate', () => { }); it('works with fragmented messages', (done) => { - const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const perMessageDeflate = new PerMessageDeflate(); const buf = Buffer.from([1, 2, 3, 4]); perMessageDeflate.accept([{}]); @@ -388,7 +388,6 @@ describe('PerMessageDeflate', () => { it('works with the negotiated parameters', (done) => { const perMessageDeflate = new PerMessageDeflate({ - threshold: 0, memLevel: 5, level: 9 }); @@ -415,11 +414,9 @@ describe('PerMessageDeflate', () => { it('honors the `level` option', (done) => { const lev0 = new PerMessageDeflate({ - threshold: 0, zlibDeflateOptions: { level: 0 } }); const lev9 = new PerMessageDeflate({ - threshold: 0, zlibDeflateOptions: { level: 9 } }); const extensionStr = @@ -459,7 +456,6 @@ describe('PerMessageDeflate', () => { it('honors the `zlib{Deflate,Inflate}Options` option', (done) => { const lev0 = new PerMessageDeflate({ - threshold: 0, zlibDeflateOptions: { level: 0, chunkSize: 256 @@ -469,7 +465,6 @@ describe('PerMessageDeflate', () => { } }); const lev9 = new PerMessageDeflate({ - threshold: 0, zlibDeflateOptions: { level: 9, chunkSize: 128 @@ -523,7 +518,7 @@ describe('PerMessageDeflate', () => { }); it("doesn't use contex takeover if not allowed", (done) => { - const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }, true); + const perMessageDeflate = new PerMessageDeflate({}, true); const extensions = extension.parse( 'permessage-deflate;server_no_context_takeover' ); @@ -554,7 +549,7 @@ describe('PerMessageDeflate', () => { }); it('uses contex takeover if allowed', (done) => { - const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }, true); + const perMessageDeflate = new PerMessageDeflate({}, true); const extensions = extension.parse('permessage-deflate'); const buf = Buffer.from('foofoo'); @@ -583,7 +578,7 @@ describe('PerMessageDeflate', () => { }); it('calls the callback when an error occurs (inflate)', (done) => { - const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const perMessageDeflate = new PerMessageDeflate(); const data = Buffer.from('something invalid'); perMessageDeflate.accept([{}]); @@ -596,11 +591,7 @@ describe('PerMessageDeflate', () => { }); it("doesn't call the callback twice when `maxPayload` is exceeded", (done) => { - const perMessageDeflate = new PerMessageDeflate( - { threshold: 0 }, - false, - 25 - ); + const perMessageDeflate = new PerMessageDeflate({}, false, 25); const buf = Buffer.from('A'.repeat(50)); perMessageDeflate.accept([{}]); @@ -616,7 +607,7 @@ describe('PerMessageDeflate', () => { }); it('calls the callback if the deflate stream is closed prematurely', (done) => { - const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const perMessageDeflate = new PerMessageDeflate(); const buf = Buffer.from('A'.repeat(50)); perMessageDeflate.accept([{}]); diff --git a/test/sender.test.js b/test/sender.test.js index 1c43dc5a5..662d2941d 100644 --- a/test/sender.test.js +++ b/test/sender.test.js @@ -3,6 +3,7 @@ const assert = require('assert'); const PerMessageDeflate = require('../lib/permessage-deflate'); +const extension = require('../lib/extension'); const Sender = require('../lib/sender'); class MockSocket { @@ -49,7 +50,7 @@ describe('Sender', () => { describe('#send', () => { it('compresses data if compress option is enabled', (done) => { - const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const perMessageDeflate = new PerMessageDeflate(); let count = 0; const mockSocket = new MockSocket({ write: (data) => { @@ -71,139 +72,156 @@ describe('Sender', () => { sender.send('hi', options); }); - it('does not compress data for small payloads', (done) => { - const perMessageDeflate = new PerMessageDeflate(); - const mockSocket = new MockSocket({ - write: (data) => { - assert.notStrictEqual(data[0] & 0x40, 0x40); - done(); - } - }); - const sender = new Sender(mockSocket, { - 'permessage-deflate': perMessageDeflate - }); - - perMessageDeflate.accept([{}]); + describe('when context takeover is disabled', () => { + it('honors the compression threshold', (done) => { + const perMessageDeflate = new PerMessageDeflate(); + const mockSocket = new MockSocket({ + write: (data) => { + assert.notStrictEqual(data[0] & 0x40, 0x40); + done(); + } + }); + const sender = new Sender(mockSocket, { + 'permessage-deflate': perMessageDeflate + }); + const extensions = extension.parse( + 'permessage-deflate; client_no_context_takeover' + ); - sender.send('hi', { compress: true, fin: true }); - }); + perMessageDeflate.accept(extensions['permessage-deflate']); - it('compresses all frames in a fragmented message', (done) => { - const chunks = []; - const perMessageDeflate = new PerMessageDeflate({ threshold: 3 }); - const mockSocket = new MockSocket({ - write: (chunk) => { - chunks.push(chunk); - if (chunks.length !== 4) return; - - assert.strictEqual(chunks[0].length, 2); - assert.strictEqual(chunks[0][0] & 0x40, 0x40); - assert.strictEqual(chunks[1].length, 9); - - assert.strictEqual(chunks[2].length, 2); - assert.strictEqual(chunks[2][0] & 0x40, 0x00); - assert.strictEqual(chunks[3].length, 4); - done(); - } - }); - const sender = new Sender(mockSocket, { - 'permessage-deflate': perMessageDeflate + sender.send('hi', { compress: true, fin: true }); }); - perMessageDeflate.accept([{}]); - - sender.send('123', { compress: true, fin: false }); - sender.send('12', { compress: true, fin: true }); - }); - - it('compresses no frames in a fragmented message', (done) => { - const chunks = []; - const perMessageDeflate = new PerMessageDeflate({ threshold: 3 }); - const mockSocket = new MockSocket({ - write: (chunk) => { - chunks.push(chunk); - if (chunks.length !== 4) return; - - assert.strictEqual(chunks[0].length, 2); - assert.strictEqual(chunks[0][0] & 0x40, 0x00); - assert.strictEqual(chunks[1].length, 2); - - assert.strictEqual(chunks[2].length, 2); - assert.strictEqual(chunks[2][0] & 0x40, 0x00); - assert.strictEqual(chunks[3].length, 3); - done(); - } - }); - const sender = new Sender(mockSocket, { - 'permessage-deflate': perMessageDeflate + it('compresses all fragments of a fragmented message', (done) => { + const chunks = []; + const perMessageDeflate = new PerMessageDeflate({ threshold: 3 }); + const mockSocket = new MockSocket({ + write: (chunk) => { + chunks.push(chunk); + if (chunks.length !== 4) return; + + assert.strictEqual(chunks[0].length, 2); + assert.strictEqual(chunks[0][0] & 0x40, 0x40); + assert.strictEqual(chunks[1].length, 9); + + assert.strictEqual(chunks[2].length, 2); + assert.strictEqual(chunks[2][0] & 0x40, 0x00); + assert.strictEqual(chunks[3].length, 4); + done(); + } + }); + const sender = new Sender(mockSocket, { + 'permessage-deflate': perMessageDeflate + }); + const extensions = extension.parse( + 'permessage-deflate; client_no_context_takeover' + ); + + perMessageDeflate.accept(extensions['permessage-deflate']); + + sender.send('123', { compress: true, fin: false }); + sender.send('12', { compress: true, fin: true }); }); - perMessageDeflate.accept([{}]); - - sender.send('12', { compress: true, fin: false }); - sender.send('123', { compress: true, fin: true }); - }); - - it('compresses empty buffer as first fragment', (done) => { - const chunks = []; - const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); - const mockSocket = new MockSocket({ - write: (chunk) => { - chunks.push(chunk); - if (chunks.length !== 4) return; - - assert.strictEqual(chunks[0].length, 2); - assert.strictEqual(chunks[0][0] & 0x40, 0x40); - assert.strictEqual(chunks[1].length, 5); - - assert.strictEqual(chunks[2].length, 2); - assert.strictEqual(chunks[2][0] & 0x40, 0x00); - assert.strictEqual(chunks[3].length, 6); - done(); - } - }); - const sender = new Sender(mockSocket, { - 'permessage-deflate': perMessageDeflate + it('does not compress any fragments of a fragmented message', (done) => { + const chunks = []; + const perMessageDeflate = new PerMessageDeflate({ threshold: 3 }); + const mockSocket = new MockSocket({ + write: (chunk) => { + chunks.push(chunk); + if (chunks.length !== 4) return; + + assert.strictEqual(chunks[0].length, 2); + assert.strictEqual(chunks[0][0] & 0x40, 0x00); + assert.strictEqual(chunks[1].length, 2); + + assert.strictEqual(chunks[2].length, 2); + assert.strictEqual(chunks[2][0] & 0x40, 0x00); + assert.strictEqual(chunks[3].length, 3); + done(); + } + }); + const sender = new Sender(mockSocket, { + 'permessage-deflate': perMessageDeflate + }); + const extensions = extension.parse( + 'permessage-deflate; client_no_context_takeover' + ); + + perMessageDeflate.accept(extensions['permessage-deflate']); + + sender.send('12', { compress: true, fin: false }); + sender.send('123', { compress: true, fin: true }); }); - perMessageDeflate.accept([{}]); - - sender.send(Buffer.alloc(0), { compress: true, fin: false }); - sender.send('data', { compress: true, fin: true }); - }); - - it('compresses empty buffer as last fragment', (done) => { - const chunks = []; - const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); - const mockSocket = new MockSocket({ - write: (chunk) => { - chunks.push(chunk); - if (chunks.length !== 4) return; - - assert.strictEqual(chunks[0].length, 2); - assert.strictEqual(chunks[0][0] & 0x40, 0x40); - assert.strictEqual(chunks[1].length, 10); - - assert.strictEqual(chunks[2].length, 2); - assert.strictEqual(chunks[2][0] & 0x40, 0x00); - assert.strictEqual(chunks[3].length, 1); - done(); - } - }); - const sender = new Sender(mockSocket, { - 'permessage-deflate': perMessageDeflate + it('compresses empty buffer as first fragment', (done) => { + const chunks = []; + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const mockSocket = new MockSocket({ + write: (chunk) => { + chunks.push(chunk); + if (chunks.length !== 4) return; + + assert.strictEqual(chunks[0].length, 2); + assert.strictEqual(chunks[0][0] & 0x40, 0x40); + assert.strictEqual(chunks[1].length, 5); + + assert.strictEqual(chunks[2].length, 2); + assert.strictEqual(chunks[2][0] & 0x40, 0x00); + assert.strictEqual(chunks[3].length, 6); + done(); + } + }); + const sender = new Sender(mockSocket, { + 'permessage-deflate': perMessageDeflate + }); + const extensions = extension.parse( + 'permessage-deflate; client_no_context_takeover' + ); + + perMessageDeflate.accept(extensions['permessage-deflate']); + + sender.send(Buffer.alloc(0), { compress: true, fin: false }); + sender.send('data', { compress: true, fin: true }); }); - perMessageDeflate.accept([{}]); - - sender.send('data', { compress: true, fin: false }); - sender.send(Buffer.alloc(0), { compress: true, fin: true }); + it('compresses empty buffer as last fragment', (done) => { + const chunks = []; + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const mockSocket = new MockSocket({ + write: (chunk) => { + chunks.push(chunk); + if (chunks.length !== 4) return; + + assert.strictEqual(chunks[0].length, 2); + assert.strictEqual(chunks[0][0] & 0x40, 0x40); + assert.strictEqual(chunks[1].length, 10); + + assert.strictEqual(chunks[2].length, 2); + assert.strictEqual(chunks[2][0] & 0x40, 0x00); + assert.strictEqual(chunks[3].length, 1); + done(); + } + }); + const sender = new Sender(mockSocket, { + 'permessage-deflate': perMessageDeflate + }); + const extensions = extension.parse( + 'permessage-deflate; client_no_context_takeover' + ); + + perMessageDeflate.accept(extensions['permessage-deflate']); + + sender.send('data', { compress: true, fin: false }); + sender.send(Buffer.alloc(0), { compress: true, fin: true }); + }); }); }); describe('#ping', () => { it('works with multiple types of data', (done) => { - const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const perMessageDeflate = new PerMessageDeflate(); let count = 0; const mockSocket = new MockSocket({ write: (data) => { @@ -235,7 +253,7 @@ describe('Sender', () => { describe('#pong', () => { it('works with multiple types of data', (done) => { - const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const perMessageDeflate = new PerMessageDeflate(); let count = 0; const mockSocket = new MockSocket({ write: (data) => { @@ -292,7 +310,7 @@ describe('Sender', () => { }); it('should consume all data before closing', (done) => { - const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const perMessageDeflate = new PerMessageDeflate(); let count = 0; const mockSocket = new MockSocket({ From fef7942a18889eb45903ff3a678f7e005a0a5f06 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 30 Sep 2021 12:54:28 +0200 Subject: [PATCH 109/328] [ci] Fix typo --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66ddf9cc4..7ded18571 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,8 +46,8 @@ jobs: - uses: coverallsapp/github-action@1.1.3 with: flag-name: - ${{ steps.get_job_id.outputs.job_id }} (Node.js ${{ matrix.node }} - - $ {{ matrix.arch }} on ${{ matrix.os }}) + ${{ steps.get_job_id.outputs.job_id }} (Node.js ${{ matrix.node }} + ${{ matrix.arch }} on ${{ matrix.os }}) github-token: ${{ secrets.GITHUB_TOKEN }} parallel: true coverage: From cfd99b6309d59da5c35c4087520b480ec060cbd9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 2 Oct 2021 20:31:21 +0200 Subject: [PATCH 110/328] [dist] 8.2.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e26e491f..8afdd2e48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.2.2", + "version": "8.2.3", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From a3a22e4ed39c1a3be8e727e9c630dd440edc61dd Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 11 Oct 2021 08:32:09 +0200 Subject: [PATCH 111/328] [pkg] Update eslint to version 8.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8afdd2e48..5b9f7e41d 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "devDependencies": { "benchmark": "^2.1.4", "bufferutil": "^4.0.1", - "eslint": "^7.2.0", + "eslint": "^8.0.0", "eslint-config-prettier": "^8.1.0", "eslint-plugin-prettier": "^4.0.0", "mocha": "^8.4.0", From 65717f64b4ad12f209d8c0e4e9b5f0c1d60e4d2e Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 20 Oct 2021 19:23:28 +0200 Subject: [PATCH 112/328] [ci] Test on node 17 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ded18571..9a3c50a32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: - 12 - 14 - 16 + - 17 os: - macOS-latest - ubuntu-latest From 5991c3548404e441129a16887e6a15250722a960 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 1 Nov 2021 21:06:17 +0100 Subject: [PATCH 113/328] [doc] Fix nits --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b78b1f665..82ca8db49 100644 --- a/README.md +++ b/README.md @@ -148,8 +148,8 @@ ws.on('open', function open() { ws.send('something'); }); -ws.on('message', function incoming(message) { - console.log('received: %s', message); +ws.on('message', function message(data) { + console.log('received: %s', data); }); ``` @@ -179,8 +179,8 @@ import { WebSocketServer } from 'ws'; const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { - ws.on('message', function incoming(message) { - console.log('received: %s', message); + ws.on('message', function message(data) { + console.log('received: %s', data); }); ws.send('something'); @@ -201,8 +201,8 @@ const server = createServer({ const wss = new WebSocketServer({ server }); wss.on('connection', function connection(ws) { - ws.on('message', function incoming(message) { - console.log('received: %s', message); + ws.on('message', function message(data) { + console.log('received: %s', data); }); ws.send('something'); @@ -259,14 +259,14 @@ const server = createServer(); const wss = new WebSocketServer({ noServer: true }); wss.on('connection', function connection(ws, request, client) { - ws.on('message', function message(msg) { - console.log(`Received message ${msg} from user ${client}`); + ws.on('message', function message(data) { + console.log(`Received message ${data} from user ${client}`); }); }); server.on('upgrade', function upgrade(request, socket, head) { // This function is not defined on purpose. Implement it with your own logic. - authenticate(request, (err, client) => { + authenticate(request, function next(err, client) { if (err || !client) { socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); socket.destroy(); @@ -295,7 +295,7 @@ import WebSocket, { WebSocketServer } from 'ws'; const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { - ws.on('message', function incoming(data, isBinary) { + ws.on('message', function message(data, isBinary) { wss.clients.forEach(function each(client) { if (client.readyState === WebSocket.OPEN) { client.send(data, { binary: isBinary }); @@ -314,7 +314,7 @@ import WebSocket, { WebSocketServer } from 'ws'; const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { - ws.on('message', function incoming(data, isBinary) { + ws.on('message', function message(data, isBinary) { wss.clients.forEach(function each(client) { if (client !== ws && client.readyState === WebSocket.OPEN) { client.send(data, { binary: isBinary }); @@ -342,7 +342,7 @@ ws.on('close', function close() { console.log('disconnected'); }); -ws.on('message', function incoming(data) { +ws.on('message', function message(data) { console.log(`Roundtrip time: ${Date.now() - data} ms`); setTimeout(function timeout() { From 4916d03ad84fe0e609ce3d4286e7637f193b5004 Mon Sep 17 00:00:00 2001 From: Austin Cheney Date: Tue, 16 Nov 2021 13:00:16 -0600 Subject: [PATCH 114/328] [minor] Allow to write frames with up to 2^48 - 1 bytes of data (#1973) --- lib/sender.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/sender.js b/lib/sender.js index c1e357c78..58d03ce0a 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -75,8 +75,9 @@ class Sender { if (payloadLength === 126) { target.writeUInt16BE(data.length, 2); } else if (payloadLength === 127) { - target.writeUInt32BE(0, 2); - target.writeUInt32BE(data.length, 6); + target[2] = 0; + target[3] = 0; + target.writeUIntBE(data.length, 4, 6); } if (!options.mask) return [target, data]; From 89d81e86703f4a494373154bd91a614668d994af Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 16 Nov 2021 20:08:00 +0100 Subject: [PATCH 115/328] [minor] Fix nit --- lib/sender.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/sender.js b/lib/sender.js index 58d03ce0a..4490a623b 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -75,8 +75,7 @@ class Sender { if (payloadLength === 126) { target.writeUInt16BE(data.length, 2); } else if (payloadLength === 127) { - target[2] = 0; - target[3] = 0; + target[2] = target[3] = 0; target.writeUIntBE(data.length, 4, 6); } From 5a905e49be91203c04a2546bfc292717b8e9bee9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 20 Nov 2021 08:07:59 +0100 Subject: [PATCH 116/328] [minor] Add missing label to the issue form Fixes #1974 --- .github/ISSUE_TEMPLATE/bug_report.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9f437067f..6c12fea14 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,4 +1,4 @@ -name: 'Bug report' +name: Bug report description: Create a bug report body: - type: markdown @@ -12,6 +12,10 @@ body: Please fill in as much of the template below as you're able. - type: checkboxes attributes: + label: Is there an existing issue for this? + description: + Please search to see if an issue already exists for the bug you + encountered. options: - label: I've searched for any related issues and avoided creating a From b8186dd11577979d5870a933fc0ce4ac29f893eb Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 22 Nov 2021 15:27:29 +0100 Subject: [PATCH 117/328] [fix] Do not throw if the redirect URL is invalid (#1980) If the redirect URL is invalid, then emit the error instead of throwing it, otherwise there is no way to handle it. Closes #1975 --- lib/websocket.js | 50 ++++++++++++++++++++++++++++++++---------- test/websocket.test.js | 47 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 818f26914..53aac9a57 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -630,19 +630,26 @@ function initAsClient(websocket, address, protocols, options) { const isSecure = parsedUrl.protocol === 'wss:'; const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; + let invalidURLMessage; if (parsedUrl.protocol !== 'ws:' && !isSecure && !isUnixSocket) { - throw new SyntaxError( - 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"' - ); + invalidURLMessage = + 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; + } else if (isUnixSocket && !parsedUrl.pathname) { + invalidURLMessage = "The URL's pathname is empty"; + } else if (parsedUrl.hash) { + invalidURLMessage = 'The URL contains a fragment identifier'; } - if (isUnixSocket && !parsedUrl.pathname) { - throw new SyntaxError("The URL's pathname is empty"); - } + if (invalidURLMessage) { + const err = new SyntaxError(invalidURLMessage); - if (parsedUrl.hash) { - throw new SyntaxError('The URL contains a fragment identifier'); + if (websocket._redirects === 0) { + throw err; + } else { + emitErrorAndClose(websocket, err); + return; + } } const defaultPort = isSecure ? 443 : 80; @@ -724,9 +731,7 @@ function initAsClient(websocket, address, protocols, options) { if (req === null || req.aborted) return; req = websocket._req = null; - websocket._readyState = WebSocket.CLOSING; - websocket.emit('error', err); - websocket.emitClose(); + emitErrorAndClose(websocket, err); }); req.on('response', (res) => { @@ -746,7 +751,15 @@ function initAsClient(websocket, address, protocols, options) { req.abort(); - const addr = new URL(location, address); + let addr; + + try { + addr = new URL(location, address); + } catch (e) { + const err = new SyntaxError(`Invalid URL: ${location}`); + emitErrorAndClose(websocket, err); + return; + } initAsClient(websocket, addr, protocols, options); } else if (!websocket.emit('unexpected-response', req, res)) { @@ -849,6 +862,19 @@ function initAsClient(websocket, address, protocols, options) { }); } +/** + * Emit the `'error'` and `'close'` event. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {Error} The error to emit + * @private + */ +function emitErrorAndClose(websocket, err) { + websocket._readyState = WebSocket.CLOSING; + websocket.emit('error', err); + websocket.emitClose(); +} + /** * Create a `net.Socket` and initiate a connection. * diff --git a/test/websocket.test.js b/test/websocket.test.js index 4b85c7948..4703debef 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1028,6 +1028,53 @@ describe('WebSocket', () => { ws.on('close', () => done()); }); }); + + it('emits an error if the redirect URL is invalid (1/2)', (done) => { + const onUpgrade = (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: ws://\r\n\r\n'); + }; + + server.on('upgrade', onUpgrade); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + followRedirects: true + }); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof SyntaxError); + assert.strictEqual(err.message, 'Invalid URL: ws://'); + assert.strictEqual(ws._redirects, 1); + + server.removeListener('upgrade', onUpgrade); + ws.on('close', () => done()); + }); + }); + + it('emits an error if the redirect URL is invalid (2/2)', (done) => { + const onUpgrade = (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: http://localhost\r\n\r\n'); + }; + + server.on('upgrade', onUpgrade); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + followRedirects: true + }); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof SyntaxError); + assert.strictEqual( + err.message, + 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"' + ); + assert.strictEqual(ws._redirects, 1); + + server.removeListener('upgrade', onUpgrade); + ws.on('close', () => done()); + }); + }); }); describe('Connection with query string', () => { From ed2b803905289fc57616c3aef6b1690a3ca282b9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 22 Nov 2021 21:32:18 +0100 Subject: [PATCH 118/328] [fix] Resume the socket in the `CLOSING` state When the value of the `readyState` attribute is `CLOSING`, the internal socket might still be open. Resume it to read any remaining data and to allow the connection to be closed cleanly. --- lib/stream.js | 5 ++++- test/create-websocket-stream.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/stream.js b/lib/stream.js index 0f85ba55d..9622a5ac6 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -155,7 +155,10 @@ function createWebSocketStream(ws, options) { }; duplex._read = function () { - if (ws.readyState === ws.OPEN && !resumeOnReceiverDrain) { + if ( + (ws.readyState === ws.OPEN || ws.readyState === ws.CLOSING) && + !resumeOnReceiverDrain + ) { resumeOnReceiverDrain = true; if (!ws._receiver._writableState.needDrain) ws._socket.resume(); } diff --git a/test/create-websocket-stream.test.js b/test/create-websocket-stream.test.js index 8aee1a18a..4d51958cd 100644 --- a/test/create-websocket-stream.test.js +++ b/test/create-websocket-stream.test.js @@ -568,5 +568,31 @@ describe('createWebSocketStream', () => { ws.close(); }); }); + + it('resumes the socket if `readyState` is `CLOSING`', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + const duplex = createWebSocketStream(ws); + + ws.on('message', () => { + assert.ok(ws._socket.isPaused()); + + duplex.on('close', () => { + wss.close(done); + }); + + duplex.end(); + + process.nextTick(() => { + assert.strictEqual(ws.readyState, WebSocket.CLOSING); + duplex.resume(); + }); + }); + }); + + wss.on('connection', (ws) => { + ws.send(randomBytes(16 * 1024)); + }); + }); }); }); From 0a8c7a9c4f5ae357b094fb586c90ee5db8793fcc Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 23 Nov 2021 14:50:20 +0100 Subject: [PATCH 119/328] [api] Add `WebSocket#pause()` and `WebSocket#resume()` Add ability to pause and resume a `WebSocket`. --- doc/ws.md | 20 +++++++ lib/stream.js | 28 +-------- lib/websocket.js | 47 ++++++++++++++- test/websocket.test.js | 130 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 27 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index fc14e7e97..20facffbb 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -31,15 +31,18 @@ - [websocket.bufferedAmount](#websocketbufferedamount) - [websocket.close([code[, reason]])](#websocketclosecode-reason) - [websocket.extensions](#websocketextensions) + - [websocket.isPaused](#websocketispaused) - [websocket.onclose](#websocketonclose) - [websocket.onerror](#websocketonerror) - [websocket.onmessage](#websocketonmessage) - [websocket.onopen](#websocketonopen) + - [websocket.pause()](#websocketpause) - [websocket.ping([data[, mask]][, callback])](#websocketpingdata-mask-callback) - [websocket.pong([data[, mask]][, callback])](#websocketpongdata-mask-callback) - [websocket.protocol](#websocketprotocol) - [websocket.readyState](#websocketreadystate) - [websocket.removeEventListener(type, listener)](#websocketremoveeventlistenertype-listener) + - [websocket.resume()](#websocketresume) - [websocket.send(data[, options][, callback])](#websocketsenddata-options-callback) - [websocket.terminate()](#websocketterminate) - [websocket.url](#websocketurl) @@ -409,6 +412,12 @@ following ways: Initiate a closing handshake. +### websocket.isPaused + +- {Boolean} + +Indicates whether the websocket is paused. + ### websocket.extensions - {Object} @@ -443,6 +452,12 @@ listener receives a `MessageEvent` named "message". An event listener to be called when the connection is established. The listener receives an `OpenEvent` named "open". +### websocket.pause() + +Pause the websocket causing it to stop emitting events. Some events can still be +emitted after this is called, until all buffered data is consumed. This method +is a noop if the ready state is `CONNECTING` or `CLOSED`. + ### websocket.ping([data[, mask]][, callback]) - `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The @@ -473,6 +488,11 @@ Send a pong. This method throws an error if the ready state is `CONNECTING`. The subprotocol selected by the server. +### websocket.resume() + +Make a paused socket resume emitting events. This method is a noop if the ready +state is `CONNECTING` or `CLOSED`. + ### websocket.readyState - {Number} diff --git a/lib/stream.js b/lib/stream.js index 9622a5ac6..230734b79 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -47,23 +47,8 @@ function duplexOnError(err) { * @public */ function createWebSocketStream(ws, options) { - let resumeOnReceiverDrain = true; let terminateOnDestroy = true; - function receiverOnDrain() { - if (resumeOnReceiverDrain) ws._socket.resume(); - } - - if (ws.readyState === ws.CONNECTING) { - ws.once('open', function open() { - ws._receiver.removeAllListeners('drain'); - ws._receiver.on('drain', receiverOnDrain); - }); - } else { - ws._receiver.removeAllListeners('drain'); - ws._receiver.on('drain', receiverOnDrain); - } - const duplex = new Duplex({ ...options, autoDestroy: false, @@ -76,10 +61,7 @@ function createWebSocketStream(ws, options) { const data = !isBinary && duplex._readableState.objectMode ? msg.toString() : msg; - if (!duplex.push(data)) { - resumeOnReceiverDrain = false; - ws._socket.pause(); - } + if (!duplex.push(data)) ws.pause(); }); ws.once('error', function error(err) { @@ -155,13 +137,7 @@ function createWebSocketStream(ws, options) { }; duplex._read = function () { - if ( - (ws.readyState === ws.OPEN || ws.readyState === ws.CLOSING) && - !resumeOnReceiverDrain - ) { - resumeOnReceiverDrain = true; - if (!ws._receiver._writableState.needDrain) ws._socket.resume(); - } + if (ws.isPaused) ws.resume(); }; duplex._write = function (chunk, encoding, callback) { diff --git a/lib/websocket.js b/lib/websocket.js index 53aac9a57..130b3dc58 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -58,6 +58,7 @@ class WebSocket extends EventEmitter { this._closeMessage = EMPTY_BUFFER; this._closeTimer = null; this._extensions = {}; + this._paused = false; this._protocol = ''; this._readyState = WebSocket.CONNECTING; this._receiver = null; @@ -124,6 +125,13 @@ class WebSocket extends EventEmitter { return Object.keys(this._extensions).join(); } + /** + * @type {Boolean} + */ + get isPaused() { + return this._paused; + } + /** * @type {Function} */ @@ -312,6 +320,23 @@ class WebSocket extends EventEmitter { ); } + /** + * Pause the socket. + * + * @public + */ + pause() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = true; + this._socket.pause(); + } + /** * Send a ping. * @@ -376,6 +401,23 @@ class WebSocket extends EventEmitter { this._sender.pong(data || EMPTY_BUFFER, mask, cb); } + /** + * Resume the socket. + * + * @public + */ + resume() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = false; + if (!this._receiver._writableState.needDrain) this._socket.resume(); + } + /** * Send a data message. * @@ -518,6 +560,7 @@ Object.defineProperty(WebSocket.prototype, 'CLOSED', { 'binaryType', 'bufferedAmount', 'extensions', + 'isPaused', 'protocol', 'readyState', 'url' @@ -1001,7 +1044,9 @@ function receiverOnConclude(code, reason) { * @private */ function receiverOnDrain() { - this[kWebSocket]._socket.resume(); + const websocket = this[kWebSocket]; + + if (!websocket.isPaused) websocket._socket.resume(); } /** diff --git a/test/websocket.test.js b/test/websocket.test.js index 4703debef..0d48887de 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -359,6 +359,39 @@ describe('WebSocket', () => { }); }); + describe('`isPaused`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'isPaused' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('indicates whether the websocket is paused', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.pause(); + assert.ok(ws.isPaused); + + ws.resume(); + assert.ok(!ws.isPaused); + + ws.close(); + wss.close(done); + }); + + assert.ok(!ws.isPaused); + }); + }); + }); + describe('`protocol`', () => { it('is enumerable and configurable', () => { const descriptor = Object.getOwnPropertyDescriptor( @@ -1109,6 +1142,51 @@ describe('WebSocket', () => { }); }); + describe('#pause', () => { + it('does nothing if `readyState` is `CONNECTING` or `CLOSED`', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + assert.strictEqual(ws.readyState, WebSocket.CONNECTING); + assert.ok(!ws.isPaused); + + ws.pause(); + assert.ok(!ws.isPaused); + + ws.on('open', () => { + ws.on('close', () => { + assert.strictEqual(ws.readyState, WebSocket.CLOSED); + + ws.pause(); + assert.ok(!ws.isPaused); + + wss.close(done); + }); + + ws.close(); + }); + }); + }); + + it('pauses the socket', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + assert.ok(!ws.isPaused); + assert.ok(!ws._socket.isPaused()); + + ws.pause(); + assert.ok(ws.isPaused); + assert.ok(ws._socket.isPaused()); + + ws.terminate(); + wss.close(done); + }); + }); + }); + describe('#ping', () => { it('throws an error if `readyState` is `CONNECTING`', () => { const ws = new WebSocket('ws://localhost', { @@ -1447,6 +1525,58 @@ describe('WebSocket', () => { }); }); + describe('#resume', () => { + it('does nothing if `readyState` is `CONNECTING` or `CLOSED`', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + assert.strictEqual(ws.readyState, WebSocket.CONNECTING); + assert.ok(!ws.isPaused); + + // Verify that no exception is thrown. + ws.resume(); + + ws.on('open', () => { + ws.pause(); + assert.ok(ws.isPaused); + + ws.on('close', () => { + assert.strictEqual(ws.readyState, WebSocket.CLOSED); + + ws.resume(); + assert.ok(ws.isPaused); + + wss.close(done); + }); + + ws.terminate(); + }); + }); + }); + + it('resumes the socket', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + assert.ok(!ws.isPaused); + assert.ok(!ws._socket.isPaused()); + + ws.pause(); + assert.ok(ws.isPaused); + assert.ok(ws._socket.isPaused()); + + ws.resume(); + assert.ok(!ws.isPaused); + assert.ok(!ws._socket.isPaused()); + + ws.close(); + wss.close(done); + }); + }); + }); + describe('#send', () => { it('throws an error if `readyState` is `CONNECTING`', () => { const ws = new WebSocket('ws://localhost', { From c82b08737fbe142dd910fc7e429399e23b95c6d6 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 23 Nov 2021 15:21:00 +0100 Subject: [PATCH 120/328] [dist] 8.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b9f7e41d..5e3d22197 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.2.3", + "version": "8.3.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From eb2e3a84a1f1c75428d7bde1578a00eed2809c88 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 19 Dec 2021 20:41:24 +0100 Subject: [PATCH 121/328] [feature] Introduce the `generateMask` option The `generateMask` option specifies a function that can be used to generate custom masking keys. Refs: https://github.com/websockets/ws/pull/1986 Refs: https://github.com/websockets/ws/pull/1988 Refs: https://github.com/websockets/ws/pull/1989 --- doc/ws.md | 4 ++++ lib/sender.js | 38 +++++++++++++++++++++++++++++++++++--- lib/websocket.js | 7 ++++++- test/websocket.test.js | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 20facffbb..d728ace82 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -270,6 +270,10 @@ This class represents a WebSocket. It extends the `EventEmitter`. - `options` {Object} - `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to `false`. + - `generateMask` {Function} The function used to generate the masking key. It + takes a `Buffer` that must be filled synchronously and is called before a + message is sent, for each message. By default the buffer is filled with + cryptographically strong random bytes. - `handshakeTimeout` {Number} Timeout in milliseconds for the handshake request. This is reset after every redirection. - `maxPayload` {Number} The maximum allowed message size in bytes. diff --git a/lib/sender.js b/lib/sender.js index 4490a623b..82cc662d6 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -11,7 +11,7 @@ const { EMPTY_BUFFER } = require('./constants'); const { isValidStatusCode } = require('./validation'); const { mask: applyMask, toBuffer } = require('./buffer-util'); -const mask = Buffer.alloc(4); +const maskBuffer = Buffer.alloc(4); /** * HyBi Sender implementation. @@ -22,9 +22,17 @@ class Sender { * * @param {(net.Socket|tls.Socket)} socket The connection socket * @param {Object} [extensions] An object containing the negotiated extensions + * @param {Function} [generateMask] The function used to generate the masking + * key */ - constructor(socket, extensions) { + constructor(socket, extensions, generateMask) { this._extensions = extensions || {}; + + if (generateMask) { + this._generateMask = generateMask; + this._maskBuffer = Buffer.alloc(4); + } + this._socket = socket; this._firstFragment = true; @@ -42,8 +50,12 @@ class Sender { * @param {Object} options Options object * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key * @param {Number} options.opcode The opcode * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be * modified @@ -81,7 +93,13 @@ class Sender { if (!options.mask) return [target, data]; - randomFillSync(mask, 0, 4); + const mask = options.maskBuffer ? options.maskBuffer : maskBuffer; + + if (options.generateMask) { + options.generateMask(mask); + } else { + randomFillSync(mask, 0, 4); + } target[1] |= 0x80; target[offset - 4] = mask[0]; @@ -156,6 +174,8 @@ class Sender { rsv1: false, opcode: 0x08, mask, + maskBuffer: this._maskBuffer, + generateMask: this._generateMask, readOnly: false }), cb @@ -200,6 +220,8 @@ class Sender { rsv1: false, opcode: 0x09, mask, + maskBuffer: this._maskBuffer, + generateMask: this._generateMask, readOnly }), cb @@ -244,6 +266,8 @@ class Sender { rsv1: false, opcode: 0x0a, mask, + maskBuffer: this._maskBuffer, + generateMask: this._generateMask, readOnly }), cb @@ -299,6 +323,8 @@ class Sender { rsv1, opcode, mask: options.mask, + maskBuffer: this._maskBuffer, + generateMask: this._generateMask, readOnly: toBuffer.readOnly }; @@ -314,6 +340,8 @@ class Sender { rsv1: false, opcode, mask: options.mask, + maskBuffer: this._maskBuffer, + generateMask: this._generateMask, readOnly: toBuffer.readOnly }), cb @@ -331,8 +359,12 @@ class Sender { * @param {Number} options.opcode The opcode * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the diff --git a/lib/websocket.js b/lib/websocket.js index 130b3dc58..57710f4e1 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -192,6 +192,8 @@ class WebSocket extends EventEmitter { * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Object} options Options object + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Number} [options.maxPayload=0] The maximum allowed message size * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or * not to skip UTF-8 validation for text and close messages @@ -206,7 +208,7 @@ class WebSocket extends EventEmitter { skipUTF8Validation: options.skipUTF8Validation }); - this._sender = new Sender(socket, this._extensions); + this._sender = new Sender(socket, this._extensions, options.generateMask); this._receiver = receiver; this._socket = socket; @@ -613,6 +615,8 @@ module.exports = WebSocket; * @param {Object} [options] Connection options * @param {Boolean} [options.followRedirects=false] Whether or not to follow * redirects + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the * handshake request * @param {Number} [options.maxPayload=104857600] The maximum allowed message @@ -899,6 +903,7 @@ function initAsClient(websocket, address, protocols, options) { } websocket.setSocket(socket, head, { + generateMask: opts.generateMask, maxPayload: opts.maxPayload, skipUTF8Validation: opts.skipUTF8Validation }); diff --git a/test/websocket.test.js b/test/websocket.test.js index 0d48887de..5f9392d84 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -126,6 +126,41 @@ describe('WebSocket', () => { /^RangeError: Unsupported protocol version: 1000 \(supported versions: 8, 13\)$/ ); }); + + it('honors the `generateMask` option', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + generateMask() {} + }); + + ws.on('open', () => { + ws.send('foo'); + }); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1005); + assert.deepStrictEqual(reason, EMPTY_BUFFER); + + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + const chunks = []; + + ws._socket.prependListener('data', (chunk) => { + chunks.push(chunk); + }); + + ws.on('message', () => { + assert.ok( + Buffer.concat(chunks).slice(2, 6).equals(Buffer.alloc(4)) + ); + + ws.close(); + }); + }); + }); }); }); From 35d45c2a4fead953654ae7bcf029cdf6d2590121 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 19 Dec 2021 20:49:13 +0100 Subject: [PATCH 122/328] [perf] Skip masking and unmasking if the masking key is zero --- lib/receiver.js | 8 +++++++- lib/sender.js | 32 ++++++++++++++++++++++---------- test/websocket.test.js | 11 +++++++---- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/lib/receiver.js b/lib/receiver.js index e11e26618..2d29d62bb 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -417,7 +417,13 @@ class Receiver extends Writable { } data = this.consume(this._payloadLength); - if (this._masked) unmask(data, this._mask); + + if ( + this._masked && + (this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0 + ) { + unmask(data, this._mask); + } } if (this._opcode > 0x07) return this.controlMessage(data); diff --git a/lib/sender.js b/lib/sender.js index 82cc662d6..2417656d7 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -65,8 +65,26 @@ class Sender { * @public */ static frame(data, options) { - const merge = options.mask && options.readOnly; - let offset = options.mask ? 6 : 2; + let mask; + let merge = false; + let offset = 2; + let skipMasking = false; + + if (options.mask) { + mask = options.maskBuffer || maskBuffer; + + if (options.generateMask) { + options.generateMask(mask); + } else { + randomFillSync(mask, 0, 4); + } + + skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; + if (options.readOnly && !skipMasking) merge = true; + + offset = 6; + } + let payloadLength = data.length; if (data.length >= 65536) { @@ -93,20 +111,14 @@ class Sender { if (!options.mask) return [target, data]; - const mask = options.maskBuffer ? options.maskBuffer : maskBuffer; - - if (options.generateMask) { - options.generateMask(mask); - } else { - randomFillSync(mask, 0, 4); - } - target[1] |= 0x80; target[offset - 4] = mask[0]; target[offset - 3] = mask[1]; target[offset - 2] = mask[2]; target[offset - 1] = mask[3]; + if (skipMasking) return [target, data]; + if (merge) { applyMask(data, mask, target, offset, data.length); return [target]; diff --git a/test/websocket.test.js b/test/websocket.test.js index 5f9392d84..16cb33c55 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -128,13 +128,14 @@ describe('WebSocket', () => { }); it('honors the `generateMask` option', (done) => { + const data = Buffer.from('foo'); const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { generateMask() {} }); ws.on('open', () => { - ws.send('foo'); + ws.send(data); }); ws.on('close', (code, reason) => { @@ -152,9 +153,11 @@ describe('WebSocket', () => { chunks.push(chunk); }); - ws.on('message', () => { - assert.ok( - Buffer.concat(chunks).slice(2, 6).equals(Buffer.alloc(4)) + ws.on('message', (message) => { + assert.deepStrictEqual(message, data); + assert.deepStrictEqual( + Buffer.concat(chunks).slice(2, 6), + Buffer.alloc(4) ); ws.close(); From 00c34d726dca1c558fe5ee5e346979159b2297fe Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 20 Dec 2021 21:07:56 +0100 Subject: [PATCH 123/328] [dist] 8.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e3d22197..813155221 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.3.0", + "version": "8.4.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 5edf1f4a1b1750109c1bb56eff7ad78902eee7dc Mon Sep 17 00:00:00 2001 From: jugglinmike Date: Thu, 30 Dec 2021 14:14:29 -0500 Subject: [PATCH 124/328] [doc] Clarify interpretation of `verifyClient` (#1994) Prior to this commit, the documentation for the `verifyClient` option was somewhat confusing due to its use of the passive voice and an uncommon interpretation of the term "arguments". Explain the way `ws` interprets the value of `verifyClient` using an active voice and more traditional meanings of "parameters" and "arguments". --- doc/ws.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index d728ace82..38e769b06 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -99,7 +99,7 @@ to share a single HTTP/S server between multiple WebSocket servers. > more details. If `verifyClient` is not set then the handshake is automatically accepted. If it -is provided with a single argument then that is: +has a single parameter then `ws` will invoke it with the following argument: - `info` {Object} - `origin` {String} The value in the Origin header indicated by the client. @@ -110,7 +110,8 @@ is provided with a single argument then that is: The return value (`Boolean`) of the function determines whether or not to accept the handshake. -if `verifyClient` is provided with two arguments then those are: +If `verifyClient` has two parameters then `ws` will invoke it with the following +arguments: - `info` {Object} Same as above. - `cb` {Function} A callback that must be called by the user upon inspection of From d2c935a477fa6999c8fa85b89dfae27b85b807e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Tue, 4 Jan 2022 07:48:06 +0100 Subject: [PATCH 125/328] [doc] Fix typo in `WebSocketServer` description (#1996) --- doc/ws.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index 38e769b06..62fd36ca5 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -91,7 +91,7 @@ must be provided or an error is thrown. An HTTP server is automatically created, started, and used if `port` is set. To use an external HTTP/S server instead, specify only `server` or `noServer`. In this case the HTTP/S server must be started manually. The "noServer" mode allows the WebSocket server to be -completly detached from the HTTP/S server. This makes it possible, for example, +completely detached from the HTTP/S server. This makes it possible, for example, to share a single HTTP/S server between multiple WebSocket servers. > **NOTE:** Use of `verifyClient` is discouraged. Rather handle client From 4081a368ffee896aa12bcc8c241523257ee34387 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 6 Jan 2022 15:36:53 +0100 Subject: [PATCH 126/328] [test] Do not call the `done` callback prematurely --- test/sender.test.js | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/test/sender.test.js b/test/sender.test.js index 662d2941d..845ff7ad1 100644 --- a/test/sender.test.js +++ b/test/sender.test.js @@ -50,12 +50,22 @@ describe('Sender', () => { describe('#send', () => { it('compresses data if compress option is enabled', (done) => { + const chunks = []; const perMessageDeflate = new PerMessageDeflate(); - let count = 0; const mockSocket = new MockSocket({ - write: (data) => { - assert.strictEqual(data[0] & 0x40, 0x40); - if (++count === 3) done(); + write: (chunk) => { + chunks.push(chunk); + if (chunks.length !== 6) return; + + assert.strictEqual(chunks[0].length, 2); + assert.strictEqual(chunks[0][0] & 0x40, 0x40); + + assert.strictEqual(chunks[2].length, 2); + assert.strictEqual(chunks[2][0] & 0x40, 0x40); + + assert.strictEqual(chunks[4].length, 2); + assert.strictEqual(chunks[4][0] & 0x40, 0x40); + done(); } }); const sender = new Sender(mockSocket, { @@ -74,10 +84,16 @@ describe('Sender', () => { describe('when context takeover is disabled', () => { it('honors the compression threshold', (done) => { + const chunks = []; const perMessageDeflate = new PerMessageDeflate(); const mockSocket = new MockSocket({ - write: (data) => { - assert.notStrictEqual(data[0] & 0x40, 0x40); + write: (chunk) => { + chunks.push(chunk); + if (chunks.length !== 2) return; + + assert.strictEqual(chunks[0].length, 2); + assert.notStrictEqual(chunk[0][0] & 0x40, 0x40); + assert.deepStrictEqual(chunks[1], Buffer.from('hi')); done(); } }); From ad3fe6dc4fb4669b62e22a94d0ef336b5000bec7 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 6 Jan 2022 18:25:52 +0100 Subject: [PATCH 127/328] [test] Improve test title --- test/sender.test.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/sender.test.js b/test/sender.test.js index 845ff7ad1..ef69d6abb 100644 --- a/test/sender.test.js +++ b/test/sender.test.js @@ -2,9 +2,10 @@ const assert = require('assert'); -const PerMessageDeflate = require('../lib/permessage-deflate'); const extension = require('../lib/extension'); +const PerMessageDeflate = require('../lib/permessage-deflate'); const Sender = require('../lib/sender'); +const { EMPTY_BUFFER } = require('../lib/constants'); class MockSocket { constructor({ write } = {}) { @@ -35,8 +36,8 @@ describe('Sender', () => { assert.ok(buf.equals(Buffer.from([1, 2, 3, 4, 5]))); }); - it('sets RSV1 bit if compressed', () => { - const list = Sender.frame(Buffer.from('hi'), { + it('honors the `rsv1` option', () => { + const list = Sender.frame(EMPTY_BUFFER, { readOnly: false, mask: false, rsv1: true, From 91f3c07b26795cf3e4e221ae468d298ae5a3be2d Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 9 Jan 2022 15:32:28 +0100 Subject: [PATCH 128/328] [minor] Replace echo.websocket.org with websocket-echo.com The echo.websocket.org service is no longer available. --- README.md | 16 ++++++---------- test/websocket.integration.js | 6 ++---- test/websocket.test.js | 4 ++-- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 82ca8db49..cab335b7b 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ can use one of the many wrappers available on npm, like - [Multiple servers sharing a single HTTP/S server](#multiple-servers-sharing-a-single-https-server) - [Client authentication](#client-authentication) - [Server broadcast](#server-broadcast) - - [echo.websocket.org demo](#echowebsocketorg-demo) + - [Round-trip time](#round-trip-time) - [Use the Node.js streams API](#use-the-nodejs-streams-api) - [Other examples](#other-examples) - [FAQ](#faq) @@ -324,14 +324,12 @@ wss.on('connection', function connection(ws) { }); ``` -### echo.websocket.org demo +### Round-trip time ```js import WebSocket from 'ws'; -const ws = new WebSocket('wss://echo.websocket.org/', { - origin: 'https://websocket.org' -}); +const ws = new WebSocket('wss://websocket-echo.com/'); ws.on('open', function open() { console.log('connected'); @@ -343,7 +341,7 @@ ws.on('close', function close() { }); ws.on('message', function message(data) { - console.log(`Roundtrip time: ${Date.now() - data} ms`); + console.log(`Round-trip time: ${Date.now() - data} ms`); setTimeout(function timeout() { ws.send(Date.now()); @@ -356,9 +354,7 @@ ws.on('message', function message(data) { ```js import WebSocket, { createWebSocketStream } from 'ws'; -const ws = new WebSocket('wss://echo.websocket.org/', { - origin: 'https://websocket.org' -}); +const ws = new WebSocket('wss://websocket-echo.com/'); const duplex = createWebSocketStream(ws, { encoding: 'utf8' }); @@ -457,7 +453,7 @@ function heartbeat() { }, 30000 + 1000); } -const client = new WebSocket('wss://echo.websocket.org/'); +const client = new WebSocket('wss://websocket-echo.com/'); client.on('open', heartbeat); client.on('ping', heartbeat); diff --git a/test/websocket.integration.js b/test/websocket.integration.js index 5ff87a640..e1fe7f558 100644 --- a/test/websocket.integration.js +++ b/test/websocket.integration.js @@ -6,8 +6,7 @@ const WebSocket = require('..'); describe('WebSocket', () => { it('communicates successfully with echo service (ws)', (done) => { - const ws = new WebSocket('ws://echo.websocket.org/', { - origin: 'http://www.websocket.org', + const ws = new WebSocket('ws://websocket-echo.com/', { protocolVersion: 13 }); const str = Date.now().toString(); @@ -27,8 +26,7 @@ describe('WebSocket', () => { }); it('communicates successfully with echo service (wss)', (done) => { - const ws = new WebSocket('wss://echo.websocket.org/', { - origin: 'https://www.websocket.org', + const ws = new WebSocket('wss://websocket-echo.com/', { protocolVersion: 13 }); const str = Date.now().toString(); diff --git a/test/websocket.test.js b/test/websocket.test.js index 16cb33c55..b46f89281 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -33,7 +33,7 @@ describe('WebSocket', () => { ); assert.throws( - () => new WebSocket('https://echo.websocket.org'), + () => new WebSocket('https://websocket-echo.com'), /^SyntaxError: The URL's protocol must be one of "ws:", "wss:", or "ws\+unix:"$/ ); @@ -43,7 +43,7 @@ describe('WebSocket', () => { ); assert.throws( - () => new WebSocket('wss://echo.websocket.org#foo'), + () => new WebSocket('wss://websocket-echo.com#foo'), /^SyntaxError: The URL contains a fragment identifier$/ ); }); From 8de448fbd105deeecada886344ca58237d423a8c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 9 Jan 2022 16:10:24 +0100 Subject: [PATCH 129/328] [test] Fix failing tests Refs: https://github.com/websockets/ws/commit/e173423c --- test/websocket.integration.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/test/websocket.integration.js b/test/websocket.integration.js index e1fe7f558..abd96c61e 100644 --- a/test/websocket.integration.js +++ b/test/websocket.integration.js @@ -9,18 +9,22 @@ describe('WebSocket', () => { const ws = new WebSocket('ws://websocket-echo.com/', { protocolVersion: 13 }); - const str = Date.now().toString(); let dataReceived = false; - ws.on('open', () => ws.send(str)); + ws.on('open', () => { + ws.send('hello'); + }); + ws.on('close', () => { assert.ok(dataReceived); done(); }); - ws.on('message', (data) => { + + ws.on('message', (message, isBinary) => { dataReceived = true; - assert.strictEqual(data, str); + assert.ok(!isBinary); + assert.strictEqual(message.toString(), 'hello'); ws.close(); }); }); @@ -29,18 +33,22 @@ describe('WebSocket', () => { const ws = new WebSocket('wss://websocket-echo.com/', { protocolVersion: 13 }); - const str = Date.now().toString(); let dataReceived = false; - ws.on('open', () => ws.send(str)); + ws.on('open', () => { + ws.send('hello'); + }); + ws.on('close', () => { assert.ok(dataReceived); done(); }); - ws.on('message', (data) => { + + ws.on('message', (message, isBinary) => { dataReceived = true; - assert.strictEqual(data, str); + assert.ok(!isBinary); + assert.strictEqual(message.toString(), 'hello'); ws.close(); }); }); From 5b7fbb000972edd34b4882384e1185f033000220 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 13 Jan 2022 20:26:14 +0100 Subject: [PATCH 130/328] [perf] Reduce buffer allocations (#2000) Do not convert strings to `Buffer`s if data does not need to be masked. Refs: https://github.com/websockets/ws/pull/1998 --- lib/permessage-deflate.js | 4 +- lib/sender.js | 232 ++++++++++++++++++++------------------ test/sender.test.js | 29 +++-- 3 files changed, 146 insertions(+), 119 deletions(-) diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index 504069719..94603c98d 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -313,7 +313,7 @@ class PerMessageDeflate { /** * Compress data. Concurrency limited. * - * @param {Buffer} data Data to compress + * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @public @@ -395,7 +395,7 @@ class PerMessageDeflate { /** * Compress data. * - * @param {Buffer} data Data to compress + * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @private diff --git a/lib/sender.js b/lib/sender.js index 2417656d7..d331f653a 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -11,6 +11,7 @@ const { EMPTY_BUFFER } = require('./constants'); const { isValidStatusCode } = require('./validation'); const { mask: applyMask, toBuffer } = require('./buffer-util'); +const kByteLength = Symbol('kByteLength'); const maskBuffer = Buffer.alloc(4); /** @@ -46,7 +47,7 @@ class Sender { /** * Frames a piece of data according to the HyBi WebSocket protocol. * - * @param {Buffer} data The data to frame + * @param {(Buffer|String)} data The data to frame * @param {Object} options Options object * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit @@ -61,7 +62,7 @@ class Sender { * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit - * @return {Buffer[]} The framed data as a list of `Buffer` instances + * @return {(Buffer|String)[]} The framed data * @public */ static frame(data, options) { @@ -80,12 +81,27 @@ class Sender { } skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; - if (options.readOnly && !skipMasking) merge = true; - offset = 6; } - let payloadLength = data.length; + let dataLength; + + if (typeof data === 'string') { + if ( + (!options.mask || skipMasking) && + options[kByteLength] !== undefined + ) { + dataLength = options[kByteLength]; + } else { + data = Buffer.from(data); + dataLength = data.length; + } + } else { + dataLength = data.length; + merge = options.mask && options.readOnly && !skipMasking; + } + + let payloadLength = dataLength; if (data.length >= 65536) { offset += 8; @@ -95,7 +111,7 @@ class Sender { payloadLength = 126; } - const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); + const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset); target[0] = options.fin ? options.opcode | 0x80 : options.opcode; if (options.rsv1) target[0] |= 0x40; @@ -103,10 +119,10 @@ class Sender { target[1] = payloadLength; if (payloadLength === 126) { - target.writeUInt16BE(data.length, 2); + target.writeUInt16BE(dataLength, 2); } else if (payloadLength === 127) { target[2] = target[3] = 0; - target.writeUIntBE(data.length, 4, 6); + target.writeUIntBE(dataLength, 4, 6); } if (!options.mask) return [target, data]; @@ -164,36 +180,24 @@ class Sender { } } + const options = { + [kByteLength]: buf.length, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x08, + readOnly: false, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doClose, buf, mask, cb]); + this.enqueue([this.dispatch, buf, false, options, cb]); } else { - this.doClose(buf, mask, cb); + this.sendFrame(Sender.frame(buf, options), cb); } } - /** - * Frames and sends a close message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Function} [cb] Callback - * @private - */ - doClose(data, mask, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x08, - mask, - maskBuffer: this._maskBuffer, - generateMask: this._generateMask, - readOnly: false - }), - cb - ); - } - /** * Sends a ping message to the other peer. * @@ -203,43 +207,40 @@ class Sender { * @public */ ping(data, mask, cb) { - const buf = toBuffer(data); + let byteLength; + let readOnly; - if (buf.length > 125) { + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + + if (byteLength > 125) { throw new RangeError('The data size must not be greater than 125 bytes'); } + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x09, + readOnly, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]); + this.enqueue([this.dispatch, data, false, options, cb]); } else { - this.doPing(buf, mask, toBuffer.readOnly, cb); + this.sendFrame(Sender.frame(data, options), cb); } } - /** - * Frames and sends a ping message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified - * @param {Function} [cb] Callback - * @private - */ - doPing(data, mask, readOnly, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x09, - mask, - maskBuffer: this._maskBuffer, - generateMask: this._generateMask, - readOnly - }), - cb - ); - } - /** * Sends a pong message to the other peer. * @@ -249,43 +250,40 @@ class Sender { * @public */ pong(data, mask, cb) { - const buf = toBuffer(data); + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } - if (buf.length > 125) { + if (byteLength > 125) { throw new RangeError('The data size must not be greater than 125 bytes'); } + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x0a, + readOnly, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]); + this.enqueue([this.dispatch, data, false, options, cb]); } else { - this.doPong(buf, mask, toBuffer.readOnly, cb); + this.sendFrame(Sender.frame(data, options), cb); } } - /** - * Frames and sends a pong message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified - * @param {Function} [cb] Callback - * @private - */ - doPong(data, mask, readOnly, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x0a, - mask, - maskBuffer: this._maskBuffer, - generateMask: this._generateMask, - readOnly - }), - cb - ); - } - /** * Sends a data message to the other peer. * @@ -303,11 +301,22 @@ class Sender { * @public */ send(data, options, cb) { - const buf = toBuffer(data); const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; let opcode = options.binary ? 2 : 1; let rsv1 = options.compress; + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + if (this._firstFragment) { this._firstFragment = false; if ( @@ -319,7 +328,7 @@ class Sender { : 'client_no_context_takeover' ] ) { - rsv1 = buf.length >= perMessageDeflate._threshold; + rsv1 = byteLength >= perMessageDeflate._threshold; } this._compress = rsv1; } else { @@ -331,30 +340,32 @@ class Sender { if (perMessageDeflate) { const opts = { + [kByteLength]: byteLength, fin: options.fin, - rsv1, - opcode, + generateMask: this._generateMask, mask: options.mask, maskBuffer: this._maskBuffer, - generateMask: this._generateMask, - readOnly: toBuffer.readOnly + opcode, + readOnly, + rsv1 }; if (this._deflating) { - this.enqueue([this.dispatch, buf, this._compress, opts, cb]); + this.enqueue([this.dispatch, data, this._compress, opts, cb]); } else { - this.dispatch(buf, this._compress, opts, cb); + this.dispatch(data, this._compress, opts, cb); } } else { this.sendFrame( - Sender.frame(buf, { + Sender.frame(data, { + [kByteLength]: byteLength, fin: options.fin, - rsv1: false, - opcode, + generateMask: this._generateMask, mask: options.mask, maskBuffer: this._maskBuffer, - generateMask: this._generateMask, - readOnly: toBuffer.readOnly + opcode, + readOnly, + rsv1: false }), cb ); @@ -362,13 +373,12 @@ class Sender { } /** - * Dispatches a data message. + * Dispatches a message. * - * @param {Buffer} data The message to send + * @param {(Buffer|String)} data The message to send * @param {Boolean} [compress=false] Specifies whether or not to compress * `data` * @param {Object} options Options object - * @param {Number} options.opcode The opcode * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit * @param {Function} [options.generateMask] The function used to generate the @@ -377,6 +387,7 @@ class Sender { * `data` * @param {Buffer} [options.maskBuffer] The buffer used to store the masking * key + * @param {Number} options.opcode The opcode * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the @@ -392,7 +403,7 @@ class Sender { const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; - this._bufferedBytes += data.length; + this._bufferedBytes += options[kByteLength]; this._deflating = true; perMessageDeflate.compress(data, options.fin, (_, buf) => { if (this._socket.destroyed) { @@ -403,7 +414,8 @@ class Sender { if (typeof cb === 'function') cb(err); for (let i = 0; i < this._queue.length; i++) { - const callback = this._queue[i][4]; + const params = this._queue[i]; + const callback = params[params.length - 1]; if (typeof callback === 'function') callback(err); } @@ -411,7 +423,7 @@ class Sender { return; } - this._bufferedBytes -= data.length; + this._bufferedBytes -= options[kByteLength]; this._deflating = false; options.readOnly = false; this.sendFrame(Sender.frame(buf, options), cb); @@ -428,7 +440,7 @@ class Sender { while (!this._deflating && this._queue.length) { const params = this._queue.shift(); - this._bufferedBytes -= params[1].length; + this._bufferedBytes -= params[3][kByteLength]; Reflect.apply(params[0], this, params.slice(1)); } } @@ -440,7 +452,7 @@ class Sender { * @private */ enqueue(params) { - this._bufferedBytes += params[1].length; + this._bufferedBytes += params[3][kByteLength]; this._queue.push(params); } diff --git a/test/sender.test.js b/test/sender.test.js index ef69d6abb..532239fa1 100644 --- a/test/sender.test.js +++ b/test/sender.test.js @@ -47,6 +47,19 @@ describe('Sender', () => { assert.strictEqual(list[0][0] & 0x40, 0x40); }); + + it('accepts a string as first argument', () => { + const list = Sender.frame('€', { + readOnly: false, + rsv1: false, + mask: false, + opcode: 1, + fin: true + }); + + assert.deepStrictEqual(list[0], Buffer.from('8103', 'hex')); + assert.deepStrictEqual(list[1], Buffer.from('e282ac', 'hex')); + }); }); describe('#send', () => { @@ -94,7 +107,7 @@ describe('Sender', () => { assert.strictEqual(chunks[0].length, 2); assert.notStrictEqual(chunk[0][0] & 0x40, 0x40); - assert.deepStrictEqual(chunks[1], Buffer.from('hi')); + assert.strictEqual(chunks[1], 'hi'); done(); } }); @@ -246,11 +259,12 @@ describe('Sender', () => { if (count % 2) { assert.ok(data.equals(Buffer.from([0x89, 0x02]))); - } else { + } else if (count < 8) { assert.ok(data.equals(Buffer.from([0x68, 0x69]))); + } else { + assert.strictEqual(data, 'hi'); + done(); } - - if (count === 8) done(); } }); const sender = new Sender(mockSocket, { @@ -278,11 +292,12 @@ describe('Sender', () => { if (count % 2) { assert.ok(data.equals(Buffer.from([0x8a, 0x02]))); - } else { + } else if (count < 8) { assert.ok(data.equals(Buffer.from([0x68, 0x69]))); + } else { + assert.strictEqual(data, 'hi'); + done(); } - - if (count === 8) done(); } }); const sender = new Sender(mockSocket, { From 6ebfeb8be70aee060852134c953794864ccf31dd Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 13 Jan 2022 20:40:28 +0100 Subject: [PATCH 131/328] [dist] 8.4.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 813155221..c4ab7a8d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.4.0", + "version": "8.4.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 0c0754b897f62a68d7da3600f96924fab54e4f8f Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 14 Jan 2022 15:26:07 +0100 Subject: [PATCH 132/328] [fix] Use the byte length of the data (#2004) Ensure that the correct length is used when framing the data. --- lib/sender.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/sender.js b/lib/sender.js index d331f653a..c84885362 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -103,10 +103,10 @@ class Sender { let payloadLength = dataLength; - if (data.length >= 65536) { + if (dataLength >= 65536) { offset += 8; payloadLength = 127; - } else if (data.length > 125) { + } else if (dataLength > 125) { offset += 2; payloadLength = 126; } @@ -136,11 +136,11 @@ class Sender { if (skipMasking) return [target, data]; if (merge) { - applyMask(data, mask, target, offset, data.length); + applyMask(data, mask, target, offset, dataLength); return [target]; } - applyMask(data, mask, data, 0, data.length); + applyMask(data, mask, data, 0, dataLength); return [target, data]; } From 33fd1016ec4266464027ab8d4a6e06649c93e938 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 14 Jan 2022 15:28:24 +0100 Subject: [PATCH 133/328] [dist] 8.4.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4ab7a8d4..723ec1b36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.4.1", + "version": "8.4.2", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 8a7016dc2fe4d9d63c428c67588d7c1f33a72e5c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 30 Jan 2022 20:27:45 +0100 Subject: [PATCH 134/328] [test] Simplify test --- test/websocket.test.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index b46f89281..fe15ed7ca 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2757,18 +2757,17 @@ describe('WebSocket', () => { requestCert: true }); - let success = false; - const wss = new WebSocket.Server({ - verifyClient: (info) => { - success = !!info.req.client.authorized; - return true; - }, - server - }); + const wss = new WebSocket.Server({ noServer: true }); - wss.on('connection', () => { - assert.ok(success); - server.close(done); + server.on('upgrade', (request, socket, head) => { + assert.ok(socket.authorized); + + wss.handleUpgrade(request, socket, head, (ws) => { + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + server.close(done); + }); + }); }); server.listen(0, () => { From e1ddacce06d302884e674f50c12cb376e000c6ea Mon Sep 17 00:00:00 2001 From: Sergey Bakulin Date: Tue, 1 Feb 2022 22:25:02 +0300 Subject: [PATCH 135/328] [feature] Introduce the `WebSocket` option (#2007) Add the ability to use a custom class that extends the `WebSocket` class. --- doc/ws.md | 2 ++ lib/websocket-server.js | 5 ++++- test/websocket-server.test.js | 26 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index 62fd36ca5..3273c1f9e 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -84,6 +84,8 @@ This class represents a WebSocket server. It extends the `EventEmitter`. - `verifyClient` {Function} A function which can be used to validate incoming connections. See description below. (Usage is discouraged: see [Issue #337](https://github.com/websockets/ws/issues/377#issuecomment-462152231)) + - `WebSocket` {Function} Specifies the `WebSocket` class to be used. It must + be extended from the original `WebSocket`. Defaults to `WebSocket`. - `callback` {Function} Create a new server instance. One and only one of `port`, `server` or `noServer` diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 3c7939f28..d0a29783f 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -49,6 +49,8 @@ class WebSocketServer extends EventEmitter { * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or * not to skip UTF-8 validation for text and close messages * @param {Function} [options.verifyClient] A hook to reject connections + * @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket` + * class to use. It must be the `WebSocket` class or class that extends it * @param {Function} [callback] A listener for the `listening` event */ constructor(options, callback) { @@ -67,6 +69,7 @@ class WebSocketServer extends EventEmitter { host: null, path: null, port: null, + WebSocket, ...options }; @@ -356,7 +359,7 @@ class WebSocketServer extends EventEmitter { `Sec-WebSocket-Accept: ${digest}` ]; - const ws = new WebSocket(null); + const ws = new this.options.WebSocket(null); if (protocols.size) { // diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index e3daf7e0b..fd494059f 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -89,6 +89,32 @@ describe('WebSocketServer', () => { wss.close(done); }); }); + + it('honors the `WebSocket` option', (done) => { + class CustomWebSocket extends WebSocket.WebSocket { + get foo() { + return 'foo'; + } + } + + const wss = new WebSocket.Server( + { + port: 0, + WebSocket: CustomWebSocket + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', ws.close); + } + ); + + wss.on('connection', (ws) => { + assert.ok(ws instanceof CustomWebSocket); + assert.strictEqual(ws.foo, 'foo'); + wss.close(done); + }); + }); }); it('emits an error if http server bind fails', (done) => { From 75fdfa9a2bb0f6f0cba2cd9e1b77b90cc1f5a6ff Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 4 Feb 2022 22:17:50 +0100 Subject: [PATCH 136/328] [test] Fix nits --- test/websocket.test.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index fe15ed7ca..1b1293fc6 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1101,11 +1101,9 @@ describe('WebSocket', () => { }); it('emits an error if the redirect URL is invalid (1/2)', (done) => { - const onUpgrade = (req, socket) => { + server.once('upgrade', (req, socket) => { socket.end('HTTP/1.1 302 Found\r\nLocation: ws://\r\n\r\n'); - }; - - server.on('upgrade', onUpgrade); + }); const ws = new WebSocket(`ws://localhost:${server.address().port}`, { followRedirects: true @@ -1117,17 +1115,14 @@ describe('WebSocket', () => { assert.strictEqual(err.message, 'Invalid URL: ws://'); assert.strictEqual(ws._redirects, 1); - server.removeListener('upgrade', onUpgrade); ws.on('close', () => done()); }); }); it('emits an error if the redirect URL is invalid (2/2)', (done) => { - const onUpgrade = (req, socket) => { + server.once('upgrade', (req, socket) => { socket.end('HTTP/1.1 302 Found\r\nLocation: http://localhost\r\n\r\n'); - }; - - server.on('upgrade', onUpgrade); + }); const ws = new WebSocket(`ws://localhost:${server.address().port}`, { followRedirects: true @@ -1142,7 +1137,6 @@ describe('WebSocket', () => { ); assert.strictEqual(ws._redirects, 1); - server.removeListener('upgrade', onUpgrade); ws.on('close', () => done()); }); }); From 6946f5fe781bafe99a36ed954904966203422b3d Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 7 Feb 2022 20:21:37 +0100 Subject: [PATCH 137/328] [security] Drop sensitive headers when following redirects (#2013) Do not forward the `Authorization` and `Cookie` headers if the redirect host is different from the original host. --- lib/websocket.js | 39 ++++++++++++++ test/websocket.test.js | 113 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/lib/websocket.js b/lib/websocket.js index 57710f4e1..6fff935cb 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -766,6 +766,45 @@ function initAsClient(websocket, address, protocols, options) { opts.path = parts[1]; } + if (opts.followRedirects) { + if (websocket._redirects === 0) { + websocket._originalHost = parsedUrl.host; + + const headers = options && options.headers; + + // + // Shallow copy the user provided options so that headers can be changed + // without mutating the original object. + // + options = { ...options, headers: {} }; + + if (headers) { + for (const [key, value] of Object.entries(headers)) { + options.headers[key.toLowerCase()] = value; + } + } + } else if (parsedUrl.host !== websocket._originalHost) { + // + // Match curl 7.77.0 behavior and drop the following headers. These + // headers are also dropped when following a redirect to a subdomain. + // + delete opts.headers.authorization; + delete opts.headers.cookie; + delete opts.headers.host; + opts.auth = undefined; + } + + // + // Match curl 7.77.0 behavior and make the first `Authorization` header win. + // If the `Authorization` header is set, then there is nothing to do as it + // will take precedence. + // + if (opts.auth && !options.headers.authorization) { + options.headers.authorization = + 'Basic ' + Buffer.from(opts.auth).toString('base64'); + } + } + let req = (websocket._req = get(opts)); if (opts.timeout) { diff --git a/test/websocket.test.js b/test/websocket.test.js index 1b1293fc6..2f2f4f529 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1140,6 +1140,119 @@ describe('WebSocket', () => { ws.on('close', () => done()); }); }); + + it('uses the first url userinfo when following redirects', (done) => { + const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); + const authorization = 'Basic Zm9vOmJhcg=='; + + server.once('upgrade', (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n'); + server.once('upgrade', (req, socket, head) => { + wss.handleUpgrade(req, socket, head, (ws, req) => { + assert.strictEqual(req.headers.authorization, authorization); + ws.close(); + }); + }); + }); + + const port = server.address().port; + const ws = new WebSocket(`ws://foo:bar@localhost:${port}`, { + followRedirects: true + }); + + assert.strictEqual(ws._req.getHeader('Authorization'), authorization); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://foo:bar@localhost:${port}/foo`); + assert.strictEqual(ws._redirects, 1); + + wss.close(done); + }); + }); + + describe('When the redirect host is different', () => { + it('drops the `auth` option', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const port = wss.address().port; + + server.once('upgrade', (req, socket) => { + socket.end( + `HTTP/1.1 302 Found\r\nLocation: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + auth: 'foo:bar', + followRedirects: true + }); + + assert.strictEqual( + ws._req.getHeader('Authorization'), + 'Basic Zm9vOmJhcg==' + ); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://localhost:${port}/`); + assert.strictEqual(ws._redirects, 1); + + wss.close(done); + }); + }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + ws.close(); + }); + }); + + it('drops the Authorization, Cookie, and Host headers', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const port = wss.address().port; + + server.once('upgrade', (req, socket) => { + socket.end( + `HTTP/1.1 302 Found\r\nLocation: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + headers: { + Authorization: 'Basic Zm9vOmJhcg==', + Cookie: 'foo=bar', + Host: 'foo' + }, + followRedirects: true + }); + + assert.strictEqual( + ws._req.getHeader('Authorization'), + 'Basic Zm9vOmJhcg==' + ); + assert.strictEqual(ws._req.getHeader('Cookie'), 'foo=bar'); + assert.strictEqual(ws._req.getHeader('Host'), 'foo'); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://localhost:${port}/`); + assert.strictEqual(ws._redirects, 1); + + wss.close(done); + }); + }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + assert.strictEqual(req.headers.cookie, undefined); + assert.strictEqual( + req.headers.host, + `localhost:${wss.address().port}` + ); + ws.close(); + }); + }); + }); }); describe('Connection with query string', () => { From c9d5436500fad16493a2cc62a0ce6daed83c9129 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 7 Feb 2022 20:23:55 +0100 Subject: [PATCH 138/328] [dist] 8.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 723ec1b36..57ddc7e95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.4.2", + "version": "8.5.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From a0524e15586c7a51546a772bd0be31d616254125 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 25 Feb 2022 21:34:18 +0100 Subject: [PATCH 139/328] [ci] Update actions/setup-node action to v3 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a3c50a32..a3beed066 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: os: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} architecture: ${{ matrix.arch }} From 7cf94db6290f86470ce2da87a88d05b604794fd9 Mon Sep 17 00:00:00 2001 From: Proxtx <39201896+Proxtx@users.noreply.github.com> Date: Sat, 5 Mar 2022 08:30:11 +0100 Subject: [PATCH 140/328] [doc] Fix typo in code snippet (#2018) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cab335b7b..97ab9fabe 100644 --- a/README.md +++ b/README.md @@ -252,8 +252,8 @@ server.listen(8080); ### Client authentication ```js -import WebSocket from 'ws'; import { createServer } from 'http'; +import { WebSocketServer } from 'ws'; const server = createServer(); const wss = new WebSocketServer({ noServer: true }); From 9c18b1e78617c0906c9ad51009b3a4b412c345da Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 6 Mar 2022 20:27:31 +0100 Subject: [PATCH 141/328] [ci] Update actions/checkout action to v3 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3beed066..4af8b6dfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: - arch: x86 os: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} From dd3d5a710b490d7b4211f92e882b34b5114c8bc2 Mon Sep 17 00:00:00 2001 From: airtable-keyhanvakil <60900335+airtable-keyhanvakil@users.noreply.github.com> Date: Thu, 10 Mar 2022 07:12:29 +0000 Subject: [PATCH 142/328] [doc] document the default value of the `maxPayload` option (#2020) --- doc/ws.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 3273c1f9e..0489a772a 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -72,7 +72,8 @@ This class represents a WebSocket server. It extends the `EventEmitter`. - `handleProtocols` {Function} A function which can be used to handle the WebSocket subprotocols. See description below. - `host` {String} The hostname where to bind the server. - - `maxPayload` {Number} The maximum allowed message size in bytes. + - `maxPayload` {Number} The maximum allowed message size in bytes. Defaults to + 100 MiB (104857600 bytes). - `noServer` {Boolean} Enable no server mode. - `path` {String} Accept only connections matching this path. - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate. @@ -279,7 +280,8 @@ This class represents a WebSocket. It extends the `EventEmitter`. cryptographically strong random bytes. - `handshakeTimeout` {Number} Timeout in milliseconds for the handshake request. This is reset after every redirection. - - `maxPayload` {Number} The maximum allowed message size in bytes. + - `maxPayload` {Number} The maximum allowed message size in bytes. Defaults to + 100 MiB (104857600 bytes). - `maxRedirects` {Number} The maximum number of redirects allowed. Defaults to 10. - `origin` {String} Value of the `Origin` or `Sec-WebSocket-Origin` header From ff80d665f9ea9452f8b8ae69f2838bdc08fe85d6 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 24 Mar 2022 14:21:49 +0100 Subject: [PATCH 143/328] [test] Do not use a relative URL Use different username and password in the redirect URL. --- test/websocket.test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index 2f2f4f529..e3e11437b 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1146,7 +1146,10 @@ describe('WebSocket', () => { const authorization = 'Basic Zm9vOmJhcg=='; server.once('upgrade', (req, socket) => { - socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n'); + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://baz:qux@localhost:${port}/foo\r\n\r\n` + ); server.once('upgrade', (req, socket, head) => { wss.handleUpgrade(req, socket, head, (ws, req) => { assert.strictEqual(req.headers.authorization, authorization); @@ -1164,7 +1167,7 @@ describe('WebSocket', () => { ws.on('close', (code) => { assert.strictEqual(code, 1005); - assert.strictEqual(ws.url, `ws://foo:bar@localhost:${port}/foo`); + assert.strictEqual(ws.url, `ws://baz:qux@localhost:${port}/foo`); assert.strictEqual(ws._redirects, 1); wss.close(done); From d086f4bcbbe235f12f6fa2ddba5a8ce1342dac58 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 26 Mar 2022 20:32:45 +0100 Subject: [PATCH 144/328] [minor] Make `abortHandshake()` emit the error in the next tick --- lib/websocket.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 6fff935cb..dd67d7357 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -950,7 +950,7 @@ function initAsClient(websocket, address, protocols, options) { } /** - * Emit the `'error'` and `'close'` event. + * Emit the `'error'` and `'close'` events. * * @param {WebSocket} websocket The WebSocket instance * @param {Error} The error to emit @@ -1018,8 +1018,7 @@ function abortHandshake(websocket, stream, message) { stream.socket.destroy(); } - stream.once('abort', websocket.emitClose.bind(websocket)); - websocket.emit('error', err); + process.nextTick(emitErrorAndClose, websocket, err); } else { stream.destroy(err); stream.once('error', websocket.emit.bind(websocket, 'error')); From 2619c003cef5ffc9bff66debfcc52231b7995a5a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 6 Apr 2022 20:48:41 +0200 Subject: [PATCH 145/328] [minor] Fix nit in comment --- lib/websocket.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index dd67d7357..3a56ea069 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -861,8 +861,8 @@ function initAsClient(websocket, address, protocols, options) { websocket.emit('upgrade', res); // - // The user may have closed the connection from a listener of the `upgrade` - // event. + // The user may have closed the connection from a listener of the + // `'upgrade'` event. // if (websocket.readyState !== WebSocket.CONNECTING) return; From 62e9b199ad96e6f7d4b44a7ed84669fdaf48ba65 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 7 Apr 2022 15:51:59 +0200 Subject: [PATCH 146/328] [doc] Fix nits --- doc/ws.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 0489a772a..ed8f04f1c 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -291,7 +291,7 @@ This class represents a WebSocket. It extends the `EventEmitter`. - `skipUTF8Validation` {Boolean} Specifies whether or not to skip UTF-8 validation for text and close messages. Defaults to `false`. Set to `true` only if the server is trusted. - - Any other option allowed in [http.request()][] or [https.request()][]. + - Any other option allowed in [`http.request()`][] or [`https.request()`][]. Options given do not have any effect if parsed from the URL given with the `address` parameter. @@ -539,7 +539,7 @@ state is `CONNECTING`. ### websocket.terminate() -Forcibly close the connection. Internally this calls [socket.destroy()][]. +Forcibly close the connection. Internally this calls [`socket.destroy()`][]. ### websocket.url @@ -610,11 +610,11 @@ as configured by the `maxPayload` option. [concurrency-limit]: https://github.com/websockets/ws/issues/1202 [duplex-options]: https://nodejs.org/api/stream.html#stream_new_stream_duplex_options -[http.request()]: +[`http.request()`]: https://nodejs.org/api/http.html#http_http_request_options_callback -[https.request()]: +[`https.request()`]: https://nodejs.org/api/https.html#https_https_request_options_callback [permessage-deflate]: https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19 -[socket.destroy()]: https://nodejs.org/api/net.html#net_socket_destroy_error +[`socket.destroy()`]: https://nodejs.org/api/net.html#net_socket_destroy_error [zlib-options]: https://nodejs.org/api/zlib.html#zlib_class_options From 2cf6202f8a023d98c0ee3435ba03a4c9ad855278 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 12 Apr 2022 21:32:23 +0200 Subject: [PATCH 147/328] [example] Use the `WebSocket.WebSocket{,Server}` aliases Closes #2034 --- examples/express-session-parse/index.js | 4 ++-- examples/server-stats/index.js | 4 ++-- examples/ssl.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/express-session-parse/index.js b/examples/express-session-parse/index.js index 8fc4ce029..28ea15412 100644 --- a/examples/express-session-parse/index.js +++ b/examples/express-session-parse/index.js @@ -5,7 +5,7 @@ const express = require('express'); const http = require('http'); const uuid = require('uuid'); -const WebSocket = require('../..'); +const { WebSocketServer } = require('..'); const app = express(); const map = new Map(); @@ -56,7 +56,7 @@ const server = http.createServer(app); // // Create a WebSocket server completely detached from the HTTP server. // -const wss = new WebSocket.Server({ clientTracking: false, noServer: true }); +const wss = new WebSocketServer({ clientTracking: false, noServer: true }); server.on('upgrade', function (request, socket, head) { console.log('Parsing session from request...'); diff --git a/examples/server-stats/index.js b/examples/server-stats/index.js index da1f95a3b..91d958d52 100644 --- a/examples/server-stats/index.js +++ b/examples/server-stats/index.js @@ -4,13 +4,13 @@ const express = require('express'); const path = require('path'); const { createServer } = require('http'); -const WebSocket = require('../../'); +const { WebSocketServer } = require('..'); const app = express(); app.use(express.static(path.join(__dirname, '/public'))); const server = createServer(app); -const wss = new WebSocket.Server({ server }); +const wss = new WebSocketServer({ server }); wss.on('connection', function (ws) { const id = setInterval(function () { diff --git a/examples/ssl.js b/examples/ssl.js index ad08632b1..a5e750b79 100644 --- a/examples/ssl.js +++ b/examples/ssl.js @@ -3,14 +3,14 @@ const https = require('https'); const fs = require('fs'); -const WebSocket = require('..'); +const { WebSocket, WebSocketServer } = require('..'); const server = https.createServer({ cert: fs.readFileSync('../test/fixtures/certificate.pem'), key: fs.readFileSync('../test/fixtures/key.pem') }); -const wss = new WebSocket.Server({ server }); +const wss = new WebSocketServer({ server }); wss.on('connection', function connection(ws) { ws.on('message', function message(msg) { From d062ded6225a1cd2593afdce0ecd91ef8317950a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 15 Apr 2022 13:10:21 +0200 Subject: [PATCH 148/328] [example] Fix require path --- examples/express-session-parse/index.js | 2 +- examples/server-stats/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/express-session-parse/index.js b/examples/express-session-parse/index.js index 28ea15412..b62a2e4a5 100644 --- a/examples/express-session-parse/index.js +++ b/examples/express-session-parse/index.js @@ -5,7 +5,7 @@ const express = require('express'); const http = require('http'); const uuid = require('uuid'); -const { WebSocketServer } = require('..'); +const { WebSocketServer } = require('../..'); const app = express(); const map = new Map(); diff --git a/examples/server-stats/index.js b/examples/server-stats/index.js index 91d958d52..e8754b5b2 100644 --- a/examples/server-stats/index.js +++ b/examples/server-stats/index.js @@ -4,7 +4,7 @@ const express = require('express'); const path = require('path'); const { createServer } = require('http'); -const { WebSocketServer } = require('..'); +const { WebSocketServer } = require('../..'); const app = express(); app.use(express.static(path.join(__dirname, '/public'))); From 0b6eb71665eabadc7d55584fdb3de7b56ae2439e Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 22 Apr 2022 08:33:54 +0200 Subject: [PATCH 149/328] [ci] Do not test on node 17 --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4af8b6dfb..a29f52e6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,6 @@ jobs: - 12 - 14 - 16 - - 17 os: - macOS-latest - ubuntu-latest From 69e682806df1913ad881643e36c8b999522b175f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 22 Apr 2022 08:34:23 +0200 Subject: [PATCH 150/328] [ci] Test on node 18 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a29f52e6d..833347413 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: - 12 - 14 - 16 + - 18 os: - macOS-latest - ubuntu-latest From 8e3f1181f2b1c7a4de8ceb120165276d2b1ccfaf Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 24 Apr 2022 07:12:02 +0200 Subject: [PATCH 151/328] [feature] Introduce the `'redirect'` event (#2030) Add the ability to remove confidential headers on a per-redirect basis. Closes #2014 --- doc/ws.md | 17 ++ lib/websocket.js | 40 ++++- test/websocket.test.js | 344 +++++++++++++++++++++++++++++++++++------ 3 files changed, 350 insertions(+), 51 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index ed8f04f1c..744112018 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -24,6 +24,7 @@ - [Event: 'open'](#event-open) - [Event: 'ping'](#event-ping) - [Event: 'pong'](#event-pong) + - [Event: 'redirect'](#event-redirect) - [Event: 'unexpected-response'](#event-unexpected-response) - [Event: 'upgrade'](#event-upgrade) - [websocket.addEventListener(type, listener[, options])](#websocketaddeventlistenertype-listener-options) @@ -361,6 +362,19 @@ Emitted when a ping is received from the server. Emitted when a pong is received from the server. +### Event: 'redirect' + +- `url` {String} +- `request` {http.ClientRequest} + +Emitted before a redirect is followed. `url` is the redirect URL. `request` is +the HTTP GET request with the headers queued. This event gives the ability to +inspect confidential headers and remove them on a per-redirect basis using the +[`request.getHeader()`][] and [`request.removeHeader()`][] API. The `request` +object should be used only for this purpose. When there is at least one listener +for this event, no header is removed by default, even if the redirect is to a +different domain. + ### Event: 'unexpected-response' - `request` {http.ClientRequest} @@ -616,5 +630,8 @@ as configured by the `maxPayload` option. https://nodejs.org/api/https.html#https_https_request_options_callback [permessage-deflate]: https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19 +[`request.getheader()`]: https://nodejs.org/api/http.html#requestgetheadername +[`request.removeheader()`]: + https://nodejs.org/api/http.html#requestremoveheadername [`socket.destroy()`]: https://nodejs.org/api/net.html#net_socket_destroy_error [zlib-options]: https://nodejs.org/api/zlib.html#zlib_class_options diff --git a/lib/websocket.js b/lib/websocket.js index 3a56ea069..ca44bc344 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -30,10 +30,11 @@ const { const { format, parse } = require('./extension'); const { toBuffer } = require('./buffer-util'); +const closeTimeout = 30 * 1000; +const kAborted = Symbol('kAborted'); +const protocolVersions = [8, 13]; const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/; -const protocolVersions = [8, 13]; -const closeTimeout = 30 * 1000; /** * Class representing a WebSocket. @@ -647,7 +648,7 @@ function initAsClient(websocket, address, protocols, options) { hostname: undefined, protocol: undefined, timeout: undefined, - method: undefined, + method: 'GET', host: undefined, path: undefined, port: undefined @@ -701,7 +702,7 @@ function initAsClient(websocket, address, protocols, options) { const defaultPort = isSecure ? 443 : 80; const key = randomBytes(16).toString('base64'); - const get = isSecure ? https.get : http.get; + const request = isSecure ? https.request : http.request; const protocolSet = new Set(); let perMessageDeflate; @@ -766,6 +767,8 @@ function initAsClient(websocket, address, protocols, options) { opts.path = parts[1]; } + let req; + if (opts.followRedirects) { if (websocket._redirects === 0) { websocket._originalHost = parsedUrl.host; @@ -783,7 +786,10 @@ function initAsClient(websocket, address, protocols, options) { options.headers[key.toLowerCase()] = value; } } - } else if (parsedUrl.host !== websocket._originalHost) { + } else if ( + websocket.listenerCount('redirect') === 0 && + parsedUrl.host !== websocket._originalHost + ) { // // Match curl 7.77.0 behavior and drop the following headers. These // headers are also dropped when following a redirect to a subdomain. @@ -803,9 +809,24 @@ function initAsClient(websocket, address, protocols, options) { options.headers.authorization = 'Basic ' + Buffer.from(opts.auth).toString('base64'); } - } - let req = (websocket._req = get(opts)); + req = websocket._req = request(opts); + + if (websocket._redirects) { + // + // Unlike what is done for the `'upgrade'` event, no early exit is + // triggered here if the user calls `websocket.close()` or + // `websocket.terminate()` from a listener of the `'redirect'` event. This + // is because the user can also call `request.destroy()` with an error + // before calling `websocket.close()` or `websocket.terminate()` and this + // would result in an error being emitted on the `request` object with no + // `'error'` event listeners attached. + // + websocket.emit('redirect', websocket.url, req); + } + } else { + req = websocket._req = request(opts); + } if (opts.timeout) { req.on('timeout', () => { @@ -814,7 +835,7 @@ function initAsClient(websocket, address, protocols, options) { } req.on('error', (err) => { - if (req === null || req.aborted) return; + if (req === null || req[kAborted]) return; req = websocket._req = null; emitErrorAndClose(websocket, err); @@ -947,6 +968,8 @@ function initAsClient(websocket, address, protocols, options) { skipUTF8Validation: opts.skipUTF8Validation }); }); + + req.end(); } /** @@ -1007,6 +1030,7 @@ function abortHandshake(websocket, stream, message) { Error.captureStackTrace(err, abortHandshake); if (stream.setHeader) { + stream[kAborted] = true; stream.abort(); if (stream.socket && !stream.socket.destroyed) { diff --git a/test/websocket.test.js b/test/websocket.test.js index e3e11437b..a27b2c278 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -645,6 +645,38 @@ describe('WebSocket', () => { ws.close(); }); }); + + it("emits a 'redirect' event", (done) => { + const server = http.createServer(); + const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); + + server.once('upgrade', (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n'); + server.once('upgrade', (req, socket, head) => { + wss.handleUpgrade(req, socket, head, (ws) => { + ws.close(); + }); + }); + }); + + server.listen(() => { + const port = server.address().port; + const ws = new WebSocket(`ws://localhost:${port}`, { + followRedirects: true + }); + + ws.on('redirect', (url, req) => { + assert.strictEqual(ws._redirects, 1); + assert.strictEqual(url, `ws://localhost:${port}/foo`); + assert.ok(req instanceof http.ClientRequest); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + server.close(done); + }); + }); + }); + }); }); describe('Connection establishing', () => { @@ -1175,83 +1207,237 @@ describe('WebSocket', () => { }); describe('When the redirect host is different', () => { - it('drops the `auth` option', (done) => { - const wss = new WebSocket.Server({ port: 0 }, () => { - const port = wss.address().port; + describe("If there is no 'redirect' event listener", () => { + it('drops the `auth` option', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const port = wss.address().port; + + server.once('upgrade', (req, socket) => { + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); - server.once('upgrade', (req, socket) => { - socket.end( - `HTTP/1.1 302 Found\r\nLocation: ws://localhost:${port}/\r\n\r\n` + const ws = new WebSocket( + `ws://localhost:${server.address().port}`, + { + auth: 'foo:bar', + followRedirects: true + } ); + + assert.strictEqual( + ws._req.getHeader('Authorization'), + 'Basic Zm9vOmJhcg==' + ); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://localhost:${port}/`); + assert.strictEqual(ws._redirects, 1); + + wss.close(done); + }); }); - const ws = new WebSocket(`ws://localhost:${server.address().port}`, { - auth: 'foo:bar', - followRedirects: true + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + ws.close(); }); + }); - assert.strictEqual( - ws._req.getHeader('Authorization'), - 'Basic Zm9vOmJhcg==' - ); + it('drops the Authorization, Cookie, and Host headers', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const port = wss.address().port; - ws.on('close', (code) => { - assert.strictEqual(code, 1005); - assert.strictEqual(ws.url, `ws://localhost:${port}/`); - assert.strictEqual(ws._redirects, 1); + server.once('upgrade', (req, socket) => { + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); - wss.close(done); + const ws = new WebSocket( + `ws://localhost:${server.address().port}`, + { + headers: { + Authorization: 'Basic Zm9vOmJhcg==', + Cookie: 'foo=bar', + Host: 'foo' + }, + followRedirects: true + } + ); + + assert.strictEqual( + ws._req.getHeader('Authorization'), + 'Basic Zm9vOmJhcg==' + ); + assert.strictEqual(ws._req.getHeader('Cookie'), 'foo=bar'); + assert.strictEqual(ws._req.getHeader('Host'), 'foo'); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://localhost:${port}/`); + assert.strictEqual(ws._redirects, 1); + + wss.close(done); + }); + }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + assert.strictEqual(req.headers.cookie, undefined); + assert.strictEqual( + req.headers.host, + `localhost:${wss.address().port}` + ); + ws.close(); }); }); + }); - wss.on('connection', (ws, req) => { - assert.strictEqual(req.headers.authorization, undefined); - ws.close(); + describe("If there is at least one 'redirect' event listener", () => { + it('does not drop any headers by default', (done) => { + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const wss = new WebSocket.Server({ port: 0 }, () => { + const port = wss.address().port; + + server.once('upgrade', (req, socket) => { + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const ws = new WebSocket( + `ws://localhost:${server.address().port}`, + { headers, followRedirects: true } + ); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('redirect', (url, req) => { + assert.strictEqual(ws._redirects, 1); + assert.strictEqual(url, `ws://localhost:${port}/`); + assert.notStrictEqual(firstRequest, req); + assert.strictEqual( + req.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual(req.getHeader('Cookie'), headers.cookie); + assert.strictEqual(req.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + wss.close(done); + }); + }); + }); + + wss.on('connection', (ws, req) => { + assert.strictEqual( + req.headers.authorization, + headers.authorization + ); + assert.strictEqual(req.headers.cookie, headers.cookie); + assert.strictEqual(req.headers.host, headers.host); + ws.close(); + }); }); }); + }); + + describe("In a listener of the 'redirect' event", () => { + it('allows to abort the request without swallowing errors', (done) => { + server.once('upgrade', (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n'); + }); + + const port = server.address().port; + const ws = new WebSocket(`ws://localhost:${port}`, { + followRedirects: true + }); + + ws.on('redirect', (url, req) => { + assert.strictEqual(ws._redirects, 1); + assert.strictEqual(url, `ws://localhost:${port}/foo`); - it('drops the Authorization, Cookie, and Host headers', (done) => { + req.on('socket', () => { + req.abort(); + }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'socket hang up'); + + ws.on('close', (code) => { + assert.strictEqual(code, 1006); + done(); + }); + }); + }); + }); + + it('allows to remove headers', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const port = wss.address().port; server.once('upgrade', (req, socket) => { socket.end( - `HTTP/1.1 302 Found\r\nLocation: ws://localhost:${port}/\r\n\r\n` + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` ); }); + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar' + }; + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { - headers: { - Authorization: 'Basic Zm9vOmJhcg==', - Cookie: 'foo=bar', - Host: 'foo' - }, + headers, followRedirects: true }); - assert.strictEqual( - ws._req.getHeader('Authorization'), - 'Basic Zm9vOmJhcg==' - ); - assert.strictEqual(ws._req.getHeader('Cookie'), 'foo=bar'); - assert.strictEqual(ws._req.getHeader('Host'), 'foo'); - - ws.on('close', (code) => { - assert.strictEqual(code, 1005); - assert.strictEqual(ws.url, `ws://localhost:${port}/`); + ws.on('redirect', (url, req) => { assert.strictEqual(ws._redirects, 1); + assert.strictEqual(url, `ws://localhost:${port}/`); + assert.strictEqual( + req.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual(req.getHeader('Cookie'), headers.cookie); - wss.close(done); + req.removeHeader('authorization'); + req.removeHeader('cookie'); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + wss.close(done); + }); }); }); wss.on('connection', (ws, req) => { assert.strictEqual(req.headers.authorization, undefined); assert.strictEqual(req.headers.cookie, undefined); - assert.strictEqual( - req.headers.host, - `localhost:${wss.address().port}` - ); ws.close(); }); }); @@ -2172,6 +2358,42 @@ describe('WebSocket', () => { }); }).timeout(4000); + it("can be called from a listener of the 'redirect' event", (done) => { + const server = http.createServer(); + + server.once('upgrade', (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n'); + }); + + server.listen(() => { + const port = server.address().port; + const ws = new WebSocket(`ws://localhost:${port}`, { + followRedirects: true + }); + + ws.on('open', () => { + done(new Error("Unexpected 'open' event")); + }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + + ws.on('close', (code) => { + assert.strictEqual(code, 1006); + server.close(done); + }); + }); + + ws.on('redirect', () => { + ws.close(); + }); + }); + }); + it("can be called from a listener of the 'upgrade' event", (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); @@ -2410,6 +2632,42 @@ describe('WebSocket', () => { }); }).timeout(4000); + it("can be called from a listener of the 'redirect' event", (done) => { + const server = http.createServer(); + + server.once('upgrade', (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n'); + }); + + server.listen(() => { + const port = server.address().port; + const ws = new WebSocket(`ws://localhost:${port}`, { + followRedirects: true + }); + + ws.on('open', () => { + done(new Error("Unexpected 'open' event")); + }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + + ws.on('close', (code) => { + assert.strictEqual(code, 1006); + server.close(done); + }); + }); + + ws.on('redirect', () => { + ws.terminate(); + }); + }); + }); + it("can be called from a listener of the 'upgrade' event", (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); From ba214d96939737304fb76f662700e9df01bb4418 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 1 May 2022 21:05:41 +0200 Subject: [PATCH 152/328] [dist] 8.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 57ddc7e95..df81cc170 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.5.0", + "version": "8.6.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From e56cdfe1ec54cdb04bc83ca91f8c4c983db6cde4 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 20 May 2022 15:21:06 +0200 Subject: [PATCH 153/328] [minor] Clarify why the handshake is aborted Add more details about why the handshake is aborted in the HTTP response. Refs: https://github.com/websockets/ws/issues/2045#issuecomment-1130081274 --- lib/websocket-server.js | 93 ++++++++++------- test/websocket-server.test.js | 181 ++++++++++++++++++++++++++++++++-- 2 files changed, 231 insertions(+), 43 deletions(-) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index d0a29783f..059b0a572 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -230,21 +230,34 @@ class WebSocketServer extends EventEmitter { handleUpgrade(req, socket, head, cb) { socket.on('error', socketOnError); - const key = - req.headers['sec-websocket-key'] !== undefined - ? req.headers['sec-websocket-key'] - : false; + const key = req.headers['sec-websocket-key']; const version = +req.headers['sec-websocket-version']; - if ( - req.method !== 'GET' || - req.headers.upgrade.toLowerCase() !== 'websocket' || - !key || - !keyRegex.test(key) || - (version !== 8 && version !== 13) || - !this.shouldHandle(req) - ) { - return abortHandshake(socket, 400); + if (req.method !== 'GET') { + abortHandshake(socket, 405, 'The HTTP method is invalid'); + return; + } + + if (req.headers.upgrade.toLowerCase() !== 'websocket') { + abortHandshake(socket, 400, 'The Upgrade header is invalid'); + return; + } + + if (!key || !keyRegex.test(key)) { + const message = 'The Sec-WebSocket-Key header is missing or invalid'; + abortHandshake(socket, 400, message); + return; + } + + if (version !== 8 && version !== 13) { + const message = 'The Sec-WebSocket-Version header is missing or invalid'; + abortHandshake(socket, 400, message); + return; + } + + if (!this.shouldHandle(req)) { + abortHandshake(socket, 400); + return; } const secWebSocketProtocol = req.headers['sec-websocket-protocol']; @@ -254,7 +267,9 @@ class WebSocketServer extends EventEmitter { try { protocols = subprotocol.parse(secWebSocketProtocol); } catch (err) { - return abortHandshake(socket, 400); + const message = 'The Sec-WebSocket-Protocol header is invalid'; + abortHandshake(socket, 400, message); + return; } } @@ -279,7 +294,10 @@ class WebSocketServer extends EventEmitter { extensions[PerMessageDeflate.extensionName] = perMessageDeflate; } } catch (err) { - return abortHandshake(socket, 400); + const message = + 'The Sec-WebSocket-Extensions header is invalid or not acceptable'; + abortHandshake(socket, 400, message); + return; } } @@ -446,7 +464,7 @@ function emitClose(server) { } /** - * Handle premature socket errors. + * Handle socket errors. * * @private */ @@ -464,25 +482,30 @@ function socketOnError() { * @private */ function abortHandshake(socket, code, message, headers) { - if (socket.writable) { - message = message || http.STATUS_CODES[code]; - headers = { - Connection: 'close', - 'Content-Type': 'text/html', - 'Content-Length': Buffer.byteLength(message), - ...headers - }; + // + // The socket is writable unless the user destroyed or ended it before calling + // `server.handleUpgrade()` or in the `verifyClient` function, which is a user + // error. Handling this does not make much sense as the worst that can happen + // is that some of the data written by the user might be discarded due to the + // call to `socket.end()` below, which triggers an `'error'` event that in + // turn causes the socket to be destroyed. + // + message = message || http.STATUS_CODES[code]; + headers = { + Connection: 'close', + 'Content-Type': 'text/html', + 'Content-Length': Buffer.byteLength(message), + ...headers + }; - socket.write( - `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + - Object.keys(headers) - .map((h) => `${h}: ${headers[h]}`) - .join('\r\n') + - '\r\n\r\n' + - message - ); - } + socket.once('finish', socket.destroy); - socket.removeListener('error', socketOnError); - socket.destroy(); + socket.end( + `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + + Object.keys(headers) + .map((h) => `${h}: ${headers[h]}`) + .join('\r\n') + + '\r\n\r\n' + + message + ); } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index fd494059f..5590d6c99 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -470,7 +470,9 @@ describe('WebSocketServer', () => { port: wss.address().port, headers: { Connection: 'Upgrade', - Upgrade: 'websocket' + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13 } }); @@ -496,7 +498,20 @@ describe('WebSocketServer', () => { req.on('response', (res) => { assert.strictEqual(res.statusCode, 400); - wss.close(done); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'The Sec-WebSocket-Key header is missing or invalid' + ); + wss.close(done); + }); }); }); }); @@ -539,6 +554,77 @@ describe('WebSocketServer', () => { }); describe('Connection establishing', () => { + it('fails if the HTTP method is not GET', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.request({ + method: 'POST', + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 405); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'The HTTP method is invalid' + ); + wss.close(done); + }); + }); + + req.end(); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('fails if the Upgrade header field value is not "websocket"', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'foo' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'The Upgrade header is invalid' + ); + wss.close(done); + }); + }); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + it('fails if the Sec-WebSocket-Key header is invalid (1/2)', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const req = http.get({ @@ -551,7 +637,20 @@ describe('WebSocketServer', () => { req.on('response', (res) => { assert.strictEqual(res.statusCode, 400); - wss.close(done); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'The Sec-WebSocket-Key header is missing or invalid' + ); + wss.close(done); + }); }); }); @@ -573,7 +672,20 @@ describe('WebSocketServer', () => { req.on('response', (res) => { assert.strictEqual(res.statusCode, 400); - wss.close(done); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'The Sec-WebSocket-Key header is missing or invalid' + ); + wss.close(done); + }); }); }); @@ -595,7 +707,20 @@ describe('WebSocketServer', () => { req.on('response', (res) => { assert.strictEqual(res.statusCode, 400); - wss.close(done); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'The Sec-WebSocket-Version header is missing or invalid' + ); + wss.close(done); + }); }); }); @@ -618,7 +743,20 @@ describe('WebSocketServer', () => { req.on('response', (res) => { assert.strictEqual(res.statusCode, 400); - wss.close(done); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'The Sec-WebSocket-Version header is missing or invalid' + ); + wss.close(done); + }); }); }); @@ -642,7 +780,20 @@ describe('WebSocketServer', () => { req.on('response', (res) => { assert.strictEqual(res.statusCode, 400); - wss.close(done); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'The Sec-WebSocket-Protocol header is invalid' + ); + wss.close(done); + }); }); }); @@ -672,7 +823,21 @@ describe('WebSocketServer', () => { req.on('response', (res) => { assert.strictEqual(res.statusCode, 400); - wss.close(done); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'The Sec-WebSocket-Extensions header is invalid or not ' + + 'acceptable' + ); + wss.close(done); + }); }); } ); From 0fdcc0af78c4d9a77f17faba48071d2f66a79662 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 21 May 2022 07:02:12 +0200 Subject: [PATCH 154/328] [fix] Abort the handshake if the Upgrade header is invalid Close the connection if the Upgrade header field in the HTTP response contains a value that is not an ASCII case-insensitive match for the value "websocket". --- lib/websocket.js | 5 +++++ test/websocket.test.js | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/lib/websocket.js b/lib/websocket.js index ca44bc344..8ea2f5e01 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -889,6 +889,11 @@ function initAsClient(websocket, address, protocols, options) { req = websocket._req = null; + if (res.headers.upgrade.toLowerCase() !== 'websocket') { + abortHandshake(websocket, socket, 'Invalid Upgrade header'); + return; + } + const digest = createHash('sha1') .update(key + GUID) .digest('base64'); diff --git a/test/websocket.test.js b/test/websocket.test.js index a27b2c278..beea20a76 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -685,6 +685,26 @@ describe('WebSocket', () => { beforeEach((done) => server.listen(0, done)); afterEach((done) => server.close(done)); + it('fails if the Upgrade header field value is not "websocket"', (done) => { + server.once('upgrade', (req, socket) => { + socket.on('end', socket.end); + socket.write( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Connection: Upgrade\r\n' + + 'Upgrade: foo\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Invalid Upgrade header'); + done(); + }); + }); + it('fails if the Sec-WebSocket-Accept header is invalid', (done) => { server.once('upgrade', (req, socket) => { socket.on('end', socket.end); From fb658bdd11e05a89ad5dc48e126e53976c5bb67a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 21 May 2022 07:25:52 +0200 Subject: [PATCH 155/328] [minor] Use consistent error messages Make some server error messages consistent with the respective client error messages. --- lib/websocket-server.js | 12 ++++++------ test/websocket-server.test.js | 19 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 059b0a572..e22e9b212 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -234,23 +234,23 @@ class WebSocketServer extends EventEmitter { const version = +req.headers['sec-websocket-version']; if (req.method !== 'GET') { - abortHandshake(socket, 405, 'The HTTP method is invalid'); + abortHandshake(socket, 405, 'Invalid HTTP method'); return; } if (req.headers.upgrade.toLowerCase() !== 'websocket') { - abortHandshake(socket, 400, 'The Upgrade header is invalid'); + abortHandshake(socket, 400, 'Invalid Upgrade header'); return; } if (!key || !keyRegex.test(key)) { - const message = 'The Sec-WebSocket-Key header is missing or invalid'; + const message = 'Missing or invalid Sec-WebSocket-Key header'; abortHandshake(socket, 400, message); return; } if (version !== 8 && version !== 13) { - const message = 'The Sec-WebSocket-Version header is missing or invalid'; + const message = 'Missing or invalid Sec-WebSocket-Version header'; abortHandshake(socket, 400, message); return; } @@ -267,7 +267,7 @@ class WebSocketServer extends EventEmitter { try { protocols = subprotocol.parse(secWebSocketProtocol); } catch (err) { - const message = 'The Sec-WebSocket-Protocol header is invalid'; + const message = 'Invalid Sec-WebSocket-Protocol header'; abortHandshake(socket, 400, message); return; } @@ -295,7 +295,7 @@ class WebSocketServer extends EventEmitter { } } catch (err) { const message = - 'The Sec-WebSocket-Extensions header is invalid or not acceptable'; + 'Invalid or unacceptable Sec-WebSocket-Extensions header'; abortHandshake(socket, 400, message); return; } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 5590d6c99..1ad63da64 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -508,7 +508,7 @@ describe('WebSocketServer', () => { res.on('end', () => { assert.strictEqual( Buffer.concat(chunks).toString(), - 'The Sec-WebSocket-Key header is missing or invalid' + 'Missing or invalid Sec-WebSocket-Key header' ); wss.close(done); }); @@ -577,7 +577,7 @@ describe('WebSocketServer', () => { res.on('end', () => { assert.strictEqual( Buffer.concat(chunks).toString(), - 'The HTTP method is invalid' + 'Invalid HTTP method' ); wss.close(done); }); @@ -613,7 +613,7 @@ describe('WebSocketServer', () => { res.on('end', () => { assert.strictEqual( Buffer.concat(chunks).toString(), - 'The Upgrade header is invalid' + 'Invalid Upgrade header' ); wss.close(done); }); @@ -647,7 +647,7 @@ describe('WebSocketServer', () => { res.on('end', () => { assert.strictEqual( Buffer.concat(chunks).toString(), - 'The Sec-WebSocket-Key header is missing or invalid' + 'Missing or invalid Sec-WebSocket-Key header' ); wss.close(done); }); @@ -682,7 +682,7 @@ describe('WebSocketServer', () => { res.on('end', () => { assert.strictEqual( Buffer.concat(chunks).toString(), - 'The Sec-WebSocket-Key header is missing or invalid' + 'Missing or invalid Sec-WebSocket-Key header' ); wss.close(done); }); @@ -717,7 +717,7 @@ describe('WebSocketServer', () => { res.on('end', () => { assert.strictEqual( Buffer.concat(chunks).toString(), - 'The Sec-WebSocket-Version header is missing or invalid' + 'Missing or invalid Sec-WebSocket-Version header' ); wss.close(done); }); @@ -753,7 +753,7 @@ describe('WebSocketServer', () => { res.on('end', () => { assert.strictEqual( Buffer.concat(chunks).toString(), - 'The Sec-WebSocket-Version header is missing or invalid' + 'Missing or invalid Sec-WebSocket-Version header' ); wss.close(done); }); @@ -790,7 +790,7 @@ describe('WebSocketServer', () => { res.on('end', () => { assert.strictEqual( Buffer.concat(chunks).toString(), - 'The Sec-WebSocket-Protocol header is invalid' + 'Invalid Sec-WebSocket-Protocol header' ); wss.close(done); }); @@ -833,8 +833,7 @@ describe('WebSocketServer', () => { res.on('end', () => { assert.strictEqual( Buffer.concat(chunks).toString(), - 'The Sec-WebSocket-Extensions header is invalid or not ' + - 'acceptable' + 'Invalid or unacceptable Sec-WebSocket-Extensions header' ); wss.close(done); }); From 8889e48018bc3381e3daa8398be7786c07f8db02 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 21 May 2022 07:46:20 +0200 Subject: [PATCH 156/328] [test] Increase code coverage --- test/websocket-server.test.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 1ad63da64..bcf82ba90 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -340,18 +340,12 @@ describe('WebSocketServer', () => { wss.close(); }); - it("emits the 'close' event if the server is already closed", (done) => { - let callbackCalled = false; + it('calls the callback if the server is already closed', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { wss.close(() => { assert.strictEqual(wss._state, 2); - wss.on('close', () => { - callbackCalled = true; - }); - wss.close((err) => { - assert.ok(callbackCalled); assert.ok(err instanceof Error); assert.strictEqual(err.message, 'The server is not running'); done(); @@ -359,6 +353,17 @@ describe('WebSocketServer', () => { }); }); }); + + it("emits the 'close' event if the server is already closed", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + wss.close(() => { + assert.strictEqual(wss._state, 2); + + wss.on('close', done); + wss.close(); + }); + }); + }); }); describe('#clients', () => { From a690791df31cede48e7bcbce9d56411483309246 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 21 May 2022 07:51:32 +0200 Subject: [PATCH 157/328] [ci] Exclude node 18 on Windows x86 from the test matrix --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 833347413..faf9bf56d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,9 @@ jobs: os: macOS-latest - arch: x86 os: ubuntu-latest + - arch: x86 + node: 18 + os: windows-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 From d68ba9e1aa6a939fd7aa39894a4b9c2f020f9361 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 25 May 2022 15:33:05 +0200 Subject: [PATCH 158/328] [security] Drop sensitive headers when following insecure redirects Drop the `Authorization` and `Cookie` headers if the original request for the opening handshake is sent over HTTPS and the client is redirected to the same host over plain HTTP (wss: to ws:). If an HTTPS server redirects to same host over plain HTTP, the problem is on the server, but handling this condition is not hard and reduces the risk of leaking credentials due to MITM issues. Refs: https://github.com/websockets/ws/commit/6946f5fe --- lib/websocket.js | 28 +++--- test/websocket.test.js | 211 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 12 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 8ea2f5e01..c4b14907b 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -771,6 +771,7 @@ function initAsClient(websocket, address, protocols, options) { if (opts.followRedirects) { if (websocket._redirects === 0) { + websocket._originalSecure = isSecure; websocket._originalHost = parsedUrl.host; const headers = options && options.headers; @@ -786,18 +787,21 @@ function initAsClient(websocket, address, protocols, options) { options.headers[key.toLowerCase()] = value; } } - } else if ( - websocket.listenerCount('redirect') === 0 && - parsedUrl.host !== websocket._originalHost - ) { - // - // Match curl 7.77.0 behavior and drop the following headers. These - // headers are also dropped when following a redirect to a subdomain. - // - delete opts.headers.authorization; - delete opts.headers.cookie; - delete opts.headers.host; - opts.auth = undefined; + } else if (websocket.listenerCount('redirect') === 0) { + const isSameHost = parsedUrl.host === websocket._originalHost; + + if (!isSameHost || (websocket._originalSecure && !isSecure)) { + // + // Match curl 7.77.0 behavior and drop the following headers. These + // headers are also dropped when following a redirect to a subdomain. + // + delete opts.headers.authorization; + delete opts.headers.cookie; + + if (!isSameHost) delete opts.headers.host; + + opts.auth = undefined; + } } // diff --git a/test/websocket.test.js b/test/websocket.test.js index beea20a76..1a3453601 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -6,6 +6,7 @@ const assert = require('assert'); const crypto = require('crypto'); const https = require('https'); const http = require('http'); +const net = require('net'); const tls = require('tls'); const fs = require('fs'); const { URL } = require('url'); @@ -1226,6 +1227,216 @@ describe('WebSocket', () => { }); }); + describe('When moving away from a secure context', () => { + function proxy(httpServer, httpsServer) { + const server = net.createServer({ allowHalfOpen: true }); + + server.on('connection', (socket) => { + socket.on('readable', function read() { + socket.removeListener('readable', read); + + const buf = socket.read(1); + const target = buf[0] === 22 ? httpsServer : httpServer; + + socket.unshift(buf); + target.emit('connection', socket); + }); + }); + + return server; + } + + describe("If there is no 'redirect' event listener", () => { + it('drops the `auth` option', (done) => { + const httpServer = http.createServer(); + const httpsServer = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem') + }); + const server = proxy(httpServer, httpsServer); + + server.listen(() => { + const port = server.address().port; + + httpsServer.on('upgrade', (req, socket) => { + socket.on('error', NOOP); + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const wss = new WebSocket.Server({ server: httpServer }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + ws.close(); + }); + + const ws = new WebSocket( + `wss://localhost:${server.address().port}`, + { + auth: 'foo:bar', + followRedirects: true, + rejectUnauthorized: false + } + ); + + assert.strictEqual( + ws._req.getHeader('Authorization'), + 'Basic Zm9vOmJhcg==' + ); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://localhost:${port}/`); + assert.strictEqual(ws._redirects, 1); + + server.close(done); + }); + }); + }); + + it('drops the Authorization, and Cookie headers', (done) => { + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const httpServer = http.createServer(); + const httpsServer = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem') + }); + const server = proxy(httpServer, httpsServer); + + server.listen(() => { + const port = server.address().port; + + httpsServer.on('upgrade', (req, socket) => { + socket.on('error', NOOP); + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const wss = new WebSocket.Server({ server: httpServer }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + assert.strictEqual(req.headers.cookie, undefined); + assert.strictEqual(req.headers.host, 'foo'); + + ws.close(); + }); + + const ws = new WebSocket( + `wss://localhost:${server.address().port}`, + { headers, followRedirects: true, rejectUnauthorized: false } + ); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://localhost:${port}/`); + assert.strictEqual(ws._redirects, 1); + + server.close(done); + }); + }); + }); + }); + + describe("If there is at least one 'redirect' event listener", () => { + it('does not drop any headers by default', (done) => { + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const httpServer = http.createServer(); + const httpsServer = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem') + }); + const server = proxy(httpServer, httpsServer); + + server.listen(() => { + const port = server.address().port; + + httpsServer.on('upgrade', (req, socket) => { + socket.on('error', NOOP); + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const wss = new WebSocket.Server({ server: httpServer }); + + wss.on('connection', (ws, req) => { + assert.strictEqual( + req.headers.authorization, + headers.authorization + ); + assert.strictEqual(req.headers.cookie, headers.cookie); + assert.strictEqual(req.headers.host, headers.host); + + ws.close(); + }); + + const ws = new WebSocket( + `wss://localhost:${server.address().port}`, + { headers, followRedirects: true, rejectUnauthorized: false } + ); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('redirect', (url, req) => { + assert.strictEqual(ws._redirects, 1); + assert.strictEqual(url, `ws://localhost:${port}/`); + assert.notStrictEqual(firstRequest, req); + assert.strictEqual( + req.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual(req.getHeader('Cookie'), headers.cookie); + assert.strictEqual(req.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + server.close(done); + }); + }); + }); + }); + }); + }); + describe('When the redirect host is different', () => { describe("If there is no 'redirect' event listener", () => { it('drops the `auth` option', (done) => { From 903ec620117d95ed31cdec8c893b7c4fbe79314d Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 25 May 2022 20:43:21 +0200 Subject: [PATCH 159/328] [doc] Update the type of the `socket` argument --- doc/ws.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index 744112018..8a0c4ce64 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -230,7 +230,8 @@ receives an `Error` if the server is already closed. ### server.handleUpgrade(request, socket, head, callback) - `request` {http.IncomingMessage} The client HTTP GET request. -- `socket` {net.Socket} The network socket between the server and client. +- `socket` {net.Socket|tls.Socket} The network socket between the server and + client. - `head` {Buffer} The first packet of the upgraded stream. - `callback` {Function}. From 6e5a5ce341ffab5ea48542f0aa82c7f4eae80df9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 26 May 2022 12:42:53 +0200 Subject: [PATCH 160/328] [feature] Introduce the `'wsClientError'` event (#2046) Add the ability to inspect the invalid handshake requests and respond to them with a custom HTTP response. Closes #2045 --- doc/ws.md | 16 ++++++++++++++++ lib/websocket-server.js | 36 +++++++++++++++++++++++++++++------ test/websocket-server.test.js | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 8a0c4ce64..21984c60a 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -9,6 +9,7 @@ - [Event: 'error'](#event-error) - [Event: 'headers'](#event-headers) - [Event: 'listening'](#event-listening) + - [Event: 'wsClientError'](event-wsclienterror) - [server.address()](#serveraddress) - [server.clients](#serverclients) - [server.close([callback])](#serverclosecallback) @@ -202,6 +203,21 @@ handshake. This allows you to inspect/modify the headers before they are sent. Emitted when the underlying server has been bound. +### Event: 'wsClientError' + +- `error` {Error} +- `socket` {net.Socket|tls.Socket} +- `request` {http.IncomingMessage} + +Emitted when an error occurs before the WebSocket connection is established. +`socket` and `request` are respectively the socket and the HTTP request from +which the error originated. The listener of this event is responsible for +closing the socket. When the `'wsClientError'` event is emitted there is no +`http.ServerResponse` object, so any HTTP response, including the response +headers and body, must be written directly to the `socket`. If there is no +listener for this event, the socket is closed with a default 4xx response +containing a descriptive error message. + ### server.address() Returns an object with `port`, `family`, and `address` properties specifying the diff --git a/lib/websocket-server.js b/lib/websocket-server.js index e22e9b212..bac30eb33 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -234,24 +234,26 @@ class WebSocketServer extends EventEmitter { const version = +req.headers['sec-websocket-version']; if (req.method !== 'GET') { - abortHandshake(socket, 405, 'Invalid HTTP method'); + const message = 'Invalid HTTP method'; + abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); return; } if (req.headers.upgrade.toLowerCase() !== 'websocket') { - abortHandshake(socket, 400, 'Invalid Upgrade header'); + const message = 'Invalid Upgrade header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } if (!key || !keyRegex.test(key)) { const message = 'Missing or invalid Sec-WebSocket-Key header'; - abortHandshake(socket, 400, message); + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } if (version !== 8 && version !== 13) { const message = 'Missing or invalid Sec-WebSocket-Version header'; - abortHandshake(socket, 400, message); + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } @@ -268,7 +270,7 @@ class WebSocketServer extends EventEmitter { protocols = subprotocol.parse(secWebSocketProtocol); } catch (err) { const message = 'Invalid Sec-WebSocket-Protocol header'; - abortHandshake(socket, 400, message); + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } } @@ -296,7 +298,7 @@ class WebSocketServer extends EventEmitter { } catch (err) { const message = 'Invalid or unacceptable Sec-WebSocket-Extensions header'; - abortHandshake(socket, 400, message); + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } } @@ -509,3 +511,25 @@ function abortHandshake(socket, code, message, headers) { message ); } + +/** + * Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least + * one listener for it, otherwise call `abortHandshake()`. + * + * @param {WebSocketServer} server The WebSocket server + * @param {http.IncomingMessage} req The request object + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} message The HTTP response body + * @private + */ +function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) { + if (server.listenerCount('wsClientError')) { + const err = new Error(message); + Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); + + server.emit('wsClientError', err, socket, req); + } else { + abortHandshake(socket, code, message); + } +} diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index bcf82ba90..12928ff49 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -851,6 +851,40 @@ describe('WebSocketServer', () => { }); }); + it("emits the 'wsClientError' event", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.request({ + method: 'POST', + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + wss.close(done); + }); + + req.end(); + }); + + wss.on('wsClientError', (err, socket, request) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Invalid HTTP method'); + + assert.ok(request instanceof http.IncomingMessage); + assert.strictEqual(request.method, 'POST'); + + socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + it('fails if the WebSocket server is closing or closed', (done) => { const server = http.createServer(); const wss = new WebSocket.Server({ noServer: true }); From 4b62fbf945cee92f0b06535d8249b0a1d5037cea Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 26 May 2022 19:16:17 +0200 Subject: [PATCH 161/328] [dist] 8.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df81cc170..2a176c8fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.6.0", + "version": "8.7.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 5e4149ec6a82e0367a925881abca5667667030e9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 30 May 2022 17:32:18 +0200 Subject: [PATCH 162/328] [test] Fix typo --- test/websocket.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index 1a3453601..3ea9ea20b 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2966,7 +2966,7 @@ describe('WebSocket', () => { ws.onmessage = 'foo'; assert.strictEqual(ws.onmessage, null); - assert.strictEqual(ws.listenerCount('onmessage'), 0); + assert.strictEqual(ws.listenerCount('message'), 0); }); it('works like the `EventEmitter` interface', (done) => { From a6dbd1c3846ce910a759118729cdb5366d8838be Mon Sep 17 00:00:00 2001 From: neilnaveen <42328488+neilnaveen@users.noreply.github.com> Date: Sat, 4 Jun 2022 01:53:40 -0500 Subject: [PATCH 163/328] [ci] Set permissions explicitly (#2051) --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index faf9bf56d..a3a787a65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,8 @@ on: - push - pull_request +permissions: {} + jobs: test: runs-on: ${{ matrix.os }} From c1a126f2bd986c9fe6c1052a6be549506bc69bf5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 8 Jun 2022 16:16:15 +0200 Subject: [PATCH 164/328] [doc] Rename WS Error Codes section to Error codes --- doc/ws.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 21984c60a..e3b03a4d3 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -49,7 +49,7 @@ - [websocket.terminate()](#websocketterminate) - [websocket.url](#websocketurl) - [createWebSocketStream(websocket[, options])](#createwebsocketstreamwebsocket-options) -- [WS Error Codes](#ws-error-codes) +- [Error codes](#error-codes) - [WS_ERR_EXPECTED_FIN](#ws_err_expected_fin) - [WS_ERR_EXPECTED_MASK](#ws_err_expected_mask) - [WS_ERR_INVALID_CLOSE_CODE](#ws_err_invalid_close_code) @@ -353,7 +353,7 @@ been closed. - `error` {Error} Emitted when an error occurs. Errors may have a `.code` property, matching one -of the string values defined below under [WS Error Codes](#ws-error-codes). +of the string values defined below under [Error codes](#error-codes). ### Event: 'message' @@ -587,7 +587,7 @@ The URL of the WebSocket server. Server clients don't have this attribute. Returns a `Duplex` stream that allows to use the Node.js streams API on top of a given `WebSocket`. -## WS Error Codes +## Error codes Errors emitted by the websocket may have a `.code` property, describing the specific type of error that has occurred: From 0792742de13bfac0faecf044d290485808192665 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 8 Jun 2022 21:19:07 +0200 Subject: [PATCH 165/328] [doc] Fix nit --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 97ab9fabe..3c38a643c 100644 --- a/README.md +++ b/README.md @@ -485,5 +485,4 @@ We're using the GitHub [releases][changelog] for changelog entries. [server-report]: http://websockets.github.io/ws/autobahn/servers/ [session-parse-example]: ./examples/express-session-parse [socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent -[ws-server-options]: - https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback +[ws-server-options]: ./doc/ws.md#new-websocketserveroptions-callback From becf237cef8924a765075ecbd45599bc10fb6d18 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 9 Jun 2022 19:00:23 +0200 Subject: [PATCH 166/328] [feature] Add the `WS_NO_{BUFFER_UTIL, UTF_8_VALIDATE}` variables When set to non empty values, the `WS_NO_BUFFER_UTIL` and `WS_NO_UTF_8_VALIDATE` environment variables, prevent the optional `bufferutil` and `utf-8-validate` dependencies from being required, respectively. These might be useful to enhance security in systems where a user can put a package in the package search path of an application of another user, due to how the Node.js resolver algorithm works. --- README.md | 7 +++++++ doc/ws.md | 15 +++++++++++++++ lib/buffer-util.js | 41 +++++++++++++++++++++-------------------- lib/validation.js | 29 +++++++++++++++-------------- 4 files changed, 58 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 3c38a643c..4ae71f6d0 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,13 @@ necessarily need to have a C++ compiler installed on your machine. - `npm install --save-optional utf-8-validate`: Allows to efficiently check if a message contains valid UTF-8. +To not even try to require and use these modules, use the +[`WS_NO_BUFFER_UTIL`](./doc/ws.md#ws_no_buffer_util) and +[`WS_NO_UTF_8_VALIDATE`](./doc/ws.md#ws_no_utf_8_validate) environment +variables. These might be useful to enhance security in systems where a user can +put a package in the package search path of an application of another user, due +to how the Node.js resolver algorithm works. + ## API docs See [`/doc/ws.md`](./doc/ws.md) for Node.js-like documentation of ws classes and diff --git a/doc/ws.md b/doc/ws.md index e3b03a4d3..9496b7f5a 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -49,6 +49,9 @@ - [websocket.terminate()](#websocketterminate) - [websocket.url](#websocketurl) - [createWebSocketStream(websocket[, options])](#createwebsocketstreamwebsocket-options) +- [Environment variables](#environment-variables) + - [WS_NO_BUFFER_UTIL](#ws_no_buffer_util) + - [WS_NO_UTF_8_VALIDATE](#ws_no_utf_8_validate) - [Error codes](#error-codes) - [WS_ERR_EXPECTED_FIN](#ws_err_expected_fin) - [WS_ERR_EXPECTED_MASK](#ws_err_expected_mask) @@ -587,6 +590,18 @@ The URL of the WebSocket server. Server clients don't have this attribute. Returns a `Duplex` stream that allows to use the Node.js streams API on top of a given `WebSocket`. +## Environment variables + +### WS_NO_BUFFER_UTIL + +When set to a non empty value, prevents the optional `bufferutil` dependency +from being required. + +### WS_NO_UTF_8_VALIDATE + +When set to a non empty value, prevents the optional `utf-8-validate` dependency +from being required. + ## Error codes Errors emitted by the websocket may have a `.code` property, describing the diff --git a/lib/buffer-util.js b/lib/buffer-util.js index 1ba1d1beb..df7595546 100644 --- a/lib/buffer-util.js +++ b/lib/buffer-util.js @@ -99,28 +99,29 @@ function toBuffer(data) { return buf; } -try { - const bufferUtil = require('bufferutil'); - - module.exports = { - concat, - mask(source, mask, output, offset, length) { +module.exports = { + concat, + mask: _mask, + toArrayBuffer, + toBuffer, + unmask: _unmask +}; + +/* istanbul ignore else */ +if (!process.env.WS_NO_BUFFER_UTIL) { + try { + const bufferUtil = require('bufferutil'); + + module.exports.mask = function (source, mask, output, offset, length) { if (length < 48) _mask(source, mask, output, offset, length); else bufferUtil.mask(source, mask, output, offset, length); - }, - toArrayBuffer, - toBuffer, - unmask(buffer, mask) { + }; + + module.exports.unmask = function (buffer, mask) { if (buffer.length < 32) _unmask(buffer, mask); else bufferUtil.unmask(buffer, mask); - } - }; -} catch (e) /* istanbul ignore next */ { - module.exports = { - concat, - mask: _mask, - toArrayBuffer, - toBuffer, - unmask: _unmask - }; + }; + } catch (e) { + // Continue regardless of the error. + } } diff --git a/lib/validation.js b/lib/validation.js index ed98c7591..44fc20290 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -105,20 +105,21 @@ function _isValidUTF8(buf) { return true; } -try { - const isValidUTF8 = require('utf-8-validate'); +module.exports = { + isValidStatusCode, + isValidUTF8: _isValidUTF8, + tokenChars +}; - module.exports = { - isValidStatusCode, - isValidUTF8(buf) { +/* istanbul ignore else */ +if (!process.env.WS_NO_UTF_8_VALIDATE) { + try { + const isValidUTF8 = require('utf-8-validate'); + + module.exports.isValidUTF8 = function (buf) { return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); - }, - tokenChars - }; -} catch (e) /* istanbul ignore next */ { - module.exports = { - isValidStatusCode, - isValidUTF8: _isValidUTF8, - tokenChars - }; + }; + } catch (e) { + // Continue regardless of the error. + } } From 982b7826f940b7caec5dd7ea82386dc531c5fdd4 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 9 Jun 2022 19:16:41 +0200 Subject: [PATCH 167/328] [dist] 8.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a176c8fd..e09fae395 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.7.0", + "version": "8.8.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 3b6af82be91713fcc21cb2e56c500977fcb63f45 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 20 Jun 2022 15:35:00 +0200 Subject: [PATCH 168/328] [minor] Prevent opening handshake headers from being overridden Ensure that the `Connection`, `Sec-WebSocket-Key`, `Sec-WebSocket-Version`, and `Upgrade` headers are not overridden. Refs: https://github.com/websockets/ws/issues/2048#issuecomment-1159832034 --- lib/websocket.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index c4b14907b..68405dceb 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -713,11 +713,11 @@ function initAsClient(websocket, address, protocols, options) { ? parsedUrl.hostname.slice(1, -1) : parsedUrl.hostname; opts.headers = { + ...opts.headers, 'Sec-WebSocket-Version': opts.protocolVersion, 'Sec-WebSocket-Key': key, Connection: 'Upgrade', - Upgrade: 'websocket', - ...opts.headers + Upgrade: 'websocket' }; opts.path = parsedUrl.pathname + parsedUrl.search; opts.timeout = opts.handshakeTimeout; From 1117af6bf45e040fb51864e29c1c2c1a6b456ab7 Mon Sep 17 00:00:00 2001 From: Lee Trout Date: Thu, 30 Jun 2022 14:12:20 -0400 Subject: [PATCH 169/328] [doc] Fix typo (#2062) --- doc/ws.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index 9496b7f5a..4888ee4c6 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -9,7 +9,7 @@ - [Event: 'error'](#event-error) - [Event: 'headers'](#event-headers) - [Event: 'listening'](#event-listening) - - [Event: 'wsClientError'](event-wsclienterror) + - [Event: 'wsClientError'](#event-wsclienterror) - [server.address()](#serveraddress) - [server.clients](#serverclients) - [server.close([callback])](#serverclosecallback) From 0ae302affd469f3f2c46718b5c0a7317f41f8dcb Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 14 Jul 2022 19:21:59 +0200 Subject: [PATCH 170/328] [test] Fix nits --- test/websocket.test.js | 96 ++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index 3ea9ea20b..64b5bf224 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1273,14 +1273,11 @@ describe('WebSocket', () => { ws.close(); }); - const ws = new WebSocket( - `wss://localhost:${server.address().port}`, - { - auth: 'foo:bar', - followRedirects: true, - rejectUnauthorized: false - } - ); + const ws = new WebSocket(`wss://localhost:${port}`, { + auth: 'foo:bar', + followRedirects: true, + rejectUnauthorized: false + }); assert.strictEqual( ws._req.getHeader('Authorization'), @@ -1297,13 +1294,7 @@ describe('WebSocket', () => { }); }); - it('drops the Authorization, and Cookie headers', (done) => { - const headers = { - authorization: 'Basic Zm9vOmJhcg==', - cookie: 'foo=bar', - host: 'foo' - }; - + it('drops the Authorization and Cookie headers', (done) => { const httpServer = http.createServer(); const httpsServer = https.createServer({ cert: fs.readFileSync('test/fixtures/certificate.pem'), @@ -1322,20 +1313,27 @@ describe('WebSocket', () => { ); }); + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + const wss = new WebSocket.Server({ server: httpServer }); wss.on('connection', (ws, req) => { assert.strictEqual(req.headers.authorization, undefined); assert.strictEqual(req.headers.cookie, undefined); - assert.strictEqual(req.headers.host, 'foo'); + assert.strictEqual(req.headers.host, headers.host); ws.close(); }); - const ws = new WebSocket( - `wss://localhost:${server.address().port}`, - { headers, followRedirects: true, rejectUnauthorized: false } - ); + const ws = new WebSocket(`wss://localhost:${port}`, { + followRedirects: true, + headers, + rejectUnauthorized: false + }); const firstRequest = ws._req; @@ -1362,12 +1360,6 @@ describe('WebSocket', () => { describe("If there is at least one 'redirect' event listener", () => { it('does not drop any headers by default', (done) => { - const headers = { - authorization: 'Basic Zm9vOmJhcg==', - cookie: 'foo=bar', - host: 'foo' - }; - const httpServer = http.createServer(); const httpsServer = https.createServer({ cert: fs.readFileSync('test/fixtures/certificate.pem'), @@ -1386,6 +1378,12 @@ describe('WebSocket', () => { ); }); + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + const wss = new WebSocket.Server({ server: httpServer }); wss.on('connection', (ws, req) => { @@ -1399,10 +1397,11 @@ describe('WebSocket', () => { ws.close(); }); - const ws = new WebSocket( - `wss://localhost:${server.address().port}`, - { headers, followRedirects: true, rejectUnauthorized: false } - ); + const ws = new WebSocket(`wss://localhost:${port}`, { + followRedirects: true, + headers, + rejectUnauthorized: false + }); const firstRequest = ws._req; @@ -1478,7 +1477,7 @@ describe('WebSocket', () => { }); }); - it('drops the Authorization, Cookie, and Host headers', (done) => { + it('drops the Authorization, Cookie and Host headers', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const port = wss.address().port; @@ -1489,24 +1488,28 @@ describe('WebSocket', () => { ); }); + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + const ws = new WebSocket( `ws://localhost:${server.address().port}`, - { - headers: { - Authorization: 'Basic Zm9vOmJhcg==', - Cookie: 'foo=bar', - Host: 'foo' - }, - followRedirects: true - } + { followRedirects: true, headers } ); + const firstRequest = ws._req; + assert.strictEqual( - ws._req.getHeader('Authorization'), - 'Basic Zm9vOmJhcg==' + firstRequest.getHeader('Authorization'), + headers.authorization ); - assert.strictEqual(ws._req.getHeader('Cookie'), 'foo=bar'); - assert.strictEqual(ws._req.getHeader('Host'), 'foo'); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); ws.on('close', (code) => { assert.strictEqual(code, 1005); @@ -1524,6 +1527,7 @@ describe('WebSocket', () => { req.headers.host, `localhost:${wss.address().port}` ); + ws.close(); }); }); @@ -1549,7 +1553,7 @@ describe('WebSocket', () => { const ws = new WebSocket( `ws://localhost:${server.address().port}`, - { headers, followRedirects: true } + { followRedirects: true, headers } ); const firstRequest = ws._req; @@ -1643,8 +1647,8 @@ describe('WebSocket', () => { }; const ws = new WebSocket(`ws://localhost:${server.address().port}`, { - headers, - followRedirects: true + followRedirects: true, + headers }); ws.on('redirect', (url, req) => { From bc8bd34e4125a5edc37211fb384c17a4703e9000 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 15 Jul 2022 09:32:52 +0200 Subject: [PATCH 171/328] [security] Fix same host check for ws+unix: redirects Drop the `Authorization` and `Cookie` headers if the original request for the opening handshake is sent to an IPC server and the client is redirected to a TCP server (ws+unix: to ws: or wss:), and vice versa (ws: or wss: to ws+unix). Also drop the `Authorization` and `Cookie` headers if the original request for the opening handshake is sent to an IPC server and the client is redirected to another IPC server. Refs: https://github.com/websockets/ws/commit/6946f5fe --- lib/websocket.js | 13 ++- test/websocket.test.js | 221 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 231 insertions(+), 3 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 68405dceb..3132cc150 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -771,8 +771,11 @@ function initAsClient(websocket, address, protocols, options) { if (opts.followRedirects) { if (websocket._redirects === 0) { + websocket._originalUnixSocket = isUnixSocket; websocket._originalSecure = isSecure; - websocket._originalHost = parsedUrl.host; + websocket._originalHostOrSocketPath = isUnixSocket + ? opts.socketPath + : parsedUrl.host; const headers = options && options.headers; @@ -788,7 +791,13 @@ function initAsClient(websocket, address, protocols, options) { } } } else if (websocket.listenerCount('redirect') === 0) { - const isSameHost = parsedUrl.host === websocket._originalHost; + const isSameHost = isUnixSocket + ? websocket._originalUnixSocket + ? opts.socketPath === websocket._originalHostOrSocketPath + : false + : websocket._originalUnixSocket + ? false + : parsedUrl.host === websocket._originalHostOrSocketPath; if (!isSameHost || (websocket._originalSecure && !isSecure)) { // diff --git a/test/websocket.test.js b/test/websocket.test.js index 64b5bf224..f5fbf1650 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -6,8 +6,10 @@ const assert = require('assert'); const crypto = require('crypto'); const https = require('https'); const http = require('http'); +const path = require('path'); const net = require('net'); const tls = require('tls'); +const os = require('os'); const fs = require('fs'); const { URL } = require('url'); @@ -1477,7 +1479,9 @@ describe('WebSocket', () => { }); }); - it('drops the Authorization, Cookie and Host headers', (done) => { + it('drops the Authorization, Cookie and Host headers (1/4)', (done) => { + // Test the `ws:` to `ws:` case. + const wss = new WebSocket.Server({ port: 0 }, () => { const port = wss.address().port; @@ -1531,6 +1535,221 @@ describe('WebSocket', () => { ws.close(); }); }); + + it('drops the Authorization, Cookie and Host headers (2/4)', function (done) { + if (process.platform === 'win32') return this.skip(); + + // Test the `ws:` to `ws+unix:` case. + + const socketPath = path.join( + os.tmpdir(), + `ws.${crypto.randomBytes(16).toString('hex')}.sock` + ); + + server.once('upgrade', (req, socket) => { + socket.end( + `HTTP/1.1 302 Found\r\nLocation: ws+unix://${socketPath}\r\n\r\n` + ); + }); + + const redirectedServer = http.createServer(); + const wss = new WebSocket.Server({ server: redirectedServer }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + assert.strictEqual(req.headers.cookie, undefined); + assert.strictEqual(req.headers.host, 'localhost'); + + ws.close(); + }); + + redirectedServer.listen(socketPath, () => { + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const ws = new WebSocket( + `ws://localhost:${server.address().port}`, + { followRedirects: true, headers } + ); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws+unix://${socketPath}`); + assert.strictEqual(ws._redirects, 1); + + redirectedServer.close(done); + }); + }); + }); + + it('drops the Authorization, Cookie and Host headers (3/4)', function (done) { + if (process.platform === 'win32') return this.skip(); + + // Test the `ws+unix:` to `ws+unix:` case. + + const redirectingServerSocketPath = path.join( + os.tmpdir(), + `ws.${crypto.randomBytes(16).toString('hex')}.sock` + ); + const redirectedServerSocketPath = path.join( + os.tmpdir(), + `ws.${crypto.randomBytes(16).toString('hex')}.sock` + ); + + const redirectingServer = http.createServer(); + + redirectingServer.on('upgrade', (req, socket) => { + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws+unix://${redirectedServerSocketPath}\r\n\r\n` + ); + }); + + const redirectedServer = http.createServer(); + const wss = new WebSocket.Server({ server: redirectedServer }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + assert.strictEqual(req.headers.cookie, undefined); + assert.strictEqual(req.headers.host, 'localhost'); + + ws.close(); + }); + + redirectingServer.listen(redirectingServerSocketPath, listening); + redirectedServer.listen(redirectedServerSocketPath, listening); + + let callCount = 0; + + function listening() { + if (++callCount !== 2) return; + + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const ws = new WebSocket( + `ws+unix://${redirectingServerSocketPath}`, + { followRedirects: true, headers } + ); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual( + ws.url, + `ws+unix://${redirectedServerSocketPath}` + ); + assert.strictEqual(ws._redirects, 1); + + redirectingServer.close(); + redirectedServer.close(done); + }); + } + }); + + it('drops the Authorization, Cookie and Host headers (4/4)', function (done) { + if (process.platform === 'win32') return this.skip(); + + // Test the `ws+unix:` to `ws:` case. + + const redirectingServer = http.createServer(); + const redirectedServer = http.createServer(); + const wss = new WebSocket.Server({ server: redirectedServer }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + assert.strictEqual(req.headers.cookie, undefined); + assert.strictEqual( + req.headers.host, + `localhost:${redirectedServer.address().port}` + ); + + ws.close(); + }); + + const socketPath = path.join( + os.tmpdir(), + `ws.${crypto.randomBytes(16).toString('hex')}.sock` + ); + + redirectingServer.listen(socketPath, listening); + redirectedServer.listen(0, listening); + + let callCount = 0; + + function listening() { + if (++callCount !== 2) return; + + const port = redirectedServer.address().port; + + redirectingServer.on('upgrade', (req, socket) => { + socket.end( + `HTTP/1.1 302 Found\r\nLocation: ws://localhost:${port}\r\n\r\n` + ); + }); + + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const ws = new WebSocket(`ws+unix://${socketPath}`, { + followRedirects: true, + headers + }); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://localhost:${port}/`); + assert.strictEqual(ws._redirects, 1); + + redirectingServer.close(); + redirectedServer.close(done); + }); + } + }); }); describe("If there is at least one 'redirect' event listener", () => { From 975382178f8a9355a5a564bb29cb1566889da9ba Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 15 Jul 2022 13:34:15 +0200 Subject: [PATCH 172/328] [dist] 8.8.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e09fae395..27b9244a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.8.0", + "version": "8.8.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From c22cbc9e743a001b3fc55c1aa77b0afcc1018ee9 Mon Sep 17 00:00:00 2001 From: Dathan Date: Sun, 18 Sep 2022 14:48:05 -0400 Subject: [PATCH 173/328] [doc] Revise `WebSocket#send()` docs for clarity (#2077) Add reference to Node.js's own related documentation about the constraints on `Object` values. Fixes #2076 --- doc/ws.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index 4888ee4c6..0b1ba7a37 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -554,7 +554,9 @@ only removes listeners added with ### websocket.send(data[, options][, callback]) - `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The - data to send. + data to send. `Object` values are only supported if they conform to the + requirements of [`Buffer.from()`][]. If those constraints are not met, a + `TypeError` is thrown. - `options` {Object} - `binary` {Boolean} Specifies whether `data` should be sent as a binary or not. Default is autodetected. @@ -656,6 +658,8 @@ as configured by the `maxPayload` option. [concurrency-limit]: https://github.com/websockets/ws/issues/1202 [duplex-options]: https://nodejs.org/api/stream.html#stream_new_stream_duplex_options +[`buffer.from()`]: + https://nodejs.org/api/buffer.html#static-method-bufferfromobject-offsetorencoding-length [`http.request()`]: https://nodejs.org/api/http.html#http_http_request_options_callback [`https.request()`]: From 2995349f5d27d39e492a273796a3145d40ec11b3 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 20 Sep 2022 20:46:57 +0200 Subject: [PATCH 174/328] [minor] Simplify the ws+unix: URL form Remove the two forward slashes after the scheme. The URL is parsed in the same way and it is easier to grok. --- doc/ws.md | 6 +++--- test/websocket.test.js | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 0b1ba7a37..2c49c766f 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -326,17 +326,17 @@ Create a new WebSocket instance. #### UNIX Domain Sockets `ws` supports making requests to UNIX domain sockets. To make one, use the -following URL scheme: +following URL form: ``` -ws+unix:///absolute/path/to/uds_socket:/pathname?search_params +ws+unix:/absolute/path/to/uds_socket:/pathname?search_params ``` Note that `:` is the separator between the socket path and the URL path. If the URL path is omitted ``` -ws+unix:///absolute/path/to/uds_socket +ws+unix:/absolute/path/to/uds_socket ``` it defaults to `/`. diff --git a/test/websocket.test.js b/test/websocket.test.js index f5fbf1650..cf4cf10f2 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1548,7 +1548,7 @@ describe('WebSocket', () => { server.once('upgrade', (req, socket) => { socket.end( - `HTTP/1.1 302 Found\r\nLocation: ws+unix://${socketPath}\r\n\r\n` + `HTTP/1.1 302 Found\r\nLocation: ws+unix:${socketPath}\r\n\r\n` ); }); @@ -1589,7 +1589,7 @@ describe('WebSocket', () => { ws.on('close', (code) => { assert.strictEqual(code, 1005); - assert.strictEqual(ws.url, `ws+unix://${socketPath}`); + assert.strictEqual(ws.url, `ws+unix:${socketPath}`); assert.strictEqual(ws._redirects, 1); redirectedServer.close(done); @@ -1616,7 +1616,7 @@ describe('WebSocket', () => { redirectingServer.on('upgrade', (req, socket) => { socket.end( 'HTTP/1.1 302 Found\r\n' + - `Location: ws+unix://${redirectedServerSocketPath}\r\n\r\n` + `Location: ws+unix:${redirectedServerSocketPath}\r\n\r\n` ); }); @@ -1645,10 +1645,10 @@ describe('WebSocket', () => { host: 'foo' }; - const ws = new WebSocket( - `ws+unix://${redirectingServerSocketPath}`, - { followRedirects: true, headers } - ); + const ws = new WebSocket(`ws+unix:${redirectingServerSocketPath}`, { + followRedirects: true, + headers + }); const firstRequest = ws._req; @@ -1666,7 +1666,7 @@ describe('WebSocket', () => { assert.strictEqual(code, 1005); assert.strictEqual( ws.url, - `ws+unix://${redirectedServerSocketPath}` + `ws+unix:${redirectedServerSocketPath}` ); assert.strictEqual(ws._redirects, 1); @@ -1723,7 +1723,7 @@ describe('WebSocket', () => { host: 'foo' }; - const ws = new WebSocket(`ws+unix://${socketPath}`, { + const ws = new WebSocket(`ws+unix:${socketPath}`, { followRedirects: true, headers }); From 6b71a49d1d02e8220eaa983e9202517c9680d2b2 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 21 Sep 2022 10:01:04 +0200 Subject: [PATCH 175/328] [minor] Rename the `invalidURLMessage` variable Rename the `invalidURLMessage` variable to `invalidUrlMessage`. --- lib/websocket.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 3132cc150..8c4512d48 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -678,19 +678,19 @@ function initAsClient(websocket, address, protocols, options) { const isSecure = parsedUrl.protocol === 'wss:'; const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; - let invalidURLMessage; + let invalidUrlMessage; if (parsedUrl.protocol !== 'ws:' && !isSecure && !isUnixSocket) { - invalidURLMessage = + invalidUrlMessage = 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; } else if (isUnixSocket && !parsedUrl.pathname) { - invalidURLMessage = "The URL's pathname is empty"; + invalidUrlMessage = "The URL's pathname is empty"; } else if (parsedUrl.hash) { - invalidURLMessage = 'The URL contains a fragment identifier'; + invalidUrlMessage = 'The URL contains a fragment identifier'; } - if (invalidURLMessage) { - const err = new SyntaxError(invalidURLMessage); + if (invalidUrlMessage) { + const err = new SyntaxError(invalidUrlMessage); if (websocket._redirects === 0) { throw err; From 0da60366f36a38e93bb1a31da54f260a146eb86c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 21 Sep 2022 12:05:53 +0200 Subject: [PATCH 176/328] [test] Remove leftover from test Refs: https://github.com/websockets/ws/commit/2995349f --- test/websocket-server.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 12928ff49..2ea81d319 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -210,8 +210,8 @@ describe('WebSocketServer', () => { } }); - const ws = new WebSocket(`ws+unix://${sockPath}:/foo?bar=bar`); - ws.on('open', () => new WebSocket(`ws+unix://${sockPath}`)); + const ws = new WebSocket(`ws+unix:${sockPath}:/foo?bar=bar`); + ws.on('open', () => new WebSocket(`ws+unix:${sockPath}`)); }); }); }); From 4ed2c8725ec796283498e4a9c90171668eef9117 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 21 Sep 2022 16:38:30 +0200 Subject: [PATCH 177/328] [doc] Remove "note that" --- doc/ws.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 2c49c766f..d857edf57 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -232,8 +232,8 @@ a pipe or UNIX domain socket, the name is returned as a string. - {Set} -A set that stores all connected clients. Please note that this property is only -added when the `clientTracking` is truthy. +A set that stores all connected clients. This property is only added when the +`clientTracking` is truthy. ### server.close([callback]) @@ -332,8 +332,8 @@ following URL form: ws+unix:/absolute/path/to/uds_socket:/pathname?search_params ``` -Note that `:` is the separator between the socket path and the URL path. If the -URL path is omitted +The character `:` is the separator between the socket path and the URL path. If +the URL path is omitted ``` ws+unix:/absolute/path/to/uds_socket From 7ff26d975a7c5e0012a6799ed15a7ef8211b59bb Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 21 Sep 2022 16:39:27 +0200 Subject: [PATCH 178/328] [doc] Fix nits --- doc/ws.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index d857edf57..ca608fde2 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -103,7 +103,7 @@ completely detached from the HTTP/S server. This makes it possible, for example, to share a single HTTP/S server between multiple WebSocket servers. > **NOTE:** Use of `verifyClient` is discouraged. Rather handle client -> authentication in the `upgrade` event of the HTTP server. See examples for +> authentication in the `'upgrade'` event of the HTTP server. See examples for > more details. If `verifyClient` is not set then the handshake is automatically accepted. If it @@ -170,7 +170,7 @@ is used. When sending a fragmented message the length of the first fragment is compared to the threshold. This determines if compression is used for the entire message. -`callback` will be added as a listener for the `listening` event on the HTTP +`callback` will be added as a listener for the `'listening'` event on the HTTP server when not operating in "noServer" mode. ### Event: 'close' From e628f2bdb6277fa36e8607cf1c3fbaf713f7bcef Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 22 Sep 2022 20:37:20 +0200 Subject: [PATCH 179/328] [feature] Support Windows named pipes (#2079) Document how to connect to a named pipe endpoint and the limitations. Refs: https://github.com/websockets/ws/pull/1808 Refs: https://github.com/websockets/ws/pull/2075 --- doc/ws.md | 28 +++++++++---- lib/websocket.js | 18 ++++---- test/websocket-server.test.js | 24 ++++------- test/websocket.test.js | 79 ++++++++++++++++++----------------- 4 files changed, 78 insertions(+), 71 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index ca608fde2..e045a0db1 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -18,7 +18,7 @@ - [Class: WebSocket](#class-websocket) - [Ready state constants](#ready-state-constants) - [new WebSocket(address[, protocols][, options])](#new-websocketaddress-protocols-options) - - [UNIX Domain Sockets](#unix-domain-sockets) + - [IPC connections](#ipc-connections) - [Event: 'close'](#event-close-1) - [Event: 'error'](#event-error-1) - [Event: 'message'](#event-message) @@ -323,17 +323,27 @@ context takeover. Create a new WebSocket instance. -#### UNIX Domain Sockets +#### IPC connections -`ws` supports making requests to UNIX domain sockets. To make one, use the -following URL form: +`ws` supports IPC connections. To connect to an IPC endpoint, use the following +URL form: -``` -ws+unix:/absolute/path/to/uds_socket:/pathname?search_params -``` +- On Unices + + ``` + ws+unix:/absolute/path/to/uds_socket:/pathname?search_params + ``` + +- On Windows + + ``` + ws+unix:\\.\pipe\pipe_name:/pathname?search_params + ``` -The character `:` is the separator between the socket path and the URL path. If -the URL path is omitted +The character `:` is the separator between the IPC path (the Unix domain socket +path or the Windows named pipe) and the URL path. The IPC path must not include +the characters `:` and `?`, otherwise the URL is incorrectly parsed. If the URL +path is omitted ``` ws+unix:/absolute/path/to/uds_socket diff --git a/lib/websocket.js b/lib/websocket.js index 8c4512d48..4391c73ab 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -677,13 +677,13 @@ function initAsClient(websocket, address, protocols, options) { } const isSecure = parsedUrl.protocol === 'wss:'; - const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; + const isIpcUrl = parsedUrl.protocol === 'ws+unix:'; let invalidUrlMessage; - if (parsedUrl.protocol !== 'ws:' && !isSecure && !isUnixSocket) { + if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { invalidUrlMessage = 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; - } else if (isUnixSocket && !parsedUrl.pathname) { + } else if (isIpcUrl && !parsedUrl.pathname) { invalidUrlMessage = "The URL's pathname is empty"; } else if (parsedUrl.hash) { invalidUrlMessage = 'The URL contains a fragment identifier'; @@ -760,7 +760,7 @@ function initAsClient(websocket, address, protocols, options) { opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; } - if (isUnixSocket) { + if (isIpcUrl) { const parts = opts.path.split(':'); opts.socketPath = parts[0]; @@ -771,9 +771,9 @@ function initAsClient(websocket, address, protocols, options) { if (opts.followRedirects) { if (websocket._redirects === 0) { - websocket._originalUnixSocket = isUnixSocket; + websocket._originalIpc = isIpcUrl; websocket._originalSecure = isSecure; - websocket._originalHostOrSocketPath = isUnixSocket + websocket._originalHostOrSocketPath = isIpcUrl ? opts.socketPath : parsedUrl.host; @@ -791,11 +791,11 @@ function initAsClient(websocket, address, protocols, options) { } } } else if (websocket.listenerCount('redirect') === 0) { - const isSameHost = isUnixSocket - ? websocket._originalUnixSocket + const isSameHost = isIpcUrl + ? websocket._originalIpc ? opts.socketPath === websocket._originalHostOrSocketPath : false - : websocket._originalUnixSocket + : websocket._originalIpc ? false : parsedUrl.host === websocket._originalHostOrSocketPath; diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 2ea81d319..abed1650a 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -178,22 +178,16 @@ describe('WebSocketServer', () => { }); }); - it('uses a precreated http server listening on unix socket', function (done) { - // - // Skip this test on Windows. The URL parser: - // - // - Throws an error if the named pipe uses backward slashes. - // - Incorrectly parses the path if the named pipe uses forward slashes. - // - if (process.platform === 'win32') return this.skip(); + it('uses a precreated http server listening on IPC', (done) => { + const randomString = crypto.randomBytes(16).toString('hex'); + const ipcPath = + process.platform === 'win32' + ? `\\\\.\\pipe\\ws-pipe-${randomString}` + : path.join(os.tmpdir(), `ws-${randomString}.sock`); const server = http.createServer(); - const sockPath = path.join( - os.tmpdir(), - `ws.${crypto.randomBytes(16).toString('hex')}.sock` - ); - server.listen(sockPath, () => { + server.listen(ipcPath, () => { const wss = new WebSocket.Server({ server }); wss.on('connection', (ws, req) => { @@ -210,8 +204,8 @@ describe('WebSocketServer', () => { } }); - const ws = new WebSocket(`ws+unix:${sockPath}:/foo?bar=bar`); - ws.on('open', () => new WebSocket(`ws+unix:${sockPath}`)); + const ws = new WebSocket(`ws+unix:${ipcPath}:/foo?bar=bar`); + ws.on('open', () => new WebSocket(`ws+unix:${ipcPath}`)); }); }); }); diff --git a/test/websocket.test.js b/test/websocket.test.js index cf4cf10f2..fad80ddb9 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1536,19 +1536,18 @@ describe('WebSocket', () => { }); }); - it('drops the Authorization, Cookie and Host headers (2/4)', function (done) { - if (process.platform === 'win32') return this.skip(); - + it('drops the Authorization, Cookie and Host headers (2/4)', (done) => { // Test the `ws:` to `ws+unix:` case. - const socketPath = path.join( - os.tmpdir(), - `ws.${crypto.randomBytes(16).toString('hex')}.sock` - ); + const randomString = crypto.randomBytes(16).toString('hex'); + const ipcPath = + process.platform === 'win32' + ? `\\\\.\\pipe\\ws-pipe-${randomString}` + : path.join(os.tmpdir(), `ws-${randomString}.sock`); server.once('upgrade', (req, socket) => { socket.end( - `HTTP/1.1 302 Found\r\nLocation: ws+unix:${socketPath}\r\n\r\n` + `HTTP/1.1 302 Found\r\nLocation: ws+unix:${ipcPath}\r\n\r\n` ); }); @@ -1563,7 +1562,7 @@ describe('WebSocket', () => { ws.close(); }); - redirectedServer.listen(socketPath, () => { + redirectedServer.listen(ipcPath, () => { const headers = { authorization: 'Basic Zm9vOmJhcg==', cookie: 'foo=bar', @@ -1589,7 +1588,7 @@ describe('WebSocket', () => { ws.on('close', (code) => { assert.strictEqual(code, 1005); - assert.strictEqual(ws.url, `ws+unix:${socketPath}`); + assert.strictEqual(ws.url, `ws+unix:${ipcPath}`); assert.strictEqual(ws._redirects, 1); redirectedServer.close(done); @@ -1597,26 +1596,34 @@ describe('WebSocket', () => { }); }); - it('drops the Authorization, Cookie and Host headers (3/4)', function (done) { - if (process.platform === 'win32') return this.skip(); - + it('drops the Authorization, Cookie and Host headers (3/4)', (done) => { // Test the `ws+unix:` to `ws+unix:` case. - const redirectingServerSocketPath = path.join( - os.tmpdir(), - `ws.${crypto.randomBytes(16).toString('hex')}.sock` - ); - const redirectedServerSocketPath = path.join( - os.tmpdir(), - `ws.${crypto.randomBytes(16).toString('hex')}.sock` - ); + const randomString1 = crypto.randomBytes(16).toString('hex'); + const randomString2 = crypto.randomBytes(16).toString('hex'); + let redirectingServerIpcPath; + let redirectedServerIpcPath; + + if (process.platform === 'win32') { + redirectingServerIpcPath = `\\\\.\\pipe\\ws-pipe-${randomString1}`; + redirectedServerIpcPath = `\\\\.\\pipe\\ws-pipe-${randomString2}`; + } else { + redirectingServerIpcPath = path.join( + os.tmpdir(), + `ws-${randomString1}.sock` + ); + redirectedServerIpcPath = path.join( + os.tmpdir(), + `ws-${randomString2}.sock` + ); + } const redirectingServer = http.createServer(); redirectingServer.on('upgrade', (req, socket) => { socket.end( 'HTTP/1.1 302 Found\r\n' + - `Location: ws+unix:${redirectedServerSocketPath}\r\n\r\n` + `Location: ws+unix:${redirectedServerIpcPath}\r\n\r\n` ); }); @@ -1631,8 +1638,8 @@ describe('WebSocket', () => { ws.close(); }); - redirectingServer.listen(redirectingServerSocketPath, listening); - redirectedServer.listen(redirectedServerSocketPath, listening); + redirectingServer.listen(redirectingServerIpcPath, listening); + redirectedServer.listen(redirectedServerIpcPath, listening); let callCount = 0; @@ -1645,7 +1652,7 @@ describe('WebSocket', () => { host: 'foo' }; - const ws = new WebSocket(`ws+unix:${redirectingServerSocketPath}`, { + const ws = new WebSocket(`ws+unix:${redirectingServerIpcPath}`, { followRedirects: true, headers }); @@ -1664,10 +1671,7 @@ describe('WebSocket', () => { ws.on('close', (code) => { assert.strictEqual(code, 1005); - assert.strictEqual( - ws.url, - `ws+unix:${redirectedServerSocketPath}` - ); + assert.strictEqual(ws.url, `ws+unix:${redirectedServerIpcPath}`); assert.strictEqual(ws._redirects, 1); redirectingServer.close(); @@ -1676,9 +1680,7 @@ describe('WebSocket', () => { } }); - it('drops the Authorization, Cookie and Host headers (4/4)', function (done) { - if (process.platform === 'win32') return this.skip(); - + it('drops the Authorization, Cookie and Host headers (4/4)', (done) => { // Test the `ws+unix:` to `ws:` case. const redirectingServer = http.createServer(); @@ -1696,12 +1698,13 @@ describe('WebSocket', () => { ws.close(); }); - const socketPath = path.join( - os.tmpdir(), - `ws.${crypto.randomBytes(16).toString('hex')}.sock` - ); + const randomString = crypto.randomBytes(16).toString('hex'); + const ipcPath = + process.platform === 'win32' + ? `\\\\.\\pipe\\ws-pipe-${randomString}` + : path.join(os.tmpdir(), `ws-${randomString}.sock`); - redirectingServer.listen(socketPath, listening); + redirectingServer.listen(ipcPath, listening); redirectedServer.listen(0, listening); let callCount = 0; @@ -1723,7 +1726,7 @@ describe('WebSocket', () => { host: 'foo' }; - const ws = new WebSocket(`ws+unix:${socketPath}`, { + const ws = new WebSocket(`ws+unix:${ipcPath}`, { followRedirects: true, headers }); From 966f9d47cd0ff5aa9db0b2aa262f9819d3f4d414 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 22 Sep 2022 20:45:50 +0200 Subject: [PATCH 180/328] [dist] 8.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27b9244a4..135a345c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.8.1", + "version": "8.9.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From c4d6eb3907e8bf6e9d1526772a4778834f9b025c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 20 Oct 2022 21:17:02 +0200 Subject: [PATCH 181/328] [ci] Do not use the set-output command The `set-output` command is deprecated. Use the `GITHUB_OUTPUT` environment file. Refs: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/ --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3a787a65..f21a98e9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,9 +44,10 @@ jobs: matrix.os == 'ubuntu-latest' && matrix.node == 16 && matrix.arch == 'x64' - run: npm test - - run: - echo ::set-output name=job_id::$(node -e - "console.log(crypto.randomBytes(16).toString('hex'))") + - run: | + id=$(node -e "console.log(crypto.randomBytes(16).toString('hex'))") + + echo "job_id=$id" >> $GITHUB_OUTPUT id: get_job_id shell: bash - uses: coverallsapp/github-action@1.1.3 From 211d5d3833893bcfde30e17e4aa9d61e0b408ee5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 24 Oct 2022 20:36:48 +0200 Subject: [PATCH 182/328] [pkg] Add package.json export Fixes #2091 --- package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 135a345c3..b145d06b1 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,11 @@ "license": "MIT", "main": "index.js", "exports": { - "import": "./wrapper.mjs", - "require": "./index.js" + ".": { + "import": "./wrapper.mjs", + "require": "./index.js" + }, + "./package.json": "./package.json" }, "browser": "browser.js", "engines": { From cdca711ad434fe4f691392807cba3f83a515eebe Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 24 Oct 2022 20:59:36 +0200 Subject: [PATCH 183/328] [dist] 8.10.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b145d06b1..c5d663f3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.9.0", + "version": "8.10.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 38f78794ca68770e962d76fcdb50dd264696a5e5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 2 Nov 2022 16:35:35 +0100 Subject: [PATCH 184/328] [ci] Test on node 19 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f21a98e9d..aee196f92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ jobs: - 14 - 16 - 18 + - 19 os: - macOS-latest - ubuntu-latest From 9ab743aa706be653e3b3c94d07960fe4342f9da5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 4 Nov 2022 17:13:56 +0100 Subject: [PATCH 185/328] [feature] Add support for objets with a `handleEvent()` method Make `WebSocket.prototype.addEventListener()` support an event listener specified as an object with a `handleEvent()` method. Fixes #2092 --- doc/ws.md | 4 ++-- lib/event-target.js | 28 ++++++++++++++++++++++------ test/websocket.test.js | 23 +++++++++++++---------- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index e045a0db1..abf0f0735 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -426,7 +426,7 @@ handshake. This allows you to read headers from the server, for example ### websocket.addEventListener(type, listener[, options]) - `type` {String} A string representing the event type to listen for. -- `listener` {Function} The listener to add. +- `listener` {Function|Object} The listener to add. - `options` {Object} - `once` {Boolean} A `Boolean` indicating that the listener should be invoked at most once after being added. If `true`, the listener would be @@ -555,7 +555,7 @@ The current state of the connection. This is one of the ready state constants. ### websocket.removeEventListener(type, listener) - `type` {String} A string representing the event type to remove. -- `listener` {Function} The listener to remove. +- `listener` {Function|Object} The listener to remove. Removes an event listener emulating the `EventTarget` interface. This method only removes listeners added with diff --git a/lib/event-target.js b/lib/event-target.js index d5abd83a0..6b185270a 100644 --- a/lib/event-target.js +++ b/lib/event-target.js @@ -178,7 +178,7 @@ const EventTarget = { * Register an event listener. * * @param {String} type A string representing the event type to listen for - * @param {Function} listener The listener to add + * @param {(Function|Object)} listener The listener to add * @param {Object} [options] An options object specifies characteristics about * the event listener * @param {Boolean} [options.once=false] A `Boolean` indicating that the @@ -196,7 +196,7 @@ const EventTarget = { }); event[kTarget] = this; - listener.call(this, event); + callListener(listener, this, event); }; } else if (type === 'close') { wrapper = function onClose(code, message) { @@ -207,7 +207,7 @@ const EventTarget = { }); event[kTarget] = this; - listener.call(this, event); + callListener(listener, this, event); }; } else if (type === 'error') { wrapper = function onError(error) { @@ -217,14 +217,14 @@ const EventTarget = { }); event[kTarget] = this; - listener.call(this, event); + callListener(listener, this, event); }; } else if (type === 'open') { wrapper = function onOpen() { const event = new Event('open'); event[kTarget] = this; - listener.call(this, event); + callListener(listener, this, event); }; } else { return; @@ -244,7 +244,7 @@ const EventTarget = { * Remove an event listener. * * @param {String} type A string representing the event type to remove - * @param {Function} handler The listener to remove + * @param {(Function|Object)} handler The listener to remove * @public */ removeEventListener(type, handler) { @@ -264,3 +264,19 @@ module.exports = { EventTarget, MessageEvent }; + +/** + * Call an event listener + * + * @param {(Function|Object)} listener The listener to call + * @param {*} thisArg The value to use as `this`` when calling the listener + * @param {Event} event The event to pass to the listener + * @private + */ +function callListener(listener, thisArg, event) { + if (typeof listener === 'object' && listener.handleEvent) { + listener.handleEvent.call(listener, event); + } else { + listener.call(thisArg, event); + } +} diff --git a/test/websocket.test.js b/test/websocket.test.js index fad80ddb9..bbbc5ae9f 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -3264,14 +3264,15 @@ describe('WebSocket', () => { assert.strictEqual(ws.listenerCount('open'), 1); - ws.addEventListener( - 'message', - () => { + const listener = { + handleEvent() { events.push('message'); + assert.strictEqual(this, listener); assert.strictEqual(ws.listenerCount('message'), 0); - }, - { once: true } - ); + } + }; + + ws.addEventListener('message', listener, { once: true }); assert.strictEqual(ws.listenerCount('message'), 1); @@ -3318,17 +3319,19 @@ describe('WebSocket', () => { it('supports the `removeEventListener` method', () => { const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); - ws.addEventListener('message', NOOP); + const listener = { handleEvent() {} }; + + ws.addEventListener('message', listener); ws.addEventListener('open', NOOP); - assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); + assert.strictEqual(ws.listeners('message')[0][kListener], listener); assert.strictEqual(ws.listeners('open')[0][kListener], NOOP); ws.removeEventListener('message', () => {}); - assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); + assert.strictEqual(ws.listeners('message')[0][kListener], listener); - ws.removeEventListener('message', NOOP); + ws.removeEventListener('message', listener); ws.removeEventListener('open', NOOP); assert.strictEqual(ws.listenerCount('message'), 0); From 1cec17da060ef1a4656a6d530c2b686039b7e094 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 4 Nov 2022 21:34:26 +0100 Subject: [PATCH 186/328] [fix] Add the same event listener only once Prevent `WebSocket.prototype.addEventListener()` from adding the same event listener multiple times. --- lib/event-target.js | 24 +++++++++++++++++------- test/websocket.test.js | 41 ++++++++++++++++++++++++----------------- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/lib/event-target.js b/lib/event-target.js index 6b185270a..fea4cbc52 100644 --- a/lib/event-target.js +++ b/lib/event-target.js @@ -178,7 +178,7 @@ const EventTarget = { * Register an event listener. * * @param {String} type A string representing the event type to listen for - * @param {(Function|Object)} listener The listener to add + * @param {(Function|Object)} handler The listener to add * @param {Object} [options] An options object specifies characteristics about * the event listener * @param {Boolean} [options.once=false] A `Boolean` indicating that the @@ -186,7 +186,17 @@ const EventTarget = { * the listener would be automatically removed when invoked. * @public */ - addEventListener(type, listener, options = {}) { + addEventListener(type, handler, options = {}) { + for (const listener of this.listeners(type)) { + if ( + !options[kForOnEventAttribute] && + listener[kListener] === handler && + !listener[kForOnEventAttribute] + ) { + return; + } + } + let wrapper; if (type === 'message') { @@ -196,7 +206,7 @@ const EventTarget = { }); event[kTarget] = this; - callListener(listener, this, event); + callListener(handler, this, event); }; } else if (type === 'close') { wrapper = function onClose(code, message) { @@ -207,7 +217,7 @@ const EventTarget = { }); event[kTarget] = this; - callListener(listener, this, event); + callListener(handler, this, event); }; } else if (type === 'error') { wrapper = function onError(error) { @@ -217,21 +227,21 @@ const EventTarget = { }); event[kTarget] = this; - callListener(listener, this, event); + callListener(handler, this, event); }; } else if (type === 'open') { wrapper = function onOpen() { const event = new Event('open'); event[kTarget] = this; - callListener(listener, this, event); + callListener(handler, this, event); }; } else { return; } wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; - wrapper[kListener] = listener; + wrapper[kListener] = handler; if (options.once) { this.once(type, wrapper); diff --git a/test/websocket.test.js b/test/websocket.test.js index bbbc5ae9f..6b2f3ef5c 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -3257,10 +3257,13 @@ describe('WebSocket', () => { ws.addEventListener('foo', () => {}); assert.strictEqual(ws.listenerCount('foo'), 0); - ws.addEventListener('open', () => { + function onOpen() { events.push('open'); assert.strictEqual(ws.listenerCount('open'), 1); - }); + } + + ws.addEventListener('open', onOpen); + ws.addEventListener('open', onOpen); assert.strictEqual(ws.listenerCount('open'), 1); @@ -3273,9 +3276,28 @@ describe('WebSocket', () => { }; ws.addEventListener('message', listener, { once: true }); + ws.addEventListener('message', listener); assert.strictEqual(ws.listenerCount('message'), 1); + ws.addEventListener('close', NOOP); + ws.onclose = NOOP; + + let listeners = ws.listeners('close'); + + assert.strictEqual(listeners.length, 2); + assert.strictEqual(listeners[0][kListener], NOOP); + assert.strictEqual(listeners[1][kListener], NOOP); + + ws.onerror = NOOP; + ws.addEventListener('error', NOOP); + + listeners = ws.listeners('error'); + + assert.strictEqual(listeners.length, 2); + assert.strictEqual(listeners[0][kListener], NOOP); + assert.strictEqual(listeners[1][kListener], NOOP); + ws.emit('open'); ws.emit('message', EMPTY_BUFFER, false); @@ -3353,21 +3375,6 @@ describe('WebSocket', () => { assert.strictEqual(ws.listenerCount('message'), 0); assert.strictEqual(ws.listenerCount('open'), 0); - // Multiple listeners. - ws.addEventListener('message', NOOP); - ws.addEventListener('message', NOOP); - - assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); - assert.strictEqual(ws.listeners('message')[1][kListener], NOOP); - - ws.removeEventListener('message', NOOP); - - assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); - - ws.removeEventListener('message', NOOP); - - assert.strictEqual(ws.listenerCount('message'), 0); - // Listeners not added with `websocket.addEventListener()`. ws.on('message', NOOP); From afd8c6269bf5056a052281c543e9f19c7d88673d Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 6 Nov 2022 19:30:29 +0100 Subject: [PATCH 187/328] [dist] 8.11.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5d663f3c..24ffdc55a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.10.0", + "version": "8.11.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From ea761933702bde061c2f5ac8aed5f62f9d5439ea Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 25 Nov 2022 18:57:14 +0100 Subject: [PATCH 188/328] [doc] Improve doc for the `callback` argument of `WebSocketServer` Clarify that the `callback` argument is added as a listener for the `'listening'` event only when the `port` option is set. Fixes #2100 --- doc/ws.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index abf0f0735..dd51eca9c 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -171,7 +171,7 @@ compared to the threshold. This determines if compression is used for the entire message. `callback` will be added as a listener for the `'listening'` event on the HTTP -server when not operating in "noServer" mode. +server when the `port` option is set. ### Event: 'close' From 8a8fc88430b2d32163d343ef507e201ffc48570a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 9 Dec 2022 12:34:03 +0100 Subject: [PATCH 189/328] [minor] Validate the payload length of the close frame sooner Avoid unnecessary work if the payload length is 1. --- lib/receiver.js | 13 ++++--------- test/receiver.test.js | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/receiver.js b/lib/receiver.js index 2d29d62bb..d9a54543c 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -259,7 +259,10 @@ class Receiver extends Writable { ); } - if (this._payloadLength > 0x7d) { + if ( + this._payloadLength > 0x7d || + (this._opcode === 0x08 && this._payloadLength === 1) + ) { this._loop = false; return error( RangeError, @@ -546,14 +549,6 @@ class Receiver extends Writable { if (data.length === 0) { this.emit('conclude', 1005, EMPTY_BUFFER); this.end(); - } else if (data.length === 1) { - return error( - RangeError, - 'invalid payload length 1', - true, - 1002, - 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' - ); } else { const code = data.readUInt16BE(0); diff --git a/test/receiver.test.js b/test/receiver.test.js index 7ee35f740..4ae279469 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -856,7 +856,7 @@ describe('Receiver', () => { done(); }); - receiver.write(Buffer.from([0x88, 0x01, 0x00])); + receiver.write(Buffer.from([0x88, 0x01])); }); it('emits an error if a close frame contains an invalid close code', (done) => { From a6fa37a1409c12f6a8672e6759a91d0fadd1b409 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 9 Dec 2022 15:47:38 +0100 Subject: [PATCH 190/328] [license] Update copyright notice --- LICENSE | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/LICENSE b/LICENSE index 65ff176bf..1da5b96a1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,19 +1,20 @@ Copyright (c) 2011 Einar Otto Stangvik +Copyright (c) 2013 Arnout Kazemier and contributors +Copyright (c) 2016 Luigi Pinca and contributors -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 9e0fd77799a0fcf16b8eb2f767358ef8bb834ec8 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 12 Dec 2022 11:18:17 +0100 Subject: [PATCH 191/328] [minor] Use `Buffer#subarray()` instead of `Buffer#slice()` `Buffer.prototype.slice()` is deprecated. --- lib/buffer-util.js | 2 +- lib/permessage-deflate.js | 2 +- lib/receiver.js | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/buffer-util.js b/lib/buffer-util.js index df7595546..eb27486ed 100644 --- a/lib/buffer-util.js +++ b/lib/buffer-util.js @@ -23,7 +23,7 @@ function concat(list, totalLength) { offset += buf.length; } - if (offset < totalLength) return target.slice(0, offset); + if (offset < totalLength) return target.subarray(0, offset); return target; } diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index 94603c98d..b03355805 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -437,7 +437,7 @@ class PerMessageDeflate { this._deflate[kTotalLength] ); - if (fin) data = data.slice(0, data.length - 4); + if (fin) data = data.subarray(0, data.length - 4); // // Ensure that the callback will not be called again in diff --git a/lib/receiver.js b/lib/receiver.js index d9a54543c..5adc208d9 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -97,8 +97,8 @@ class Receiver extends Writable { if (n < this._buffers[0].length) { const buf = this._buffers[0]; - this._buffers[0] = buf.slice(n); - return buf.slice(0, n); + this._buffers[0] = buf.subarray(n); + return buf.subarray(0, n); } const dst = Buffer.allocUnsafe(n); @@ -111,7 +111,7 @@ class Receiver extends Writable { dst.set(this._buffers.shift(), offset); } else { dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); - this._buffers[0] = buf.slice(n); + this._buffers[0] = buf.subarray(n); } n -= buf.length; @@ -562,7 +562,7 @@ class Receiver extends Writable { ); } - const buf = data.slice(2); + const buf = data.subarray(2); if (!this._skipUTF8Validation && !isValidUTF8(buf)) { return error( From e6a32f85a77c56f9bfe03e91b38b504192c8df5a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 13 Dec 2022 10:05:18 +0100 Subject: [PATCH 192/328] [perf] Use `FastBuffer` instead of `Buffer#subarray()` `Buffer.prototype.subarray()` performance is subpar on Node.js < 16.15.0. --- lib/buffer-util.js | 6 +++++- lib/permessage-deflate.js | 5 ++++- lib/receiver.js | 22 ++++++++++++++++++---- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/buffer-util.js b/lib/buffer-util.js index eb27486ed..e70feaaca 100644 --- a/lib/buffer-util.js +++ b/lib/buffer-util.js @@ -2,6 +2,8 @@ const { EMPTY_BUFFER } = require('./constants'); +const FastBuffer = Buffer[Symbol.species]; + /** * Merges an array of buffers into a new buffer. * @@ -23,7 +25,9 @@ function concat(list, totalLength) { offset += buf.length; } - if (offset < totalLength) return target.subarray(0, offset); + if (offset < totalLength) { + return new FastBuffer(target.buffer, target.byteOffset, offset); + } return target; } diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index b03355805..77d918b55 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -6,6 +6,7 @@ const bufferUtil = require('./buffer-util'); const Limiter = require('./limiter'); const { kStatusCode } = require('./constants'); +const FastBuffer = Buffer[Symbol.species]; const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); const kPerMessageDeflate = Symbol('permessage-deflate'); const kTotalLength = Symbol('total-length'); @@ -437,7 +438,9 @@ class PerMessageDeflate { this._deflate[kTotalLength] ); - if (fin) data = data.subarray(0, data.length - 4); + if (fin) { + data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4); + } // // Ensure that the callback will not be called again in diff --git a/lib/receiver.js b/lib/receiver.js index 5adc208d9..96f572cb1 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -12,6 +12,7 @@ const { const { concat, toArrayBuffer, unmask } = require('./buffer-util'); const { isValidStatusCode, isValidUTF8 } = require('./validation'); +const FastBuffer = Buffer[Symbol.species]; const GET_INFO = 0; const GET_PAYLOAD_LENGTH_16 = 1; const GET_PAYLOAD_LENGTH_64 = 2; @@ -97,8 +98,13 @@ class Receiver extends Writable { if (n < this._buffers[0].length) { const buf = this._buffers[0]; - this._buffers[0] = buf.subarray(n); - return buf.subarray(0, n); + this._buffers[0] = new FastBuffer( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); + + return new FastBuffer(buf.buffer, buf.byteOffset, n); } const dst = Buffer.allocUnsafe(n); @@ -111,7 +117,11 @@ class Receiver extends Writable { dst.set(this._buffers.shift(), offset); } else { dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); - this._buffers[0] = buf.subarray(n); + this._buffers[0] = new FastBuffer( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); } n -= buf.length; @@ -562,7 +572,11 @@ class Receiver extends Writable { ); } - const buf = data.subarray(2); + const buf = new FastBuffer( + data.buffer, + data.byteOffset + 2, + data.length - 2 + ); if (!this._skipUTF8Validation && !isValidUTF8(buf)) { return error( From 1b057f98709b2b3be31827ec8a89549fa012d52a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 13 Dec 2022 11:32:20 +0100 Subject: [PATCH 193/328] [minor] Fix nit --- lib/buffer-util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/buffer-util.js b/lib/buffer-util.js index e70feaaca..4a6e8e7cd 100644 --- a/lib/buffer-util.js +++ b/lib/buffer-util.js @@ -69,11 +69,11 @@ function _unmask(buffer, mask) { * @public */ function toArrayBuffer(buf) { - if (buf.byteLength === buf.buffer.byteLength) { + if (buf.length === buf.buffer.byteLength) { return buf.buffer; } - return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length); } /** From 83c72cfbe03ba9a5cc999776524f72f472c8ac29 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 13 Dec 2022 18:47:29 +0100 Subject: [PATCH 194/328] [perf] Make `toBuffer()` use `FastBuffer` Skip unnecessary parameter validation performed by `Buffer.from()` when possible. --- lib/buffer-util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/buffer-util.js b/lib/buffer-util.js index 4a6e8e7cd..f7536e28e 100644 --- a/lib/buffer-util.js +++ b/lib/buffer-util.js @@ -92,9 +92,9 @@ function toBuffer(data) { let buf; if (data instanceof ArrayBuffer) { - buf = Buffer.from(data); + buf = new FastBuffer(data); } else if (ArrayBuffer.isView(data)) { - buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); + buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength); } else { buf = Buffer.from(data); toBuffer.readOnly = false; From fb1dfd217861757d60c1d02c6e66b4da3630cc93 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 16 Dec 2022 10:30:38 +0100 Subject: [PATCH 195/328] [doc] Fix badge URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ae71f6d0..9fcd51af3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ws: a Node.js WebSocket library [![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws) -[![CI](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) +[![CI](https://img.shields.io/github/actions/workflow/status/websockets/ws/ci.yml?branch=master&label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) [![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws) ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and From 2dc2812942c8a70d55012887e99723795590d448 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 20 Dec 2022 20:07:00 +0100 Subject: [PATCH 196/328] [minor] Make `sendAfterClose()` call the callback in the next tick --- lib/websocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/websocket.js b/lib/websocket.js index 4391c73ab..de1b03b6a 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -1096,7 +1096,7 @@ function sendAfterClose(websocket, data, cb) { `WebSocket is not open: readyState ${websocket.readyState} ` + `(${readyStates[websocket.readyState]})` ); - cb(err); + process.nextTick(cb, err); } } From d412358ccb5320bc00c8993ecd5d9b992df0753e Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 20 Dec 2022 21:41:28 +0100 Subject: [PATCH 197/328] [minor] Fix nits --- lib/websocket.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index de1b03b6a..35a788ac4 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -282,7 +282,8 @@ class WebSocket extends EventEmitter { if (this.readyState === WebSocket.CLOSED) return; if (this.readyState === WebSocket.CONNECTING) { const msg = 'WebSocket was closed before the connection was established'; - return abortHandshake(this, this._req, msg); + abortHandshake(this, this._req, msg); + return; } if (this.readyState === WebSocket.CLOSING) { @@ -477,7 +478,8 @@ class WebSocket extends EventEmitter { if (this.readyState === WebSocket.CLOSED) return; if (this.readyState === WebSocket.CONNECTING) { const msg = 'WebSocket was closed before the connection was established'; - return abortHandshake(this, this._req, msg); + abortHandshake(this, this._req, msg); + return; } if (this._socket) { From ff63bba37fc0c052a609c7db88cc6d47bfc320e7 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 6 Jan 2023 21:31:34 +0100 Subject: [PATCH 198/328] [pkg] Update utf-8-validate to version 6.0.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 24ffdc55a..ee25d33c8 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -59,6 +59,6 @@ "mocha": "^8.4.0", "nyc": "^15.0.0", "prettier": "^2.0.5", - "utf-8-validate": "^5.0.2" + "utf-8-validate": "^6.0.0" } } From 42d79f60efb739b349b84b020c9d0ee062150633 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 7 Jan 2023 16:47:14 +0100 Subject: [PATCH 199/328] [minor] Use `buffer.isUtf8()` if possible Closes #2110 --- README.md | 11 ++++++++--- lib/validation.js | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9fcd51af3..4539df294 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,9 @@ npm install ws ### Opt-in for performance There are 2 optional modules that can be installed along side with the ws -module. These modules are binary addons which improve certain operations. -Prebuilt binaries are available for the most popular platforms so you don't -necessarily need to have a C++ compiler installed on your machine. +module. These modules are binary addons that improve the performance of certain +operations. Prebuilt binaries are available for the most popular platforms so +you don't necessarily need to have a C++ compiler installed on your machine. - `npm install --save-optional bufferutil`: Allows to efficiently perform operations such as masking and unmasking the data payload of the WebSocket @@ -75,6 +75,10 @@ variables. These might be useful to enhance security in systems where a user can put a package in the package search path of an application of another user, due to how the Node.js resolver algorithm works. +The `utf-8-validate` module is not needed and is not required, even if it is +already installed, regardless of the value of the `WS_NO_UTF_8_VALIDATE` +environment variable, if [`buffer.isUtf8()`][] is available. + ## API docs See [`/doc/ws.md`](./doc/ws.md) for Node.js-like documentation of ws classes and @@ -482,6 +486,7 @@ We're using the GitHub [releases][changelog] for changelog entries. [MIT](LICENSE) +[`buffer.isutf8()`]: https://nodejs.org/api/buffer.html#bufferisutf8input [changelog]: https://github.com/websockets/ws/releases [client-report]: http://websockets.github.io/ws/autobahn/clients/ [https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent diff --git a/lib/validation.js b/lib/validation.js index 44fc20290..c352e6ea7 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -1,5 +1,7 @@ 'use strict'; +const { isUtf8 } = require('buffer'); + // // Allowed token characters: // @@ -111,13 +113,16 @@ module.exports = { tokenChars }; -/* istanbul ignore else */ -if (!process.env.WS_NO_UTF_8_VALIDATE) { +if (isUtf8) { + module.exports.isValidUTF8 = function (buf) { + return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf); + }; +} /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) { try { const isValidUTF8 = require('utf-8-validate'); module.exports.isValidUTF8 = function (buf) { - return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); + return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf); }; } catch (e) { // Continue regardless of the error. From a3214d31b63acee8e31065be9f5ce3dd89203055 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 7 Jan 2023 20:27:46 +0100 Subject: [PATCH 200/328] [dist] 8.12.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee25d33c8..a810d95fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.11.0", + "version": "8.12.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 2862c2fef1064940c1deb9c4aa961cb76c279feb Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 23 Jan 2023 21:32:38 +0100 Subject: [PATCH 201/328] [doc] Add error handlers to examples and code snippets Closes #2112 --- README.md | 36 +++++++++++++++++++++++++ examples/express-session-parse/index.js | 10 +++++++ examples/server-stats/index.js | 2 ++ examples/ssl.js | 4 +++ 4 files changed, 52 insertions(+) diff --git a/README.md b/README.md index 4539df294..a550ca1c7 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,8 @@ import WebSocket from 'ws'; const ws = new WebSocket('ws://www.host.com/path'); +ws.on('error', console.error); + ws.on('open', function open() { ws.send('something'); }); @@ -171,6 +173,8 @@ import WebSocket from 'ws'; const ws = new WebSocket('ws://www.host.com/path'); +ws.on('error', console.error); + ws.on('open', function open() { const array = new Float32Array(5); @@ -190,6 +194,8 @@ import { WebSocketServer } from 'ws'; const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { + ws.on('error', console.error); + ws.on('message', function message(data) { console.log('received: %s', data); }); @@ -212,6 +218,8 @@ const server = createServer({ const wss = new WebSocketServer({ server }); wss.on('connection', function connection(ws) { + ws.on('error', console.error); + ws.on('message', function message(data) { console.log('received: %s', data); }); @@ -234,10 +242,14 @@ const wss1 = new WebSocketServer({ noServer: true }); const wss2 = new WebSocketServer({ noServer: true }); wss1.on('connection', function connection(ws) { + ws.on('error', console.error); + // ... }); wss2.on('connection', function connection(ws) { + ws.on('error', console.error); + // ... }); @@ -266,16 +278,24 @@ server.listen(8080); import { createServer } from 'http'; import { WebSocketServer } from 'ws'; +function onSocketError(err) { + console.error(err); +} + const server = createServer(); const wss = new WebSocketServer({ noServer: true }); wss.on('connection', function connection(ws, request, client) { + ws.on('error', console.error); + ws.on('message', function message(data) { console.log(`Received message ${data} from user ${client}`); }); }); server.on('upgrade', function upgrade(request, socket, head) { + socket.on('error', onSocketError); + // This function is not defined on purpose. Implement it with your own logic. authenticate(request, function next(err, client) { if (err || !client) { @@ -284,6 +304,8 @@ server.on('upgrade', function upgrade(request, socket, head) { return; } + socket.removeListener('error', onSocketError); + wss.handleUpgrade(request, socket, head, function done(ws) { wss.emit('connection', ws, request, client); }); @@ -306,6 +328,8 @@ import WebSocket, { WebSocketServer } from 'ws'; const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { + ws.on('error', console.error); + ws.on('message', function message(data, isBinary) { wss.clients.forEach(function each(client) { if (client.readyState === WebSocket.OPEN) { @@ -325,6 +349,8 @@ import WebSocket, { WebSocketServer } from 'ws'; const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { + ws.on('error', console.error); + ws.on('message', function message(data, isBinary) { wss.clients.forEach(function each(client) { if (client !== ws && client.readyState === WebSocket.OPEN) { @@ -342,6 +368,8 @@ import WebSocket from 'ws'; const ws = new WebSocket('wss://websocket-echo.com/'); +ws.on('error', console.error); + ws.on('open', function open() { console.log('connected'); ws.send(Date.now()); @@ -369,6 +397,8 @@ const ws = new WebSocket('wss://websocket-echo.com/'); const duplex = createWebSocketStream(ws, { encoding: 'utf8' }); +duplex.on('error', console.error); + duplex.pipe(process.stdout); process.stdin.pipe(duplex); ``` @@ -393,6 +423,8 @@ const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws, req) { const ip = req.socket.remoteAddress; + + ws.on('error', console.error); }); ``` @@ -402,6 +434,8 @@ the `X-Forwarded-For` header. ```js wss.on('connection', function connection(ws, req) { const ip = req.headers['x-forwarded-for'].split(',')[0].trim(); + + ws.on('error', console.error); }); ``` @@ -425,6 +459,7 @@ const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { ws.isAlive = true; + ws.on('error', console.error); ws.on('pong', heartbeat); }); @@ -466,6 +501,7 @@ function heartbeat() { const client = new WebSocket('wss://websocket-echo.com/'); +client.on('error', console.error); client.on('open', heartbeat); client.on('ping', heartbeat); client.on('close', function clear() { diff --git a/examples/express-session-parse/index.js b/examples/express-session-parse/index.js index b62a2e4a5..e0f214406 100644 --- a/examples/express-session-parse/index.js +++ b/examples/express-session-parse/index.js @@ -7,6 +7,10 @@ const uuid = require('uuid'); const { WebSocketServer } = require('../..'); +function onSocketError(err) { + console.error(err); +} + const app = express(); const map = new Map(); @@ -59,6 +63,8 @@ const server = http.createServer(app); const wss = new WebSocketServer({ clientTracking: false, noServer: true }); server.on('upgrade', function (request, socket, head) { + socket.on('error', onSocketError); + console.log('Parsing session from request...'); sessionParser(request, {}, () => { @@ -70,6 +76,8 @@ server.on('upgrade', function (request, socket, head) { console.log('Session is parsed!'); + socket.removeListener('error', onSocketError); + wss.handleUpgrade(request, socket, head, function (ws) { wss.emit('connection', ws, request); }); @@ -81,6 +89,8 @@ wss.on('connection', function (ws, request) { map.set(userId, ws); + ws.on('error', console.error); + ws.on('message', function (message) { // // Here we can now use session parameters. diff --git a/examples/server-stats/index.js b/examples/server-stats/index.js index e8754b5b2..afab8363f 100644 --- a/examples/server-stats/index.js +++ b/examples/server-stats/index.js @@ -22,6 +22,8 @@ wss.on('connection', function (ws) { }, 100); console.log('started client interval'); + ws.on('error', console.error); + ws.on('close', function () { console.log('stopping client interval'); clearInterval(id); diff --git a/examples/ssl.js b/examples/ssl.js index a5e750b79..83fb5f280 100644 --- a/examples/ssl.js +++ b/examples/ssl.js @@ -13,6 +13,8 @@ const server = https.createServer({ const wss = new WebSocketServer({ server }); wss.on('connection', function connection(ws) { + ws.on('error', console.error); + ws.on('message', function message(msg) { console.log(msg.toString()); }); @@ -31,6 +33,8 @@ server.listen(function listening() { rejectUnauthorized: false }); + ws.on('error', console.error); + ws.on('open', function open() { ws.send('All glory to WebSockets!'); }); From 0d114ef48d8baca790733dd2bce23938dd08cb10 Mon Sep 17 00:00:00 2001 From: Kalin Kostov Date: Mon, 13 Feb 2023 22:12:13 +0200 Subject: [PATCH 202/328] [pkg] Add browser condition (#2118) Fixes #2117 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a810d95fc..5f9a3de32 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "main": "index.js", "exports": { ".": { + "browser": "./browser.js", "import": "./wrapper.mjs", "require": "./index.js" }, From a04578e36611998d089fbb7c6057d1363a5d5754 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 13 Feb 2023 21:14:47 +0100 Subject: [PATCH 203/328] [dist] 8.12.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f9a3de32..df8c648a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.12.0", + "version": "8.12.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 41dc56a4ba504243a6efd0eb614510320e32d4cf Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 14 Feb 2023 21:01:42 +0100 Subject: [PATCH 204/328] [doc] Remove misleading information --- doc/ws.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index dd51eca9c..ae7993e68 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -384,13 +384,13 @@ Emitted when the connection is established. - `data` {Buffer} -Emitted when a ping is received from the server. +Emitted when a ping is received. ### Event: 'pong' - `data` {Buffer} -Emitted when a pong is received from the server. +Emitted when a pong is received. ### Event: 'redirect' @@ -495,8 +495,8 @@ An event listener to be called when an error occurs. The listener receives an - {Function} -An event listener to be called when a message is received from the server. The -listener receives a `MessageEvent` named "message". +An event listener to be called when a message is received. The listener receives +a `MessageEvent` named "message". ### websocket.onopen From b4b9d5a76e8c105fdeec64232fb6f12b6f88416d Mon Sep 17 00:00:00 2001 From: Matthijs van Duin Date: Thu, 9 Mar 2023 21:24:34 +0100 Subject: [PATCH 205/328] [test] Fix failing test when using the domain module (#2126) Fix a failure in `test/create-websocket-stream.test.js` if the domain module is loaded (e.g. due to `NODE_OPTIONS` in environment). The cause of the failure was that installing an `'uncaughtException'` event handler on `process` causes the domain module to prepend its own handler for the same event, which confused the test. Fixes #2124 --- test/create-websocket-stream.test.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/create-websocket-stream.test.js b/test/create-websocket-stream.test.js index 4d51958cd..572f5c4f2 100644 --- a/test/create-websocket-stream.test.js +++ b/test/create-websocket-stream.test.js @@ -295,11 +295,14 @@ describe('createWebSocketStream', () => { ws._socket.write(Buffer.from([0x85, 0x00])); }); - assert.strictEqual(process.listenerCount('uncaughtException'), 1); + assert.strictEqual( + process.listenerCount('uncaughtException'), + EventEmitter.usingDomains ? 2 : 1 + ); - const [listener] = process.listeners('uncaughtException'); + const listener = process.listeners('uncaughtException').pop(); - process.removeAllListeners('uncaughtException'); + process.removeListener('uncaughtException', listener); process.once('uncaughtException', (err) => { assert.ok(err instanceof Error); assert.strictEqual( From cd89e077f68ba9a999d408cb4fdb3e91289096a7 Mon Sep 17 00:00:00 2001 From: Matthijs van Duin Date: Fri, 10 Mar 2023 15:16:35 +0100 Subject: [PATCH 206/328] [feature] Add option to support late addition of headers (#2123) This supports the use-case where headers need to be added that depend on the socket connection (e.g. for TLS channel binding). --- doc/ws.md | 16 +++++++++++++++- lib/websocket.js | 6 +++++- test/websocket.test.js | 29 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index ae7993e68..0fc44d6e6 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -293,6 +293,8 @@ This class represents a WebSocket. It extends the `EventEmitter`. - `address` {String|url.URL} The URL to which to connect. - `protocols` {String|Array} The list of subprotocols. - `options` {Object} + - `finishRequest` {Function} A function which can be used to customize the + headers of each http request before it is sent. See description below. - `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to `false`. - `generateMask` {Function} The function used to generate the masking key. It @@ -316,12 +318,24 @@ This class represents a WebSocket. It extends the `EventEmitter`. Options given do not have any effect if parsed from the URL given with the `address` parameter. +Create a new WebSocket instance. + `perMessageDeflate` default value is `true`. When using an object, parameters are the same of the server. The only difference is the direction of requests. For example, `serverNoContextTakeover` can be used to ask the server to disable context takeover. -Create a new WebSocket instance. +`finishRequest` is called with arguments + +- `request` {http.ClientRequest} +- `websocket` {WebSocket} + +for each HTTP GET request (the initial one and any caused by redirects) when it +is ready to be sent, to allow for last minute customization of the headers. If +`finishRequest` is set then it has the responsibility to call `request.end()` +once it is done setting request headers. This is intended for niche use-cases +where some headers can't be provided in advance e.g. because they depend on the +underlying socket. #### IPC connections diff --git a/lib/websocket.js b/lib/websocket.js index 35a788ac4..b2b2b0926 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -989,7 +989,11 @@ function initAsClient(websocket, address, protocols, options) { }); }); - req.end(); + if (opts.finishRequest) { + opts.finishRequest(req, websocket); + } else { + req.end(); + } } /** diff --git a/test/websocket.test.js b/test/websocket.test.js index 6b2f3ef5c..f80acd3d5 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -3857,6 +3857,35 @@ describe('WebSocket', () => { agent }); }); + + it('honors the `finishRequest` option', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + finishRequest(request, websocket) { + process.nextTick(() => { + assert.strictEqual(request, ws._req); + assert.strictEqual(websocket, ws); + }); + request.on('socket', (socket) => { + socket.on('connect', () => { + request.setHeader('Cookie', 'foo=bar'); + request.end(); + }); + }); + } + }); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + wss.close(done); + }); + }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.cookie, 'foo=bar'); + ws.close(); + }); + }); }); describe('permessage-deflate', () => { From 23acf8cfaff73fadf89c69be669b3baa29b60233 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 10 Mar 2023 16:00:24 +0100 Subject: [PATCH 207/328] [test] Fix nits --- test/websocket.test.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index f80acd3d5..cb5b434c0 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -3860,16 +3860,18 @@ describe('WebSocket', () => { it('honors the `finishRequest` option', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { - const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { - finishRequest(request, websocket) { - process.nextTick(() => { - assert.strictEqual(request, ws._req); - assert.strictEqual(websocket, ws); - }); - request.on('socket', (socket) => { + const host = `localhost:${wss.address().port}`; + const ws = new WebSocket(`ws://${host}`, { + finishRequest(req, ws) { + assert.ok(req instanceof http.ClientRequest); + assert.strictEqual(req.getHeader('host'), host); + assert.ok(ws instanceof WebSocket); + assert.strictEqual(req, ws._req); + + req.on('socket', (socket) => { socket.on('connect', () => { - request.setHeader('Cookie', 'foo=bar'); - request.end(); + req.setHeader('Cookie', 'foo=bar'); + req.end(); }); }); } From 45e17acea791d865df6b255a55182e9c42e5877a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 10 Mar 2023 18:47:00 +0100 Subject: [PATCH 208/328] [pkg] 8.13.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df8c648a2..4b5d92bdc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.12.1", + "version": "8.13.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 5bdc8803f2a2887b7dc81d0ad82aedb0a7ef0ea1 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 19 Apr 2023 21:24:12 +0200 Subject: [PATCH 209/328] [ci] Do not test on node 19 --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aee196f92..f21a98e9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,6 @@ jobs: - 14 - 16 - 18 - - 19 os: - macOS-latest - ubuntu-latest From d1bb536cbc35a9a1af15486d973ae05f1ff2f4b5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 19 Apr 2023 21:25:01 +0200 Subject: [PATCH 210/328] [ci] Test on node 20 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f21a98e9d..69e74f00d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ jobs: - 14 - 16 - 18 + - 20 os: - macOS-latest - ubuntu-latest From 06728e444d8f54aa5602b51360f4f98794cb1754 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 25 Apr 2023 20:29:28 +0200 Subject: [PATCH 211/328] [ci] Update coverallsapp/github-action action to v2 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69e74f00d..23ae22b21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: echo "job_id=$id" >> $GITHUB_OUTPUT id: get_job_id shell: bash - - uses: coverallsapp/github-action@1.1.3 + - uses: coverallsapp/github-action@v2 with: flag-name: ${{ steps.get_job_id.outputs.job_id }} (Node.js ${{ matrix.node }} @@ -62,7 +62,7 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: coverallsapp/github-action@1.1.3 + - uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true From 0368beb23755462ab1a64dab7d8b9e28502f17f9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 11 Jul 2023 14:05:48 +0200 Subject: [PATCH 212/328] [pkg] Update prettier to version 3.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4b5d92bdc..e7808e2ea 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "eslint-plugin-prettier": "^4.0.0", "mocha": "^8.4.0", "nyc": "^15.0.0", - "prettier": "^2.0.5", + "prettier": "^3.0.0", "utf-8-validate": "^6.0.0" } } From 12a0a9c65d095cf565086706dd676cd7c6976d01 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 11 Jul 2023 14:06:34 +0200 Subject: [PATCH 213/328] [pkg] Update eslint-plugin-prettier to version 5.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e7808e2ea..a42932fbf 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "bufferutil": "^4.0.1", "eslint": "^8.0.0", "eslint-config-prettier": "^8.1.0", - "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-prettier": "^5.0.0", "mocha": "^8.4.0", "nyc": "^15.0.0", "prettier": "^3.0.0", From 0b235e0f9b650b1bdcbdb974cbeaaaa6a0797855 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 11 Jul 2023 14:09:24 +0200 Subject: [PATCH 214/328] [ci] Run the lint step on node 20 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23ae22b21..c8209e348 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: - run: npm install - run: npm run lint if: - matrix.os == 'ubuntu-latest' && matrix.node == 16 && matrix.arch == + matrix.os == 'ubuntu-latest' && matrix.node == 20 && matrix.arch == 'x64' - run: npm test - run: | From 8f5cc9df0e9e930a021142f0dbd4d1a4878bf350 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 19 Aug 2023 10:30:15 +0200 Subject: [PATCH 215/328] [pkg] Update eslint-config-prettier to version 9.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a42932fbf..2f076d2f3 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "benchmark": "^2.1.4", "bufferutil": "^4.0.1", "eslint": "^8.0.0", - "eslint-config-prettier": "^8.1.0", + "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "mocha": "^8.4.0", "nyc": "^15.0.0", From 5299b0ee6cfbdc50991a2f78f3600c36df1d3f4d Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 20 Aug 2023 10:54:08 +0200 Subject: [PATCH 216/328] [test] Remove redundant tests --- test/websocket-server.test.js | 6 ++++-- test/websocket.test.js | 32 -------------------------------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index abed1650a..176c29dbd 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -1213,7 +1213,9 @@ describe('WebSocketServer', () => { it("emits the 'headers' event", (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { - const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + const ws = new WebSocket( + `ws://localhost:${wss.address().port}?foo=bar` + ); ws.on('open', ws.close); }); @@ -1225,7 +1227,7 @@ describe('WebSocketServer', () => { 'Connection: Upgrade' ]); assert.ok(request instanceof http.IncomingMessage); - assert.strictEqual(request.url, '/'); + assert.strictEqual(request.url, '/?foo=bar'); wss.on('connection', () => wss.close(done)); }); diff --git a/test/websocket.test.js b/test/websocket.test.js index cb5b434c0..fd68ba726 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1901,38 +1901,6 @@ describe('WebSocket', () => { }); }); - describe('Connection with query string', () => { - it('connects when pathname is not null', (done) => { - const wss = new WebSocket.Server({ port: 0 }, () => { - const port = wss.address().port; - const ws = new WebSocket(`ws://localhost:${port}/?token=qwerty`); - - ws.on('open', () => { - wss.close(done); - }); - }); - - wss.on('connection', (ws) => { - ws.close(); - }); - }); - - it('connects when pathname is null', (done) => { - const wss = new WebSocket.Server({ port: 0 }, () => { - const port = wss.address().port; - const ws = new WebSocket(`ws://localhost:${port}?token=qwerty`); - - ws.on('open', () => { - wss.close(done); - }); - }); - - wss.on('connection', (ws) => { - ws.close(); - }); - }); - }); - describe('#pause', () => { it('does nothing if `readyState` is `CONNECTING` or `CLOSED`', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { From 8eb2c4754a9418a2dac56a5330322cc1d9721508 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 20 Aug 2023 11:12:01 +0200 Subject: [PATCH 217/328] [test] Fix nits --- test/websocket.test.js | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index fd68ba726..7b3978428 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -61,7 +61,7 @@ describe('WebSocket', () => { }); it('accepts `url.URL` objects as url', (done) => { - const agent = new CustomAgent(); + const agent = new http.Agent(); agent.addRequest = (req, opts) => { assert.strictEqual(opts.host, '::1'); @@ -74,7 +74,7 @@ describe('WebSocket', () => { describe('options', () => { it('accepts the `options` object as 3rd argument', () => { - const agent = new CustomAgent(); + const agent = new http.Agent(); let count = 0; let ws; @@ -122,10 +122,8 @@ describe('WebSocket', () => { }); it('throws an error when using an invalid `protocolVersion`', () => { - const options = { agent: new CustomAgent(), protocolVersion: 1000 }; - assert.throws( - () => new WebSocket('ws://localhost', options), + () => new WebSocket('ws://localhost', { protocolVersion: 1000 }), /^RangeError: Unsupported protocol version: 1000 \(supported versions: 8, 13\)$/ ); }); @@ -3709,7 +3707,7 @@ describe('WebSocket', () => { describe('Request headers', () => { it('adds the authorization header if the url has userinfo', (done) => { - const agent = new CustomAgent(); + const agent = new http.Agent(); const userinfo = 'test:testpass'; agent.addRequest = (req) => { @@ -3724,7 +3722,7 @@ describe('WebSocket', () => { }); it('honors the `auth` option', (done) => { - const agent = new CustomAgent(); + const agent = new http.Agent(); const auth = 'user:pass'; agent.addRequest = (req) => { @@ -3739,7 +3737,7 @@ describe('WebSocket', () => { }); it('favors the url userinfo over the `auth` option', (done) => { - const agent = new CustomAgent(); + const agent = new http.Agent(); const auth = 'foo:bar'; const userinfo = 'baz:qux'; @@ -3755,7 +3753,7 @@ describe('WebSocket', () => { }); it('adds custom headers', (done) => { - const agent = new CustomAgent(); + const agent = new http.Agent(); agent.addRequest = (req) => { assert.strictEqual(req.getHeader('cookie'), 'foo=bar'); @@ -3784,7 +3782,7 @@ describe('WebSocket', () => { }); it("doesn't add the origin header by default", (done) => { - const agent = new CustomAgent(); + const agent = new http.Agent(); agent.addRequest = (req) => { assert.strictEqual(req.getHeader('origin'), undefined); @@ -3795,7 +3793,7 @@ describe('WebSocket', () => { }); it('honors the `origin` option (1/2)', (done) => { - const agent = new CustomAgent(); + const agent = new http.Agent(); agent.addRequest = (req) => { assert.strictEqual(req.getHeader('origin'), 'https://example.com:8000'); @@ -3809,7 +3807,7 @@ describe('WebSocket', () => { }); it('honors the `origin` option (2/2)', (done) => { - const agent = new CustomAgent(); + const agent = new http.Agent(); agent.addRequest = (req) => { assert.strictEqual( @@ -3860,7 +3858,7 @@ describe('WebSocket', () => { describe('permessage-deflate', () => { it('is enabled by default', (done) => { - const agent = new CustomAgent(); + const agent = new http.Agent(); agent.addRequest = (req) => { assert.strictEqual( @@ -3874,7 +3872,7 @@ describe('WebSocket', () => { }); it('can be disabled', (done) => { - const agent = new CustomAgent(); + const agent = new http.Agent(); agent.addRequest = (req) => { assert.strictEqual( @@ -3891,7 +3889,7 @@ describe('WebSocket', () => { }); it('can send extension parameters', (done) => { - const agent = new CustomAgent(); + const agent = new http.Agent(); const value = 'permessage-deflate; server_no_context_takeover;' + From 67007fc8003a0a9822a559a6b0234227af382aee Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 21 Aug 2023 16:50:15 +0200 Subject: [PATCH 218/328] [test] Reduce message size from 20 MiB to 4 MiB --- test/websocket.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index 7b3978428..487b02acd 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2424,7 +2424,7 @@ describe('WebSocket', () => { it('can send a big binary message', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { - const array = new Float32Array(5 * 1024 * 1024); + const array = new Float32Array(1024 * 1024); for (let i = 0; i < array.length; i++) { array[i] = i / 5; From 79dab96227f1df55c93fc99569fc9d0b33240483 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 28 Aug 2023 15:05:11 +0200 Subject: [PATCH 219/328] [fix] Emit at most one event per microtask (#2160) To improve compatibility with the WHATWG standard, emit at most one of `'message'`, `'ping'`, and `'pong'` events per tick. Fixes #2159 --- lib/receiver.js | 29 +++++++++++-- test/receiver.test.js | 92 ++++++++++++++++++++++++++++++++++++++++++ test/websocket.test.js | 23 ++++++----- 3 files changed, 130 insertions(+), 14 deletions(-) diff --git a/lib/receiver.js b/lib/receiver.js index 96f572cb1..b5e9a8bca 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -13,12 +13,15 @@ const { concat, toArrayBuffer, unmask } = require('./buffer-util'); const { isValidStatusCode, isValidUTF8 } = require('./validation'); const FastBuffer = Buffer[Symbol.species]; +const promise = Promise.resolve(); + const GET_INFO = 0; const GET_PAYLOAD_LENGTH_16 = 1; const GET_PAYLOAD_LENGTH_64 = 2; const GET_MASK = 3; const GET_DATA = 4; const INFLATING = 5; +const WAIT_MICROTASK = 6; /** * HyBi Receiver implementation. @@ -157,9 +160,23 @@ class Receiver extends Writable { case GET_DATA: err = this.getData(cb); break; + case INFLATING: + this._loop = false; + return; default: - // `INFLATING` + // + // `WAIT_MICROTASK`. + // this._loop = false; + + // + // `queueMicrotask()` is not available in Node.js < 11 and is no + // better anyway. + // + promise.then(() => { + this._state = GET_INFO; + this.startLoop(cb); + }); return; } } while (this._loop); @@ -542,7 +559,7 @@ class Receiver extends Writable { } } - this._state = GET_INFO; + this._state = WAIT_MICROTASK; } /** @@ -559,6 +576,8 @@ class Receiver extends Writable { if (data.length === 0) { this.emit('conclude', 1005, EMPTY_BUFFER); this.end(); + + this._state = GET_INFO; } else { const code = data.readUInt16BE(0); @@ -590,14 +609,16 @@ class Receiver extends Writable { this.emit('conclude', code, buf); this.end(); + + this._state = GET_INFO; } } else if (this._opcode === 0x09) { this.emit('ping', data); + this._state = WAIT_MICROTASK; } else { this.emit('pong', data); + this._state = WAIT_MICROTASK; } - - this._state = GET_INFO; } } diff --git a/test/receiver.test.js b/test/receiver.test.js index 4ae279469..a4e1bb5ad 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -1083,4 +1083,96 @@ describe('Receiver', () => { receiver.write(Buffer.from([0x88, 0x03, 0x03, 0xe8, 0xf8])); }); + + it("waits a microtask after each 'message' event", (done) => { + const messages = []; + const receiver = new Receiver(); + + receiver.on('message', (data, isBinary) => { + assert.ok(!isBinary); + + const message = data.toString(); + messages.push(message); + + // `queueMicrotask()` is not available in Node.js < 11. + Promise.resolve().then(() => { + messages.push(`microtask ${message}`); + + if (messages.length === 6) { + assert.deepStrictEqual(messages, [ + '1', + 'microtask 1', + '2', + 'microtask 2', + '3', + 'microtask 3' + ]); + + done(); + } + }); + }); + + receiver.write(Buffer.from('810131810132810133', 'hex')); + }); + + it("waits a microtask after each 'ping' event", (done) => { + const actual = []; + const receiver = new Receiver(); + + receiver.on('ping', (data) => { + const message = data.toString(); + actual.push(message); + + // `queueMicrotask()` is not available in Node.js < 11. + Promise.resolve().then(() => { + actual.push(`microtask ${message}`); + + if (actual.length === 6) { + assert.deepStrictEqual(actual, [ + '1', + 'microtask 1', + '2', + 'microtask 2', + '3', + 'microtask 3' + ]); + + done(); + } + }); + }); + + receiver.write(Buffer.from('890131890132890133', 'hex')); + }); + + it("waits a microtask after each 'pong' event", (done) => { + const actual = []; + const receiver = new Receiver(); + + receiver.on('pong', (data) => { + const message = data.toString(); + actual.push(message); + + // `queueMicrotask()` is not available in Node.js < 11. + Promise.resolve().then(() => { + actual.push(`microtask ${message}`); + + if (actual.length === 6) { + assert.deepStrictEqual(actual, [ + '1', + 'microtask 1', + '2', + 'microtask 2', + '3', + 'microtask 3' + ]); + + done(); + } + }); + }); + + receiver.write(Buffer.from('8A01318A01328A0133', 'hex')); + }); }); diff --git a/test/websocket.test.js b/test/websocket.test.js index 487b02acd..4d45fad49 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -4075,18 +4075,18 @@ describe('WebSocket', () => { const messages = []; const ws = new WebSocket(`ws://localhost:${wss.address().port}`); - ws.on('open', () => { - ws._socket.on('end', () => { - assert.strictEqual(ws._receiver._state, 5); - }); - }); - ws.on('message', (message, isBinary) => { assert.ok(!isBinary); if (messages.push(message.toString()) > 1) return; - ws.close(1000); + // `queueMicrotask()` is not available in Node.js < 11. + Promise.resolve().then(() => { + process.nextTick(() => { + assert.strictEqual(ws._receiver._state, 5); + ws.close(1000); + }); + }); }); ws.on('close', (code, reason) => { @@ -4331,9 +4331,12 @@ describe('WebSocket', () => { if (messages.push(message.toString()) > 1) return; - process.nextTick(() => { - assert.strictEqual(ws._receiver._state, 5); - ws.terminate(); + // `queueMicrotask()` is not available in Node.js < 11. + Promise.resolve().then(() => { + process.nextTick(() => { + assert.strictEqual(ws._receiver._state, 5); + ws.terminate(); + }); }); }); From 347aab6cd1609797295f482ef4368e7ffbf4c53a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 28 Aug 2023 15:05:57 +0200 Subject: [PATCH 220/328] [feature] Allow http and https schemes (#2162) Allow HTTP(S) URLs to be used in the WebSocket constructor. They are immediately converted to the ws and wss schemes. Refs: https://github.com/whatwg/websockets/pull/45 --- lib/websocket.js | 12 ++++++--- test/websocket.test.js | 57 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index b2b2b0926..f71d3d8e7 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -667,24 +667,30 @@ function initAsClient(websocket, address, protocols, options) { if (address instanceof URL) { parsedUrl = address; - websocket._url = address.href; } else { try { parsedUrl = new URL(address); } catch (e) { throw new SyntaxError(`Invalid URL: ${address}`); } + } - websocket._url = address; + if (parsedUrl.protocol === 'http:') { + parsedUrl.protocol = 'ws:'; + } else if (parsedUrl.protocol === 'https:') { + parsedUrl.protocol = 'wss:'; } + websocket._url = parsedUrl.href; + const isSecure = parsedUrl.protocol === 'wss:'; const isIpcUrl = parsedUrl.protocol === 'ws+unix:'; let invalidUrlMessage; if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { invalidUrlMessage = - 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; + 'The URL\'s protocol must be one of "ws:", "wss:", ' + + '"http:", "https", or "ws+unix:"'; } else if (isIpcUrl && !parsedUrl.pathname) { invalidUrlMessage = "The URL's pathname is empty"; } else if (parsedUrl.hash) { diff --git a/test/websocket.test.js b/test/websocket.test.js index 4d45fad49..24553a1fa 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -36,8 +36,16 @@ describe('WebSocket', () => { ); assert.throws( - () => new WebSocket('https://websocket-echo.com'), - /^SyntaxError: The URL's protocol must be one of "ws:", "wss:", or "ws\+unix:"$/ + () => new WebSocket('bad-scheme://websocket-echo.com'), + (err) => { + assert.strictEqual( + err.message, + 'The URL\'s protocol must be one of "ws:", "wss:", ' + + '"http:", "https", or "ws+unix:"' + ); + + return true; + } ); assert.throws( @@ -72,6 +80,30 @@ describe('WebSocket', () => { const ws = new WebSocket(new URL('ws://[::1]'), { agent }); }); + it('allows the http scheme', (done) => { + const agent = new CustomAgent(); + + agent.addRequest = (req, opts) => { + assert.strictEqual(opts.host, 'localhost'); + assert.strictEqual(opts.port, 80); + done(); + }; + + const ws = new WebSocket('http://localhost', { agent }); + }); + + it('allows the https scheme', (done) => { + const agent = new https.Agent(); + + agent.addRequest = (req, opts) => { + assert.strictEqual(opts.host, 'localhost'); + assert.strictEqual(opts.port, 443); + done(); + }; + + const ws = new WebSocket('https://localhost', { agent }); + }); + describe('options', () => { it('accepts the `options` object as 3rd argument', () => { const agent = new http.Agent(); @@ -539,10 +571,18 @@ describe('WebSocket', () => { }); it('exposes the server url', () => { - const url = 'ws://localhost'; - const ws = new WebSocket(url, { agent: new CustomAgent() }); + const schemes = new Map([ + ['ws', 'ws'], + ['wss', 'wss'], + ['http', 'ws'], + ['https', 'wss'] + ]); + + for (const [key, value] of schemes) { + const ws = new WebSocket(`${key}://localhost/`, { lookup() {} }); - assert.strictEqual(ws.url, url); + assert.strictEqual(ws.url, `${value}://localhost/`); + } }); }); }); @@ -1174,7 +1214,9 @@ describe('WebSocket', () => { it('emits an error if the redirect URL is invalid (2/2)', (done) => { server.once('upgrade', (req, socket) => { - socket.end('HTTP/1.1 302 Found\r\nLocation: http://localhost\r\n\r\n'); + socket.end( + 'HTTP/1.1 302 Found\r\nLocation: bad-scheme://localhost\r\n\r\n' + ); }); const ws = new WebSocket(`ws://localhost:${server.address().port}`, { @@ -1186,7 +1228,8 @@ describe('WebSocket', () => { assert.ok(err instanceof SyntaxError); assert.strictEqual( err.message, - 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"' + 'The URL\'s protocol must be one of "ws:", "wss:", ' + + '"http:", "https", or "ws+unix:"' ); assert.strictEqual(ws._redirects, 1); From 31da41728ff5484bbc10e6f9b5487198d396fcb7 Mon Sep 17 00:00:00 2001 From: Tim Perry <1526883+pimterry@users.noreply.github.com> Date: Thu, 31 Aug 2023 21:09:31 +0200 Subject: [PATCH 221/328] [fix] Make `server.handleUpgrade()` work with any duplex stream (#2165) --- lib/websocket.js | 9 +++++++-- package.json | 1 + test/websocket-server.test.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index f71d3d8e7..15f61acee 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -223,8 +223,13 @@ class WebSocket extends EventEmitter { receiver.on('ping', receiverOnPing); receiver.on('pong', receiverOnPong); - socket.setTimeout(0); - socket.setNoDelay(); + // These methods may not be available if `socket` is actually just a stream: + if (socket.setTimeout) { + socket.setTimeout(0); + } + if (socket.setNoDelay) { + socket.setNoDelay(); + } if (head.length > 0) socket.unshift(head); diff --git a/package.json b/package.json index 2f076d2f3..0cc387471 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "mocha": "^8.4.0", + "native-duplexpair": "^1.0.0", "nyc": "^15.0.0", "prettier": "^3.0.0", "utf-8-validate": "^6.0.0" diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 176c29dbd..33d7a65f8 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -10,6 +10,7 @@ const path = require('path'); const net = require('net'); const fs = require('fs'); const os = require('os'); +const DuplexPair = require('native-duplexpair'); const Sender = require('../lib/sender'); const WebSocket = require('..'); @@ -514,6 +515,35 @@ describe('WebSocketServer', () => { }); }); }); + + it('can complete a WebSocket upgrade over any duplex stream', (done) => { + const server = http.createServer(); + + server.listen(0, () => { + const wss = new WebSocket.Server({ noServer: true }); + + server.on('upgrade', (req, socket, head) => { + // Put a stream between the raw socket and our websocket processing: + const { socket1: stream1, socket2: stream2 } = new DuplexPair(); + socket.pipe(stream1); + stream1.pipe(socket); + + // Pass the other side of the stream as the socket to upgrade: + wss.handleUpgrade(req, stream2, head, (ws) => { + ws.send('hello'); + ws.close(); + }); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from('hello')); + assert.ok(!isBinary); + server.close(done); + }); + }); + }); }); describe('#completeUpgrade', () => { From 62521f26d7d7b349ec4e532db85a4b0d2de1296a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 31 Aug 2023 21:37:11 +0200 Subject: [PATCH 222/328] [minor] Fix nits --- lib/sender.js | 7 +++---- lib/websocket-server.js | 16 ++++++---------- lib/websocket.js | 27 ++++++++++++--------------- test/websocket-server.test.js | 11 ++++++++--- 4 files changed, 29 insertions(+), 32 deletions(-) diff --git a/lib/sender.js b/lib/sender.js index c84885362..1ed04b027 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -1,9 +1,8 @@ -/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls$" }] */ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex" }] */ 'use strict'; -const net = require('net'); -const tls = require('tls'); +const { Duplex } = require('stream'); const { randomFillSync } = require('crypto'); const PerMessageDeflate = require('./permessage-deflate'); @@ -21,7 +20,7 @@ class Sender { /** * Creates a Sender instance. * - * @param {(net.Socket|tls.Socket)} socket The connection socket + * @param {Duplex} socket The connection socket * @param {Object} [extensions] An object containing the negotiated extensions * @param {Function} [generateMask] The function used to generate the masking * key diff --git a/lib/websocket-server.js b/lib/websocket-server.js index bac30eb33..b0ed7bd2e 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -1,12 +1,10 @@ -/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$" }] */ 'use strict'; const EventEmitter = require('events'); const http = require('http'); -const https = require('https'); -const net = require('net'); -const tls = require('tls'); +const { Duplex } = require('stream'); const { createHash } = require('crypto'); const extension = require('./extension'); @@ -221,8 +219,7 @@ class WebSocketServer extends EventEmitter { * Handle a HTTP Upgrade request. * * @param {http.IncomingMessage} req The request object - * @param {(net.Socket|tls.Socket)} socket The network socket between the - * server and client + * @param {Duplex} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @public @@ -346,8 +343,7 @@ class WebSocketServer extends EventEmitter { * @param {String} key The value of the `Sec-WebSocket-Key` header * @param {Set} protocols The subprotocols * @param {http.IncomingMessage} req The request object - * @param {(net.Socket|tls.Socket)} socket The network socket between the - * server and client + * @param {Duplex} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @throws {Error} If called more than once with the same socket @@ -477,7 +473,7 @@ function socketOnError() { /** * Close the connection when preconditions are not fulfilled. * - * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request + * @param {Duplex} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} [message] The HTTP response body * @param {Object} [headers] Additional HTTP response headers @@ -518,7 +514,7 @@ function abortHandshake(socket, code, message, headers) { * * @param {WebSocketServer} server The WebSocket server * @param {http.IncomingMessage} req The request object - * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request + * @param {Duplex} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} message The HTTP response body * @private diff --git a/lib/websocket.js b/lib/websocket.js index 15f61acee..8685ff73a 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -1,4 +1,4 @@ -/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Readable$" }] */ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex|Readable$" }] */ 'use strict'; @@ -8,7 +8,7 @@ const http = require('http'); const net = require('net'); const tls = require('tls'); const { randomBytes, createHash } = require('crypto'); -const { Readable } = require('stream'); +const { Duplex, Readable } = require('stream'); const { URL } = require('url'); const PerMessageDeflate = require('./permessage-deflate'); @@ -189,8 +189,7 @@ class WebSocket extends EventEmitter { /** * Set up the socket and the internal resources. * - * @param {(net.Socket|tls.Socket)} socket The network socket between the - * server and client + * @param {Duplex} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Object} options Options object * @param {Function} [options.generateMask] The function used to generate the @@ -223,13 +222,11 @@ class WebSocket extends EventEmitter { receiver.on('ping', receiverOnPing); receiver.on('pong', receiverOnPong); - // These methods may not be available if `socket` is actually just a stream: - if (socket.setTimeout) { - socket.setTimeout(0); - } - if (socket.setNoDelay) { - socket.setNoDelay(); - } + // + // These methods may not be available if `socket` is just a `Duplex`. + // + if (socket.setTimeout) socket.setTimeout(0); + if (socket.setNoDelay) socket.setNoDelay(); if (head.length > 0) socket.unshift(head); @@ -1229,7 +1226,7 @@ function resume(stream) { } /** - * The listener of the `net.Socket` `'close'` event. + * The listener of the socket `'close'` event. * * @private */ @@ -1280,7 +1277,7 @@ function socketOnClose() { } /** - * The listener of the `net.Socket` `'data'` event. + * The listener of the socket `'data'` event. * * @param {Buffer} chunk A chunk of data * @private @@ -1292,7 +1289,7 @@ function socketOnData(chunk) { } /** - * The listener of the `net.Socket` `'end'` event. + * The listener of the socket `'end'` event. * * @private */ @@ -1305,7 +1302,7 @@ function socketOnEnd() { } /** - * The listener of the `net.Socket` `'error'` event. + * The listener of the socket `'error'` event. * * @private */ diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 33d7a65f8..b962edcb9 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -516,19 +516,24 @@ describe('WebSocketServer', () => { }); }); - it('can complete a WebSocket upgrade over any duplex stream', (done) => { + it('completes a WebSocket upgrade over any duplex stream', (done) => { const server = http.createServer(); server.listen(0, () => { const wss = new WebSocket.Server({ noServer: true }); server.on('upgrade', (req, socket, head) => { - // Put a stream between the raw socket and our websocket processing: + // + // Put a stream between the raw socket and our websocket processing. + // const { socket1: stream1, socket2: stream2 } = new DuplexPair(); + socket.pipe(stream1); stream1.pipe(socket); - // Pass the other side of the stream as the socket to upgrade: + // + // Pass the other side of the stream as the socket to upgrade. + // wss.handleUpgrade(req, stream2, head, (ws) => { ws.send('hello'); ws.close(); From 5b577fe6653f896859f936255d8e2b792a75c501 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 6 Sep 2023 15:06:15 +0200 Subject: [PATCH 223/328] [pkg] Remove native-duplexpair dev dependency It seems to be no longer maintained. --- package.json | 1 - test/duplex-pair.js | 73 +++++++++++++++++++++++++++++++++++ test/websocket-server.test.js | 10 ++--- 3 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 test/duplex-pair.js diff --git a/package.json b/package.json index 0cc387471..2f076d2f3 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "mocha": "^8.4.0", - "native-duplexpair": "^1.0.0", "nyc": "^15.0.0", "prettier": "^3.0.0", "utf-8-validate": "^6.0.0" diff --git a/test/duplex-pair.js b/test/duplex-pair.js new file mode 100644 index 000000000..92d5e778e --- /dev/null +++ b/test/duplex-pair.js @@ -0,0 +1,73 @@ +// +// This code was copied from +// https://github.com/nodejs/node/blob/c506660f3267/test/common/duplexpair.js +// +// Copyright Node.js contributors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +'use strict'; + +const assert = require('assert'); +const { Duplex } = require('stream'); + +const kCallback = Symbol('Callback'); +const kOtherSide = Symbol('Other'); + +class DuplexSocket extends Duplex { + constructor() { + super(); + this[kCallback] = null; + this[kOtherSide] = null; + } + + _read() { + const callback = this[kCallback]; + if (callback) { + this[kCallback] = null; + callback(); + } + } + + _write(chunk, encoding, callback) { + assert.notStrictEqual(this[kOtherSide], null); + assert.strictEqual(this[kOtherSide][kCallback], null); + if (chunk.length === 0) { + process.nextTick(callback); + } else { + this[kOtherSide].push(chunk); + this[kOtherSide][kCallback] = callback; + } + } + + _final(callback) { + this[kOtherSide].on('end', callback); + this[kOtherSide].push(null); + } +} + +function makeDuplexPair() { + const clientSide = new DuplexSocket(); + const serverSide = new DuplexSocket(); + clientSide[kOtherSide] = serverSide; + serverSide[kOtherSide] = clientSide; + return { clientSide, serverSide }; +} + +module.exports = makeDuplexPair; diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index b962edcb9..ace3cb650 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -10,8 +10,8 @@ const path = require('path'); const net = require('net'); const fs = require('fs'); const os = require('os'); -const DuplexPair = require('native-duplexpair'); +const makeDuplexPair = require('./duplex-pair'); const Sender = require('../lib/sender'); const WebSocket = require('..'); const { NOOP } = require('../lib/constants'); @@ -526,15 +526,15 @@ describe('WebSocketServer', () => { // // Put a stream between the raw socket and our websocket processing. // - const { socket1: stream1, socket2: stream2 } = new DuplexPair(); + const { clientSide, serverSide } = makeDuplexPair(); - socket.pipe(stream1); - stream1.pipe(socket); + socket.pipe(clientSide); + clientSide.pipe(socket); // // Pass the other side of the stream as the socket to upgrade. // - wss.handleUpgrade(req, stream2, head, (ws) => { + wss.handleUpgrade(req, serverSide, head, (ws) => { ws.send('hello'); ws.close(); }); From c1d26c372efb116e3339284f9b7d269b21790a8f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 6 Sep 2023 15:30:33 +0200 Subject: [PATCH 224/328] [test] Fix failing test --- test/websocket-server.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index ace3cb650..5b6937cee 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -281,11 +281,15 @@ describe('WebSocketServer', () => { it('cleans event handlers on precreated server', (done) => { const server = http.createServer(); + const listeningListenerCount = server.listenerCount('listening'); const wss = new WebSocket.Server({ server }); server.listen(0, () => { wss.close(() => { - assert.strictEqual(server.listenerCount('listening'), 0); + assert.strictEqual( + server.listenerCount('listening'), + listeningListenerCount + ); assert.strictEqual(server.listenerCount('upgrade'), 0); assert.strictEqual(server.listenerCount('error'), 0); From d30768405fc295f0365c4bad8b7e14a9ad54c64b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 6 Sep 2023 15:35:25 +0200 Subject: [PATCH 225/328] [dist] 8.14.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f076d2f3..b1b287efc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.13.0", + "version": "8.14.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From ddba690ab8c5da2da2fc9af3131d5e5629cbdbd4 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 6 Sep 2023 16:14:55 +0200 Subject: [PATCH 226/328] [doc] Fix the type of the `socket` argument --- doc/ws.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 0fc44d6e6..a0d9f88ad 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -249,8 +249,7 @@ receives an `Error` if the server is already closed. ### server.handleUpgrade(request, socket, head, callback) - `request` {http.IncomingMessage} The client HTTP GET request. -- `socket` {net.Socket|tls.Socket} The network socket between the server and - client. +- `socket` {stream.Duplex} The network socket between the server and client. - `head` {Buffer} The first packet of the upgraded stream. - `callback` {Function}. From 511aefece49ee38c6fcca19d230c115fbfeaefd8 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 6 Sep 2023 16:18:08 +0200 Subject: [PATCH 227/328] [pkg] Silence npm warning --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b1b287efc..ee94cb68c 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,10 @@ ], "homepage": "https://github.com/websockets/ws", "bugs": "https://github.com/websockets/ws/issues", - "repository": "websockets/ws", + "repository": { + "type": "git", + "url": "git+https://github.com/websockets/ws.git" + }, "author": "Einar Otto Stangvik (http://2x.io)", "license": "MIT", "main": "index.js", From ae60ce0d1eaa239844bc8d60d220b47e302c3d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 8 Sep 2023 16:12:32 +0200 Subject: [PATCH 228/328] [ci] Cache downloaded npm dependencies (#2166) --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8209e348..05cf9f59b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,8 @@ jobs: with: node-version: ${{ matrix.node }} architecture: ${{ matrix.arch }} + cache: 'npm' + cache-dependency-path: ./package.json - run: npm install - run: npm run lint if: From fd3c64cbd60606f75763350133ba2757b6a64545 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 8 Sep 2023 16:03:26 +0200 Subject: [PATCH 229/328] [test] Fix flaky tests on Windows --- test/websocket.test.js | 56 ++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index 24553a1fa..d4dd762d8 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2813,16 +2813,28 @@ describe('WebSocket', () => { }); it('can be called from an error listener while connecting', (done) => { - const ws = new WebSocket('ws://localhost:1337'); + const server = net.createServer(); - ws.on('open', () => done(new Error("Unexpected 'open' event"))); - ws.on('error', (err) => { - assert.ok(err instanceof Error); - assert.strictEqual(err.code, 'ECONNREFUSED'); - ws.close(); - ws.on('close', () => done()); + server.on('connection', (socket) => { + socket.on('end', socket.end); + socket.resume(); + socket.write(Buffer.from('foo\r\n')); }); - }).timeout(4000); + + server.listen(0, () => { + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.code, 'HPE_INVALID_CONSTANT'); + ws.close(); + ws.on('close', () => { + server.close(done); + }); + }); + }); + }); it("can be called from a listener of the 'redirect' event", (done) => { const server = http.createServer(); @@ -3087,16 +3099,28 @@ describe('WebSocket', () => { }); it('can be called from an error listener while connecting', (done) => { - const ws = new WebSocket('ws://localhost:1337'); + const server = net.createServer(); - ws.on('open', () => done(new Error("Unexpected 'open' event"))); - ws.on('error', (err) => { - assert.ok(err instanceof Error); - assert.strictEqual(err.code, 'ECONNREFUSED'); - ws.terminate(); - ws.on('close', () => done()); + server.on('connection', (socket) => { + socket.on('end', socket.end); + socket.resume(); + socket.write(Buffer.from('foo\r\n')); }); - }).timeout(4000); + + server.listen(0, function () { + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.code, 'HPE_INVALID_CONSTANT'); + ws.terminate(); + ws.on('close', () => { + server.close(done); + }); + }); + }); + }); it("can be called from a listener of the 'redirect' event", (done) => { const server = http.createServer(); From 397b89e3db6782022bbcf328b1191f5a1eb7800f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 8 Sep 2023 17:41:04 +0200 Subject: [PATCH 230/328] [ci] Update actions/checkout action to v4 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05cf9f59b..cf3901557 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: node: 18 os: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} From 7460049ff0a61bef8d5eda4b1d5c8170bc7d6b6f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 8 Sep 2023 17:52:39 +0200 Subject: [PATCH 231/328] [dist] 8.14.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee94cb68c..988d87565 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.14.0", + "version": "8.14.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 7f4e1a75afbcee162cff0d44000b4fda82008d05 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 19 Sep 2023 15:33:39 +0200 Subject: [PATCH 232/328] [fix] Add missing rejection handler Use `queueMicrotask()` when available and add a rejection handler to the shim for it. --- lib/receiver.js | 41 ++++++++++++++++++++++++++++++++++++----- test/receiver.test.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/lib/receiver.js b/lib/receiver.js index b5e9a8bca..1d425ead0 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -15,6 +15,12 @@ const { isValidStatusCode, isValidUTF8 } = require('./validation'); const FastBuffer = Buffer[Symbol.species]; const promise = Promise.resolve(); +// +// `queueMicrotask()` is not available in Node.js < 11. +// +const queueTask = + typeof queueMicrotask === 'function' ? queueMicrotask : queueMicrotaskShim; + const GET_INFO = 0; const GET_PAYLOAD_LENGTH_16 = 1; const GET_PAYLOAD_LENGTH_64 = 2; @@ -169,11 +175,7 @@ class Receiver extends Writable { // this._loop = false; - // - // `queueMicrotask()` is not available in Node.js < 11 and is no - // better anyway. - // - promise.then(() => { + queueTask(() => { this._state = GET_INFO; this.startLoop(cb); }); @@ -646,3 +648,32 @@ function error(ErrorCtor, message, prefix, statusCode, errorCode) { err[kStatusCode] = statusCode; return err; } + +/** + * A shim for `queueMicrotask()`. + * + * @param {Function} cb Callback + */ +function queueMicrotaskShim(cb) { + promise.then(cb).catch(throwErrorNextTick); +} + +/** + * Throws an error. + * + * @param {Error} err The error to throw + * @private + */ +function throwError(err) { + throw err; +} + +/** + * Throws an error in the next tick. + * + * @param {Error} err The error to throw + * @private + */ +function throwErrorNextTick(err) { + process.nextTick(throwError, err); +} diff --git a/test/receiver.test.js b/test/receiver.test.js index a4e1bb5ad..40e0565ad 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -2,6 +2,7 @@ const assert = require('assert'); const crypto = require('crypto'); +const EventEmitter = require('events'); const PerMessageDeflate = require('../lib/permessage-deflate'); const Receiver = require('../lib/receiver'); @@ -1175,4 +1176,33 @@ describe('Receiver', () => { receiver.write(Buffer.from('8A01318A01328A0133', 'hex')); }); + + it('does not swallow errors thrown from event handlers', (done) => { + const receiver = new Receiver(); + let count = 0; + + receiver.on('message', function () { + if (++count === 2) { + throw new Error('Oops'); + } + }); + + assert.strictEqual( + process.listenerCount('uncaughtException'), + EventEmitter.usingDomains ? 2 : 1 + ); + + const listener = process.listeners('uncaughtException').pop(); + + process.removeListener('uncaughtException', listener); + process.once('uncaughtException', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Oops'); + + process.on('uncaughtException', listener); + done(); + }); + + receiver.write(Buffer.from('82008200', 'hex')); + }); }); From d8dd4852b81982fc0a6d633673968dff90985000 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 19 Sep 2023 17:20:57 +0200 Subject: [PATCH 233/328] [dist] 8.14.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 988d87565..107c188d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.14.1", + "version": "8.14.2", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From a049674d936746c36fe928cc1baaaafd3029a83e Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 4 Nov 2023 20:49:31 +0100 Subject: [PATCH 234/328] [ci] Update actions/setup-node action to v4 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf3901557..7a6490630 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: os: windows-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} architecture: ${{ matrix.arch }} From 726abc3d1e96a51eebb8d1460303dc68d9d3d4b4 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 8 Dec 2023 17:43:13 +0100 Subject: [PATCH 235/328] [test] Fix flaky test --- test/websocket.test.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index d4dd762d8..ba1485478 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -626,15 +626,19 @@ describe('WebSocket', () => { }); }); - it('does not re-emit `net.Socket` errors', (done) => { - const codes = ['EPIPE', 'ECONNABORTED', 'ECANCELED', 'ECONNRESET']; + it('does not re-emit `net.Socket` errors', function (done) { + // + // `socket.resetAndDestroy()` is not available in Node.js < 16.17.0. + // + if (process.versions.modules < 93) return this.skip(); + const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); ws.on('open', () => { ws._socket.on('error', (err) => { assert.ok(err instanceof Error); - assert.ok(codes.includes(err.code), `Unexpected code: ${err.code}`); + assert.strictEqual(err.code, 'ECONNRESET'); ws.on('close', (code, message) => { assert.strictEqual(code, 1006); assert.strictEqual(message, EMPTY_BUFFER); @@ -642,9 +646,7 @@ describe('WebSocket', () => { }); }); - for (const client of wss.clients) client.terminate(); - ws.send('foo'); - ws.send('bar'); + wss.clients.values().next().value._socket.resetAndDestroy(); }); }); }); From 208220d018a3571b5bbac541b1e513d1027a6d66 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 8 Dec 2023 17:45:44 +0100 Subject: [PATCH 236/328] [lint] Fix prettier error --- lib/websocket.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 8685ff73a..60da37d4f 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -806,8 +806,8 @@ function initAsClient(websocket, address, protocols, options) { ? opts.socketPath === websocket._originalHostOrSocketPath : false : websocket._originalIpc - ? false - : parsedUrl.host === websocket._originalHostOrSocketPath; + ? false + : parsedUrl.host === websocket._originalHostOrSocketPath; if (!isSameHost || (websocket._originalSecure && !isSecure)) { // From dd1994df04670df521a3744af6e6ba435ede7cba Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 8 Dec 2023 21:13:55 +0100 Subject: [PATCH 237/328] [ci] Test on node 21 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a6490630..7ca1a776a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: - 16 - 18 - 20 + - 21 os: - macOS-latest - ubuntu-latest From 5a3036e3f502c07dc4fdd64e5d40b9280de139be Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 9 Dec 2023 08:38:10 +0100 Subject: [PATCH 238/328] [test] Merge some tests --- test/receiver.test.js | 93 +++++++++---------------------------------- 1 file changed, 19 insertions(+), 74 deletions(-) diff --git a/test/receiver.test.js b/test/receiver.test.js index 40e0565ad..8884703b4 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -1085,43 +1085,20 @@ describe('Receiver', () => { receiver.write(Buffer.from([0x88, 0x03, 0x03, 0xe8, 0xf8])); }); - it("waits a microtask after each 'message' event", (done) => { - const messages = []; - const receiver = new Receiver(); - - receiver.on('message', (data, isBinary) => { - assert.ok(!isBinary); - - const message = data.toString(); - messages.push(message); - - // `queueMicrotask()` is not available in Node.js < 11. - Promise.resolve().then(() => { - messages.push(`microtask ${message}`); - - if (messages.length === 6) { - assert.deepStrictEqual(messages, [ - '1', - 'microtask 1', - '2', - 'microtask 2', - '3', - 'microtask 3' - ]); - - done(); - } - }); - }); - - receiver.write(Buffer.from('810131810132810133', 'hex')); - }); - - it("waits a microtask after each 'ping' event", (done) => { + it("waits a microtask after the 'message', and 'p{i,o}ng' events", (done) => { const actual = []; - const receiver = new Receiver(); + const expected = [ + '1', + 'microtask 1', + '2', + 'microtask 2', + '3', + 'microtask 3', + '4', + 'microtask 4' + ]; - receiver.on('ping', (data) => { + function listener(data) { const message = data.toString(); actual.push(message); @@ -1129,52 +1106,20 @@ describe('Receiver', () => { Promise.resolve().then(() => { actual.push(`microtask ${message}`); - if (actual.length === 6) { - assert.deepStrictEqual(actual, [ - '1', - 'microtask 1', - '2', - 'microtask 2', - '3', - 'microtask 3' - ]); - + if (actual.length === 8) { + assert.deepStrictEqual(actual, expected); done(); } }); - }); - - receiver.write(Buffer.from('890131890132890133', 'hex')); - }); + } - it("waits a microtask after each 'pong' event", (done) => { - const actual = []; const receiver = new Receiver(); - receiver.on('pong', (data) => { - const message = data.toString(); - actual.push(message); - - // `queueMicrotask()` is not available in Node.js < 11. - Promise.resolve().then(() => { - actual.push(`microtask ${message}`); - - if (actual.length === 6) { - assert.deepStrictEqual(actual, [ - '1', - 'microtask 1', - '2', - 'microtask 2', - '3', - 'microtask 3' - ]); - - done(); - } - }); - }); + receiver.on('message', listener); + receiver.on('ping', listener); + receiver.on('pong', listener); - receiver.write(Buffer.from('8A01318A01328A0133', 'hex')); + receiver.write(Buffer.from('8101318901328a0133810134', 'hex')); }); it('does not swallow errors thrown from event handlers', (done) => { From c320738b1d2236900d2dd2fe391ab83bbed1e63f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 9 Dec 2023 11:00:40 +0100 Subject: [PATCH 239/328] [test] Fix nits --- test/receiver.test.js | 2 +- test/websocket.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/receiver.test.js b/test/receiver.test.js index 8884703b4..0f82cf3ea 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -1126,7 +1126,7 @@ describe('Receiver', () => { const receiver = new Receiver(); let count = 0; - receiver.on('message', function () { + receiver.on('message', () => { if (++count === 2) { throw new Error('Oops'); } diff --git a/test/websocket.test.js b/test/websocket.test.js index ba1485478..4699ae5cd 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -3109,7 +3109,7 @@ describe('WebSocket', () => { socket.write(Buffer.from('foo\r\n')); }); - server.listen(0, function () { + server.listen(0, () => { const ws = new WebSocket(`ws://localhost:${server.address().port}`); ws.on('open', () => done(new Error("Unexpected 'open' event"))); From 603a0391de32732df415778ed32f311a21c82731 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 9 Dec 2023 13:23:48 +0100 Subject: [PATCH 240/328] [doc] Add JSDoc for the `finishRequest` option --- lib/websocket.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/websocket.js b/lib/websocket.js index 60da37d4f..312f6a237 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -618,6 +618,8 @@ module.exports = WebSocket; * @param {(String|URL)} address The URL to which to connect * @param {Array} protocols The subprotocols * @param {Object} [options] Connection options + * @param {Function} [options.finishRequest] A function which can be used to + * customize the headers of each http request before it is sent * @param {Boolean} [options.followRedirects=false] Whether or not to follow * redirects * @param {Function} [options.generateMask] The function used to generate the From 93e3552e95ba5ad656c30b94f6be96afe22d4805 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 9 Dec 2023 14:21:18 +0100 Subject: [PATCH 241/328] [feature] Introduce the `allowMultipleEventsPerMicrotask` option The `allowMultipleEventsPerMicrotask` option allows the `'message'`, `'ping'`, and `'pong'` events to be emitted more than once per microtask. Refs: https://github.com/websockets/ws/pull/2160 --- doc/ws.md | 8 ++++++++ lib/receiver.js | 27 ++++++++++++++++----------- lib/websocket-server.js | 6 ++++++ lib/websocket.js | 9 +++++++++ test/receiver.test.js | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 11 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index a0d9f88ad..c39ac356c 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -72,6 +72,10 @@ This class represents a WebSocket server. It extends the `EventEmitter`. ### new WebSocketServer(options[, callback]) - `options` {Object} + - `allowMultipleEventsPerMicrotask` {Boolean} Specifies whether or not to + process more than one of the `'message'`, `'ping'`, and `'pong'` events per + microtask. To improve compatibility with the WHATWG standard, the default + value is `false`. Setting it to `true` improves performance slightly. - `backlog` {Number} The maximum length of the queue of pending connections. - `clientTracking` {Boolean} Specifies whether or not to track clients. - `handleProtocols` {Function} A function which can be used to handle the @@ -292,6 +296,10 @@ This class represents a WebSocket. It extends the `EventEmitter`. - `address` {String|url.URL} The URL to which to connect. - `protocols` {String|Array} The list of subprotocols. - `options` {Object} + - `allowMultipleEventsPerMicrotask` {Boolean} Specifies whether or not to + process more than one of the `'message'`, `'ping'`, and `'pong'` events per + microtask. To improve compatibility with the WHATWG standard, the default + value is `false`. Setting it to `true` improves performance slightly. - `finishRequest` {Function} A function which can be used to customize the headers of each http request before it is sent. See description below. - `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to diff --git a/lib/receiver.js b/lib/receiver.js index 1d425ead0..d0c68432d 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -39,6 +39,9 @@ class Receiver extends Writable { * Creates a Receiver instance. * * @param {Object} [options] Options object + * @param {Boolean} [options.allowMultipleEventsPerMicrotask=false] Specifies + * whether or not to process more than one of the `'message'`, `'ping'`, + * and `'pong'` events per microtask * @param {String} [options.binaryType=nodebuffer] The type for binary data * @param {Object} [options.extensions] An object containing the negotiated * extensions @@ -51,6 +54,8 @@ class Receiver extends Writable { constructor(options = {}) { super(); + this._allowMultipleEventsPerMicrotask = + !!options.allowMultipleEventsPerMicrotask; this._binaryType = options.binaryType || BINARY_TYPES[0]; this._extensions = options.extensions || {}; this._isServer = !!options.isServer; @@ -561,7 +566,9 @@ class Receiver extends Writable { } } - this._state = WAIT_MICROTASK; + this._state = this._allowMultipleEventsPerMicrotask + ? GET_INFO + : WAIT_MICROTASK; } /** @@ -578,8 +585,6 @@ class Receiver extends Writable { if (data.length === 0) { this.emit('conclude', 1005, EMPTY_BUFFER); this.end(); - - this._state = GET_INFO; } else { const code = data.readUInt16BE(0); @@ -611,16 +616,16 @@ class Receiver extends Writable { this.emit('conclude', code, buf); this.end(); - - this._state = GET_INFO; } - } else if (this._opcode === 0x09) { - this.emit('ping', data); - this._state = WAIT_MICROTASK; - } else { - this.emit('pong', data); - this._state = WAIT_MICROTASK; + + this._state = GET_INFO; + return; } + + this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); + this._state = this._allowMultipleEventsPerMicrotask + ? GET_INFO + : WAIT_MICROTASK; } } diff --git a/lib/websocket-server.js b/lib/websocket-server.js index b0ed7bd2e..78c0bb289 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -29,6 +29,9 @@ class WebSocketServer extends EventEmitter { * Create a `WebSocketServer` instance. * * @param {Object} options Configuration options + * @param {Boolean} [options.allowMultipleEventsPerMicrotask=false] Specifies + * whether or not to process more than one of the `'message'`, `'ping'`, + * and `'pong'` events per microtask * @param {Number} [options.backlog=511] The maximum length of the queue of * pending connections * @param {Boolean} [options.clientTracking=true] Specifies whether or not to @@ -55,6 +58,7 @@ class WebSocketServer extends EventEmitter { super(); options = { + allowMultipleEventsPerMicrotask: false, maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, perMessageDeflate: false, @@ -409,6 +413,8 @@ class WebSocketServer extends EventEmitter { socket.removeListener('error', socketOnError); ws.setSocket(socket, head, { + allowMultipleEventsPerMicrotask: + this.options.allowMultipleEventsPerMicrotask, maxPayload: this.options.maxPayload, skipUTF8Validation: this.options.skipUTF8Validation }); diff --git a/lib/websocket.js b/lib/websocket.js index 312f6a237..d2c6a36fe 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -192,6 +192,9 @@ class WebSocket extends EventEmitter { * @param {Duplex} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Object} options Options object + * @param {Boolean} [options.allowMultipleEventsPerMicrotask=false] Specifies + * whether or not to process more than one of the `'message'`, `'ping'`, + * and `'pong'` events per microtask * @param {Function} [options.generateMask] The function used to generate the * masking key * @param {Number} [options.maxPayload=0] The maximum allowed message size @@ -201,6 +204,7 @@ class WebSocket extends EventEmitter { */ setSocket(socket, head, options) { const receiver = new Receiver({ + allowMultipleEventsPerMicrotask: options.allowMultipleEventsPerMicrotask, binaryType: this.binaryType, extensions: this._extensions, isServer: this._isServer, @@ -618,6 +622,9 @@ module.exports = WebSocket; * @param {(String|URL)} address The URL to which to connect * @param {Array} protocols The subprotocols * @param {Object} [options] Connection options + * @param {Boolean} [options.allowMultipleEventsPerMicrotask=false] Specifies + * whether or not to process more than one of the `'message'`, `'ping'`, + * and `'pong'` events per microtask * @param {Function} [options.finishRequest] A function which can be used to * customize the headers of each http request before it is sent * @param {Boolean} [options.followRedirects=false] Whether or not to follow @@ -642,6 +649,7 @@ module.exports = WebSocket; */ function initAsClient(websocket, address, protocols, options) { const opts = { + allowMultipleEventsPerMicrotask: false, protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, @@ -993,6 +1001,7 @@ function initAsClient(websocket, address, protocols, options) { } websocket.setSocket(socket, head, { + allowMultipleEventsPerMicrotask: opts.allowMultipleEventsPerMicrotask, generateMask: opts.generateMask, maxPayload: opts.maxPayload, skipUTF8Validation: opts.skipUTF8Validation diff --git a/test/receiver.test.js b/test/receiver.test.js index 0f82cf3ea..4e3ee923d 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -1150,4 +1150,41 @@ describe('Receiver', () => { receiver.write(Buffer.from('82008200', 'hex')); }); + + it('honors the `allowMultipleEventsPerMicrotask` option', (done) => { + const actual = []; + const expected = [ + '1', + '2', + '3', + '4', + 'microtask 1', + 'microtask 2', + 'microtask 3', + 'microtask 4' + ]; + + function listener(data) { + const message = data.toString(); + actual.push(message); + + // `queueMicrotask()` is not available in Node.js < 11. + Promise.resolve().then(() => { + actual.push(`microtask ${message}`); + + if (actual.length === 8) { + assert.deepStrictEqual(actual, expected); + done(); + } + }); + } + + const receiver = new Receiver({ allowMultipleEventsPerMicrotask: true }); + + receiver.on('message', listener); + receiver.on('ping', listener); + receiver.on('pong', listener); + + receiver.write(Buffer.from('8101318901328a0133810134', 'hex')); + }); }); From 297fff8eded6328e4386fda735002b9c4d17b537 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 9 Dec 2023 15:36:51 +0100 Subject: [PATCH 242/328] [dist] 8.15.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 107c188d2..78679492b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.14.2", + "version": "8.15.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From fccc580061a4a35e5f286babafe7416768fd777b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 12 Dec 2023 08:29:34 +0100 Subject: [PATCH 243/328] [fix] Emit the event when the microtask is executed Emit the `'message'`, `'ping'`, and `'pong'` event when the microtask for that event is executed. --- lib/receiver.js | 337 +++++++++++++++++++++++++----------------- test/receiver.test.js | 2 +- 2 files changed, 199 insertions(+), 140 deletions(-) diff --git a/lib/receiver.js b/lib/receiver.js index d0c68432d..18bb9b54d 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -27,7 +27,7 @@ const GET_PAYLOAD_LENGTH_64 = 2; const GET_MASK = 3; const GET_DATA = 4; const INFLATING = 5; -const WAIT_MICROTASK = 6; +const DEFER_EVENT = 6; /** * HyBi Receiver implementation. @@ -78,8 +78,9 @@ class Receiver extends Writable { this._messageLength = 0; this._fragments = []; - this._state = GET_INFO; + this._errored = false; this._loop = false; + this._state = GET_INFO; } /** @@ -151,53 +152,42 @@ class Receiver extends Writable { * @private */ startLoop(cb) { - let err; this._loop = true; do { switch (this._state) { case GET_INFO: - err = this.getInfo(); + this.getInfo(cb); break; case GET_PAYLOAD_LENGTH_16: - err = this.getPayloadLength16(); + this.getPayloadLength16(cb); break; case GET_PAYLOAD_LENGTH_64: - err = this.getPayloadLength64(); + this.getPayloadLength64(cb); break; case GET_MASK: this.getMask(); break; case GET_DATA: - err = this.getData(cb); + this.getData(cb); break; case INFLATING: + case DEFER_EVENT: this._loop = false; return; - default: - // - // `WAIT_MICROTASK`. - // - this._loop = false; - - queueTask(() => { - this._state = GET_INFO; - this.startLoop(cb); - }); - return; } } while (this._loop); - cb(err); + if (!this._errored) cb(); } /** * Reads the first two bytes of a frame. * - * @return {(RangeError|undefined)} A possible error + * @param {Function} cb Callback * @private */ - getInfo() { + getInfo(cb) { if (this._bufferedBytes < 2) { this._loop = false; return; @@ -206,27 +196,31 @@ class Receiver extends Writable { const buf = this.consume(2); if ((buf[0] & 0x30) !== 0x00) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'RSV2 and RSV3 must be clear', true, 1002, 'WS_ERR_UNEXPECTED_RSV_2_3' ); + + cb(error); + return; } const compressed = (buf[0] & 0x40) === 0x40; if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'RSV1 must be clear', true, 1002, 'WS_ERR_UNEXPECTED_RSV_1' ); + + cb(error); + return; } this._fin = (buf[0] & 0x80) === 0x80; @@ -235,86 +229,100 @@ class Receiver extends Writable { if (this._opcode === 0x00) { if (compressed) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'RSV1 must be clear', true, 1002, 'WS_ERR_UNEXPECTED_RSV_1' ); + + cb(error); + return; } if (!this._fragmented) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'invalid opcode 0', true, 1002, 'WS_ERR_INVALID_OPCODE' ); + + cb(error); + return; } this._opcode = this._fragmented; } else if (this._opcode === 0x01 || this._opcode === 0x02) { if (this._fragmented) { - this._loop = false; - return error( + const error = this.createError( RangeError, `invalid opcode ${this._opcode}`, true, 1002, 'WS_ERR_INVALID_OPCODE' ); + + cb(error); + return; } this._compressed = compressed; } else if (this._opcode > 0x07 && this._opcode < 0x0b) { if (!this._fin) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'FIN must be set', true, 1002, 'WS_ERR_EXPECTED_FIN' ); + + cb(error); + return; } if (compressed) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'RSV1 must be clear', true, 1002, 'WS_ERR_UNEXPECTED_RSV_1' ); + + cb(error); + return; } if ( this._payloadLength > 0x7d || (this._opcode === 0x08 && this._payloadLength === 1) ) { - this._loop = false; - return error( + const error = this.createError( RangeError, `invalid payload length ${this._payloadLength}`, true, 1002, 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' ); + + cb(error); + return; } } else { - this._loop = false; - return error( + const error = this.createError( RangeError, `invalid opcode ${this._opcode}`, true, 1002, 'WS_ERR_INVALID_OPCODE' ); + + cb(error); + return; } if (!this._fin && !this._fragmented) this._fragmented = this._opcode; @@ -322,54 +330,58 @@ class Receiver extends Writable { if (this._isServer) { if (!this._masked) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'MASK must be set', true, 1002, 'WS_ERR_EXPECTED_MASK' ); + + cb(error); + return; } } else if (this._masked) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'MASK must be clear', true, 1002, 'WS_ERR_UNEXPECTED_MASK' ); + + cb(error); + return; } if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64; - else return this.haveLength(); + else this.haveLength(cb); } /** * Gets extended payload length (7+16). * - * @return {(RangeError|undefined)} A possible error + * @param {Function} cb Callback * @private */ - getPayloadLength16() { + getPayloadLength16(cb) { if (this._bufferedBytes < 2) { this._loop = false; return; } this._payloadLength = this.consume(2).readUInt16BE(0); - return this.haveLength(); + this.haveLength(cb); } /** * Gets extended payload length (7+64). * - * @return {(RangeError|undefined)} A possible error + * @param {Function} cb Callback * @private */ - getPayloadLength64() { + getPayloadLength64(cb) { if (this._bufferedBytes < 8) { this._loop = false; return; @@ -383,38 +395,42 @@ class Receiver extends Writable { // if payload length is greater than this number. // if (num > Math.pow(2, 53 - 32) - 1) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'Unsupported WebSocket frame: payload length > 2^53 - 1', false, 1009, 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' ); + + cb(error); + return; } this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4); - return this.haveLength(); + this.haveLength(cb); } /** * Payload length has been read. * - * @return {(RangeError|undefined)} A possible error + * @param {Function} cb Callback * @private */ - haveLength() { + haveLength(cb) { if (this._payloadLength && this._opcode < 0x08) { this._totalPayloadLength += this._payloadLength; if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'Max payload size exceeded', false, 1009, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' ); + + cb(error); + return; } } @@ -441,7 +457,6 @@ class Receiver extends Writable { * Reads data bytes. * * @param {Function} cb Callback - * @return {(Error|RangeError|undefined)} A possible error * @private */ getData(cb) { @@ -463,7 +478,10 @@ class Receiver extends Writable { } } - if (this._opcode > 0x07) return this.controlMessage(data); + if (this._opcode > 0x07) { + this.controlMessage(data, cb); + return; + } if (this._compressed) { this._state = INFLATING; @@ -480,7 +498,7 @@ class Receiver extends Writable { this._fragments.push(data); } - return this.dataMessage(); + this.dataMessage(cb); } /** @@ -499,76 +517,101 @@ class Receiver extends Writable { if (buf.length) { this._messageLength += buf.length; if (this._messageLength > this._maxPayload && this._maxPayload > 0) { - return cb( - error( - RangeError, - 'Max payload size exceeded', - false, - 1009, - 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' - ) + const error = this.createError( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' ); + + cb(error); + return; } this._fragments.push(buf); } - const er = this.dataMessage(); - if (er) return cb(er); - - this.startLoop(cb); + this.dataMessage(cb); + if (this._state === GET_INFO) this.startLoop(cb); }); } /** * Handles a data message. * - * @return {(Error|undefined)} A possible error + * @param {Function} cb Callback * @private */ - dataMessage() { - if (this._fin) { - const messageLength = this._messageLength; - const fragments = this._fragments; - - this._totalPayloadLength = 0; - this._messageLength = 0; - this._fragmented = 0; - this._fragments = []; - - if (this._opcode === 2) { - let data; - - if (this._binaryType === 'nodebuffer') { - data = concat(fragments, messageLength); - } else if (this._binaryType === 'arraybuffer') { - data = toArrayBuffer(concat(fragments, messageLength)); - } else { - data = fragments; - } + dataMessage(cb) { + if (!this._fin) { + this._state = GET_INFO; + return; + } + + const messageLength = this._messageLength; + const fragments = this._fragments; + + this._totalPayloadLength = 0; + this._messageLength = 0; + this._fragmented = 0; + this._fragments = []; + if (this._opcode === 2) { + let data; + + if (this._binaryType === 'nodebuffer') { + data = concat(fragments, messageLength); + } else if (this._binaryType === 'arraybuffer') { + data = toArrayBuffer(concat(fragments, messageLength)); + } else { + data = fragments; + } + + // + // If the state is `INFLATING`, it means that the frame data was + // decompressed asynchronously, so there is no need to defer the event + // as it will be emitted asynchronously anyway. + // + if (this._state === INFLATING || this._allowMultipleEventsPerMicrotask) { this.emit('message', data, true); + this._state = GET_INFO; } else { - const buf = concat(fragments, messageLength); + this._state = DEFER_EVENT; + queueTask(() => { + this.emit('message', data, true); + this._state = GET_INFO; + this.startLoop(cb); + }); + } + } else { + const buf = concat(fragments, messageLength); - if (!this._skipUTF8Validation && !isValidUTF8(buf)) { - this._loop = false; - return error( - Error, - 'invalid UTF-8 sequence', - true, - 1007, - 'WS_ERR_INVALID_UTF8' - ); - } + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { + const error = this.createError( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); + + cb(error); + return; + } + if (this._state === INFLATING || this._allowMultipleEventsPerMicrotask) { this.emit('message', buf, false); + this._state = GET_INFO; + } else { + this._state = DEFER_EVENT; + queueTask(() => { + this.emit('message', buf, false); + this._state = GET_INFO; + this.startLoop(cb); + }); } } - - this._state = this._allowMultipleEventsPerMicrotask - ? GET_INFO - : WAIT_MICROTASK; } /** @@ -578,24 +621,26 @@ class Receiver extends Writable { * @return {(Error|RangeError|undefined)} A possible error * @private */ - controlMessage(data) { + controlMessage(data, cb) { if (this._opcode === 0x08) { - this._loop = false; - if (data.length === 0) { + this._loop = false; this.emit('conclude', 1005, EMPTY_BUFFER); this.end(); } else { const code = data.readUInt16BE(0); if (!isValidStatusCode(code)) { - return error( + const error = this.createError( RangeError, `invalid status code ${code}`, true, 1002, 'WS_ERR_INVALID_CLOSE_CODE' ); + + cb(error); + return; } const buf = new FastBuffer( @@ -605,15 +650,19 @@ class Receiver extends Writable { ); if (!this._skipUTF8Validation && !isValidUTF8(buf)) { - return error( + const error = this.createError( Error, 'invalid UTF-8 sequence', true, 1007, 'WS_ERR_INVALID_UTF8' ); + + cb(error); + return; } + this._loop = false; this.emit('conclude', code, buf); this.end(); } @@ -622,38 +671,48 @@ class Receiver extends Writable { return; } - this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); - this._state = this._allowMultipleEventsPerMicrotask - ? GET_INFO - : WAIT_MICROTASK; + if (this._allowMultipleEventsPerMicrotask) { + this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); + this._state = GET_INFO; + } else { + this._state = DEFER_EVENT; + queueTask(() => { + this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); + this._state = GET_INFO; + this.startLoop(cb); + }); + } } -} -module.exports = Receiver; + /** + * Builds an error object. + * + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor + * @param {String} message The error message + * @param {Boolean} prefix Specifies whether or not to add a default prefix to + * `message` + * @param {Number} statusCode The status code + * @param {String} errorCode The exposed error code + * @return {(Error|RangeError)} The error + * @private + */ + createError(ErrorCtor, message, prefix, statusCode, errorCode) { + this._loop = false; + this._errored = true; -/** - * Builds an error object. - * - * @param {function(new:Error|RangeError)} ErrorCtor The error constructor - * @param {String} message The error message - * @param {Boolean} prefix Specifies whether or not to add a default prefix to - * `message` - * @param {Number} statusCode The status code - * @param {String} errorCode The exposed error code - * @return {(Error|RangeError)} The error - * @private - */ -function error(ErrorCtor, message, prefix, statusCode, errorCode) { - const err = new ErrorCtor( - prefix ? `Invalid WebSocket frame: ${message}` : message - ); - - Error.captureStackTrace(err, error); - err.code = errorCode; - err[kStatusCode] = statusCode; - return err; + const err = new ErrorCtor( + prefix ? `Invalid WebSocket frame: ${message}` : message + ); + + Error.captureStackTrace(err, this.createError); + err.code = errorCode; + err[kStatusCode] = statusCode; + return err; + } } +module.exports = Receiver; + /** * A shim for `queueMicrotask()`. * diff --git a/test/receiver.test.js b/test/receiver.test.js index 4e3ee923d..ab2d3c749 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -1085,7 +1085,7 @@ describe('Receiver', () => { receiver.write(Buffer.from([0x88, 0x03, 0x03, 0xe8, 0xf8])); }); - it("waits a microtask after the 'message', and 'p{i,o}ng' events", (done) => { + it('emits at most one event per microtask', (done) => { const actual = []; const expected = [ '1', From 4ed7fe58b42a87d06452b6bc19028d167262c30b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 12 Dec 2023 18:48:02 +0100 Subject: [PATCH 244/328] [major] Rename the `allowMultipleEventsPerMicrotask` option Rename the `allowMultipleEventsPerMicrotask` option to `allowSynchronousEvents`. --- doc/ws.md | 16 ++++++++-------- lib/receiver.js | 15 +++++++-------- lib/websocket-server.js | 11 +++++------ lib/websocket.js | 18 +++++++++--------- test/receiver.test.js | 6 +++--- 5 files changed, 32 insertions(+), 34 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index c39ac356c..92eb2c23e 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -72,10 +72,10 @@ This class represents a WebSocket server. It extends the `EventEmitter`. ### new WebSocketServer(options[, callback]) - `options` {Object} - - `allowMultipleEventsPerMicrotask` {Boolean} Specifies whether or not to - process more than one of the `'message'`, `'ping'`, and `'pong'` events per - microtask. To improve compatibility with the WHATWG standard, the default - value is `false`. Setting it to `true` improves performance slightly. + - `allowSynchronousEvents` {Boolean} Specifies whether any of the `'message'`, + `'ping'`, and `'pong'` events can be emitted multiple times in the same + tick. To improve compatibility with the WHATWG standard, the default value + is `false`. Setting it to `true` improves performance slightly. - `backlog` {Number} The maximum length of the queue of pending connections. - `clientTracking` {Boolean} Specifies whether or not to track clients. - `handleProtocols` {Function} A function which can be used to handle the @@ -296,10 +296,10 @@ This class represents a WebSocket. It extends the `EventEmitter`. - `address` {String|url.URL} The URL to which to connect. - `protocols` {String|Array} The list of subprotocols. - `options` {Object} - - `allowMultipleEventsPerMicrotask` {Boolean} Specifies whether or not to - process more than one of the `'message'`, `'ping'`, and `'pong'` events per - microtask. To improve compatibility with the WHATWG standard, the default - value is `false`. Setting it to `true` improves performance slightly. + - `allowSynchronousEvents` {Boolean} Specifies whether any of the `'message'`, + `'ping'`, and `'pong'` events can be emitted multiple times in the same + tick. To improve compatibility with the WHATWG standard, the default value + is `false`. Setting it to `true` improves performance slightly. - `finishRequest` {Function} A function which can be used to customize the headers of each http request before it is sent. See description below. - `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to diff --git a/lib/receiver.js b/lib/receiver.js index 18bb9b54d..9e87d811f 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -39,9 +39,9 @@ class Receiver extends Writable { * Creates a Receiver instance. * * @param {Object} [options] Options object - * @param {Boolean} [options.allowMultipleEventsPerMicrotask=false] Specifies - * whether or not to process more than one of the `'message'`, `'ping'`, - * and `'pong'` events per microtask + * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted + * multiple times in the same tick * @param {String} [options.binaryType=nodebuffer] The type for binary data * @param {Object} [options.extensions] An object containing the negotiated * extensions @@ -54,8 +54,7 @@ class Receiver extends Writable { constructor(options = {}) { super(); - this._allowMultipleEventsPerMicrotask = - !!options.allowMultipleEventsPerMicrotask; + this._allowSynchronousEvents = !!options.allowSynchronousEvents; this._binaryType = options.binaryType || BINARY_TYPES[0]; this._extensions = options.extensions || {}; this._isServer = !!options.isServer; @@ -573,7 +572,7 @@ class Receiver extends Writable { // decompressed asynchronously, so there is no need to defer the event // as it will be emitted asynchronously anyway. // - if (this._state === INFLATING || this._allowMultipleEventsPerMicrotask) { + if (this._state === INFLATING || this._allowSynchronousEvents) { this.emit('message', data, true); this._state = GET_INFO; } else { @@ -600,7 +599,7 @@ class Receiver extends Writable { return; } - if (this._state === INFLATING || this._allowMultipleEventsPerMicrotask) { + if (this._state === INFLATING || this._allowSynchronousEvents) { this.emit('message', buf, false); this._state = GET_INFO; } else { @@ -671,7 +670,7 @@ class Receiver extends Writable { return; } - if (this._allowMultipleEventsPerMicrotask) { + if (this._allowSynchronousEvents) { this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); this._state = GET_INFO; } else { diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 78c0bb289..58b63019d 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -29,9 +29,9 @@ class WebSocketServer extends EventEmitter { * Create a `WebSocketServer` instance. * * @param {Object} options Configuration options - * @param {Boolean} [options.allowMultipleEventsPerMicrotask=false] Specifies - * whether or not to process more than one of the `'message'`, `'ping'`, - * and `'pong'` events per microtask + * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted + * multiple times in the same tick * @param {Number} [options.backlog=511] The maximum length of the queue of * pending connections * @param {Boolean} [options.clientTracking=true] Specifies whether or not to @@ -58,7 +58,7 @@ class WebSocketServer extends EventEmitter { super(); options = { - allowMultipleEventsPerMicrotask: false, + allowSynchronousEvents: false, maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, perMessageDeflate: false, @@ -413,8 +413,7 @@ class WebSocketServer extends EventEmitter { socket.removeListener('error', socketOnError); ws.setSocket(socket, head, { - allowMultipleEventsPerMicrotask: - this.options.allowMultipleEventsPerMicrotask, + allowSynchronousEvents: this.options.allowSynchronousEvents, maxPayload: this.options.maxPayload, skipUTF8Validation: this.options.skipUTF8Validation }); diff --git a/lib/websocket.js b/lib/websocket.js index d2c6a36fe..29e706ef5 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -192,9 +192,9 @@ class WebSocket extends EventEmitter { * @param {Duplex} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Object} options Options object - * @param {Boolean} [options.allowMultipleEventsPerMicrotask=false] Specifies - * whether or not to process more than one of the `'message'`, `'ping'`, - * and `'pong'` events per microtask + * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted + * multiple times in the same tick * @param {Function} [options.generateMask] The function used to generate the * masking key * @param {Number} [options.maxPayload=0] The maximum allowed message size @@ -204,7 +204,7 @@ class WebSocket extends EventEmitter { */ setSocket(socket, head, options) { const receiver = new Receiver({ - allowMultipleEventsPerMicrotask: options.allowMultipleEventsPerMicrotask, + allowSynchronousEvents: options.allowSynchronousEvents, binaryType: this.binaryType, extensions: this._extensions, isServer: this._isServer, @@ -622,9 +622,9 @@ module.exports = WebSocket; * @param {(String|URL)} address The URL to which to connect * @param {Array} protocols The subprotocols * @param {Object} [options] Connection options - * @param {Boolean} [options.allowMultipleEventsPerMicrotask=false] Specifies - * whether or not to process more than one of the `'message'`, `'ping'`, - * and `'pong'` events per microtask + * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether any + * of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple + * times in the same tick * @param {Function} [options.finishRequest] A function which can be used to * customize the headers of each http request before it is sent * @param {Boolean} [options.followRedirects=false] Whether or not to follow @@ -649,7 +649,7 @@ module.exports = WebSocket; */ function initAsClient(websocket, address, protocols, options) { const opts = { - allowMultipleEventsPerMicrotask: false, + allowSynchronousEvents: false, protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, @@ -1001,7 +1001,7 @@ function initAsClient(websocket, address, protocols, options) { } websocket.setSocket(socket, head, { - allowMultipleEventsPerMicrotask: opts.allowMultipleEventsPerMicrotask, + allowSynchronousEvents: opts.allowSynchronousEvents, generateMask: opts.generateMask, maxPayload: opts.maxPayload, skipUTF8Validation: opts.skipUTF8Validation diff --git a/test/receiver.test.js b/test/receiver.test.js index ab2d3c749..a88f29b9a 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -443,7 +443,7 @@ describe('Receiver', () => { buf[i + 1] = 0x00; } - const receiver = new Receiver(); + const receiver = new Receiver({ allowSynchronousEvents: true }); let counter = 0; receiver.on('message', (data, isBinary) => { @@ -1151,7 +1151,7 @@ describe('Receiver', () => { receiver.write(Buffer.from('82008200', 'hex')); }); - it('honors the `allowMultipleEventsPerMicrotask` option', (done) => { + it('honors the `allowSynchronousEvents` option', (done) => { const actual = []; const expected = [ '1', @@ -1179,7 +1179,7 @@ describe('Receiver', () => { }); } - const receiver = new Receiver({ allowMultipleEventsPerMicrotask: true }); + const receiver = new Receiver({ allowSynchronousEvents: true }); receiver.on('message', listener); receiver.on('ping', listener); From a57e963f946860f6418baaa55b307bfa7d0bc143 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 12 Dec 2023 19:02:46 +0100 Subject: [PATCH 245/328] [dist] 8.15.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78679492b..1424de93b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.15.0", + "version": "8.15.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From d37756a973d48c4e924344916823a9189cbfa454 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 20 Dec 2023 03:17:04 -0500 Subject: [PATCH 246/328] [doc] Clarify legacy deps (#2184) --- README.md | 52 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a550ca1c7..82aa26a48 100644 --- a/README.md +++ b/README.md @@ -57,27 +57,37 @@ npm install ws ### Opt-in for performance -There are 2 optional modules that can be installed along side with the ws -module. These modules are binary addons that improve the performance of certain -operations. Prebuilt binaries are available for the most popular platforms so -you don't necessarily need to have a C++ compiler installed on your machine. - -- `npm install --save-optional bufferutil`: Allows to efficiently perform - operations such as masking and unmasking the data payload of the WebSocket - frames. -- `npm install --save-optional utf-8-validate`: Allows to efficiently check if a - message contains valid UTF-8. - -To not even try to require and use these modules, use the -[`WS_NO_BUFFER_UTIL`](./doc/ws.md#ws_no_buffer_util) and -[`WS_NO_UTF_8_VALIDATE`](./doc/ws.md#ws_no_utf_8_validate) environment -variables. These might be useful to enhance security in systems where a user can -put a package in the package search path of an application of another user, due -to how the Node.js resolver algorithm works. - -The `utf-8-validate` module is not needed and is not required, even if it is -already installed, regardless of the value of the `WS_NO_UTF_8_VALIDATE` -environment variable, if [`buffer.isUtf8()`][] is available. +`bufferutil` is an optional module that can be installed alongside the `ws` +module: + +``` +npm install --save-optional bufferutil +``` + +This is a binary addon that improves the performance of certain operations such +as masking and unmasking the data payload of the WebSocket frames. Prebuilt +binaries are available for the most popular platforms, so you don't necessarily +need to have a C++ compiler installed on your machine. + +To force `ws` to not use `bufferutil`, use the +[`WS_NO_BUFFER_UTIL`](./doc/ws.md#ws_no_buffer_util) environment variable. This +can be useful to enhance security in systems where a user can put a package in +the package search path of an application of another user, due to how the +Node.js resolver algorithm works. + +#### Legacy opt-in for performance + +If you are running on an old version of Node.js (prior to v18.14.0), `ws` also +supports the `utf-8-validate` module: + +``` +npm install --save-optional utf-8-validate +``` + +This contains a binary polyfill for [`buffer.isUtf8()`][]. + +To force `ws` to not use `utf-8-validate`, use the +[`WS_NO_UTF_8_VALIDATE`](./doc/ws.md#ws_no_utf_8_validate) environment variable. ## API docs From 3e230c16b70efa82fd28da7aca45c341a2b3efd8 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 20 Dec 2023 11:02:13 +0100 Subject: [PATCH 247/328] [doc] Fix nits --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 82aa26a48..40a9bba63 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ npm install ws ### Opt-in for performance -`bufferutil` is an optional module that can be installed alongside the `ws` +[bufferutil][] is an optional module that can be installed alongside the ws module: ``` @@ -69,7 +69,7 @@ as masking and unmasking the data payload of the WebSocket frames. Prebuilt binaries are available for the most popular platforms, so you don't necessarily need to have a C++ compiler installed on your machine. -To force `ws` to not use `bufferutil`, use the +To force ws to not use bufferutil, use the [`WS_NO_BUFFER_UTIL`](./doc/ws.md#ws_no_buffer_util) environment variable. This can be useful to enhance security in systems where a user can put a package in the package search path of an application of another user, due to how the @@ -77,8 +77,8 @@ Node.js resolver algorithm works. #### Legacy opt-in for performance -If you are running on an old version of Node.js (prior to v18.14.0), `ws` also -supports the `utf-8-validate` module: +If you are running on an old version of Node.js (prior to v18.14.0), ws also +supports the [utf-8-validate][] module: ``` npm install --save-optional utf-8-validate @@ -86,7 +86,7 @@ npm install --save-optional utf-8-validate This contains a binary polyfill for [`buffer.isUtf8()`][]. -To force `ws` to not use `utf-8-validate`, use the +To force ws to not use utf-8-validate, use the [`WS_NO_UTF_8_VALIDATE`](./doc/ws.md#ws_no_utf_8_validate) environment variable. ## API docs @@ -533,6 +533,7 @@ We're using the GitHub [releases][changelog] for changelog entries. [MIT](LICENSE) [`buffer.isutf8()`]: https://nodejs.org/api/buffer.html#bufferisutf8input +[bufferutil]: https://github.com/websockets/bufferutil [changelog]: https://github.com/websockets/ws/releases [client-report]: http://websockets.github.io/ws/autobahn/clients/ [https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent @@ -543,4 +544,5 @@ We're using the GitHub [releases][changelog] for changelog entries. [server-report]: http://websockets.github.io/ws/autobahn/servers/ [session-parse-example]: ./examples/express-session-parse [socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent +[utf-8-validate]: https://github.com/websockets/utf-8-validate [ws-server-options]: ./doc/ws.md#new-websocketserveroptions-callback From 527ec97264cf063bd9c75f33e6a085559fb7d1da Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 20 Dec 2023 18:21:29 +0100 Subject: [PATCH 248/328] [doc] Add missing subsubsection to TOC --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 40a9bba63..80d988655 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ can use one of the many wrappers available on npm, like - [Protocol support](#protocol-support) - [Installing](#installing) - [Opt-in for performance](#opt-in-for-performance) + - [Legacy opt-in for performance](#legacy-opt-in-for-performance) - [API docs](#api-docs) - [WebSocket compression](#websocket-compression) - [Usage examples](#usage-examples) From 01ba54edaeff0f3a58abd7cb9f8e1f3bf134d0fc Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 25 Dec 2023 16:00:21 +0100 Subject: [PATCH 249/328] [feature] Introduce the `autoPong` option Add the ability to disable the automatic sending of pong responses to pings. Fixes #2186 --- doc/ws.md | 4 +++ lib/websocket-server.js | 5 +++- lib/websocket.js | 8 +++++- test/websocket-server.test.js | 24 ++++++++++++++++++ test/websocket.test.js | 47 +++++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 92eb2c23e..f79cfc901 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -72,6 +72,8 @@ This class represents a WebSocket server. It extends the `EventEmitter`. ### new WebSocketServer(options[, callback]) - `options` {Object} + - `autoPong` {Boolean} Specifies whether or not to automatically send a pong + in response to a ping. Defaults to `true`. - `allowSynchronousEvents` {Boolean} Specifies whether any of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple times in the same tick. To improve compatibility with the WHATWG standard, the default value @@ -296,6 +298,8 @@ This class represents a WebSocket. It extends the `EventEmitter`. - `address` {String|url.URL} The URL to which to connect. - `protocols` {String|Array} The list of subprotocols. - `options` {Object} + - `autoPong` {Boolean} Specifies whether or not to automatically send a pong + in response to a ping. Defaults to `true`. - `allowSynchronousEvents` {Boolean} Specifies whether any of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple times in the same tick. To improve compatibility with the WHATWG standard, the default value diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 58b63019d..377c45a8b 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -32,6 +32,8 @@ class WebSocketServer extends EventEmitter { * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted * multiple times in the same tick + * @param {Boolean} [options.autoPong=true] Specifies whether or not to + * automatically send a pong in response to a ping * @param {Number} [options.backlog=511] The maximum length of the queue of * pending connections * @param {Boolean} [options.clientTracking=true] Specifies whether or not to @@ -59,6 +61,7 @@ class WebSocketServer extends EventEmitter { options = { allowSynchronousEvents: false, + autoPong: true, maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, perMessageDeflate: false, @@ -379,7 +382,7 @@ class WebSocketServer extends EventEmitter { `Sec-WebSocket-Accept: ${digest}` ]; - const ws = new this.options.WebSocket(null); + const ws = new this.options.WebSocket(null, undefined, this.options); if (protocols.size) { // diff --git a/lib/websocket.js b/lib/websocket.js index 29e706ef5..df5034cc7 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -84,6 +84,7 @@ class WebSocket extends EventEmitter { initAsClient(this, address, protocols, options); } else { + this._autoPong = options.autoPong; this._isServer = true; } } @@ -625,6 +626,8 @@ module.exports = WebSocket; * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether any * of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple * times in the same tick + * @param {Boolean} [options.autoPong=true] Specifies whether or not to + * automatically send a pong in response to a ping * @param {Function} [options.finishRequest] A function which can be used to * customize the headers of each http request before it is sent * @param {Boolean} [options.followRedirects=false] Whether or not to follow @@ -650,6 +653,7 @@ module.exports = WebSocket; function initAsClient(websocket, address, protocols, options) { const opts = { allowSynchronousEvents: false, + autoPong: true, protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, @@ -668,6 +672,8 @@ function initAsClient(websocket, address, protocols, options) { port: undefined }; + websocket._autoPong = opts.autoPong; + if (!protocolVersions.includes(opts.protocolVersion)) { throw new RangeError( `Unsupported protocol version: ${opts.protocolVersion} ` + @@ -1212,7 +1218,7 @@ function receiverOnMessage(data, isBinary) { function receiverOnPing(data) { const websocket = this[kWebSocket]; - websocket.pong(data, !websocket._isServer, NOOP); + if (websocket._autoPong) websocket.pong(data, !this._isServer, NOOP); websocket.emit('ping', data); } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 5b6937cee..44c2c6709 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -116,6 +116,30 @@ describe('WebSocketServer', () => { wss.close(done); }); }); + + it('honors the `autoPong` option', (done) => { + const wss = new WebSocket.Server({ autoPong: false, port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.ping(); + }); + + ws.on('pong', () => { + done(new Error("Unexpected 'pong' event")); + }); + }); + + wss.on('connection', (ws) => { + ws.on('ping', () => { + ws.close(); + }); + + ws.on('close', () => { + wss.close(done); + }); + }); + }); }); it('emits an error if http server bind fails', (done) => { diff --git a/test/websocket.test.js b/test/websocket.test.js index 4699ae5cd..28dcb8808 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -197,6 +197,30 @@ describe('WebSocket', () => { }); }); }); + + it('honors the `autoPong` option', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + autoPong: false + }); + + ws.on('ping', () => { + ws.close(); + }); + + ws.on('close', () => { + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('pong', () => { + done(new Error("Unexpected 'pong' event")); + }); + + ws.ping(); + }); + }); }); }); @@ -2325,6 +2349,29 @@ describe('WebSocket', () => { ws.close(); }); }); + + it('is called automatically when a ping is received', (done) => { + const buf = Buffer.from('hi'); + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.ping(buf); + }); + + ws.on('pong', (data) => { + assert.deepStrictEqual(data, buf); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('ping', (data) => { + assert.deepStrictEqual(data, buf); + ws.close(); + }); + }); + }); }); describe('#resume', () => { From 391ddf3a9a8852ac70fed55a17fad803e27a77ee Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 26 Dec 2023 11:27:06 +0100 Subject: [PATCH 250/328] [test] Use `stream.getDefaultHighWaterMark()` when available Refs: https://github.com/nodejs/node/pull/50120 --- test/create-websocket-stream.test.js | 18 ++++++++++++++---- test/websocket.test.js | 21 +++++++++++++++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/test/create-websocket-stream.test.js b/test/create-websocket-stream.test.js index 572f5c4f2..0a83a45ea 100644 --- a/test/create-websocket-stream.test.js +++ b/test/create-websocket-stream.test.js @@ -3,7 +3,7 @@ const assert = require('assert'); const EventEmitter = require('events'); const { createServer } = require('http'); -const { Duplex } = require('stream'); +const { Duplex, getDefaultHighWaterMark } = require('stream'); const { randomBytes } = require('crypto'); const createWebSocketStream = require('../lib/stream'); @@ -11,6 +11,10 @@ const Sender = require('../lib/sender'); const WebSocket = require('..'); const { EMPTY_BUFFER } = require('../lib/constants'); +const highWaterMark = getDefaultHighWaterMark + ? getDefaultHighWaterMark(false) + : 16 * 1024; + describe('createWebSocketStream', () => { it('is exposed as a property of the `WebSocket` class', () => { assert.strictEqual(WebSocket.createWebSocketStream, createWebSocketStream); @@ -445,12 +449,15 @@ describe('createWebSocketStream', () => { }; const list = [ - ...Sender.frame(randomBytes(16 * 1024), { rsv1: false, ...opts }), + ...Sender.frame(randomBytes(highWaterMark), { + rsv1: false, + ...opts + }), ...Sender.frame(Buffer.alloc(1), { rsv1: true, ...opts }) ]; // This hack is used because there is no guarantee that more than - // 16 KiB will be sent as a single TCP packet. + // `highWaterMark` bytes will be sent as a single TCP packet. ws._socket.push(Buffer.concat(list)); }); @@ -494,7 +501,10 @@ describe('createWebSocketStream', () => { }; const list = [ - ...Sender.frame(randomBytes(16 * 1024), { rsv1: false, ...opts }), + ...Sender.frame(randomBytes(highWaterMark), { + rsv1: false, + ...opts + }), ...Sender.frame(Buffer.alloc(1), { rsv1: true, ...opts }) ]; diff --git a/test/websocket.test.js b/test/websocket.test.js index 28dcb8808..e1b3bd239 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -11,6 +11,7 @@ const net = require('net'); const tls = require('tls'); const os = require('os'); const fs = require('fs'); +const { getDefaultHighWaterMark } = require('stream'); const { URL } = require('url'); const Sender = require('../lib/sender'); @@ -23,6 +24,10 @@ const { } = require('../lib/event-target'); const { EMPTY_BUFFER, GUID, kListener, NOOP } = require('../lib/constants'); +const highWaterMark = getDefaultHighWaterMark + ? getDefaultHighWaterMark(false) + : 16 * 1024; + class CustomAgent extends http.Agent { addRequest() {} } @@ -4092,7 +4097,7 @@ describe('WebSocket', () => { ws.terminate(); }; - const payload1 = Buffer.alloc(15 * 1024); + const payload1 = Buffer.alloc(highWaterMark - 1024); const payload2 = Buffer.alloc(1); const opts = { @@ -4107,13 +4112,17 @@ describe('WebSocket', () => { ...Sender.frame(payload2, { rsv1: true, ...opts }) ]; - for (let i = 0; i < 399; i++) { + for (let i = 0; i < 340; i++) { list.push(list[list.length - 2], list[list.length - 1]); } + const data = Buffer.concat(list); + + assert.ok(data.length > highWaterMark); + // This hack is used because there is no guarantee that more than - // 16 KiB will be sent as a single TCP packet. - push.call(ws._socket, Buffer.concat(list)); + // `highWaterMark` bytes will be sent as a single TCP packet. + push.call(ws._socket, data); wss.clients .values() @@ -4128,8 +4137,8 @@ describe('WebSocket', () => { ws.on('close', (code) => { assert.strictEqual(code, 1006); - assert.strictEqual(messageLengths.length, 402); - assert.strictEqual(messageLengths[0], 15360); + assert.strictEqual(messageLengths.length, 343); + assert.strictEqual(messageLengths[0], highWaterMark - 1024); assert.strictEqual(messageLengths[messageLengths.length - 1], 1); wss.close(done); }); From d343a0cf7bba29a4e14217cb010446bec8fdf444 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 26 Dec 2023 16:31:20 +0100 Subject: [PATCH 251/328] [dist] 8.16.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1424de93b..a2443200d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.15.1", + "version": "8.16.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 5e42cfdc5fa114659908eaad4d9ead7d5051d740 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 24 Jan 2024 07:50:08 +0100 Subject: [PATCH 252/328] [meta] Add FUNDING.json Refs: https://github.com/websockets/ws/issues/2194 --- FUNDING.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 FUNDING.json diff --git a/FUNDING.json b/FUNDING.json new file mode 100644 index 000000000..043b42fec --- /dev/null +++ b/FUNDING.json @@ -0,0 +1,7 @@ +{ + "drips": { + "ethereum": { + "ownedBy": "0x3D4f997A071d2BA735AC767E68052679423c3dBe" + } + } +} From 8be840e0a93c9c90565c6f137834ecacba0f14bf Mon Sep 17 00:00:00 2001 From: Al-phonsio <160236920+Al-phonsio@users.noreply.github.com> Date: Sat, 16 Mar 2024 15:24:36 +0100 Subject: [PATCH 253/328] [doc] Replace `url.parse()` with `new URL()` (#2208) --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 80d988655..b6cacbe92 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,6 @@ server.listen(8080); ```js import { createServer } from 'http'; -import { parse } from 'url'; import { WebSocketServer } from 'ws'; const server = createServer(); @@ -265,7 +264,7 @@ wss2.on('connection', function connection(ws) { }); server.on('upgrade', function upgrade(request, socket, head) { - const { pathname } = parse(request.url); + const { pathname } = new URL(request.url, 'wss://base.url'); if (pathname === '/foo') { wss1.handleUpgrade(request, socket, head, function done(ws) { From 2405c17775fb57f5e07db123c6133733dd58bbab Mon Sep 17 00:00:00 2001 From: Jana R <94439978+grjan7@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:23:43 +0530 Subject: [PATCH 254/328] [doc] Add punctuation for readability (#2213) --- README.md | 16 ++++++++-------- SECURITY.md | 14 +++++++------- doc/ws.md | 46 +++++++++++++++++++++++----------------------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index b6cacbe92..21f10df10 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Passes the quite extensive Autobahn test suite: [server][server-report], [client][client-report]. **Note**: This module does not work in the browser. The client in the docs is a -reference to a back end with the role of a client in the WebSocket -communication. Browser clients must use the native +reference to a backend with the role of a client in the WebSocket communication. +Browser clients must use the native [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object. To make the same code work seamlessly on Node.js and the browser, you can use one of the many wrappers available on npm, like @@ -87,7 +87,7 @@ npm install --save-optional utf-8-validate This contains a binary polyfill for [`buffer.isUtf8()`][]. -To force ws to not use utf-8-validate, use the +To force ws not to use utf-8-validate, use the [`WS_NO_UTF_8_VALIDATE`](./doc/ws.md#ws_no_utf_8_validate) environment variable. ## API docs @@ -146,7 +146,7 @@ const wss = new WebSocketServer({ ``` The client will only use the extension if it is supported and enabled on the -server. To always disable the extension on the client set the +server. To always disable the extension on the client, set the `perMessageDeflate` option to `false`. ```js @@ -451,11 +451,11 @@ wss.on('connection', function connection(ws, req) { ### How to detect and close broken connections? -Sometimes the link between the server and the client can be interrupted in a way -that keeps both the server and the client unaware of the broken state of the +Sometimes, the link between the server and the client can be interrupted in a +way that keeps both the server and the client unaware of the broken state of the connection (e.g. when pulling the cord). -In these cases ping messages can be used as a means to verify that the remote +In these cases, ping messages can be used as a means to verify that the remote endpoint is still responsive. ```js @@ -490,7 +490,7 @@ wss.on('close', function close() { Pong messages are automatically sent in response to ping messages as required by the spec. -Just like the server example above your clients might as well lose connection +Just like the server example above, your clients might as well lose connection without knowing it. You might want to add a ping listener on your clients to prevent that. A simple implementation would be: diff --git a/SECURITY.md b/SECURITY.md index 0baf19a63..cbaf84de2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,21 +12,21 @@ blocked instantly. ## Exceptions -If you do not receive an acknowledgement within the said time frame please give +If you do not receive an acknowledgement within the said time frame, please give us the benefit of the doubt as it's possible that we haven't seen it yet. In -this case please send us a message **without details** using one of the +this case, please send us a message **without details** using one of the following methods: - Contact the lead developers of this project on their personal e-mails. You can - find the e-mails in the git logs, for example using the following command: + find the e-mails in the git logs, for example, using the following command: `git --no-pager show -s --format='%an <%ae>' ` where `` is the SHA1 of their latest commit in the project. - Create a GitHub issue stating contact details and the severity of the issue. -Once we have acknowledged receipt of your report and confirmed the bug ourselves -we will work with you to fix the vulnerability and publicly acknowledge your -responsible disclosure, if you wish. In addition to that we will create and -publish a security advisory to +Once we have acknowledged receipt of your report and confirmed the bug +ourselves, we will work with you to fix the vulnerability and publicly +acknowledge your responsible disclosure, if you wish. In addition to that, we +will create and publish a security advisory to [GitHub Security Advisories](https://github.com/websockets/ws/security/advisories?state=published). ## History diff --git a/doc/ws.md b/doc/ws.md index f79cfc901..017087f5f 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -103,7 +103,7 @@ This class represents a WebSocket server. It extends the `EventEmitter`. Create a new server instance. One and only one of `port`, `server` or `noServer` must be provided or an error is thrown. An HTTP server is automatically created, started, and used if `port` is set. To use an external HTTP/S server instead, -specify only `server` or `noServer`. In this case the HTTP/S server must be +specify only `server` or `noServer`. In this case, the HTTP/S server must be started manually. The "noServer" mode allows the WebSocket server to be completely detached from the HTTP/S server. This makes it possible, for example, to share a single HTTP/S server between multiple WebSocket servers. @@ -112,8 +112,8 @@ to share a single HTTP/S server between multiple WebSocket servers. > authentication in the `'upgrade'` event of the HTTP server. See examples for > more details. -If `verifyClient` is not set then the handshake is automatically accepted. If it -has a single parameter then `ws` will invoke it with the following argument: +If `verifyClient` is not set, then the handshake is automatically accepted. If +it has a single parameter, then `ws` will invoke it with the following argument: - `info` {Object} - `origin` {String} The value in the Origin header indicated by the client. @@ -124,19 +124,19 @@ has a single parameter then `ws` will invoke it with the following argument: The return value (`Boolean`) of the function determines whether or not to accept the handshake. -If `verifyClient` has two parameters then `ws` will invoke it with the following -arguments: +If `verifyClient` has two parameters, then `ws` will invoke it with the +following arguments: - `info` {Object} Same as above. - `cb` {Function} A callback that must be called by the user upon inspection of the `info` fields. Arguments in this callback are: - `result` {Boolean} Whether or not to accept the handshake. - - `code` {Number} When `result` is `false` this field determines the HTTP + - `code` {Number} When `result` is `false`, this field determines the HTTP error status code to be sent to the client. - - `name` {String} When `result` is `false` this field determines the HTTP + - `name` {String} When `result` is `false`, this field determines the HTTP reason phrase. - - `headers` {Object} When `result` is `false` this field determines additional - HTTP headers to be sent to the client. For example, + - `headers` {Object} When `result` is `false`, this field determines + additional HTTP headers to be sent to the client. For example, `{ 'Retry-After': 120 }`. `handleProtocols` takes two arguments: @@ -146,15 +146,15 @@ arguments: - `request` {http.IncomingMessage} The client HTTP GET request. The returned value sets the value of the `Sec-WebSocket-Protocol` header in the -HTTP 101 response. If returned value is `false` the header is not added in the +HTTP 101 response. If returned value is `false`, the header is not added in the response. -If `handleProtocols` is not set then the first of the client's requested +If `handleProtocols` is not set, then the first of the client's requested subprotocols is used. `perMessageDeflate` can be used to control the behavior of [permessage-deflate extension][permessage-deflate]. The extension is disabled when `false` (default -value). If an object is provided then that is extension parameters: +value). If an object is provided, then that is extension parameters: - `serverNoContextTakeover` {Boolean} Whether to use context takeover or not. - `clientNoContextTakeover` {Boolean} Acknowledge disabling of client context @@ -171,8 +171,8 @@ value). If an object is provided then that is extension parameters: above this limit will be queued. Default 10. You usually won't need to touch this option. See [this issue][concurrency-limit] for more details. -If a property is empty then either an offered configuration or a default value -is used. When sending a fragmented message the length of the first fragment is +If a property is empty, then either an offered configuration or a default value +is used. When sending a fragmented message, the length of the first fragment is compared to the threshold. This determines if compression is used for the entire message. @@ -248,7 +248,7 @@ created internally. If an external HTTP server is used via the `server` or `noServer` constructor options, it must be closed manually. Existing connections are not closed automatically. The server emits a `'close'` event when all connections are closed unless an external HTTP server is used and client -tracking is disabled. In this case the `'close'` event is emitted in the next +tracking is disabled. In this case, the `'close'` event is emitted in the next tick. The optional callback is called when the `'close'` event occurs and receives an `Error` if the server is already closed. @@ -273,7 +273,7 @@ If the upgrade is successful, the `callback` is called with two arguments: - `request` {http.IncomingMessage} The client HTTP GET request. -See if a given request should be handled by this server. By default this method +See if a given request should be handled by this server. By default, this method validates the pathname of the request, matching it against the `path` option if provided. The return value, `true` or `false`, determines whether or not to accept the handshake. @@ -305,12 +305,12 @@ This class represents a WebSocket. It extends the `EventEmitter`. tick. To improve compatibility with the WHATWG standard, the default value is `false`. Setting it to `true` improves performance slightly. - `finishRequest` {Function} A function which can be used to customize the - headers of each http request before it is sent. See description below. + headers of each HTTP request before it is sent. See description below. - `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to `false`. - `generateMask` {Function} The function used to generate the masking key. It takes a `Buffer` that must be filled synchronously and is called before a - message is sent, for each message. By default the buffer is filled with + message is sent, for each message. By default, the buffer is filled with cryptographically strong random bytes. - `handshakeTimeout` {Number} Timeout in milliseconds for the handshake request. This is reset after every redirection. @@ -343,7 +343,7 @@ context takeover. for each HTTP GET request (the initial one and any caused by redirects) when it is ready to be sent, to allow for last minute customization of the headers. If -`finishRequest` is set then it has the responsibility to call `request.end()` +`finishRequest` is set, then it has the responsibility to call `request.end()` once it is done setting request headers. This is intended for niche use-cases where some headers can't be provided in advance e.g. because they depend on the underlying socket. @@ -479,7 +479,7 @@ The number of bytes of data that have been queued using calls to `send()` but not yet transmitted to the network. This deviates from the HTML standard in the following ways: -1. If the data is immediately sent the value is `0`. +1. If the data is immediately sent, the value is `0`. 1. All framing bytes are included. ### websocket.close([code[, reason]]) @@ -610,7 +610,7 @@ state is `CONNECTING`. ### websocket.terminate() -Forcibly close the connection. Internally this calls [`socket.destroy()`][]. +Forcibly close the connection. Internally, this calls [`socket.destroy()`][]. ### websocket.url @@ -631,12 +631,12 @@ given `WebSocket`. ### WS_NO_BUFFER_UTIL -When set to a non empty value, prevents the optional `bufferutil` dependency +When set to a non-empty value, prevents the optional `bufferutil` dependency from being required. ### WS_NO_UTF_8_VALIDATE -When set to a non empty value, prevents the optional `utf-8-validate` dependency +When set to a non-empty value, prevents the optional `utf-8-validate` dependency from being required. ## Error codes From b119b41db3bde7c5929609b4a52aa95c3af06f04 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 9 Apr 2024 19:07:26 +0200 Subject: [PATCH 255/328] [pkg] Update eslint to version 9.0.0 --- .eslintrc.yaml | 19 ------------------- eslint.config.js | 28 ++++++++++++++++++++++++++++ lib/websocket-server.js | 2 +- lib/websocket.js | 2 +- package.json | 5 +++-- 5 files changed, 33 insertions(+), 23 deletions(-) delete mode 100644 .eslintrc.yaml create mode 100644 eslint.config.js diff --git a/.eslintrc.yaml b/.eslintrc.yaml deleted file mode 100644 index f3d983b9c..000000000 --- a/.eslintrc.yaml +++ /dev/null @@ -1,19 +0,0 @@ -env: - browser: true - es6: true - mocha: true - node: true -extends: - - eslint:recommended - - plugin:prettier/recommended -parserOptions: - ecmaVersion: latest - sourceType: module -rules: - no-console: off - no-var: error - prefer-const: error - quotes: - - error - - single - - avoidEscape: true diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..4e685b9ad --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,28 @@ +'use strict'; + +const pluginPrettierRecommended = require('eslint-plugin-prettier/recommended'); +const globals = require('globals'); +const js = require('@eslint/js'); + +module.exports = [ + js.configs.recommended, + { + ignores: ['.nyc_output/', '.vscode/', 'coverage/', 'node_modules/'], + languageOptions: { + ecmaVersion: 'latest', + globals: { + ...globals.browser, + ...globals.mocha, + ...globals.node + }, + sourceType: 'module' + }, + rules: { + 'no-console': 'off', + 'no-unused-vars': ['error', { caughtErrors: 'none' }], + 'no-var': 'error', + 'prefer-const': 'error' + } + }, + pluginPrettierRecommended +]; diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 377c45a8b..4873ad9fb 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -1,4 +1,4 @@ -/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$" }] */ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$", "caughtErrors": "none" }] */ 'use strict'; diff --git a/lib/websocket.js b/lib/websocket.js index df5034cc7..f133d08fc 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -1,4 +1,4 @@ -/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex|Readable$" }] */ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex|Readable$", "caughtErrors": "none" }] */ 'use strict'; diff --git a/package.json b/package.json index a2443200d..74ae3c0c2 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "scripts": { "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js", "integration": "mocha --throw-deprecation test/*.integration.js", - "lint": "eslint --ignore-path .gitignore . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\"" + "lint": "eslint . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\"" }, "peerDependencies": { "bufferutil": "^4.0.1", @@ -57,9 +57,10 @@ "devDependencies": { "benchmark": "^2.1.4", "bufferutil": "^4.0.1", - "eslint": "^8.0.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", + "globals": "^15.0.0", "mocha": "^8.4.0", "nyc": "^15.0.0", "prettier": "^3.0.0", From 53a88881cf5da8307ecbd5020db0a8fb72cf0d20 Mon Sep 17 00:00:00 2001 From: Tim Perry <1526883+pimterry@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:20:52 +0200 Subject: [PATCH 256/328] [feature] Allow the `createConnection` option (#2219) Allow passing in a custom `createConnection` function. --- doc/ws.md | 4 ++++ lib/websocket.js | 7 +++++-- test/websocket.test.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 017087f5f..37f4c9707 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -304,6 +304,10 @@ This class represents a WebSocket. It extends the `EventEmitter`. `'ping'`, and `'pong'` events can be emitted multiple times in the same tick. To improve compatibility with the WHATWG standard, the default value is `false`. Setting it to `true` improves performance slightly. + - `createConnection` {Function} An alternative function to use in place of + `tls.createConnection` or `net.createConnection`. This can be used to + manually control exactly how the connection to the server is made, or to + make a connection over an existing Duplex stream obtained elsewhere. - `finishRequest` {Function} A function which can be used to customize the headers of each HTTP request before it is sent. See description below. - `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to diff --git a/lib/websocket.js b/lib/websocket.js index f133d08fc..a2c8edbec 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -628,6 +628,8 @@ module.exports = WebSocket; * times in the same tick * @param {Boolean} [options.autoPong=true] Specifies whether or not to * automatically send a pong in response to a ping + * @param {Function} [options.createConnection] An alternative function to use + * in place of `tls.createConnection` or `net.createConnection`. * @param {Function} [options.finishRequest] A function which can be used to * customize the headers of each http request before it is sent * @param {Boolean} [options.followRedirects=false] Whether or not to follow @@ -660,8 +662,8 @@ function initAsClient(websocket, address, protocols, options) { perMessageDeflate: true, followRedirects: false, maxRedirects: 10, - ...options, createConnection: undefined, + ...options, socketPath: undefined, hostname: undefined, protocol: undefined, @@ -732,7 +734,8 @@ function initAsClient(websocket, address, protocols, options) { const protocolSet = new Set(); let perMessageDeflate; - opts.createConnection = isSecure ? tlsConnect : netConnect; + opts.createConnection = + opts.createConnection || (isSecure ? tlsConnect : netConnect); opts.defaultPort = opts.defaultPort || defaultPort; opts.port = parsedUrl.port || defaultPort; opts.host = parsedUrl.hostname.startsWith('[') diff --git a/test/websocket.test.js b/test/websocket.test.js index e1b3bd239..fc41ae755 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1224,6 +1224,34 @@ describe('WebSocket', () => { }); }); + it('honors the `createConnection` option', (done) => { + const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); + + server.once('upgrade', (req, socket, head) => { + assert.strictEqual(req.headers.host, 'google.com:22'); + wss.handleUpgrade(req, socket, head, NOOP); + }); + + const ws = new WebSocket('ws://google.com:22/foo', { + createConnection: (options) => { + assert.strictEqual(options.host, 'google.com'); + assert.strictEqual(options.port, '22'); + + // Ignore the invalid host address, and connect to the server manually: + return net.createConnection({ + host: 'localhost', + port: server.address().port + }); + } + }); + + ws.on('open', () => { + assert.strictEqual(ws.url, 'ws://google.com:22/foo'); + ws.on('close', () => done()); + ws.close(); + }); + }); + it('emits an error if the redirect URL is invalid (1/2)', (done) => { server.once('upgrade', (req, socket) => { socket.end('HTTP/1.1 302 Found\r\nLocation: ws://\r\n\r\n'); From 2aa0405a5e96754b296fef6bd6ebdfb2f11967fc Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 15 Apr 2024 15:02:18 +0200 Subject: [PATCH 257/328] [minor] Fix nits --- doc/ws.md | 4 --- lib/websocket.js | 3 --- test/websocket.test.js | 57 +++++++++++++++++++++--------------------- 3 files changed, 29 insertions(+), 35 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 37f4c9707..017087f5f 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -304,10 +304,6 @@ This class represents a WebSocket. It extends the `EventEmitter`. `'ping'`, and `'pong'` events can be emitted multiple times in the same tick. To improve compatibility with the WHATWG standard, the default value is `false`. Setting it to `true` improves performance slightly. - - `createConnection` {Function} An alternative function to use in place of - `tls.createConnection` or `net.createConnection`. This can be used to - manually control exactly how the connection to the server is made, or to - make a connection over an existing Duplex stream obtained elsewhere. - `finishRequest` {Function} A function which can be used to customize the headers of each HTTP request before it is sent. See description below. - `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to diff --git a/lib/websocket.js b/lib/websocket.js index a2c8edbec..56d6c6fe3 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -628,8 +628,6 @@ module.exports = WebSocket; * times in the same tick * @param {Boolean} [options.autoPong=true] Specifies whether or not to * automatically send a pong in response to a ping - * @param {Function} [options.createConnection] An alternative function to use - * in place of `tls.createConnection` or `net.createConnection`. * @param {Function} [options.finishRequest] A function which can be used to * customize the headers of each http request before it is sent * @param {Boolean} [options.followRedirects=false] Whether or not to follow @@ -662,7 +660,6 @@ function initAsClient(websocket, address, protocols, options) { perMessageDeflate: true, followRedirects: false, maxRedirects: 10, - createConnection: undefined, ...options, socketPath: undefined, hostname: undefined, diff --git a/test/websocket.test.js b/test/websocket.test.js index fc41ae755..7cb17f0ea 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1158,6 +1158,35 @@ describe('WebSocket', () => { }); }); + it('honors the `createConnection` option', (done) => { + const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); + + server.once('upgrade', (req, socket, head) => { + assert.strictEqual(req.headers.host, 'google.com:22'); + wss.handleUpgrade(req, socket, head, NOOP); + }); + + const ws = new WebSocket('ws://google.com:22/foo', { + createConnection: (options) => { + assert.strictEqual(options.host, 'google.com'); + assert.strictEqual(options.port, '22'); + + // Ignore the `options` argument, and use the correct hostname and + // port to connect to the server. + return net.createConnection({ + host: 'localhost', + port: server.address().port + }); + } + }); + + ws.on('open', () => { + assert.strictEqual(ws.url, 'ws://google.com:22/foo'); + ws.on('close', () => done()); + ws.close(); + }); + }); + it('does not follow redirects by default', (done) => { server.once('upgrade', (req, socket) => { socket.end( @@ -1224,34 +1253,6 @@ describe('WebSocket', () => { }); }); - it('honors the `createConnection` option', (done) => { - const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); - - server.once('upgrade', (req, socket, head) => { - assert.strictEqual(req.headers.host, 'google.com:22'); - wss.handleUpgrade(req, socket, head, NOOP); - }); - - const ws = new WebSocket('ws://google.com:22/foo', { - createConnection: (options) => { - assert.strictEqual(options.host, 'google.com'); - assert.strictEqual(options.port, '22'); - - // Ignore the invalid host address, and connect to the server manually: - return net.createConnection({ - host: 'localhost', - port: server.address().port - }); - } - }); - - ws.on('open', () => { - assert.strictEqual(ws.url, 'ws://google.com:22/foo'); - ws.on('close', () => done()); - ws.close(); - }); - }); - it('emits an error if the redirect URL is invalid (1/2)', (done) => { server.once('upgrade', (req, socket) => { socket.end('HTTP/1.1 302 Found\r\nLocation: ws://\r\n\r\n'); From e5f32c7e1e6d3d19cd4a1fdec84890e154db30c1 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 24 Apr 2024 09:32:43 -0400 Subject: [PATCH 258/328] [fix] Emit at most one event per event loop iteration (#2218) Fixes #2216 --- lib/receiver.js | 49 ++++-------------------------------------- test/receiver.test.js | 28 +++++++++++++++--------- test/websocket.test.js | 6 ++---- 3 files changed, 24 insertions(+), 59 deletions(-) diff --git a/lib/receiver.js b/lib/receiver.js index 9e87d811f..4515e6887 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -13,13 +13,6 @@ const { concat, toArrayBuffer, unmask } = require('./buffer-util'); const { isValidStatusCode, isValidUTF8 } = require('./validation'); const FastBuffer = Buffer[Symbol.species]; -const promise = Promise.resolve(); - -// -// `queueMicrotask()` is not available in Node.js < 11. -// -const queueTask = - typeof queueMicrotask === 'function' ? queueMicrotask : queueMicrotaskShim; const GET_INFO = 0; const GET_PAYLOAD_LENGTH_16 = 1; @@ -567,17 +560,12 @@ class Receiver extends Writable { data = fragments; } - // - // If the state is `INFLATING`, it means that the frame data was - // decompressed asynchronously, so there is no need to defer the event - // as it will be emitted asynchronously anyway. - // - if (this._state === INFLATING || this._allowSynchronousEvents) { + if (this._allowSynchronousEvents) { this.emit('message', data, true); this._state = GET_INFO; } else { this._state = DEFER_EVENT; - queueTask(() => { + setImmediate(() => { this.emit('message', data, true); this._state = GET_INFO; this.startLoop(cb); @@ -604,7 +592,7 @@ class Receiver extends Writable { this._state = GET_INFO; } else { this._state = DEFER_EVENT; - queueTask(() => { + setImmediate(() => { this.emit('message', buf, false); this._state = GET_INFO; this.startLoop(cb); @@ -675,7 +663,7 @@ class Receiver extends Writable { this._state = GET_INFO; } else { this._state = DEFER_EVENT; - queueTask(() => { + setImmediate(() => { this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); this._state = GET_INFO; this.startLoop(cb); @@ -711,32 +699,3 @@ class Receiver extends Writable { } module.exports = Receiver; - -/** - * A shim for `queueMicrotask()`. - * - * @param {Function} cb Callback - */ -function queueMicrotaskShim(cb) { - promise.then(cb).catch(throwErrorNextTick); -} - -/** - * Throws an error. - * - * @param {Error} err The error to throw - * @private - */ -function throwError(err) { - throw err; -} - -/** - * Throws an error in the next tick. - * - * @param {Error} err The error to throw - * @private - */ -function throwErrorNextTick(err) { - process.nextTick(throwError, err); -} diff --git a/test/receiver.test.js b/test/receiver.test.js index a88f29b9a..f3a0fa645 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -1085,17 +1085,21 @@ describe('Receiver', () => { receiver.write(Buffer.from([0x88, 0x03, 0x03, 0xe8, 0xf8])); }); - it('emits at most one event per microtask', (done) => { + it('emits at most one event per event loop iteration', (done) => { const actual = []; const expected = [ '1', - 'microtask 1', + '- 1', + '-- 1', '2', - 'microtask 2', + '- 2', + '-- 2', '3', - 'microtask 3', + '- 3', + '-- 3', '4', - 'microtask 4' + '- 4', + '-- 4' ]; function listener(data) { @@ -1104,12 +1108,16 @@ describe('Receiver', () => { // `queueMicrotask()` is not available in Node.js < 11. Promise.resolve().then(() => { - actual.push(`microtask ${message}`); + actual.push(`- ${message}`); - if (actual.length === 8) { - assert.deepStrictEqual(actual, expected); - done(); - } + Promise.resolve().then(() => { + actual.push(`-- ${message}`); + + if (actual.length === 12) { + assert.deepStrictEqual(actual, expected); + done(); + } + }); }); } diff --git a/test/websocket.test.js b/test/websocket.test.js index 7cb17f0ea..5570b1caf 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -4234,8 +4234,7 @@ describe('WebSocket', () => { if (messages.push(message.toString()) > 1) return; - // `queueMicrotask()` is not available in Node.js < 11. - Promise.resolve().then(() => { + setImmediate(() => { process.nextTick(() => { assert.strictEqual(ws._receiver._state, 5); ws.close(1000); @@ -4485,8 +4484,7 @@ describe('WebSocket', () => { if (messages.push(message.toString()) > 1) return; - // `queueMicrotask()` is not available in Node.js < 11. - Promise.resolve().then(() => { + setImmediate(() => { process.nextTick(() => { assert.strictEqual(ws._receiver._state, 5); ws.terminate(); From 96c9b3deddf56cacb2d756aaa918071e03cdbc42 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 24 Apr 2024 09:42:40 -0400 Subject: [PATCH 259/328] [major] Flip the default value of `allowSynchronousEvents` (#2221) Flip the default value of the `allowSynchronousEvents` option to `true`. Refs: https://github.com/websockets/ws/pull/2218 --- doc/ws.md | 8 +++---- lib/receiver.js | 7 ++++-- lib/websocket-server.js | 4 ++-- lib/websocket.js | 4 ++-- test/receiver.test.js | 47 ++++++----------------------------------- 5 files changed, 19 insertions(+), 51 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 017087f5f..1189fd02a 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -76,8 +76,8 @@ This class represents a WebSocket server. It extends the `EventEmitter`. in response to a ping. Defaults to `true`. - `allowSynchronousEvents` {Boolean} Specifies whether any of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple times in the same - tick. To improve compatibility with the WHATWG standard, the default value - is `false`. Setting it to `true` improves performance slightly. + tick. Defaults to `true`. Setting it to `false` improves compatibility with + the WHATWG standardbut may negatively impact performance. - `backlog` {Number} The maximum length of the queue of pending connections. - `clientTracking` {Boolean} Specifies whether or not to track clients. - `handleProtocols` {Function} A function which can be used to handle the @@ -302,8 +302,8 @@ This class represents a WebSocket. It extends the `EventEmitter`. in response to a ping. Defaults to `true`. - `allowSynchronousEvents` {Boolean} Specifies whether any of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple times in the same - tick. To improve compatibility with the WHATWG standard, the default value - is `false`. Setting it to `true` improves performance slightly. + tick. Defaults to `true`. Setting it to `false` improves compatibility with + the WHATWG standardbut may negatively impact performance. - `finishRequest` {Function} A function which can be used to customize the headers of each HTTP request before it is sent. See description below. - `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to diff --git a/lib/receiver.js b/lib/receiver.js index 4515e6887..70dfd9933 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -32,7 +32,7 @@ class Receiver extends Writable { * Creates a Receiver instance. * * @param {Object} [options] Options object - * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted * multiple times in the same tick * @param {String} [options.binaryType=nodebuffer] The type for binary data @@ -47,7 +47,10 @@ class Receiver extends Writable { constructor(options = {}) { super(); - this._allowSynchronousEvents = !!options.allowSynchronousEvents; + this._allowSynchronousEvents = + options.allowSynchronousEvents !== undefined + ? options.allowSynchronousEvents + : true; this._binaryType = options.binaryType || BINARY_TYPES[0]; this._extensions = options.extensions || {}; this._isServer = !!options.isServer; diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 4873ad9fb..40980f6e9 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -29,7 +29,7 @@ class WebSocketServer extends EventEmitter { * Create a `WebSocketServer` instance. * * @param {Object} options Configuration options - * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted * multiple times in the same tick * @param {Boolean} [options.autoPong=true] Specifies whether or not to @@ -60,7 +60,7 @@ class WebSocketServer extends EventEmitter { super(); options = { - allowSynchronousEvents: false, + allowSynchronousEvents: true, autoPong: true, maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, diff --git a/lib/websocket.js b/lib/websocket.js index 56d6c6fe3..709ad825a 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -623,7 +623,7 @@ module.exports = WebSocket; * @param {(String|URL)} address The URL to which to connect * @param {Array} protocols The subprotocols * @param {Object} [options] Connection options - * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether any + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether any * of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple * times in the same tick * @param {Boolean} [options.autoPong=true] Specifies whether or not to @@ -652,7 +652,7 @@ module.exports = WebSocket; */ function initAsClient(websocket, address, protocols, options) { const opts = { - allowSynchronousEvents: false, + allowSynchronousEvents: true, autoPong: true, protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, diff --git a/test/receiver.test.js b/test/receiver.test.js index f3a0fa645..88a6326d1 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -443,7 +443,7 @@ describe('Receiver', () => { buf[i + 1] = 0x00; } - const receiver = new Receiver({ allowSynchronousEvents: true }); + const receiver = new Receiver(); let counter = 0; receiver.on('message', (data, isBinary) => { @@ -1085,7 +1085,7 @@ describe('Receiver', () => { receiver.write(Buffer.from([0x88, 0x03, 0x03, 0xe8, 0xf8])); }); - it('emits at most one event per event loop iteration', (done) => { + it('honors the `allowSynchronousEvents` option', (done) => { const actual = []; const expected = [ '1', @@ -1121,7 +1121,7 @@ describe('Receiver', () => { }); } - const receiver = new Receiver(); + const receiver = new Receiver({ allowSynchronousEvents: false }); receiver.on('message', listener); receiver.on('ping', listener); @@ -1156,43 +1156,8 @@ describe('Receiver', () => { done(); }); - receiver.write(Buffer.from('82008200', 'hex')); - }); - - it('honors the `allowSynchronousEvents` option', (done) => { - const actual = []; - const expected = [ - '1', - '2', - '3', - '4', - 'microtask 1', - 'microtask 2', - 'microtask 3', - 'microtask 4' - ]; - - function listener(data) { - const message = data.toString(); - actual.push(message); - - // `queueMicrotask()` is not available in Node.js < 11. - Promise.resolve().then(() => { - actual.push(`microtask ${message}`); - - if (actual.length === 8) { - assert.deepStrictEqual(actual, expected); - done(); - } - }); - } - - const receiver = new Receiver({ allowSynchronousEvents: true }); - - receiver.on('message', listener); - receiver.on('ping', listener); - receiver.on('pong', listener); - - receiver.write(Buffer.from('8101318901328a0133810134', 'hex')); + setImmediate(() => { + receiver.write(Buffer.from('82008200', 'hex')); + }); }); }); From 1817bac06e1204bfb578b8b3f4bafd0fa09623d0 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 26 Apr 2024 07:26:09 +0200 Subject: [PATCH 260/328] [ci] Do not test on node 21 --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ca1a776a..7a6490630 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,6 @@ jobs: - 16 - 18 - 20 - - 21 os: - macOS-latest - ubuntu-latest From 934c9d6b938b93c045cb13e5f7c19c27a8dd925a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 26 Apr 2024 07:26:27 +0200 Subject: [PATCH 261/328] [ci] Test on node 22 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a6490630..04693fc7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: - 16 - 18 - 20 + - 22 os: - macOS-latest - ubuntu-latest From 29694a5905fa703e86667928e6bacac397469471 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 26 Apr 2024 07:53:26 +0200 Subject: [PATCH 262/328] [test] Use the `highWaterMark` variable Use the value of the `highWaterMark` variable instead of `16384`. --- test/create-websocket-stream.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/create-websocket-stream.test.js b/test/create-websocket-stream.test.js index 0a83a45ea..54a13c6c8 100644 --- a/test/create-websocket-stream.test.js +++ b/test/create-websocket-stream.test.js @@ -604,7 +604,7 @@ describe('createWebSocketStream', () => { }); wss.on('connection', (ws) => { - ws.send(randomBytes(16 * 1024)); + ws.send(randomBytes(highWaterMark)); }); }); }); From b73b11828d166e9692a9bffe9c01a7e93bab04a8 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 28 Apr 2024 07:41:02 +0200 Subject: [PATCH 263/328] [dist] 8.17.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 74ae3c0c2..ed9c681db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.16.0", + "version": "8.17.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From ddfe4a804d79e7788ab136290e609f91cf68423f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 18 May 2024 17:11:07 +0200 Subject: [PATCH 264/328] [perf] Reduce the amount of `crypto.randomFillSync()` calls Use a pool of random bytes to reduce the amount of `crypto.randomFillSync()` calls. Refs: https://github.com/nodejs/undici/pull/3204 --- lib/sender.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/sender.js b/lib/sender.js index 1ed04b027..5ea2986ee 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -12,6 +12,9 @@ const { mask: applyMask, toBuffer } = require('./buffer-util'); const kByteLength = Symbol('kByteLength'); const maskBuffer = Buffer.alloc(4); +const RANDOM_POOL_SIZE = 8 * 1024; +let randomPool; +let randomPoolPointer = RANDOM_POOL_SIZE; /** * HyBi Sender implementation. @@ -76,7 +79,19 @@ class Sender { if (options.generateMask) { options.generateMask(mask); } else { - randomFillSync(mask, 0, 4); + if (randomPoolPointer === RANDOM_POOL_SIZE) { + if (randomPool === undefined) { + randomPool = Buffer.alloc(RANDOM_POOL_SIZE); + } + + randomFillSync(randomPool, 0, RANDOM_POOL_SIZE); + randomPoolPointer = 0; + } + + mask[0] = randomPool[randomPoolPointer++]; + mask[1] = randomPool[randomPoolPointer++]; + mask[2] = randomPool[randomPoolPointer++]; + mask[3] = randomPool[randomPoolPointer++]; } skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; From 6a00029edd924499f892aed8003cef1fa724cfe5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 13 Jun 2024 21:53:40 +0200 Subject: [PATCH 265/328] [test] Increase code coverage --- lib/sender.js | 5 +++++ test/receiver.test.js | 2 +- test/websocket.test.js | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/sender.js b/lib/sender.js index 5ea2986ee..c81ec66f6 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -80,7 +80,12 @@ class Sender { options.generateMask(mask); } else { if (randomPoolPointer === RANDOM_POOL_SIZE) { + /* istanbul ignore else */ if (randomPool === undefined) { + // + // This is lazily initialized because server-sent frames must not + // be masked so it may never be used. + // randomPool = Buffer.alloc(RANDOM_POOL_SIZE); } diff --git a/test/receiver.test.js b/test/receiver.test.js index 88a6326d1..1f9e75d3a 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -1127,7 +1127,7 @@ describe('Receiver', () => { receiver.on('ping', listener); receiver.on('pong', listener); - receiver.write(Buffer.from('8101318901328a0133810134', 'hex')); + receiver.write(Buffer.from('8101318901328a0133820134', 'hex')); }); it('does not swallow errors thrown from event handlers', (done) => { diff --git a/test/websocket.test.js b/test/websocket.test.js index 5570b1caf..aa53c3bc9 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -4423,6 +4423,7 @@ describe('WebSocket', () => { 'The socket was closed while data was being compressed' ); }); + ws.close(); }); } ); From e55e5106f10fcbaac37cfa89759e4cc0d073a52c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 16 Jun 2024 11:30:42 +0200 Subject: [PATCH 266/328] [security] Fix crash when the Upgrade header cannot be read (#2231) It is possible that the Upgrade header is correctly received and handled (the `'upgrade'` event is emitted) without its value being returned to the user. This can happen if the number of received headers exceed the `server.maxHeadersCount` or `request.maxHeadersCount` threshold. In this case `incomingMessage.headers.upgrade` may not be set. Handle the case correctly and abort the handshake. Fixes #2230 --- lib/websocket-server.js | 5 ++-- lib/websocket.js | 4 +++- test/websocket-server.test.js | 44 +++++++++++++++++++++++++++++++++++ test/websocket.test.js | 26 +++++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 40980f6e9..67b52ffdd 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -235,6 +235,7 @@ class WebSocketServer extends EventEmitter { socket.on('error', socketOnError); const key = req.headers['sec-websocket-key']; + const upgrade = req.headers.upgrade; const version = +req.headers['sec-websocket-version']; if (req.method !== 'GET') { @@ -243,13 +244,13 @@ class WebSocketServer extends EventEmitter { return; } - if (req.headers.upgrade.toLowerCase() !== 'websocket') { + if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') { const message = 'Invalid Upgrade header'; abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } - if (!key || !keyRegex.test(key)) { + if (key === undefined || !keyRegex.test(key)) { const message = 'Missing or invalid Sec-WebSocket-Key header'; abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; diff --git a/lib/websocket.js b/lib/websocket.js index 709ad825a..aa57bbade 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -928,7 +928,9 @@ function initAsClient(websocket, address, protocols, options) { req = websocket._req = null; - if (res.headers.upgrade.toLowerCase() !== 'websocket') { + const upgrade = res.headers.upgrade; + + if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') { abortHandshake(websocket, socket, 'Invalid Upgrade header'); return; } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 44c2c6709..34de4dcfa 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -653,6 +653,50 @@ describe('WebSocketServer', () => { }); }); + it('fails if the Upgrade header field value cannot be read', (done) => { + const server = http.createServer(); + const wss = new WebSocket.Server({ noServer: true }); + + server.maxHeadersCount = 1; + + server.on('upgrade', (req, socket, head) => { + assert.deepStrictEqual(req.headers, { foo: 'bar' }); + wss.handleUpgrade(req, socket, head, () => { + done(new Error('Unexpected callback invocation')); + }); + }); + + server.listen(() => { + const req = http.get({ + port: server.address().port, + headers: { + foo: 'bar', + bar: 'baz', + Connection: 'Upgrade', + Upgrade: 'websocket' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'Invalid Upgrade header' + ); + server.close(done); + }); + }); + }); + }); + it('fails if the Upgrade header field value is not "websocket"', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const req = http.get({ diff --git a/test/websocket.test.js b/test/websocket.test.js index aa53c3bc9..8a05f073b 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -757,6 +757,32 @@ describe('WebSocket', () => { beforeEach((done) => server.listen(0, done)); afterEach((done) => server.close(done)); + it('fails if the Upgrade header field value cannot be read', (done) => { + server.once('upgrade', (req, socket) => { + socket.on('end', socket.end); + socket.write( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Connection: Upgrade\r\n' + + 'Upgrade: websocket\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws._req.maxHeadersCount = 1; + + ws.on('upgrade', (res) => { + assert.deepStrictEqual(res.headers, { connection: 'Upgrade' }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Invalid Upgrade header'); + done(); + }); + }); + }); + it('fails if the Upgrade header field value is not "websocket"', (done) => { server.once('upgrade', (req, socket) => { socket.on('end', socket.end); From 3c56601092872f7d7566989f0e379271afd0e4a1 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 16 Jun 2024 15:35:43 +0200 Subject: [PATCH 267/328] [dist] 8.17.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ed9c681db..4abcf2989 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.17.0", + "version": "8.17.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 15f11a052a231d1f819fffef53bf4b287646d1ca Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 16 Jun 2024 21:00:17 +0200 Subject: [PATCH 268/328] [security] Add new DoS vulnerability to SECURITY.md --- SECURITY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index cbaf84de2..65c896975 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -37,3 +37,5 @@ will create and publish a security advisory to [DoS vulnerability](https://github.com/websockets/ws/releases/tag/3.3.1) - 25 May 2021: [ReDoS in `Sec-Websocket-Protocol` header](https://github.com/websockets/ws/releases/tag/7.4.6) +- 16 Jun 2024: + [DoS when handling a request with many HTTP headers](https://github.com/websockets/ws/releases/tag/8.17.1) From 0d1b5e6c4acad16a6b1a1904426eb266a5ba2f72 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 16 Jun 2024 21:14:45 +0200 Subject: [PATCH 269/328] [security] Use more descriptive text for 2017 vulnerability link --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 65c896975..fb492e834 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -34,7 +34,7 @@ will create and publish a security advisory to - 04 Jan 2016: [Buffer vulnerability](https://github.com/websockets/ws/releases/tag/1.0.1) - 08 Nov 2017: - [DoS vulnerability](https://github.com/websockets/ws/releases/tag/3.3.1) + [DoS in the `Sec-Websocket-Extensions` header parser](https://github.com/websockets/ws/releases/tag/3.3.1) - 25 May 2021: [ReDoS in `Sec-Websocket-Protocol` header](https://github.com/websockets/ws/releases/tag/7.4.6) - 16 Jun 2024: From 59b9629b78aa66bcf9acce20468004cd3751c136 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 2 Jul 2024 17:50:16 +0200 Subject: [PATCH 270/328] [feature] Add support for `Blob` (#2229) Closes #2206 --- doc/ws.md | 18 +- lib/constants.js | 8 +- lib/receiver.js | 2 + lib/sender.js | 197 ++++++++++++++++------ lib/validation.js | 22 +++ lib/websocket.js | 76 +++++++-- test/receiver.test.js | 40 ++++- test/sender.test.js | 142 ++++++++++++++-- test/websocket.test.js | 363 ++++++++++++++++++++++++++++++++++++++++- 9 files changed, 775 insertions(+), 93 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 1189fd02a..f30ad4cae 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -466,10 +466,11 @@ does nothing if `type` is not one of `'close'`, `'error'`, `'message'`, or - {String} A string indicating the type of binary data being transmitted by the connection. -This should be one of "nodebuffer", "arraybuffer" or "fragments". Defaults to -"nodebuffer". Type "fragments" will emit the array of fragments as received from -the sender, without copyfull concatenation, which is useful for the performance -of binary protocols transferring large messages with multiple fragments. +This should be one of "nodebuffer", "arraybuffer", "blob", or "fragments". +Defaults to "nodebuffer". Type "fragments" will emit the array of fragments as +received from the sender, without copyfull concatenation, which is useful for +the performance of binary protocols transferring large messages with multiple +fragments. ### websocket.bufferedAmount @@ -538,7 +539,8 @@ is a noop if the ready state is `CONNECTING` or `CLOSED`. ### websocket.ping([data[, mask]][, callback]) -- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The +- `data` + {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray|Blob} The data to send in the ping frame. - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults to `true` when `websocket` is not a server client. @@ -550,7 +552,8 @@ Send a ping. This method throws an error if the ready state is `CONNECTING`. ### websocket.pong([data[, mask]][, callback]) -- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The +- `data` + {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray|Blob} The data to send in the pong frame. - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults to `true` when `websocket` is not a server client. @@ -588,7 +591,8 @@ only removes listeners added with ### websocket.send(data[, options][, callback]) -- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The +- `data` + {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray|Blob} The data to send. `Object` values are only supported if they conform to the requirements of [`Buffer.from()`][]. If those constraints are not met, a `TypeError` is thrown. diff --git a/lib/constants.js b/lib/constants.js index d691b30a1..74214d466 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,9 +1,15 @@ 'use strict'; +const BINARY_TYPES = ['nodebuffer', 'arraybuffer', 'fragments']; +const hasBlob = typeof Blob !== 'undefined'; + +if (hasBlob) BINARY_TYPES.push('blob'); + module.exports = { - BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], + BINARY_TYPES, EMPTY_BUFFER: Buffer.alloc(0), GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + hasBlob, kForOnEventAttribute: Symbol('kIsForOnEventAttribute'), kListener: Symbol('kListener'), kStatusCode: Symbol('status-code'), diff --git a/lib/receiver.js b/lib/receiver.js index 70dfd9933..54d9b4fad 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -559,6 +559,8 @@ class Receiver extends Writable { data = concat(fragments, messageLength); } else if (this._binaryType === 'arraybuffer') { data = toArrayBuffer(concat(fragments, messageLength)); + } else if (this._binaryType === 'blob') { + data = new Blob(fragments); } else { data = fragments; } diff --git a/lib/sender.js b/lib/sender.js index c81ec66f6..ee16cea5a 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -6,8 +6,8 @@ const { Duplex } = require('stream'); const { randomFillSync } = require('crypto'); const PerMessageDeflate = require('./permessage-deflate'); -const { EMPTY_BUFFER } = require('./constants'); -const { isValidStatusCode } = require('./validation'); +const { EMPTY_BUFFER, kWebSocket, NOOP } = require('./constants'); +const { isBlob, isValidStatusCode } = require('./validation'); const { mask: applyMask, toBuffer } = require('./buffer-util'); const kByteLength = Symbol('kByteLength'); @@ -16,6 +16,10 @@ const RANDOM_POOL_SIZE = 8 * 1024; let randomPool; let randomPoolPointer = RANDOM_POOL_SIZE; +const DEFAULT = 0; +const DEFLATING = 1; +const GET_BLOB_DATA = 2; + /** * HyBi Sender implementation. */ @@ -42,8 +46,10 @@ class Sender { this._compress = false; this._bufferedBytes = 0; - this._deflating = false; this._queue = []; + this._state = DEFAULT; + this.onerror = NOOP; + this[kWebSocket] = undefined; } /** @@ -210,7 +216,7 @@ class Sender { rsv1: false }; - if (this._deflating) { + if (this._state !== DEFAULT) { this.enqueue([this.dispatch, buf, false, options, cb]); } else { this.sendFrame(Sender.frame(buf, options), cb); @@ -232,6 +238,9 @@ class Sender { if (typeof data === 'string') { byteLength = Buffer.byteLength(data); readOnly = false; + } else if (isBlob(data)) { + byteLength = data.size; + readOnly = false; } else { data = toBuffer(data); byteLength = data.length; @@ -253,7 +262,13 @@ class Sender { rsv1: false }; - if (this._deflating) { + if (isBlob(data)) { + if (this._state !== DEFAULT) { + this.enqueue([this.getBlobData, data, false, options, cb]); + } else { + this.getBlobData(data, false, options, cb); + } + } else if (this._state !== DEFAULT) { this.enqueue([this.dispatch, data, false, options, cb]); } else { this.sendFrame(Sender.frame(data, options), cb); @@ -275,6 +290,9 @@ class Sender { if (typeof data === 'string') { byteLength = Buffer.byteLength(data); readOnly = false; + } else if (isBlob(data)) { + byteLength = data.size; + readOnly = false; } else { data = toBuffer(data); byteLength = data.length; @@ -296,7 +314,13 @@ class Sender { rsv1: false }; - if (this._deflating) { + if (isBlob(data)) { + if (this._state !== DEFAULT) { + this.enqueue([this.getBlobData, data, false, options, cb]); + } else { + this.getBlobData(data, false, options, cb); + } + } else if (this._state !== DEFAULT) { this.enqueue([this.dispatch, data, false, options, cb]); } else { this.sendFrame(Sender.frame(data, options), cb); @@ -330,6 +354,9 @@ class Sender { if (typeof data === 'string') { byteLength = Buffer.byteLength(data); readOnly = false; + } else if (isBlob(data)) { + byteLength = data.size; + readOnly = false; } else { data = toBuffer(data); byteLength = data.length; @@ -357,40 +384,94 @@ class Sender { if (options.fin) this._firstFragment = true; - if (perMessageDeflate) { - const opts = { - [kByteLength]: byteLength, - fin: options.fin, - generateMask: this._generateMask, - mask: options.mask, - maskBuffer: this._maskBuffer, - opcode, - readOnly, - rsv1 - }; - - if (this._deflating) { - this.enqueue([this.dispatch, data, this._compress, opts, cb]); + const opts = { + [kByteLength]: byteLength, + fin: options.fin, + generateMask: this._generateMask, + mask: options.mask, + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1 + }; + + if (isBlob(data)) { + if (this._state !== DEFAULT) { + this.enqueue([this.getBlobData, data, this._compress, opts, cb]); } else { - this.dispatch(data, this._compress, opts, cb); + this.getBlobData(data, this._compress, opts, cb); } + } else if (this._state !== DEFAULT) { + this.enqueue([this.dispatch, data, this._compress, opts, cb]); } else { - this.sendFrame( - Sender.frame(data, { - [kByteLength]: byteLength, - fin: options.fin, - generateMask: this._generateMask, - mask: options.mask, - maskBuffer: this._maskBuffer, - opcode, - readOnly, - rsv1: false - }), - cb - ); + this.dispatch(data, this._compress, opts, cb); } } + /** + * Gets the contents of a blob as binary data. + * + * @param {Blob} blob The blob + * @param {Boolean} [compress=false] Specifies whether or not to compress + * the data + * @param {Object} options Options object + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit + * @param {Function} [cb] Callback + * @private + */ + getBlobData(blob, compress, options, cb) { + this._bufferedBytes += options[kByteLength]; + this._state = GET_BLOB_DATA; + + blob + .arrayBuffer() + .then((arrayBuffer) => { + if (this._socket.destroyed) { + const err = new Error( + 'The socket was closed while the blob was being read' + ); + + // + // `callCallbacks` is called in the next tick to ensure that errors + // that might be thrown in the callbacks behave like errors thrown + // outside the promise chain. + // + process.nextTick(callCallbacks, this, err, cb); + return; + } + + this._bufferedBytes -= options[kByteLength]; + const data = toBuffer(arrayBuffer); + + if (!compress) { + this._state = DEFAULT; + this.sendFrame(Sender.frame(data, options), cb); + this.dequeue(); + } else { + this.dispatch(data, compress, options, cb); + } + }) + .catch((err) => { + // + // `onError` is called in the next tick for the same reason that + // `callCallbacks` above is. + // + process.nextTick(onError, this, err, cb); + }); + } + /** * Dispatches a message. * @@ -423,27 +504,19 @@ class Sender { const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; this._bufferedBytes += options[kByteLength]; - this._deflating = true; + this._state = DEFLATING; perMessageDeflate.compress(data, options.fin, (_, buf) => { if (this._socket.destroyed) { const err = new Error( 'The socket was closed while data was being compressed' ); - if (typeof cb === 'function') cb(err); - - for (let i = 0; i < this._queue.length; i++) { - const params = this._queue[i]; - const callback = params[params.length - 1]; - - if (typeof callback === 'function') callback(err); - } - + callCallbacks(this, err, cb); return; } this._bufferedBytes -= options[kByteLength]; - this._deflating = false; + this._state = DEFAULT; options.readOnly = false; this.sendFrame(Sender.frame(buf, options), cb); this.dequeue(); @@ -456,7 +529,7 @@ class Sender { * @private */ dequeue() { - while (!this._deflating && this._queue.length) { + while (this._state === DEFAULT && this._queue.length) { const params = this._queue.shift(); this._bufferedBytes -= params[3][kByteLength]; @@ -495,3 +568,35 @@ class Sender { } module.exports = Sender; + +/** + * Calls queued callbacks with an error. + * + * @param {Sender} sender The `Sender` instance + * @param {Error} err The error to call the callbacks with + * @param {Function} [cb] The first callback + * @private + */ +function callCallbacks(sender, err, cb) { + if (typeof cb === 'function') cb(err); + + for (let i = 0; i < sender._queue.length; i++) { + const params = sender._queue[i]; + const callback = params[params.length - 1]; + + if (typeof callback === 'function') callback(err); + } +} + +/** + * Handles a `Sender` error. + * + * @param {Sender} sender The `Sender` instance + * @param {Error} err The error + * @param {Function} [cb] The first pending callback + * @private + */ +function onError(sender, err, cb) { + callCallbacks(sender, err, cb); + sender.onerror(err); +} diff --git a/lib/validation.js b/lib/validation.js index c352e6ea7..4a2e68d51 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -2,6 +2,8 @@ const { isUtf8 } = require('buffer'); +const { hasBlob } = require('./constants'); + // // Allowed token characters: // @@ -107,7 +109,27 @@ function _isValidUTF8(buf) { return true; } +/** + * Determines whether a value is a `Blob`. + * + * @param {*} value The value to be tested + * @return {Boolean} `true` if `value` is a `Blob`, else `false` + * @private + */ +function isBlob(value) { + return ( + hasBlob && + typeof value === 'object' && + typeof value.arrayBuffer === 'function' && + typeof value.type === 'string' && + typeof value.stream === 'function' && + (value[Symbol.toStringTag] === 'Blob' || + value[Symbol.toStringTag] === 'File') + ); +} + module.exports = { + isBlob, isValidStatusCode, isValidUTF8: _isValidUTF8, tokenChars diff --git a/lib/websocket.js b/lib/websocket.js index aa57bbade..7fb402970 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -14,6 +14,8 @@ const { URL } = require('url'); const PerMessageDeflate = require('./permessage-deflate'); const Receiver = require('./receiver'); const Sender = require('./sender'); +const { isBlob } = require('./validation'); + const { BINARY_TYPES, EMPTY_BUFFER, @@ -58,6 +60,7 @@ class WebSocket extends EventEmitter { this._closeFrameSent = false; this._closeMessage = EMPTY_BUFFER; this._closeTimer = null; + this._errorEmitted = false; this._extensions = {}; this._paused = false; this._protocol = ''; @@ -90,9 +93,8 @@ class WebSocket extends EventEmitter { } /** - * This deviates from the WHATWG interface since ws doesn't support the - * required default "blob" type (instead we define a custom "nodebuffer" - * type). + * For historical reasons, the custom "nodebuffer" type is used by the default + * instead of "blob". * * @type {String} */ @@ -213,11 +215,14 @@ class WebSocket extends EventEmitter { skipUTF8Validation: options.skipUTF8Validation }); - this._sender = new Sender(socket, this._extensions, options.generateMask); + const sender = new Sender(socket, this._extensions, options.generateMask); + this._receiver = receiver; + this._sender = sender; this._socket = socket; receiver[kWebSocket] = this; + sender[kWebSocket] = this; socket[kWebSocket] = this; receiver.on('conclude', receiverOnConclude); @@ -227,6 +232,8 @@ class WebSocket extends EventEmitter { receiver.on('ping', receiverOnPing); receiver.on('pong', receiverOnPong); + sender.onerror = senderOnError; + // // These methods may not be available if `socket` is just a `Duplex`. // @@ -322,13 +329,7 @@ class WebSocket extends EventEmitter { } }); - // - // Specify a timeout for the closing handshake to complete. - // - this._closeTimer = setTimeout( - this._socket.destroy.bind(this._socket), - closeTimeout - ); + setCloseTimer(this); } /** @@ -1032,6 +1033,11 @@ function initAsClient(websocket, address, protocols, options) { */ function emitErrorAndClose(websocket, err) { websocket._readyState = WebSocket.CLOSING; + // + // The following assignment is practically useless and is done only for + // consistency. + // + websocket._errorEmitted = true; websocket.emit('error', err); websocket.emitClose(); } @@ -1112,7 +1118,7 @@ function abortHandshake(websocket, stream, message) { */ function sendAfterClose(websocket, data, cb) { if (data) { - const length = toBuffer(data).length; + const length = isBlob(data) ? data.size : toBuffer(data).length; // // The `_bufferedAmount` property is used only when the peer is a client and @@ -1188,7 +1194,10 @@ function receiverOnError(err) { websocket.close(err[kStatusCode]); } - websocket.emit('error', err); + if (!websocket._errorEmitted) { + websocket._errorEmitted = true; + websocket.emit('error', err); + } } /** @@ -1244,6 +1253,47 @@ function resume(stream) { stream.resume(); } +/** + * The `Sender` error event handler. + * + * @param {Error} The error + * @private + */ +function senderOnError(err) { + const websocket = this[kWebSocket]; + + if (websocket.readyState === WebSocket.CLOSED) return; + if (websocket.readyState === WebSocket.OPEN) { + websocket._readyState = WebSocket.CLOSING; + setCloseTimer(websocket); + } + + // + // `socket.end()` is used instead of `socket.destroy()` to allow the other + // peer to finish sending queued data. There is no need to set a timer here + // because `CLOSING` means that it is already set or not needed. + // + this._socket.end(); + + if (!websocket._errorEmitted) { + websocket._errorEmitted = true; + websocket.emit('error', err); + } +} + +/** + * Set a timer to destroy the underlying raw socket of a WebSocket. + * + * @param {WebSocket} websocket The WebSocket instance + * @private + */ +function setCloseTimer(websocket) { + websocket._closeTimer = setTimeout( + websocket._socket.destroy.bind(websocket._socket), + closeTimeout + ); +} + /** * The listener of the socket `'close'` event. * diff --git a/test/receiver.test.js b/test/receiver.test.js index 1f9e75d3a..243a91606 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -7,7 +7,7 @@ const EventEmitter = require('events'); const PerMessageDeflate = require('../lib/permessage-deflate'); const Receiver = require('../lib/receiver'); const Sender = require('../lib/sender'); -const { EMPTY_BUFFER, kStatusCode } = require('../lib/constants'); +const { EMPTY_BUFFER, hasBlob, kStatusCode } = require('../lib/constants'); describe('Receiver', () => { it('parses an unmasked text message', (done) => { @@ -1061,6 +1061,44 @@ describe('Receiver', () => { }); }); + it("honors the 'blob' binary type", function (done) { + if (!hasBlob) return this.skip(); + + const receiver = new Receiver({ binaryType: 'blob' }); + const frags = [ + crypto.randomBytes(75688), + crypto.randomBytes(2688), + crypto.randomBytes(46753) + ]; + + receiver.on('message', (data, isBinary) => { + assert.ok(data instanceof Blob); + assert.ok(isBinary); + + data + .arrayBuffer() + .then((arrayBuffer) => { + assert.deepStrictEqual( + Buffer.from(arrayBuffer), + Buffer.concat(frags) + ); + + done(); + }) + .catch(done); + }); + + frags.forEach((frag, i) => { + Sender.frame(frag, { + fin: i === frags.length - 1, + opcode: i === 0 ? 2 : 0, + readOnly: true, + mask: false, + rsv1: false + }).forEach((buf) => receiver.write(buf)); + }); + }); + it('honors the `skipUTF8Validation` option (1/2)', (done) => { const receiver = new Receiver({ skipUTF8Validation: true }); diff --git a/test/sender.test.js b/test/sender.test.js index 532239fa1..df9057e8a 100644 --- a/test/sender.test.js +++ b/test/sender.test.js @@ -5,7 +5,7 @@ const assert = require('assert'); const extension = require('../lib/extension'); const PerMessageDeflate = require('../lib/permessage-deflate'); const Sender = require('../lib/sender'); -const { EMPTY_BUFFER } = require('../lib/constants'); +const { EMPTY_BUFFER, hasBlob } = require('../lib/constants'); class MockSocket { constructor({ write } = {}) { @@ -250,17 +250,15 @@ describe('Sender', () => { }); describe('#ping', () => { - it('works with multiple types of data', (done) => { + it('can send a string as ping payload', (done) => { const perMessageDeflate = new PerMessageDeflate(); let count = 0; const mockSocket = new MockSocket({ write: (data) => { if (++count < 3) return; - if (count % 2) { - assert.ok(data.equals(Buffer.from([0x89, 0x02]))); - } else if (count < 8) { - assert.ok(data.equals(Buffer.from([0x68, 0x69]))); + if (count === 3) { + assert.deepStrictEqual(data, Buffer.from([0x89, 0x02])); } else { assert.strictEqual(data, 'hi'); done(); @@ -273,27 +271,81 @@ describe('Sender', () => { perMessageDeflate.accept([{}]); + sender.send('foo', { compress: true, fin: true }); + sender.ping('hi', false); + }); + + it('can send a `TypedArray` as ping payload', (done) => { + let count = 0; + const mockSocket = new MockSocket({ + write: (data) => { + if (++count === 1) { + assert.deepStrictEqual(data, Buffer.from([0x89, 0x02])); + } else { + assert.deepStrictEqual(data, Buffer.from([0x68, 0x69])); + done(); + } + } + }); + + const sender = new Sender(mockSocket); const array = new Uint8Array([0x68, 0x69]); - sender.send('foo', { compress: true, fin: true }); - sender.ping(array.buffer, false); sender.ping(array, false); - sender.ping('hi', false); + }); + + it('can send an `ArrayBuffer` as ping payload', (done) => { + let count = 0; + const mockSocket = new MockSocket({ + write: (data) => { + if (++count === 1) { + assert.deepStrictEqual(data, Buffer.from([0x89, 0x02])); + } else { + assert.deepStrictEqual(data, Buffer.from([0x68, 0x69])); + done(); + } + } + }); + + const sender = new Sender(mockSocket); + const array = new Uint8Array([0x68, 0x69]); + + sender.ping(array.buffer, false); + }); + + it('can send a `Blob` as ping payload', function (done) { + if (!hasBlob) return this.skip(); + + let count = 0; + const mockSocket = new MockSocket({ + write: (data) => { + if (++count % 2) { + assert.deepStrictEqual(data, Buffer.from([0x89, 0x02])); + } else { + assert.deepStrictEqual(data, Buffer.from([0x68, 0x69])); + if (count === 4) done(); + } + } + }); + + const sender = new Sender(mockSocket); + const blob = new Blob(['hi']); + + sender.ping(blob, false); + sender.ping(blob, false); }); }); describe('#pong', () => { - it('works with multiple types of data', (done) => { + it('can send a string as ping payload', (done) => { const perMessageDeflate = new PerMessageDeflate(); let count = 0; const mockSocket = new MockSocket({ write: (data) => { if (++count < 3) return; - if (count % 2) { - assert.ok(data.equals(Buffer.from([0x8a, 0x02]))); - } else if (count < 8) { - assert.ok(data.equals(Buffer.from([0x68, 0x69]))); + if (count === 3) { + assert.deepStrictEqual(data, Buffer.from([0x8a, 0x02])); } else { assert.strictEqual(data, 'hi'); done(); @@ -306,12 +358,68 @@ describe('Sender', () => { perMessageDeflate.accept([{}]); + sender.send('foo', { compress: true, fin: true }); + sender.pong('hi', false); + }); + + it('can send a `TypedArray` as ping payload', (done) => { + let count = 0; + const mockSocket = new MockSocket({ + write: (data) => { + if (++count === 1) { + assert.deepStrictEqual(data, Buffer.from([0x8a, 0x02])); + } else { + assert.deepStrictEqual(data, Buffer.from([0x68, 0x69])); + done(); + } + } + }); + + const sender = new Sender(mockSocket); const array = new Uint8Array([0x68, 0x69]); - sender.send('foo', { compress: true, fin: true }); - sender.pong(array.buffer, false); sender.pong(array, false); - sender.pong('hi', false); + }); + + it('can send an `ArrayBuffer` as ping payload', (done) => { + let count = 0; + const mockSocket = new MockSocket({ + write: (data) => { + if (++count === 1) { + assert.deepStrictEqual(data, Buffer.from([0x8a, 0x02])); + } else { + assert.deepStrictEqual(data, Buffer.from([0x68, 0x69])); + done(); + } + } + }); + + const sender = new Sender(mockSocket); + const array = new Uint8Array([0x68, 0x69]); + + sender.pong(array.buffer, false); + }); + + it('can send a `Blob` as ping payload', function (done) { + if (!hasBlob) return this.skip(); + + let count = 0; + const mockSocket = new MockSocket({ + write: (data) => { + if (++count % 2) { + assert.deepStrictEqual(data, Buffer.from([0x8a, 0x02])); + } else { + assert.deepStrictEqual(data, Buffer.from([0x68, 0x69])); + if (count === 4) done(); + } + } + }); + + const sender = new Sender(mockSocket); + const blob = new Blob(['hi']); + + sender.pong(blob, false); + sender.pong(blob, false); }); }); diff --git a/test/websocket.test.js b/test/websocket.test.js index 8a05f073b..811a3e15b 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -22,7 +22,13 @@ const { Event, MessageEvent } = require('../lib/event-target'); -const { EMPTY_BUFFER, GUID, kListener, NOOP } = require('../lib/constants'); +const { + EMPTY_BUFFER, + GUID, + hasBlob, + kListener, + NOOP +} = require('../lib/constants'); const highWaterMark = getDefaultHighWaterMark ? getDefaultHighWaterMark(false) @@ -617,7 +623,7 @@ describe('WebSocket', () => { }); describe('Events', () => { - it("emits an 'error' event if an error occurs", (done) => { + it("emits an 'error' event if an error occurs (1/2)", (done) => { let clientCloseEventEmitted = false; let serverClientCloseEventEmitted = false; @@ -655,6 +661,200 @@ describe('WebSocket', () => { }); }); + it("emits an 'error' event if an error occurs (2/2)", function (done) { + if (!fs.openAsBlob) return this.skip(); + + const randomString = crypto.randomBytes(16).toString('hex'); + const file = path.join(os.tmpdir(), `ws-${randomString}.txt`); + + fs.writeFileSync(file, 'x'.repeat(64)); + + fs.openAsBlob(file) + .then((blob) => { + fs.writeFileSync(file, 'x'.repeat(32)); + runTest(blob); + }) + .catch(done); + + function runTest(blob) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + ws.send(blob); + + ws.on('error', (err) => { + try { + assert.ok(err instanceof DOMException); + assert.strictEqual(err.name, 'NotReadableError'); + assert.strictEqual(err.message, 'The blob could not be read'); + } finally { + fs.unlinkSync(file); + } + + ws.on('close', () => { + wss.close(done); + }); + }); + }); + } + }); + + it("emits the 'error' event only once (1/2)", function (done) { + if (!fs.openAsBlob) return this.skip(); + + const randomString = crypto.randomBytes(16).toString('hex'); + const file = path.join(os.tmpdir(), `ws-${randomString}.txt`); + + fs.writeFileSync(file, 'x'.repeat(64)); + + fs.openAsBlob(file) + .then((blob) => { + fs.writeFileSync(file, 'x'.repeat(32)); + runTest(blob); + }) + .catch(done); + + function runTest(blob) { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws.send('foo'); + ws.send(blob); + }); + + ws.on('error', (err) => { + try { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + } finally { + fs.unlinkSync(file); + } + + ws.on('close', () => { + wss.close(done); + }); + }); + } + ); + + wss.on('connection', (ws) => { + ws._socket.write(Buffer.from([0x85, 0x00])); + }); + } + }); + + it("emits the 'error' event only once (2/2)", function (done) { + if (!fs.openAsBlob) return this.skip(); + + const randomString = crypto.randomBytes(16).toString('hex'); + const file = path.join(os.tmpdir(), `ws-${randomString}.txt`); + + fs.writeFileSync(file, 'x'.repeat(64)); + + fs.openAsBlob(file) + .then((blob) => { + fs.writeFileSync(file, 'x'.repeat(32)); + runTest(blob); + }) + .catch(done); + + function runTest(blob) { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.send(blob); + }); + + ws.on('error', (err) => { + try { + assert.ok(err instanceof DOMException); + assert.strictEqual(err.name, 'NotReadableError'); + assert.strictEqual(err.message, 'The blob could not be read'); + } finally { + fs.unlinkSync(file); + } + + ws.on('close', () => { + wss.close(done); + }); + }); + } + ); + + wss.on('connection', (ws) => { + const buf = Buffer.from('c10100'.repeat(5) + '8500', 'hex'); + + ws._socket.write(buf); + }); + } + }); + + it("does not emit 'error' after 'close'", function (done) { + if (!fs.openAsBlob) return this.skip(); + + const randomString = crypto.randomBytes(16).toString('hex'); + const file = path.join(os.tmpdir(), `ws-${randomString}.bin`); + + fs.writeFileSync(file, crypto.randomBytes(1024 * 1024)); + fs.openAsBlob(file).then(runTest).catch(done); + + function runTest(blob) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.send(blob, (err) => { + try { + assert.ok(err instanceof DOMException); + assert.strictEqual(err.name, 'NotReadableError'); + assert.strictEqual(err.message, 'The blob could not be read'); + } catch (e) { + ws.removeListener(onClose); + throw e; + } finally { + fs.unlinkSync(file); + } + + wss.close(done); + }); + }); + + ws.on('error', () => { + done(new Error("Unexpected 'error' event")); + }); + ws.on('close', onClose); + + function onClose() { + fs.writeFileSync(file, crypto.randomBytes(32)); + } + }); + + wss.on('connection', (ws) => { + ws._socket.end(); + }); + } + }); + it('does not re-emit `net.Socket` errors', function (done) { // // `socket.resetAndDestroy()` is not available in Node.js < 16.17.0. @@ -2120,6 +2320,11 @@ describe('WebSocket', () => { ws.ping(); assert.strictEqual(ws.bufferedAmount, 4); + if (hasBlob) { + ws.ping(new Blob(['hi'])); + assert.strictEqual(ws.bufferedAmount, 6); + } + done(); }); }); @@ -2289,6 +2494,11 @@ describe('WebSocket', () => { ws.pong(); assert.strictEqual(ws.bufferedAmount, 4); + if (hasBlob) { + ws.pong(new Blob(['hi'])); + assert.strictEqual(ws.bufferedAmount, 6); + } + done(); }); }); @@ -2533,6 +2743,11 @@ describe('WebSocket', () => { ws.send(); assert.strictEqual(ws.bufferedAmount, 4); + if (hasBlob) { + ws.send(new Blob(['hi'])); + assert.strictEqual(ws.bufferedAmount, 6); + } + done(); }); }); @@ -2750,6 +2965,39 @@ describe('WebSocket', () => { }); }); + it('can send a `Blob`', function (done) { + if (!hasBlob) return this.skip(); + + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + const messages = []; + + ws.on('open', () => { + ws.send(new Blob(['foo'])); + ws.send(new Blob(['bar'])); + ws.close(); + }); + + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + messages.push(message.toString()); + + if (messages.length === 2) { + assert.deepStrictEqual(messages, ['foo', 'bar']); + wss.close(done); + } + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + ws.send(message); + }); + }); + }); + it('calls the callback when data is written out', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); @@ -2767,6 +3015,48 @@ describe('WebSocket', () => { }); }); + it('calls the callback if the socket is forcibly closed', function (done) { + if (!hasBlob) return this.skip(); + + const called = []; + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.send(new Blob(['foo']), (err) => { + called.push(1); + + assert.strictEqual(ws.readyState, WebSocket.CLOSING); + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'The socket was closed while the blob was being read' + ); + }); + ws.send('bar'); + ws.send('baz', (err) => { + called.push(2); + + assert.strictEqual(ws.readyState, WebSocket.CLOSING); + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'The socket was closed while the blob was being read' + ); + }); + + ws.terminate(); + }); + }); + + wss.on('connection', (ws) => { + ws.on('close', () => { + assert.deepStrictEqual(called, [1, 2]); + wss.close(done); + }); + }); + }); + it('works when the `data` argument is falsy', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); @@ -3671,25 +3961,41 @@ describe('WebSocket', () => { ws.onmessage = (evt) => { if (binaryType === 'nodebuffer') { assert.ok(Buffer.isBuffer(evt.data)); - assert.ok(evt.data.equals(buf)); + assert.deepStrictEqual(evt.data, buf); + next(); } else if (binaryType === 'arraybuffer') { assert.ok(evt.data instanceof ArrayBuffer); - assert.ok(Buffer.from(evt.data).equals(buf)); + assert.deepStrictEqual(Buffer.from(evt.data), buf); + next(); } else if (binaryType === 'fragments') { assert.deepStrictEqual(evt.data, [buf]); + next(); + } else if (binaryType === 'blob') { + assert.ok(evt.data instanceof Blob); + evt.data + .arrayBuffer() + .then((arrayBuffer) => { + assert.deepStrictEqual(Buffer.from(arrayBuffer), buf); + next(); + }) + .catch(done); } - next(); }; ws.send(buf); } + function close() { + ws.close(); + wss.close(done); + } + ws.onopen = () => { testType('nodebuffer', () => { testType('arraybuffer', () => { testType('fragments', () => { - ws.close(); - wss.close(done); + if (hasBlob) testType('blob', close); + else close(); }); }); }); @@ -4214,7 +4520,7 @@ describe('WebSocket', () => { ws.on('open', () => { ws._receiver.on('conclude', () => { - assert.ok(ws._sender._deflating); + assert.strictEqual(ws._sender._state, 1); }); ws.send('foo'); @@ -4392,6 +4698,47 @@ describe('WebSocket', () => { }); }); + it('can send a `Blob`', function (done) { + if (!hasBlob) return this.skip(); + + const wss = new WebSocket.Server( + { + perMessageDeflate: { threshold: 0 }, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + const messages = []; + + ws.on('open', () => { + ws.send(new Blob(['foo'])); + ws.send(new Blob(['bar'])); + ws.close(); + }); + + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + messages.push(message.toString()); + + if (messages.length === 2) { + assert.deepStrictEqual(messages, ['foo', 'bar']); + wss.close(done); + } + }); + } + ); + + wss.on('connection', (ws) => { + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + ws.send(message); + }); + }); + }); + it('ignores the `compress` option if the extension is disabled', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { From 976c53c4065c49ede73bfba824caf5a6e0f290cb Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 3 Jul 2024 18:36:56 +0200 Subject: [PATCH 271/328] [dist] 8.18.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4abcf2989..4f7155deb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.17.1", + "version": "8.18.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From f7dc46971768b9678b4a255f9f3e202ab203135e Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 4 Jul 2024 07:48:31 +0200 Subject: [PATCH 272/328] [doc] Fix the type of the `data` argument --- doc/ws.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index f30ad4cae..2fbdbb79d 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -395,7 +395,7 @@ of the string values defined below under [Error codes](#error-codes). ### Event: 'message' -- `data` {Buffer|ArrayBuffer|Buffer[]} +- `data` {ArrayBuffer|Blob|Buffer|Buffer[]} - `isBinary` {Boolean} Emitted when a message is received. `data` is the message content. `isBinary` From bfe1b2a623eb881a107ccf9a95042e8b22933687 Mon Sep 17 00:00:00 2001 From: yuhao <76569582+yuhao423@users.noreply.github.com> Date: Fri, 5 Jul 2024 01:37:58 +0800 Subject: [PATCH 273/328] [doc] Remove unnecessary period (#2240) --- doc/ws.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index 2fbdbb79d..a1a20203f 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -257,7 +257,7 @@ receives an `Error` if the server is already closed. - `request` {http.IncomingMessage} The client HTTP GET request. - `socket` {stream.Duplex} The network socket between the server and client. - `head` {Buffer} The first packet of the upgraded stream. -- `callback` {Function}. +- `callback` {Function} Handle a HTTP upgrade request. When the HTTP server is created internally or when the HTTP server is passed via the `server` option, this method is called From 019f28ff1ffddfcdc428d1de5ecd98648057a2ab Mon Sep 17 00:00:00 2001 From: Isaiah Thomason <47364027+ITenthusiasm@users.noreply.github.com> Date: Sun, 14 Jul 2024 09:20:58 -0400 Subject: [PATCH 274/328] [minor] Improve JSDoc-inferred types (#2242) --- lib/sender.js | 2 +- lib/stream.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/sender.js b/lib/sender.js index ee16cea5a..a8b1da3a9 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -551,7 +551,7 @@ class Sender { /** * Sends a frame. * - * @param {Buffer[]} list The frame to send + * @param {(Buffer | String)[]} list The frame to send * @param {Function} [cb] Callback * @private */ diff --git a/lib/stream.js b/lib/stream.js index 230734b79..4c58c911b 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -1,5 +1,7 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^WebSocket$" }] */ 'use strict'; +const WebSocket = require('./websocket'); const { Duplex } = require('stream'); /** From 68614728c164eb55462ff12ba24cb30451d28ec6 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 26 Oct 2024 17:47:34 +0200 Subject: [PATCH 275/328] [ci] Test on node 23 --- .github/workflows/ci.yml | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04693fc7e..db0bd21a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,6 @@ jobs: matrix: arch: - x64 - - x86 node: - 10 - 12 @@ -22,17 +21,29 @@ jobs: - 18 - 20 - 22 + - 23 os: - macOS-latest - ubuntu-latest - windows-latest - exclude: + include: - arch: x86 - os: macOS-latest + node: 10 + os: windows-latest + - arch: x86 + node: 12 + os: windows-latest + - arch: x86 + node: 14 + os: windows-latest + - arch: x86 + node: 16 + os: windows-latest - arch: x86 - os: ubuntu-latest + node: 20 + os: windows-latest - arch: x86 - node: 18 + node: 22 os: windows-latest steps: - uses: actions/checkout@v4 @@ -40,7 +51,7 @@ jobs: with: node-version: ${{ matrix.node }} architecture: ${{ matrix.arch }} - cache: 'npm' + cache: npm cache-dependency-path: ./package.json - run: npm install - run: npm run lint From c798dd4ee20efb2d7591b5659839ad05cdb3eb70 Mon Sep 17 00:00:00 2001 From: Syed Sadiq Ali <69423208+SadiqOnGithub@users.noreply.github.com> Date: Thu, 2 Jan 2025 00:43:35 +0530 Subject: [PATCH 276/328] [doc] Fix typo (#2271) --- doc/ws.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index a1a20203f..087cb9ba3 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -77,7 +77,7 @@ This class represents a WebSocket server. It extends the `EventEmitter`. - `allowSynchronousEvents` {Boolean} Specifies whether any of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple times in the same tick. Defaults to `true`. Setting it to `false` improves compatibility with - the WHATWG standardbut may negatively impact performance. + the WHATWG standard but may negatively impact performance. - `backlog` {Number} The maximum length of the queue of pending connections. - `clientTracking` {Boolean} Specifies whether or not to track clients. - `handleProtocols` {Function} A function which can be used to handle the From b9ca55b0aa8c72b39a778542bd0fa9b6c455d4c4 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 14 Jan 2025 14:29:04 +0100 Subject: [PATCH 277/328] [pkg] Update eslint-config-prettier to version 10.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f7155deb..82c4bb150 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "benchmark": "^2.1.4", "bufferutil": "^4.0.1", "eslint": "^9.0.0", - "eslint-config-prettier": "^9.0.0", + "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.0.0", "globals": "^15.0.0", "mocha": "^8.4.0", From 021f7b8bff3233a607b74937bc73a224ed8b2951 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 20 Feb 2025 12:51:27 +0100 Subject: [PATCH 278/328] [test] Shorten the path lengths Shorten the length of temporary file and UNIX domain socket paths. Refs: https://github.com/nodejs/node/pull/57005#issuecomment-2666921592 --- test/websocket-server.test.js | 2 +- test/websocket.test.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 34de4dcfa..98168edd2 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -204,7 +204,7 @@ describe('WebSocketServer', () => { }); it('uses a precreated http server listening on IPC', (done) => { - const randomString = crypto.randomBytes(16).toString('hex'); + const randomString = crypto.randomBytes(4).toString('hex'); const ipcPath = process.platform === 'win32' ? `\\\\.\\pipe\\ws-pipe-${randomString}` diff --git a/test/websocket.test.js b/test/websocket.test.js index 811a3e15b..bd264d9be 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -664,7 +664,7 @@ describe('WebSocket', () => { it("emits an 'error' event if an error occurs (2/2)", function (done) { if (!fs.openAsBlob) return this.skip(); - const randomString = crypto.randomBytes(16).toString('hex'); + const randomString = crypto.randomBytes(4).toString('hex'); const file = path.join(os.tmpdir(), `ws-${randomString}.txt`); fs.writeFileSync(file, 'x'.repeat(64)); @@ -704,7 +704,7 @@ describe('WebSocket', () => { it("emits the 'error' event only once (1/2)", function (done) { if (!fs.openAsBlob) return this.skip(); - const randomString = crypto.randomBytes(16).toString('hex'); + const randomString = crypto.randomBytes(4).toString('hex'); const file = path.join(os.tmpdir(), `ws-${randomString}.txt`); fs.writeFileSync(file, 'x'.repeat(64)); @@ -760,7 +760,7 @@ describe('WebSocket', () => { it("emits the 'error' event only once (2/2)", function (done) { if (!fs.openAsBlob) return this.skip(); - const randomString = crypto.randomBytes(16).toString('hex'); + const randomString = crypto.randomBytes(4).toString('hex'); const file = path.join(os.tmpdir(), `ws-${randomString}.txt`); fs.writeFileSync(file, 'x'.repeat(64)); @@ -812,7 +812,7 @@ describe('WebSocket', () => { it("does not emit 'error' after 'close'", function (done) { if (!fs.openAsBlob) return this.skip(); - const randomString = crypto.randomBytes(16).toString('hex'); + const randomString = crypto.randomBytes(4).toString('hex'); const file = path.join(os.tmpdir(), `ws-${randomString}.bin`); fs.writeFileSync(file, crypto.randomBytes(1024 * 1024)); @@ -1866,7 +1866,7 @@ describe('WebSocket', () => { it('drops the Authorization, Cookie and Host headers (2/4)', (done) => { // Test the `ws:` to `ws+unix:` case. - const randomString = crypto.randomBytes(16).toString('hex'); + const randomString = crypto.randomBytes(4).toString('hex'); const ipcPath = process.platform === 'win32' ? `\\\\.\\pipe\\ws-pipe-${randomString}` @@ -1926,8 +1926,8 @@ describe('WebSocket', () => { it('drops the Authorization, Cookie and Host headers (3/4)', (done) => { // Test the `ws+unix:` to `ws+unix:` case. - const randomString1 = crypto.randomBytes(16).toString('hex'); - const randomString2 = crypto.randomBytes(16).toString('hex'); + const randomString1 = crypto.randomBytes(4).toString('hex'); + const randomString2 = crypto.randomBytes(4).toString('hex'); let redirectingServerIpcPath; let redirectedServerIpcPath; @@ -2025,7 +2025,7 @@ describe('WebSocket', () => { ws.close(); }); - const randomString = crypto.randomBytes(16).toString('hex'); + const randomString = crypto.randomBytes(4).toString('hex'); const ipcPath = process.platform === 'win32' ? `\\\\.\\pipe\\ws-pipe-${randomString}` From b3d97470ce3dea64be4a7aceeaa1ce121ea2b496 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 20 Feb 2025 14:12:01 +0100 Subject: [PATCH 279/328] [doc] Fix nit --- doc/ws.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index 087cb9ba3..feda79560 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -365,7 +365,7 @@ URL form: ws+unix:\\.\pipe\pipe_name:/pathname?search_params ``` -The character `:` is the separator between the IPC path (the Unix domain socket +The character `:` is the separator between the IPC path (the UNIX domain socket path or the Windows named pipe) and the URL path. The IPC path must not include the characters `:` and `?`, otherwise the URL is incorrectly parsed. If the URL path is omitted From b92745a9d6760e6b4b2394bfac78cbcd258a8c8d Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 21 Feb 2025 10:07:39 +0100 Subject: [PATCH 280/328] [dist] 8.18.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 82c4bb150..6056c0054 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.18.0", + "version": "8.18.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From cf259541e8c2c5a57b6d343b681a33447570f67b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 25 Mar 2025 15:59:48 +0100 Subject: [PATCH 281/328] [minor] Fix nit in error message Closes #2276 --- lib/websocket.js | 2 +- test/websocket.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 7fb402970..ad8764a02 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -708,7 +708,7 @@ function initAsClient(websocket, address, protocols, options) { if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { invalidUrlMessage = 'The URL\'s protocol must be one of "ws:", "wss:", ' + - '"http:", "https", or "ws+unix:"'; + '"http:", "https:", or "ws+unix:"'; } else if (isIpcUrl && !parsedUrl.pathname) { invalidUrlMessage = "The URL's pathname is empty"; } else if (parsedUrl.hash) { diff --git a/test/websocket.test.js b/test/websocket.test.js index bd264d9be..32b9d1b5c 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -52,7 +52,7 @@ describe('WebSocket', () => { assert.strictEqual( err.message, 'The URL\'s protocol must be one of "ws:", "wss:", ' + - '"http:", "https", or "ws+unix:"' + '"http:", "https:", or "ws+unix:"' ); return true; @@ -1515,7 +1515,7 @@ describe('WebSocket', () => { assert.strictEqual( err.message, 'The URL\'s protocol must be one of "ws:", "wss:", ' + - '"http:", "https", or "ws+unix:"' + '"http:", "https:", or "ws+unix:"' ); assert.strictEqual(ws._redirects, 1); From aa998e38c5f62844eb2fb1ccccb69a9c953ccd4f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 5 Apr 2025 12:05:50 +0200 Subject: [PATCH 282/328] [pkg] Update globals to version 16.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6056c0054..e68d288e5 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "eslint": "^9.0.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.0.0", - "globals": "^15.0.0", + "globals": "^16.0.0", "mocha": "^8.4.0", "nyc": "^15.0.0", "prettier": "^3.0.0", From 4f20aedbe2c6acba5e4361fcc5e694e1cdb4430c Mon Sep 17 00:00:00 2001 From: Amirali Amirifar Date: Fri, 2 May 2025 16:33:02 +0330 Subject: [PATCH 283/328] [fix] Handle oversized messages with designated error (#2285) Ensure that the correct error is surfaced when the max message size is exceeded. --- lib/permessage-deflate.js | 14 ++++++++++++++ test/permessage-deflate.test.js | 22 ++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index 77d918b55..41ff70e27 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -494,6 +494,14 @@ function inflateOnData(chunk) { this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; this[kError][kStatusCode] = 1009; this.removeListener('data', inflateOnData); + + // + // The choice to employ `zlib.reset()` over `zlib.close()` is dictated by the + // fact that in Node.js versions prior to 13.10.0, the callback for + // `zlib.flush()` is not called if `zlib.close()` is used. Utilizing + // `zlib.reset()` ensures that either the callback is invoked or an error is + // emitted. + // this.reset(); } @@ -509,6 +517,12 @@ function inflateOnError(err) { // closed when an error is emitted. // this[kPerMessageDeflate]._inflate = null; + + if (this[kError]) { + this[kCallback](this[kError]); + return; + } + err[kStatusCode] = 1007; this[kCallback](err); } diff --git a/test/permessage-deflate.test.js b/test/permessage-deflate.test.js index a9c9bf165..cf795b799 100644 --- a/test/permessage-deflate.test.js +++ b/test/permessage-deflate.test.js @@ -590,9 +590,27 @@ describe('PerMessageDeflate', () => { }); }); - it("doesn't call the callback twice when `maxPayload` is exceeded", (done) => { + it('calls the callback when `maxPayload` is exceeded (1/2)', (done) => { const perMessageDeflate = new PerMessageDeflate({}, false, 25); - const buf = Buffer.from('A'.repeat(50)); + const buf = Buffer.alloc(50, 'A'); + + perMessageDeflate.accept([{}]); + perMessageDeflate.compress(buf, true, (err, data) => { + if (err) return done(err); + + perMessageDeflate.decompress(data, true, (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.message, 'Max payload size exceeded'); + done(); + }); + }); + }); + + it('calls the callback when `maxPayload` is exceeded (2/2)', (done) => { + // A copy of the previous test but with a larger input. See + // https://github.com/websockets/ws/pull/2285. + const perMessageDeflate = new PerMessageDeflate({}, false, 25); + const buf = Buffer.alloc(1024 * 1024, 'A'); perMessageDeflate.accept([{}]); perMessageDeflate.compress(buf, true, (err, data) => { From 0eb8535f9b0dc11ac30a9ccb8824c9fc8388f1fd Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 2 May 2025 20:58:15 +0200 Subject: [PATCH 284/328] [dist] 8.18.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e68d288e5..90ffff457 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.18.1", + "version": "8.18.2", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From fa670f2b240afba6bffb3929e90ea41cd5e9d072 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 13 May 2025 21:20:33 +0200 Subject: [PATCH 285/328] [ci] Run the lint step on node 22 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db0bd21a3..22fc88c85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: - run: npm install - run: npm run lint if: - matrix.os == 'ubuntu-latest' && matrix.node == 20 && matrix.arch == + matrix.os == 'ubuntu-latest' && matrix.node == 22 && matrix.arch == 'x64' - run: npm test - run: | From e67eb7ad70e7e5bc2e31ded42460793ae02feb82 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 13 May 2025 21:21:07 +0200 Subject: [PATCH 286/328] [ci] Do not test on node 23 --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22fc88c85..aa2c591e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,6 @@ jobs: - 18 - 20 - 22 - - 23 os: - macOS-latest - ubuntu-latest From 22a5a17db5fef470725f1b2f72342eccb4262662 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 13 May 2025 21:21:39 +0200 Subject: [PATCH 287/328] [ci] Test on node 24 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa2c591e9..bae231be3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: - 18 - 20 - 22 + - 24 os: - macOS-latest - ubuntu-latest From 33f5dbaf332764530a6ca97a93101b4402f3121d Mon Sep 17 00:00:00 2001 From: yusheng chen Date: Sat, 28 Jun 2025 21:00:23 +0800 Subject: [PATCH 288/328] [fix] Respond with the supported protocol versions (#2291) Add the `Sec-WebSocket-Version` header, listing all supported versions, to the response if the client requested version is either invalid or unacceptable. --- lib/websocket-server.js | 18 ++++++++++++++---- test/websocket-server.test.js | 2 ++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 67b52ffdd..33e09858c 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -256,9 +256,11 @@ class WebSocketServer extends EventEmitter { return; } - if (version !== 8 && version !== 13) { + if (version !== 13 && version !== 8) { const message = 'Missing or invalid Sec-WebSocket-Version header'; - abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message, { + 'Sec-WebSocket-Version': '13, 8' + }); return; } @@ -526,15 +528,23 @@ function abortHandshake(socket, code, message, headers) { * @param {Duplex} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} message The HTTP response body + * @param {Object} [headers] The HTTP response headers * @private */ -function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) { +function abortHandshakeOrEmitwsClientError( + server, + req, + socket, + code, + message, + headers +) { if (server.listenerCount('wsClientError')) { const err = new Error(message); Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); server.emit('wsClientError', err, socket, req); } else { - abortHandshake(socket, code, message); + abortHandshake(socket, code, message, headers); } } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 98168edd2..db8d62da5 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -813,6 +813,7 @@ describe('WebSocketServer', () => { req.on('response', (res) => { assert.strictEqual(res.statusCode, 400); + assert.strictEqual(res.headers['sec-websocket-version'], '13, 8'); const chunks = []; @@ -849,6 +850,7 @@ describe('WebSocketServer', () => { req.on('response', (res) => { assert.strictEqual(res.statusCode, 400); + assert.strictEqual(res.headers['sec-websocket-version'], '13, 8'); const chunks = []; From dabbdec92f4c1f1777689733d477344e3c6c2e67 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 28 Jun 2025 15:03:00 +0200 Subject: [PATCH 289/328] [dist] 8.18.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90ffff457..2004b1c21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.18.2", + "version": "8.18.3", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From aa28c779041b5951058e8edd73649d20de56d139 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 4 Sep 2025 21:21:46 +0200 Subject: [PATCH 290/328] [ci] Update actions/checkout action to v5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bae231be3..b8cbb9a6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: node: 22 os: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} From 1891e1474f0c9279459ce49e448867f1a46bc0a4 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 4 Sep 2025 21:22:08 +0200 Subject: [PATCH 291/328] [ci] Update actions/setup-node action to v5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8cbb9a6d..2c953133e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: os: windows-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: ${{ matrix.node }} architecture: ${{ matrix.arch }} From 86eac5b44ac2bff9087ec40c9bd06bc7b4f0da07 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 20 Oct 2025 09:13:06 +0200 Subject: [PATCH 292/328] [ci] Test on node 25 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c953133e..86872f551 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ jobs: - 20 - 22 - 24 + - 25 os: - macOS-latest - ubuntu-latest From dabdd5b3018dd05792627a53517dfdb58ae19824 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 3 Nov 2025 20:39:08 +0100 Subject: [PATCH 293/328] [ci] Update actions/setup-node action to v6 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86872f551..ae3c8f5c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: os: windows-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} architecture: ${{ matrix.arch }} From b151f1e8a3d1742756d2620198bdce8cff863e1c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 2 Dec 2025 14:08:11 +0100 Subject: [PATCH 294/328] [ci] Update actions/checkout action to v6 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae3c8f5c1..538662f24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: node: 22 os: windows-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} From 726c3732b3e5319219ed73cac4826fd36917e2e1 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 18 Dec 2025 10:13:29 +0100 Subject: [PATCH 295/328] [doc] Sort options alphabetically --- doc/ws.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index feda79560..04c785feb 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -72,12 +72,12 @@ This class represents a WebSocket server. It extends the `EventEmitter`. ### new WebSocketServer(options[, callback]) - `options` {Object} - - `autoPong` {Boolean} Specifies whether or not to automatically send a pong - in response to a ping. Defaults to `true`. - `allowSynchronousEvents` {Boolean} Specifies whether any of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple times in the same tick. Defaults to `true`. Setting it to `false` improves compatibility with the WHATWG standard but may negatively impact performance. + - `autoPong` {Boolean} Specifies whether or not to automatically send a pong + in response to a ping. Defaults to `true`. - `backlog` {Number} The maximum length of the queue of pending connections. - `clientTracking` {Boolean} Specifies whether or not to track clients. - `handleProtocols` {Function} A function which can be used to handle the @@ -298,12 +298,12 @@ This class represents a WebSocket. It extends the `EventEmitter`. - `address` {String|url.URL} The URL to which to connect. - `protocols` {String|Array} The list of subprotocols. - `options` {Object} - - `autoPong` {Boolean} Specifies whether or not to automatically send a pong - in response to a ping. Defaults to `true`. - `allowSynchronousEvents` {Boolean} Specifies whether any of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple times in the same tick. Defaults to `true`. Setting it to `false` improves compatibility with the WHATWG standardbut may negatively impact performance. + - `autoPong` {Boolean} Specifies whether or not to automatically send a pong + in response to a ping. Defaults to `true`. - `finishRequest` {Function} A function which can be used to customize the headers of each HTTP request before it is sent. See description below. - `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to From 19984854104e637a21cbc080272ac63d3d60a273 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 2 Jan 2026 17:25:11 +0100 Subject: [PATCH 296/328] [fix] Ensure all remaining data is read as a single chunk Do not rely on undocumented behavior, call `socket.read()` with the actual number of bytes to read. Refs: https://github.com/nodejs/node/pull/60441 --- lib/websocket.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index ad8764a02..43c517992 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -1308,23 +1308,23 @@ function socketOnClose() { websocket._readyState = WebSocket.CLOSING; - let chunk; - // // The close frame might not have been received or the `'end'` event emitted, // for example, if the socket was destroyed due to an error. Ensure that the // `receiver` stream is closed after writing any remaining buffered data to // it. If the readable side of the socket is in flowing mode then there is no - // buffered data as everything has been already written and `readable.read()` - // will return `null`. If instead, the socket is paused, any possible buffered - // data will be read as a single chunk. + // buffered data as everything has been already written. If instead, the + // socket is paused, any possible buffered data will be read as a single + // chunk. // if ( !this._readableState.endEmitted && !websocket._closeFrameReceived && !websocket._receiver._writableState.errorEmitted && - (chunk = websocket._socket.read()) !== null + this._readableState.length !== 0 ) { + const chunk = this.read(this._readableState.length); + websocket._receiver.write(chunk); } From 3f9ffc688f1172cd599e6fbd87a06e20044bd359 Mon Sep 17 00:00:00 2001 From: Anthony <46906167+anthony-michael-bennett@users.noreply.github.com> Date: Mon, 5 Jan 2026 02:24:13 -0800 Subject: [PATCH 297/328] [feature] Introduce the `closeTimeout` option (#2308) Make graceful close timeout configurable. --- doc/ws.md | 7 +++++++ lib/constants.js | 1 + lib/websocket-server.js | 6 +++++- lib/websocket.js | 9 +++++++-- test/websocket-server.test.js | 16 ++++++++++++++++ test/websocket.test.js | 18 ++++++++++++++++++ 6 files changed, 54 insertions(+), 3 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 04c785feb..7d22a0480 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -80,6 +80,9 @@ This class represents a WebSocket server. It extends the `EventEmitter`. in response to a ping. Defaults to `true`. - `backlog` {Number} The maximum length of the queue of pending connections. - `clientTracking` {Boolean} Specifies whether or not to track clients. + - `closeTimeout` {Number} Duration in milliseconds to wait for a graceful + close after [`websocket.close()`][] is called. If the limit is reached, the + connection is forcibly terminated. Defaults to 30000. - `handleProtocols` {Function} A function which can be used to handle the WebSocket subprotocols. See description below. - `host` {String} The hostname where to bind the server. @@ -304,6 +307,9 @@ This class represents a WebSocket. It extends the `EventEmitter`. the WHATWG standardbut may negatively impact performance. - `autoPong` {Boolean} Specifies whether or not to automatically send a pong in response to a ping. Defaults to `true`. + - `closeTimeout` {Number} Duration in milliseconds to wait for a graceful + close after [`websocket.close()`][] is called. If the limit is reached, the + connection is forcibly terminated. Defaults to 30000. - `finishRequest` {Function} A function which can be used to customize the headers of each HTTP request before it is sent. See description below. - `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to @@ -709,4 +715,5 @@ as configured by the `maxPayload` option. [`request.removeheader()`]: https://nodejs.org/api/http.html#requestremoveheadername [`socket.destroy()`]: https://nodejs.org/api/net.html#net_socket_destroy_error +[`websocket.close()`]: #websocketclosecode-reason [zlib-options]: https://nodejs.org/api/zlib.html#zlib_class_options diff --git a/lib/constants.js b/lib/constants.js index 74214d466..69b2fe3c4 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -7,6 +7,7 @@ if (hasBlob) BINARY_TYPES.push('blob'); module.exports = { BINARY_TYPES, + CLOSE_TIMEOUT: 30000, EMPTY_BUFFER: Buffer.alloc(0), GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', hasBlob, diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 33e09858c..75e04c1d6 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -11,7 +11,7 @@ const extension = require('./extension'); const PerMessageDeflate = require('./permessage-deflate'); const subprotocol = require('./subprotocol'); const WebSocket = require('./websocket'); -const { GUID, kWebSocket } = require('./constants'); +const { CLOSE_TIMEOUT, GUID, kWebSocket } = require('./constants'); const keyRegex = /^[+/0-9A-Za-z]{22}==$/; @@ -38,6 +38,9 @@ class WebSocketServer extends EventEmitter { * pending connections * @param {Boolean} [options.clientTracking=true] Specifies whether or not to * track clients + * @param {Number} [options.closeTimeout=30000] Duration in milliseconds to + * wait for the closing handshake to finish after `websocket.close()` is + * called * @param {Function} [options.handleProtocols] A hook to handle protocols * @param {String} [options.host] The hostname where to bind the server * @param {Number} [options.maxPayload=104857600] The maximum allowed message @@ -67,6 +70,7 @@ class WebSocketServer extends EventEmitter { perMessageDeflate: false, handleProtocols: null, clientTracking: true, + closeTimeout: CLOSE_TIMEOUT, verifyClient: null, noServer: false, backlog: null, // use default (511 as implemented in net.js) diff --git a/lib/websocket.js b/lib/websocket.js index 43c517992..0da2949cf 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -18,6 +18,7 @@ const { isBlob } = require('./validation'); const { BINARY_TYPES, + CLOSE_TIMEOUT, EMPTY_BUFFER, GUID, kForOnEventAttribute, @@ -32,7 +33,6 @@ const { const { format, parse } = require('./extension'); const { toBuffer } = require('./buffer-util'); -const closeTimeout = 30 * 1000; const kAborted = Symbol('kAborted'); const protocolVersions = [8, 13]; const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; @@ -88,6 +88,7 @@ class WebSocket extends EventEmitter { initAsClient(this, address, protocols, options); } else { this._autoPong = options.autoPong; + this._closeTimeout = options.closeTimeout; this._isServer = true; } } @@ -629,6 +630,8 @@ module.exports = WebSocket; * times in the same tick * @param {Boolean} [options.autoPong=true] Specifies whether or not to * automatically send a pong in response to a ping + * @param {Number} [options.closeTimeout=30000] Duration in milliseconds to wait + * for the closing handshake to finish after `websocket.close()` is called * @param {Function} [options.finishRequest] A function which can be used to * customize the headers of each http request before it is sent * @param {Boolean} [options.followRedirects=false] Whether or not to follow @@ -655,6 +658,7 @@ function initAsClient(websocket, address, protocols, options) { const opts = { allowSynchronousEvents: true, autoPong: true, + closeTimeout: CLOSE_TIMEOUT, protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, @@ -673,6 +677,7 @@ function initAsClient(websocket, address, protocols, options) { }; websocket._autoPong = opts.autoPong; + websocket._closeTimeout = opts.closeTimeout; if (!protocolVersions.includes(opts.protocolVersion)) { throw new RangeError( @@ -1290,7 +1295,7 @@ function senderOnError(err) { function setCloseTimer(websocket) { websocket._closeTimer = setTimeout( websocket._socket.destroy.bind(websocket._socket), - closeTimeout + websocket._closeTimeout ); } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index db8d62da5..4d5201735 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -140,6 +140,22 @@ describe('WebSocketServer', () => { }); }); }); + + it('honors the `closeTimeout` option', (done) => { + const closeTimeout = 1000; + const wss = new WebSocket.Server({ closeTimeout, port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + ws.on('close', () => { + wss.close(done); + }); + + ws.close(); + assert.strictEqual(ws._closeTimer._idleTimeout, closeTimeout); + }); + }); }); it('emits an error if http server bind fails', (done) => { diff --git a/test/websocket.test.js b/test/websocket.test.js index 32b9d1b5c..012f7c0a6 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -232,6 +232,24 @@ describe('WebSocket', () => { ws.ping(); }); }); + + it('honors the `closeTimeout` option', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const closeTimeout = 1000; + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + closeTimeout + }); + + ws.on('open', () => { + ws.close(); + assert.strictEqual(ws._closeTimer._idleTimeout, closeTimeout); + }); + + ws.on('close', () => { + wss.close(done); + }); + }); + }); }); }); From 61349ec5dad363c56e51c68bc1c644f29af05db0 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 5 Jan 2026 18:30:07 +0100 Subject: [PATCH 298/328] [dist] 8.19.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2004b1c21..91b826966 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.18.3", + "version": "8.19.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From ca533a53f338c4a40a3881dd6dff7e5867d06893 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 9 Jan 2026 13:10:14 +0100 Subject: [PATCH 299/328] [pkg] Update globals to version 17.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 91b826966..383f41bc9 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "eslint": "^9.0.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.0.0", - "globals": "^16.0.0", + "globals": "^17.0.0", "mocha": "^8.4.0", "nyc": "^15.0.0", "prettier": "^3.0.0", From 8b553192268810a83253e2a4a39ac16768e75bb3 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 23 Feb 2026 10:30:23 +0100 Subject: [PATCH 300/328] [pkg] Update eslint to version 10.0.1 --- lib/websocket.js | 2 +- package.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 0da2949cf..ca2e1ad80 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -693,7 +693,7 @@ function initAsClient(websocket, address, protocols, options) { } else { try { parsedUrl = new URL(address); - } catch (e) { + } catch { throw new SyntaxError(`Invalid URL: ${address}`); } } diff --git a/package.json b/package.json index 383f41bc9..54281442d 100644 --- a/package.json +++ b/package.json @@ -55,9 +55,10 @@ } }, "devDependencies": { + "@eslint/js": "^10.0.1", "benchmark": "^2.1.4", "bufferutil": "^4.0.1", - "eslint": "^9.0.0", + "eslint": "^10.0.1", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.0.0", "globals": "^17.0.0", From 91707b470ebd803aaa3fd1e896217740f39267d4 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 23 Feb 2026 22:17:46 +0100 Subject: [PATCH 301/328] [doc] Add missing space --- doc/ws.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index 7d22a0480..a9c3a2fd5 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -304,7 +304,7 @@ This class represents a WebSocket. It extends the `EventEmitter`. - `allowSynchronousEvents` {Boolean} Specifies whether any of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple times in the same tick. Defaults to `true`. Setting it to `false` improves compatibility with - the WHATWG standardbut may negatively impact performance. + the WHATWG standard but may negatively impact performance. - `autoPong` {Boolean} Specifies whether or not to automatically send a pong in response to a ping. Defaults to `true`. - `closeTimeout` {Number} Duration in milliseconds to wait for a graceful From 3ee5349a0b1580f6e1f347b59ec3371011bd8481 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 11 Mar 2026 20:52:30 +0100 Subject: [PATCH 302/328] [api] Convert the `isServer` and `maxPayload` parameters to options To simplify the API before exporting the `PerMessageDeflate` class, reduce the number of positional parameters in the constructor by moving `isServer` and `maxPayload` into the options object. --- lib/permessage-deflate.js | 12 ++--- lib/websocket-server.js | 10 ++-- lib/websocket.js | 10 ++-- test/permessage-deflate.test.js | 82 ++++++++++++++++++--------------- test/receiver.test.js | 10 +++- 5 files changed, 68 insertions(+), 56 deletions(-) diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index 41ff70e27..aa5db761d 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -37,6 +37,9 @@ class PerMessageDeflate { * acknowledge disabling of client context takeover * @param {Number} [options.concurrencyLimit=10] The number of concurrent * calls to zlib + * @param {Boolean} [options.isServer=false] Create the instance in either + * server or client mode + * @param {Number} [options.maxPayload=0] The maximum allowed message length * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the * use of a custom server window size * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept @@ -47,16 +50,13 @@ class PerMessageDeflate { * deflate * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on * inflate - * @param {Boolean} [isServer=false] Create the instance in either server or - * client mode - * @param {Number} [maxPayload=0] The maximum allowed message length */ - constructor(options, isServer, maxPayload) { - this._maxPayload = maxPayload | 0; + constructor(options) { this._options = options || {}; this._threshold = this._options.threshold !== undefined ? this._options.threshold : 1024; - this._isServer = !!isServer; + this._maxPayload = this._options.maxPayload | 0; + this._isServer = !!this._options.isServer; this._deflate = null; this._inflate = null; diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 75e04c1d6..68aa7897d 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -293,11 +293,11 @@ class WebSocketServer extends EventEmitter { this.options.perMessageDeflate && secWebSocketExtensions !== undefined ) { - const perMessageDeflate = new PerMessageDeflate( - this.options.perMessageDeflate, - true, - this.options.maxPayload - ); + const perMessageDeflate = new PerMessageDeflate({ + ...this.options.perMessageDeflate, + isServer: true, + maxPayload: this.options.maxPayload + }); try { const offers = extension.parse(secWebSocketExtensions); diff --git a/lib/websocket.js b/lib/websocket.js index ca2e1ad80..75d5bb283 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -755,11 +755,11 @@ function initAsClient(websocket, address, protocols, options) { opts.timeout = opts.handshakeTimeout; if (opts.perMessageDeflate) { - perMessageDeflate = new PerMessageDeflate( - opts.perMessageDeflate !== true ? opts.perMessageDeflate : {}, - false, - opts.maxPayload - ); + perMessageDeflate = new PerMessageDeflate({ + ...opts.perMessageDeflate, + isServer: false, + maxPayload: opts.maxPayload + }); opts.headers['Sec-WebSocket-Extensions'] = format({ [PerMessageDeflate.extensionName]: perMessageDeflate.offer() }); diff --git a/test/permessage-deflate.test.js b/test/permessage-deflate.test.js index cf795b799..3eea6c0c2 100644 --- a/test/permessage-deflate.test.js +++ b/test/permessage-deflate.test.js @@ -101,13 +101,13 @@ describe('PerMessageDeflate', () => { describe('As server', () => { it('accepts an offer with no parameters', () => { - const perMessageDeflate = new PerMessageDeflate({}, true); + const perMessageDeflate = new PerMessageDeflate({ isServer: true }); assert.deepStrictEqual(perMessageDeflate.accept([{}]), {}); }); it('accepts an offer with parameters', () => { - const perMessageDeflate = new PerMessageDeflate({}, true); + const perMessageDeflate = new PerMessageDeflate({ isServer: true }); const extensions = extension.parse( 'permessage-deflate; server_no_context_takeover; ' + 'client_no_context_takeover; server_max_window_bits=10; ' + @@ -127,15 +127,13 @@ describe('PerMessageDeflate', () => { }); it('prefers the configuration options', () => { - const perMessageDeflate = new PerMessageDeflate( - { - serverNoContextTakeover: true, - clientNoContextTakeover: true, - serverMaxWindowBits: 12, - clientMaxWindowBits: 11 - }, - true - ); + const perMessageDeflate = new PerMessageDeflate({ + serverNoContextTakeover: true, + clientNoContextTakeover: true, + serverMaxWindowBits: 12, + clientMaxWindowBits: 11, + isServer: true + }); const extensions = extension.parse( 'permessage-deflate; server_max_window_bits=14; client_max_window_bits=13' ); @@ -153,10 +151,10 @@ describe('PerMessageDeflate', () => { }); it('accepts the first supported offer', () => { - const perMessageDeflate = new PerMessageDeflate( - { serverMaxWindowBits: 11 }, - true - ); + const perMessageDeflate = new PerMessageDeflate({ + isServer: true, + serverMaxWindowBits: 11 + }); const extensions = extension.parse( 'permessage-deflate; server_max_window_bits=10, permessage-deflate' ); @@ -171,10 +169,10 @@ describe('PerMessageDeflate', () => { }); it('throws an error if server_no_context_takeover is unsupported', () => { - const perMessageDeflate = new PerMessageDeflate( - { serverNoContextTakeover: false }, - true - ); + const perMessageDeflate = new PerMessageDeflate({ + isServer: true, + serverNoContextTakeover: false + }); const extensions = extension.parse( 'permessage-deflate; server_no_context_takeover' ); @@ -186,10 +184,10 @@ describe('PerMessageDeflate', () => { }); it('throws an error if server_max_window_bits is unsupported', () => { - const perMessageDeflate = new PerMessageDeflate( - { serverMaxWindowBits: false }, - true - ); + const perMessageDeflate = new PerMessageDeflate({ + isServer: true, + serverMaxWindowBits: false + }); const extensions = extension.parse( 'permessage-deflate; server_max_window_bits=10' ); @@ -201,10 +199,10 @@ describe('PerMessageDeflate', () => { }); it('throws an error if server_max_window_bits is less than configuration', () => { - const perMessageDeflate = new PerMessageDeflate( - { serverMaxWindowBits: 11 }, - true - ); + const perMessageDeflate = new PerMessageDeflate({ + isServer: true, + serverMaxWindowBits: 11 + }); const extensions = extension.parse( 'permessage-deflate; server_max_window_bits=10' ); @@ -216,10 +214,10 @@ describe('PerMessageDeflate', () => { }); it('throws an error if client_max_window_bits is unsupported on client', () => { - const perMessageDeflate = new PerMessageDeflate( - { clientMaxWindowBits: 10 }, - true - ); + const perMessageDeflate = new PerMessageDeflate({ + isServer: true, + clientMaxWindowBits: 10 + }); const extensions = extension.parse('permessage-deflate'); assert.throws( @@ -229,7 +227,7 @@ describe('PerMessageDeflate', () => { }); it('throws an error if client_max_window_bits has an invalid value', () => { - const perMessageDeflate = new PerMessageDeflate({}, true); + const perMessageDeflate = new PerMessageDeflate({ isServer: true }); const extensions = extension.parse( 'permessage-deflate; client_max_window_bits=16' @@ -388,8 +386,10 @@ describe('PerMessageDeflate', () => { it('works with the negotiated parameters', (done) => { const perMessageDeflate = new PerMessageDeflate({ - memLevel: 5, - level: 9 + zlibDeflateOptions: { + memLevel: 5, + level: 9 + } }); const extensions = extension.parse( 'permessage-deflate; server_no_context_takeover; ' + @@ -518,7 +518,7 @@ describe('PerMessageDeflate', () => { }); it("doesn't use contex takeover if not allowed", (done) => { - const perMessageDeflate = new PerMessageDeflate({}, true); + const perMessageDeflate = new PerMessageDeflate({ isServer: true }); const extensions = extension.parse( 'permessage-deflate;server_no_context_takeover' ); @@ -549,7 +549,7 @@ describe('PerMessageDeflate', () => { }); it('uses contex takeover if allowed', (done) => { - const perMessageDeflate = new PerMessageDeflate({}, true); + const perMessageDeflate = new PerMessageDeflate({ isServer: true }); const extensions = extension.parse('permessage-deflate'); const buf = Buffer.from('foofoo'); @@ -591,7 +591,10 @@ describe('PerMessageDeflate', () => { }); it('calls the callback when `maxPayload` is exceeded (1/2)', (done) => { - const perMessageDeflate = new PerMessageDeflate({}, false, 25); + const perMessageDeflate = new PerMessageDeflate({ + isServer: false, + maxPayload: 25 + }); const buf = Buffer.alloc(50, 'A'); perMessageDeflate.accept([{}]); @@ -609,7 +612,10 @@ describe('PerMessageDeflate', () => { it('calls the callback when `maxPayload` is exceeded (2/2)', (done) => { // A copy of the previous test but with a larger input. See // https://github.com/websockets/ws/pull/2285. - const perMessageDeflate = new PerMessageDeflate({}, false, 25); + const perMessageDeflate = new PerMessageDeflate({ + isServer: false, + maxPayload: 25 + }); const buf = Buffer.alloc(1024 * 1024, 'A'); perMessageDeflate.accept([{}]); diff --git a/test/receiver.test.js b/test/receiver.test.js index 243a91606..39418905c 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -922,7 +922,10 @@ describe('Receiver', () => { }); it('emits an error if the message length exceeds `maxPayload`', (done) => { - const perMessageDeflate = new PerMessageDeflate({}, false, 25); + const perMessageDeflate = new PerMessageDeflate({ + isServer: false, + maxPayload: 25 + }); perMessageDeflate.accept([{}]); const receiver = new Receiver({ @@ -949,7 +952,10 @@ describe('Receiver', () => { }); it('emits an error if the sum of fragment lengths exceeds `maxPayload`', (done) => { - const perMessageDeflate = new PerMessageDeflate({}, false, 25); + const perMessageDeflate = new PerMessageDeflate({ + isServer: false, + maxPayload: 25 + }); perMessageDeflate.accept([{}]); const receiver = new Receiver({ From d3503c1fd36a310985108f62b343bae18346ab67 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 11 Mar 2026 22:01:00 +0100 Subject: [PATCH 303/328] [minor] Export the `PerMessageDeflate` class and header utils Export the `PerMessageDeflate` class, the parser and serializer for the `Sec-WebSocket-Extensions` header, and the parser for the `Sec-WebSocket-Protocol` header. Documentation is intentionally omitted as these utilities are primarily intended for niche use cases rather than general consumption. Refs: https://github.com/websockets/ws/pull/2313 --- index.js | 21 +++++++++++++++------ wrapper.mjs | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 41edb3b81..3fdb7b210 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,22 @@ 'use strict'; +const createWebSocketStream = require('./lib/stream'); +const extension = require('./lib/extension'); +const PerMessageDeflate = require('./lib/permessage-deflate'); +const Receiver = require('./lib/receiver'); +const Sender = require('./lib/sender'); +const subprotocol = require('./lib/subprotocol'); const WebSocket = require('./lib/websocket'); +const WebSocketServer = require('./lib/websocket-server'); -WebSocket.createWebSocketStream = require('./lib/stream'); -WebSocket.Server = require('./lib/websocket-server'); -WebSocket.Receiver = require('./lib/receiver'); -WebSocket.Sender = require('./lib/sender'); - +WebSocket.createWebSocketStream = createWebSocketStream; +WebSocket.extension = extension; +WebSocket.PerMessageDeflate = PerMessageDeflate; +WebSocket.Receiver = Receiver; +WebSocket.Sender = Sender; +WebSocket.Server = WebSocketServer; +WebSocket.subprotocol = subprotocol; WebSocket.WebSocket = WebSocket; -WebSocket.WebSocketServer = WebSocket.Server; +WebSocket.WebSocketServer = WebSocketServer; module.exports = WebSocket; diff --git a/wrapper.mjs b/wrapper.mjs index 7245ad15d..a8ffabbb0 100644 --- a/wrapper.mjs +++ b/wrapper.mjs @@ -1,8 +1,21 @@ import createWebSocketStream from './lib/stream.js'; +import extension from './lib/extension.js'; +import PerMessageDeflate from './lib/permessage-deflate.js'; import Receiver from './lib/receiver.js'; import Sender from './lib/sender.js'; +import subprotocol from './lib/subprotocol.js'; import WebSocket from './lib/websocket.js'; import WebSocketServer from './lib/websocket-server.js'; -export { createWebSocketStream, Receiver, Sender, WebSocket, WebSocketServer }; +export { + createWebSocketStream, + extension, + PerMessageDeflate, + Receiver, + Sender, + subprotocol, + WebSocket, + WebSocketServer +}; + export default WebSocket; From 843925544e2f4cffe445e0179947f56d6c5b608f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 21 Mar 2026 18:12:40 +0100 Subject: [PATCH 304/328] [dist] 8.20.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 54281442d..3618050a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.19.0", + "version": "8.20.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 5f26c245231a4b018479a9269e8c3da4773fe42f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 7 May 2026 11:52:49 +0200 Subject: [PATCH 305/328] [ci] Run the lint step on node 24 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 538662f24..a5c68eb38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: - run: npm install - run: npm run lint if: - matrix.os == 'ubuntu-latest' && matrix.node == 22 && matrix.arch == + matrix.os == 'ubuntu-latest' && matrix.node == 24 && matrix.arch == 'x64' - run: npm test - run: | From 58e45b872bb0f35a3edd553c27e105300a4f5bd0 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 7 May 2026 11:53:20 +0200 Subject: [PATCH 306/328] [ci] Do not test on node 25 --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5c68eb38..0dd8472a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,6 @@ jobs: - 20 - 22 - 24 - - 25 os: - macOS-latest - ubuntu-latest From ce2a3d62437995a47e6056d485a33d21b6a8f867 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 7 May 2026 11:53:46 +0200 Subject: [PATCH 307/328] [ci] Test on node 26 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dd8472a5..fa9033b6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ jobs: - 20 - 22 - 24 + - 26 os: - macOS-latest - ubuntu-latest From c0327ec15a54d701eb6ccefaa8bef328cfc03086 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 12 May 2026 17:10:27 +0200 Subject: [PATCH 308/328] [security] Fix uninitialized memory disclosure in `websocket.close()` When the `reason` argument for `websocket.close()` is a `TypedArray` instead of a string or `Buffer`, the function does not correctly overwrite the dirty buffer allocated via `Buffer.allocUnsafe()`. This results in the disclosure of uninitialized memory, potentially leaking sensitive data to the remote peer. Add stricter validation for the argument type. --- lib/sender.js | 7 ++++++- test/sender.test.js | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/sender.js b/lib/sender.js index a8b1da3a9..2c767e2c6 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -4,6 +4,9 @@ const { Duplex } = require('stream'); const { randomFillSync } = require('crypto'); +const { + types: { isUint8Array } +} = require('util'); const PerMessageDeflate = require('./permessage-deflate'); const { EMPTY_BUFFER, kWebSocket, NOOP } = require('./constants'); @@ -200,8 +203,10 @@ class Sender { if (typeof data === 'string') { buf.write(data, 2); - } else { + } else if (isUint8Array(data)) { buf.set(data, 2); + } else { + throw new TypeError('Second argument must be a string or a Uint8Array'); } } diff --git a/test/sender.test.js b/test/sender.test.js index df9057e8a..8ae5f4e11 100644 --- a/test/sender.test.js +++ b/test/sender.test.js @@ -439,6 +439,16 @@ describe('Sender', () => { ); }); + it('throws an error if the second argument is invalid', () => { + const mockSocket = new MockSocket(); + const sender = new Sender(mockSocket); + + assert.throws( + () => sender.close(1000, new Float32Array(20)), + /^TypeError: Second argument must be a string or a Uint8Array$/ + ); + }); + it('throws an error if the message is greater than 123 bytes', () => { const mockSocket = new MockSocket(); const sender = new Sender(mockSocket); From 5d9b316230ea931532a6671cc450f18c11edd02f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 12 May 2026 17:12:36 +0200 Subject: [PATCH 309/328] [dist] 8.20.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3618050a8..88ead7870 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.20.0", + "version": "8.20.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 78eabe2a6677b231bf9c82601bde86ff91639490 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 12 May 2026 19:29:30 +0200 Subject: [PATCH 310/328] [security] Add latest vulnerability to SECURITY.md --- SECURITY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index fb492e834..6c0f59f09 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -39,3 +39,5 @@ will create and publish a security advisory to [ReDoS in `Sec-Websocket-Protocol` header](https://github.com/websockets/ws/releases/tag/7.4.6) - 16 Jun 2024: [DoS when handling a request with many HTTP headers](https://github.com/websockets/ws/releases/tag/8.17.1) +- 12 May 2026: + [Uninitialized memory disclosure in `websocket.close()`](https://github.com/websockets/ws/releases/tag/8.20.1) From 2b2abd458a1b647d0b6033bd62a619c36189839a Mon Sep 17 00:00:00 2001 From: Nadav0077 <18245584+Nadav0077@users.noreply.github.com> Date: Thu, 21 May 2026 19:23:52 +0300 Subject: [PATCH 311/328] [security] Limit retained message parts Previously, the receiver could retain one `Buffer` entry per buffered chunk or message fragment until enough data was parsed or the message completed. A peer could use many tiny fragments/chunks and make retained memory scale with retained part count rather than message payload size. Add configurable `maxBufferedChunks` and `maxFragments` options to bound the number of retained parts. When either limit is exceeded, emit a `WS_ERR_TOO_MANY_BUFFERED_PARTS` error and close the connection with close code 1008. Signed-off-by: Nadav0077 <18245584+Nadav0077@users.noreply.github.com> --- doc/ws.md | 14 +++++++ lib/receiver.js | 54 ++++++++++++++++++++++++ lib/websocket-server.js | 8 ++++ lib/websocket.js | 14 +++++++ test/receiver.test.js | 79 +++++++++++++++++++++++++++++++++++ test/websocket-server.test.js | 11 ++++- test/websocket.test.js | 11 ++++- 7 files changed, 189 insertions(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index a9c3a2fd5..4bca4a494 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -62,6 +62,7 @@ - [WS_ERR_UNEXPECTED_MASK](#ws_err_unexpected_mask) - [WS_ERR_UNEXPECTED_RSV_1](#ws_err_unexpected_rsv_1) - [WS_ERR_UNEXPECTED_RSV_2_3](#ws_err_unexpected_rsv_2_3) + - [WS_ERR_TOO_MANY_BUFFERED_PARTS](#ws_err_too_many_buffered_parts) - [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#ws_err_unsupported_data_payload_length) - [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#ws_err_unsupported_message_length) @@ -86,6 +87,10 @@ This class represents a WebSocket server. It extends the `EventEmitter`. - `handleProtocols` {Function} A function which can be used to handle the WebSocket subprotocols. See description below. - `host` {String} The hostname where to bind the server. + - `maxBufferedChunks` {Number} The maximum number of buffered data chunks. + Defaults to 1048576. Set to 0 to disable the limit. + - `maxFragments` {Number} The maximum number of fragments in a message. + Defaults to 131072. Set to 0 to disable the limit. - `maxPayload` {Number} The maximum allowed message size in bytes. Defaults to 100 MiB (104857600 bytes). - `noServer` {Boolean} Enable no server mode. @@ -320,6 +325,10 @@ This class represents a WebSocket. It extends the `EventEmitter`. cryptographically strong random bytes. - `handshakeTimeout` {Number} Timeout in milliseconds for the handshake request. This is reset after every redirection. + - `maxBufferedChunks` {Number} The maximum number of buffered data chunks. + Defaults to 1048576. Set to 0 to disable the limit. + - `maxFragments` {Number} The maximum number of fragments in a message. + Defaults to 131072. Set to 0 to disable the limit. - `maxPayload` {Number} The maximum allowed message size in bytes. Defaults to 100 MiB (104857600 bytes). - `maxRedirects` {Number} The maximum number of redirects allowed. Defaults @@ -690,6 +699,11 @@ A WebSocket frame was received with the RSV1 bit set unexpectedly. A WebSocket frame was received with the RSV2 or RSV3 bit set unexpectedly. +### WS_ERR_TOO_MANY_BUFFERED_PARTS + +The configured maximum number of buffered data chunks or message fragments was +exceeded. + ### WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH A data frame was received with a length longer than the max supported length diff --git a/lib/receiver.js b/lib/receiver.js index 54d9b4fad..1f8f0286c 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -40,6 +40,10 @@ class Receiver extends Writable { * extensions * @param {Boolean} [options.isServer=false] Specifies whether to operate in * client or server mode + * @param {Number} [options.maxBufferedChunks=0] The maximum number of + * buffered data chunks + * @param {Number} [options.maxFragments=0] The maximum number of message + * fragments * @param {Number} [options.maxPayload=0] The maximum allowed message length * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or * not to skip UTF-8 validation for text and close messages @@ -54,6 +58,8 @@ class Receiver extends Writable { this._binaryType = options.binaryType || BINARY_TYPES[0]; this._extensions = options.extensions || {}; this._isServer = !!options.isServer; + this._maxBufferedChunks = options.maxBufferedChunks | 0; + this._maxFragments = options.maxFragments | 0; this._maxPayload = options.maxPayload | 0; this._skipUTF8Validation = !!options.skipUTF8Validation; this[kWebSocket] = undefined; @@ -89,6 +95,22 @@ class Receiver extends Writable { _write(chunk, encoding, cb) { if (this._opcode === 0x08 && this._state == GET_INFO) return cb(); + if ( + this._maxBufferedChunks > 0 && + this._buffers.length >= this._maxBufferedChunks + ) { + cb( + this.createError( + RangeError, + 'Too many buffered chunks', + false, + 1008, + 'WS_ERR_TOO_MANY_BUFFERED_PARTS' + ) + ); + return; + } + this._bufferedBytes += chunk.length; this._buffers.push(chunk); this.startLoop(cb); @@ -485,6 +507,22 @@ class Receiver extends Writable { } if (data.length) { + if ( + this._maxFragments > 0 && + this._fragments.length >= this._maxFragments + ) { + const error = this.createError( + RangeError, + 'Too many message fragments', + false, + 1008, + 'WS_ERR_TOO_MANY_BUFFERED_PARTS' + ); + + cb(error); + return; + } + // // This message is not compressed so its length is the sum of the payload // length of all fragments. @@ -524,6 +562,22 @@ class Receiver extends Writable { return; } + if ( + this._maxFragments > 0 && + this._fragments.length >= this._maxFragments + ) { + const error = this.createError( + RangeError, + 'Too many message fragments', + false, + 1008, + 'WS_ERR_TOO_MANY_BUFFERED_PARTS' + ); + + cb(error); + return; + } + this._fragments.push(buf); } diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 68aa7897d..8ce979846 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -43,6 +43,10 @@ class WebSocketServer extends EventEmitter { * called * @param {Function} [options.handleProtocols] A hook to handle protocols * @param {String} [options.host] The hostname where to bind the server + * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of + * buffered data chunks + * @param {Number} [options.maxFragments=131072] The maximum number of message + * fragments * @param {Number} [options.maxPayload=104857600] The maximum allowed message * size * @param {Boolean} [options.noServer=false] Enable no server mode @@ -65,6 +69,8 @@ class WebSocketServer extends EventEmitter { options = { allowSynchronousEvents: true, autoPong: true, + maxBufferedChunks: 1024 * 1024, + maxFragments: 128 * 1024, maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, perMessageDeflate: false, @@ -424,6 +430,8 @@ class WebSocketServer extends EventEmitter { ws.setSocket(socket, head, { allowSynchronousEvents: this.options.allowSynchronousEvents, + maxBufferedChunks: this.options.maxBufferedChunks, + maxFragments: this.options.maxFragments, maxPayload: this.options.maxPayload, skipUTF8Validation: this.options.skipUTF8Validation }); diff --git a/lib/websocket.js b/lib/websocket.js index 75d5bb283..ed3735ea4 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -201,6 +201,10 @@ class WebSocket extends EventEmitter { * multiple times in the same tick * @param {Function} [options.generateMask] The function used to generate the * masking key + * @param {Number} [options.maxBufferedChunks=0] The maximum number of + * buffered data chunks + * @param {Number} [options.maxFragments=0] The maximum number of message + * fragments * @param {Number} [options.maxPayload=0] The maximum allowed message size * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or * not to skip UTF-8 validation for text and close messages @@ -212,6 +216,8 @@ class WebSocket extends EventEmitter { binaryType: this.binaryType, extensions: this._extensions, isServer: this._isServer, + maxBufferedChunks: options.maxBufferedChunks, + maxFragments: options.maxFragments, maxPayload: options.maxPayload, skipUTF8Validation: options.skipUTF8Validation }); @@ -640,6 +646,10 @@ module.exports = WebSocket; * masking key * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the * handshake request + * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of + * buffered data chunks + * @param {Number} [options.maxFragments=131072] The maximum number of message + * fragments * @param {Number} [options.maxPayload=104857600] The maximum allowed message * size * @param {Number} [options.maxRedirects=10] The maximum number of redirects @@ -660,6 +670,8 @@ function initAsClient(websocket, address, protocols, options) { autoPong: true, closeTimeout: CLOSE_TIMEOUT, protocolVersion: protocolVersions[1], + maxBufferedChunks: 1024 * 1024, + maxFragments: 128 * 1024, maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, perMessageDeflate: true, @@ -1017,6 +1029,8 @@ function initAsClient(websocket, address, protocols, options) { websocket.setSocket(socket, head, { allowSynchronousEvents: opts.allowSynchronousEvents, generateMask: opts.generateMask, + maxBufferedChunks: opts.maxBufferedChunks, + maxFragments: opts.maxFragments, maxPayload: opts.maxPayload, skipUTF8Validation: opts.skipUTF8Validation }); diff --git a/test/receiver.test.js b/test/receiver.test.js index 39418905c..e42a33b4b 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -988,6 +988,85 @@ describe('Receiver', () => { }); }); + it('emits an error if there are too many message fragments (1/2)', (done) => { + const receiver = new Receiver({ maxFragments: 2 }); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_TOO_MANY_BUFFERED_PARTS'); + assert.strictEqual(err.message, 'Too many message fragments'); + assert.strictEqual(err[kStatusCode], 1008); + done(); + }); + + receiver.write( + Buffer.from([ + 0x02, + 0x01, + 0x61, // First non-final binary fragment. + 0x00, + 0x01, + 0x62, // Continuation fragment. + 0x00, + 0x01, + 0x63 // Continuation fragment that exceeds the limit. + ]) + ); + }); + + it('emits an error if there are too many message fragments (2/2)', (done) => { + const perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + + const receiver = new Receiver({ + extensions: { + 'permessage-deflate': perMessageDeflate + }, + maxFragments: 1 + }); + const fragment1 = Buffer.from('foo'); + const fragment2 = Buffer.from('bar'); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_TOO_MANY_BUFFERED_PARTS'); + assert.strictEqual(err.message, 'Too many message fragments'); + assert.strictEqual(err[kStatusCode], 1008); + done(); + }); + + perMessageDeflate.compress(fragment1, false, (err, data) => { + if (err) return done(err); + + receiver.write(Buffer.from([0x41, data.length])); + receiver.write(data); + + perMessageDeflate.compress(fragment2, true, (err, data) => { + if (err) return done(err); + + receiver.write(Buffer.from([0x80, data.length])); + receiver.write(data); + }); + }); + }); + + it('emits an error if there are too many buffered chunks', (done) => { + const receiver = new Receiver({ maxBufferedChunks: 2 }); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_TOO_MANY_BUFFERED_PARTS'); + assert.strictEqual(err.message, 'Too many buffered chunks'); + assert.strictEqual(err[kStatusCode], 1008); + done(); + }); + + receiver.write(Buffer.from([0x82, 0x05])); + receiver.write(Buffer.from([0x61])); + receiver.write(Buffer.from([0x62])); + receiver.write(Buffer.from([0x63])); + }); + it("honors the 'nodebuffer' binary type", (done) => { const receiver = new Receiver(); const frags = [ diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 4d5201735..4bf9852a4 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -66,11 +66,15 @@ describe('WebSocketServer', () => { }); }); - it('accepts the `maxPayload` option', (done) => { + it('accepts the receiver limit options', (done) => { + const maxBufferedChunks = 1024; + const maxFragments = 512; const maxPayload = 20480; const wss = new WebSocket.Server( { perMessageDeflate: true, + maxBufferedChunks, + maxFragments, maxPayload, port: 0 }, @@ -82,6 +86,11 @@ describe('WebSocketServer', () => { ); wss.on('connection', (ws) => { + assert.strictEqual( + ws._receiver._maxBufferedChunks, + maxBufferedChunks + ); + assert.strictEqual(ws._receiver._maxFragments, maxFragments); assert.strictEqual(ws._receiver._maxPayload, maxPayload); assert.strictEqual( ws._receiver._extensions['permessage-deflate']._maxPayload, diff --git a/test/websocket.test.js b/test/websocket.test.js index 012f7c0a6..e91e2b08a 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -135,7 +135,9 @@ describe('WebSocket', () => { assert.strictEqual(count, 2); }); - it('accepts the `maxPayload` option', (done) => { + it('accepts the receiver limit options', (done) => { + const maxBufferedChunks = 1024; + const maxFragments = 512; const maxPayload = 20480; const wss = new WebSocket.Server( { @@ -145,10 +147,17 @@ describe('WebSocket', () => { () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { perMessageDeflate: true, + maxBufferedChunks, + maxFragments, maxPayload }); ws.on('open', () => { + assert.strictEqual( + ws._receiver._maxBufferedChunks, + maxBufferedChunks + ); + assert.strictEqual(ws._receiver._maxFragments, maxFragments); assert.strictEqual(ws._receiver._maxPayload, maxPayload); assert.strictEqual( ws._receiver._extensions['permessage-deflate']._maxPayload, From bca91adf15677e47dbe4f959653452727be28b94 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 22 May 2026 17:11:24 +0200 Subject: [PATCH 312/328] [dist] 8.21.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 88ead7870..9116e6632 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.20.1", + "version": "8.21.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 4c534a6b8a5224a563af116e85c6ced7d4ca60cf Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 22 May 2026 21:03:55 +0200 Subject: [PATCH 313/328] [security] Add latest vulnerability to SECURITY.md --- SECURITY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index 6c0f59f09..d38232045 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -41,3 +41,5 @@ will create and publish a security advisory to [DoS when handling a request with many HTTP headers](https://github.com/websockets/ws/releases/tag/8.17.1) - 12 May 2026: [Uninitialized memory disclosure in `websocket.close()`](https://github.com/websockets/ws/releases/tag/8.20.1) +- 22 May 2026: + [Memory exhaustion DoS from tiny fragments and data chunks](https://github.com/websockets/ws/releases/tag/8.21.0) From 2120f4c8c625a76316792680a231496e1b615252 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 22 May 2026 21:30:44 +0200 Subject: [PATCH 314/328] [example] Remove uuid dependency --- examples/express-session-parse/index.js | 4 ++-- examples/express-session-parse/package.json | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/express-session-parse/index.js b/examples/express-session-parse/index.js index e0f214406..20e79e43d 100644 --- a/examples/express-session-parse/index.js +++ b/examples/express-session-parse/index.js @@ -2,8 +2,8 @@ const session = require('express-session'); const express = require('express'); +const crypto = require('crypto'); const http = require('http'); -const uuid = require('uuid'); const { WebSocketServer } = require('../..'); @@ -34,7 +34,7 @@ app.post('/login', function (req, res) { // // "Log in" user and set userId to session. // - const id = uuid.v4(); + const id = crypto.randomUUID(); console.log(`Updating session for user ${id}`); req.session.userId = id; diff --git a/examples/express-session-parse/package.json b/examples/express-session-parse/package.json index 406706ce8..382d4827f 100644 --- a/examples/express-session-parse/package.json +++ b/examples/express-session-parse/package.json @@ -5,7 +5,6 @@ "repository": "websockets/ws", "dependencies": { "express": "^4.16.4", - "express-session": "^1.16.1", - "uuid": "^8.3.2" + "express-session": "^1.16.1" } } From 4ea355d6d3069394994f82ca1b6d38c32ba208fb Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 10 Jun 2026 11:13:28 +0200 Subject: [PATCH 315/328] [doc] Document 32-bit signed integer coercion for option values Clarifiy that the values of the `maxBufferedChunks`, `maxFragments`, and `maxPayload` options are coerced to 32-bit signed integers. --- doc/ws.md | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 4bca4a494..57ec20fd2 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -87,12 +87,15 @@ This class represents a WebSocket server. It extends the `EventEmitter`. - `handleProtocols` {Function} A function which can be used to handle the WebSocket subprotocols. See description below. - `host` {String} The hostname where to bind the server. - - `maxBufferedChunks` {Number} The maximum number of buffered data chunks. - Defaults to 1048576. Set to 0 to disable the limit. - - `maxFragments` {Number} The maximum number of fragments in a message. - Defaults to 131072. Set to 0 to disable the limit. - - `maxPayload` {Number} The maximum allowed message size in bytes. Defaults to - 100 MiB (104857600 bytes). + - `maxBufferedChunks` {Number} The maximum number of buffered data chunks. The + value is coerced to a 32-bit signed integer. Defaults to 1048576. Set to 0 + to disable the limit. + - `maxFragments` {Number} The maximum number of fragments in a message. The + value is coerced to a 32-bit signed integer. Defaults to 131072. Set to 0 to + disable the limit. + - `maxPayload` {Number} The maximum allowed message size in bytes. The value + is coerced to a 32-bit signed integer. Defaults to 104857600 (100 MiB). Set + to 0 to disable the limit. - `noServer` {Boolean} Enable no server mode. - `path` {String} Accept only connections matching this path. - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate. @@ -325,12 +328,15 @@ This class represents a WebSocket. It extends the `EventEmitter`. cryptographically strong random bytes. - `handshakeTimeout` {Number} Timeout in milliseconds for the handshake request. This is reset after every redirection. - - `maxBufferedChunks` {Number} The maximum number of buffered data chunks. - Defaults to 1048576. Set to 0 to disable the limit. - - `maxFragments` {Number} The maximum number of fragments in a message. - Defaults to 131072. Set to 0 to disable the limit. - - `maxPayload` {Number} The maximum allowed message size in bytes. Defaults to - 100 MiB (104857600 bytes). + - `maxBufferedChunks` {Number} The maximum number of buffered data chunks. The + value is coerced to a 32-bit signed integer. Defaults to 1048576. Set to 0 + to disable the limit. + - `maxFragments` {Number} The maximum number of fragments in a message. The + value is coerced to a 32-bit signed integer. Defaults to 131072. Set to 0 to + disable the limit. + - `maxPayload` {Number} The maximum allowed message size in bytes. The value + is coerced to a 32-bit signed integer. Defaults to 104857600 (100 MiB). Set + to 0 to disable the limit. - `maxRedirects` {Number} The maximum number of redirects allowed. Defaults to 10. - `origin` {String} Value of the `Origin` or `Sec-WebSocket-Origin` header From e79f912cb3f492ae04c28feb9459a209e186b0ad Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 11 Jun 2026 14:01:49 +0200 Subject: [PATCH 316/328] [pkg] Approve install scripts for bufferutil and utf-8-validate --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 9116e6632..b9c73e5d2 100644 --- a/package.json +++ b/package.json @@ -66,5 +66,9 @@ "nyc": "^15.0.0", "prettier": "^3.0.0", "utf-8-validate": "^6.0.0" + }, + "allowScripts": { + "bufferutil": true, + "utf-8-validate": true } } From a2f4e7c046c2112bbce6fef39a083dac77d6f0d2 Mon Sep 17 00:00:00 2001 From: Shaurya Singh Date: Mon, 22 Jun 2026 12:05:49 -0700 Subject: [PATCH 317/328] [fix] Count empty fragments toward the limit (#2329) Ensure that empty fragments are counted against the `maxFragments` limit. --- lib/receiver.js | 47 ++++++++++++++----------------------------- test/receiver.test.js | 20 ++++++++++++++++-- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/lib/receiver.js b/lib/receiver.js index 1f8f0286c..784dbe1b5 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -77,6 +77,7 @@ class Receiver extends Writable { this._totalPayloadLength = 0; this._messageLength = 0; + this._numFragments = 0; this._fragments = []; this._errored = false; @@ -500,6 +501,19 @@ class Receiver extends Writable { return; } + if (this._maxFragments > 0 && ++this._numFragments > this._maxFragments) { + const error = this.createError( + RangeError, + 'Too many message fragments', + false, + 1008, + 'WS_ERR_TOO_MANY_BUFFERED_PARTS' + ); + + cb(error); + return; + } + if (this._compressed) { this._state = INFLATING; this.decompress(data, cb); @@ -507,22 +521,6 @@ class Receiver extends Writable { } if (data.length) { - if ( - this._maxFragments > 0 && - this._fragments.length >= this._maxFragments - ) { - const error = this.createError( - RangeError, - 'Too many message fragments', - false, - 1008, - 'WS_ERR_TOO_MANY_BUFFERED_PARTS' - ); - - cb(error); - return; - } - // // This message is not compressed so its length is the sum of the payload // length of all fragments. @@ -562,22 +560,6 @@ class Receiver extends Writable { return; } - if ( - this._maxFragments > 0 && - this._fragments.length >= this._maxFragments - ) { - const error = this.createError( - RangeError, - 'Too many message fragments', - false, - 1008, - 'WS_ERR_TOO_MANY_BUFFERED_PARTS' - ); - - cb(error); - return; - } - this._fragments.push(buf); } @@ -604,6 +586,7 @@ class Receiver extends Writable { this._totalPayloadLength = 0; this._messageLength = 0; this._fragmented = 0; + this._numFragments = 0; this._fragments = []; if (this._opcode === 2) { diff --git a/test/receiver.test.js b/test/receiver.test.js index e42a33b4b..2ed79a85b 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -988,7 +988,7 @@ describe('Receiver', () => { }); }); - it('emits an error if there are too many message fragments (1/2)', (done) => { + it('emits an error if there are too many message fragments (1/3)', (done) => { const receiver = new Receiver({ maxFragments: 2 }); receiver.on('error', (err) => { @@ -1014,7 +1014,23 @@ describe('Receiver', () => { ); }); - it('emits an error if there are too many message fragments (2/2)', (done) => { + it('emits an error if there are too many message fragments (2/3)', (done) => { + const receiver = new Receiver({ maxFragments: 2 }); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_TOO_MANY_BUFFERED_PARTS'); + assert.strictEqual(err.message, 'Too many message fragments'); + assert.strictEqual(err[kStatusCode], 1008); + done(); + }); + + receiver.write(Buffer.from([0x02, 0x00])); + receiver.write(Buffer.from([0x00, 0x00])); + receiver.write(Buffer.from([0x00, 0x00])); + }); + + it('emits an error if there are too many message fragments (3/3)', (done) => { const perMessageDeflate = new PerMessageDeflate(); perMessageDeflate.accept([{}]); From 8df8265c2f63fd44af3193a98e23cf38888cd991 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 4 Jul 2026 19:05:18 +0200 Subject: [PATCH 318/328] [ci] Update actions/checkout action to v7 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa9033b6e..3fb018bfe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: node: 22 os: windows-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} From f197ac65140920bdcecdab74bfc69c2d7858e55d Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 13 Jul 2026 19:57:13 +0200 Subject: [PATCH 319/328] [fix] Lower default values of `maxBufferedChunks` and `maxFragments` Use smaller and safer default values for the `maxBufferedChunks` and `maxFragments` options. Closes #2331 Closes #2332 --- doc/ws.md | 12 ++++++------ lib/websocket-server.js | 8 ++++---- lib/websocket.js | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 57ec20fd2..dce97600d 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -88,10 +88,10 @@ This class represents a WebSocket server. It extends the `EventEmitter`. WebSocket subprotocols. See description below. - `host` {String} The hostname where to bind the server. - `maxBufferedChunks` {Number} The maximum number of buffered data chunks. The - value is coerced to a 32-bit signed integer. Defaults to 1048576. Set to 0 - to disable the limit. + value is coerced to a 32-bit signed integer. Defaults to 262144. Set to 0 to + disable the limit. - `maxFragments` {Number} The maximum number of fragments in a message. The - value is coerced to a 32-bit signed integer. Defaults to 131072. Set to 0 to + value is coerced to a 32-bit signed integer. Defaults to 16384. Set to 0 to disable the limit. - `maxPayload` {Number} The maximum allowed message size in bytes. The value is coerced to a 32-bit signed integer. Defaults to 104857600 (100 MiB). Set @@ -329,10 +329,10 @@ This class represents a WebSocket. It extends the `EventEmitter`. - `handshakeTimeout` {Number} Timeout in milliseconds for the handshake request. This is reset after every redirection. - `maxBufferedChunks` {Number} The maximum number of buffered data chunks. The - value is coerced to a 32-bit signed integer. Defaults to 1048576. Set to 0 - to disable the limit. + value is coerced to a 32-bit signed integer. Defaults to 262144. Set to 0 to + disable the limit. - `maxFragments` {Number} The maximum number of fragments in a message. The - value is coerced to a 32-bit signed integer. Defaults to 131072. Set to 0 to + value is coerced to a 32-bit signed integer. Defaults to 16384. Set to 0 to disable the limit. - `maxPayload` {Number} The maximum allowed message size in bytes. The value is coerced to a 32-bit signed integer. Defaults to 104857600 (100 MiB). Set diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 8ce979846..4d859192b 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -43,9 +43,9 @@ class WebSocketServer extends EventEmitter { * called * @param {Function} [options.handleProtocols] A hook to handle protocols * @param {String} [options.host] The hostname where to bind the server - * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of + * @param {Number} [options.maxBufferedChunks=262144] The maximum number of * buffered data chunks - * @param {Number} [options.maxFragments=131072] The maximum number of message + * @param {Number} [options.maxFragments=16384] The maximum number of message * fragments * @param {Number} [options.maxPayload=104857600] The maximum allowed message * size @@ -69,8 +69,8 @@ class WebSocketServer extends EventEmitter { options = { allowSynchronousEvents: true, autoPong: true, - maxBufferedChunks: 1024 * 1024, - maxFragments: 128 * 1024, + maxBufferedChunks: 256 * 1024, + maxFragments: 16 * 1024, maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, perMessageDeflate: false, diff --git a/lib/websocket.js b/lib/websocket.js index ed3735ea4..ea58e7dfd 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -646,9 +646,9 @@ module.exports = WebSocket; * masking key * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the * handshake request - * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of + * @param {Number} [options.maxBufferedChunks=262144] The maximum number of * buffered data chunks - * @param {Number} [options.maxFragments=131072] The maximum number of message + * @param {Number} [options.maxFragments=16384] The maximum number of message * fragments * @param {Number} [options.maxPayload=104857600] The maximum allowed message * size @@ -670,8 +670,8 @@ function initAsClient(websocket, address, protocols, options) { autoPong: true, closeTimeout: CLOSE_TIMEOUT, protocolVersion: protocolVersions[1], - maxBufferedChunks: 1024 * 1024, - maxFragments: 128 * 1024, + maxBufferedChunks: 256 * 1024, + maxFragments: 16 * 1024, maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, perMessageDeflate: true, From 8e9511b86b3fc6deebbd97dd9af7c9056deea8d1 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 14 Jul 2026 18:31:55 +0200 Subject: [PATCH 320/328] [ci] Trust Coveralls Homebrew tap Refs: https://github.com/coverallsapp/github-action/issues/264 --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fb018bfe..2431d5647 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,6 +66,8 @@ jobs: echo "job_id=$id" >> $GITHUB_OUTPUT id: get_job_id shell: bash + - run: brew trust coverallsapp/coveralls + if: matrix.os == 'macOS-latest' - uses: coverallsapp/github-action@v2 with: flag-name: From ae1de54330cef77e487548890fabfeb9aae1d83d Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 14 Jul 2026 18:37:58 +0200 Subject: [PATCH 321/328] [dist] 8.21.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9c73e5d2..ad5f9709c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.21.0", + "version": "8.21.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 2eb3be0bff2453e2654b1315c5872e8d5d424a50 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 3 Aug 2026 20:53:21 +0200 Subject: [PATCH 322/328] [test] Skip test on Node.js versions where it does not apply In Node.js versions >= 22.23.2 < 23.0.0, >= 24.18.1 < 25.0.0, and >= 26.5.1, the HTTP server rejects the request before emitting the `'upgrade'` event. Refs: https://github.com/nodejs/node/commit/064d339f --- test/websocket-server.test.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 4bf9852a4..e7109f9dd 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -678,7 +678,14 @@ describe('WebSocketServer', () => { }); }); - it('fails if the Upgrade header field value cannot be read', (done) => { + it('fails if the Upgrade header cannot be read', function (done) { + // + // In Node.js versions >= 22.23.2 < 23.0.0, >= 24.18.1 < 25.0.0, and >= + // 26.5.1, the HTTP server rejects the request before emitting the + // `'upgrade'` event. See https://github.com/nodejs/node/commit/064d339f. + // + if (process.versions.modules >= 127) return this.skip(); + const server = http.createServer(); const wss = new WebSocket.Server({ noServer: true }); From e4bb883723a0c18452eea10a74139901ae33c61d Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 3 Aug 2026 21:48:59 +0200 Subject: [PATCH 323/328] [security] Use GitHub PVR as main reporting channel Direct security reporters to use private vulnerability reporting on GitHub as the primary reporting channel. --- SECURITY.md | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index d38232045..3051eb649 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,32 +1,23 @@ # Security Guidelines -Please contact us directly at **security@3rd-Eden.com** for any bug that might -impact the security of this project. Please prefix the subject of your email -with `[security]` in lowercase and square brackets. Our email filters will -automatically prevent these messages from being moved to our spam box. +## Reporting a Vulnerability -You will receive an acknowledgement of your report within **24 hours**. +Please report security vulnerabilities privately via +[GitHub Private Vulnerability Reporting](https://github.com/websockets/ws/security/advisories/new). +Feedback is generally provided within **72 hours**. If no response is received +within that time frame, please follow up directly with the maintainers. Email +addresses for the lead developers can be found in the git commit history, for +example, by running the following command: -All emails that do not include security vulnerabilities will be removed and -blocked instantly. +``` +git --no-pager show -s --format='%an <%ae>' +``` -## Exceptions +where `` is the SHA of their latest commit in the project. -If you do not receive an acknowledgement within the said time frame, please give -us the benefit of the doubt as it's possible that we haven't seen it yet. In -this case, please send us a message **without details** using one of the -following methods: - -- Contact the lead developers of this project on their personal e-mails. You can - find the e-mails in the git logs, for example, using the following command: - `git --no-pager show -s --format='%an <%ae>' ` where `` is the - SHA1 of their latest commit in the project. -- Create a GitHub issue stating contact details and the severity of the issue. - -Once we have acknowledged receipt of your report and confirmed the bug -ourselves, we will work with you to fix the vulnerability and publicly -acknowledge your responsible disclosure, if you wish. In addition to that, we -will create and publish a security advisory to +Once the report is acknowledged and the vulnerability is confirmed, a fix will +be developed in collaboration with the reporter and a public security advisory +published on [GitHub Security Advisories](https://github.com/websockets/ws/security/advisories?state=published). ## History From b4d62ebad40c3b925c84ff305a47975406015422 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 3 Aug 2026 22:21:35 +0200 Subject: [PATCH 324/328] Revert "[ci] Trust Coveralls Homebrew tap" This reverts commit 8e9511b86b3fc6deebbd97dd9af7c9056deea8d1. Refs: https://github.com/coverallsapp/github-action/commit/8d6379e1 --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2431d5647..3fb018bfe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,8 +66,6 @@ jobs: echo "job_id=$id" >> $GITHUB_OUTPUT id: get_job_id shell: bash - - run: brew trust coverallsapp/coveralls - if: matrix.os == 'macOS-latest' - uses: coverallsapp/github-action@v2 with: flag-name: From 787ebf22ce3d091fb6f931d20b4c7e914ba7cf85 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 3 Aug 2026 22:25:49 +0200 Subject: [PATCH 325/328] [dist] 8.21.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ad5f9709c..2e6db6ab0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.21.1", + "version": "8.21.2", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From e97a20eaa6f2ad7969419eed732a506453251eb9 Mon Sep 17 00:00:00 2001 From: JSap0914 Date: Wed, 17 Jun 2026 21:08:55 +0900 Subject: [PATCH 326/328] [fix] Reject offers with `client_max_window_bits` below config When a server is configured with `clientMaxWindowBits = N` and a client offers `client_max_window_bits = M` where `M < N`, the server must not respond with a value greater than `M`. Previously the server accepted such offers and responded with `client_max_window_bits = N`. The analogous check for `serverMaxWindowBits` already existed; this commit adds the symmetric check for `clientMaxWindowBits`. --- lib/permessage-deflate.js | 4 +++- test/permessage-deflate.test.js | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index aa5db761d..c51d8f5e3 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -167,7 +167,9 @@ class PerMessageDeflate { (typeof opts.serverMaxWindowBits === 'number' && opts.serverMaxWindowBits > params.server_max_window_bits))) || (typeof opts.clientMaxWindowBits === 'number' && - !params.client_max_window_bits) + (typeof params.client_max_window_bits === 'number' + ? opts.clientMaxWindowBits > params.client_max_window_bits + : !params.client_max_window_bits)) ) { return false; } diff --git a/test/permessage-deflate.test.js b/test/permessage-deflate.test.js index 3eea6c0c2..d3e9218ab 100644 --- a/test/permessage-deflate.test.js +++ b/test/permessage-deflate.test.js @@ -213,6 +213,21 @@ describe('PerMessageDeflate', () => { ); }); + it('throws an error if client_max_window_bits is less than configuration', () => { + const perMessageDeflate = new PerMessageDeflate({ + isServer: true, + clientMaxWindowBits: 11 + }); + const extensions = extension.parse( + 'permessage-deflate; client_max_window_bits=10' + ); + + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^Error: None of the extension offers can be accepted$/ + ); + }); + it('throws an error if client_max_window_bits is unsupported on client', () => { const perMessageDeflate = new PerMessageDeflate({ isServer: true, From c791e707eab3c13dd9a261d2479c3cc4a49a6fed Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 6 Aug 2026 18:15:41 +0200 Subject: [PATCH 327/328] [dist] 8.21.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e6db6ab0..f3eceef27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "8.21.2", + "version": "8.21.3", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From d9b89544e627f2a260fb85a6f42c8ecba98d7615 Mon Sep 17 00:00:00 2001 From: Rajan Pantha Date: Thu, 13 Aug 2026 13:23:50 +0545 Subject: [PATCH 328/328] [fix] Change the ready state after validating the arguments (#2337) `Sender.prototype.close()` validates `code` and `data` and throws if they are invalid. Do not change the ready state before it returns, otherwise the state is set to `CLOSING` with no close frame sent and no close timer set, and any subsequent `WebSocket.prototype.close()` call becomes a noop because the state is already `CLOSING`. --- lib/websocket.js | 2 +- test/websocket.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/websocket.js b/lib/websocket.js index ea58e7dfd..224e1523a 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -318,7 +318,6 @@ class WebSocket extends EventEmitter { return; } - this._readyState = WebSocket.CLOSING; this._sender.close(code, data, !this._isServer, (err) => { // // This error is handled by the `'error'` listener on the socket. We only @@ -336,6 +335,7 @@ class WebSocket extends EventEmitter { } }); + this._readyState = WebSocket.CLOSING; setCloseTimer(this); } diff --git a/test/websocket.test.js b/test/websocket.test.js index e91e2b08a..2cec6db43 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -3451,6 +3451,32 @@ describe('WebSocket', () => { wss.on('connection', (ws) => ws.close()); }); + it('keeps `readyState` unchanged if the arguments are invalid', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + assert.throws( + () => ws.close(1000, 'a'.repeat(124)), + /^RangeError: The message must not be greater than 123 bytes$/ + ); + + // + // The ready state must not be changed to `CLOSING` and a subsequent + // valid `websocket.close()` call must still work. + // + assert.strictEqual(ws.readyState, WebSocket.OPEN); + + ws.on('close', (code) => { + assert.strictEqual(code, 1000); + wss.close(done); + }); + + ws.close(1000, 'ok'); + }); + }); + }); + it('sets a timer for the closing handshake to complete', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`);