polling.js 9.5 KB

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