You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
338 lines
7.2 KiB
338 lines
7.2 KiB
4 years ago
|
/* global attachEvent */
|
||
|
|
||
|
const XMLHttpRequest = require("xmlhttprequest-ssl");
|
||
|
const Polling = require("./polling");
|
||
|
const Emitter = require("component-emitter");
|
||
|
const { pick } = require("../util");
|
||
|
const globalThis = require("../globalThis");
|
||
|
|
||
|
const debug = require("debug")("engine.io-client:polling-xhr");
|
||
|
|
||
|
/**
|
||
|
* Empty function
|
||
|
*/
|
||
|
|
||
|
function empty() {}
|
||
|
|
||
|
const hasXHR2 = (function() {
|
||
|
const XMLHttpRequest = require("xmlhttprequest-ssl");
|
||
|
const xhr = new XMLHttpRequest({ xdomain: false });
|
||
|
return null != xhr.responseType;
|
||
|
})();
|
||
|
|
||
|
class XHR extends Polling {
|
||
|
/**
|
||
|
* XHR Polling constructor.
|
||
|
*
|
||
|
* @param {Object} opts
|
||
|
* @api public
|
||
|
*/
|
||
|
constructor(opts) {
|
||
|
super(opts);
|
||
|
|
||
|
if (typeof location !== "undefined") {
|
||
|
const isSSL = "https:" === location.protocol;
|
||
|
let port = location.port;
|
||
|
|
||
|
// some user agents have empty `location.port`
|
||
|
if (!port) {
|
||
|
port = isSSL ? 443 : 80;
|
||
|
}
|
||
|
|
||
|
this.xd =
|
||
|
(typeof location !== "undefined" &&
|
||
|
opts.hostname !== location.hostname) ||
|
||
|
port !== opts.port;
|
||
|
this.xs = opts.secure !== isSSL;
|
||
|
}
|
||
|
/**
|
||
|
* XHR supports binary
|
||
|
*/
|
||
|
const forceBase64 = opts && opts.forceBase64;
|
||
|
this.supportsBinary = hasXHR2 && !forceBase64;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a request.
|
||
|
*
|
||
|
* @param {String} method
|
||
|
* @api private
|
||
|
*/
|
||
|
request(opts = {}) {
|
||
|
Object.assign(opts, { xd: this.xd, xs: this.xs }, this.opts);
|
||
|
return new Request(this.uri(), opts);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sends data.
|
||
|
*
|
||
|
* @param {String} data to send.
|
||
|
* @param {Function} called upon flush.
|
||
|
* @api private
|
||
|
*/
|
||
|
doWrite(data, fn) {
|
||
|
const req = this.request({
|
||
|
method: "POST",
|
||
|
data: data
|
||
|
});
|
||
|
const self = this;
|
||
|
req.on("success", fn);
|
||
|
req.on("error", function(err) {
|
||
|
self.onError("xhr post error", err);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Starts a poll cycle.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
doPoll() {
|
||
|
debug("xhr poll");
|
||
|
const req = this.request();
|
||
|
const self = this;
|
||
|
req.on("data", function(data) {
|
||
|
self.onData(data);
|
||
|
});
|
||
|
req.on("error", function(err) {
|
||
|
self.onError("xhr poll error", err);
|
||
|
});
|
||
|
this.pollXhr = req;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class Request extends Emitter {
|
||
|
/**
|
||
|
* Request constructor
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api public
|
||
|
*/
|
||
|
constructor(uri, opts) {
|
||
|
super();
|
||
|
this.opts = opts;
|
||
|
|
||
|
this.method = opts.method || "GET";
|
||
|
this.uri = uri;
|
||
|
this.async = false !== opts.async;
|
||
|
this.data = undefined !== opts.data ? opts.data : null;
|
||
|
|
||
|
this.create();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates the XHR object and sends the request.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
create() {
|
||
|
const opts = pick(
|
||
|
this.opts,
|
||
|
"agent",
|
||
|
"enablesXDR",
|
||
|
"pfx",
|
||
|
"key",
|
||
|
"passphrase",
|
||
|
"cert",
|
||
|
"ca",
|
||
|
"ciphers",
|
||
|
"rejectUnauthorized"
|
||
|
);
|
||
|
opts.xdomain = !!this.opts.xd;
|
||
|
opts.xscheme = !!this.opts.xs;
|
||
|
|
||
|
const xhr = (this.xhr = new XMLHttpRequest(opts));
|
||
|
const self = this;
|
||
|
|
||
|
try {
|
||
|
debug("xhr open %s: %s", this.method, this.uri);
|
||
|
xhr.open(this.method, this.uri, this.async);
|
||
|
try {
|
||
|
if (this.opts.extraHeaders) {
|
||
|
xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);
|
||
|
for (let i in this.opts.extraHeaders) {
|
||
|
if (this.opts.extraHeaders.hasOwnProperty(i)) {
|
||
|
xhr.setRequestHeader(i, this.opts.extraHeaders[i]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} catch (e) {}
|
||
|
|
||
|
if ("POST" === this.method) {
|
||
|
try {
|
||
|
xhr.setRequestHeader("Content-type", "text/plain;charset=UTF-8");
|
||
|
} catch (e) {}
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
xhr.setRequestHeader("Accept", "*/*");
|
||
|
} catch (e) {}
|
||
|
|
||
|
// ie6 check
|
||
|
if ("withCredentials" in xhr) {
|
||
|
xhr.withCredentials = this.opts.withCredentials;
|
||
|
}
|
||
|
|
||
|
if (this.opts.requestTimeout) {
|
||
|
xhr.timeout = this.opts.requestTimeout;
|
||
|
}
|
||
|
|
||
|
if (this.hasXDR()) {
|
||
|
xhr.onload = function() {
|
||
|
self.onLoad();
|
||
|
};
|
||
|
xhr.onerror = function() {
|
||
|
self.onError(xhr.responseText);
|
||
|
};
|
||
|
} else {
|
||
|
xhr.onreadystatechange = function() {
|
||
|
if (4 !== xhr.readyState) return;
|
||
|
if (200 === xhr.status || 1223 === xhr.status) {
|
||
|
self.onLoad();
|
||
|
} else {
|
||
|
// make sure the `error` event handler that's user-set
|
||
|
// does not throw in the same tick and gets caught here
|
||
|
setTimeout(function() {
|
||
|
self.onError(typeof xhr.status === "number" ? xhr.status : 0);
|
||
|
}, 0);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
debug("xhr data %s", this.data);
|
||
|
xhr.send(this.data);
|
||
|
} catch (e) {
|
||
|
// Need to defer since .create() is called directly from the constructor
|
||
|
// and thus the 'error' event can only be only bound *after* this exception
|
||
|
// occurs. Therefore, also, we cannot throw here at all.
|
||
|
setTimeout(function() {
|
||
|
self.onError(e);
|
||
|
}, 0);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (typeof document !== "undefined") {
|
||
|
this.index = Request.requestsCount++;
|
||
|
Request.requests[this.index] = this;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called upon successful response.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
onSuccess() {
|
||
|
this.emit("success");
|
||
|
this.cleanup();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called if we have data.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
onData(data) {
|
||
|
this.emit("data", data);
|
||
|
this.onSuccess();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called upon error.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
onError(err) {
|
||
|
this.emit("error", err);
|
||
|
this.cleanup(true);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Cleans up house.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
cleanup(fromError) {
|
||
|
if ("undefined" === typeof this.xhr || null === this.xhr) {
|
||
|
return;
|
||
|
}
|
||
|
// xmlhttprequest
|
||
|
if (this.hasXDR()) {
|
||
|
this.xhr.onload = this.xhr.onerror = empty;
|
||
|
} else {
|
||
|
this.xhr.onreadystatechange = empty;
|
||
|
}
|
||
|
|
||
|
if (fromError) {
|
||
|
try {
|
||
|
this.xhr.abort();
|
||
|
} catch (e) {}
|
||
|
}
|
||
|
|
||
|
if (typeof document !== "undefined") {
|
||
|
delete Request.requests[this.index];
|
||
|
}
|
||
|
|
||
|
this.xhr = null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called upon load.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
onLoad() {
|
||
|
const data = this.xhr.responseText;
|
||
|
if (data !== null) {
|
||
|
this.onData(data);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if it has XDomainRequest.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
hasXDR() {
|
||
|
return typeof XDomainRequest !== "undefined" && !this.xs && this.enablesXDR;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Aborts the request.
|
||
|
*
|
||
|
* @api public
|
||
|
*/
|
||
|
abort() {
|
||
|
this.cleanup();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Aborts pending requests when unloading the window. This is needed to prevent
|
||
|
* memory leaks (e.g. when using IE) and to ensure that no spurious error is
|
||
|
* emitted.
|
||
|
*/
|
||
|
|
||
|
Request.requestsCount = 0;
|
||
|
Request.requests = {};
|
||
|
|
||
|
if (typeof document !== "undefined") {
|
||
|
if (typeof attachEvent === "function") {
|
||
|
attachEvent("onunload", unloadHandler);
|
||
|
} else if (typeof addEventListener === "function") {
|
||
|
const terminationEvent = "onpagehide" in globalThis ? "pagehide" : "unload";
|
||
|
addEventListener(terminationEvent, unloadHandler, false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function unloadHandler() {
|
||
|
for (let i in Request.requests) {
|
||
|
if (Request.requests.hasOwnProperty(i)) {
|
||
|
Request.requests[i].abort();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = XHR;
|
||
|
module.exports.Request = Request;
|