Skip to content

Commit

Permalink
Merge pull request websockets#890 from websockets/remove/handle-proto…
Browse files Browse the repository at this point in the history
…cols-callback

Remove callback from `handleProtocols`
  • Loading branch information
3rd-Eden authored Nov 15, 2016
2 parents 490d921 + 6bd0a57 commit 109db7f
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 135 deletions.
11 changes: 5 additions & 6 deletions doc/ws.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,12 @@ If `verifyClient` is not set then the handshake is automatically accepted.

### options.handleProtocols

`handleProtocols` receives two arguments:
* `protocols` Array: The list of WebSocket sub-protocols indicated by the client in the Sec-WebSocket-Protocol header.
* `cb` Function: A callback that must be called by the user upon inspection of the protocols. Arguments in this callback are:
* `result` Boolean: Whether the user accepts or not the handshake.
* `protocol` String: If `result` is `true` then this field sets the value of the Sec-WebSocket-Protocol header in the HTTP 101 response.
`handleProtocols` takes a single argument:
* `protocols` Array: The list of WebSocket sub-protocols indicated by the client in the `Sec-WebSocket-Protocol` header.

If returned value is `false` then the handshake is rejected with the HTTP 401 status code, otherwise the returned value sets the value of the `Sec-WebSocket-Protocol` header in the HTTP 101 response.

If `handleProtocols` is not set then the handshake is accepted regardless the value of Sec-WebSocket-Protocol header. If it is set but the user does not invoke the `cb` callback then the handshake is rejected with error HTTP 501.
If `handleProtocols` is not set then the handshake is automatically accepted.

### options.perMessageDeflate

Expand Down
139 changes: 59 additions & 80 deletions lib/WebSocketServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,58 +145,51 @@ WebSocketServer.prototype.handleUpgrade = function (req, socket, head, cb) {
if (
!this.shouldHandle(req) ||
!req.headers.upgrade ||
req.headers.upgrade.toLowerCase() !== 'websocket'
req.headers.upgrade.toLowerCase() !== 'websocket' ||
!req.headers['sec-websocket-key']
) {
return abortConnection(socket, 400);
}

handleHybiUpgrade.apply(this, arguments);
socket.on('error', socketError);
upgrade.apply(this, arguments);
};

module.exports = WebSocketServer;

/**
* Entirely private apis,
* which may or may not be bound to a specific WebSocket instance.
* Handle premature socket errors.
*
* @private
*/
function socketError () {
this.destroy();
}

