From 01b07e1d205a6b57cb3e6a7d82b600dc4b2742fc Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 27 May 2020 17:27:18 +0200 Subject: [PATCH 01/57] [minor] Add missing tag to JSDoc comment --- lib/receiver.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/receiver.js b/lib/receiver.js index 57daa725d..d762393ee 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -68,6 +68,7 @@ class Receiver extends Writable { * @param {Buffer} chunk The chunk of data to write * @param {String} encoding The character encoding of `chunk` * @param {Function} cb Callback + * @private */ _write(chunk, encoding, cb) { if (this._opcode === 0x08 && this._state == GET_INFO) return cb(); From c02a4b77c2b257336400c7aed9ca7c222d87c6ff Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 3 Jun 2020 20:04:41 +0200 Subject: [PATCH 02/57] [pkg] Remove Greenkeeper configuration --- package.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/package.json b/package.json index 68314cf76..c3c1151ac 100644 --- a/package.json +++ b/package.json @@ -53,11 +53,5 @@ "nyc": "^15.0.0", "prettier": "^1.17.0", "utf-8-validate": "^5.0.2" - }, - "greenkeeper": { - "commitMessages": { - "dependencyUpdate": "[pkg] Update ${dependency} to version ${version}", - "devDependencyUpdate": "[pkg] Update ${dependency} to version ${version}" - } } } From 14d53d72507ab367cff3e4609889bd8a7f68d1df Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 13 Jun 2020 07:44:08 +0200 Subject: [PATCH 03/57] [pkg] Do not run the lint script before the test script --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c3c1151ac..ef5ac2d28 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "lib/*.js" ], "scripts": { - "test": "npm run lint && nyc --reporter=html --reporter=text mocha --throw-deprecation test/*.test.js", - "integration": "npm run lint && mocha --throw-deprecation test/*.integration.js", + "test": "nyc --reporter=html --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}\"" }, "peerDependencies": { From c4c7f3c545e96d273ba27fe24641eda12831463f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 13 Jun 2020 08:22:34 +0200 Subject: [PATCH 04/57] [ci] Run the lint script only once --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 3a1f1ba57..8b3424c53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,5 +9,8 @@ os: - linux - osx - windows +script: + - if [ "${TRAVIS_NODE_VERSION}" == "14" ] && [ "${TRAVIS_OS_NAME}" == linux ]; then npm run lint; fi + - npm test after_success: - nyc report --reporter=text-lcov | coveralls From b6ae22a5b54dbdaba11dc3dfb5cb77747598d72b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 13 Jun 2020 08:40:10 +0200 Subject: [PATCH 05/57] [pkg] Update eslint to version 7.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef5ac2d28..684ce3554 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "benchmark": "^2.1.4", "bufferutil": "^4.0.1", "coveralls": "^3.0.3", - "eslint": "^6.0.0", + "eslint": "^7.2.0", "eslint-config-prettier": "^6.0.0", "eslint-plugin-prettier": "^3.0.1", "mocha": "^7.0.0", From 88d0345997ea14b262519c7d5b5baaf6f9d78035 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 13 Jun 2020 08:48:58 +0200 Subject: [PATCH 06/57] [pkg] Update prettier to version 2.0.5 --- .prettierrc.yaml | 1 + .travis.yml | 3 ++- browser.js | 2 +- examples/express-session-parse/index.js | 18 +++++++++--------- examples/express-session-parse/public/app.js | 20 ++++++++++---------- examples/server-stats/index.js | 10 +++++----- lib/stream.js | 8 ++++---- package.json | 2 +- test/websocket-server.test.js | 2 +- test/websocket.test.js | 6 ++---- 10 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 754ed23e2..fe2f506e3 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -2,3 +2,4 @@ arrowParens: always endOfLine: lf proseWrap: always singleQuote: true +trailingComma: none diff --git a/.travis.yml b/.travis.yml index 8b3424c53..fbff55562 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,8 @@ os: - osx - windows script: - - if [ "${TRAVIS_NODE_VERSION}" == "14" ] && [ "${TRAVIS_OS_NAME}" == linux ]; then npm run lint; fi + - if [ "${TRAVIS_NODE_VERSION}" == "14" ] && [ "${TRAVIS_OS_NAME}" == linux ]; + then npm run lint; fi - npm test after_success: - nyc report --reporter=text-lcov | coveralls diff --git a/browser.js b/browser.js index 782077969..ca4f628ac 100644 --- a/browser.js +++ b/browser.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = function() { +module.exports = function () { throw new Error( 'ws does not work in the browser. Browser clients must use the native ' + 'WebSocket object' diff --git a/examples/express-session-parse/index.js b/examples/express-session-parse/index.js index 196251fa3..47edc0519 100644 --- a/examples/express-session-parse/index.js +++ b/examples/express-session-parse/index.js @@ -26,7 +26,7 @@ const sessionParser = session({ app.use(express.static('public')); app.use(sessionParser); -app.post('/login', function(req, res) { +app.post('/login', function (req, res) { // // "Log in" user and set userId to session. // @@ -37,11 +37,11 @@ app.post('/login', function(req, res) { res.send({ result: 'OK', message: 'Session updated' }); }); -app.delete('/logout', function(request, response) { +app.delete('/logout', function (request, response) { const ws = map.get(request.session.userId); console.log('Destroying session'); - request.session.destroy(function() { + request.session.destroy(function () { if (ws) ws.close(); response.send({ result: 'OK', message: 'Session destroyed' }); @@ -54,7 +54,7 @@ app.delete('/logout', function(request, response) { const server = http.createServer(app); const wss = new WebSocket.Server({ clientTracking: false, noServer: true }); -server.on('upgrade', function(request, socket, head) { +server.on('upgrade', function (request, socket, head) { console.log('Parsing session from request...'); sessionParser(request, {}, () => { @@ -65,25 +65,25 @@ server.on('upgrade', function(request, socket, head) { console.log('Session is parsed!'); - wss.handleUpgrade(request, socket, head, function(ws) { + wss.handleUpgrade(request, socket, head, function (ws) { wss.emit('connection', ws, request); }); }); }); -wss.on('connection', function(ws, request) { +wss.on('connection', function (ws, request) { const userId = request.session.userId; map.set(userId, ws); - ws.on('message', function(message) { + ws.on('message', function (message) { // // Here we can now use session parameters. // console.log(`Received message ${message} from user ${userId}`); }); - ws.on('close', function() { + ws.on('close', function () { map.delete(userId); }); }); @@ -91,6 +91,6 @@ wss.on('connection', function(ws, request) { // // Start the server. // -server.listen(8080, function() { +server.listen(8080, function () { console.log('Listening on http://localhost:8080'); }); diff --git a/examples/express-session-parse/public/app.js b/examples/express-session-parse/public/app.js index ea3a776b8..f70dc2183 100644 --- a/examples/express-session-parse/public/app.js +++ b/examples/express-session-parse/public/app.js @@ -1,4 +1,4 @@ -(function() { +(function () { const messages = document.querySelector('#messages'); const wsButton = document.querySelector('#wsButton'); const wsSendButton = document.querySelector('#wsSendButton'); @@ -16,46 +16,46 @@ : Promise.reject(new Error('Unexpected response')); } - login.onclick = function() { + login.onclick = function () { fetch('/login', { method: 'POST', credentials: 'same-origin' }) .then(handleResponse) .then(showMessage) - .catch(function(err) { + .catch(function (err) { showMessage(err.message); }); }; - logout.onclick = function() { + logout.onclick = function () { fetch('/logout', { method: 'DELETE', credentials: 'same-origin' }) .then(handleResponse) .then(showMessage) - .catch(function(err) { + .catch(function (err) { showMessage(err.message); }); }; let ws; - wsButton.onclick = function() { + wsButton.onclick = function () { if (ws) { ws.onerror = ws.onopen = ws.onclose = null; ws.close(); } ws = new WebSocket(`ws://${location.host}`); - ws.onerror = function() { + ws.onerror = function () { showMessage('WebSocket error'); }; - ws.onopen = function() { + ws.onopen = function () { showMessage('WebSocket connection established'); }; - ws.onclose = function() { + ws.onclose = function () { showMessage('WebSocket connection closed'); ws = null; }; }; - wsSendButton.onclick = function() { + wsSendButton.onclick = function () { if (!ws) { showMessage('No WebSocket connection'); return; diff --git a/examples/server-stats/index.js b/examples/server-stats/index.js index 85e3ddb8c..da1f95a3b 100644 --- a/examples/server-stats/index.js +++ b/examples/server-stats/index.js @@ -12,9 +12,9 @@ app.use(express.static(path.join(__dirname, '/public'))); const server = createServer(app); const wss = new WebSocket.Server({ server }); -wss.on('connection', function(ws) { - const id = setInterval(function() { - ws.send(JSON.stringify(process.memoryUsage()), function() { +wss.on('connection', function (ws) { + const id = setInterval(function () { + ws.send(JSON.stringify(process.memoryUsage()), function () { // // Ignore errors. // @@ -22,12 +22,12 @@ wss.on('connection', function(ws) { }, 100); console.log('started client interval'); - ws.on('close', function() { + ws.on('close', function () { console.log('stopping client interval'); clearInterval(id); }); }); -server.listen(8080, function() { +server.listen(8080, function () { console.log('Listening on http://localhost:8080'); }); diff --git a/lib/stream.js b/lib/stream.js index f8d410eba..6e2f0b4d0 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -89,7 +89,7 @@ function createWebSocketStream(ws, options) { duplex.push(null); }); - duplex._destroy = function(err, callback) { + duplex._destroy = function (err, callback) { if (ws.readyState === ws.CLOSED) { callback(err); process.nextTick(emitClose, duplex); @@ -110,7 +110,7 @@ function createWebSocketStream(ws, options) { ws.terminate(); }; - duplex._final = function(callback) { + duplex._final = function (callback) { if (ws.readyState === ws.CONNECTING) { ws.once('open', function open() { duplex._final(callback); @@ -138,14 +138,14 @@ function createWebSocketStream(ws, options) { } }; - duplex._read = function() { + duplex._read = function () { if (ws.readyState === ws.OPEN && !resumeOnReceiverDrain) { resumeOnReceiverDrain = true; if (!ws._receiver._writableState.needDrain) ws._socket.resume(); } }; - duplex._write = function(chunk, encoding, callback) { + duplex._write = function (chunk, encoding, callback) { if (ws.readyState === ws.CONNECTING) { ws.once('open', function open() { duplex._write(chunk, encoding, callback); diff --git a/package.json b/package.json index 684ce3554..475731122 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "eslint-plugin-prettier": "^3.0.1", "mocha": "^7.0.0", "nyc": "^15.0.0", - "prettier": "^1.17.0", + "prettier": "^2.0.5", "utf-8-validate": "^5.0.2" } } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 2f20b5880..314ee51ff 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -112,7 +112,7 @@ describe('WebSocketServer', () => { }); }); - it('uses a precreated http server listening on unix socket', function(done) { + it('uses a precreated http server listening on unix socket', function (done) { // // Skip this test on Windows as it throws errors for obvious reasons. // diff --git a/test/websocket.test.js b/test/websocket.test.js index d02aa504b..ef3746028 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -26,7 +26,7 @@ describe('WebSocket', () => { ); }); - it('accepts `url.URL` objects as url', function(done) { + it('accepts `url.URL` objects as url', function (done) { const agent = new CustomAgent(); agent.addRequest = (req, opts) => { @@ -1345,9 +1345,7 @@ describe('WebSocket', () => { 'Invalid WebSocket frame: MASK must be set' ); assert.ok( - Buffer.concat(chunks) - .slice(0, 2) - .equals(Buffer.from('8102', 'hex')) + Buffer.concat(chunks).slice(0, 2).equals(Buffer.from('8102', 'hex')) ); ws.on('close', (code, reason) => { From 0954abcebe027aa10eb4cb203fc717291e1b3dbd Mon Sep 17 00:00:00 2001 From: sa-linetco Date: Tue, 23 Jun 2020 19:07:56 +0200 Subject: [PATCH 07/57] [doc] Add clarification for `http{,s}.request()` options (#1773) --- doc/ws.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/ws.md b/doc/ws.md index 6317f9be7..935360bfa 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -255,6 +255,8 @@ This class represents a WebSocket. It extends the `EventEmitter`. depending on the `protocolVersion`. - `maxPayload` {Number} The maximum allowed message size in bytes. - Any other option allowed in [http.request()][] or [https.request()][]. + Options given do not have any effect if parsed from the URL given with the + `address` parameter. `perMessageDeflate` default value is `true`. When using an object, parameters are the same of the server. The only difference is the direction of requests. From e1349c047d7f1c120ca606364e35d5c4c627c599 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 24 Jun 2020 17:44:40 +0200 Subject: [PATCH 08/57] [fix] Take into account the data that is being compressed Improve `websocket.bufferedAmount` accuracy by taking into account the number of bytes of a message while it is being compressed. --- lib/sender.js | 2 ++ test/websocket.test.js | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/sender.js b/lib/sender.js index 75c78fb35..d385d6456 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -318,6 +318,7 @@ class Sender { const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + this._bufferedBytes += data.length; this._deflating = true; perMessageDeflate.compress(data, options.fin, (_, buf) => { if (this._socket.destroyed) { @@ -336,6 +337,7 @@ class Sender { return; } + this._bufferedBytes -= data.length; this._deflating = false; options.readOnly = false; this.sendFrame(Sender.frame(buf, options), cb); diff --git a/test/websocket.test.js b/test/websocket.test.js index ef3746028..64d52bec9 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -185,13 +185,16 @@ describe('WebSocket', () => { ws.on('open', () => { ws.send('foo'); + + assert.strictEqual(ws.bufferedAmount, 3); + ws.send('bar', (err) => { assert.ifError(err); assert.strictEqual(ws.bufferedAmount, 0); wss.close(done); }); - assert.strictEqual(ws.bufferedAmount, 3); + assert.strictEqual(ws.bufferedAmount, 6); }); } ); From 44bcbc85313373e31eef8a5ca7b2be4447982763 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 24 Jun 2020 18:24:28 +0200 Subject: [PATCH 09/57] [minor] Fix JSDoc comments --- lib/sender.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sender.js b/lib/sender.js index d385d6456..3ea2fc147 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -174,7 +174,7 @@ class Sender { /** * Frames and sends a ping message. * - * @param {*} data The message to send + * @param {Buffer} data The message to send * @param {Boolean} mask Specifies whether or not to mask `data` * @param {Boolean} readOnly Specifies whether `data` can be modified * @param {Function} cb Callback @@ -218,7 +218,7 @@ class Sender { /** * Frames and sends a pong message. * - * @param {*} data The message to send + * @param {Buffer} data The message to send * @param {Boolean} mask Specifies whether or not to mask `data` * @param {Boolean} readOnly Specifies whether `data` can be modified * @param {Function} cb Callback From 41b0f9b36749ca1498d22726d22f72233de1424a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 24 Jun 2020 18:26:09 +0200 Subject: [PATCH 10/57] [minor] Fix nit --- test/websocket.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index 64d52bec9..c4f050dc9 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -26,7 +26,7 @@ describe('WebSocket', () => { ); }); - it('accepts `url.URL` objects as url', function (done) { + it('accepts `url.URL` objects as url', (done) => { const agent = new CustomAgent(); agent.addRequest = (req, opts) => { From a162942649e27174590bb0162bbbc63912941ce7 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 29 Jun 2020 19:28:46 +0200 Subject: [PATCH 11/57] [fix] Use `socket._writableState.length` instead of `socket.bufferSize` Refs: https://github.com/nodejs/node/pull/34088 --- lib/websocket.js | 5 +---- test/websocket.test.js | 9 ++++++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 3e8e4afd7..ce6f34caf 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -116,10 +116,7 @@ class WebSocket extends EventEmitter { get bufferedAmount() { if (!this._socket) return this._bufferedAmount; - // - // `socket.bufferSize` is `undefined` if the socket is closed. - // - return (this._socket.bufferSize || 0) + this._sender._bufferedBytes; + return this._socket._writableState.length + this._sender._bufferedBytes; } /** diff --git a/test/websocket.test.js b/test/websocket.test.js index c4f050dc9..8defba689 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -208,12 +208,15 @@ describe('WebSocket', () => { wss.on('connection', (ws) => { const data = Buffer.alloc(1024, 61); - while (ws._socket.bufferSize === 0) { + while (ws.bufferedAmount === 0) { ws.send(data); } - assert.ok(ws._socket.bufferSize > 0); - assert.strictEqual(ws.bufferedAmount, ws._socket.bufferSize); + assert.ok(ws.bufferedAmount > 0); + assert.strictEqual( + ws.bufferedAmount, + ws._socket._writableState.length + ); ws.on('close', () => wss.close(done)); ws.close(); From d09daaf67c282e301eeebe21797215ddffd819c5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 5 Jul 2020 07:18:22 +0200 Subject: [PATCH 12/57] [dist] 7.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 475731122..f45e9d347 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.3.0", + "version": "7.3.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From da42ea17451f11eed54adb54d3beeedbb1c2aa70 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 20 Jul 2020 20:11:25 +0200 Subject: [PATCH 13/57] [doc] Improve documentation for `websocket.bufferedAmount` Closes #492 --- doc/ws.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index 935360bfa..84e348d9f 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -364,7 +364,11 @@ of binary protocols transferring large messages with multiple fragments. - {Number} The number of bytes of data that have been queued using calls to `send()` but -not yet transmitted to the network. +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. All framing bytes are included. ### websocket.close([code[, reason]]) From cc656df6804465864a1b7fa51f1812395daee186 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 29 Jul 2020 19:10:05 +0200 Subject: [PATCH 14/57] [meta] Add FUNDING.yml --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..38ee540b5 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: + - 3rd-Eden + - lpinca From 622e2f3371cd23621108b553a3853bbcbc902e43 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 29 Jul 2020 20:14:33 +0200 Subject: [PATCH 15/57] [meta] Remove 3rd-Eden from FUNDING.yml 3rd-Eden is not yet enrolled in GitHub Sponsors. --- .github/FUNDING.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 38ee540b5..2c4cd0207 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,2 @@ github: - - 3rd-Eden - lpinca From 42abb0ef55279d98c3071e4230d7a0ecde073a8b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 5 Aug 2020 16:24:00 +0200 Subject: [PATCH 16/57] [test] Use `os.tmpdir()` instead of hardcoded /tmp --- test/websocket-server.test.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 314ee51ff..8d4990a47 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -6,8 +6,10 @@ const assert = require('assert'); const crypto = require('crypto'); const https = require('https'); const http = require('http'); +const path = require('path'); const net = require('net'); const fs = require('fs'); +const os = require('os'); const Sender = require('../lib/sender'); const WebSocket = require('..'); @@ -119,9 +121,10 @@ describe('WebSocketServer', () => { if (process.platform === 'win32') return this.skip(); const server = http.createServer(); - const sockPath = `/tmp/ws.${crypto - .randomBytes(16) - .toString('hex')}.socket`; + const sockPath = path.join( + os.tmpdir(), + `ws.${crypto.randomBytes(16).toString('hex')}.sock` + ); server.listen(sockPath, () => { const wss = new WebSocket.Server({ server }); From 9a99197698a9ea8b56983cb187b2ed4bdc98359a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 5 Aug 2020 19:19:39 +0200 Subject: [PATCH 17/57] [test] Clarify comment --- test/websocket-server.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 8d4990a47..f5003b216 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -116,7 +116,10 @@ describe('WebSocketServer', () => { it('uses a precreated http server listening on unix socket', function (done) { // - // Skip this test on Windows as it throws errors for obvious reasons. + // Skip this test on Windows. The URL parser: + // + // - Throws an error if the named pipe uses backward slashes. + // - Incorrectly parses the path if the named pipe used forward slashes. // if (process.platform === 'win32') return this.skip(); From 40a9d2aff0214e255294efceadab59f4f38d099d Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 6 Aug 2020 07:42:37 +0200 Subject: [PATCH 18/57] [test] Fix typo --- test/websocket-server.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index f5003b216..f41993f3f 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -119,7 +119,7 @@ describe('WebSocketServer', () => { // Skip this test on Windows. The URL parser: // // - Throws an error if the named pipe uses backward slashes. - // - Incorrectly parses the path if the named pipe used forward slashes. + // - Incorrectly parses the path if the named pipe uses forward slashes. // if (process.platform === 'win32') return this.skip(); From 535c55648db0602735f2ff8466131adfca911dae Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 1 Sep 2020 21:00:19 +0200 Subject: [PATCH 19/57] [ci] Do not test on node 13 --- .travis.yml | 1 - appveyor.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index fbff55562..c87ee06ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: node_js node_js: - '14' - - '13' - '12' - '10' - '8' diff --git a/appveyor.yml b/appveyor.yml index 93bc5bb86..2217103dd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,6 @@ environment: matrix: - nodejs_version: '14' - - nodejs_version: '13' - nodejs_version: '12' - nodejs_version: '10' - nodejs_version: '8' From d972c33cb47c87439a1c68c7cf06d9a2aa9f7141 Mon Sep 17 00:00:00 2001 From: Eugene Maslovich Date: Thu, 17 Sep 2020 08:48:21 +0300 Subject: [PATCH 20/57] [example] Write 401 response before destroying the socket (#1798) --- examples/express-session-parse/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/express-session-parse/index.js b/examples/express-session-parse/index.js index 47edc0519..8fc4ce029 100644 --- a/examples/express-session-parse/index.js +++ b/examples/express-session-parse/index.js @@ -49,9 +49,13 @@ app.delete('/logout', function (request, response) { }); // -// Create HTTP server by ourselves. +// Create an HTTP server. // const server = http.createServer(app); + +// +// Create a WebSocket server completely detached from the HTTP server. +// const wss = new WebSocket.Server({ clientTracking: false, noServer: true }); server.on('upgrade', function (request, socket, head) { @@ -59,6 +63,7 @@ server.on('upgrade', function (request, socket, head) { sessionParser(request, {}, () => { if (!request.session.userId) { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); socket.destroy(); return; } From 69172fc9c0d25d51d412002f748d9844e75cf89f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 22 Oct 2020 09:35:07 +0200 Subject: [PATCH 21/57] [minor] Add missing parameters in JSDoc comments --- lib/limiter.js | 1 + lib/stream.js | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/limiter.js b/lib/limiter.js index 8e990865d..c5e600958 100644 --- a/lib/limiter.js +++ b/lib/limiter.js @@ -27,6 +27,7 @@ class Limiter { /** * Adds a job to the queue. * + * @param {Function} job The job to run * @public */ add(job) { diff --git a/lib/stream.js b/lib/stream.js index 6e2f0b4d0..eb03a4993 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -26,6 +26,7 @@ function duplexOnEnd() { /** * The listener of the `'error'` event. * + * @param {Error} err The error * @private */ function duplexOnError(err) { From 02ecf45a29ccee55b3d09521f29689769b66e5be Mon Sep 17 00:00:00 2001 From: Dmitry Kirilyuk Date: Thu, 22 Oct 2020 12:50:15 +0300 Subject: [PATCH 22/57] [minor] Specify optional parameters in JSDoc (#1799) --- lib/event-target.js | 8 ++-- lib/limiter.js | 4 +- lib/permessage-deflate.js | 34 +++++++++-------- lib/receiver.js | 8 ++-- lib/sender.js | 79 +++++++++++++++++++++++---------------- lib/stream.js | 2 +- lib/websocket-server.js | 31 +++++++-------- lib/websocket.js | 63 +++++++++++++++++-------------- 8 files changed, 125 insertions(+), 104 deletions(-) diff --git a/lib/event-target.js b/lib/event-target.js index d38e1f2d7..bc1a385fe 100644 --- a/lib/event-target.js +++ b/lib/event-target.js @@ -111,11 +111,11 @@ const EventTarget = { * * @param {String} type A string representing the event type to listen for * @param {Function} listener The listener to add - * @param {Object} options An options object specifies characteristics about + * @param {Object} [options] An options object specifies characteristics about * the event listener - * @param {Boolean} options.once A `Boolean`` indicating that the listener - * should be invoked at most once after being added. If `true`, the - * listener would be automatically removed when invoked. + * @param {Boolean} [options.once=false] A `Boolean`` indicating that the + * listener should be invoked at most once after being added. If `true`, + * the listener would be automatically removed when invoked. * @public */ addEventListener(type, listener, options) { diff --git a/lib/limiter.js b/lib/limiter.js index c5e600958..3fd35784e 100644 --- a/lib/limiter.js +++ b/lib/limiter.js @@ -11,8 +11,8 @@ class Limiter { /** * Creates a new `Limiter`. * - * @param {Number} concurrency The maximum number of jobs allowed to run - * concurrently + * @param {Number} [concurrency=Infinity] The maximum number of jobs allowed + * to run concurrently */ constructor(concurrency) { this[kDone] = () => { diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index 7bb7c98d7..7d7209b9e 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -29,24 +29,26 @@ class PerMessageDeflate { /** * Creates a PerMessageDeflate instance. * - * @param {Object} options Configuration options - * @param {Boolean} options.serverNoContextTakeover Request/accept disabling - * of server context takeover - * @param {Boolean} options.clientNoContextTakeover Advertise/acknowledge - * disabling of client context takeover - * @param {(Boolean|Number)} options.serverMaxWindowBits Request/confirm the + * @param {Object} [options] Configuration options + * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept + * disabling of server context takeover + * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ + * acknowledge disabling of client context takeover + * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the * use of a custom server window size - * @param {(Boolean|Number)} options.clientMaxWindowBits Advertise support + * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support * for, or request, a custom client window size - * @param {Object} options.zlibDeflateOptions Options to pass to zlib on deflate - * @param {Object} options.zlibInflateOptions Options to pass to zlib on inflate - * @param {Number} options.threshold Size (in bytes) below which messages - * should not be compressed - * @param {Number} options.concurrencyLimit The number of concurrent calls to - * zlib - * @param {Boolean} isServer Create the instance in either server or client - * mode - * @param {Number} maxPayload The maximum allowed message length + * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on + * deflate + * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on + * inflate + * @param {Number} [options.threshold=1024] Size (in bytes) below which + * messages should not be compressed + * @param {Number} [options.concurrencyLimit=10] The number of concurrent + * calls to zlib + * @param {Boolean} [isServer=false] Create the instance in either server or + * client mode + * @param {Number} [maxPayload=0] The maximum allowed message length */ constructor(options, isServer, maxPayload) { this._maxPayload = maxPayload | 0; diff --git a/lib/receiver.js b/lib/receiver.js index d762393ee..15bf4b6e8 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -28,11 +28,11 @@ class Receiver extends Writable { /** * Creates a Receiver instance. * - * @param {String} binaryType The type for binary data - * @param {Object} extensions An object containing the negotiated extensions - * @param {Boolean} isServer Specifies whether to operate in client or server + * @param {String} [binaryType=nodebuffer] The type for binary data + * @param {Object} [extensions] An object containing the negotiated extensions + * @param {Boolean} [isServer=false] Specifies whether to operate in client or server * mode - * @param {Number} maxPayload The maximum allowed message length + * @param {Number} [maxPayload=0] The maximum allowed message length */ constructor(binaryType, extensions, isServer, maxPayload) { super(); diff --git a/lib/sender.js b/lib/sender.js index 3ea2fc147..ad71e1950 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -17,7 +17,7 @@ class Sender { * Creates a Sender instance. * * @param {net.Socket} socket The connection socket - * @param {Object} extensions An object containing the negotiated extensions + * @param {Object} [extensions] An object containing the negotiated extensions */ constructor(socket, extensions) { this._extensions = extensions || {}; @@ -37,10 +37,14 @@ class Sender { * @param {Buffer} data The data to frame * @param {Object} options Options object * @param {Number} options.opcode The opcode - * @param {Boolean} options.readOnly Specifies whether `data` can be modified - * @param {Boolean} options.fin Specifies whether or not to set the FIN bit - * @param {Boolean} options.mask Specifies whether or not to mask `data` - * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit * @return {Buffer[]} The framed data as a list of `Buffer` instances * @public */ @@ -93,10 +97,10 @@ class Sender { /** * Sends a close message to the other peer. * - * @param {(Number|undefined)} code The status code component of the body - * @param {String} data The message component of the body - * @param {Boolean} mask Specifies whether or not to mask the message - * @param {Function} cb Callback + * @param {Number} [code] The status code component of the body + * @param {String} [data] The message component of the body + * @param {Boolean} [mask=false] Specifies whether or not to mask the message + * @param {Function} [cb] Callback * @public */ close(code, data, mask, cb) { @@ -132,8 +136,8 @@ class Sender { * Frames and sends a close message. * * @param {Buffer} data The message to send - * @param {Boolean} mask Specifies whether or not to mask `data` - * @param {Function} cb Callback + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback * @private */ doClose(data, mask, cb) { @@ -153,8 +157,8 @@ class Sender { * Sends a ping message to the other peer. * * @param {*} data The message to send - * @param {Boolean} mask Specifies whether or not to mask `data` - * @param {Function} cb Callback + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback * @public */ ping(data, mask, cb) { @@ -175,9 +179,9 @@ class Sender { * Frames and sends a ping message. * * @param {Buffer} data The message to send - * @param {Boolean} mask Specifies whether or not to mask `data` - * @param {Boolean} readOnly Specifies whether `data` can be modified - * @param {Function} cb Callback + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified + * @param {Function} [cb] Callback * @private */ doPing(data, mask, readOnly, cb) { @@ -197,8 +201,8 @@ class Sender { * Sends a pong message to the other peer. * * @param {*} data The message to send - * @param {Boolean} mask Specifies whether or not to mask `data` - * @param {Function} cb Callback + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback * @public */ pong(data, mask, cb) { @@ -219,9 +223,9 @@ class Sender { * Frames and sends a pong message. * * @param {Buffer} data The message to send - * @param {Boolean} mask Specifies whether or not to mask `data` - * @param {Boolean} readOnly Specifies whether `data` can be modified - * @param {Function} cb Callback + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified + * @param {Function} [cb] Callback * @private */ doPong(data, mask, readOnly, cb) { @@ -242,11 +246,15 @@ class Sender { * * @param {*} data The message to send * @param {Object} options Options object - * @param {Boolean} options.compress Specifies whether or not to compress `data` - * @param {Boolean} options.binary Specifies whether `data` is binary or text - * @param {Boolean} options.fin Specifies whether the fragment is the last one - * @param {Boolean} options.mask Specifies whether or not to mask `data` - * @param {Function} cb Callback + * @param {Boolean} [options.compress=false] Specifies whether or not to + * compress `data` + * @param {Boolean} [options.binary=false] Specifies whether `data` is binary + * or text + * @param {Boolean} [options.fin=false] Specifies whether the fragment is the + * last one + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Function} [cb] Callback * @public */ send(data, options, cb) { @@ -300,14 +308,19 @@ class Sender { * Dispatches a data message. * * @param {Buffer} data The message to send - * @param {Boolean} compress Specifies whether or not to compress `data` + * @param {Boolean} [compress=false] Specifies whether or not to compress + * `data` * @param {Object} options Options object * @param {Number} options.opcode The opcode - * @param {Boolean} options.readOnly Specifies whether `data` can be modified - * @param {Boolean} options.fin Specifies whether or not to set the FIN bit - * @param {Boolean} options.mask Specifies whether or not to mask `data` - * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit - * @param {Function} cb Callback + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit + * @param {Function} [cb] Callback * @private */ dispatch(data, compress, options, cb) { @@ -374,7 +387,7 @@ class Sender { * Sends a frame. * * @param {Buffer[]} list The frame to send - * @param {Function} cb Callback + * @param {Function} [cb] Callback * @private */ sendFrame(list, cb) { diff --git a/lib/stream.js b/lib/stream.js index eb03a4993..604cf366b 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -42,7 +42,7 @@ function duplexOnError(err) { * Wraps a `WebSocket` in a duplex stream. * * @param {WebSocket} ws The `WebSocket` to wrap - * @param {Object} options The options for the `Duplex` constructor + * @param {Object} [options] The options for the `Duplex` constructor * @return {stream.Duplex} The duplex stream * @public */ diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 0d3f39592..b3922c82e 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -21,21 +21,22 @@ class WebSocketServer extends EventEmitter { * Create a `WebSocketServer` instance. * * @param {Object} options Configuration options - * @param {Number} options.backlog The maximum length of the queue of pending - * connections - * @param {Boolean} options.clientTracking Specifies whether or not to track - * clients - * @param {Function} options.handleProtocols A hook to handle protocols - * @param {String} options.host The hostname where to bind the server - * @param {Number} options.maxPayload The maximum allowed message size - * @param {Boolean} options.noServer Enable no server mode - * @param {String} options.path Accept only connections matching this path - * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable + * @param {Number} [options.backlog=511] The maximum length of the queue of + * pending connections + * @param {Boolean} [options.clientTracking=true] Specifies whether or not to + * track clients + * @param {Function} [options.handleProtocols] A hook to handle protocols + * @param {String} [options.host] The hostname where to bind the server + * @param {Number} [options.maxPayload=104857600] The maximum allowed message + * size + * @param {Boolean} [options.noServer=false] Enable no server mode + * @param {String} [options.path] Accept only connections matching this path + * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable * permessage-deflate - * @param {Number} options.port The port where to bind the server - * @param {http.Server} options.server A pre-created HTTP/S server to use - * @param {Function} options.verifyClient A hook to reject connections - * @param {Function} callback A listener for the `listening` event + * @param {Number} [options.port] The port where to bind the server + * @param {http.Server} [options.server] A pre-created HTTP/S server to use + * @param {Function} [options.verifyClient] A hook to reject connections + * @param {Function} [callback] A listener for the `listening` event */ constructor(options, callback) { super(); @@ -119,7 +120,7 @@ class WebSocketServer extends EventEmitter { /** * Close the server. * - * @param {Function} cb Callback + * @param {Function} [cb] Callback * @public */ close(cb) { diff --git a/lib/websocket.js b/lib/websocket.js index ce6f34caf..83e35d37b 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -37,8 +37,8 @@ class WebSocket extends EventEmitter { * Create a new `WebSocket`. * * @param {(String|url.URL)} address The URL to which to connect - * @param {(String|String[])} protocols The subprotocols - * @param {Object} options Connection options + * @param {(String|String[])} [protocols] The subprotocols + * @param {Object} [options] Connection options */ constructor(address, protocols, options) { super(); @@ -131,7 +131,7 @@ class WebSocket extends EventEmitter { * * @param {net.Socket} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream - * @param {Number} maxPayload The maximum allowed message size + * @param {Number} [maxPayload=0] The maximum allowed message size * @private */ setSocket(socket, head, maxPayload) { @@ -206,8 +206,8 @@ class WebSocket extends EventEmitter { * - - - - -|fin|<---------------------+ * +---+ * - * @param {Number} code Status code explaining why the connection is closing - * @param {String} data A string explaining why the connection is closing + * @param {Number} [code] Status code explaining why the connection is closing + * @param {String} [data] A string explaining why the connection is closing * @public */ close(code, data) { @@ -246,9 +246,9 @@ class WebSocket extends EventEmitter { /** * Send a ping. * - * @param {*} data The data to send - * @param {Boolean} mask Indicates whether or not to mask `data` - * @param {Function} cb Callback which is executed when the ping is sent + * @param {*} [data] The data to send + * @param {Boolean} [mask] Indicates whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when the ping is sent * @public */ ping(data, mask, cb) { @@ -278,9 +278,9 @@ class WebSocket extends EventEmitter { /** * Send a pong. * - * @param {*} data The data to send - * @param {Boolean} mask Indicates whether or not to mask `data` - * @param {Function} cb Callback which is executed when the pong is sent + * @param {*} [data] The data to send + * @param {Boolean} [mask] Indicates whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when the pong is sent * @public */ pong(data, mask, cb) { @@ -311,13 +311,15 @@ class WebSocket extends EventEmitter { * Send a data message. * * @param {*} data The message to send - * @param {Object} options Options object - * @param {Boolean} options.compress Specifies whether or not to compress + * @param {Object} [options] Options object + * @param {Boolean} [options.compress] Specifies whether or not to compress * `data` - * @param {Boolean} options.binary Specifies whether `data` is binary or text - * @param {Boolean} options.fin Specifies whether the fragment is the last one - * @param {Boolean} options.mask Specifies whether or not to mask `data` - * @param {Function} cb Callback which is executed when data is written out + * @param {Boolean} [options.binary] Specifies whether `data` is binary or + * text + * @param {Boolean} [options.fin=true] Specifies whether the fragment is the + * last one + * @param {Boolean} [options.mask] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when data is written out * @public */ send(data, options, cb) { @@ -424,19 +426,22 @@ module.exports = WebSocket; * * @param {WebSocket} websocket The client to initialize * @param {(String|url.URL)} address The URL to which to connect - * @param {String} protocols The subprotocols - * @param {Object} options Connection options - * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable + * @param {String} [protocols] The subprotocols + * @param {Object} [options] Connection options + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable * permessage-deflate - * @param {Number} options.handshakeTimeout Timeout in milliseconds for the + * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the * handshake request - * @param {Number} options.protocolVersion Value of the `Sec-WebSocket-Version` - * header - * @param {String} options.origin Value of the `Origin` or + * @param {Number} [options.protocolVersion=13] Value of the + * `Sec-WebSocket-Version` header + * @param {String} [options.origin] Value of the `Origin` or * `Sec-WebSocket-Origin` header - * @param {Number} options.maxPayload The maximum allowed message size - * @param {Boolean} options.followRedirects Whether or not to follow redirects - * @param {Number} options.maxRedirects The maximum number of redirects allowed + * @param {Number} [options.maxPayload=104857600] The maximum allowed message + * size + * @param {Boolean} [options.followRedirects=false] Whether or not to follow + * redirects + * @param {Number} [options.maxRedirects=10] The maximum number of redirects + * allowed * @private */ function initAsClient(websocket, address, protocols, options) { @@ -704,8 +709,8 @@ function abortHandshake(websocket, stream, message) { * when the `readyState` attribute is `CLOSING` or `CLOSED`. * * @param {WebSocket} websocket The WebSocket instance - * @param {*} data The data to send - * @param {Function} cb Callback + * @param {*} [data] The data to send + * @param {Function} [cb] Callback * @private */ function sendAfterClose(websocket, data, cb) { From 237960e915b3f41b8c7aabfa5c938171976f2978 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 22 Oct 2020 12:37:37 +0200 Subject: [PATCH 23/57] [codestyle] Use 80 characters per line in JSDoc comments --- lib/event-target.js | 21 ++++++++++++++------- lib/receiver.js | 4 ++-- lib/websocket-server.js | 3 ++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/event-target.js b/lib/event-target.js index bc1a385fe..a6fbe72b7 100644 --- a/lib/event-target.js +++ b/lib/event-target.js @@ -10,7 +10,8 @@ class Event { * Create a new `Event`. * * @param {String} type The name of the event - * @param {Object} target A reference to the target to which the event was dispatched + * @param {Object} target A reference to the target to which the event was + * dispatched */ constructor(type, target) { this.target = target; @@ -29,7 +30,8 @@ class MessageEvent extends Event { * Create a new `MessageEvent`. * * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data - * @param {WebSocket} target A reference to the target to which the event was dispatched + * @param {WebSocket} target A reference to the target to which the event was + * dispatched */ constructor(data, target) { super('message', target); @@ -48,9 +50,12 @@ class CloseEvent extends Event { /** * Create a new `CloseEvent`. * - * @param {Number} code The status code explaining why the connection is being closed - * @param {String} reason A human-readable string explaining why the connection is closing - * @param {WebSocket} target A reference to the target to which the event was dispatched + * @param {Number} code The status code explaining why the connection is being + * closed + * @param {String} reason A human-readable string explaining why the + * connection is closing + * @param {WebSocket} target A reference to the target to which the event was + * dispatched */ constructor(code, reason, target) { super('close', target); @@ -71,7 +76,8 @@ class OpenEvent extends Event { /** * Create a new `OpenEvent`. * - * @param {WebSocket} target A reference to the target to which the event was dispatched + * @param {WebSocket} target A reference to the target to which the event was + * dispatched */ constructor(target) { super('open', target); @@ -89,7 +95,8 @@ class ErrorEvent extends Event { * Create a new `ErrorEvent`. * * @param {Object} error The error that generated this event - * @param {WebSocket} target A reference to the target to which the event was dispatched + * @param {WebSocket} target A reference to the target to which the event was + * dispatched */ constructor(error, target) { super('error', target); diff --git a/lib/receiver.js b/lib/receiver.js index 15bf4b6e8..65a5ab45f 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -30,8 +30,8 @@ class Receiver extends Writable { * * @param {String} [binaryType=nodebuffer] The type for binary data * @param {Object} [extensions] An object containing the negotiated extensions - * @param {Boolean} [isServer=false] Specifies whether to operate in client or server - * mode + * @param {Boolean} [isServer=false] Specifies whether to operate in client or + * server mode * @param {Number} [maxPayload=0] The maximum allowed message length */ constructor(binaryType, extensions, isServer, maxPayload) { diff --git a/lib/websocket-server.js b/lib/websocket-server.js index b3922c82e..c6a68ad0e 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -339,7 +339,8 @@ module.exports = WebSocketServer; * * @param {EventEmitter} server The event emitter * @param {Object.} map The listeners to add - * @return {Function} A function that will remove the added listeners when called + * @return {Function} A function that will remove the added listeners when + * called * @private */ function addListeners(server, map) { From 572c81f375e7ba771af0db24860f5c08c644bf2b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 5 Nov 2020 20:53:09 +0100 Subject: [PATCH 24/57] [ci] Test on node 15 --- .travis.yml | 1 + appveyor.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index c87ee06ef..07c6e235d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: node_js node_js: + - '15' - '14' - '12' - '10' diff --git a/appveyor.yml b/appveyor.yml index 2217103dd..5daff04e1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,6 @@ environment: matrix: + - nodejs_version: '15' - nodejs_version: '14' - nodejs_version: '12' - nodejs_version: '10' From 7d39f19ee2b87e5c4eaec34c13a88de651533bdc Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 6 Nov 2020 20:02:30 +0100 Subject: [PATCH 25/57] [minor] Pass the request object to `server.handleUpgrade()` callback Fixes #1813 --- doc/ws.md | 8 +++++--- lib/websocket-server.js | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 84e348d9f..8e5a0d2a0 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -154,7 +154,7 @@ emitted independently. ### Event: 'connection' -- `socket` {WebSocket} +- `websocket` {WebSocket} - `request` {http.IncomingMessage} Emitted when the handshake is complete. `request` is the http GET request sent @@ -211,8 +211,10 @@ when the HTTP server is passed via the `server` option, this method is called automatically. When operating in "noServer" mode, this method must be called manually. -If the upgrade is successful, the `callback` is called with a `WebSocket` object -as parameter. +If the upgrade is successful, the `callback` is called with two arguments: + +- `websocket` {WebSocket} A `WebSocket` object. +- `request` {http.IncomingMessage} The client HTTP GET request. ### server.shouldHandle(request) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index c6a68ad0e..84d1dc9b3 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -83,13 +83,13 @@ class WebSocketServer extends EventEmitter { } if (this._server) { + const emitConnection = this.emit.bind(this, 'connection'); + this._removeListeners = addListeners(this._server, { listening: this.emit.bind(this, 'listening'), error: this.emit.bind(this, 'error'), upgrade: (req, socket, head) => { - this.handleUpgrade(req, socket, head, (ws) => { - this.emit('connection', ws, req); - }); + this.handleUpgrade(req, socket, head, emitConnection); } }); } @@ -327,7 +327,7 @@ class WebSocketServer extends EventEmitter { ws.on('close', () => this.clients.delete(ws)); } - cb(ws); + cb(ws, req); } } From eabed8fcc3694893e603460008994e3f4bcbaf42 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 7 Nov 2020 08:21:53 +0100 Subject: [PATCH 26/57] [fix] Make read-only properties read-only Fixes #1814 --- lib/websocket-server.js | 2 +- lib/websocket.js | 61 ++++++++++++++++++++++++++++------------- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 84d1dc9b3..be481a0f0 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -299,7 +299,7 @@ class WebSocketServer extends EventEmitter { if (protocol) { headers.push(`Sec-WebSocket-Protocol: ${protocol}`); - ws.protocol = protocol; + ws._protocol = protocol; } } diff --git a/lib/websocket.js b/lib/websocket.js index 83e35d37b..fa994ff91 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -43,16 +43,15 @@ class WebSocket extends EventEmitter { constructor(address, protocols, options) { super(); - this.readyState = WebSocket.CONNECTING; - this.protocol = ''; - this._binaryType = BINARY_TYPES[0]; + this._closeCode = 1006; this._closeFrameReceived = false; this._closeFrameSent = false; this._closeMessage = ''; this._closeTimer = null; - this._closeCode = 1006; this._extensions = {}; + this._protocol = ''; + this._readyState = WebSocket.CONNECTING; this._receiver = null; this._sender = null; this._socket = null; @@ -126,6 +125,27 @@ class WebSocket extends EventEmitter { return Object.keys(this._extensions).join(); } + /** + * @type {String} + */ + get protocol() { + return this._protocol; + } + + /** + * @type {Number} + */ + get readyState() { + return this._readyState; + } + + /** + * @type {String} + */ + get url() { + return this._url; + } + /** * Set up the socket and the internal resources. * @@ -166,7 +186,7 @@ class WebSocket extends EventEmitter { socket.on('end', socketOnEnd); socket.on('error', socketOnError); - this.readyState = WebSocket.OPEN; + this._readyState = WebSocket.OPEN; this.emit('open'); } @@ -177,7 +197,7 @@ class WebSocket extends EventEmitter { */ emitClose() { if (!this._socket) { - this.readyState = WebSocket.CLOSED; + this._readyState = WebSocket.CLOSED; this.emit('close', this._closeCode, this._closeMessage); return; } @@ -187,7 +207,7 @@ class WebSocket extends EventEmitter { } this._receiver.removeAllListeners(); - this.readyState = WebSocket.CLOSED; + this._readyState = WebSocket.CLOSED; this.emit('close', this._closeCode, this._closeMessage); } @@ -222,7 +242,7 @@ class WebSocket extends EventEmitter { return; } - this.readyState = WebSocket.CLOSING; + this._readyState = WebSocket.CLOSING; this._sender.close(code, data, !this._isServer, (err) => { // // This error is handled by the `'error'` listener on the socket. We only @@ -367,14 +387,17 @@ class WebSocket extends EventEmitter { } if (this._socket) { - this.readyState = WebSocket.CLOSING; + this._readyState = WebSocket.CLOSING; this._socket.destroy(); } } } readyStates.forEach((readyState, i) => { - WebSocket[readyState] = i; + Object.defineProperty(WebSocket, readyState, { + enumerable: true, + value: i + }); }); // @@ -474,10 +497,10 @@ function initAsClient(websocket, address, protocols, options) { if (address instanceof URL) { parsedUrl = address; - websocket.url = address.href; + websocket._url = address.href; } else { parsedUrl = new URL(address); - websocket.url = address; + websocket._url = address; } const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; @@ -552,7 +575,7 @@ function initAsClient(websocket, address, protocols, options) { if (websocket._req.aborted) return; req = websocket._req = null; - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; websocket.emit('error', err); websocket.emitClose(); }); @@ -623,7 +646,7 @@ function initAsClient(websocket, address, protocols, options) { return; } - if (serverProt) websocket.protocol = serverProt; + if (serverProt) websocket._protocol = serverProt; if (perMessageDeflate) { try { @@ -688,7 +711,7 @@ function tlsConnect(options) { * @private */ function abortHandshake(websocket, stream, message) { - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; const err = new Error(message); Error.captureStackTrace(err, abortHandshake); @@ -777,7 +800,7 @@ function receiverOnError(err) { websocket._socket.removeListener('data', socketOnData); - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; websocket._closeCode = err[kStatusCode]; websocket.emit('error', err); websocket._socket.destroy(); @@ -836,7 +859,7 @@ function socketOnClose() { this.removeListener('close', socketOnClose); this.removeListener('end', socketOnEnd); - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; // // The close frame might not have been received or the `'end'` event emitted, @@ -887,7 +910,7 @@ function socketOnData(chunk) { function socketOnEnd() { const websocket = this[kWebSocket]; - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; websocket._receiver.end(); this.end(); } @@ -904,7 +927,7 @@ function socketOnError() { this.on('error', NOOP); if (websocket) { - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; this.destroy(); } } From 2069e684707f078d8f5f4f4a3f026ea89c491418 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 7 Nov 2020 08:56:02 +0100 Subject: [PATCH 27/57] [fix] Fix the enumerability of some properties Make the `CONNECTING`, `OPEN`, `CLOSING`, `CLOSED`, `binaryType`, `bufferedAmount`, `extensions`, `onclose`, `onerror`, `onmessage`, `onopen`, `protocol`, `readyState`, and `url` properties enumerable. --- lib/websocket.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index fa994ff91..09fb6710b 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -74,19 +74,6 @@ class WebSocket extends EventEmitter { } } - get CONNECTING() { - return WebSocket.CONNECTING; - } - get CLOSING() { - return WebSocket.CLOSING; - } - get CLOSED() { - return WebSocket.CLOSED; - } - get OPEN() { - return WebSocket.OPEN; - } - /** * This deviates from the WHATWG interface since ws doesn't support the * required default "blob" type (instead we define a custom "nodebuffer" @@ -394,10 +381,21 @@ class WebSocket extends EventEmitter { } readyStates.forEach((readyState, i) => { - Object.defineProperty(WebSocket, readyState, { - enumerable: true, - value: i - }); + const descriptor = { enumerable: true, value: i }; + + Object.defineProperty(WebSocket.prototype, readyState, descriptor); + Object.defineProperty(WebSocket, readyState, descriptor); +}); + +[ + 'binaryType', + 'bufferedAmount', + 'extensions', + 'protocol', + 'readyState', + 'url' +].forEach((property) => { + Object.defineProperty(WebSocket.prototype, property, { enumerable: true }); }); // @@ -406,6 +404,8 @@ readyStates.forEach((readyState, i) => { // ['open', 'error', 'close', 'message'].forEach((method) => { Object.defineProperty(WebSocket.prototype, `on${method}`, { + configurable: true, + enumerable: true, /** * Return the listener of the event. * From 3f185bf34a03dc4c94c0ff92b21d1a7d4c784623 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 7 Nov 2020 09:41:03 +0100 Subject: [PATCH 28/57] [minor] Use the public `binaryType` property --- lib/websocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/websocket.js b/lib/websocket.js index 09fb6710b..5386c8ec9 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -143,7 +143,7 @@ class WebSocket extends EventEmitter { */ setSocket(socket, head, maxPayload) { const receiver = new Receiver( - this._binaryType, + this.binaryType, this._extensions, this._isServer, maxPayload From eb36a63183bfaeb130eb288d8e1374533cd7dfbe Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 8 Nov 2020 07:41:21 +0100 Subject: [PATCH 29/57] [dist] 7.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f45e9d347..3ab0e2f7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.3.1", + "version": "7.4.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 3d5066a7cad9fe3176002916aeda720a7b5ee419 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 8 Nov 2020 09:07:08 +0100 Subject: [PATCH 30/57] [test] Check configurability and enumerability of WebSocket properties --- test/websocket.test.js | 113 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 11 deletions(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index 8defba689..a9a66e739 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -100,21 +100,28 @@ describe('WebSocket', () => { Object.keys(readyStates).forEach((state) => { describe(`\`${state}\``, () => { it('is enumerable property of class', () => { - const propertyDescripter = Object.getOwnPropertyDescriptor( - WebSocket, - state - ); + const descriptor = Object.getOwnPropertyDescriptor(WebSocket, state); - assert.strictEqual(propertyDescripter.value, readyStates[state]); - assert.strictEqual(propertyDescripter.enumerable, true); + assert.deepStrictEqual(descriptor, { + configurable: false, + enumerable: true, + value: readyStates[state], + writable: false + }); }); - it('is property of instance', () => { - const ws = new WebSocket('ws://localhost', { - agent: new CustomAgent() - }); + it('is enumerable property of prototype', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + state + ); - assert.strictEqual(ws[state], readyStates[state]); + assert.deepStrictEqual(descriptor, { + configurable: false, + enumerable: true, + value: readyStates[state], + writable: false + }); }); }); }); @@ -122,6 +129,18 @@ describe('WebSocket', () => { describe('Attributes', () => { describe('`binaryType`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'binaryType' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set !== undefined); + }); + it("defaults to 'nodebuffer'", () => { const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() @@ -153,6 +172,18 @@ describe('WebSocket', () => { }); describe('`bufferedAmount`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'bufferedAmount' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + it('defaults to zero', () => { const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() @@ -225,6 +256,18 @@ describe('WebSocket', () => { }); describe('`extensions`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'bufferedAmount' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + it('exposes the negotiated extensions names (1/2)', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); @@ -269,6 +312,18 @@ describe('WebSocket', () => { }); describe('`protocol`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'protocol' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + it('exposes the subprotocol selected by the server', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const port = wss.address().port; @@ -290,6 +345,18 @@ describe('WebSocket', () => { }); describe('`readyState`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'readyState' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + it('defaults to `CONNECTING`', () => { const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() @@ -339,6 +406,18 @@ describe('WebSocket', () => { }); describe('`url`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'url' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + it('exposes the server url', () => { const url = 'ws://localhost'; const ws = new WebSocket(url, { agent: new CustomAgent() }); @@ -1729,6 +1808,18 @@ describe('WebSocket', () => { describe('WHATWG API emulation', () => { it('supports the `on{close,error,message,open}` attributes', () => { + for (const property of ['onclose', 'onerror', 'onmessage', 'onopen']) { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + property + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set !== undefined); + } + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); assert.strictEqual(ws.onmessage, undefined); From 38d6ab3b0605e245e7177e056a767a100f617e4f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 4 Dec 2020 20:28:49 +0100 Subject: [PATCH 31/57] [fix] Handle cases where the `'error'` event is emitted multiple times The `'error'` event can be emitted multiple times by the `http.ClientRequest` object in Node.js < 13. Handle the case properly. Fixes #1819 --- lib/websocket.js | 2 +- test/websocket.test.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/websocket.js b/lib/websocket.js index 5386c8ec9..739d7e714 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -572,7 +572,7 @@ function initAsClient(websocket, address, protocols, options) { } req.on('error', (err) => { - if (websocket._req.aborted) return; + if (req === null || req.aborted) return; req = websocket._req = null; websocket._readyState = WebSocket.CLOSING; diff --git a/test/websocket.test.js b/test/websocket.test.js index a9a66e739..ebd8a3d3d 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2242,6 +2242,39 @@ describe('WebSocket', () => { const ws = new WebSocket('wss://127.0.0.1', { servername: '' }); }); + + it("works around a double 'error' event bug in Node.js", function (done) { + // + // The `minVersion` and `maxVersion` options are not supported in + // Node.js < 10.16.0. + // + if (process.versions.modules < 64) return this.skip(); + + // + // The `'error'` event can be emitted multiple times by the + // `http.ClientRequest` object in Node.js < 13. This test reproduces the + // issue in Node.js 12. + // + const server = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem'), + minVersion: 'TLSv1.2' + }); + const wss = new WebSocket.Server({ server }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + maxVersion: 'TLSv1.1', + rejectUnauthorized: false + }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + server.close(done); + wss.close(); + }); + }); + }); }); describe('Request headers', () => { From c171962844e1862cadff27804700e00e2f2adbf5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 4 Dec 2020 21:41:31 +0100 Subject: [PATCH 32/57] [dist] 7.4.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ab0e2f7a..43d657045 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.0", + "version": "7.4.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From a2c0d447af711ca245cb534159fa7c4d9ae67e64 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 28 Dec 2020 21:42:32 +0100 Subject: [PATCH 33/57] [minor] Silence deprecation warning Fixes #1829 --- lib/websocket.js | 2 +- test/websocket.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 739d7e714..0e2a83d06 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -695,7 +695,7 @@ function tlsConnect(options) { options.path = undefined; if (!options.servername && options.servername !== '') { - options.servername = options.host; + options.servername = net.isIP(options.host) ? '' : options.host; } return tls.connect(options); diff --git a/test/websocket.test.js b/test/websocket.test.js index ebd8a3d3d..acc94c81f 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2123,7 +2123,7 @@ describe('WebSocket', () => { }); server.listen(0, () => { - const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + const ws = new WebSocket(`wss://127.0.0.1:${server.address().port}`, { rejectUnauthorized: false }); }); From 48a2349d229e1814ad6bcd2eb25e4e04e8b67aef Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 29 Dec 2020 21:09:51 +0100 Subject: [PATCH 34/57] [pkg] Update eslint-config-prettier to version 7.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 43d657045..905f10114 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "bufferutil": "^4.0.1", "coveralls": "^3.0.3", "eslint": "^7.2.0", - "eslint-config-prettier": "^6.0.0", + "eslint-config-prettier": "^7.1.0", "eslint-plugin-prettier": "^3.0.1", "mocha": "^7.0.0", "nyc": "^15.0.0", From d1a8af4ddb1b24a4ee23acf66decb0ed0e0d8862 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 29 Dec 2020 21:06:39 +0100 Subject: [PATCH 35/57] [dist] 7.4.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 905f10114..0d7a8b743 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.1", + "version": "7.4.2", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 2079ca5e373738e7783d2010f03432f287695e0f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 26 Jan 2021 16:49:10 +0100 Subject: [PATCH 36/57] [test] Increase code coverage --- test/websocket-server.test.js | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index f41993f3f..71646e5a4 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -320,6 +320,7 @@ describe('WebSocketServer', () => { const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); assert.strictEqual(wss.shouldHandle({ url: '/foo' }), true); + assert.strictEqual(wss.shouldHandle({ url: '/foo?bar=baz' }), true); }); it("returns false when the path doesn't match", () => { @@ -546,6 +547,41 @@ describe('WebSocketServer', () => { }); }); + it('handles unsupported extensions', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Sec-WebSocket-Extensions': 'foo; bar' + } + }); + + req.on('upgrade', (res, socket, head) => { + if (head.length) socket.unshift(head); + + socket.once('data', (chunk) => { + assert.strictEqual(chunk[0], 0x88); + wss.close(done); + }); + }); + } + ); + + wss.on('connection', (ws) => { + assert.strictEqual(ws.extensions, ''); + ws.close(); + }); + }); + describe('`verifyClient`', () => { it('can reject client synchronously', (done) => { const wss = new WebSocket.Server( From 2789887c4c3769721c371a0edf3caa6c6933f114 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 27 Jan 2021 08:53:40 +0100 Subject: [PATCH 37/57] [minor] Use `request.socket` instead of `request.connection` `request.connection` is deprecated. --- doc/ws.md | 4 ++-- lib/websocket-server.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 8e5a0d2a0..4c1b52aba 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -86,8 +86,8 @@ is provided with a single argument then that is: - `info` {Object} - `origin` {String} The value in the Origin header indicated by the client. - `req` {http.IncomingMessage} The client HTTP GET request. - - `secure` {Boolean} `true` if `req.connection.authorized` or - `req.connection.encrypted` is set. + - `secure` {Boolean} `true` if `req.socket.authorized` or + `req.socket.encrypted` is set. The return value (`Boolean`) of the function determines whether or not to accept the handshake. diff --git a/lib/websocket-server.js b/lib/websocket-server.js index be481a0f0..b99ad050a 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -225,7 +225,7 @@ class WebSocketServer extends EventEmitter { const info = { origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], - secure: !!(req.connection.authorized || req.connection.encrypted), + secure: !!(req.socket.authorized || req.socket.encrypted), req }; From 4e9607bb259dc3747881c2c22c3f65127d018a16 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 2 Feb 2021 19:18:21 +0100 Subject: [PATCH 38/57] [perf] Reset compressor/decompressor instead of re-initialize (#1840) --- lib/permessage-deflate.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index 7d7209b9e..74bf14a23 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -376,12 +376,11 @@ class PerMessageDeflate { this._inflate[kTotalLength] ); + this._inflate[kTotalLength] = 0; + this._inflate[kBuffers] = []; + if (fin && this.params[`${endpoint}_no_context_takeover`]) { - this._inflate.close(); - this._inflate = null; - } else { - this._inflate[kTotalLength] = 0; - this._inflate[kBuffers] = []; + this._inflate.reset(); } callback(null, data); @@ -448,12 +447,11 @@ class PerMessageDeflate { // this._deflate[kCallback] = null; + this._deflate[kTotalLength] = 0; + this._deflate[kBuffers] = []; + if (fin && this.params[`${endpoint}_no_context_takeover`]) { - this._deflate.close(); - this._deflate = null; - } else { - this._deflate[kTotalLength] = 0; - this._deflate[kBuffers] = []; + this._deflate.reset(); } callback(null, data); From 223194e5af389d1ab8019010cd54baccb79f0916 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 2 Feb 2021 20:16:11 +0100 Subject: [PATCH 39/57] [dist] 7.4.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d7a8b743..17cafb7d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.2", + "version": "7.4.3", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 99338f7ec6a869dbdd48ae0bcf56ca5d9aaa3f90 Mon Sep 17 00:00:00 2001 From: Ryan Christian <33403762+rschristian@users.noreply.github.com> Date: Sun, 7 Feb 2021 01:38:09 -0600 Subject: [PATCH 40/57] [doc] Fix `data` argument type (#1843) --- doc/ws.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 4c1b52aba..2da2f6440 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -417,7 +417,8 @@ receives an `OpenEvent` named "open". ### websocket.ping([data[, mask]][, callback]) -- `data` {Any} The data to send in the ping frame. +- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The + data to send in the ping frame. - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults to `true` when `websocket` is not a server client. - `callback` {Function} An optional callback which is invoked when the ping @@ -427,7 +428,8 @@ Send a ping. ### websocket.pong([data[, mask]][, callback]) -- `data` {Any} The data to send in the pong frame. +- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The + data to send in the pong frame. - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults to `true` when `websocket` is not a server client. - `callback` {Function} An optional callback which is invoked when the pong @@ -456,7 +458,8 @@ Removes an event listener emulating the `EventTarget` interface. ### websocket.send(data[, options][, callback]) -- `data` {Any} The data to send. +- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The + data to send. - `options` {Object} - `compress` {Boolean} Specifies whether `data` should be compressed or not. Defaults to `true` when permessage-deflate is enabled. From 77370e00ca75b2f88c35be7202fbe641abab5ee7 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 24 Feb 2021 20:18:51 +0100 Subject: [PATCH 41/57] [pkg] Update eslint-config-prettier to version 8.1.0 --- .eslintrc.yaml | 5 +---- package.json | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 70f32c8d4..45998d206 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -5,16 +5,13 @@ env: node: true extends: - eslint:recommended - - prettier + - plugin:prettier/recommended parserOptions: ecmaVersion: 9 -plugins: - - prettier rules: no-console: off no-var: error prefer-const: error - prettier/prettier: error quotes: - error - single diff --git a/package.json b/package.json index 17cafb7d8..6a86ac0b0 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "bufferutil": "^4.0.1", "coveralls": "^3.0.3", "eslint": "^7.2.0", - "eslint-config-prettier": "^7.1.0", + "eslint-config-prettier": "^8.1.0", "eslint-plugin-prettier": "^3.0.1", "mocha": "^7.0.0", "nyc": "^15.0.0", From 489a295be632feea34266c9966a16d5453f123dc Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 26 Feb 2021 17:42:21 +0100 Subject: [PATCH 42/57] [ci] Use GitHub Actions (#1853) --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++++++++++++++++++ .travis.yml | 17 ---------------- README.md | 2 +- package.json | 3 +-- 4 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..ceb1490f3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + - push + - pull_request + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + node: + - 8 + - 10 + - 12 + - 14 + - 15 + os: + - macOS-latest + - ubuntu-latest + - windows-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + - run: npm install + - run: npm run lint + if: matrix.node == 14 && matrix.os == 'ubuntu-latest' + - run: npm test + - uses: coverallsapp/github-action@v1.1.2 + with: + flag-name: Node.js ${{ matrix.node }} on ${{ matrix.os }} + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel: true + coverage: + needs: test + runs-on: ubuntu-latest + steps: + - uses: coverallsapp/github-action@v1.1.2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 07c6e235d..000000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: node_js -node_js: - - '15' - - '14' - - '12' - - '10' - - '8' -os: - - linux - - osx - - windows -script: - - if [ "${TRAVIS_NODE_VERSION}" == "14" ] && [ "${TRAVIS_OS_NAME}" == linux ]; - then npm run lint; fi - - npm test -after_success: - - nyc report --reporter=text-lcov | coveralls diff --git a/README.md b/README.md index f36a354bb..6aea136b1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ws: a Node.js WebSocket library [![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws) -[![Build](https://img.shields.io/travis/websockets/ws/master.svg?logo=travis)](https://travis-ci.com/websockets/ws) +[![Build](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=build&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) [![Windows x86 Build](https://img.shields.io/appveyor/ci/lpinca/ws/master.svg?logo=appveyor)](https://ci.appveyor.com/project/lpinca/ws) [![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg)](https://coveralls.io/github/websockets/ws) diff --git a/package.json b/package.json index 6a86ac0b0..659e50b91 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "lib/*.js" ], "scripts": { - "test": "nyc --reporter=html --reporter=text mocha --throw-deprecation test/*.test.js", + "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js", "integration": "mocha --throw-deprecation test/*.integration.js", "lint": "eslint --ignore-path .gitignore . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\"" }, @@ -45,7 +45,6 @@ "devDependencies": { "benchmark": "^2.1.4", "bufferutil": "^4.0.1", - "coveralls": "^3.0.3", "eslint": "^7.2.0", "eslint-config-prettier": "^8.1.0", "eslint-plugin-prettier": "^3.0.1", From cbff929b810529f64a88e4b7b8f25d19023dc912 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 3 Mar 2021 09:30:15 +0100 Subject: [PATCH 43/57] [doc] Improve `websocket.terminate()` documentation Fixes #1858 --- doc/ws.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/ws.md b/doc/ws.md index 2da2f6440..de62e6756 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -476,7 +476,7 @@ Send `data` through the connection. ### websocket.terminate() -Forcibly close the connection. +Forcibly close the connection. Internally this calls [socket.destroy()][]. ### websocket.url @@ -502,4 +502,5 @@ given `WebSocket`. https://nodejs.org/api/https.html#https_https_request_options_callback [permessage-deflate]: https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19 +[socket.destroy()]: https://nodejs.org/api/net.html#net_socket_destroy_error [zlib-options]: https://nodejs.org/api/zlib.html#zlib_class_options From 92774377166b9e9241982cada4e80331093021ae Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 6 Mar 2021 21:12:07 +0100 Subject: [PATCH 44/57] [fix] Recreate the inflate stream if it ends Refs: https://github.com/nodejs/node/issues/37612 --- lib/permessage-deflate.js | 13 +++++++++---- test/permessage-deflate.test.js | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index 74bf14a23..a8974b988 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -376,11 +376,16 @@ class PerMessageDeflate { this._inflate[kTotalLength] ); - this._inflate[kTotalLength] = 0; - this._inflate[kBuffers] = []; + if (this._inflate._readableState.endEmitted) { + this._inflate.close(); + this._inflate = null; + } else { + this._inflate[kTotalLength] = 0; + this._inflate[kBuffers] = []; - if (fin && this.params[`${endpoint}_no_context_takeover`]) { - this._inflate.reset(); + if (fin && this.params[`${endpoint}_no_context_takeover`]) { + this._inflate.reset(); + } } callback(null, data); diff --git a/test/permessage-deflate.test.js b/test/permessage-deflate.test.js index 09681d96f..a547762ca 100644 --- a/test/permessage-deflate.test.js +++ b/test/permessage-deflate.test.js @@ -631,5 +631,26 @@ describe('PerMessageDeflate', () => { process.nextTick(() => perMessageDeflate.cleanup()); }); + + it('recreates the inflate stream if it ends', (done) => { + const perMessageDeflate = new PerMessageDeflate(); + const extensions = extension.parse( + 'permessage-deflate; client_no_context_takeover; ' + + 'server_no_context_takeover' + ); + const buf = Buffer.from('33343236313533b7000000', 'hex'); + const expected = Buffer.from('12345678'); + + perMessageDeflate.accept(extensions['permessage-deflate']); + + perMessageDeflate.decompress(buf, true, (err, data) => { + assert.ok(data.equals(expected)); + + perMessageDeflate.decompress(buf, true, (err, data) => { + assert.ok(data.equals(expected)); + done(); + }); + }); + }); }); }); From a74dd2ee88ca87e1e0af7062331996bc35f311a6 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 6 Mar 2021 21:29:14 +0100 Subject: [PATCH 45/57] [dist] 7.4.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 659e50b91..66cba8372 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.3", + "version": "7.4.4", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From d75a62ed661af25244e4825bec4813688886e3bd Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 7 Mar 2021 06:54:46 +0100 Subject: [PATCH 46/57] [ci] Include commit SHA in `flag-name` --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceb1490f3..21f0e8396 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,8 @@ jobs: - run: npm test - uses: coverallsapp/github-action@v1.1.2 with: - flag-name: Node.js ${{ matrix.node }} on ${{ matrix.os }} + flag-name: + ${{github.sha}} - Node.js ${{ matrix.node }} on ${{ matrix.os }} github-token: ${{ secrets.GITHUB_TOKEN }} parallel: true coverage: From 114de9e33668075f0af88dc440f1ebd813161e72 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 7 Mar 2021 08:44:53 +0100 Subject: [PATCH 47/57] [ci] Use a unique ID instead of commit SHA --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21f0e8396..76326adeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,10 +28,16 @@ jobs: - run: npm run lint if: matrix.node == 14 && matrix.os == 'ubuntu-latest' - run: npm test + - run: + echo ::set-output name=job_id::$(node -e + "console.log(crypto.randomBytes(16).toString('hex'))") + id: get_job_id + shell: bash - uses: coverallsapp/github-action@v1.1.2 with: flag-name: - ${{github.sha}} - Node.js ${{ matrix.node }} on ${{ matrix.os }} + ${{ steps.get_job_id.outputs.job_id }} (Node.js ${{ matrix.node }} + on ${{ matrix.os }}) github-token: ${{ secrets.GITHUB_TOKEN }} parallel: true coverage: From 23ba6b2922f521f2b656891a997ab562b7139dd4 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 17 Apr 2021 16:23:19 +0200 Subject: [PATCH 48/57] [fix] Make UTF-8 validation work even if utf-8-validate is not installed Fixes #1868 --- README.md | 4 +- lib/validation.js | 100 ++++++++++++++++++++++++++++++++++------ test/validation.test.js | 52 +++++++++++++++++++++ 3 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 test/validation.test.js diff --git a/README.md b/README.md index 6aea136b1..9c6e5287c 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ can use one of the many wrappers available on npm, like npm install ws ``` -### Opt-in for performance and spec compliance +### Opt-in for performance There are 2 optional modules that can be installed along side with the ws module. These modules are binary addons which improve certain operations. @@ -67,7 +67,7 @@ necessarily need to have a C++ compiler installed on your machine. operations such as masking and unmasking the data payload of the WebSocket frames. - `npm install --save-optional utf-8-validate`: Allows to efficiently check if a - message contains valid UTF-8 as required by the spec. + message contains valid UTF-8. ## API docs diff --git a/lib/validation.js b/lib/validation.js index 32db5a570..d8693fdb9 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -1,16 +1,5 @@ 'use strict'; -try { - const isValidUTF8 = require('utf-8-validate'); - - exports.isValidUTF8 = - typeof isValidUTF8 === 'object' - ? isValidUTF8.Validation.isValidUTF8 // utf-8-validate@<3.0.0 - : isValidUTF8; -} catch (e) /* istanbul ignore next */ { - exports.isValidUTF8 = () => true; -} - /** * Checks if a status code is allowed in a close frame. * @@ -18,7 +7,7 @@ try { * @return {Boolean} `true` if the status code is valid, else `false` * @public */ -exports.isValidStatusCode = (code) => { +function isValidStatusCode(code) { return ( (code >= 1000 && code <= 1014 && @@ -27,4 +16,89 @@ exports.isValidStatusCode = (code) => { code !== 1006) || (code >= 3000 && code <= 4999) ); -}; +} + +/** + * Checks if a given buffer contains only correct UTF-8. + * Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by + * Markus Kuhn. + * + * @param {Buffer} buf The buffer to check + * @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false` + * @public + */ +function _isValidUTF8(buf) { + const len = buf.length; + let i = 0; + + while (i < len) { + if (buf[i] < 0x80) { + // 0xxxxxxx + i++; + } else if ((buf[i] & 0xe0) === 0xc0) { + // 110xxxxx 10xxxxxx + if ( + i + 1 === len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i] & 0xfe) === 0xc0 // Overlong + ) { + return false; + } else { + i += 2; + } + } else if ((buf[i] & 0xf0) === 0xe0) { + // 1110xxxx 10xxxxxx 10xxxxxx + if ( + i + 2 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong + (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) + ) { + return false; + } else { + i += 3; + } + } else if ((buf[i] & 0xf8) === 0xf0) { + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if ( + i + 3 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i + 3] & 0xc0) !== 0x80 || + (buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong + (buf[i] === 0xf4 && buf[i + 1] > 0x8f) || + buf[i] > 0xf4 // > U+10FFFF + ) { + return false; + } else { + i += 4; + } + } else { + return false; + } + } + + return true; +} + +try { + let isValidUTF8 = require('utf-8-validate'); + + /* istanbul ignore if */ + if (typeof isValidUTF8 === 'object') { + isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0 + } + + module.exports = { + isValidStatusCode, + isValidUTF8(buf) { + return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); + } + }; +} catch (e) /* istanbul ignore next */ { + module.exports = { + isValidStatusCode, + isValidUTF8: _isValidUTF8 + }; +} diff --git a/test/validation.test.js b/test/validation.test.js new file mode 100644 index 000000000..5718b12f0 --- /dev/null +++ b/test/validation.test.js @@ -0,0 +1,52 @@ +'use strict'; + +const assert = require('assert'); + +const { isValidUTF8 } = require('../lib/validation'); + +describe('extension', () => { + describe('isValidUTF8', () => { + it('returns false if it finds invalid bytes', () => { + assert.strictEqual(isValidUTF8(Buffer.from([0xf8])), false); + }); + + it('returns false for overlong encodings', () => { + assert.strictEqual(isValidUTF8(Buffer.from([0xc0, 0xa0])), false); + assert.strictEqual(isValidUTF8(Buffer.from([0xe0, 0x80, 0xa0])), false); + assert.strictEqual( + isValidUTF8(Buffer.from([0xf0, 0x80, 0x80, 0xa0])), + false + ); + }); + + it('returns false for code points in the range U+D800 - U+DFFF', () => { + for (let i = 0xa0; i < 0xc0; i++) { + for (let j = 0x80; j < 0xc0; j++) { + assert.strictEqual(isValidUTF8(Buffer.from([0xed, i, j])), false); + } + } + }); + + it('returns false for code points greater than U+10FFFF', () => { + assert.strictEqual( + isValidUTF8(Buffer.from([0xf4, 0x90, 0x80, 0x80])), + false + ); + assert.strictEqual( + isValidUTF8(Buffer.from([0xf5, 0x80, 0x80, 0x80])), + false + ); + }); + + it('returns true for a well-formed UTF-8 byte sequence', () => { + // prettier-ignore + const buf = Buffer.from([ + 0xe2, 0x82, 0xAC, // € + 0xf0, 0x90, 0x8c, 0x88, // 𐍈 + 0x24 // $ + ]); + + assert.strictEqual(isValidUTF8(buf), true); + }); + }); +}); From 67e25ff50230d131d76b1061ca0be5c991df161f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 18 Apr 2021 09:34:27 +0200 Subject: [PATCH 49/57] [fix] Fix case where `abortHandshake()` does not close the connection On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if called after the request completed. Fixes #1869 --- lib/websocket.js | 10 ++++++++ test/websocket.test.js | 52 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 0e2a83d06..539238190 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -718,6 +718,16 @@ function abortHandshake(websocket, stream, message) { if (stream.setHeader) { stream.abort(); + + if (stream.socket && !stream.socket.destroyed) { + // + // On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if + // called after the request completed. See + // https://github.com/websockets/ws/issues/1869. + // + stream.socket.destroy(); + } + stream.once('abort', websocket.emitClose.bind(websocket)); websocket.emit('error', err); } else { diff --git a/test/websocket.test.js b/test/websocket.test.js index acc94c81f..8bfc9e151 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1444,7 +1444,7 @@ describe('WebSocket', () => { }); describe('#close', () => { - it('closes the connection if called while connecting (1/2)', (done) => { + it('closes the connection if called while connecting (1/3)', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); @@ -1461,7 +1461,7 @@ describe('WebSocket', () => { }); }); - it('closes the connection if called while connecting (2/2)', (done) => { + it('closes the connection if called while connecting (2/3)', (done) => { const wss = new WebSocket.Server( { verifyClient: (info, cb) => setTimeout(cb, 300, true), @@ -1484,6 +1484,54 @@ describe('WebSocket', () => { ); }); + it('closes the connection if called while connecting (3/3)', (done) => { + const server = http.createServer(); + + server.listen(0, function () { + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + ws.on('close', () => { + server.close(done); + }); + }); + + ws.on('unexpected-response', (req, res) => { + assert.strictEqual(res.statusCode, 502); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual(Buffer.concat(chunks).toString(), 'foo'); + ws.close(); + }); + }); + }); + + server.on('upgrade', (req, socket) => { + socket.on('end', socket.end); + + socket.write( + `HTTP/1.1 502 ${http.STATUS_CODES[502]}\r\n` + + 'Connection: keep-alive\r\n' + + 'Content-type: text/html\r\n' + + 'Content-Length: 3\r\n' + + '\r\n' + + 'foo' + ); + }); + }); + it('can be called from an error listener while connecting', (done) => { const ws = new WebSocket('ws://localhost:1337'); From f67271079755e79a1ac2b40f3f4efb94ca024539 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 18 Apr 2021 10:00:59 +0200 Subject: [PATCH 50/57] [dist] 7.4.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66cba8372..c2d63afe1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.4", + "version": "7.4.5", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 587c201bfc22c460658ca304d23477fc7ebd2a60 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 23 Apr 2021 20:23:23 +0200 Subject: [PATCH 51/57] [ci] Do not test on node 15 --- .github/workflows/ci.yml | 1 - appveyor.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76326adeb..4ae3b6aa4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,6 @@ jobs: - 10 - 12 - 14 - - 15 os: - macOS-latest - ubuntu-latest diff --git a/appveyor.yml b/appveyor.yml index 5daff04e1..2217103dd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,5 @@ environment: matrix: - - nodejs_version: '15' - nodejs_version: '14' - nodejs_version: '12' - nodejs_version: '10' From fc7e27d12ad0af90ce05302afc85c292024000b4 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 23 Apr 2021 20:24:19 +0200 Subject: [PATCH 52/57] [ci] Test on node 16 --- .github/workflows/ci.yml | 3 ++- appveyor.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ae3b6aa4..158a50e32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ jobs: - 10 - 12 - 14 + - 16 os: - macOS-latest - ubuntu-latest @@ -25,7 +26,7 @@ jobs: node-version: ${{ matrix.node }} - run: npm install - run: npm run lint - if: matrix.node == 14 && matrix.os == 'ubuntu-latest' + if: matrix.node == 16 && matrix.os == 'ubuntu-latest' - run: npm test - run: echo ::set-output name=job_id::$(node -e diff --git a/appveyor.yml b/appveyor.yml index 2217103dd..f4c05fbf4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,6 @@ environment: matrix: + - nodejs_version: '16' - nodejs_version: '14' - nodejs_version: '12' - nodejs_version: '10' From 8c914d18b86a7d1408884d18eeadae0fa41b0bb5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 4 May 2021 12:18:24 +0200 Subject: [PATCH 53/57] [minor] Fix nits --- lib/validation.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/validation.js b/lib/validation.js index d8693fdb9..169ac6f06 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -32,7 +32,7 @@ function _isValidUTF8(buf) { let i = 0; while (i < len) { - if (buf[i] < 0x80) { + if ((buf[i] & 0x80) === 0) { // 0xxxxxxx i++; } else if ((buf[i] & 0xe0) === 0xc0) { @@ -43,9 +43,9 @@ function _isValidUTF8(buf) { (buf[i] & 0xfe) === 0xc0 // Overlong ) { return false; - } else { - i += 2; } + + i += 2; } else if ((buf[i] & 0xf0) === 0xe0) { // 1110xxxx 10xxxxxx 10xxxxxx if ( @@ -56,9 +56,9 @@ function _isValidUTF8(buf) { (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) ) { return false; - } else { - i += 3; } + + i += 3; } else if ((buf[i] & 0xf8) === 0xf0) { // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx if ( @@ -71,9 +71,9 @@ function _isValidUTF8(buf) { buf[i] > 0xf4 // > U+10FFFF ) { return false; - } else { - i += 4; } + + i += 4; } else { return false; } From 32e3a8439b7c8273b44fe1adb5682f529e34d0ba Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 25 May 2021 15:54:54 +0200 Subject: [PATCH 54/57] [security] Remove reference to Node Security Project The Node Security Platform service no longer exists. New security advisories will be published to GitHub Security Advisories. --- SECURITY.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 258ff59fd..eebb2a552 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -25,8 +25,9 @@ following methods: Once we have acknowledged receipt of your report and confirmed the bug ourselves we will work with you to fix the vulnerability and publicly acknowledge your -responsible disclosure, if you wish. In addition to that we will report all -vulnerabilities to the [Node Security Project](https://nodesecurity.io/). +responsible disclosure, if you wish. In addition to that we will create and +publish a security advisory to +[GitHub Security Advisories](https://github.com/websockets/ws/security/advisories). ## History From 990306d1446faf346c76452409a4c11455690514 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 25 May 2021 16:48:37 +0200 Subject: [PATCH 55/57] [lint] Fix prettier error --- lib/websocket.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 539238190..83b471d94 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -654,9 +654,8 @@ function initAsClient(websocket, address, protocols, options) { if (extensions[PerMessageDeflate.extensionName]) { perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); - websocket._extensions[ - PerMessageDeflate.extensionName - ] = perMessageDeflate; + websocket._extensions[PerMessageDeflate.extensionName] = + perMessageDeflate; } } catch (err) { abortHandshake( From 00c425ec77993773d823f018f64a5c44e17023ff Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 25 May 2021 11:00:58 +0200 Subject: [PATCH 56/57] [security] Fix ReDoS vulnerability A specially crafted value of the `Sec-Websocket-Protocol` header could be used to significantly slow down a ws server. PoC and fix were sent privately by Robert McLaughlin from University of California, Santa Barbara. --- lib/websocket-server.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index b99ad050a..3c3bbe0b0 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -286,7 +286,7 @@ class WebSocketServer extends EventEmitter { let protocol = req.headers['sec-websocket-protocol']; if (protocol) { - protocol = protocol.trim().split(/ *, */); + protocol = protocol.split(',').map(trim); // // Optionally call external protocol selection handler. @@ -404,3 +404,15 @@ function abortHandshake(socket, code, message, headers) { socket.removeListener('error', socketOnError); socket.destroy(); } + +/** + * Remove whitespace characters from both ends of a string. + * + * @param {String} str The string + * @return {String} A new string representing `str` stripped of whitespace + * characters from both its beginning and end + * @private + */ +function trim(str) { + return str.trim(); +} From f5297f7090f6a628832a730187c5b3a06a247f00 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 25 May 2021 18:11:07 +0200 Subject: [PATCH 57/57] [dist] 7.4.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c2d63afe1..2ab6e3769 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.5", + "version": "7.4.6", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi",