Description
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.
 
 

337 lines
7.2 KiB

/* 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;