From ea29b6cdd22fd024a95446888ea5979a5e9cb34b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 11 Aug 2021 15:48:52 +0200 Subject: [PATCH] [feature] Add ability to skip UTF-8 validation (#1928) Add the `skipUTF8Validation` option to skip UTF-8 validation for text and close messages. Refs: https://github.com/websockets/ws/issues/1878 Closes #1924 --- doc/ws.md | 6 ++++++ lib/receiver.js | 5 +++-- lib/websocket-server.js | 8 +++++++- lib/websocket.js | 18 ++++++++++++++---- test/receiver.test.js | 24 ++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 7 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index d657ceeee..22c712034 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -78,6 +78,9 @@ This class represents a WebSocket server. It extends the `EventEmitter`. - `clientTracking` {Boolean} Specifies whether or not to track clients. - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate. - `maxPayload` {Number} The maximum allowed message size in bytes. + - `skipUTF8Validation` {Boolean} Specifies whether or not to skip UTF-8 + validation for text and close messages. Defaults to `false`. Set to `true` + only if clients are trusted. - `callback` {Function} Create a new server instance. One and only one of `port`, `server` or `noServer` @@ -273,6 +276,9 @@ This class represents a WebSocket. It extends the `EventEmitter`. - `origin` {String} Value of the `Origin` or `Sec-WebSocket-Origin` header depending on the `protocolVersion`. - `maxPayload` {Number} The maximum allowed message size in bytes. + - `skipUTF8Validation` {Boolean} Specifies whether or not to skip UTF-8 + validation for text and close messages. Defaults to `false`. Set to `true` + only if the server is trusted. - Any other option allowed in [http.request()][] or [https.request()][]. Options given do not have any effect if parsed from the URL given with the `address` parameter. diff --git a/lib/receiver.js b/lib/receiver.js index fe0703b2d..774301819 100644 --- a/lib/receiver.js +++ b/lib/receiver.js @@ -43,6 +43,7 @@ class Receiver extends Writable { this._extensions = options.extensions || {}; this._isServer = !!options.isServer; this._maxPayload = options.maxPayload | 0; + this._skipUTF8Validation = !!options.skipUTF8Validation; this[kWebSocket] = undefined; this._bufferedBytes = 0; @@ -505,7 +506,7 @@ class Receiver extends Writable { } else { const buf = concat(fragments, messageLength); - if (!isValidUTF8(buf)) { + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { this._loop = false; return error( Error, @@ -560,7 +561,7 @@ class Receiver extends Writable { const buf = data.slice(2); - if (!isValidUTF8(buf)) { + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { return error( Error, 'invalid UTF-8 sequence', diff --git a/lib/websocket-server.js b/lib/websocket-server.js index b147fe5d5..3c7939f28 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -46,6 +46,8 @@ class WebSocketServer extends EventEmitter { * @param {Number} [options.port] The port where to bind the server * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S * server to use + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @param {Function} [options.verifyClient] A hook to reject connections * @param {Function} [callback] A listener for the `listening` event */ @@ -54,6 +56,7 @@ class WebSocketServer extends EventEmitter { options = { maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: false, handleProtocols: null, clientTracking: true, @@ -386,7 +389,10 @@ class WebSocketServer extends EventEmitter { socket.write(headers.concat('\r\n').join('\r\n')); socket.removeListener('error', socketOnError); - ws.setSocket(socket, head, this.options.maxPayload); + ws.setSocket(socket, head, { + maxPayload: this.options.maxPayload, + skipUTF8Validation: this.options.skipUTF8Validation + }); if (this.clients) { this.clients.add(ws); diff --git a/lib/websocket.js b/lib/websocket.js index 2d7b5c43b..77ebe5ff2 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -180,15 +180,19 @@ class WebSocket extends EventEmitter { * @param {(net.Socket|tls.Socket)} socket The network socket between the * server and client * @param {Buffer} head The first packet of the upgraded stream - * @param {Number} [maxPayload=0] The maximum allowed message size + * @param {Object} [options] Options object + * @param {Number} [options.maxPayload=0] The maximum allowed message size + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ - setSocket(socket, head, maxPayload) { + setSocket(socket, head, options = {}) { const receiver = new Receiver({ binaryType: this.binaryType, extensions: this._extensions, isServer: this._isServer, - maxPayload + maxPayload: options.maxPayload, + skipUTF8Validation: options.skipUTF8Validation }); this._sender = new Sender(socket, this._extensions); @@ -575,12 +579,15 @@ module.exports = WebSocket; * redirects * @param {Number} [options.maxRedirects=10] The maximum number of redirects * allowed + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ function initAsClient(websocket, address, protocols, options) { const opts = { protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: true, followRedirects: false, maxRedirects: 10, @@ -832,7 +839,10 @@ function initAsClient(websocket, address, protocols, options) { perMessageDeflate; } - websocket.setSocket(socket, head, opts.maxPayload); + websocket.setSocket(socket, head, { + maxPayload: opts.maxPayload, + skipUTF8Validation: opts.skipUTF8Validation + }); }); } diff --git a/test/receiver.test.js b/test/receiver.test.js index 4f03cea02..7ee35f740 100644 --- a/test/receiver.test.js +++ b/test/receiver.test.js @@ -1059,4 +1059,28 @@ describe('Receiver', () => { }).forEach((buf) => receiver.write(buf)); }); }); + + it('honors the `skipUTF8Validation` option (1/2)', (done) => { + const receiver = new Receiver({ skipUTF8Validation: true }); + + receiver.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, Buffer.from([0xf8])); + assert.ok(!isBinary); + done(); + }); + + receiver.write(Buffer.from([0x81, 0x01, 0xf8])); + }); + + it('honors the `skipUTF8Validation` option (2/2)', (done) => { + const receiver = new Receiver({ skipUTF8Validation: true }); + + receiver.on('conclude', (code, data) => { + assert.strictEqual(code, 1000); + assert.deepStrictEqual(data, Buffer.from([0xf8])); + done(); + }); + + receiver.write(Buffer.from([0x88, 0x03, 0x03, 0xe8, 0xf8])); + }); });