twitst4tz

twitter statistics web application
Log | Files | Refs | README | LICENSE

index.js (12947B)


      1 'use strict';
      2 
      3 /**
      4  * Module dependencies.
      5  */
      6 
      7 var http = require('http');
      8 var read = require('fs').readFileSync;
      9 var path = require('path');
     10 var exists = require('fs').existsSync;
     11 var engine = require('engine.io');
     12 var clientVersion = require('socket.io-client/package.json').version;
     13 var Client = require('./client');
     14 var Emitter = require('events').EventEmitter;
     15 var Namespace = require('./namespace');
     16 var ParentNamespace = require('./parent-namespace');
     17 var Adapter = require('socket.io-adapter');
     18 var parser = require('socket.io-parser');
     19 var debug = require('debug')('socket.io:server');
     20 var url = require('url');
     21 
     22 /**
     23  * Module exports.
     24  */
     25 
     26 module.exports = Server;
     27 
     28 /**
     29  * Socket.IO client source.
     30  */
     31 
     32 var clientSource = undefined;
     33 var clientSourceMap = undefined;
     34 
     35 /**
     36  * Server constructor.
     37  *
     38  * @param {http.Server|Number|Object} srv http server, port or options
     39  * @param {Object} [opts]
     40  * @api public
     41  */
     42 
     43 function Server(srv, opts){
     44   if (!(this instanceof Server)) return new Server(srv, opts);
     45   if ('object' == typeof srv && srv instanceof Object && !srv.listen) {
     46     opts = srv;
     47     srv = null;
     48   }
     49   opts = opts || {};
     50   this.nsps = {};
     51   this.parentNsps = new Map();
     52   this.path(opts.path || '/socket.io');
     53   this.serveClient(false !== opts.serveClient);
     54   this.parser = opts.parser || parser;
     55   this.encoder = new this.parser.Encoder();
     56   this.adapter(opts.adapter || Adapter);
     57   this.origins(opts.origins || '*:*');
     58   this.sockets = this.of('/');
     59   if (srv) this.attach(srv, opts);
     60 }
     61 
     62 /**
     63  * Server request verification function, that checks for allowed origins
     64  *
     65  * @param {http.IncomingMessage} req request
     66  * @param {Function} fn callback to be called with the result: `fn(err, success)`
     67  */
     68 
     69 Server.prototype.checkRequest = function(req, fn) {
     70   var origin = req.headers.origin || req.headers.referer;
     71 
     72   // file:// URLs produce a null Origin which can't be authorized via echo-back
     73   if ('null' == origin || null == origin) origin = '*';
     74 
     75   if (!!origin && typeof(this._origins) == 'function') return this._origins(origin, fn);
     76   if (this._origins.indexOf('*:*') !== -1) return fn(null, true);
     77   if (origin) {
     78     try {
     79       var parts = url.parse(origin);
     80       var defaultPort = 'https:' == parts.protocol ? 443 : 80;
     81       parts.port = parts.port != null
     82         ? parts.port
     83         : defaultPort;
     84       var ok =
     85         ~this._origins.indexOf(parts.protocol + '//' + parts.hostname + ':' + parts.port) ||
     86         ~this._origins.indexOf(parts.hostname + ':' + parts.port) ||
     87         ~this._origins.indexOf(parts.hostname + ':*') ||
     88         ~this._origins.indexOf('*:' + parts.port);
     89       debug('origin %s is %svalid', origin, !!ok ? '' : 'not ');
     90       return fn(null, !!ok);
     91     } catch (ex) {
     92     }
     93   }
     94   fn(null, false);
     95 };
     96 
     97 /**
     98  * Sets/gets whether client code is being served.
     99  *
    100  * @param {Boolean} v whether to serve client code
    101  * @return {Server|Boolean} self when setting or value when getting
    102  * @api public
    103  */
    104 
    105 Server.prototype.serveClient = function(v){
    106   if (!arguments.length) return this._serveClient;
    107   this._serveClient = v;
    108   var resolvePath = function(file){
    109     var filepath = path.resolve(__dirname, './../../', file);
    110     if (exists(filepath)) {
    111       return filepath;
    112     }
    113     return require.resolve(file);
    114   };
    115   if (v && !clientSource) {
    116     clientSource = read(resolvePath( 'socket.io-client/dist/socket.io.js'), 'utf-8');
    117     try {
    118       clientSourceMap = read(resolvePath( 'socket.io-client/dist/socket.io.js.map'), 'utf-8');
    119     } catch(err) {
    120       debug('could not load sourcemap file');
    121     }
    122   }
    123   return this;
    124 };
    125 
    126 /**
    127  * Old settings for backwards compatibility
    128  */
    129 
    130 var oldSettings = {
    131   "transports": "transports",
    132   "heartbeat timeout": "pingTimeout",
    133   "heartbeat interval": "pingInterval",
    134   "destroy buffer size": "maxHttpBufferSize"
    135 };
    136 
    137 /**
    138  * Backwards compatibility.
    139  *
    140  * @api public
    141  */
    142 
    143 Server.prototype.set = function(key, val){
    144   if ('authorization' == key && val) {
    145     this.use(function(socket, next) {
    146       val(socket.request, function(err, authorized) {
    147         if (err) return next(new Error(err));
    148         if (!authorized) return next(new Error('Not authorized'));
    149         next();
    150       });
    151     });
    152   } else if ('origins' == key && val) {
    153     this.origins(val);
    154   } else if ('resource' == key) {
    155     this.path(val);
    156   } else if (oldSettings[key] && this.eio[oldSettings[key]]) {
    157     this.eio[oldSettings[key]] = val;
    158   } else {
    159     console.error('Option %s is not valid. Please refer to the README.', key);
    160   }
    161 
    162   return this;
    163 };
    164 
    165 /**
    166  * Executes the middleware for an incoming namespace not already created on the server.
    167  *
    168  * @param {String} name name of incoming namespace
    169  * @param {Object} query the query parameters
    170  * @param {Function} fn callback
    171  * @api private
    172  */
    173 
    174 Server.prototype.checkNamespace = function(name, query, fn){
    175   if (this.parentNsps.size === 0) return fn(false);
    176 
    177   const keysIterator = this.parentNsps.keys();
    178 
    179   const run = () => {
    180     let nextFn = keysIterator.next();
    181     if (nextFn.done) {
    182       return fn(false);
    183     }
    184     nextFn.value(name, query, (err, allow) => {
    185       if (err || !allow) {
    186         run();
    187       } else {
    188         fn(this.parentNsps.get(nextFn.value).createChild(name));
    189       }
    190     });
    191   };
    192 
    193   run();
    194 };
    195 
    196 /**
    197  * Sets the client serving path.
    198  *
    199  * @param {String} v pathname
    200  * @return {Server|String} self when setting or value when getting
    201  * @api public
    202  */
    203 
    204 Server.prototype.path = function(v){
    205   if (!arguments.length) return this._path;
    206   this._path = v.replace(/\/$/, '');
    207   return this;
    208 };
    209 
    210 /**
    211  * Sets the adapter for rooms.
    212  *
    213  * @param {Adapter} v pathname
    214  * @return {Server|Adapter} self when setting or value when getting
    215  * @api public
    216  */
    217 
    218 Server.prototype.adapter = function(v){
    219   if (!arguments.length) return this._adapter;
    220   this._adapter = v;
    221   for (var i in this.nsps) {
    222     if (this.nsps.hasOwnProperty(i)) {
    223       this.nsps[i].initAdapter();
    224     }
    225   }
    226   return this;
    227 };
    228 
    229 /**
    230  * Sets the allowed origins for requests.
    231  *
    232  * @param {String|String[]} v origins
    233  * @return {Server|Adapter} self when setting or value when getting
    234  * @api public
    235  */
    236 
    237 Server.prototype.origins = function(v){
    238   if (!arguments.length) return this._origins;
    239 
    240   this._origins = v;
    241   return this;
    242 };
    243 
    244 /**
    245  * Attaches socket.io to a server or port.
    246  *
    247  * @param {http.Server|Number} server or port
    248  * @param {Object} options passed to engine.io
    249  * @return {Server} self
    250  * @api public
    251  */
    252 
    253 Server.prototype.listen =
    254 Server.prototype.attach = function(srv, opts){
    255   if ('function' == typeof srv) {
    256     var msg = 'You are trying to attach socket.io to an express ' +
    257     'request handler function. Please pass a http.Server instance.';
    258     throw new Error(msg);
    259   }
    260 
    261   // handle a port as a string
    262   if (Number(srv) == srv) {
    263     srv = Number(srv);
    264   }
    265 
    266   if ('number' == typeof srv) {
    267     debug('creating http server and binding to %d', srv);
    268     var port = srv;
    269     srv = http.Server(function(req, res){
    270       res.writeHead(404);
    271       res.end();
    272     });
    273     srv.listen(port);
    274 
    275   }
    276 
    277   // set engine.io path to `/socket.io`
    278   opts = opts || {};
    279   opts.path = opts.path || this.path();
    280   // set origins verification
    281   opts.allowRequest = opts.allowRequest || this.checkRequest.bind(this);
    282 
    283   if (this.sockets.fns.length > 0) {
    284     this.initEngine(srv, opts);
    285     return this;
    286   }
    287 
    288   var self = this;
    289   var connectPacket = { type: parser.CONNECT, nsp: '/' };
    290   this.encoder.encode(connectPacket, function (encodedPacket){
    291     // the CONNECT packet will be merged with Engine.IO handshake,
    292     // to reduce the number of round trips
    293     opts.initialPacket = encodedPacket;
    294 
    295     self.initEngine(srv, opts);
    296   });
    297   return this;
    298 };
    299 
    300 /**
    301  * Initialize engine
    302  *
    303  * @param {Object} options passed to engine.io
    304  * @api private
    305  */
    306 
    307 Server.prototype.initEngine = function(srv, opts){
    308   // initialize engine
    309   debug('creating engine.io instance with opts %j', opts);
    310   this.eio = engine.attach(srv, opts);
    311 
    312   // attach static file serving
    313   if (this._serveClient) this.attachServe(srv);
    314 
    315   // Export http server
    316   this.httpServer = srv;
    317 
    318   // bind to engine events
    319   this.bind(this.eio);
    320 };
    321 
    322 /**
    323  * Attaches the static file serving.
    324  *
    325  * @param {Function|http.Server} srv http server
    326  * @api private
    327  */
    328 
    329 Server.prototype.attachServe = function(srv){
    330   debug('attaching client serving req handler');
    331   var url = this._path + '/socket.io.js';
    332   var urlMap = this._path + '/socket.io.js.map';
    333   var evs = srv.listeners('request').slice(0);
    334   var self = this;
    335   srv.removeAllListeners('request');
    336   srv.on('request', function(req, res) {
    337     if (0 === req.url.indexOf(urlMap)) {
    338       self.serveMap(req, res);
    339     } else if (0 === req.url.indexOf(url)) {
    340       self.serve(req, res);
    341     } else {
    342       for (var i = 0; i < evs.length; i++) {
    343         evs[i].call(srv, req, res);
    344       }
    345     }
    346   });
    347 };
    348 
    349 /**
    350  * Handles a request serving `/socket.io.js`
    351  *
    352  * @param {http.Request} req
    353  * @param {http.Response} res
    354  * @api private
    355  */
    356 
    357 Server.prototype.serve = function(req, res){
    358   // Per the standard, ETags must be quoted:
    359   // https://tools.ietf.org/html/rfc7232#section-2.3
    360   var expectedEtag = '"' + clientVersion + '"';
    361 
    362   var etag = req.headers['if-none-match'];
    363   if (etag) {
    364     if (expectedEtag == etag) {
    365       debug('serve client 304');
    366       res.writeHead(304);
    367       res.end();
    368       return;
    369     }
    370   }
    371 
    372   debug('serve client source');
    373   res.setHeader("Cache-Control", "public, max-age=0");
    374   res.setHeader('Content-Type', 'application/javascript');
    375   res.setHeader('ETag', expectedEtag);
    376   res.writeHead(200);
    377   res.end(clientSource);
    378 };
    379 
    380 /**
    381  * Handles a request serving `/socket.io.js.map`
    382  *
    383  * @param {http.Request} req
    384  * @param {http.Response} res
    385  * @api private
    386  */
    387 
    388 Server.prototype.serveMap = function(req, res){
    389   // Per the standard, ETags must be quoted:
    390   // https://tools.ietf.org/html/rfc7232#section-2.3
    391   var expectedEtag = '"' + clientVersion + '"';
    392 
    393   var etag = req.headers['if-none-match'];
    394   if (etag) {
    395     if (expectedEtag == etag) {
    396       debug('serve client 304');
    397       res.writeHead(304);
    398       res.end();
    399       return;
    400     }
    401   }
    402 
    403   debug('serve client sourcemap');
    404   res.setHeader('Content-Type', 'application/json');
    405   res.setHeader('ETag', expectedEtag);
    406   res.writeHead(200);
    407   res.end(clientSourceMap);
    408 };
    409 
    410 /**
    411  * Binds socket.io to an engine.io instance.
    412  *
    413  * @param {engine.Server} engine engine.io (or compatible) server
    414  * @return {Server} self
    415  * @api public
    416  */
    417 
    418 Server.prototype.bind = function(engine){
    419   this.engine = engine;
    420   this.engine.on('connection', this.onconnection.bind(this));
    421   return this;
    422 };
    423 
    424 /**
    425  * Called with each incoming transport connection.
    426  *
    427  * @param {engine.Socket} conn
    428  * @return {Server} self
    429  * @api public
    430  */
    431 
    432 Server.prototype.onconnection = function(conn){
    433   debug('incoming connection with id %s', conn.id);
    434   var client = new Client(this, conn);
    435   client.connect('/');
    436   return this;
    437 };
    438 
    439 /**
    440  * Looks up a namespace.
    441  *
    442  * @param {String|RegExp|Function} name nsp name
    443  * @param {Function} [fn] optional, nsp `connection` ev handler
    444  * @api public
    445  */
    446 
    447 Server.prototype.of = function(name, fn){
    448   if (typeof name === 'function' || name instanceof RegExp) {
    449     const parentNsp = new ParentNamespace(this);
    450     debug('initializing parent namespace %s', parentNsp.name);
    451     if (typeof name === 'function') {
    452       this.parentNsps.set(name, parentNsp);
    453     } else {
    454       this.parentNsps.set((nsp, conn, next) => next(null, name.test(nsp)), parentNsp);
    455     }
    456     if (fn) parentNsp.on('connect', fn);
    457     return parentNsp;
    458   }
    459 
    460   if (String(name)[0] !== '/') name = '/' + name;
    461 
    462   var nsp = this.nsps[name];
    463   if (!nsp) {
    464     debug('initializing namespace %s', name);
    465     nsp = new Namespace(this, name);
    466     this.nsps[name] = nsp;
    467   }
    468   if (fn) nsp.on('connect', fn);
    469   return nsp;
    470 };
    471 
    472 /**
    473  * Closes server connection
    474  *
    475  * @param {Function} [fn] optional, called as `fn([err])` on error OR all conns closed
    476  * @api public
    477  */
    478 
    479 Server.prototype.close = function(fn){
    480   for (var id in this.nsps['/'].sockets) {
    481     if (this.nsps['/'].sockets.hasOwnProperty(id)) {
    482       this.nsps['/'].sockets[id].onclose();
    483     }
    484   }
    485 
    486   this.engine.close();
    487 
    488   if (this.httpServer) {
    489     this.httpServer.close(fn);
    490   } else {
    491     fn && fn();
    492   }
    493 };
    494 
    495 /**
    496  * Expose main namespace (/).
    497  */
    498 
    499 var emitterMethods = Object.keys(Emitter.prototype).filter(function(key){
    500   return typeof Emitter.prototype[key] === 'function';
    501 });
    502 
    503 emitterMethods.concat(['to', 'in', 'use', 'send', 'write', 'clients', 'compress', 'binary']).forEach(function(fn){
    504   Server.prototype[fn] = function(){
    505     return this.sockets[fn].apply(this.sockets, arguments);
    506   };
    507 });
    508 
    509 Namespace.flags.forEach(function(flag){
    510   Object.defineProperty(Server.prototype, flag, {
    511     get: function() {
    512       this.sockets.flags = this.sockets.flags || {};
    513       this.sockets.flags[flag] = true;
    514       return this;
    515     }
    516   });
    517 });
    518 
    519 /**
    520  * BC with `io.listen`
    521  */
    522 
    523 Server.listen = Server;