twitst4tz

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

polling.js (8313B)


      1 
      2 /**
      3  * Module requirements.
      4  */
      5 
      6 var Transport = require('../transport');
      7 var parser = require('engine.io-parser');
      8 var zlib = require('zlib');
      9 var accepts = require('accepts');
     10 var util = require('util');
     11 var debug = require('debug')('engine:polling');
     12 
     13 var compressionMethods = {
     14   gzip: zlib.createGzip,
     15   deflate: zlib.createDeflate
     16 };
     17 
     18 /**
     19  * Exports the constructor.
     20  */
     21 
     22 module.exports = Polling;
     23 
     24 /**
     25  * HTTP polling constructor.
     26  *
     27  * @api public.
     28  */
     29 
     30 function Polling (req) {
     31   Transport.call(this, req);
     32 
     33   this.closeTimeout = 30 * 1000;
     34   this.maxHttpBufferSize = null;
     35   this.httpCompression = null;
     36 }
     37 
     38 /**
     39  * Inherits from Transport.
     40  *
     41  * @api public.
     42  */
     43 
     44 util.inherits(Polling, Transport);
     45 
     46 /**
     47  * Transport name
     48  *
     49  * @api public
     50  */
     51 
     52 Polling.prototype.name = 'polling';
     53 
     54 /**
     55  * Overrides onRequest.
     56  *
     57  * @param {http.IncomingMessage}
     58  * @api private
     59  */
     60 
     61 Polling.prototype.onRequest = function (req) {
     62   var res = req.res;
     63 
     64   if ('GET' === req.method) {
     65     this.onPollRequest(req, res);
     66   } else if ('POST' === req.method) {
     67     this.onDataRequest(req, res);
     68   } else {
     69     res.writeHead(500);
     70     res.end();
     71   }
     72 };
     73 
     74 /**
     75  * The client sends a request awaiting for us to send data.
     76  *
     77  * @api private
     78  */
     79 
     80 Polling.prototype.onPollRequest = function (req, res) {
     81   if (this.req) {
     82     debug('request overlap');
     83     // assert: this.res, '.req and .res should be (un)set together'
     84     this.onError('overlap from client');
     85     res.writeHead(500);
     86     res.end();
     87     return;
     88   }
     89 
     90   debug('setting request');
     91 
     92   this.req = req;
     93   this.res = res;
     94 
     95   var self = this;
     96 
     97   function onClose () {
     98     self.onError('poll connection closed prematurely');
     99   }
    100 
    101   function cleanup () {
    102     req.removeListener('close', onClose);
    103     self.req = self.res = null;
    104   }
    105 
    106   req.cleanup = cleanup;
    107   req.on('close', onClose);
    108 
    109   this.writable = true;
    110   this.emit('drain');
    111 
    112   // if we're still writable but had a pending close, trigger an empty send
    113   if (this.writable && this.shouldClose) {
    114     debug('triggering empty send to append close packet');
    115     this.send([{ type: 'noop' }]);
    116   }
    117 };
    118 
    119 /**
    120  * The client sends a request with data.
    121  *
    122  * @api private
    123  */
    124 
    125 Polling.prototype.onDataRequest = function (req, res) {
    126   if (this.dataReq) {
    127     // assert: this.dataRes, '.dataReq and .dataRes should be (un)set together'
    128     this.onError('data request overlap from client');
    129     res.writeHead(500);
    130     res.end();
    131     return;
    132   }
    133 
    134   var isBinary = 'application/octet-stream' === req.headers['content-type'];
    135 
    136   this.dataReq = req;
    137   this.dataRes = res;
    138 
    139   var chunks = isBinary ? Buffer.concat([]) : '';
    140   var self = this;
    141 
    142   function cleanup () {
    143     req.removeListener('data', onData);
    144     req.removeListener('end', onEnd);
    145     req.removeListener('close', onClose);
    146     self.dataReq = self.dataRes = chunks = null;
    147   }
    148 
    149   function onClose () {
    150     cleanup();
    151     self.onError('data request connection closed prematurely');
    152   }
    153 
    154   function onData (data) {
    155     var contentLength;
    156     if (isBinary) {
    157       chunks = Buffer.concat([chunks, data]);
    158       contentLength = chunks.length;
    159     } else {
    160       chunks += data;
    161       contentLength = Buffer.byteLength(chunks);
    162     }
    163 
    164     if (contentLength > self.maxHttpBufferSize) {
    165       chunks = isBinary ? Buffer.concat([]) : '';
    166       req.connection.destroy();
    167     }
    168   }
    169 
    170   function onEnd () {
    171     self.onData(chunks);
    172 
    173     var headers = {
    174       // text/html is required instead of text/plain to avoid an
    175       // unwanted download dialog on certain user-agents (GH-43)
    176       'Content-Type': 'text/html',
    177       'Content-Length': 2
    178     };
    179 
    180     res.writeHead(200, self.headers(req, headers));
    181     res.end('ok');
    182     cleanup();
    183   }
    184 
    185   req.on('close', onClose);
    186   if (!isBinary) req.setEncoding('utf8');
    187   req.on('data', onData);
    188   req.on('end', onEnd);
    189 };
    190 
    191 /**
    192  * Processes the incoming data payload.
    193  *
    194  * @param {String} encoded payload
    195  * @api private
    196  */
    197 
    198 Polling.prototype.onData = function (data) {
    199   debug('received "%s"', data);
    200   var self = this;
    201   var callback = function (packet) {
    202     if ('close' === packet.type) {
    203       debug('got xhr close packet');
    204       self.onClose();
    205       return false;
    206     }
    207 
    208     self.onPacket(packet);
    209   };
    210 
    211   parser.decodePayload(data, callback);
    212 };
    213 
    214 /**
    215  * Overrides onClose.
    216  *
    217  * @api private
    218  */
    219 
    220 Polling.prototype.onClose = function () {
    221   if (this.writable) {
    222     // close pending poll request
    223     this.send([{ type: 'noop' }]);
    224   }
    225   Transport.prototype.onClose.call(this);
    226 };
    227 
    228 /**
    229  * Writes a packet payload.
    230  *
    231  * @param {Object} packet
    232  * @api private
    233  */
    234 
    235 Polling.prototype.send = function (packets) {
    236   this.writable = false;
    237 
    238   if (this.shouldClose) {
    239     debug('appending close packet to payload');
    240     packets.push({ type: 'close' });
    241     this.shouldClose();
    242     this.shouldClose = null;
    243   }
    244 
    245   var self = this;
    246   parser.encodePayload(packets, this.supportsBinary, function (data) {
    247     var compress = packets.some(function (packet) {
    248       return packet.options && packet.options.compress;
    249     });
    250     self.write(data, { compress: compress });
    251   });
    252 };
    253 
    254 /**
    255  * Writes data as response to poll request.
    256  *
    257  * @param {String} data
    258  * @param {Object} options
    259  * @api private
    260  */
    261 
    262 Polling.prototype.write = function (data, options) {
    263   debug('writing "%s"', data);
    264   var self = this;
    265   this.doWrite(data, options, function () {
    266     self.req.cleanup();
    267   });
    268 };
    269 
    270 /**
    271  * Performs the write.
    272  *
    273  * @api private
    274  */
    275 
    276 Polling.prototype.doWrite = function (data, options, callback) {
    277   var self = this;
    278 
    279   // explicit UTF-8 is required for pages not served under utf
    280   var isString = typeof data === 'string';
    281   var contentType = isString
    282     ? 'text/plain; charset=UTF-8'
    283     : 'application/octet-stream';
    284 
    285   var headers = {
    286     'Content-Type': contentType
    287   };
    288 
    289   if (!this.httpCompression || !options.compress) {
    290     respond(data);
    291     return;
    292   }
    293 
    294   var len = isString ? Buffer.byteLength(data) : data.length;
    295   if (len < this.httpCompression.threshold) {
    296     respond(data);
    297     return;
    298   }
    299 
    300   var encoding = accepts(this.req).encodings(['gzip', 'deflate']);
    301   if (!encoding) {
    302     respond(data);
    303     return;
    304   }
    305 
    306   this.compress(data, encoding, function (err, data) {
    307     if (err) {
    308       self.res.writeHead(500);
    309       self.res.end();
    310       callback(err);
    311       return;
    312     }
    313 
    314     headers['Content-Encoding'] = encoding;
    315     respond(data);
    316   });
    317 
    318   function respond (data) {
    319     headers['Content-Length'] = 'string' === typeof data ? Buffer.byteLength(data) : data.length;
    320     self.res.writeHead(200, self.headers(self.req, headers));
    321     self.res.end(data);
    322     callback();
    323   }
    324 };
    325 
    326 /**
    327  * Compresses data.
    328  *
    329  * @api private
    330  */
    331 
    332 Polling.prototype.compress = function (data, encoding, callback) {
    333   debug('compressing');
    334 
    335   var buffers = [];
    336   var nread = 0;
    337 
    338   compressionMethods[encoding](this.httpCompression)
    339     .on('error', callback)
    340     .on('data', function (chunk) {
    341       buffers.push(chunk);
    342       nread += chunk.length;
    343     })
    344     .on('end', function () {
    345       callback(null, Buffer.concat(buffers, nread));
    346     })
    347     .end(data);
    348 };
    349 
    350 /**
    351  * Closes the transport.
    352  *
    353  * @api private
    354  */
    355 
    356 Polling.prototype.doClose = function (fn) {
    357   debug('closing');
    358 
    359   var self = this;
    360   var closeTimeoutTimer;
    361 
    362   if (this.dataReq) {
    363     debug('aborting ongoing data request');
    364     this.dataReq.destroy();
    365   }
    366 
    367   if (this.writable) {
    368     debug('transport writable - closing right away');
    369     this.send([{ type: 'close' }]);
    370     onClose();
    371   } else if (this.discarded) {
    372     debug('transport discarded - closing right away');
    373     onClose();
    374   } else {
    375     debug('transport not writable - buffering orderly close');
    376     this.shouldClose = onClose;
    377     closeTimeoutTimer = setTimeout(onClose, this.closeTimeout);
    378   }
    379 
    380   function onClose () {
    381     clearTimeout(closeTimeoutTimer);
    382     fn();
    383     self.onClose();
    384   }
    385 };
    386 
    387 /**
    388  * Returns headers for a response.
    389  *
    390  * @param {http.IncomingMessage} request
    391  * @param {Object} extra headers
    392  * @api private
    393  */
    394 
    395 Polling.prototype.headers = function (req, headers) {
    396   headers = headers || {};
    397 
    398   // prevent XSS warnings on IE
    399   // https://github.com/LearnBoost/socket.io/pull/1333
    400   var ua = req.headers['user-agent'];
    401   if (ua && (~ua.indexOf(';MSIE') || ~ua.indexOf('Trident/'))) {
    402     headers['X-XSS-Protection'] = '0';
    403   }
    404 
    405   this.emit('headers', headers);
    406   return headers;
    407 };