From 6e5a5ce341ffab5ea48542f0aa82c7f4eae80df9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 26 May 2022 12:42:53 +0200 Subject: [PATCH] [feature] Introduce the `'wsClientError'` event (#2046) Add the ability to inspect the invalid handshake requests and respond to them with a custom HTTP response. Closes #2045 --- doc/ws.md | 16 ++++++++++++++++ lib/websocket-server.js | 36 +++++++++++++++++++++++++++++------ test/websocket-server.test.js | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 8a0c4ce64..21984c60a 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -9,6 +9,7 @@ - [Event: 'error'](#event-error) - [Event: 'headers'](#event-headers) - [Event: 'listening'](#event-listening) + - [Event: 'wsClientError'](event-wsclienterror) - [server.address()](#serveraddress) - [server.clients](#serverclients) - [server.close([callback])](#serverclosecallback) @@ -202,6 +203,21 @@ handshake. This allows you to inspect/modify the headers before they are sent. Emitted when the underlying server has been bound. +### Event: 'wsClientError' + +- `error` {Error} +- `socket` {net.Socket|tls.Socket} +- `request` {http.IncomingMessage} + +Emitted when an error occurs before the WebSocket connection is established. +`socket` and `request` are respectively the socket and the HTTP request from +which the error originated. The listener of this event is responsible for +closing the socket. When the `'wsClientError'` event is emitted there is no +`http.ServerResponse` object, so any HTTP response, including the response +headers and body, must be written directly to the `socket`. If there is no +listener for this event, the socket is closed with a default 4xx response +containing a descriptive error message. + ### server.address() Returns an object with `port`, `family`, and `address` properties specifying the diff --git a/lib/websocket-server.js b/lib/websocket-server.js index e22e9b212..bac30eb33 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -234,24 +234,26 @@ class WebSocketServer extends EventEmitter { const version = +req.headers['sec-websocket-version']; if (req.method !== 'GET') { - abortHandshake(socket, 405, 'Invalid HTTP method'); + const message = 'Invalid HTTP method'; + abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); return; } if (req.headers.upgrade.toLowerCase() !== 'websocket') { - abortHandshake(socket, 400, 'Invalid Upgrade header'); + const message = 'Invalid Upgrade header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } if (!key || !keyRegex.test(key)) { const message = 'Missing or invalid Sec-WebSocket-Key header'; - abortHandshake(socket, 400, message); + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } if (version !== 8 && version !== 13) { const message = 'Missing or invalid Sec-WebSocket-Version header'; - abortHandshake(socket, 400, message); + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } @@ -268,7 +270,7 @@ class WebSocketServer extends EventEmitter { protocols = subprotocol.parse(secWebSocketProtocol); } catch (err) { const message = 'Invalid Sec-WebSocket-Protocol header'; - abortHandshake(socket, 400, message); + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } } @@ -296,7 +298,7 @@ class WebSocketServer extends EventEmitter { } catch (err) { const message = 'Invalid or unacceptable Sec-WebSocket-Extensions header'; - abortHandshake(socket, 400, message); + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } } @@ -509,3 +511,25 @@ function abortHandshake(socket, code, message, headers) { message ); } + +/** + * Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least + * one listener for it, otherwise call `abortHandshake()`. + * + * @param {WebSocketServer} server The WebSocket server + * @param {http.IncomingMessage} req The request object + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} message The HTTP response body + * @private + */ +function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) { + if (server.listenerCount('wsClientError')) { + const err = new Error(message); + Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); + + server.emit('wsClientError', err, socket, req); + } else { + abortHandshake(socket, code, message); + } +} diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index bcf82ba90..12928ff49 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -851,6 +851,40 @@ describe('WebSocketServer', () => { }); }); + it("emits the 'wsClientError' event", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.request({ + method: 'POST', + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + wss.close(done); + }); + + req.end(); + }); + + wss.on('wsClientError', (err, socket, request) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Invalid HTTP method'); + + assert.ok(request instanceof http.IncomingMessage); + assert.strictEqual(request.method, 'POST'); + + socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + it('fails if the WebSocket server is closing or closed', (done) => { const server = http.createServer(); const wss = new WebSocket.Server({ noServer: true });