You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

460 lines
14 KiB

  1. /*!
  2. * ws: a node.js websocket client
  3. * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
  4. * MIT Licensed
  5. */
  6. var util = require('util')
  7. , events = require('events')
  8. , http = require('http')
  9. , crypto = require('crypto')
  10. , url = require('url')
  11. , Options = require('options')
  12. , WebSocket = require('./WebSocket')
  13. , tls = require('tls')
  14. , url = require('url');
  15. /**
  16. * WebSocket Server implementation
  17. */
  18. function WebSocketServer(options, callback) {
  19. options = new Options({
  20. host: '0.0.0.0',
  21. port: null,
  22. server: null,
  23. verifyClient: null,
  24. handleProtocols: null,
  25. path: null,
  26. noServer: false,
  27. disableHixie: false,
  28. clientTracking: true
  29. }).merge(options);
  30. if (!options.isDefinedAndNonNull('port') && !options.isDefinedAndNonNull('server') && !options.value.noServer) {
  31. throw new TypeError('`port` or a `server` must be provided');
  32. }
  33. var self = this;
  34. if (options.isDefinedAndNonNull('port')) {
  35. this._server = http.createServer(function (req, res) {
  36. res.writeHead(200, {'Content-Type': 'text/plain'});
  37. res.end('Not implemented');
  38. });
  39. this._server.listen(options.value.port, options.value.host, callback);
  40. this._closeServer = function() { self._server.close(); };
  41. }
  42. else if (options.value.server) {
  43. this._server = options.value.server;
  44. if (options.value.path) {
  45. // take note of the path, to avoid collisions when multiple websocket servers are
  46. // listening on the same http server
  47. if (this._server._webSocketPaths && options.value.server._webSocketPaths[options.value.path]) {
  48. throw new Error('two instances of WebSocketServer cannot listen on the same http server path');
  49. }
  50. if (typeof this._server._webSocketPaths !== 'object') {
  51. this._server._webSocketPaths = {};
  52. }
  53. this._server._webSocketPaths[options.value.path] = 1;
  54. }
  55. }
  56. if (this._server) this._server.once('listening', function() { self.emit('listening'); });
  57. if (typeof this._server != 'undefined') {
  58. this._server.on('error', function(error) {
  59. self.emit('error', error)
  60. });
  61. this._server.on('upgrade', function(req, socket, upgradeHead) {
  62. //copy upgradeHead to avoid retention of large slab buffers used in node core
  63. var head = new Buffer(upgradeHead.length);
  64. upgradeHead.copy(head);
  65. self.handleUpgrade(req, socket, head, function(client) {
  66. self.emit('connection'+req.url, client);
  67. self.emit('connection', client);
  68. });
  69. });
  70. }
  71. this.options = options.value;
  72. this.path = options.value.path;
  73. this.clients = [];
  74. }
  75. /**
  76. * Inherits from EventEmitter.
  77. */
  78. util.inherits(WebSocketServer, events.EventEmitter);
  79. /**
  80. * Immediately shuts down the connection.
  81. *
  82. * @api public
  83. */
  84. WebSocketServer.prototype.close = function() {
  85. // terminate all associated clients
  86. var error = null;
  87. try {
  88. for (var i = 0, l = this.clients.length; i < l; ++i) {
  89. this.clients[i].terminate();
  90. }
  91. }
  92. catch (e) {
  93. error = e;
  94. }
  95. // remove path descriptor, if any
  96. if (this.path && this._server._webSocketPaths) {
  97. delete this._server._webSocketPaths[this.path];
  98. if (Object.keys(this._server._webSocketPaths).length == 0) {
  99. delete this._server._webSocketPaths;
  100. }
  101. }
  102. // close the http server if it was internally created
  103. try {
  104. if (typeof this._closeServer !== 'undefined') {
  105. this._closeServer();
  106. }
  107. }
  108. finally {
  109. delete this._server;
  110. }
  111. if (error) throw error;
  112. }
  113. /**
  114. * Handle a HTTP Upgrade request.
  115. *
  116. * @api public
  117. */
  118. WebSocketServer.prototype.handleUpgrade = function(req, socket, upgradeHead, cb) {
  119. // check for wrong path
  120. if (this.options.path) {
  121. var u = url.parse(req.url);
  122. if (u && u.pathname !== this.options.path) return;
  123. }
  124. if (typeof req.headers.upgrade === 'undefined' || req.headers.upgrade.toLowerCase() !== 'websocket') {
  125. abortConnection(socket, 400, 'Bad Request');
  126. return;
  127. }
  128. if (req.headers['sec-websocket-key1']) handleHixieUpgrade.apply(this, arguments);
  129. else handleHybiUpgrade.apply(this, arguments);
  130. }
  131. module.exports = WebSocketServer;
  132. /**
  133. * Entirely private apis,
  134. * which may or may not be bound to a sepcific WebSocket instance.
  135. */
  136. function handleHybiUpgrade(req, socket, upgradeHead, cb) {
  137. // handle premature socket errors
  138. var errorHandler = function() {
  139. try { socket.destroy(); } catch (e) {}
  140. }
  141. socket.on('error', errorHandler);
  142. // verify key presence
  143. if (!req.headers['sec-websocket-key']) {
  144. abortConnection(socket, 400, 'Bad Request');
  145. return;
  146. }
  147. // verify version
  148. var version = parseInt(req.headers['sec-websocket-version']);
  149. if ([8, 13].indexOf(version) === -1) {
  150. abortConnection(socket, 400, 'Bad Request');
  151. return;
  152. }
  153. // verify protocol
  154. var protocols = req.headers['sec-websocket-protocol'];
  155. // verify client
  156. var origin = version < 13 ?
  157. req.headers['sec-websocket-origin'] :
  158. req.headers['origin'];
  159. // handler to call when the connection sequence completes
  160. var self = this;
  161. var completeHybiUpgrade2 = function(protocol) {
  162. // calc key
  163. var key = req.headers['sec-websocket-key'];
  164. var shasum = crypto.createHash('sha1');
  165. shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
  166. key = shasum.digest('base64');
  167. var headers = [
  168. 'HTTP/1.1 101 Switching Protocols'
  169. , 'Upgrade: websocket'
  170. , 'Connection: Upgrade'
  171. , 'Sec-WebSocket-Accept: ' + key
  172. ];
  173. if (typeof protocol != 'undefined') {
  174. headers.push('Sec-WebSocket-Protocol: ' + protocol);
  175. }
  176. // allows external modification/inspection of handshake headers
  177. self.emit('headers', headers);
  178. socket.setTimeout(0);
  179. socket.setNoDelay(true);
  180. try {
  181. socket.write(headers.concat('', '').join('\r\n'));
  182. }
  183. catch (e) {
  184. // if the upgrade write fails, shut the connection down hard
  185. try { socket.destroy(); } catch (e) {}
  186. return;
  187. }
  188. var client = new WebSocket([req, socket, upgradeHead], {
  189. protocolVersion: version,
  190. protocol: protocol
  191. });
  192. if (self.options.clientTracking) {
  193. self.clients.push(client);
  194. client.on('close', function() {
  195. var index = self.clients.indexOf(client);
  196. if (index != -1) {
  197. self.clients.splice(index, 1);
  198. }
  199. });
  200. }
  201. // signal upgrade complete
  202. socket.removeListener('error', errorHandler);
  203. cb(client);
  204. }
  205. // optionally call external protocol selection handler before
  206. // calling completeHybiUpgrade2
  207. var completeHybiUpgrade1 = function() {
  208. // choose from the sub-protocols
  209. if (typeof self.options.handleProtocols == 'function') {
  210. var protList = (protocols || "").split(/, */);
  211. var callbackCalled = false;
  212. var res = self.options.handleProtocols(protList, function(result, protocol) {
  213. callbackCalled = true;
  214. if (!result) abortConnection(socket, 404, 'Unauthorized')
  215. else completeHybiUpgrade2(protocol);
  216. });
  217. if (!callbackCalled) {
  218. // the handleProtocols handler never called our callback
  219. abortConnection(socket, 501, 'Could not process protocols');
  220. }
  221. return;
  222. } else {
  223. if (typeof protocols !== 'undefined') {
  224. completeHybiUpgrade2(protocols.split(/, */)[0]);
  225. }
  226. else {
  227. completeHybiUpgrade2();
  228. }
  229. }
  230. }
  231. // optionally call external client verification handler
  232. if (typeof this.options.verifyClient == 'function') {
  233. var info = {
  234. origin: origin,
  235. secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined',
  236. req: req
  237. };
  238. if (this.options.verifyClient.length == 2) {
  239. this.options.verifyClient(info, function(result) {
  240. if (!result) abortConnection(socket, 401, 'Unauthorized')
  241. else completeHybiUpgrade1();
  242. });
  243. return;
  244. }
  245. else if (!this.options.verifyClient(info)) {
  246. abortConnection(socket, 401, 'Unauthorized');
  247. return;
  248. }
  249. }
  250. completeHybiUpgrade1();
  251. }
  252. function handleHixieUpgrade(req, socket, upgradeHead, cb) {
  253. // handle premature socket errors
  254. var errorHandler = function() {
  255. try { socket.destroy(); } catch (e) {}
  256. }
  257. socket.on('error', errorHandler);
  258. // bail if options prevent hixie
  259. if (this.options.disableHixie) {
  260. abortConnection(socket, 401, 'Hixie support disabled');
  261. return;
  262. }
  263. // verify key presence
  264. if (!req.headers['sec-websocket-key2']) {
  265. abortConnection(socket, 400, 'Bad Request');
  266. return;
  267. }
  268. var origin = req.headers['origin']
  269. , self = this;
  270. // setup handshake completion to run after client has been verified
  271. var onClientVerified = function() {
  272. var wshost;
  273. if (!req.headers['x-forwarded-host'])
  274. wshost = req.headers.host;
  275. else
  276. wshost = req.headers['x-forwarded-host'];
  277. var location = ((req.headers['x-forwarded-proto'] === 'https' || socket.encrypted) ? 'wss' : 'ws') + '://' + wshost + req.url
  278. , protocol = req.headers['sec-websocket-protocol'];
  279. // handshake completion code to run once nonce has been successfully retrieved
  280. var completeHandshake = function(nonce, rest) {
  281. // calculate key
  282. var k1 = req.headers['sec-websocket-key1']
  283. , k2 = req.headers['sec-websocket-key2']
  284. , md5 = crypto.createHash('md5');
  285. [k1, k2].forEach(function (k) {
  286. var n = parseInt(k.replace(/[^\d]/g, ''))
  287. , spaces = k.replace(/[^ ]/g, '').length;
  288. if (spaces === 0 || n % spaces !== 0){
  289. abortConnection(socket, 400, 'Bad Request');
  290. return;
  291. }
  292. n /= spaces;
  293. md5.update(String.fromCharCode(
  294. n >> 24 & 0xFF,
  295. n >> 16 & 0xFF,
  296. n >> 8 & 0xFF,
  297. n & 0xFF));
  298. });
  299. md5.update(nonce.toString('binary'));
  300. var headers = [
  301. 'HTTP/1.1 101 Switching Protocols'
  302. , 'Upgrade: WebSocket'
  303. , 'Connection: Upgrade'
  304. , 'Sec-WebSocket-Location: ' + location
  305. ];
  306. if (typeof protocol != 'undefined') headers.push('Sec-WebSocket-Protocol: ' + protocol);
  307. if (typeof origin != 'undefined') headers.push('Sec-WebSocket-Origin: ' + origin);
  308. socket.setTimeout(0);
  309. socket.setNoDelay(true);
  310. try {
  311. // merge header and hash buffer
  312. var headerBuffer = new Buffer(headers.concat('', '').join('\r\n'));
  313. var hashBuffer = new Buffer(md5.digest('binary'), 'binary');
  314. var handshakeBuffer = new Buffer(headerBuffer.length + hashBuffer.length);
  315. headerBuffer.copy(handshakeBuffer, 0);
  316. hashBuffer.copy(handshakeBuffer, headerBuffer.length);
  317. // do a single write, which - upon success - causes a new client websocket to be setup
  318. socket.write(handshakeBuffer, 'binary', function(err) {
  319. if (err) return; // do not create client if an error happens
  320. var client = new WebSocket([req, socket, rest], {
  321. protocolVersion: 'hixie-76',
  322. protocol: protocol
  323. });
  324. if (self.options.clientTracking) {
  325. self.clients.push(client);
  326. client.on('close', function() {
  327. var index = self.clients.indexOf(client);
  328. if (index != -1) {
  329. self.clients.splice(index, 1);
  330. }
  331. });
  332. }
  333. // signal upgrade complete
  334. socket.removeListener('error', errorHandler);
  335. cb(client);
  336. });
  337. }
  338. catch (e) {
  339. try { socket.destroy(); } catch (e) {}
  340. return;
  341. }
  342. }
  343. // retrieve nonce
  344. var nonceLength = 8;
  345. if (upgradeHead && upgradeHead.length >= nonceLength) {
  346. var nonce = upgradeHead.slice(0, nonceLength);
  347. var rest = upgradeHead.length > nonceLength ? upgradeHead.slice(nonceLength) : null;
  348. completeHandshake.call(self, nonce, rest);
  349. }
  350. else {
  351. // nonce not present in upgradeHead, so we must wait for enough data
  352. // data to arrive before continuing
  353. var nonce = new Buffer(nonceLength);
  354. upgradeHead.copy(nonce, 0);
  355. var received = upgradeHead.length;
  356. var rest = null;
  357. var handler = function (data) {
  358. var toRead = Math.min(data.length, nonceLength - received);
  359. if (toRead === 0) return;
  360. data.copy(nonce, received, 0, toRead);
  361. received += toRead;
  362. if (received == nonceLength) {
  363. socket.removeListener('data', handler);
  364. if (toRead < data.length) rest = data.slice(toRead);
  365. completeHandshake.call(self, nonce, rest);
  366. }
  367. }
  368. socket.on('data', handler);
  369. }
  370. }
  371. // verify client
  372. if (typeof this.options.verifyClient == 'function') {
  373. var info = {
  374. origin: origin,
  375. secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined',
  376. req: req
  377. };
  378. if (this.options.verifyClient.length == 2) {
  379. var self = this;
  380. this.options.verifyClient(info, function(result) {
  381. if (!result) abortConnection(socket, 401, 'Unauthorized')
  382. else onClientVerified.apply(self);
  383. });
  384. return;
  385. }
  386. else if (!this.options.verifyClient(info)) {
  387. abortConnection(socket, 401, 'Unauthorized');
  388. return;
  389. }
  390. }
  391. // no client verification required
  392. onClientVerified();
  393. }
  394. function abortConnection(socket, code, name) {
  395. try {
  396. var response = [
  397. 'HTTP/1.1 ' + code + ' ' + name,
  398. 'Content-type: text/html'
  399. ];
  400. socket.write(response.concat('', '').join('\r\n'));
  401. }
  402. catch (e) { /* ignore errors - we've aborted this connection */ }
  403. finally {
  404. // ensure that an early aborted connection is shut down completely
  405. try { socket.destroy(); } catch (e) {}
  406. }
  407. }