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 isBinary = "application/octet-stream" === req.headers["content-type"];
|
---|
102 | if (isBinary && this.protocol === 4) {
|
---|
103 | return this.onError("invalid content");
|
---|
104 | }
|
---|
105 | this.dataReq = req;
|
---|
106 | this.dataRes = res;
|
---|
107 | let chunks = [];
|
---|
108 | let contentLength = 0;
|
---|
109 | const cleanup = () => {
|
---|
110 | this.dataReq = this.dataRes = chunks = null;
|
---|
111 | };
|
---|
112 | const onClose = () => {
|
---|
113 | cleanup();
|
---|
114 | this.onError("data request connection closed prematurely");
|
---|
115 | };
|
---|
116 | const headers = {
|
---|
117 | // text/html is required instead of text/plain to avoid an
|
---|
118 | // unwanted download dialog on certain user-agents (GH-43)
|
---|
119 | "Content-Type": "text/html"
|
---|
120 | };
|
---|
121 | this.headers(req, headers);
|
---|
122 | Object.keys(headers).forEach(key => {
|
---|
123 | res.writeHeader(key, String(headers[key]));
|
---|
124 | });
|
---|
125 | const onEnd = () => {
|
---|
126 | this.onData(Buffer.concat(chunks).toString());
|
---|
127 | if (this.readyState !== "closing") {
|
---|
128 | res.end("ok");
|
---|
129 | }
|
---|
130 | cleanup();
|
---|
131 | };
|
---|
132 | res.onAborted(onClose);
|
---|
133 | res.onData((chunk, isLast) => {
|
---|
134 | chunks.push(Buffer.from(chunk));
|
---|
135 | contentLength += Buffer.byteLength(chunk);
|
---|
136 | if (contentLength > this.maxHttpBufferSize) {
|
---|
137 | this.onError("payload too large");
|
---|
138 | res.writeStatus("413 Payload Too Large");
|
---|
139 | res.end();
|
---|
140 | return;
|
---|
141 | }
|
---|
142 | if (isLast) {
|
---|
143 | onEnd();
|
---|
144 | }
|
---|
145 | });
|
---|
146 | }
|
---|
147 | /**
|
---|
148 | * Processes the incoming data payload.
|
---|
149 | *
|
---|
150 | * @param {String} encoded payload
|
---|
151 | * @api private
|
---|
152 | */
|
---|
153 | onData(data) {
|
---|
154 | debug('received "%s"', data);
|
---|
155 | const callback = packet => {
|
---|
156 | if ("close" === packet.type) {
|
---|
157 | debug("got xhr close packet");
|
---|
158 | this.onClose();
|
---|
159 | return false;
|
---|
160 | }
|
---|
161 | this.onPacket(packet);
|
---|
162 | };
|
---|
163 | if (this.protocol === 3) {
|
---|
164 | this.parser.decodePayload(data, callback);
|
---|
165 | }
|
---|
166 | else {
|
---|
167 | this.parser.decodePayload(data).forEach(callback);
|
---|
168 | }
|
---|
169 | }
|
---|
170 | /**
|
---|
171 | * Overrides onClose.
|
---|
172 | *
|
---|
173 | * @api private
|
---|
174 | */
|
---|
175 | onClose() {
|
---|
176 | if (this.writable) {
|
---|
177 | // close pending poll request
|
---|
178 | this.send([{ type: "noop" }]);
|
---|
179 | }
|
---|
180 | super.onClose();
|
---|
181 | }
|
---|
182 | /**
|
---|
183 | * Writes a packet payload.
|
---|
184 | *
|
---|
185 | * @param {Object} packet
|
---|
186 | * @api private
|
---|
187 | */
|
---|
188 | send(packets) {
|
---|
189 | this.writable = false;
|
---|
190 | if (this.shouldClose) {
|
---|
191 | debug("appending close packet to payload");
|
---|
192 | packets.push({ type: "close" });
|
---|
193 | this.shouldClose();
|
---|
194 | this.shouldClose = null;
|
---|
195 | }
|
---|
196 | const doWrite = data => {
|
---|
197 | const compress = packets.some(packet => {
|
---|
198 | return packet.options && packet.options.compress;
|
---|
199 | });
|
---|
200 | this.write(data, { compress });
|
---|
201 | };
|
---|
202 | if (this.protocol === 3) {
|
---|
203 | this.parser.encodePayload(packets, this.supportsBinary, doWrite);
|
---|
204 | }
|
---|
205 | else {
|
---|
206 | this.parser.encodePayload(packets, doWrite);
|
---|
207 | }
|
---|
208 | }
|
---|
209 | /**
|
---|
210 | * Writes data as response to poll request.
|
---|
211 | *
|
---|
212 | * @param {String} data
|
---|
213 | * @param {Object} options
|
---|
214 | * @api private
|
---|
215 | */
|
---|
216 | write(data, options) {
|
---|
217 | debug('writing "%s"', data);
|
---|
218 | this.doWrite(data, options, () => {
|
---|
219 | this.req.cleanup();
|
---|
220 | });
|
---|
221 | }
|
---|
222 | /**
|
---|
223 | * Performs the write.
|
---|
224 | *
|
---|
225 | * @api private
|
---|
226 | */
|
---|
227 | doWrite(data, options, callback) {
|
---|
228 | // explicit UTF-8 is required for pages not served under utf
|
---|
229 | const isString = typeof data === "string";
|
---|
230 | const contentType = isString
|
---|
231 | ? "text/plain; charset=UTF-8"
|
---|
232 | : "application/octet-stream";
|
---|
233 | const headers = {
|
---|
234 | "Content-Type": contentType
|
---|
235 | };
|
---|
236 | const respond = data => {
|
---|
237 | this.headers(this.req, headers);
|
---|
238 | Object.keys(headers).forEach(key => {
|
---|
239 | this.res.writeHeader(key, String(headers[key]));
|
---|
240 | });
|
---|
241 | this.res.end(data);
|
---|
242 | callback();
|
---|
243 | };
|
---|
244 | if (!this.httpCompression || !options.compress) {
|
---|
245 | respond(data);
|
---|
246 | return;
|
---|
247 | }
|
---|
248 | const len = isString ? Buffer.byteLength(data) : data.length;
|
---|
249 | if (len < this.httpCompression.threshold) {
|
---|
250 | respond(data);
|
---|
251 | return;
|
---|
252 | }
|
---|
253 | const encoding = accepts(this.req).encodings(["gzip", "deflate"]);
|
---|
254 | if (!encoding) {
|
---|
255 | respond(data);
|
---|
256 | return;
|
---|
257 | }
|
---|
258 | this.compress(data, encoding, (err, data) => {
|
---|
259 | if (err) {
|
---|
260 | this.res.writeStatus("500 Internal Server Error");
|
---|
261 | this.res.end();
|
---|
262 | callback(err);
|
---|
263 | return;
|
---|
264 | }
|
---|
265 | headers["Content-Encoding"] = encoding;
|
---|
266 | respond(data);
|
---|
267 | });
|
---|
268 | }
|
---|
269 | /**
|
---|
270 | * Compresses data.
|
---|
271 | *
|
---|
272 | * @api private
|
---|
273 | */
|
---|
274 | compress(data, encoding, callback) {
|
---|
275 | debug("compressing");
|
---|
276 | const buffers = [];
|
---|
277 | let nread = 0;
|
---|
278 | compressionMethods[encoding](this.httpCompression)
|
---|
279 | .on("error", callback)
|
---|
280 | .on("data", function (chunk) {
|
---|
281 | buffers.push(chunk);
|
---|
282 | nread += chunk.length;
|
---|
283 | })
|
---|
284 | .on("end", function () {
|
---|
285 | callback(null, Buffer.concat(buffers, nread));
|
---|
286 | })
|
---|
287 | .end(data);
|
---|
288 | }
|
---|
289 | /**
|
---|
290 | * Closes the transport.
|
---|
291 | *
|
---|
292 | * @api private
|
---|
293 | */
|
---|
294 | doClose(fn) {
|
---|
295 | debug("closing");
|
---|
296 | let closeTimeoutTimer;
|
---|
297 | const onClose = () => {
|
---|
298 | clearTimeout(closeTimeoutTimer);
|
---|
299 | fn();
|
---|
300 | this.onClose();
|
---|
301 | };
|
---|
302 | if (this.writable) {
|
---|
303 | debug("transport writable - closing right away");
|
---|
304 | this.send([{ type: "close" }]);
|
---|
305 | onClose();
|
---|
306 | }
|
---|
307 | else if (this.discarded) {
|
---|
308 | debug("transport discarded - closing right away");
|
---|
309 | onClose();
|
---|
310 | }
|
---|
311 | else {
|
---|
312 | debug("transport not writable - buffering orderly close");
|
---|
313 | this.shouldClose = onClose;
|
---|
314 | closeTimeoutTimer = setTimeout(onClose, this.closeTimeout);
|
---|
315 | }
|
---|
316 | }
|
---|
317 | /**
|
---|
318 | * Returns headers for a response.
|
---|
319 | *
|
---|
320 | * @param req - request
|
---|
321 | * @param {Object} extra headers
|
---|
322 | * @api private
|
---|
323 | */
|
---|
324 | headers(req, headers) {
|
---|
325 | headers = headers || {};
|
---|
326 | // prevent XSS warnings on IE
|
---|
327 | // https://github.com/LearnBoost/socket.io/pull/1333
|
---|
328 | const ua = req.headers["user-agent"];
|
---|
329 | if (ua && (~ua.indexOf(";MSIE") || ~ua.indexOf("Trident/"))) {
|
---|
330 | headers["X-XSS-Protection"] = "0";
|
---|
331 | }
|
---|
332 | this.emit("headers", headers, req);
|
---|
333 | return headers;
|
---|
334 | }
|
---|
335 | }
|
---|
336 | exports.Polling = Polling;
|
---|