polling.js 9.8 KB

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