From 2079ca5e373738e7783d2010f03432f287695e0f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 26 Jan 2021 16:49:10 +0100 Subject: [PATCH 01/50] [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 02/50] [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 03/50] [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 04/50] [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 05/50] [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 06/50] [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 07/50] [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 08/50] [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 09/50] [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 10/50] [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 11/50] [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 12/50] [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 13/50] [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 14/50] [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 15/50] [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 16/50] [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 17/50] [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 18/50] [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 19/50] [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 20/50] [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 21/50] [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 22/50] [dist] 7.4.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c2d63afe1..2ab6e3769 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.5", + "version": "7.4.6", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From c05d51f167e2464a3e8cf1888d60ac1da9b38197 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 25 May 2021 18:33:07 +0200 Subject: [PATCH 23/50] [security] Add ReDoS vulnerability to SECURITY.md --- SECURITY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index eebb2a552..9e3f475d4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -35,3 +35,5 @@ publish a security advisory to [Buffer vulnerability](https://github.com/websockets/ws/releases/tag/1.0.1) - 08 Nov 2017: [DoS vulnerability](https://github.com/websockets/ws/releases/tag/3.3.1) +- 25 May 2021: + [ReDoS in `Sec-Websocket-Protocol` header](https://github.com/websockets/ws/releases/tag/7.4.6) From 2f2b3e8f8417c799fd579ced1a3e89f9a18fbb1c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 26 May 2021 21:20:45 +0200 Subject: [PATCH 24/50] [test] Update certificates and private keys Fixes #1890 --- test/fixtures/agent1-cert.pem | 24 ++++++++++-------------- test/fixtures/agent1-key.pem | 20 +++++--------------- test/fixtures/ca1-cert.pem | 23 ++++++++++------------- test/fixtures/ca1-key.pem | 22 +++++----------------- test/fixtures/certificate.pem | 21 ++++++++++----------- test/fixtures/key.pem | 20 +++++--------------- test/fixtures/request.pem | 11 ----------- 7 files changed, 45 insertions(+), 96 deletions(-) delete mode 100644 test/fixtures/request.pem diff --git a/test/fixtures/agent1-cert.pem b/test/fixtures/agent1-cert.pem index cccb9fb4d..0e20560b8 100644 --- a/test/fixtures/agent1-cert.pem +++ b/test/fixtures/agent1-cert.pem @@ -1,16 +1,12 @@ -----BEGIN CERTIFICATE----- -MIICbjCCAdcCCQCVvok5oeLpqzANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV -UzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQKEwZKb3llbnQxEDAO -BgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqGSIb3DQEJARYRcnlA -dGlueWNsb3Vkcy5vcmcwHhcNMTMwMzA4MDAzMDIyWhcNNDAwNzIzMDAzMDIyWjB9 -MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQK -EwZKb3llbnQxEDAOBgNVBAsTB05vZGUuanMxDzANBgNVBAMTBmFnZW50MTEgMB4G -CSqGSIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcwgZ8wDQYJKoZIhvcNAQEBBQAD -gY0AMIGJAoGBAL6GwKosYb0Yc3Qo0OtQVlCJ4208Idw11ij+t2W5sfYbCil5tyQo -jnhGM1CJhEXynQpXXwjKJuIeTQCkeUibTyFKa0bs8+li2FiGoKYbb4G81ovnqkmE -2iDVb8Gw3rrM4zeZ0ZdFnjMsAZac8h6+C4sB/pS9BiMOo6qTl15RQlcJAgMBAAEw -DQYJKoZIhvcNAQEFBQADgYEAOtmLo8DwTPnI4wfQbQ3hWlTS/9itww6IsxH2ODt9 -ggB7wi7N3uAdIWRZ54ke0NEAO5CW1xNTwsWcxQbiHrDOqX1vfVCjIenI76jVEEap -/Ay53ydHNBKdsKkib61Me14Mu0bA3lUul57VXwmH4NUEFB3w973Q60PschUhOEXj -7DY= +MIIBtzCCAV0CCQDDIX2dKuKP0zAKBggqhkjOPQQDAjBhMQswCQYDVQQGEwJJVDEQ +MA4GA1UECAwHUGVydWdpYTEQMA4GA1UEBwwHRm9saWdubzETMBEGA1UECgwKd2Vi +c29ja2V0czELMAkGA1UECwwCd3MxDDAKBgNVBAMMA2NhMTAgFw0yMTA1MjYxOTE3 +NDJaGA8yMTIxMDUwMjE5MTc0MlowZDELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB1Bl +cnVnaWExEDAOBgNVBAcMB0ZvbGlnbm8xEzARBgNVBAoMCndlYnNvY2tldHMxCzAJ +BgNVBAsMAndzMQ8wDQYDVQQDDAZhZ2VudDEwWTATBgcqhkjOPQIBBggqhkjOPQMB +BwNCAATwHlNS2b13TMhBTSWBXAn6TEPxrsvG93ZZyUlmrEMOXSMX2hI7sv660YNj ++eGyE2CV33XsQxV3TUqi51fUjIu8MAoGCCqGSM49BAMCA0gAMEUCIQCxsqBre+Do +jnfg6XmCaB0fywNzcDlvdoVNuNAWfVNrSAIgDQmbM0mXZaSAkf4sgtKdXnpE3vrb +MElb457Bi3B+rkE= -----END CERTIFICATE----- diff --git a/test/fixtures/agent1-key.pem b/test/fixtures/agent1-key.pem index cbd5f0c26..e034f57fc 100644 --- a/test/fixtures/agent1-key.pem +++ b/test/fixtures/agent1-key.pem @@ -1,15 +1,5 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQC+hsCqLGG9GHN0KNDrUFZQieNtPCHcNdYo/rdlubH2Gwopebck -KI54RjNQiYRF8p0KV18IyibiHk0ApHlIm08hSmtG7PPpYthYhqCmG2+BvNaL56pJ -hNog1W/BsN66zOM3mdGXRZ4zLAGWnPIevguLAf6UvQYjDqOqk5deUUJXCQIDAQAB -AoGANu/CBA+SCyVOvRK70u4yRTzNMAUjukxnuSBhH1rg/pajYnwvG6T6F6IeT72n -P0gKkh3JUE6B0bds+p9yPUZTFUXghxjcF33wlIY44H6gFE4K5WutsFJ9c450wtuu -8rXZTsIg7lAXWjTFVmdtOEPetcGlO2Hpi1O7ZzkzHgB2w9ECQQDksCCYx78or1zY -ZSokm8jmpIjG3VLKdvI9HAoJRN40ldnwFoigrFa1AHwsFtWNe8bKyVRPDoLDUjpB -dkPWgweVAkEA1UfgqguQ2KIkbtp9nDBionu3QaajksrRHwIa8vdfRfLxszfHk2fh -NGY3dkRZF8HUAbzYLrd9poVhCBAEjWekpQJASOM6AHfpnXYHCZF01SYx6hEW5wsz -kARJQODm8f1ZNTlttO/5q/xBxn7ZFNRSTD3fJlL05B2j380ddC/Vf1FT4QJAP1BC -GliqnBSuGhZUWYxni3KMeTm9rzL0F29pjpzutHYlWB2D6ndY/FQnvL0XcZ0Bka58 -womIDGnl3x3aLBwLXQJBAJv6h5CHbXHx7VyDJAcNfppAqZGcEaiVg8yf2F33iWy2 -FLthhJucx7df7SO2aw5h06bRDRAhb9br0R9/3mLr7RE= ------END RSA PRIVATE KEY----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKVGskK0UR86WwMo5H0+hNAFGRBYsEevK3ye4y1YberVoAoGCCqGSM49 +AwEHoUQDQgAE8B5TUtm9d0zIQU0lgVwJ+kxD8a7Lxvd2WclJZqxDDl0jF9oSO7L+ +utGDY/nhshNgld917EMVd01KoudX1IyLvA== +-----END EC PRIVATE KEY----- diff --git a/test/fixtures/ca1-cert.pem b/test/fixtures/ca1-cert.pem index 1d0c0d688..0f1658821 100644 --- a/test/fixtures/ca1-cert.pem +++ b/test/fixtures/ca1-cert.pem @@ -1,15 +1,12 @@ -----BEGIN CERTIFICATE----- -MIICazCCAdQCCQC9/g69HtxXRzANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV -UzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQKEwZKb3llbnQxEDAO -BgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqGSIb3DQEJARYRcnlA -dGlueWNsb3Vkcy5vcmcwHhcNMTMwMzA4MDAzMDIyWhcNNDAwNzIzMDAzMDIyWjB6 -MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQK -EwZKb3llbnQxEDAOBgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqG -SIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAKxr1mARUcv7zaqx5y4AxJPK6c1jdbSg7StcL4vg8klaPAlfNO6o+/Cl -w5CdQD3ukaVUwUOJ4T/+b3Xf7785XcWBC33GdjVQkfbHATJYcka7j7JDw3qev5Jk -1rAbRw48hF6rYlSGcx1mccAjoLoa3I8jgxCNAYHIjUQXgdmU893rAgMBAAEwDQYJ -KoZIhvcNAQEFBQADgYEAis05yxjCtJRuv8uX/DK6TX/j9C9Lzp1rKDNFTaTZ0iRw -KCw1EcNx4OXSj9gNblW4PWxpDvygrt1AmH9h2cb8K859NSHa9JOBFw6MA5C2A4Sj -NQfNATqUl4T6cdORlcDEZwHtT8b6D4A6Er31G/eJF4Sen0TUFpjdjd+l9RBjHlo= +MIIBtTCCAVoCCQCXqK2FegDgiDAKBggqhkjOPQQDAjBhMQswCQYDVQQGEwJJVDEQ +MA4GA1UECAwHUGVydWdpYTEQMA4GA1UEBwwHRm9saWdubzETMBEGA1UECgwKd2Vi +c29ja2V0czELMAkGA1UECwwCd3MxDDAKBgNVBAMMA2NhMTAgFw0yMTA1MjYxOTA1 +MjdaGA8yMTIxMDUwMjE5MDUyN1owYTELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB1Bl +cnVnaWExEDAOBgNVBAcMB0ZvbGlnbm8xEzARBgNVBAoMCndlYnNvY2tldHMxCzAJ +BgNVBAsMAndzMQwwCgYDVQQDDANjYTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC +AASHE75QDQN6XNo/711YSbckaa8r4lt0hGkgtADaBFT9Qn9gcm5omapePZT76Ff9 +rwjMcS+YPXS7J7bk+QHLihJMMAoGCCqGSM49BAMCA0kAMEYCIQCUMdUih+sE0ZTu +ORlcKiM8DKyiKkGU4Ty+dslz6nVJjAIhAMcSy0SBsBDgsai1s9aCmAGJXCijNb6g +vfWaatgq+ma2 -----END CERTIFICATE----- diff --git a/test/fixtures/ca1-key.pem b/test/fixtures/ca1-key.pem index df1495083..a9352fb6a 100644 --- a/test/fixtures/ca1-key.pem +++ b/test/fixtures/ca1-key.pem @@ -1,17 +1,5 @@ ------BEGIN ENCRYPTED PRIVATE KEY----- -MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIFeWxJE1BrRECAggA -MBQGCCqGSIb3DQMHBAgu9PlMSQ+BOASCAoDEZN2tX0xWo/N+Jg+PrvCrFDk3P+3x -5xG/PEDjtMCAWPBEwbnaYHDzYmhNcAmxzGqEHGMDiWYs46LbO560VS3uMvFbEWPo -KYYVb13vkxl2poXdonCb5cHZA5GUYzTIVVJFptl4LHwBczHoMHtA4FqAhKlYvlWw -EOrdLB8XcwMmGPFabbbGxno0+EWWM27uNjlogfoxj35mQqSW4rOlhZ460XjOB1Zx -LjXMuZeONojkGYQRG5EUMchBoctQpCOM6cAi9r1B9BvtFCBpDV1c1zEZBzTEUd8o -kLn6tjLmY+QpTdylFjEWc7U3ppLY/pkoTBv4r85a2sEMWqkhSJboLaTboWzDJcU3 -Ke61pMpovt/3yCUd3TKgwduVwwQtDVTlBe0p66aN9QVj3CrFy/bKAGO3vxlli24H -aIjZf+OVoBY21ESlW3jLvNlBf7Ezf///2E7j4SCDLyZSFMTpFoAG/jDRyvi+wTKX -Kh485Bptnip6DCSuoH4u2SkOqwz3gJS/6s02YKe4m311QT4Pzne5/FwOFaS/HhQg -Xvyh2/d00OgJ0Y0PYQsHILPRgTUCKUXvj1O58opn3fxSacsPxIXwj6Z4FYAjUTaV -2B85k1lpant/JJEilDqMjqzx4pHZ/Z3Uto1lSM1JZs9SNL/0UR+6F0TXZTULVU9V -w8jYzz4sPr7LEyrrTbzmjQgnQFVbhAN/eKgRZK/SpLjxpmBV5MfpbPKsPUZqT4UC -4nXa8a/NYUQ9e+QKK8enq9E599c2W442W7Z1uFRZTWReMx/lF8wwA6G8zOPG0bdj -d+T5Gegzd5mvRiXMBklCo8RLxOOvgxun1n3PY4a63aH6mqBhdfhiLp5j ------END ENCRYPTED PRIVATE KEY----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIAa/Onpk27cLkqzje69Bac8yG+LTBXIPWT8yGlyjEFbboAoGCCqGSM49 +AwEHoUQDQgAEhxO+UA0DelzaP+9dWEm3JGmvK+JbdIRpILQA2gRU/UJ/YHJuaJmq +Xj2U++hX/a8IzHEvmD10uye25PkBy4oSTA== +-----END EC PRIVATE KEY----- diff --git a/test/fixtures/certificate.pem b/test/fixtures/certificate.pem index 0efc2ef5b..538553ee0 100644 --- a/test/fixtures/certificate.pem +++ b/test/fixtures/certificate.pem @@ -1,13 +1,12 @@ -----BEGIN CERTIFICATE----- -MIICATCCAWoCCQDPufXH86n2QzANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJu -bzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMB4XDTEyMDEwMTE0NDQwMFoXDTIwMDMxOTE0NDQwMFowRTELMAkG -A1UEBhMCbm8xEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 -IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtrQ7 -+r//2iV/B6F+4boH0XqFn7alcV9lpjvAmwRXNKnxAoa0f97AjYPGNLKrjpkNXXhB -JROIdbRbZnCNeC5fzX1a+JCo7KStzBXuGSZr27TtFmcV4H+9gIRIcNHtZmJLnxbJ -sIhkGR8yVYdmJZe4eT5ldk1zoB1adgPF1hZhCBMCAwEAATANBgkqhkiG9w0BAQUF -AAOBgQCeWBEHYJ4mCB5McwSSUox0T+/mJ4W48L/ZUE4LtRhHasU9hiW92xZkTa7E -QLcoJKQiWfiLX2ysAro0NX4+V8iqLziMqvswnPzz5nezaOLE/9U/QvH3l8qqNkXu -rNbsW1h/IO6FV8avWFYVFoutUwOaZ809k7iMh2F2JMgXQ5EymQ== +MIIBujCCAWACCQDjKdAMt3mZhDAKBggqhkjOPQQDAjBkMQswCQYDVQQGEwJJVDEQ +MA4GA1UECAwHUGVydWdpYTEQMA4GA1UEBwwHRm9saWdubzETMBEGA1UECgwKd2Vi +c29ja2V0czELMAkGA1UECwwCd3MxDzANBgNVBAMMBnNlcnZlcjAgFw0yMTA1MjYx +OTEwMjlaGA8yMTIxMDUwMjE5MTAyOVowZDELMAkGA1UEBhMCSVQxEDAOBgNVBAgM +B1BlcnVnaWExEDAOBgNVBAcMB0ZvbGlnbm8xEzARBgNVBAoMCndlYnNvY2tldHMx +CzAJBgNVBAsMAndzMQ8wDQYDVQQDDAZzZXJ2ZXIwWTATBgcqhkjOPQIBBggqhkjO +PQMBBwNCAAQKhyRhdSVOecbJU4O5XkB/iGodbnCOqmchs4TXmE3Prv5SrNDhODDv +rOWTXwR3/HrrdNfOzPdb54amu8POwpohMAoGCCqGSM49BAMCA0gAMEUCIHMRUSPl +8FGkDLl8KF1A+SbT2ds3zUOLdYvj30Z2SKSVAiEA84U/R1ly9wf5Rzv93sTHI99o +KScsr/PHN8rT2pop5pk= -----END CERTIFICATE----- diff --git a/test/fixtures/key.pem b/test/fixtures/key.pem index 176fe320b..05bfdb71e 100644 --- a/test/fixtures/key.pem +++ b/test/fixtures/key.pem @@ -1,15 +1,5 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQC2tDv6v//aJX8HoX7hugfReoWftqVxX2WmO8CbBFc0qfEChrR/ -3sCNg8Y0squOmQ1deEElE4h1tFtmcI14Ll/NfVr4kKjspK3MFe4ZJmvbtO0WZxXg -f72AhEhw0e1mYkufFsmwiGQZHzJVh2Yll7h5PmV2TXOgHVp2A8XWFmEIEwIDAQAB -AoGAAlVY8sHi/aE+9xT77twWX3mGHV0SzdjfDnly40fx6S1Gc7bOtVdd9DC7pk6l -3ENeJVR02IlgU8iC5lMHq4JEHPE272jtPrLlrpWLTGmHEqoVFv9AITPqUDLhB9Kk -Hjl7h8NYBKbr2JHKICr3DIPKOT+RnXVb1PD4EORbJ3ooYmkCQQDfknUnVxPgxUGs -ouABw1WJIOVgcCY/IFt4Ihf6VWTsxBgzTJKxn3HtgvE0oqTH7V480XoH0QxHhjLq -DrgobWU9AkEA0TRJ8/ouXGnFEPAXjWr9GdPQRZ1Use2MrFjneH2+Sxc0CmYtwwqL -Kr5kS6mqJrxprJeluSjBd+3/ElxURrEXjwJAUvmlN1OPEhXDmRHd92mKnlkyKEeX -OkiFCiIFKih1S5Y/sRJTQ0781nyJjtJqO7UyC3pnQu1oFEePL+UEniRztQJAMfav -AtnpYKDSM+1jcp7uu9BemYGtzKDTTAYfoiNF42EzSJiGrWJDQn4eLgPjY0T0aAf/ -yGz3Z9ErbhMm/Ysl+QJBAL4kBxRT8gM4ByJw4sdOvSeCCANFq8fhbgm8pGWlCPb5 -JGmX3/GHFM8x2tbWMGpyZP1DLtiNEFz7eCGktWK5rqE= ------END RSA PRIVATE KEY----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIjLz7YEWIrsGem2+YV8eJhHhetsjYIrjuqJLbdG7B3zoAoGCCqGSM49 +AwEHoUQDQgAECockYXUlTnnGyVODuV5Af4hqHW5wjqpnIbOE15hNz67+UqzQ4Tgw +76zlk18Ed/x663TXzsz3W+eGprvDzsKaIQ== +-----END EC PRIVATE KEY----- diff --git a/test/fixtures/request.pem b/test/fixtures/request.pem deleted file mode 100644 index 51bc7f625..000000000 --- a/test/fixtures/request.pem +++ /dev/null @@ -1,11 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIBhDCB7gIBADBFMQswCQYDVQQGEwJubzETMBEGA1UECAwKU29tZS1TdGF0ZTEh -MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB -AQUAA4GNADCBiQKBgQC2tDv6v//aJX8HoX7hugfReoWftqVxX2WmO8CbBFc0qfEC -hrR/3sCNg8Y0squOmQ1deEElE4h1tFtmcI14Ll/NfVr4kKjspK3MFe4ZJmvbtO0W -ZxXgf72AhEhw0e1mYkufFsmwiGQZHzJVh2Yll7h5PmV2TXOgHVp2A8XWFmEIEwID -AQABoAAwDQYJKoZIhvcNAQEFBQADgYEAjsUXEARgfxZNkMjuUcudgU2w4JXS0gGI -JQ0U1LmU0vMDSKwqndMlvCbKzEgPbJnGJDI8D4MeINCJHa5Ceyb8c+jaJYUcCabl -lQW5Psn3+eWp8ncKlIycDRj1Qk615XuXtV0fhkrgQM2ZCm9LaQ1O1Gd/CzLihLjF -W0MmgMKMMRk= ------END CERTIFICATE REQUEST----- From d18c677dbd88f38ab8312d341f2b0284e1648713 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 26 May 2021 21:27:29 +0200 Subject: [PATCH 25/50] [security] Update link to point to published security advisories --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 9e3f475d4..0baf19a63 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,7 +27,7 @@ Once we have acknowledged receipt of your report and confirmed the bug ourselves we will work with you to fix the vulnerability and publicly acknowledge your responsible disclosure, if you wish. In addition to that we will create and publish a security advisory to -[GitHub Security Advisories](https://github.com/websockets/ws/security/advisories). +[GitHub Security Advisories](https://github.com/websockets/ws/security/advisories?state=published). ## History From 262e45ac93622ffcf5ad0c33a2acf1eab481a501 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 27 May 2021 08:55:10 +0200 Subject: [PATCH 26/50] [test] Rename certificates and private keys files Rename `ca1-cert.pem` to `ca-certificate.pem`, `ca1-key.pem` to `ca-key.pem`, `agent1-cert.pem` to `client-certificate.pem`, and `agent1-key.pem` to `client-key.pem`. --- test/fixtures/{ca1-cert.pem => ca-certificate.pem} | 0 test/fixtures/{ca1-key.pem => ca-key.pem} | 0 test/fixtures/{agent1-cert.pem => client-certificate.pem} | 0 test/fixtures/{agent1-key.pem => client-key.pem} | 0 test/websocket.test.js | 6 +++--- 5 files changed, 3 insertions(+), 3 deletions(-) rename test/fixtures/{ca1-cert.pem => ca-certificate.pem} (100%) rename test/fixtures/{ca1-key.pem => ca-key.pem} (100%) rename test/fixtures/{agent1-cert.pem => client-certificate.pem} (100%) rename test/fixtures/{agent1-key.pem => client-key.pem} (100%) diff --git a/test/fixtures/ca1-cert.pem b/test/fixtures/ca-certificate.pem similarity index 100% rename from test/fixtures/ca1-cert.pem rename to test/fixtures/ca-certificate.pem diff --git a/test/fixtures/ca1-key.pem b/test/fixtures/ca-key.pem similarity index 100% rename from test/fixtures/ca1-key.pem rename to test/fixtures/ca-key.pem diff --git a/test/fixtures/agent1-cert.pem b/test/fixtures/client-certificate.pem similarity index 100% rename from test/fixtures/agent1-cert.pem rename to test/fixtures/client-certificate.pem diff --git a/test/fixtures/agent1-key.pem b/test/fixtures/client-key.pem similarity index 100% rename from test/fixtures/agent1-key.pem rename to test/fixtures/client-key.pem diff --git a/test/websocket.test.js b/test/websocket.test.js index 8bfc9e151..03ae90be9 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2180,7 +2180,7 @@ describe('WebSocket', () => { it('connects to secure websocket server with client side certificate', (done) => { const server = https.createServer({ cert: fs.readFileSync('test/fixtures/certificate.pem'), - ca: [fs.readFileSync('test/fixtures/ca1-cert.pem')], + ca: [fs.readFileSync('test/fixtures/ca-certificate.pem')], key: fs.readFileSync('test/fixtures/key.pem'), requestCert: true }); @@ -2202,8 +2202,8 @@ describe('WebSocket', () => { server.listen(0, () => { const ws = new WebSocket(`wss://localhost:${server.address().port}`, { - cert: fs.readFileSync('test/fixtures/agent1-cert.pem'), - key: fs.readFileSync('test/fixtures/agent1-key.pem'), + cert: fs.readFileSync('test/fixtures/client-certificate.pem'), + key: fs.readFileSync('test/fixtures/client-key.pem'), rejectUnauthorized: false }); }); From edff6bb01f1102ad2cc389ad25fce7a6aef40f72 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 28 May 2021 11:38:47 +0200 Subject: [PATCH 27/50] [test] Fix nit --- test/websocket.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/websocket.test.js b/test/websocket.test.js index 03ae90be9..d7cf3aee8 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1487,7 +1487,7 @@ describe('WebSocket', () => { it('closes the connection if called while connecting (3/3)', (done) => { const server = http.createServer(); - server.listen(0, function () { + server.listen(0, () => { const ws = new WebSocket(`ws://localhost:${server.address().port}`); ws.on('open', () => done(new Error("Unexpected 'open' event"))); From 7ee31157d7b14bb94e0d0fd223a4a5508f4c39b9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 29 May 2021 21:21:01 +0200 Subject: [PATCH 28/50] [doc] Add logo to coverage badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c6e5287c..261ab95b0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws) [![Build](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=build&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) [![Windows x86 Build](https://img.shields.io/appveyor/ci/lpinca/ws/master.svg?logo=appveyor)](https://ci.appveyor.com/project/lpinca/ws) -[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg)](https://coveralls.io/github/websockets/ws) +[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws) ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and server implementation. From 03a707884c591d56ad69c4c1ddd34cab0449b1fe Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 31 May 2021 18:59:21 +0200 Subject: [PATCH 29/50] [doc] Remove unsafe regex from code snippet --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 261ab95b0..c16df2036 100644 --- a/README.md +++ b/README.md @@ -395,7 +395,7 @@ the `X-Forwarded-For` header. ```js wss.on('connection', function connection(ws, req) { - const ip = req.headers['x-forwarded-for'].split(/\s*,\s*/)[0]; + const ip = req.headers['x-forwarded-for'].split(',')[0].trim(); }); ``` From 05b8ccd639a91428d7440ad350b8d4301636b2e2 Mon Sep 17 00:00:00 2001 From: Mestery Date: Sat, 5 Jun 2021 21:02:44 +0200 Subject: [PATCH 30/50] [doc] Fix broken link (#1897) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c16df2036..1cb19d650 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ can use one of the many wrappers available on npm, like - [Protocol support](#protocol-support) - [Installing](#installing) - - [Opt-in for performance and spec compliance](#opt-in-for-performance-and-spec-compliance) + - [Opt-in for performance](#opt-in-for-performance) - [API docs](#api-docs) - [WebSocket compression](#websocket-compression) - [Usage examples](#usage-examples) From 8806aa9a836c3a616c9511adad159c65eeb153b0 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 8 Jun 2021 11:29:50 +0200 Subject: [PATCH 31/50] [fix] Close the connection cleanly when an error occurs Instead of destroying the socket, try to close the connection cleanly if an error (such as a data framing error) occurs after the opening handshake has completed. Also, to comply with the specification, use the 1006 status code if no close frame is received, even if the connection is closed due to an error. Fixes #1898 --- lib/websocket.js | 5 ++--- test/websocket.test.js | 23 +++++++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 83b471d94..40ab4e86a 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -808,11 +808,10 @@ function receiverOnError(err) { const websocket = this[kWebSocket]; websocket._socket.removeListener('data', socketOnData); + websocket._socket.resume(); - websocket._readyState = WebSocket.CLOSING; - websocket._closeCode = err[kStatusCode]; + websocket.close(err[kStatusCode]); websocket.emit('error', err); - websocket._socket.destroy(); } /** diff --git a/test/websocket.test.js b/test/websocket.test.js index d7cf3aee8..a0557980e 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -429,6 +429,7 @@ describe('WebSocket', () => { describe('Events', () => { it("emits an 'error' event if an error occurs", (done) => { + let clientCloseEventEmitted = false; const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); @@ -440,14 +441,21 @@ describe('WebSocket', () => { ); ws.on('close', (code, reason) => { - assert.strictEqual(code, 1002); + clientCloseEventEmitted = true; + assert.strictEqual(code, 1006); assert.strictEqual(reason, ''); - wss.close(done); }); }); }); wss.on('connection', (ws) => { + ws.on('close', (code, reason) => { + assert.ok(clientCloseEventEmitted); + assert.strictEqual(code, 1002); + assert.strictEqual(reason, ''); + wss.close(done); + }); + ws._socket.write(Buffer.from([0x85, 0x00])); }); }); @@ -1410,10 +1418,17 @@ describe('WebSocket', () => { }); it('honors the `mask` option', (done) => { + let serverClientCloseEventEmitted = false; const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); ws.on('open', () => ws.send('hi', { mask: false })); + ws.on('close', (code, reason) => { + assert.ok(serverClientCloseEventEmitted); + assert.strictEqual(code, 1002); + assert.strictEqual(reason, ''); + wss.close(done); + }); }); wss.on('connection', (ws) => { @@ -1434,9 +1449,9 @@ describe('WebSocket', () => { ); ws.on('close', (code, reason) => { - assert.strictEqual(code, 1002); + serverClientCloseEventEmitted = true; + assert.strictEqual(code, 1006); assert.strictEqual(reason, ''); - wss.close(done); }); }); }); From 074e6a8be7275a69a407f6c1fa2270c754d2834b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 9 Jun 2021 09:14:33 +0200 Subject: [PATCH 32/50] [fix] Don't call `ws.terminate()` unconditionally in `duplex._destroy()` Call `ws.terminate()` only if `duplex.destroy()` is called directly by the user and not indirectly by the listener of the `'error'` event of the `WebSocket` object. Calling `ws.terminate()` right after the `'error'` event is emitted on the `WebSocket` object, might prevent the close frame from being sent to the other peer. --- lib/stream.js | 14 +++++++++++++- test/create-websocket-stream.test.js | 9 ++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/stream.js b/lib/stream.js index 604cf366b..53706cc90 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -48,6 +48,7 @@ function duplexOnError(err) { */ function createWebSocketStream(ws, options) { let resumeOnReceiverDrain = true; + let terminateOnDestroy = true; function receiverOnDrain() { if (resumeOnReceiverDrain) ws._socket.resume(); @@ -81,6 +82,16 @@ function createWebSocketStream(ws, options) { ws.once('error', function error(err) { if (duplex.destroyed) return; + // Prevent `ws.terminate()` from being called by `duplex._destroy()`. + // + // - If the state of the `WebSocket` connection is `CONNECTING`, + // `ws.terminate()` is a noop as no socket was assigned. + // - Otherwise, the error was re-emitted from the listener of the `'error'` + // event of the `Receiver` object. The listener already closes the + // connection by calling `ws.close()`. This allows a close frame to be + // sent to the other peer. If `ws.terminate()` is called right after this, + // the close frame might not be sent. + terminateOnDestroy = false; duplex.destroy(err); }); @@ -108,7 +119,8 @@ function createWebSocketStream(ws, options) { if (!called) callback(err); process.nextTick(emitClose, duplex); }); - ws.terminate(); + + if (terminateOnDestroy) ws.terminate(); }; duplex._final = function (callback) { diff --git a/test/create-websocket-stream.test.js b/test/create-websocket-stream.test.js index 5da01bb18..ddccb56b2 100644 --- a/test/create-websocket-stream.test.js +++ b/test/create-websocket-stream.test.js @@ -203,6 +203,7 @@ describe('createWebSocketStream', () => { }); it('reemits errors', (done) => { + let duplexCloseEventEmitted = false; const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); const duplex = createWebSocketStream(ws); @@ -215,13 +216,19 @@ describe('createWebSocketStream', () => { ); duplex.on('close', () => { - wss.close(done); + duplexCloseEventEmitted = true; }); }); }); wss.on('connection', (ws) => { ws._socket.write(Buffer.from([0x85, 0x00])); + ws.on('close', (code, reason) => { + assert.ok(duplexCloseEventEmitted); + assert.strictEqual(code, 1002); + assert.strictEqual(reason, ''); + wss.close(done); + }); }); }); From c6e30806704cd1ff35282b85132bd29fca8acec8 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 15 Jun 2021 06:22:01 -0700 Subject: [PATCH 33/50] [minor] Attach error codes to all receiver errors (#1901) Fixes #1892 --- doc/ws.md | 66 ++++++++++++- lib/permessage-deflate.js | 1 + lib/receiver.js | 138 +++++++++++++++++++++++---- test/create-websocket-stream.test.js | 1 + test/receiver.test.js | 22 +++++ test/websocket.test.js | 1 + 6 files changed, 209 insertions(+), 20 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index de62e6756..7212a1c01 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -44,6 +44,18 @@ - [websocket.terminate()](#websocketterminate) - [websocket.url](#websocketurl) - [WebSocket.createWebSocketStream(websocket[, options])](#websocketcreatewebsocketstreamwebsocket-options) +- [WS Error Codes](#ws-error-codes) + - [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#wserrunsupporteddatapayloadlength) + - [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#wserrunsupporteddatapayloadlength) + - [WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH](#wserrinvalidcontrolpayloadlength) + - [WS_ERR_INVALID_UTF8](#wserrinvalidutf8) + - [WS_ERR_INVALID_OPCODE](#wserrinvalidopcode) + - [WS_ERR_INVALID_CLOSE_CODE](#wserrinvalidclosecode) + - [WS_ERR_UNEXPECTED_RSV_1](#wserrunexpectedrsv1) + - [WS_ERR_UNEXPECTED_RSV_2_3](#wserrunexpectedrsv23) + - [WS_ERR_EXPECTED_FIN](#wserrexpectedfin) + - [WS_ERR_EXPECTED_MASK](#wserrexpectedmask) + - [WS_ERR_UNEXPECTED_MASK](#wserrunexpectedmask) ## Class: WebSocket.Server @@ -298,7 +310,8 @@ human-readable string explaining why the connection has been closed. - `error` {Error} -Emitted when an error occurs. +Emitted when an error occurs. Errors may have a `.code` property, matching one +of the string values defined below under [WS Error Codes](#ws-error-codes). ### Event: 'message' @@ -493,6 +506,57 @@ The URL of the WebSocket server. Server clients don't have this attribute. Returns a `Duplex` stream that allows to use the Node.js streams API on top of a given `WebSocket`. +## WS Error Codes + +Errors emitted by the websocket may have a `.code` property, describing the +specific type of error that has occurred: + +### WS_ERR_UNSUPPORTED_MESSAGE_LENGTH + +A message was received with a length longer than the maximum supported length, +as configured by the `maxPayload` option. + +### WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH + +A data frame was received with a length longer the max supported length (2^53-1, +due to JavaScript language limitations). + +### WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH + +A control frame with an invalid payload length was received. + +### WS_ERR_INVALID_UTF8 + +A text or close frame was received containing invalid UTF-8 data. + +### WS_ERR_INVALID_OPCODE + +A WebSocket frame was received with an invalid opcode. + +### WS_ERR_INVALID_CLOSE_CODE + +A WebSocket close frame was received with an invalid close code. + +### WS_ERR_UNEXPECTED_RSV_1 + +A WebSocket frame was received with the RSV1 bit set unexpectedly. + +### WS_ERR_UNEXPECTED_RSV_2_3 + +A WebSocket frame was received with the RSV2 or RSV3 bit set unexpectedly. + +### WS_ERR_EXPECTED_FIN + +A WebSocket frame was received with the FIN bit not set when it was expected. + +### WS_ERR_EXPECTED_MASK + +An unmasked WebSocket frame was received by a WebSocket server. + +### WS_ERR_UNEXPECTED_MASK + +A masked WebSocket frame was received by a WebSocket client. + [concurrency-limit]: https://github.com/websockets/ws/issues/1202 [duplex-options]: https://nodejs.org/api/stream.html#stream_new_stream_duplex_options diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js index a8974b988..ce9178429 100644 --- a/lib/permessage-deflate.js +++ b/lib/permessage-deflate.js @@ -495,6 +495,7 @@ function inflateOnData(chunk) { } this[kError] = new RangeError('Max payload size exceeded'); + this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; this[kError][kStatusCode] = 1009; this.removeListener('data', inflateOnData); this.reset(); diff --git a/lib/receiver.js b/lib/receiver.js index 65a5ab45f..7ed992214 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -168,14 +168,26 @@ class Receiver extends Writable { if ((buf[0] & 0x30) !== 0x00) { this._loop = false; - return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002); + return error( + RangeError, + 'RSV2 and RSV3 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_2_3' + ); } const compressed = (buf[0] & 0x40) === 0x40; if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } this._fin = (buf[0] & 0x80) === 0x80; @@ -185,31 +197,61 @@ class Receiver extends Writable { if (this._opcode === 0x00) { if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } if (!this._fragmented) { this._loop = false; - return error(RangeError, 'invalid opcode 0', true, 1002); + return error( + RangeError, + 'invalid opcode 0', + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._opcode = this._fragmented; } else if (this._opcode === 0x01 || this._opcode === 0x02) { if (this._fragmented) { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._compressed = compressed; } else if (this._opcode > 0x07 && this._opcode < 0x0b) { if (!this._fin) { this._loop = false; - return error(RangeError, 'FIN must be set', true, 1002); + return error( + RangeError, + 'FIN must be set', + true, + 1002, + 'WS_ERR_EXPECTED_FIN' + ); } if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } if (this._payloadLength > 0x7d) { @@ -218,12 +260,19 @@ class Receiver extends Writable { RangeError, `invalid payload length ${this._payloadLength}`, true, - 1002 + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' ); } } else { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } if (!this._fin && !this._fragmented) this._fragmented = this._opcode; @@ -232,11 +281,23 @@ class Receiver extends Writable { if (this._isServer) { if (!this._masked) { this._loop = false; - return error(RangeError, 'MASK must be set', true, 1002); + return error( + RangeError, + 'MASK must be set', + true, + 1002, + 'WS_ERR_EXPECTED_MASK' + ); } } else if (this._masked) { this._loop = false; - return error(RangeError, 'MASK must be clear', true, 1002); + return error( + RangeError, + 'MASK must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_MASK' + ); } if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; @@ -285,7 +346,8 @@ class Receiver extends Writable { RangeError, 'Unsupported WebSocket frame: payload length > 2^53 - 1', false, - 1009 + 1009, + 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' ); } @@ -304,7 +366,13 @@ class Receiver extends Writable { this._totalPayloadLength += this._payloadLength; if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { this._loop = false; - return error(RangeError, 'Max payload size exceeded', false, 1009); + return error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); } } @@ -384,7 +452,13 @@ class Receiver extends Writable { this._messageLength += buf.length; if (this._messageLength > this._maxPayload && this._maxPayload > 0) { return cb( - error(RangeError, 'Max payload size exceeded', false, 1009) + error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ) ); } @@ -431,7 +505,13 @@ class Receiver extends Writable { if (!isValidUTF8(buf)) { this._loop = false; - return error(Error, 'invalid UTF-8 sequence', true, 1007); + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } this.emit('message', buf.toString()); @@ -456,18 +536,36 @@ class Receiver extends Writable { this.emit('conclude', 1005, ''); this.end(); } else if (data.length === 1) { - return error(RangeError, 'invalid payload length 1', true, 1002); + return error( + RangeError, + 'invalid payload length 1', + true, + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' + ); } else { const code = data.readUInt16BE(0); if (!isValidStatusCode(code)) { - return error(RangeError, `invalid status code ${code}`, true, 1002); + return error( + RangeError, + `invalid status code ${code}`, + true, + 1002, + 'WS_ERR_INVALID_CLOSE_CODE' + ); } const buf = data.slice(2); if (!isValidUTF8(buf)) { - return error(Error, 'invalid UTF-8 sequence', true, 1007); + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } this.emit('conclude', code, buf.toString()); @@ -493,15 +591,17 @@ module.exports = Receiver; * @param {Boolean} prefix Specifies whether or not to add a default prefix to * `message` * @param {Number} statusCode The status code + * @param {String} errorCode The exposed error code * @return {(Error|RangeError)} The error * @private */ -function error(ErrorCtor, message, prefix, statusCode) { +function error(ErrorCtor, message, prefix, statusCode, errorCode) { const err = new ErrorCtor( prefix ? `Invalid WebSocket frame: ${message}` : message ); Error.captureStackTrace(err, error); + err.code = errorCode; err[kStatusCode] = statusCode; return err; } diff --git a/test/create-websocket-stream.test.js b/test/create-websocket-stream.test.js index ddccb56b2..bcd240974 100644 --- a/test/create-websocket-stream.test.js +++ b/test/create-websocket-stream.test.js @@ -210,6 +210,7 @@ describe('createWebSocketStream', () => { duplex.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid opcode 5' diff --git a/test/receiver.test.js b/test/receiver.test.js index a70cc8dbe..cd5770dfb 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -513,6 +513,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_1'); assert.strictEqual( err.message, 'Invalid WebSocket frame: RSV1 must be clear' @@ -534,6 +535,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_1'); assert.strictEqual( err.message, 'Invalid WebSocket frame: RSV1 must be clear' @@ -550,6 +552,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_2_3'); assert.strictEqual( err.message, 'Invalid WebSocket frame: RSV2 and RSV3 must be clear' @@ -566,6 +569,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_2_3'); assert.strictEqual( err.message, 'Invalid WebSocket frame: RSV2 and RSV3 must be clear' @@ -582,6 +586,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid opcode 0' @@ -598,6 +603,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid opcode 1' @@ -615,6 +621,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid opcode 2' @@ -632,6 +639,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_EXPECTED_FIN'); assert.strictEqual( err.message, 'Invalid WebSocket frame: FIN must be set' @@ -653,6 +661,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_1'); assert.strictEqual( err.message, 'Invalid WebSocket frame: RSV1 must be clear' @@ -669,6 +678,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_EXPECTED_FIN'); assert.strictEqual( err.message, 'Invalid WebSocket frame: FIN must be set' @@ -685,6 +695,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_EXPECTED_MASK'); assert.strictEqual( err.message, 'Invalid WebSocket frame: MASK must be set' @@ -701,6 +712,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_MASK'); assert.strictEqual( err.message, 'Invalid WebSocket frame: MASK must be clear' @@ -719,6 +731,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid payload length 126' @@ -735,6 +748,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH'); assert.strictEqual( err.message, 'Unsupported WebSocket frame: payload length > 2^53 - 1' @@ -756,6 +770,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof Error); + assert.strictEqual(err.code, 'WS_ERR_INVALID_UTF8'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid UTF-8 sequence' @@ -778,6 +793,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof Error); + assert.strictEqual(err.code, 'WS_ERR_INVALID_UTF8'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid UTF-8 sequence' @@ -799,6 +815,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid payload length 1' @@ -815,6 +832,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_CLOSE_CODE'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid status code 0' @@ -831,6 +849,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof Error); + assert.strictEqual(err.code, 'WS_ERR_INVALID_UTF8'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid UTF-8 sequence' @@ -860,6 +879,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'); assert.strictEqual(err.message, 'Max payload size exceeded'); assert.strictEqual(err[kStatusCode], 1009); done(); @@ -884,6 +904,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'); assert.strictEqual(err.message, 'Max payload size exceeded'); assert.strictEqual(err[kStatusCode], 1009); done(); @@ -913,6 +934,7 @@ describe('Receiver', () => { receiver.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'); assert.strictEqual(err.message, 'Max payload size exceeded'); assert.strictEqual(err[kStatusCode], 1009); done(); diff --git a/test/websocket.test.js b/test/websocket.test.js index a0557980e..03f984fcb 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -435,6 +435,7 @@ describe('WebSocket', () => { ws.on('error', (err) => { assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); assert.strictEqual( err.message, 'Invalid WebSocket frame: invalid opcode 5' From bb5d44b11880861f9fb0429e2c132f435a78198b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 15 Jun 2021 15:48:30 +0200 Subject: [PATCH 34/50] [doc] Sort error codes alphabetically --- doc/ws.md | 54 +++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 7212a1c01..3f041627f 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -45,17 +45,17 @@ - [websocket.url](#websocketurl) - [WebSocket.createWebSocketStream(websocket[, options])](#websocketcreatewebsocketstreamwebsocket-options) - [WS Error Codes](#ws-error-codes) - - [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#wserrunsupporteddatapayloadlength) - - [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#wserrunsupporteddatapayloadlength) + - [WS_ERR_EXPECTED_FIN](#wserrexpectedfin) + - [WS_ERR_EXPECTED_MASK](#wserrexpectedmask) + - [WS_ERR_INVALID_CLOSE_CODE](#wserrinvalidclosecode) - [WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH](#wserrinvalidcontrolpayloadlength) - - [WS_ERR_INVALID_UTF8](#wserrinvalidutf8) - [WS_ERR_INVALID_OPCODE](#wserrinvalidopcode) - - [WS_ERR_INVALID_CLOSE_CODE](#wserrinvalidclosecode) + - [WS_ERR_INVALID_UTF8](#wserrinvalidutf8) + - [WS_ERR_UNEXPECTED_MASK](#wserrunexpectedmask) - [WS_ERR_UNEXPECTED_RSV_1](#wserrunexpectedrsv1) - [WS_ERR_UNEXPECTED_RSV_2_3](#wserrunexpectedrsv23) - - [WS_ERR_EXPECTED_FIN](#wserrexpectedfin) - - [WS_ERR_EXPECTED_MASK](#wserrexpectedmask) - - [WS_ERR_UNEXPECTED_MASK](#wserrunexpectedmask) + - [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#wserrunsupporteddatapayloadlength) + - [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#wserrunsupporteddatapayloadlength) ## Class: WebSocket.Server @@ -511,31 +511,33 @@ given `WebSocket`. Errors emitted by the websocket may have a `.code` property, describing the specific type of error that has occurred: -### WS_ERR_UNSUPPORTED_MESSAGE_LENGTH +### WS_ERR_EXPECTED_FIN -A message was received with a length longer than the maximum supported length, -as configured by the `maxPayload` option. +A WebSocket frame was received with the FIN bit not set when it was expected. -### WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH +### WS_ERR_EXPECTED_MASK -A data frame was received with a length longer the max supported length (2^53-1, -due to JavaScript language limitations). +An unmasked WebSocket frame was received by a WebSocket server. -### WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH +### WS_ERR_INVALID_CLOSE_CODE -A control frame with an invalid payload length was received. +A WebSocket close frame was received with an invalid close code. -### WS_ERR_INVALID_UTF8 +### WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH -A text or close frame was received containing invalid UTF-8 data. +A control frame with an invalid payload length was received. ### WS_ERR_INVALID_OPCODE A WebSocket frame was received with an invalid opcode. -### WS_ERR_INVALID_CLOSE_CODE +### WS_ERR_INVALID_UTF8 -A WebSocket close frame was received with an invalid close code. +A text or close frame was received containing invalid UTF-8 data. + +### WS_ERR_UNEXPECTED_MASK + +A masked WebSocket frame was received by a WebSocket client. ### WS_ERR_UNEXPECTED_RSV_1 @@ -545,17 +547,15 @@ A WebSocket frame was received with the RSV1 bit set unexpectedly. A WebSocket frame was received with the RSV2 or RSV3 bit set unexpectedly. -### WS_ERR_EXPECTED_FIN - -A WebSocket frame was received with the FIN bit not set when it was expected. - -### WS_ERR_EXPECTED_MASK +### WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH -An unmasked WebSocket frame was received by a WebSocket server. +A data frame was received with a length longer the max supported length (2^53-1, +due to JavaScript language limitations). -### WS_ERR_UNEXPECTED_MASK +### WS_ERR_UNSUPPORTED_MESSAGE_LENGTH -A masked WebSocket frame was received by a WebSocket client. +A message was received with a length longer than the maximum supported length, +as configured by the `maxPayload` option. [concurrency-limit]: https://github.com/websockets/ws/issues/1202 [duplex-options]: From 6eea0d466b08a278c048092ee1cb06aee9f48cc9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 15 Jun 2021 15:50:18 +0200 Subject: [PATCH 35/50] [doc] Fix typo --- doc/ws.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 3f041627f..56681ade5 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -549,8 +549,8 @@ A WebSocket frame was received with the RSV2 or RSV3 bit set unexpectedly. ### WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH -A data frame was received with a length longer the max supported length (2^53-1, -due to JavaScript language limitations). +A data frame was received with a length longer than the max supported length +(2^53 - 1, due to JavaScript language limitations). ### WS_ERR_UNSUPPORTED_MESSAGE_LENGTH From 1d3f4cbb0ebb2519f6cc707e9f4344006d74ce03 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 15 Jun 2021 16:20:37 +0200 Subject: [PATCH 36/50] [doc] Fix anchor tags for error codes --- doc/ws.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 56681ade5..af0dfb325 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -45,17 +45,17 @@ - [websocket.url](#websocketurl) - [WebSocket.createWebSocketStream(websocket[, options])](#websocketcreatewebsocketstreamwebsocket-options) - [WS Error Codes](#ws-error-codes) - - [WS_ERR_EXPECTED_FIN](#wserrexpectedfin) - - [WS_ERR_EXPECTED_MASK](#wserrexpectedmask) - - [WS_ERR_INVALID_CLOSE_CODE](#wserrinvalidclosecode) - - [WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH](#wserrinvalidcontrolpayloadlength) - - [WS_ERR_INVALID_OPCODE](#wserrinvalidopcode) - - [WS_ERR_INVALID_UTF8](#wserrinvalidutf8) - - [WS_ERR_UNEXPECTED_MASK](#wserrunexpectedmask) - - [WS_ERR_UNEXPECTED_RSV_1](#wserrunexpectedrsv1) - - [WS_ERR_UNEXPECTED_RSV_2_3](#wserrunexpectedrsv23) - - [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#wserrunsupporteddatapayloadlength) - - [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#wserrunsupporteddatapayloadlength) + - [WS_ERR_EXPECTED_FIN](#ws_err_expected_fin) + - [WS_ERR_EXPECTED_MASK](#ws_err_expected_mask) + - [WS_ERR_INVALID_CLOSE_CODE](#ws_err_invalid_close_code) + - [WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH](#ws_err_invalid_control_payload_length) + - [WS_ERR_INVALID_OPCODE](#ws_err_invalid_opcode) + - [WS_ERR_INVALID_UTF8](#ws_err_invalid_utf8) + - [WS_ERR_UNEXPECTED_MASK](#ws_err_unexpected_mask) + - [WS_ERR_UNEXPECTED_RSV_1](#ws_err_unexpected_rsv_1) + - [WS_ERR_UNEXPECTED_RSV_2_3](#ws_err_unexpected_rsv_2_3) + - [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#ws_err_unsupported_data_payload_length) + - [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#ws_err_unsupported_message_length) ## Class: WebSocket.Server From e3f0c1720aab640fe78dc578907046fb84422ccd Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 16 Jun 2021 15:13:32 +0200 Subject: [PATCH 37/50] [dist] 7.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ab6e3769..9f38e4660 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.6", + "version": "7.5.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From 145480a5b520ee951d848009d51069bfd7ed928c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 20 Jun 2021 19:48:58 +0200 Subject: [PATCH 38/50] [test] Fix repeated typo --- test/websocket-server.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 71646e5a4..ce6617ec6 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -471,7 +471,7 @@ describe('WebSocketServer', () => { }); }); - it('fails is the Sec-WebSocket-Version header is invalid (1/2)', (done) => { + it('fails if the Sec-WebSocket-Version header is invalid (1/2)', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const req = http.get({ port: wss.address().port, @@ -493,7 +493,7 @@ describe('WebSocketServer', () => { }); }); - it('fails is the Sec-WebSocket-Version header is invalid (2/2)', (done) => { + it('fails if the Sec-WebSocket-Version header is invalid (2/2)', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const req = http.get({ port: wss.address().port, @@ -516,7 +516,7 @@ describe('WebSocketServer', () => { }); }); - it('fails is the Sec-WebSocket-Extensions header is invalid', (done) => { + it('fails if the Sec-WebSocket-Extensions header is invalid', (done) => { const wss = new WebSocket.Server( { perMessageDeflate: true, From c3fdc994502cfb2f9a1274e78530a08609f5efb1 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 28 Jun 2021 11:16:11 +0200 Subject: [PATCH 39/50] [minor] Fix misleading comment --- lib/stream.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/stream.js b/lib/stream.js index 53706cc90..c3a793691 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -84,13 +84,13 @@ function createWebSocketStream(ws, options) { // Prevent `ws.terminate()` from being called by `duplex._destroy()`. // - // - If the state of the `WebSocket` connection is `CONNECTING`, - // `ws.terminate()` is a noop as no socket was assigned. - // - Otherwise, the error was re-emitted from the listener of the `'error'` + // - If the `'error'` event is emitted before the `'open'` event, then + // `ws.terminate()` is a noop as no socket is assigned. + // - Otherwise, the error is re-emitted by the listener of the `'error'` // event of the `Receiver` object. The listener already closes the // connection by calling `ws.close()`. This allows a close frame to be // sent to the other peer. If `ws.terminate()` is called right after this, - // the close frame might not be sent. + // then the close frame might not be sent. terminateOnDestroy = false; duplex.destroy(err); }); From b434b9f1653d6fda562c937f65b1f07f81c6aa1a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 25 Jun 2021 22:08:01 +0200 Subject: [PATCH 40/50] [fix] Fix close edge cases Ensure that `socket.end()` is called if an error occurs simultaneously on both peers. Refs: https://github.com/websockets/ws/pull/1902 --- lib/websocket.js | 16 +++- test/create-websocket-stream.test.js | 8 +- test/websocket.test.js | 136 +++++++++++++++++++++++++-- 3 files changed, 150 insertions(+), 10 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 40ab4e86a..280364a1b 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -225,7 +225,13 @@ class WebSocket extends EventEmitter { } if (this.readyState === WebSocket.CLOSING) { - if (this._closeFrameSent && this._closeFrameReceived) this._socket.end(); + if ( + this._closeFrameSent && + (this._closeFrameReceived || this._receiver._writableState.errorEmitted) + ) { + this._socket.end(); + } + return; } @@ -238,7 +244,13 @@ class WebSocket extends EventEmitter { if (err) return; this._closeFrameSent = true; - if (this._closeFrameReceived) this._socket.end(); + + if ( + this._closeFrameReceived || + this._receiver._writableState.errorEmitted + ) { + this._socket.end(); + } }); // diff --git a/test/create-websocket-stream.test.js b/test/create-websocket-stream.test.js index bcd240974..b96ac9b1b 100644 --- a/test/create-websocket-stream.test.js +++ b/test/create-websocket-stream.test.js @@ -204,6 +204,8 @@ describe('createWebSocketStream', () => { it('reemits errors', (done) => { let duplexCloseEventEmitted = false; + let serverClientCloseEventEmitted = false; + const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); const duplex = createWebSocketStream(ws); @@ -218,6 +220,7 @@ describe('createWebSocketStream', () => { duplex.on('close', () => { duplexCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); }); }); }); @@ -225,10 +228,11 @@ describe('createWebSocketStream', () => { wss.on('connection', (ws) => { ws._socket.write(Buffer.from([0x85, 0x00])); ws.on('close', (code, reason) => { - assert.ok(duplexCloseEventEmitted); assert.strictEqual(code, 1002); assert.strictEqual(reason, ''); - wss.close(done); + + serverClientCloseEventEmitted = true; + if (duplexCloseEventEmitted) wss.close(done); }); }); }); diff --git a/test/websocket.test.js b/test/websocket.test.js index 03f984fcb..21126b824 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -430,6 +430,8 @@ describe('WebSocket', () => { describe('Events', () => { it("emits an 'error' event if an error occurs", (done) => { let clientCloseEventEmitted = false; + let serverClientCloseEventEmitted = false; + const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); @@ -442,19 +444,22 @@ describe('WebSocket', () => { ); ws.on('close', (code, reason) => { - clientCloseEventEmitted = true; assert.strictEqual(code, 1006); assert.strictEqual(reason, ''); + + clientCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); }); }); }); wss.on('connection', (ws) => { ws.on('close', (code, reason) => { - assert.ok(clientCloseEventEmitted); assert.strictEqual(code, 1002); assert.strictEqual(reason, ''); - wss.close(done); + + serverClientCloseEventEmitted = true; + if (clientCloseEventEmitted) wss.close(done); }); ws._socket.write(Buffer.from([0x85, 0x00])); @@ -1419,16 +1424,19 @@ describe('WebSocket', () => { }); it('honors the `mask` option', (done) => { + let clientCloseEventEmitted = false; let serverClientCloseEventEmitted = false; + const wss = new WebSocket.Server({ port: 0 }, () => { const ws = new WebSocket(`ws://localhost:${wss.address().port}`); ws.on('open', () => ws.send('hi', { mask: false })); ws.on('close', (code, reason) => { - assert.ok(serverClientCloseEventEmitted); assert.strictEqual(code, 1002); assert.strictEqual(reason, ''); - wss.close(done); + + clientCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); }); }); @@ -1450,9 +1458,11 @@ describe('WebSocket', () => { ); ws.on('close', (code, reason) => { - serverClientCloseEventEmitted = true; assert.strictEqual(code, 1006); assert.strictEqual(reason, ''); + + serverClientCloseEventEmitted = true; + if (clientCloseEventEmitted) wss.close(done); }); }); }); @@ -2760,4 +2770,118 @@ describe('WebSocket', () => { }); }); }); + + describe('Connection close edge cases', () => { + it('closes cleanly after simultaneous errors (1/2)', (done) => { + let clientCloseEventEmitted = false; + let serverClientCloseEventEmitted = false; + + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, ''); + + clientCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); + }); + }); + + ws.on('open', () => { + // Write an invalid frame in both directions to trigger simultaneous + // failure. + const chunk = Buffer.from([0x85, 0x00]); + + wss.clients.values().next().value._socket.write(chunk); + ws._socket.write(chunk); + }); + }); + + wss.on('connection', (ws) => { + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, ''); + + serverClientCloseEventEmitted = true; + if (clientCloseEventEmitted) wss.close(done); + }); + }); + }); + }); + + it('closes cleanly after simultaneous errors (2/2)', (done) => { + let clientCloseEventEmitted = false; + let serverClientCloseEventEmitted = false; + + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, ''); + + clientCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); + }); + }); + + ws.on('open', () => { + // Write an invalid frame in both directions and change the + // `readyState` to `WebSocket.CLOSING`. + const chunk = Buffer.from([0x85, 0x00]); + const serverWs = wss.clients.values().next().value; + + serverWs._socket.write(chunk); + serverWs.close(); + + ws._socket.write(chunk); + ws.close(); + }); + }); + + wss.on('connection', (ws) => { + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, ''); + + serverClientCloseEventEmitted = true; + if (clientCloseEventEmitted) wss.close(done); + }); + }); + }); + }); + }); }); From 2916006477bd50d5a7513640fcb610f7fd0dddda Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 22 Jun 2021 14:43:34 +0200 Subject: [PATCH 41/50] [test] Add more tests for `WebSocket.prototype.close()` --- test/websocket.test.js | 81 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/test/websocket.test.js b/test/websocket.test.js index 21126b824..5642d968d 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -2638,6 +2638,87 @@ describe('WebSocket', () => { }); }); + it('handles a close frame received while compressing data', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws._receiver.on('conclude', () => { + assert.ok(ws._sender._deflating); + }); + + ws.send('foo'); + ws.send('bar'); + ws.send('baz'); + ws.send('qux'); + }); + } + ); + + wss.on('connection', (ws) => { + const messages = []; + + ws.on('message', (message) => { + messages.push(message); + }); + + ws.on('close', (code, reason) => { + assert.deepStrictEqual(messages, ['foo', 'bar', 'baz', 'qux']); + assert.strictEqual(code, 1000); + assert.strictEqual(reason, ''); + wss.close(done); + }); + + ws.close(1000); + }); + }); + + describe('#close', () => { + it('can be used while data is being decompressed', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const messages = []; + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws._socket.on('end', () => { + assert.strictEqual(ws._receiver._state, 5); + }); + }); + + ws.on('message', (message) => { + if (messages.push(message) > 1) return; + + ws.close(1000); + }); + + ws.on('close', (code, reason) => { + assert.deepStrictEqual(messages, ['', '', '', '']); + assert.strictEqual(code, 1000); + assert.strictEqual(reason, ''); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + const buf = Buffer.from('c10100c10100c10100c10100', 'hex'); + ws._socket.write(buf); + }); + }); + }); + describe('#send', () => { it('ignores the `compress` option if the extension is disabled', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { From 38c6c734daf8e15d5cd902ed3e47b8651fd1032c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 29 Jun 2021 06:53:49 +0200 Subject: [PATCH 42/50] [dist] 7.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f38e4660..1fb2075f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.5.0", + "version": "7.5.1", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From aca94c86e000675900b09729559e405f9207d154 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 3 Jul 2021 09:21:39 +0200 Subject: [PATCH 43/50] [fix] Abort the handshake if an unexpected extension is received Abort the handshake if the client receives a `Sec-WebSocket-Extensions` header but no extension was requested. Also abort the handshake if the server indicates an extension not requested by the client. --- lib/websocket.js | 52 +++++++++++++---- test/websocket.test.js | 126 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 13 deletions(-) diff --git a/lib/websocket.js b/lib/websocket.js index 280364a1b..50d85576a 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -660,22 +660,50 @@ function initAsClient(websocket, address, protocols, options) { if (serverProt) websocket._protocol = serverProt; - if (perMessageDeflate) { + const secWebSocketExtensions = res.headers['sec-websocket-extensions']; + + if (secWebSocketExtensions !== undefined) { + if (!perMessageDeflate) { + const message = + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested'; + abortHandshake(websocket, socket, message); + return; + } + + let extensions; + try { - const extensions = parse(res.headers['sec-websocket-extensions']); + extensions = parse(secWebSocketExtensions); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } + + const extensionNames = Object.keys(extensions); + + if (extensionNames.length) { + if ( + extensionNames.length !== 1 || + extensionNames[0] !== PerMessageDeflate.extensionName + ) { + const message = + 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } - if (extensions[PerMessageDeflate.extensionName]) { + try { perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); - websocket._extensions[PerMessageDeflate.extensionName] = - perMessageDeflate; + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; } - } catch (err) { - abortHandshake( - websocket, - socket, - 'Invalid Sec-WebSocket-Extensions header' - ); - return; + + websocket._extensions[PerMessageDeflate.extensionName] = + perMessageDeflate; } } diff --git a/test/websocket.test.js b/test/websocket.test.js index 5642d968d..a1a1cda43 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -663,7 +663,40 @@ describe('WebSocket', () => { }); }); - it('fails if the Sec-WebSocket-Extensions response header is invalid', (done) => { + it('fails if an unexpected Sec-WebSocket-Extensions header is received', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: foo\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + perMessageDeflate: false + }); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested' + ); + ws.on('close', () => done()); + }); + }); + + it('fails if the Sec-WebSocket-Extensions header is invalid (1/2)', (done) => { server.once('upgrade', (req, socket) => { const key = crypto .createHash('sha1') @@ -693,6 +726,97 @@ describe('WebSocket', () => { }); }); + it('fails if the Sec-WebSocket-Extensions header is invalid (2/2)', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: ' + + 'permessage-deflate; client_max_window_bits=7\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Invalid Sec-WebSocket-Extensions header' + ); + ws.on('close', () => done()); + }); + }); + + it('fails if an unexpected extension is received (1/2)', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: foo\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Server indicated an extension that was not requested' + ); + ws.on('close', () => done()); + }); + }); + + it('fails if an unexpected extension is received (2/2)', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: permessage-deflate,foo\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Server indicated an extension that was not requested' + ); + ws.on('close', () => done()); + }); + }); + it('fails if server sends a subprotocol when none was requested', (done) => { const wss = new WebSocket.Server({ server }); From 0ad1f9d6a48ed1b30bda09b958cb142c1e09cced Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 4 Jul 2021 07:22:35 +0200 Subject: [PATCH 44/50] [dist] 7.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1fb2075f3..85cea0fab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.5.1", + "version": "7.5.2", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", From ecb9d9ea8f126416f2c07a2a8485b1d1e4ab3989 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 7 Jul 2021 13:07:17 +0200 Subject: [PATCH 45/50] [minor] Improve JSDoc-inferred types (#1912) Refs: https://github.com/websockets/ws/pull/1910 --- lib/receiver.js | 4 +- lib/sender.js | 6 +- lib/stream.js | 4 +- lib/websocket-server.js | 26 +++++--- lib/websocket.js | 141 +++++++++++++++++++++++++++++++++------- 5 files changed, 145 insertions(+), 36 deletions(-) diff --git a/lib/receiver.js b/lib/receiver.js index 7ed992214..1d2af76e1 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -22,7 +22,7 @@ const INFLATING = 5; /** * HyBi Receiver implementation. * - * @extends stream.Writable + * @extends Writable */ class Receiver extends Writable { /** @@ -586,7 +586,7 @@ module.exports = Receiver; /** * Builds an error object. * - * @param {(Error|RangeError)} ErrorCtor The error constructor + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor * @param {String} message The error message * @param {Boolean} prefix Specifies whether or not to add a default prefix to * `message` diff --git a/lib/sender.js b/lib/sender.js index ad71e1950..441171c57 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -1,5 +1,9 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls$" }] */ + 'use strict'; +const net = require('net'); +const tls = require('tls'); const { randomFillSync } = require('crypto'); const PerMessageDeflate = require('./permessage-deflate'); @@ -16,7 +20,7 @@ class Sender { /** * Creates a Sender instance. * - * @param {net.Socket} socket The connection socket + * @param {(net.Socket|tls.Socket)} socket The connection socket * @param {Object} [extensions] An object containing the negotiated extensions */ constructor(socket, extensions) { diff --git a/lib/stream.js b/lib/stream.js index c3a793691..b0896ff83 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -5,7 +5,7 @@ const { Duplex } = require('stream'); /** * Emits the `'close'` event on a stream. * - * @param {stream.Duplex} The stream. + * @param {Duplex} stream The stream. * @private */ function emitClose(stream) { @@ -43,7 +43,7 @@ function duplexOnError(err) { * * @param {WebSocket} ws The `WebSocket` to wrap * @param {Object} [options] The options for the `Duplex` constructor - * @return {stream.Duplex} The duplex stream + * @return {Duplex} The duplex stream * @public */ function createWebSocketStream(ws, options) { diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 3c3bbe0b0..1610e767a 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -1,8 +1,13 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */ + 'use strict'; const EventEmitter = require('events'); +const http = require('http'); +const https = require('https'); +const net = require('net'); +const tls = require('tls'); const { createHash } = require('crypto'); -const { createServer, STATUS_CODES } = require('http'); const PerMessageDeflate = require('./permessage-deflate'); const WebSocket = require('./websocket'); @@ -34,7 +39,8 @@ class WebSocketServer extends EventEmitter { * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable * permessage-deflate * @param {Number} [options.port] The port where to bind the server - * @param {http.Server} [options.server] A pre-created HTTP/S server to use + * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S + * server to use * @param {Function} [options.verifyClient] A hook to reject connections * @param {Function} [callback] A listener for the `listening` event */ @@ -63,8 +69,8 @@ class WebSocketServer extends EventEmitter { } if (options.port != null) { - this._server = createServer((req, res) => { - const body = STATUS_CODES[426]; + this._server = http.createServer((req, res) => { + const body = http.STATUS_CODES[426]; res.writeHead(426, { 'Content-Length': body.length, @@ -173,7 +179,8 @@ class WebSocketServer extends EventEmitter { * Handle a HTTP Upgrade request. * * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @public @@ -252,7 +259,8 @@ class WebSocketServer extends EventEmitter { * @param {String} key The value of the `Sec-WebSocket-Key` header * @param {Object} extensions The accepted extensions * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @throws {Error} If called more than once with the same socket @@ -375,7 +383,7 @@ function socketOnError() { /** * Close the connection when preconditions are not fulfilled. * - * @param {net.Socket} socket The socket of the upgrade request + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} [message] The HTTP response body * @param {Object} [headers] Additional HTTP response headers @@ -383,7 +391,7 @@ function socketOnError() { */ function abortHandshake(socket, code, message, headers) { if (socket.writable) { - message = message || STATUS_CODES[code]; + message = message || http.STATUS_CODES[code]; headers = { Connection: 'close', 'Content-Type': 'text/html', @@ -392,7 +400,7 @@ function abortHandshake(socket, code, message, headers) { }; socket.write( - `HTTP/1.1 ${code} ${STATUS_CODES[code]}\r\n` + + `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + Object.keys(headers) .map((h) => `${h}: ${headers[h]}`) .join('\r\n') + diff --git a/lib/websocket.js b/lib/websocket.js index 50d85576a..baf493458 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -36,7 +36,7 @@ class WebSocket extends EventEmitter { /** * Create a new `WebSocket`. * - * @param {(String|url.URL)} address The URL to which to connect + * @param {(String|URL)} address The URL to which to connect * @param {(String|String[])} [protocols] The subprotocols * @param {Object} [options] Connection options */ @@ -112,6 +112,50 @@ class WebSocket extends EventEmitter { return Object.keys(this._extensions).join(); } + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onclose() { + return undefined; + } + + /* istanbul ignore next */ + set onclose(listener) {} + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onerror() { + return null; + } + + /* istanbul ignore next */ + set onerror(listener) {} + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onopen() { + return undefined; + } + + /* istanbul ignore next */ + set onopen(listener) {} + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onmessage() { + return undefined; + } + + /* istanbul ignore next */ + set onmessage(listener) {} + /** * @type {String} */ @@ -136,7 +180,8 @@ class WebSocket extends EventEmitter { /** * Set up the socket and the internal resources. * - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Number} [maxPayload=0] The maximum allowed message size * @private @@ -392,11 +437,76 @@ class WebSocket extends EventEmitter { } } -readyStates.forEach((readyState, i) => { - const descriptor = { enumerable: true, value: i }; +/** + * @constant {Number} CONNECTING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); - Object.defineProperty(WebSocket.prototype, readyState, descriptor); - Object.defineProperty(WebSocket, readyState, descriptor); +/** + * @constant {Number} CONNECTING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') }); [ @@ -416,14 +526,7 @@ readyStates.forEach((readyState, i) => { // ['open', 'error', 'close', 'message'].forEach((method) => { Object.defineProperty(WebSocket.prototype, `on${method}`, { - configurable: true, enumerable: true, - /** - * Return the listener of the event. - * - * @return {(Function|undefined)} The event listener or `undefined` - * @public - */ get() { const listeners = this.listeners(method); for (let i = 0; i < listeners.length; i++) { @@ -432,12 +535,6 @@ readyStates.forEach((readyState, i) => { return undefined; }, - /** - * Add a listener for the event. - * - * @param {Function} listener The listener to add - * @public - */ set(listener) { const listeners = this.listeners(method); for (let i = 0; i < listeners.length; i++) { @@ -460,7 +557,7 @@ module.exports = WebSocket; * Initialize a WebSocket client. * * @param {WebSocket} websocket The client to initialize - * @param {(String|url.URL)} address The URL to which to connect + * @param {(String|URL)} address The URL to which to connect * @param {String} [protocols] The subprotocols * @param {Object} [options] Connection options * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable @@ -744,8 +841,8 @@ function tlsConnect(options) { * Abort the handshake and emit an error. * * @param {WebSocket} websocket The WebSocket instance - * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the - * socket to destroy + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to + * abort or the socket to destroy * @param {String} message The error message * @private */ From 66e58d279ffabe5108424c08ab71403aceddcad9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 8 Jul 2021 13:55:29 +0200 Subject: [PATCH 46/50] [fix] Make the `{noS,s}erver`, and `port` options mutually exclusive Remove ambiguity and prevent `WebSocketServer.prototype.address()` from throwing an error if the `noServer` option is used along with the `port` and/or `server` options. --- doc/ws.md | 4 ++-- lib/websocket-server.js | 9 +++++++-- test/websocket-server.test.js | 38 ++++++++++++++++++++++++++++++++--- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index af0dfb325..a3b1bff81 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -80,8 +80,8 @@ This class represents a WebSocket server. It extends the `EventEmitter`. - `maxPayload` {Number} The maximum allowed message size in bytes. - `callback` {Function} -Create a new server instance. One of `port`, `server` or `noServer` must be -provided or an error is thrown. An HTTP server is automatically created, +Create a new server instance. One and only one of `port`, `server` or `noServer` +must be provided or an error is thrown. An HTTP server is automatically created, started, and used if `port` is set. To use an external HTTP/S server instead, specify only `server` or `noServer`. In this case the HTTP/S server must be started manually. The "noServer" mode allows the WebSocket server to be diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 1610e767a..20276edb3 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -62,9 +62,14 @@ class WebSocketServer extends EventEmitter { ...options }; - if (options.port == null && !options.server && !options.noServer) { + if ( + (options.port == null && !options.server && !options.noServer) || + (options.port != null && (options.server || options.noServer)) || + (options.server && options.noServer) + ) { throw new TypeError( - 'One of the "port", "server", or "noServer" options must be specified' + 'One and only one of the "port", "server", or "noServer" options ' + + 'must be specified' ); } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index ce6617ec6..9b43284a1 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -18,12 +18,44 @@ const { NOOP } = require('../lib/constants'); describe('WebSocketServer', () => { describe('#ctor', () => { it('throws an error if no option object is passed', () => { - assert.throws(() => new WebSocket.Server()); + assert.throws( + () => new WebSocket.Server(), + new RegExp( + '^TypeError: One and only one of the "port", "server", or ' + + '"noServer" options must be specified$' + ) + ); }); describe('options', () => { - it('throws an error if no `port` or `server` option is specified', () => { - assert.throws(() => new WebSocket.Server({})); + it('throws an error if required options are not specified', () => { + assert.throws( + () => new WebSocket.Server({}), + new RegExp( + '^TypeError: One and only one of the "port", "server", or ' + + '"noServer" options must be specified$' + ) + ); + }); + + it('throws an error if mutually exclusive options are specified', () => { + const server = http.createServer(); + const variants = [ + { port: 0, noServer: true, server }, + { port: 0, noServer: true }, + { port: 0, server }, + { noServer: true, server } + ]; + + for (const options of variants) { + assert.throws( + () => new WebSocket.Server(options), + new RegExp( + '^TypeError: One and only one of the "port", "server", or ' + + '"noServer" options must be specified$' + ) + ); + } }); it('exposes options passed to constructor', (done) => { From ea63b29e81f95f7c5d38079487952b2eae94391e Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 8 Jul 2021 19:21:44 +0200 Subject: [PATCH 47/50] [minor] Fix typo --- lib/websocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/websocket.js b/lib/websocket.js index baf493458..7f1e3bcfa 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -128,7 +128,7 @@ class WebSocket extends EventEmitter { */ /* istanbul ignore next */ get onerror() { - return null; + return undefined; } /* istanbul ignore next */ From 5a5873048005cf5d25a2186fb9dc6db2a85096b0 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 9 Jul 2021 11:06:26 +0200 Subject: [PATCH 48/50] [fix] Emit the `'close'` event after the server is closed Ensure that `WebSocketServer.prototype.close()` does not emit a `'close'` event prematurely if called while the internal HTTP/S server is closing. --- lib/websocket-server.js | 16 ++++++++++++- test/websocket-server.test.js | 42 +++++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 20276edb3..e9ec2cbd7 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -16,6 +16,10 @@ const { GUID, kWebSocket } = require('./constants'); const keyRegex = /^[+/0-9A-Za-z]{22}==$/; +const RUNNING = 0; +const CLOSING = 1; +const CLOSED = 2; + /** * Class representing a WebSocket server. * @@ -108,6 +112,7 @@ class WebSocketServer extends EventEmitter { if (options.perMessageDeflate === true) options.perMessageDeflate = {}; if (options.clientTracking) this.clients = new Set(); this.options = options; + this._state = RUNNING; } /** @@ -137,6 +142,14 @@ class WebSocketServer extends EventEmitter { close(cb) { if (cb) this.once('close', cb); + if (this._state === CLOSED) { + process.nextTick(emitClose, this); + return; + } + + if (this._state === CLOSING) return; + this._state = CLOSING; + // // Terminate all associated clients. // @@ -154,7 +167,7 @@ class WebSocketServer extends EventEmitter { // Close the http server if it was internally created. // if (this.options.port != null) { - server.close(() => this.emit('close')); + server.close(emitClose.bind(undefined, this)); return; } } @@ -373,6 +386,7 @@ function addListeners(server, map) { * @private */ function emitClose(server) { + server._state = CLOSED; server.emit('close'); } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 9b43284a1..310c87c2e 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -278,11 +278,45 @@ describe('WebSocketServer', () => { }); }); - it("emits the 'close' event", (done) => { - const wss = new WebSocket.Server({ noServer: true }); + it("emits the 'close' event after the server closes", (done) => { + let serverCloseEventEmitted = false; + + const wss = new WebSocket.Server({ port: 0 }, () => { + net.createConnection({ port: wss.address().port }); + }); + + wss._server.on('connection', (socket) => { + wss.close(); + + // + // The server is closing. Ensure this does not emit a `'close'` + // event before the server is actually closed. + // + wss.close(); + + process.nextTick(() => { + socket.end(); + }); + }); - wss.on('close', done); - wss.close(); + wss._server.on('close', () => { + serverCloseEventEmitted = true; + }); + + wss.on('close', () => { + assert.ok(serverCloseEventEmitted); + done(); + }); + }); + + it("emits the 'close' event if the server is already closed", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + wss.close(() => { + assert.strictEqual(wss._state, 2); + wss.on('close', done); + wss.close(); + }); + }); }); }); From 772236a13ff2bd28291c911b7c25fbfe99580ed1 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 9 Jul 2021 12:34:22 +0200 Subject: [PATCH 49/50] [fix] Abort the handshake if the server is closing or closed Prevent WebSocket connections from being established after `WebSocketServer.prototype.close()` is called. --- lib/websocket-server.js | 2 ++ test/websocket-server.test.js | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index e9ec2cbd7..fe7fdf501 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -297,6 +297,8 @@ class WebSocketServer extends EventEmitter { ); } + if (this._state > RUNNING) return abortHandshake(socket, 503); + const digest = createHash('sha1') .update(key + GUID) .digest('base64'); diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 310c87c2e..90ceb5646 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -613,6 +613,28 @@ describe('WebSocketServer', () => { }); }); + it('fails if the WebSocket server is closing or closed', (done) => { + const server = http.createServer(); + const wss = new WebSocket.Server({ noServer: true }); + + server.on('upgrade', (req, socket, head) => { + wss.close(); + wss.handleUpgrade(req, socket, head, () => { + done(new Error('Unexpected callback invocation')); + }); + }); + + server.listen(0, () => { + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('unexpected-response', (req, res) => { + assert.strictEqual(res.statusCode, 503); + res.resume(); + server.close(done); + }); + }); + }); + it('handles unsupported extensions', (done) => { const wss = new WebSocket.Server( { From 4c1849a61e773fe0ce016f6eb59bc3877f09aeee Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 10 Jul 2021 07:26:57 +0200 Subject: [PATCH 50/50] [dist] 7.5.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 85cea0fab..d6dff1396 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.5.2", + "version": "7.5.3", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi",