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