polling.js 9.8 KB

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