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