You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

408 lines
8.1 KiB

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