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