var WebSocketServer = require('ws').Server; /*! * ws: a node.js websocket client * Copyright(c) 2011 Einar Otto Stangvik * MIT Licensed */ var http = require('http'); var crypto = require('crypto'); var WebSocket = require('./ws/WebSocket'); var Extensions = require('./ws/Extensions'); var PerMessageDeflate = require('./ws/PerMessageDeflate'); var url = require('url'); /** * 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); }; /** * 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, maxPayload: self.options.maxPayload }); 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; 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']; // build the response header and return a Buffer var buildResponseHeader = function() { 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); return new Buffer(headers.concat('', '').join('\r\n')); }; // send handshake response before receiving the nonce var handshakeResponse = function() { socket.setTimeout(0); socket.setNoDelay(true); var headerBuffer = buildResponseHeader(); try { socket.write(headerBuffer, 'binary', function(err) { // remove listener if there was an error if (err) socket.removeListener('data', handler); return; }); } catch (e) { try { socket.destroy(); } catch (e) {} return; } }; // handshake completion code to run once nonce has been successfully retrieved var completeHandshake = function(nonce, rest, headerBuffer) { // 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')); socket.setTimeout(0); socket.setNoDelay(true); try { 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 nonce; var rest = null; var nonceLength = 8; if (upgradeHead && upgradeHead.length >= nonceLength) { nonce = upgradeHead.slice(0, nonceLength); rest = upgradeHead.length > nonceLength ? upgradeHead.slice(nonceLength) : null; completeHandshake.call(self, nonce, rest, buildResponseHeader()); } else { // nonce not present in upgradeHead nonce = new Buffer(nonceLength); upgradeHead.copy(nonce, 0); var received = upgradeHead.length; 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); // complete the handshake but send empty buffer for headers since they have already been sent completeHandshake.call(self, nonce, rest, new Buffer(0)); } }; // handle additional data as we receive it socket.on('data', handler); // send header response before we have the nonce to fix haproxy buffering handshakeResponse(); } }; // 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) { 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; var maxPayload = this.options.maxPayload; if (options && offer[PerMessageDeflate.extensionName]) { var perMessageDeflate = new PerMessageDeflate(options !== true ? options : {}, true, maxPayload); 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) {} } } module.exports = WebSocketServer;