twitst4tz

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

server.js (15670B)


      1 
      2 /**
      3  * Module dependencies.
      4  */
      5 
      6 var qs = require('querystring');
      7 var parse = require('url').parse;
      8 var base64id = require('base64id');
      9 var transports = require('./transports');
     10 var EventEmitter = require('events').EventEmitter;
     11 var Socket = require('./socket');
     12 var util = require('util');
     13 var debug = require('debug')('engine');
     14 var cookieMod = require('cookie');
     15 
     16 /**
     17  * Module exports.
     18  */
     19 
     20 module.exports = Server;
     21 
     22 /**
     23  * Server constructor.
     24  *
     25  * @param {Object} options
     26  * @api public
     27  */
     28 
     29 function Server (opts) {
     30   if (!(this instanceof Server)) {
     31     return new Server(opts);
     32   }
     33 
     34   this.clients = {};
     35   this.clientsCount = 0;
     36 
     37   opts = opts || {};
     38 
     39   this.wsEngine = opts.wsEngine || process.env.EIO_WS_ENGINE || 'ws';
     40   this.pingTimeout = opts.pingTimeout || 5000;
     41   this.pingInterval = opts.pingInterval || 25000;
     42   this.upgradeTimeout = opts.upgradeTimeout || 10000;
     43   this.maxHttpBufferSize = opts.maxHttpBufferSize || 10E7;
     44   this.transports = opts.transports || Object.keys(transports);
     45   this.allowUpgrades = false !== opts.allowUpgrades;
     46   this.allowRequest = opts.allowRequest;
     47   this.cookie = false !== opts.cookie ? (opts.cookie || 'io') : false;
     48   this.cookiePath = false !== opts.cookiePath ? (opts.cookiePath || '/') : false;
     49   this.cookieHttpOnly = false !== opts.cookieHttpOnly;
     50   this.perMessageDeflate = false !== opts.perMessageDeflate ? (opts.perMessageDeflate || true) : false;
     51   this.httpCompression = false !== opts.httpCompression ? (opts.httpCompression || {}) : false;
     52   this.initialPacket = opts.initialPacket;
     53 
     54   var self = this;
     55 
     56   // initialize compression options
     57   ['perMessageDeflate', 'httpCompression'].forEach(function (type) {
     58     var compression = self[type];
     59     if (true === compression) self[type] = compression = {};
     60     if (compression && null == compression.threshold) {
     61       compression.threshold = 1024;
     62     }
     63   });
     64 
     65   this.init();
     66 }
     67 
     68 /**
     69  * Protocol errors mappings.
     70  */
     71 
     72 Server.errors = {
     73   UNKNOWN_TRANSPORT: 0,
     74   UNKNOWN_SID: 1,
     75   BAD_HANDSHAKE_METHOD: 2,
     76   BAD_REQUEST: 3,
     77   FORBIDDEN: 4
     78 };
     79 
     80 Server.errorMessages = {
     81   0: 'Transport unknown',
     82   1: 'Session ID unknown',
     83   2: 'Bad handshake method',
     84   3: 'Bad request',
     85   4: 'Forbidden'
     86 };
     87 
     88 /**
     89  * Inherits from EventEmitter.
     90  */
     91 
     92 util.inherits(Server, EventEmitter);
     93 
     94 /**
     95  * Initialize websocket server
     96  *
     97  * @api private
     98  */
     99 
    100 Server.prototype.init = function () {
    101   if (!~this.transports.indexOf('websocket')) return;
    102 
    103   if (this.ws) this.ws.close();
    104 
    105   var wsModule;
    106   switch (this.wsEngine) {
    107     case 'uws': wsModule = require('uws'); break;
    108     case 'ws': wsModule = require('ws'); break;
    109     default: throw new Error('unknown wsEngine');
    110   }
    111   this.ws = new wsModule.Server({
    112     noServer: true,
    113     clientTracking: false,
    114     perMessageDeflate: this.perMessageDeflate,
    115     maxPayload: this.maxHttpBufferSize
    116   });
    117 };
    118 
    119 /**
    120  * Returns a list of available transports for upgrade given a certain transport.
    121  *
    122  * @return {Array}
    123  * @api public
    124  */
    125 
    126 Server.prototype.upgrades = function (transport) {
    127   if (!this.allowUpgrades) return [];
    128   return transports[transport].upgradesTo || [];
    129 };
    130 
    131 /**
    132  * Verifies a request.
    133  *
    134  * @param {http.IncomingMessage}
    135  * @return {Boolean} whether the request is valid
    136  * @api private
    137  */
    138 
    139 Server.prototype.verify = function (req, upgrade, fn) {
    140   // transport check
    141   var transport = req._query.transport;
    142   if (!~this.transports.indexOf(transport)) {
    143     debug('unknown transport "%s"', transport);
    144     return fn(Server.errors.UNKNOWN_TRANSPORT, false);
    145   }
    146 
    147   // 'Origin' header check
    148   var isOriginInvalid = checkInvalidHeaderChar(req.headers.origin);
    149   if (isOriginInvalid) {
    150     req.headers.origin = null;
    151     debug('origin header invalid');
    152     return fn(Server.errors.BAD_REQUEST, false);
    153   }
    154 
    155   // sid check
    156   var sid = req._query.sid;
    157   if (sid) {
    158     if (!this.clients.hasOwnProperty(sid)) {
    159       debug('unknown sid "%s"', sid);
    160       return fn(Server.errors.UNKNOWN_SID, false);
    161     }
    162     if (!upgrade && this.clients[sid].transport.name !== transport) {
    163       debug('bad request: unexpected transport without upgrade');
    164       return fn(Server.errors.BAD_REQUEST, false);
    165     }
    166   } else {
    167     // handshake is GET only
    168     if ('GET' !== req.method) return fn(Server.errors.BAD_HANDSHAKE_METHOD, false);
    169     if (!this.allowRequest) return fn(null, true);
    170     return this.allowRequest(req, fn);
    171   }
    172 
    173   fn(null, true);
    174 };
    175 
    176 /**
    177  * Prepares a request by processing the query string.
    178  *
    179  * @api private
    180  */
    181 
    182 Server.prototype.prepare = function (req) {
    183   // try to leverage pre-existing `req._query` (e.g: from connect)
    184   if (!req._query) {
    185     req._query = ~req.url.indexOf('?') ? qs.parse(parse(req.url).query) : {};
    186   }
    187 };
    188 
    189 /**
    190  * Closes all clients.
    191  *
    192  * @api public
    193  */
    194 
    195 Server.prototype.close = function () {
    196   debug('closing all open clients');
    197   for (var i in this.clients) {
    198     if (this.clients.hasOwnProperty(i)) {
    199       this.clients[i].close(true);
    200     }
    201   }
    202   if (this.ws) {
    203     debug('closing webSocketServer');
    204     this.ws.close();
    205     // don't delete this.ws because it can be used again if the http server starts listening again
    206   }
    207   return this;
    208 };
    209 
    210 /**
    211  * Handles an Engine.IO HTTP request.
    212  *
    213  * @param {http.IncomingMessage} request
    214  * @param {http.ServerResponse|http.OutgoingMessage} response
    215  * @api public
    216  */
    217 
    218 Server.prototype.handleRequest = function (req, res) {
    219   debug('handling "%s" http request "%s"', req.method, req.url);
    220   this.prepare(req);
    221   req.res = res;
    222 
    223   var self = this;
    224   this.verify(req, false, function (err, success) {
    225     if (!success) {
    226       sendErrorMessage(req, res, err);
    227       return;
    228     }
    229 
    230     if (req._query.sid) {
    231       debug('setting new request for existing client');
    232       self.clients[req._query.sid].transport.onRequest(req);
    233     } else {
    234       self.handshake(req._query.transport, req);
    235     }
    236   });
    237 };
    238 
    239 /**
    240  * Sends an Engine.IO Error Message
    241  *
    242  * @param {http.ServerResponse} response
    243  * @param {code} error code
    244  * @api private
    245  */
    246 
    247 function sendErrorMessage (req, res, code) {
    248   var headers = { 'Content-Type': 'application/json' };
    249 
    250   var isForbidden = !Server.errorMessages.hasOwnProperty(code);
    251   if (isForbidden) {
    252     res.writeHead(403, headers);
    253     res.end(JSON.stringify({
    254       code: Server.errors.FORBIDDEN,
    255       message: code || Server.errorMessages[Server.errors.FORBIDDEN]
    256     }));
    257     return;
    258   }
    259   if (req.headers.origin) {
    260     headers['Access-Control-Allow-Credentials'] = 'true';
    261     headers['Access-Control-Allow-Origin'] = req.headers.origin;
    262   } else {
    263     headers['Access-Control-Allow-Origin'] = '*';
    264   }
    265   if (res !== undefined) {
    266     res.writeHead(400, headers);
    267     res.end(JSON.stringify({
    268       code: code,
    269       message: Server.errorMessages[code]
    270     }));
    271   }
    272 }
    273 
    274 /**
    275  * generate a socket id.
    276  * Overwrite this method to generate your custom socket id
    277  *
    278  * @param {Object} request object
    279  * @api public
    280  */
    281 
    282 Server.prototype.generateId = function (req) {
    283   return base64id.generateId();
    284 };
    285 
    286 /**
    287  * Handshakes a new client.
    288  *
    289  * @param {String} transport name
    290  * @param {Object} request object
    291  * @api private
    292  */
    293 
    294 Server.prototype.handshake = function (transportName, req) {
    295   var id = this.generateId(req);
    296 
    297   debug('handshaking client "%s"', id);
    298 
    299   try {
    300     var transport = new transports[transportName](req);
    301     if ('polling' === transportName) {
    302       transport.maxHttpBufferSize = this.maxHttpBufferSize;
    303       transport.httpCompression = this.httpCompression;
    304     } else if ('websocket' === transportName) {
    305       transport.perMessageDeflate = this.perMessageDeflate;
    306     }
    307 
    308     if (req._query && req._query.b64) {
    309       transport.supportsBinary = false;
    310     } else {
    311       transport.supportsBinary = true;
    312     }
    313   } catch (e) {
    314     debug('error handshaking to transport "%s"', transportName);
    315     sendErrorMessage(req, req.res, Server.errors.BAD_REQUEST);
    316     return;
    317   }
    318   var socket = new Socket(id, this, transport, req);
    319   var self = this;
    320 
    321   if (false !== this.cookie) {
    322     transport.on('headers', function (headers) {
    323       headers['Set-Cookie'] = cookieMod.serialize(self.cookie, id,
    324         {
    325           path: self.cookiePath,
    326           httpOnly: self.cookiePath ? self.cookieHttpOnly : false,
    327           sameSite: true
    328         });
    329     });
    330   }
    331 
    332   transport.onRequest(req);
    333 
    334   this.clients[id] = socket;
    335   this.clientsCount++;
    336 
    337   socket.once('close', function () {
    338     delete self.clients[id];
    339     self.clientsCount--;
    340   });
    341 
    342   this.emit('connection', socket);
    343 };
    344 
    345 /**
    346  * Handles an Engine.IO HTTP Upgrade.
    347  *
    348  * @api public
    349  */
    350 
    351 Server.prototype.handleUpgrade = function (req, socket, upgradeHead) {
    352   this.prepare(req);
    353 
    354   var self = this;
    355   this.verify(req, true, function (err, success) {
    356     if (!success) {
    357       abortConnection(socket, err);
    358       return;
    359     }
    360 
    361     var head = Buffer.from(upgradeHead); // eslint-disable-line node/no-deprecated-api
    362     upgradeHead = null;
    363 
    364     // delegate to ws
    365     self.ws.handleUpgrade(req, socket, head, function (conn) {
    366       self.onWebSocket(req, conn);
    367     });
    368   });
    369 };
    370 
    371 /**
    372  * Called upon a ws.io connection.
    373  *
    374  * @param {ws.Socket} websocket
    375  * @api private
    376  */
    377 
    378 Server.prototype.onWebSocket = function (req, socket) {
    379   socket.on('error', onUpgradeError);
    380 
    381   if (transports[req._query.transport] !== undefined && !transports[req._query.transport].prototype.handlesUpgrades) {
    382     debug('transport doesnt handle upgraded requests');
    383     socket.close();
    384     return;
    385   }
    386 
    387   // get client id
    388   var id = req._query.sid;
    389 
    390   // keep a reference to the ws.Socket
    391   req.websocket = socket;
    392 
    393   if (id) {
    394     var client = this.clients[id];
    395     if (!client) {
    396       debug('upgrade attempt for closed client');
    397       socket.close();
    398     } else if (client.upgrading) {
    399       debug('transport has already been trying to upgrade');
    400       socket.close();
    401     } else if (client.upgraded) {
    402       debug('transport had already been upgraded');
    403       socket.close();
    404     } else {
    405       debug('upgrading existing transport');
    406 
    407       // transport error handling takes over
    408       socket.removeListener('error', onUpgradeError);
    409 
    410       var transport = new transports[req._query.transport](req);
    411       if (req._query && req._query.b64) {
    412         transport.supportsBinary = false;
    413       } else {
    414         transport.supportsBinary = true;
    415       }
    416       transport.perMessageDeflate = this.perMessageDeflate;
    417       client.maybeUpgrade(transport);
    418     }
    419   } else {
    420     // transport error handling takes over
    421     socket.removeListener('error', onUpgradeError);
    422 
    423     this.handshake(req._query.transport, req);
    424   }
    425 
    426   function onUpgradeError () {
    427     debug('websocket error before upgrade');
    428     // socket.close() not needed
    429   }
    430 };
    431 
    432 /**
    433  * Captures upgrade requests for a http.Server.
    434  *
    435  * @param {http.Server} server
    436  * @param {Object} options
    437  * @api public
    438  */
    439 
    440 Server.prototype.attach = function (server, options) {
    441   var self = this;
    442   options = options || {};
    443   var path = (options.path || '/engine.io').replace(/\/$/, '');
    444 
    445   var destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000;
    446 
    447   // normalize path
    448   path += '/';
    449 
    450   function check (req) {
    451     if ('OPTIONS' === req.method && false === options.handlePreflightRequest) {
    452       return false;
    453     }
    454     return path === req.url.substr(0, path.length);
    455   }
    456 
    457   // cache and clean up listeners
    458   var listeners = server.listeners('request').slice(0);
    459   server.removeAllListeners('request');
    460   server.on('close', self.close.bind(self));
    461   server.on('listening', self.init.bind(self));
    462 
    463   // add request handler
    464   server.on('request', function (req, res) {
    465     if (check(req)) {
    466       debug('intercepting request for path "%s"', path);
    467       if ('OPTIONS' === req.method && 'function' === typeof options.handlePreflightRequest) {
    468         options.handlePreflightRequest.call(server, req, res);
    469       } else {
    470         self.handleRequest(req, res);
    471       }
    472     } else {
    473       for (var i = 0, l = listeners.length; i < l; i++) {
    474         listeners[i].call(server, req, res);
    475       }
    476     }
    477   });
    478 
    479   if (~self.transports.indexOf('websocket')) {
    480     server.on('upgrade', function (req, socket, head) {
    481       if (check(req)) {
    482         self.handleUpgrade(req, socket, head);
    483       } else if (false !== options.destroyUpgrade) {
    484         // default node behavior is to disconnect when no handlers
    485         // but by adding a handler, we prevent that
    486         // and if no eio thing handles the upgrade
    487         // then the socket needs to die!
    488         setTimeout(function () {
    489           if (socket.writable && socket.bytesWritten <= 0) {
    490             return socket.end();
    491           }
    492         }, destroyUpgradeTimeout);
    493       }
    494     });
    495   }
    496 };
    497 
    498 /**
    499  * Closes the connection
    500  *
    501  * @param {net.Socket} socket
    502  * @param {code} error code
    503  * @api private
    504  */
    505 
    506 function abortConnection (socket, code) {
    507   socket.on('error', () => {
    508     debug('ignoring error from closed connection');
    509   });
    510   if (socket.writable) {
    511     var message = Server.errorMessages.hasOwnProperty(code) ? Server.errorMessages[code] : String(code || '');
    512     var length = Buffer.byteLength(message);
    513     socket.write(
    514       'HTTP/1.1 400 Bad Request\r\n' +
    515       'Connection: close\r\n' +
    516       'Content-type: text/html\r\n' +
    517       'Content-Length: ' + length + '\r\n' +
    518       '\r\n' +
    519       message
    520     );
    521   }
    522   socket.destroy();
    523 }
    524 
    525 /* eslint-disable */
    526 
    527 /**
    528  * From https://github.com/nodejs/node/blob/v8.4.0/lib/_http_common.js#L303-L354
    529  *
    530  * True if val contains an invalid field-vchar
    531  *  field-value    = *( field-content / obs-fold )
    532  *  field-content  = field-vchar [ 1*( SP / HTAB ) field-vchar ]
    533  *  field-vchar    = VCHAR / obs-text
    534  *
    535  * checkInvalidHeaderChar() is currently designed to be inlinable by v8,
    536  * so take care when making changes to the implementation so that the source
    537  * code size does not exceed v8's default max_inlined_source_size setting.
    538  **/
    539 var validHdrChars = [
    540   0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, // 0 - 15
    541   0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
    542   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 32 - 47
    543   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48 - 63
    544   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
    545   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 80 - 95
    546   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
    547   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 112 - 127
    548   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 128 ...
    549   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    550   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    551   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    552   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    553   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    554   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    555   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1  // ... 255
    556 ];
    557 
    558 function checkInvalidHeaderChar(val) {
    559   val += '';
    560   if (val.length < 1)
    561     return false;
    562   if (!validHdrChars[val.charCodeAt(0)]) {
    563     debug('invalid header, index 0, char "%s"', val.charCodeAt(0));
    564     return true;
    565   }
    566   if (val.length < 2)
    567     return false;
    568   if (!validHdrChars[val.charCodeAt(1)]) {
    569     debug('invalid header, index 1, char "%s"', val.charCodeAt(1));
    570     return true;
    571   }
    572   if (val.length < 3)
    573     return false;
    574   if (!validHdrChars[val.charCodeAt(2)]) {
    575     debug('invalid header, index 2, char "%s"', val.charCodeAt(2));
    576     return true;
    577   }
    578   if (val.length < 4)
    579     return false;
    580   if (!validHdrChars[val.charCodeAt(3)]) {
    581     debug('invalid header, index 3, char "%s"', val.charCodeAt(3));
    582     return true;
    583   }
    584   for (var i = 4; i < val.length; ++i) {
    585     if (!validHdrChars[val.charCodeAt(i)]) {
    586       debug('invalid header, index "%i", char "%s"', i, val.charCodeAt(i));
    587       return true;
    588     }
    589   }
    590   return false;
    591 }