Skip to content

Commit

Permalink
[feature] Introduce the 'wsClientError' event (#2046)
Browse files Browse the repository at this point in the history
Add the ability to inspect the invalid handshake requests and respond
to them with a custom HTTP response.

Closes #2045
  • Loading branch information
lpinca committed May 26, 2022
1 parent 903ec62 commit 6e5a5ce
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 6 deletions.
16 changes: 16 additions & 0 deletions doc/ws.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
36 changes: 30 additions & 6 deletions lib/websocket-server.js
Expand Up @@ -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;
}

Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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);
}
}
34 changes: 34 additions & 0 deletions test/websocket-server.test.js
Expand Up @@ -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 });
Expand Down

0 comments on commit 6e5a5ce

Please sign in to comment.