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 };