[e29cc2e] | 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;
|
---|