|
"use strict"; |
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
exports.Polling = void 0; |
|
const transport_1 = require("../transport"); |
|
const zlib_1 = require("zlib"); |
|
const accepts = require("accepts"); |
|
const debug_1 = require("debug"); |
|
const debug = (0, debug_1.default)("engine:polling"); |
|
const compressionMethods = { |
|
gzip: zlib_1.createGzip, |
|
deflate: zlib_1.createDeflate, |
|
}; |
|
class Polling extends transport_1.Transport { |
|
|
|
|
|
|
|
|
|
|
|
constructor(req) { |
|
super(req); |
|
this.closeTimeout = 30 * 1000; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
get name() { |
|
return "polling"; |
|
} |
|
get supportsFraming() { |
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onRequest(req) { |
|
const res = req.res; |
|
if (req.getMethod() === "get") { |
|
this.onPollRequest(req, res); |
|
} |
|
else if (req.getMethod() === "post") { |
|
this.onDataRequest(req, res); |
|
} |
|
else { |
|
res.writeStatus("500 Internal Server Error"); |
|
res.end(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
onPollRequest(req, res) { |
|
if (this.req) { |
|
debug("request overlap"); |
|
|
|
this.onError("overlap from client"); |
|
res.writeStatus("500 Internal Server Error"); |
|
res.end(); |
|
return; |
|
} |
|
debug("setting request"); |
|
this.req = req; |
|
this.res = res; |
|
const onClose = () => { |
|
this.writable = false; |
|
this.onError("poll connection closed prematurely"); |
|
}; |
|
const cleanup = () => { |
|
this.req = this.res = null; |
|
}; |
|
req.cleanup = cleanup; |
|
res.onAborted(onClose); |
|
this.writable = true; |
|
this.emit("drain"); |
|
|
|
if (this.writable && this.shouldClose) { |
|
debug("triggering empty send to append close packet"); |
|
this.send([{ type: "noop" }]); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
onDataRequest(req, res) { |
|
if (this.dataReq) { |
|
|
|
this.onError("data request overlap from client"); |
|
res.writeStatus("500 Internal Server Error"); |
|
res.end(); |
|
return; |
|
} |
|
const expectedContentLength = Number(req.headers["content-length"]); |
|
if (!expectedContentLength) { |
|
this.onError("content-length header required"); |
|
res.writeStatus("411 Length Required").end(); |
|
return; |
|
} |
|
if (expectedContentLength > this.maxHttpBufferSize) { |
|
this.onError("payload too large"); |
|
res.writeStatus("413 Payload Too Large").end(); |
|
return; |
|
} |
|
const isBinary = "application/octet-stream" === req.headers["content-type"]; |
|
if (isBinary && this.protocol === 4) { |
|
return this.onError("invalid content"); |
|
} |
|
this.dataReq = req; |
|
this.dataRes = res; |
|
let buffer; |
|
let offset = 0; |
|
const headers = { |
|
|
|
|
|
"Content-Type": "text/html", |
|
}; |
|
this.headers(req, headers); |
|
for (let key in headers) { |
|
res.writeHeader(key, String(headers[key])); |
|
} |
|
const onEnd = (buffer) => { |
|
this.onData(buffer.toString()); |
|
this.onDataRequestCleanup(); |
|
res.end("ok"); |
|
}; |
|
res.onAborted(() => { |
|
this.onDataRequestCleanup(); |
|
this.onError("data request connection closed prematurely"); |
|
}); |
|
res.onData((arrayBuffer, isLast) => { |
|
const totalLength = offset + arrayBuffer.byteLength; |
|
if (totalLength > expectedContentLength) { |
|
this.onError("content-length mismatch"); |
|
res.close(); |
|
return; |
|
} |
|
if (!buffer) { |
|
if (isLast) { |
|
onEnd(Buffer.from(arrayBuffer)); |
|
return; |
|
} |
|
buffer = Buffer.allocUnsafe(expectedContentLength); |
|
} |
|
Buffer.from(arrayBuffer).copy(buffer, offset); |
|
if (isLast) { |
|
if (totalLength != expectedContentLength) { |
|
this.onError("content-length mismatch"); |
|
res.writeStatus("400 Content-Length Mismatch").end(); |
|
this.onDataRequestCleanup(); |
|
return; |
|
} |
|
onEnd(buffer); |
|
return; |
|
} |
|
offset = totalLength; |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
onDataRequestCleanup() { |
|
this.dataReq = this.dataRes = null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
onData(data) { |
|
debug('received "%s"', data); |
|
const callback = (packet) => { |
|
if ("close" === packet.type) { |
|
debug("got xhr close packet"); |
|
this.onClose(); |
|
return false; |
|
} |
|
this.onPacket(packet); |
|
}; |
|
if (this.protocol === 3) { |
|
this.parser.decodePayload(data, callback); |
|
} |
|
else { |
|
this.parser.decodePayload(data).forEach(callback); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
onClose() { |
|
if (this.writable) { |
|
|
|
this.send([{ type: "noop" }]); |
|
} |
|
super.onClose(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
send(packets) { |
|
this.writable = false; |
|
if (this.shouldClose) { |
|
debug("appending close packet to payload"); |
|
packets.push({ type: "close" }); |
|
this.shouldClose(); |
|
this.shouldClose = null; |
|
} |
|
const doWrite = (data) => { |
|
const compress = packets.some((packet) => { |
|
return packet.options && packet.options.compress; |
|
}); |
|
this.write(data, { compress }); |
|
}; |
|
if (this.protocol === 3) { |
|
this.parser.encodePayload(packets, this.supportsBinary, doWrite); |
|
} |
|
else { |
|
this.parser.encodePayload(packets, doWrite); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
write(data, options) { |
|
debug('writing "%s"', data); |
|
this.doWrite(data, options, () => { |
|
this.req.cleanup(); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
doWrite(data, options, callback) { |
|
|
|
const isString = typeof data === "string"; |
|
const contentType = isString |
|
? "text/plain; charset=UTF-8" |
|
: "application/octet-stream"; |
|
const headers = { |
|
"Content-Type": contentType, |
|
}; |
|
const respond = (data) => { |
|
this.headers(this.req, headers); |
|
Object.keys(headers).forEach((key) => { |
|
this.res.writeHeader(key, String(headers[key])); |
|
}); |
|
this.res.end(data); |
|
callback(); |
|
}; |
|
if (!this.httpCompression || !options.compress) { |
|
respond(data); |
|
return; |
|
} |
|
const len = isString ? Buffer.byteLength(data) : data.length; |
|
if (len < this.httpCompression.threshold) { |
|
respond(data); |
|
return; |
|
} |
|
const encoding = accepts(this.req).encodings(["gzip", "deflate"]); |
|
if (!encoding) { |
|
respond(data); |
|
return; |
|
} |
|
this.compress(data, encoding, (err, data) => { |
|
if (err) { |
|
this.res.writeStatus("500 Internal Server Error"); |
|
this.res.end(); |
|
callback(err); |
|
return; |
|
} |
|
headers["Content-Encoding"] = encoding; |
|
respond(data); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
compress(data, encoding, callback) { |
|
debug("compressing"); |
|
const buffers = []; |
|
let nread = 0; |
|
compressionMethods[encoding](this.httpCompression) |
|
.on("error", callback) |
|
.on("data", function (chunk) { |
|
buffers.push(chunk); |
|
nread += chunk.length; |
|
}) |
|
.on("end", function () { |
|
callback(null, Buffer.concat(buffers, nread)); |
|
}) |
|
.end(data); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
doClose(fn) { |
|
debug("closing"); |
|
let closeTimeoutTimer; |
|
const onClose = () => { |
|
clearTimeout(closeTimeoutTimer); |
|
fn(); |
|
this.onClose(); |
|
}; |
|
if (this.writable) { |
|
debug("transport writable - closing right away"); |
|
this.send([{ type: "close" }]); |
|
onClose(); |
|
} |
|
else if (this.discarded) { |
|
debug("transport discarded - closing right away"); |
|
onClose(); |
|
} |
|
else { |
|
debug("transport not writable - buffering orderly close"); |
|
this.shouldClose = onClose; |
|
closeTimeoutTimer = setTimeout(onClose, this.closeTimeout); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
headers(req, headers) { |
|
headers = headers || {}; |
|
|
|
|
|
const ua = req.headers["user-agent"]; |
|
if (ua && (~ua.indexOf(";MSIE") || ~ua.indexOf("Trident/"))) { |
|
headers["X-XSS-Protection"] = "0"; |
|
} |
|
this.emit("headers", headers, req); |
|
return headers; |
|
} |
|
} |
|
exports.Polling = Polling; |
|
|