From 01ba54edaeff0f3a58abd7cb9f8e1f3bf134d0fc Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 25 Dec 2023 16:00:21 +0100 Subject: [PATCH] [feature] Introduce the `autoPong` option Add the ability to disable the automatic sending of pong responses to pings. Fixes #2186 --- doc/ws.md | 4 +++ lib/websocket-server.js | 5 +++- lib/websocket.js | 8 +++++- test/websocket-server.test.js | 24 ++++++++++++++++++ test/websocket.test.js | 47 +++++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 2 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 92eb2c23e..f79cfc901 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -72,6 +72,8 @@ This class represents a WebSocket server. It extends the `EventEmitter`. ### new WebSocketServer(options[, callback]) - `options` {Object} + - `autoPong` {Boolean} Specifies whether or not to automatically send a pong + in response to a ping. Defaults to `true`. - `allowSynchronousEvents` {Boolean} Specifies whether any of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple times in the same tick. To improve compatibility with the WHATWG standard, the default value @@ -296,6 +298,8 @@ This class represents a WebSocket. It extends the `EventEmitter`. - `address` {String|url.URL} The URL to which to connect. - `protocols` {String|Array} The list of subprotocols. - `options` {Object} + - `autoPong` {Boolean} Specifies whether or not to automatically send a pong + in response to a ping. Defaults to `true`. - `allowSynchronousEvents` {Boolean} Specifies whether any of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple times in the same tick. To improve compatibility with the WHATWG standard, the default value diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 58b63019d..377c45a8b 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -32,6 +32,8 @@ class WebSocketServer extends EventEmitter { * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted * multiple times in the same tick + * @param {Boolean} [options.autoPong=true] Specifies whether or not to + * automatically send a pong in response to a ping * @param {Number} [options.backlog=511] The maximum length of the queue of * pending connections * @param {Boolean} [options.clientTracking=true] Specifies whether or not to @@ -59,6 +61,7 @@ class WebSocketServer extends EventEmitter { options = { allowSynchronousEvents: false, + autoPong: true, maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, perMessageDeflate: false, @@ -379,7 +382,7 @@ class WebSocketServer extends EventEmitter { `Sec-WebSocket-Accept: ${digest}` ]; - const ws = new this.options.WebSocket(null); + const ws = new this.options.WebSocket(null, undefined, this.options); if (protocols.size) { // diff --git a/lib/websocket.js b/lib/websocket.js index 29e706ef5..df5034cc7 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -84,6 +84,7 @@ class WebSocket extends EventEmitter { initAsClient(this, address, protocols, options); } else { + this._autoPong = options.autoPong; this._isServer = true; } } @@ -625,6 +626,8 @@ module.exports = WebSocket; * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether any * of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple * times in the same tick + * @param {Boolean} [options.autoPong=true] Specifies whether or not to + * automatically send a pong in response to a ping * @param {Function} [options.finishRequest] A function which can be used to * customize the headers of each http request before it is sent * @param {Boolean} [options.followRedirects=false] Whether or not to follow @@ -650,6 +653,7 @@ module.exports = WebSocket; function initAsClient(websocket, address, protocols, options) { const opts = { allowSynchronousEvents: false, + autoPong: true, protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, @@ -668,6 +672,8 @@ function initAsClient(websocket, address, protocols, options) { port: undefined }; + websocket._autoPong = opts.autoPong; + if (!protocolVersions.includes(opts.protocolVersion)) { throw new RangeError( `Unsupported protocol version: ${opts.protocolVersion} ` + @@ -1212,7 +1218,7 @@ function receiverOnMessage(data, isBinary) { function receiverOnPing(data) { const websocket = this[kWebSocket]; - websocket.pong(data, !websocket._isServer, NOOP); + if (websocket._autoPong) websocket.pong(data, !this._isServer, NOOP); websocket.emit('ping', data); } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 5b6937cee..44c2c6709 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -116,6 +116,30 @@ describe('WebSocketServer', () => { wss.close(done); }); }); + + it('honors the `autoPong` option', (done) => { + const wss = new WebSocket.Server({ autoPong: false, port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.ping(); + }); + + ws.on('pong', () => { + done(new Error("Unexpected 'pong' event")); + }); + }); + + wss.on('connection', (ws) => { + ws.on('ping', () => { + ws.close(); + }); + + ws.on('close', () => { + wss.close(done); + }); + }); + }); }); it('emits an error if http server bind fails', (done) => { diff --git a/test/websocket.test.js b/test/websocket.test.js index 4699ae5cd..28dcb8808 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -197,6 +197,30 @@ describe('WebSocket', () => { }); }); }); + + it('honors the `autoPong` option', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + autoPong: false + }); + + ws.on('ping', () => { + ws.close(); + }); + + ws.on('close', () => { + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('pong', () => { + done(new Error("Unexpected 'pong' event")); + }); + + ws.ping(); + }); + }); }); }); @@ -2325,6 +2349,29 @@ describe('WebSocket', () => { ws.close(); }); }); + + it('is called automatically when a ping is received', (done) => { + const buf = Buffer.from('hi'); + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.ping(buf); + }); + + ws.on('pong', (data) => { + assert.deepStrictEqual(data, buf); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('ping', (data) => { + assert.deepStrictEqual(data, buf); + ws.close(); + }); + }); + }); }); describe('#resume', () => {