418 lines
9.0 KiB
JavaScript
418 lines
9.0 KiB
JavaScript
|
/* global attachEvent */
|
||
|
|
||
|
/**
|
||
|
* Module requirements.
|
||
|
*/
|
||
|
|
||
|
var XMLHttpRequest = require('xmlhttprequest-ssl');
|
||
|
var Polling = require('./polling');
|
||
|
var Emitter = require('component-emitter');
|
||
|
var inherit = require('component-inherit');
|
||
|
var debug = require('debug')('engine.io-client:polling-xhr');
|
||
|
|
||
|
/**
|
||
|
* Module exports.
|
||
|
*/
|
||
|
|
||
|
module.exports = XHR;
|
||
|
module.exports.Request = Request;
|
||
|
|
||
|
/**
|
||
|
* Empty function
|
||
|
*/
|
||
|
|
||
|
function empty () {}
|
||
|
|
||
|
/**
|
||
|
* XHR Polling constructor.
|
||
|
*
|
||
|
* @param {Object} opts
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
function XHR (opts) {
|
||
|
Polling.call(this, opts);
|
||
|
this.requestTimeout = opts.requestTimeout;
|
||
|
this.extraHeaders = opts.extraHeaders;
|
||
|
|
||
|
if (typeof location !== 'undefined') {
|
||
|
var isSSL = 'https:' === location.protocol;
|
||
|
var 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;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Inherits from Polling.
|
||
|
*/
|
||
|
|
||
|
inherit(XHR, Polling);
|
||
|
|
||
|
/**
|
||
|
* XHR supports binary
|
||
|
*/
|
||
|
|
||
|
XHR.prototype.supportsBinary = true;
|
||
|
|
||
|
/**
|
||
|
* Creates a request.
|
||
|
*
|
||
|
* @param {String} method
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
XHR.prototype.request = function (opts) {
|
||
|
opts = opts || {};
|
||
|
opts.uri = this.uri();
|
||
|
opts.xd = this.xd;
|
||
|
opts.xs = this.xs;
|
||
|
opts.agent = this.agent || false;
|
||
|
opts.supportsBinary = this.supportsBinary;
|
||
|
opts.enablesXDR = this.enablesXDR;
|
||
|
opts.withCredentials = this.withCredentials;
|
||
|
|
||
|
// SSL options for Node.js client
|
||
|
opts.pfx = this.pfx;
|
||
|
opts.key = this.key;
|
||
|
opts.passphrase = this.passphrase;
|
||
|
opts.cert = this.cert;
|
||
|
opts.ca = this.ca;
|
||
|
opts.ciphers = this.ciphers;
|
||
|
opts.rejectUnauthorized = this.rejectUnauthorized;
|
||
|
opts.requestTimeout = this.requestTimeout;
|
||
|
|
||
|
// other options for Node.js client
|
||
|
opts.extraHeaders = this.extraHeaders;
|
||
|
|
||
|
return new Request(opts);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Sends data.
|
||
|
*
|
||
|
* @param {String} data to send.
|
||
|
* @param {Function} called upon flush.
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
XHR.prototype.doWrite = function (data, fn) {
|
||
|
var isBinary = typeof data !== 'string' && data !== undefined;
|
||
|
var req = this.request({ method: 'POST', data: data, isBinary: isBinary });
|
||
|
var self = this;
|
||
|
req.on('success', fn);
|
||
|
req.on('error', function (err) {
|
||
|
self.onError('xhr post error', err);
|
||
|
});
|
||
|
this.sendXhr = req;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Starts a poll cycle.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
XHR.prototype.doPoll = function () {
|
||
|
debug('xhr poll');
|
||
|
var req = this.request();
|
||
|
var self = this;
|
||
|
req.on('data', function (data) {
|
||
|
self.onData(data);
|
||
|
});
|
||
|
req.on('error', function (err) {
|
||
|
self.onError('xhr poll error', err);
|
||
|
});
|
||
|
this.pollXhr = req;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Request constructor
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
function Request (opts) {
|
||
|
this.method = opts.method || 'GET';
|
||
|
this.uri = opts.uri;
|
||
|
this.xd = !!opts.xd;
|
||
|
this.xs = !!opts.xs;
|
||
|
this.async = false !== opts.async;
|
||
|
this.data = undefined !== opts.data ? opts.data : null;
|
||
|
this.agent = opts.agent;
|
||
|
this.isBinary = opts.isBinary;
|
||
|
this.supportsBinary = opts.supportsBinary;
|
||
|
this.enablesXDR = opts.enablesXDR;
|
||
|
this.withCredentials = opts.withCredentials;
|
||
|
this.requestTimeout = opts.requestTimeout;
|
||
|
|
||
|
// SSL options for Node.js client
|
||
|
this.pfx = opts.pfx;
|
||
|
this.key = opts.key;
|
||
|
this.passphrase = opts.passphrase;
|
||
|
this.cert = opts.cert;
|
||
|
this.ca = opts.ca;
|
||
|
this.ciphers = opts.ciphers;
|
||
|
this.rejectUnauthorized = opts.rejectUnauthorized;
|
||
|
|
||
|
// other options for Node.js client
|
||
|
this.extraHeaders = opts.extraHeaders;
|
||
|
|
||
|
this.create();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Mix in `Emitter`.
|
||
|
*/
|
||
|
|
||
|
Emitter(Request.prototype);
|
||
|
|
||
|
/**
|
||
|
* Creates the XHR object and sends the request.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Request.prototype.create = function () {
|
||
|
var opts = { agent: this.agent, xdomain: this.xd, xscheme: this.xs, enablesXDR: this.enablesXDR };
|
||
|
|
||
|
// SSL options for Node.js client
|
||
|
opts.pfx = this.pfx;
|
||
|
opts.key = this.key;
|
||
|
opts.passphrase = this.passphrase;
|
||
|
opts.cert = this.cert;
|
||
|
opts.ca = this.ca;
|
||
|
opts.ciphers = this.ciphers;
|
||
|
opts.rejectUnauthorized = this.rejectUnauthorized;
|
||
|
|
||
|
var xhr = this.xhr = new XMLHttpRequest(opts);
|
||
|
var self = this;
|
||
|
|
||
|
try {
|
||
|
debug('xhr open %s: %s', this.method, this.uri);
|
||
|
xhr.open(this.method, this.uri, this.async);
|
||
|
try {
|
||
|
if (this.extraHeaders) {
|
||
|
xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);
|
||
|
for (var i in this.extraHeaders) {
|
||
|
if (this.extraHeaders.hasOwnProperty(i)) {
|
||
|
xhr.setRequestHeader(i, this.extraHeaders[i]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} catch (e) {}
|
||
|
|
||
|
if ('POST' === this.method) {
|
||
|
try {
|
||
|
if (this.isBinary) {
|
||
|
xhr.setRequestHeader('Content-type', 'application/octet-stream');
|
||
|
} else {
|
||
|
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.withCredentials;
|
||
|
}
|
||
|
|
||
|
if (this.requestTimeout) {
|
||
|
xhr.timeout = this.requestTimeout;
|
||
|
}
|
||
|
|
||
|
if (this.hasXDR()) {
|
||
|
xhr.onload = function () {
|
||
|
self.onLoad();
|
||
|
};
|
||
|
xhr.onerror = function () {
|
||
|
self.onError(xhr.responseText);
|
||
|
};
|
||
|
} else {
|
||
|
xhr.onreadystatechange = function () {
|
||
|
if (xhr.readyState === 2) {
|
||
|
try {
|
||
|
var contentType = xhr.getResponseHeader('Content-Type');
|
||
|
if (self.supportsBinary && contentType === 'application/octet-stream' || contentType === 'application/octet-stream; charset=UTF-8') {
|
||
|
xhr.responseType = 'arraybuffer';
|
||
|
}
|
||
|
} catch (e) {}
|
||
|
}
|
||
|
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 fhrom 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
|
||
|
*/
|
||
|
|
||
|
Request.prototype.onSuccess = function () {
|
||
|
this.emit('success');
|
||
|
this.cleanup();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Called if we have data.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Request.prototype.onData = function (data) {
|
||
|
this.emit('data', data);
|
||
|
this.onSuccess();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Called upon error.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Request.prototype.onError = function (err) {
|
||
|
this.emit('error', err);
|
||
|
this.cleanup(true);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Cleans up house.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Request.prototype.cleanup = function (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
|
||
|
*/
|
||
|
|
||
|
Request.prototype.onLoad = function () {
|
||
|
var data;
|
||
|
try {
|
||
|
var contentType;
|
||
|
try {
|
||
|
contentType = this.xhr.getResponseHeader('Content-Type');
|
||
|
} catch (e) {}
|
||
|
if (contentType === 'application/octet-stream' || contentType === 'application/octet-stream; charset=UTF-8') {
|
||
|
data = this.xhr.response || this.xhr.responseText;
|
||
|
} else {
|
||
|
data = this.xhr.responseText;
|
||
|
}
|
||
|
} catch (e) {
|
||
|
this.onError(e);
|
||
|
}
|
||
|
if (null != data) {
|
||
|
this.onData(data);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Check if it has XDomainRequest.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Request.prototype.hasXDR = function () {
|
||
|
return typeof XDomainRequest !== 'undefined' && !this.xs && this.enablesXDR;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Aborts the request.
|
||
|
*
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Request.prototype.abort = function () {
|
||
|
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') {
|
||
|
var terminationEvent = 'onpagehide' in self ? 'pagehide' : 'unload';
|
||
|
addEventListener(terminationEvent, unloadHandler, false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function unloadHandler () {
|
||
|
for (var i in Request.requests) {
|
||
|
if (Request.requests.hasOwnProperty(i)) {
|
||
|
Request.requests[i].abort();
|
||
|
}
|
||
|
}
|
||
|
}
|