function handleHybiUpgrade (req, socket, upgradeHead, cb) {
// handle premature socket errors
var errorHandler = () => {
try { socket.destroy(); } catch (e) {}
};
socket.on('error', errorHandler);

// verify key presence
if (!req.headers['sec-websocket-key']) {
return abortConnection(socket, 400);
}

// verify version
var version = +req.headers['sec-websocket-version'];
if (version !== 8 && version !== 13) {
return abortConnection(socket, 400);
}

// verify protocol
var protocols = req.headers['sec-websocket-protocol'];
/**
* Upgrade the connection to WebSocket.
*
* @param {http.IncomingMessage} req The request object
* @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
* @private
*/
function upgrade (req, socket, head, cb) {
const version = +req.headers['sec-websocket-version'];

// verify client
var origin = version !== 13
? req.headers['sec-websocket-origin']
: req.headers['origin'];
if (version !== 8 && version !== 13) return abortConnection(socket, 400);

// handle extensions offer
var extensionsOffer = Extensions.parse(req.headers['sec-websocket-extensions']);
var protocol = (req.headers['sec-websocket-protocol'] || '').split(/, */);

// handler to call when the connection sequence completes
var completeHybiUpgrade2 = (protocol) => {
const completeUpgrade = () => {
// calc key
var key = crypto.createHash('sha1')
const key = crypto.createHash('sha1')
.update(`${req.headers['sec-websocket-key']}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`, 'binary')
.digest('base64');

var headers = [
const headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
Expand All @@ -207,40 +200,39 @@ function handleHybiUpgrade (req, socket, upgradeHead, cb) {
headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
}

var extensions = {};
const offer = Extensions.parse(req.headers['sec-websocket-extensions']);
var extensions;

try {
extensions = acceptExtensions.call(this, extensionsOffer);
extensions = acceptExtensions.call(this, offer);
} catch (err) {
return abortConnection(socket, 400);
}

if (Object.keys(extensions).length) {
var serverExtensions = {};
Object.keys(extensions).forEach((token) => {
serverExtensions[token] = [extensions[token].params];
});
const serverExtensions = Object.keys(extensions).reduce((obj, key) => {
obj[key] = [extensions[key].params];
return obj;
}, {});

headers.push(`Sec-WebSocket-Extensions: ${Extensions.format(serverExtensions)}`);
}

// allows external modification/inspection of handshake headers
this.emit('headers', headers);

socket.setTimeout(0);
socket.setNoDelay(true);

try {
if (socket.writable) {
socket.write(headers.concat('', '').join('\r\n'));
} catch (e) {
// if the upgrade write fails, shut the connection down hard
try { socket.destroy(); } catch (e) {}
} else {
socket.destroy();
return;
}

var client = new WebSocket([req, socket, upgradeHead], {
const client = new WebSocket([req, socket, head], {
maxPayload: this.options.maxPayload,
protocolVersion: version,
protocol: protocol,
extensions: extensions,
maxPayload: this.options.maxPayload
extensions,
protocol
});

if (this.clients) {
Expand All @@ -249,52 +241,39 @@ function handleHybiUpgrade (req, socket, upgradeHead, cb) {
}

// signal upgrade complete
socket.removeListener('error', errorHandler);
socket.removeListener('error', socketError);
cb(client);
};

// optionally call external protocol selection handler before
// calling completeHybiUpgrade2
var completeHybiUpgrade1 = () => {
// choose from the sub-protocols
if (this.options.handleProtocols) {
var protList = (protocols || '').split(/, */);
var callbackCalled = false;
this.options.handleProtocols(protList, (result, protocol) => {
callbackCalled = true;
if (!result) return abortConnection(socket, 401);

completeHybiUpgrade2(protocol);
});
if (!callbackCalled) {
// the handleProtocols handler never called our callback
abortConnection(socket, 501, 'Could not process protocols');
}
} else {
completeHybiUpgrade2(protocols && protocols.split(/, */)[0]);
}
};
// optionally call external protocol selection handler
if (this.options.handleProtocols) {
protocol = this.options.handleProtocols(protocol);
if (protocol === false) return abortConnection(socket, 401);
} else {
protocol = protocol[0];
}

// optionally call external client verification handler
if (this.options.verifyClient) {
var info = {
secure: req.connection.authorized !== undefined || req.connection.encrypted !== undefined,
origin: origin,
req: req
const info = {
origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
secure: !!(req.connection.authorized || req.connection.encrypted),
req
};

if (this.options.verifyClient.length === 2) {
this.options.verifyClient(info, (result, code, message) => {
if (!result) return abortConnection(socket, code || 401, message);
this.options.verifyClient(info, (verified, code, message) => {
if (!verified) return abortConnection(socket, code || 401, message);

completeHybiUpgrade1();
completeUpgrade();
});
return;
} else if (!this.options.verifyClient(info)) {
return abortConnection(socket, 401);
}
}

completeHybiUpgrade1();
completeUpgrade();
}

function acceptExtensions (offer) {
Expand Down
54 changes: 5 additions & 49 deletions test/WebSocketServer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ describe('WebSocketServer', function () {

it('selects the last protocol via protocol handler', function (done) {
const wss = new WebSocketServer({
handleProtocols: (ps, cb) => cb(true, ps[ps.length - 1]),
handleProtocols: (ps) => ps[ps.length - 1],
port: ++port
}, () => {
const ws = new WebSocket(`ws://localhost:${port}`, ['prot1', 'prot2']);
Expand All @@ -780,7 +780,7 @@ describe('WebSocketServer', function () {

it('client detects invalid server protocol', function (done) {
const wss = new WebSocketServer({
handleProtocols: (ps, cb) => cb(true, 'prot3'),
handleProtocols: (ps) => 'prot3',
port: ++port
}, () => {
const ws = new WebSocket(`ws://localhost:${port}`, ['prot1', 'prot2']);
Expand All @@ -795,22 +795,7 @@ describe('WebSocketServer', function () {

it('client detects no server protocol', function (done) {
const wss = new WebSocketServer({
handleProtocols: (ps, cb) => cb(true),
port: ++port
}, () => {
const ws = new WebSocket(`ws://localhost:${port}`, ['prot1', 'prot2']);

ws.on('open', () => done(new Error('connection must not be established')));
ws.on('error', () => {
wss.close();
done();
});
});
});

it('client refuses server protocols', function (done) {
const wss = new WebSocketServer({
handleProtocols: (ps, cb) => cb(false),
handleProtocols: (ps) => {},
port: ++port
}, () => {
const ws = new WebSocket(`ws://localhost:${port}`, ['prot1', 'prot2']);
Expand All @@ -825,7 +810,7 @@ describe('WebSocketServer', function () {

it('server detects unauthorized protocol handler', function (done) {
const wss = new WebSocketServer({
handleProtocols: (ps, cb) => cb(false),
handleProtocols: (ps) => false,
port: ++port
}, () => {
const req = http.request({
Expand All @@ -834,7 +819,7 @@ describe('WebSocketServer', function () {
'Upgrade': 'websocket',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': 13,
'Sec-WebSocket-Origin': 'http://foobar.com'
'Origin': 'http://foobar.com'
},
host: '127.0.0.1',
port
Expand All @@ -850,35 +835,6 @@ describe('WebSocketServer', function () {
});
});

it('server detects invalid protocol handler', function (done) {
const wss = new WebSocketServer({
handleProtocols: (ps, cb) => {
// not calling callback is an error and shouldn't timeout
},
port: ++port
}, () => {
const req = http.request({
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': 13,
'Sec-WebSocket-Origin': 'http://foobar.com'
},
host: '127.0.0.1',
port
});

req.on('response', (res) => {
assert.strictEqual(res.statusCode, 501);
wss.close();
done();
});

req.end();
});
});

it('accept connections with sec-websocket-extensions', function (done) {
const wss = new WebSocketServer({ port: ++port }, () => {
const req = http.request({
Expand Down

0 comments on commit 109db7f

Please sign in to comment.