Skip to content

Commit

Permalink
[feature] attach result of verifyClient to ws
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianhopebailie committed Aug 1, 2019
1 parent 91b5173 commit 3d2bacc
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 14 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ can use one of the many wrappers available on npm, like
- [Simple server](#simple-server)
- [External HTTP/S server](#external-https-server)
- [Multiple servers sharing a single HTTP/S server](#multiple-servers-sharing-a-single-https-server)
- [Authentication on the server (sync)](#authentication-on-the-server-sync)
- [Authentication on the server (async)](#authentication-on-the-server-async)
- [Server broadcast](#server-broadcast)
- [echo.websocket.org demo](#echowebsocketorg-demo)
- [Use the Node.js streams API](#use-the-nodejs-streams-api)
Expand Down Expand Up @@ -249,6 +251,54 @@ server.on('upgrade', function upgrade(request, socket, head) {
server.listen(8080);
```

### Authentication on the server (sync)

```js
const WebSocket = require('ws');

const wss = new WebSocket.Server({
port: 8080
verifyClient: (info) => {
return authenticateHandshakeRequest(info.req);
}
});

wss.on('connection', function connection(ws) {
// ws.client is the value returned in verifyClient
const client = ws.client;
ws.on('message', function incoming(message) {
console.log('received from %s: %s', client, message);
});

ws.send('hello, ' + client.toString());
});
```

### Authentication on the server (async)

```js
const WebSocket = require('ws');

const wss = new WebSocket.Server({
port: 8080
verifyClient: (info, cb) => {
const client = authenticateHandshakeRequest(info.req);
if(client) cb(client);
cb(false, 403, 'Forbidden')
}
});

wss.on('connection', function connection(ws) {
// ws.client is the value returned in verifyClient
const client = ws.client;
ws.on('message', function incoming(message) {
console.log('received from %s: %s', client, message);
});

ws.send('hello, ' + client.toString());
});
```

### Server broadcast

A client WebSocket broadcasting to all connected WebSocket clients, including
Expand Down
25 changes: 19 additions & 6 deletions doc/ws.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,24 @@ is provided with a single argument then that is:
- `secure` {Boolean} `true` if `req.connection.authorized` or
`req.connection.encrypted` is set.

The return value (Boolean) of the function determines whether or not to accept
the handshake.
The return value of the function determines whether or not to accept the
handshake. If it is
[`falsy`](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) then the
handshake is rejected, otherwise the return value is attached to the `WebSocket`
instance that is subsequently created as the property `client`.

if `verifyClient` is provided with two arguments then those are:

- `info` {Object} Same as above.
- `cb` {Function} A callback that must be called by the user upon inspection of
the `info` fields. Arguments in this callback are:
- `result` {Boolean} Whether or not to accept the handshake.
- `code` {Number} When `result` is `false` this field determines the HTTP
- `result` {Object} Either the verified client or a `falsy` value to reject
the handshake.
- `code` {Number} When `result` is `falsy` this field determines the HTTP
error status code to be sent to the client.
- `name` {String} When `result` is `false` this field determines the HTTP
- `name` {String} When `result` is `falsy` this field determines the HTTP
reason phrase.
- `headers` {Object} When `result` is `false` this field determines additional
- `headers` {Object} When `result` is `falsy` this field determines additional
HTTP headers to be sent to the client. For example,
`{ 'Retry-After': 120 }`.

Expand Down Expand Up @@ -355,6 +359,15 @@ of binary protocols transferring large messages with multiple fragments.
The number of bytes of data that have been queued using calls to `send()` but
not yet transmitted to the network.

### websocket.client

- {Object}

**Server created instances only.**

The return value of the `verifyClient` function if defined in the server
options.

### websocket.close([code[, reason]])

- `code` {Number} A numeric value indicating the status code explaining why the
Expand Down
17 changes: 10 additions & 7 deletions lib/websocket-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ class WebSocketServer extends EventEmitter {
//
// Optionally call external client verification handler.
//
let client = null;
if (this.options.verifyClient) {
const info = {
origin:
Expand All @@ -239,20 +240,21 @@ class WebSocketServer extends EventEmitter {
};

if (this.options.verifyClient.length === 2) {
this.options.verifyClient(info, (verified, code, message, headers) => {
if (!verified) {
this.options.verifyClient(info, (client, code, message, headers) => {
if (!client) {
return abortHandshake(socket, code || 401, message, headers);
}

this.completeUpgrade(key, extensions, req, socket, head, cb);
this.completeUpgrade(key, extensions, req, socket, head, cb, client);
});
return;
}

if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
client = this.options.verifyClient(info);
if (!client) return abortHandshake(socket, 401);
}

this.completeUpgrade(key, extensions, req, socket, head, cb);
this.completeUpgrade(key, extensions, req, socket, head, cb, client);
}

/**
Expand All @@ -264,9 +266,10 @@ class WebSocketServer extends EventEmitter {
* @param {net.Socket} socket The network socket between the server and client
* @param {Buffer} head The first packet of the upgraded stream
* @param {Function} cb Callback
* @param {Object} client Return value from `verifyClient`, if provided, else `undefined`
* @private
*/
completeUpgrade(key, extensions, req, socket, head, cb) {
completeUpgrade(key, extensions, req, socket, head, cb, client) {
//
// Destroy the socket if the client has already sent a FIN packet.
//
Expand Down Expand Up @@ -321,7 +324,7 @@ 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, this.options.maxPayload, client);

if (this.clients) {
this.clients.add(ws);
Expand Down
19 changes: 18 additions & 1 deletion lib/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class WebSocket extends EventEmitter {
this._receiver = null;
this._sender = null;
this._socket = null;
this._client = null;

if (address !== null) {
this._bufferedAmount = 0;
Expand Down Expand Up @@ -88,6 +89,20 @@ class WebSocket extends EventEmitter {
return WebSocket.OPEN;
}

/**
* Only used when the socket is created by a server.
* This value is returned by the `verifyClient` function provided in the server options.
*
* If verifyClient is defined then an object representing the authenticated client such
* as a client id or token can be returned and will be available here as `ws.client` for
* access by application code higher in the stack
*
* `this._client` is only set when calling `setSocket()`
*/
get client() {
return this._client;
}

/**
* This deviates from the WHATWG interface since ws doesn't support the
* required default "blob" type (instead we define a custom "nodebuffer"
Expand Down Expand Up @@ -135,9 +150,10 @@ class WebSocket extends EventEmitter {
* @param {net.Socket} socket The network socket between the server and client
* @param {Buffer} head The first packet of the upgraded stream
* @param {Number} maxPayload The maximum allowed message size
* @param {Object} client An object that identifies the remote client. Only used when the ws is created by a server.
* @private
*/
setSocket(socket, head, maxPayload) {
setSocket(socket, head, maxPayload, client) {
const receiver = new Receiver(
this._binaryType,
this._extensions,
Expand All @@ -147,6 +163,7 @@ class WebSocket extends EventEmitter {
this._sender = new Sender(socket, this._extensions);
this._receiver = receiver;
this._socket = socket;
if (client) this._client = client;

receiver[kWebSocket] = this;
socket[kWebSocket] = this;
Expand Down
47 changes: 47 additions & 0 deletions test/websocket-server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,36 @@ describe('WebSocketServer', () => {
});
});

it('can auth client synchronously', (done) => {
const server = https.createServer({
cert: fs.readFileSync('test/fixtures/certificate.pem'),
key: fs.readFileSync('test/fixtures/key.pem')
});

const wss = new WebSocket.Server({
verifyClient: (info) => {
assert.strictEqual(info.origin, 'https://example.com');
assert.strictEqual(info.req.headers.foo, 'bar');
assert.ok(info.secure, true);
return 'alice';
},
server
});

wss.on('connection', (ws) => {
assert.strictEqual(ws.client, 'alice');
wss.close();
server.close(done);
});

server.listen(0, () => {
const ws = new WebSocket(`wss://localhost:${server.address().port}`, {
headers: { Origin: 'https://example.com', foo: 'bar' },
rejectUnauthorized: false
});
});
});

it('can accept client asynchronously', (done) => {
const wss = new WebSocket.Server(
{
Expand All @@ -592,6 +622,23 @@ describe('WebSocketServer', () => {
wss.on('connection', () => wss.close(done));
});

it('can auth client asynchronously', (done) => {
const wss = new WebSocket.Server(
{
verifyClient: (o, cb) => process.nextTick(cb, 'alice'),
port: 0
},
() => {
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
}
);

wss.on('connection', (ws) => {
assert.strictEqual(ws.client, 'alice');
wss.close(done);
});
});

it('can reject client asynchronously', (done) => {
const wss = new WebSocket.Server(
{
Expand Down

0 comments on commit 3d2bacc

Please sign in to comment.