polling.js 11 KB

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