polling.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  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 expectedContentLength = Number(req.headers["content-length"]);
  102. if (!expectedContentLength) {
  103. this.onError("content-length header required");
  104. res.writeStatus("411 Length Required").end();
  105. return;
  106. }
  107. if (expectedContentLength > this.maxHttpBufferSize) {
  108. this.onError("payload too large");
  109. res.writeStatus("413 Payload Too Large").end();
  110. return;
  111. }
  112. const isBinary = "application/octet-stream" === req.headers["content-type"];
  113. if (isBinary && this.protocol === 4) {
  114. return this.onError("invalid content");
  115. }
  116. this.dataReq = req;
  117. this.dataRes = res;
  118. let buffer;
  119. let offset = 0;
  120. const headers = {
  121. // text/html is required instead of text/plain to avoid an
  122. // unwanted download dialog on certain user-agents (GH-43)
  123. "Content-Type": "text/html"
  124. };
  125. this.headers(req, headers);
  126. for (let key in headers) {
  127. res.writeHeader(key, String(headers[key]));
  128. }
  129. const onEnd = buffer => {
  130. this.onData(buffer.toString());
  131. this.onDataRequestCleanup();
  132. res.end("ok");
  133. };
  134. res.onAborted(() => {
  135. this.onDataRequestCleanup();
  136. this.onError("data request connection closed prematurely");
  137. });
  138. res.onData((arrayBuffer, isLast) => {
  139. const totalLength = offset + arrayBuffer.byteLength;
  140. if (totalLength > expectedContentLength) {
  141. this.onError("content-length mismatch");
  142. res.close(); // calls onAborted
  143. return;
  144. }
  145. if (!buffer) {
  146. if (isLast) {
  147. onEnd(Buffer.from(arrayBuffer));
  148. return;
  149. }
  150. buffer = Buffer.allocUnsafe(expectedContentLength);
  151. }
  152. Buffer.from(arrayBuffer).copy(buffer, offset);
  153. if (isLast) {
  154. if (totalLength != expectedContentLength) {
  155. this.onError("content-length mismatch");
  156. res.writeStatus("400 Content-Length Mismatch").end();
  157. this.onDataRequestCleanup();
  158. return;
  159. }
  160. onEnd(buffer);
  161. return;
  162. }
  163. offset = totalLength;
  164. });
  165. }
  166. /**
  167. * Cleanup request.
  168. *
  169. * @api private
  170. */
  171. onDataRequestCleanup() {
  172. this.dataReq = this.dataRes = null;
  173. }
  174. /**
  175. * Processes the incoming data payload.
  176. *
  177. * @param {String} encoded payload
  178. * @api private
  179. */
  180. onData(data) {
  181. debug('received "%s"', data);
  182. const callback = packet => {
  183. if ("close" === packet.type) {
  184. debug("got xhr close packet");
  185. this.onClose();
  186. return false;
  187. }
  188. this.onPacket(packet);
  189. };
  190. if (this.protocol === 3) {
  191. this.parser.decodePayload(data, callback);
  192. }
  193. else {
  194. this.parser.decodePayload(data).forEach(callback);
  195. }
  196. }
  197. /**
  198. * Overrides onClose.
  199. *
  200. * @api private
  201. */
  202. onClose() {
  203. if (this.writable) {
  204. // close pending poll request
  205. this.send([{ type: "noop" }]);
  206. }
  207. super.onClose();
  208. }
  209. /**
  210. * Writes a packet payload.
  211. *
  212. * @param {Object} packet
  213. * @api private
  214. */
  215. send(packets) {
  216. this.writable = false;
  217. if (this.shouldClose) {
  218. debug("appending close packet to payload");
  219. packets.push({ type: "close" });
  220. this.shouldClose();
  221. this.shouldClose = null;
  222. }
  223. const doWrite = data => {
  224. const compress = packets.some(packet => {
  225. return packet.options && packet.options.compress;
  226. });
  227. this.write(data, { compress });
  228. };
  229. if (this.protocol === 3) {
  230. this.parser.encodePayload(packets, this.supportsBinary, doWrite);
  231. }
  232. else {
  233. this.parser.encodePayload(packets, doWrite);
  234. }
  235. }
  236. /**
  237. * Writes data as response to poll request.
  238. *
  239. * @param {String} data
  240. * @param {Object} options
  241. * @api private
  242. */
  243. write(data, options) {
  244. debug('writing "%s"', data);
  245. this.doWrite(data, options, () => {
  246. this.req.cleanup();
  247. });
  248. }
  249. /**
  250. * Performs the write.
  251. *
  252. * @api private
  253. */
  254. doWrite(data, options, callback) {
  255. // explicit UTF-8 is required for pages not served under utf
  256. const isString = typeof data === "string";
  257. const contentType = isString
  258. ? "text/plain; charset=UTF-8"
  259. : "application/octet-stream";
  260. const headers = {
  261. "Content-Type": contentType
  262. };
  263. const respond = data => {
  264. this.headers(this.req, headers);
  265. Object.keys(headers).forEach(key => {
  266. this.res.writeHeader(key, String(headers[key]));
  267. });
  268. this.res.end(data);
  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. * @api 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. * @api 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. * @api 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. this.emit("headers", headers, req);
  360. return headers;
  361. }
  362. }
  363. exports.Polling = Polling;