|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501 |
- /*!
- * ws: a node.js websocket client
- * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
- * MIT Licensed
- */
-
- var util = require('util')
- , events = require('events')
- , http = require('http')
- , crypto = require('crypto')
- , Options = require('options')
- , WebSocket = require('./WebSocket')
- , Extensions = require('./Extensions')
- , PerMessageDeflate = require('./PerMessageDeflate')
- , tls = require('tls')
- , url = require('url');
-
- /**
- * WebSocket Server implementation
- */
-
- function WebSocketServer(options, callback) {
- events.EventEmitter.call(this);
-
- options = new Options({
- host: '0.0.0.0',
- port: null,
- server: null,
- verifyClient: null,
- handleProtocols: null,
- path: null,
- noServer: false,
- disableHixie: false,
- clientTracking: true,
- perMessageDeflate: true
- }).merge(options);
-
- if (!options.isDefinedAndNonNull('port') && !options.isDefinedAndNonNull('server') && !options.value.noServer) {
- throw new TypeError('`port` or a `server` must be provided');
- }
-
- var self = this;
-
- if (options.isDefinedAndNonNull('port')) {
- this._server = http.createServer(function (req, res) {
- res.writeHead(200, {'Content-Type': 'text/plain'});
- res.end('Not implemented');
- });
- this._server.listen(options.value.port, options.value.host, callback);
- this._closeServer = function() { if (self._server) self._server.close(); };
- }
- else if (options.value.server) {
- this._server = options.value.server;
- if (options.value.path) {
- // take note of the path, to avoid collisions when multiple websocket servers are
- // listening on the same http server
- if (this._server._webSocketPaths && options.value.server._webSocketPaths[options.value.path]) {
- throw new Error('two instances of WebSocketServer cannot listen on the same http server path');
- }
- if (typeof this._server._webSocketPaths !== 'object') {
- this._server._webSocketPaths = {};
- }
- this._server._webSocketPaths[options.value.path] = 1;
- }
- }
- if (this._server) this._server.once('listening', function() { self.emit('listening'); });
-
- if (typeof this._server != 'undefined') {
- this._server.on('error', function(error) {
- self.emit('error', error)
- });
- this._server.on('upgrade', function(req, socket, upgradeHead) {
- //copy upgradeHead to avoid retention of large slab buffers used in node core
- var head = new Buffer(upgradeHead.length);
- upgradeHead.copy(head);
-
- self.handleUpgrade(req, socket, head, function(client) {
- self.emit('connection'+req.url, client);
- self.emit('connection', client);
- });
- });
- }
-
- this.options = options.value;
- this.path = options.value.path;
- this.clients = [];
- }
-
- /**
- * Inherits from EventEmitter.
- */
-
- util.inherits(WebSocketServer, events.EventEmitter);
-
- /**
- * Immediately shuts down the connection.
- *
- * @api public
- */
-
- WebSocketServer.prototype.close = function() {
- // terminate all associated clients
- var error = null;
- try {
- for (var i = 0, l = this.clients.length; i < l; ++i) {
- this.clients[i].terminate();
- }
- }
- catch (e) {
- error = e;
- }
-
- // remove path descriptor, if any
- if (this.path && this._server._webSocketPaths) {
- delete this._server._webSocketPaths[this.path];
- if (Object.keys(this._server._webSocketPaths).length == 0) {
- delete this._server._webSocketPaths;
- }
- }
-
- // close the http server if it was internally created
- try {
- if (typeof this._closeServer !== 'undefined') {
- this._closeServer();
- }
- }
- finally {
- delete this._server;
- }
- if (error) throw error;
- }
-
- /**
- * Handle a HTTP Upgrade request.
- *
- * @api public
- */
-
- WebSocketServer.prototype.handleUpgrade = function(req, socket, upgradeHead, cb) {
- // check for wrong path
- if (this.options.path) {
- var u = url.parse(req.url);
- if (u && u.pathname !== this.options.path) return;
- }
-
- if (typeof req.headers.upgrade === 'undefined' || req.headers.upgrade.toLowerCase() !== 'websocket') {
- abortConnection(socket, 400, 'Bad Request');
- return;
- }
-
- if (req.headers['sec-websocket-key1']) handleHixieUpgrade.apply(this, arguments);
- else handleHybiUpgrade.apply(this, arguments);
- }
-
- module.exports = WebSocketServer;
-
- /**
- * Entirely private apis,
- * which may or may not be bound to a sepcific WebSocket instance.
- */
-
- function handleHybiUpgrade(req, socket, upgradeHead, cb) {
- // handle premature socket errors
- var errorHandler = function() {
- try { socket.destroy(); } catch (e) {}
- }
- socket.on('error', errorHandler);
-
- // verify key presence
- if (!req.headers['sec-websocket-key']) {
- abortConnection(socket, 400, 'Bad Request');
- return;
- }
-
- // verify version
- var version = parseInt(req.headers['sec-websocket-version']);
- if ([8, 13].indexOf(version) === -1) {
- abortConnection(socket, 400, 'Bad Request');
- return;
- }
-
- // verify protocol
- var protocols = req.headers['sec-websocket-protocol'];
-
- // verify client
- var origin = version < 13 ?
- req.headers['sec-websocket-origin'] :
- req.headers['origin'];
-
- // handle extensions offer
- var extensionsOffer = Extensions.parse(req.headers['sec-websocket-extensions']);
-
- // handler to call when the connection sequence completes
- var self = this;
- var completeHybiUpgrade2 = function(protocol) {
-
- // calc key
- var key = req.headers['sec-websocket-key'];
- var shasum = crypto.createHash('sha1');
- shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
- key = shasum.digest('base64');
-
- var headers = [
- 'HTTP/1.1 101 Switching Protocols'
- , 'Upgrade: websocket'
- , 'Connection: Upgrade'
- , 'Sec-WebSocket-Accept: ' + key
- ];
-
- if (typeof protocol != 'undefined') {
- headers.push('Sec-WebSocket-Protocol: ' + protocol);
- }
-
- var extensions = {};
- try {
- extensions = acceptExtensions.call(self, extensionsOffer);
- } catch (err) {
- abortConnection(socket, 400, 'Bad Request');
- return;
- }
-
- if (Object.keys(extensions).length) {
- var serverExtensions = {};
- Object.keys(extensions).forEach(function(token) {
- serverExtensions[token] = [extensions[token].params]
- });
- headers.push('Sec-WebSocket-Extensions: ' + Extensions.format(serverExtensions));
- }
-
- // allows external modification/inspection of handshake headers
- self.emit('headers', headers);
-
- socket.setTimeout(0);
- socket.setNoDelay(true);
- try {
- socket.write(headers.concat('', '').join('\r\n'));
- }
- catch (e) {
- // if the upgrade write fails, shut the connection down hard
- try { socket.destroy(); } catch (e) {}
- return;
- }
-
- var client = new WebSocket([req, socket, upgradeHead], {
- protocolVersion: version,
- protocol: protocol,
- extensions: extensions
- });
-
- if (self.options.clientTracking) {
- self.clients.push(client);
- client.on('close', function() {
- var index = self.clients.indexOf(client);
- if (index != -1) {
- self.clients.splice(index, 1);
- }
- });
- }
-
- // signal upgrade complete
- socket.removeListener('error', errorHandler);
- cb(client);
- }
-
- // optionally call external protocol selection handler before
- // calling completeHybiUpgrade2
- var completeHybiUpgrade1 = function() {
- // choose from the sub-protocols
- if (typeof self.options.handleProtocols == 'function') {
- var protList = (protocols || "").split(/, */);
- var callbackCalled = false;
- var res = self.options.handleProtocols(protList, function(result, protocol) {
- callbackCalled = true;
- if (!result) abortConnection(socket, 401, 'Unauthorized');
- else completeHybiUpgrade2(protocol);
- });
- if (!callbackCalled) {
- // the handleProtocols handler never called our callback
- abortConnection(socket, 501, 'Could not process protocols');
- }
- return;
- } else {
- if (typeof protocols !== 'undefined') {
- completeHybiUpgrade2(protocols.split(/, */)[0]);
- }
- else {
- completeHybiUpgrade2();
- }
- }
- }
-
- // optionally call external client verification handler
- if (typeof this.options.verifyClient == 'function') {
- var info = {
- origin: origin,
- secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined',
- req: req
- };
- if (this.options.verifyClient.length == 2) {
- this.options.verifyClient(info, function(result, code, name) {
- if (typeof code === 'undefined') code = 401;
- if (typeof name === 'undefined') name = http.STATUS_CODES[code];
-
- if (!result) abortConnection(socket, code, name);
- else completeHybiUpgrade1();
- });
- return;
- }
- else if (!this.options.verifyClient(info)) {
- abortConnection(socket, 401, 'Unauthorized');
- return;
- }
- }
-
- completeHybiUpgrade1();
- }
-
- function handleHixieUpgrade(req, socket, upgradeHead, cb) {
- // handle premature socket errors
- var errorHandler = function() {
- try { socket.destroy(); } catch (e) {}
- }
- socket.on('error', errorHandler);
-
- // bail if options prevent hixie
- if (this.options.disableHixie) {
- abortConnection(socket, 401, 'Hixie support disabled');
- return;
- }
-
- // verify key presence
- if (!req.headers['sec-websocket-key2']) {
- abortConnection(socket, 400, 'Bad Request');
- return;
- }
-
- var origin = req.headers['origin']
- , self = this;
-
- // setup handshake completion to run after client has been verified
- var onClientVerified = function() {
- var wshost;
- if (!req.headers['x-forwarded-host'])
- wshost = req.headers.host;
- else
- wshost = req.headers['x-forwarded-host'];
- var location = ((req.headers['x-forwarded-proto'] === 'https' || socket.encrypted) ? 'wss' : 'ws') + '://' + wshost + req.url
- , protocol = req.headers['sec-websocket-protocol'];
-
- // handshake completion code to run once nonce has been successfully retrieved
- var completeHandshake = function(nonce, rest) {
- // calculate key
- var k1 = req.headers['sec-websocket-key1']
- , k2 = req.headers['sec-websocket-key2']
- , md5 = crypto.createHash('md5');
-
- [k1, k2].forEach(function (k) {
- var n = parseInt(k.replace(/[^\d]/g, ''))
- , spaces = k.replace(/[^ ]/g, '').length;
- if (spaces === 0 || n % spaces !== 0){
- abortConnection(socket, 400, 'Bad Request');
- return;
- }
- n /= spaces;
- md5.update(String.fromCharCode(
- n >> 24 & 0xFF,
- n >> 16 & 0xFF,
- n >> 8 & 0xFF,
- n & 0xFF));
- });
- md5.update(nonce.toString('binary'));
-
- var headers = [
- 'HTTP/1.1 101 Switching Protocols'
- , 'Upgrade: WebSocket'
- , 'Connection: Upgrade'
- , 'Sec-WebSocket-Location: ' + location
- ];
- if (typeof protocol != 'undefined') headers.push('Sec-WebSocket-Protocol: ' + protocol);
- if (typeof origin != 'undefined') headers.push('Sec-WebSocket-Origin: ' + origin);
-
- socket.setTimeout(0);
- socket.setNoDelay(true);
- try {
- // merge header and hash buffer
- var headerBuffer = new Buffer(headers.concat('', '').join('\r\n'));
- var hashBuffer = new Buffer(md5.digest('binary'), 'binary');
- var handshakeBuffer = new Buffer(headerBuffer.length + hashBuffer.length);
- headerBuffer.copy(handshakeBuffer, 0);
- hashBuffer.copy(handshakeBuffer, headerBuffer.length);
-
- // do a single write, which - upon success - causes a new client websocket to be setup
- socket.write(handshakeBuffer, 'binary', function(err) {
- if (err) return; // do not create client if an error happens
- var client = new WebSocket([req, socket, rest], {
- protocolVersion: 'hixie-76',
- protocol: protocol
- });
- if (self.options.clientTracking) {
- self.clients.push(client);
- client.on('close', function() {
- var index = self.clients.indexOf(client);
- if (index != -1) {
- self.clients.splice(index, 1);
- }
- });
- }
-
- // signal upgrade complete
- socket.removeListener('error', errorHandler);
- cb(client);
- });
- }
- catch (e) {
- try { socket.destroy(); } catch (e) {}
- return;
- }
- }
-
- // retrieve nonce
- var nonceLength = 8;
- if (upgradeHead && upgradeHead.length >= nonceLength) {
- var nonce = upgradeHead.slice(0, nonceLength);
- var rest = upgradeHead.length > nonceLength ? upgradeHead.slice(nonceLength) : null;
- completeHandshake.call(self, nonce, rest);
- }
- else {
- // nonce not present in upgradeHead, so we must wait for enough data
- // data to arrive before continuing
- var nonce = new Buffer(nonceLength);
- upgradeHead.copy(nonce, 0);
- var received = upgradeHead.length;
- var rest = null;
- var handler = function (data) {
- var toRead = Math.min(data.length, nonceLength - received);
- if (toRead === 0) return;
- data.copy(nonce, received, 0, toRead);
- received += toRead;
- if (received == nonceLength) {
- socket.removeListener('data', handler);
- if (toRead < data.length) rest = data.slice(toRead);
- completeHandshake.call(self, nonce, rest);
- }
- }
- socket.on('data', handler);
- }
- }
-
- // verify client
- if (typeof this.options.verifyClient == 'function') {
- var info = {
- origin: origin,
- secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined',
- req: req
- };
- if (this.options.verifyClient.length == 2) {
- var self = this;
- this.options.verifyClient(info, function(result, code, name) {
- if (typeof code === 'undefined') code = 401;
- if (typeof name === 'undefined') name = http.STATUS_CODES[code];
-
- if (!result) abortConnection(socket, code, name);
- else onClientVerified.apply(self);
- });
- return;
- }
- else if (!this.options.verifyClient(info)) {
- abortConnection(socket, 401, 'Unauthorized');
- return;
- }
- }
-
- // no client verification required
- onClientVerified();
- }
-
- function acceptExtensions(offer) {
- var extensions = {};
- var options = this.options.perMessageDeflate;
- if (options && offer[PerMessageDeflate.extensionName]) {
- var perMessageDeflate = new PerMessageDeflate(options !== true ? options : {}, true);
- perMessageDeflate.accept(offer[PerMessageDeflate.extensionName]);
- extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
- }
- return extensions;
- }
-
- function abortConnection(socket, code, name) {
- try {
- var response = [
- 'HTTP/1.1 ' + code + ' ' + name,
- 'Content-type: text/html'
- ];
- socket.write(response.concat('', '').join('\r\n'));
- }
- catch (e) { /* ignore errors - we've aborted this connection */ }
- finally {
- // ensure that an early aborted connection is shut down completely
- try { socket.destroy(); } catch (e) {}
- }
- }
|