polling.js 11 KB

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