From 2862c2fef1064940c1deb9c4aa961cb76c279feb Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 23 Jan 2023 21:32:38 +0100 Subject: [PATCH 01/67] [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 02/67] [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 03/67] [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 04/67] [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 05/67] [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 06/67] [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 07/67] [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 08/67] [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 09/67] [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 10/67] [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 11/67] [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 12/67] [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 13/67] [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 14/67] [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 15/67] [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 16/67] [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 17/67] [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 18/67] [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 19/67] [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 20/67] [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 21/67] [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 22/67] [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 23/67] [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 24/67] [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 25/67] [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 26/67] [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 27/67] [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 28/67] [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 29/67] [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 30/67] [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 31/67] [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 32/67] [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 33/67] [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 34/67] [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 35/67] [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 36/67] [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 37/67] [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 38/67] [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 39/67] [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 40/67] [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 41/67] [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 42/67] [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 43/67] [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 44/67] [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 45/67] [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 46/67] [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 47/67] [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 48/67] [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 49/67] [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 50/67] [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 51/67] [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 52/67] [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 53/67] [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 54/67] [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 55/67] [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 56/67] [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 57/67] [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 58/67] [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 59/67] [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 60/67] [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 61/67] [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 62/67] [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 63/67] [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 64/67] [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 65/67] [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 66/67] [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 67/67] [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",