|
|
/** * 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;
if (global.location) { var isSSL = 'https:' === location.protocol; var port = location.port;
// some user agents have empty `location.port`
if (!port) { port = isSSL ? 443 : 80; }
this.xd = opts.hostname !== global.location.hostname || port !== opts.port; this.xs = opts.secure !== isSSL; } else { this.extraHeaders = opts.extraHeaders; } }
/** * 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;
// 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.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(true); for (var i in this.extraHeaders) { if (this.extraHeaders.hasOwnProperty(i)) { xhr.setRequestHeader(i, this.extraHeaders[i]); } } } } catch (e) {} if (this.supportsBinary) { // This has to be done after open because Firefox is stupid
// http://stackoverflow.com/questions/13216903/get-binary-data-with-xmlhttprequest-in-a-firefox-extension
xhr.responseType = 'arraybuffer'; }
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 = true; }
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 (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(xhr.status); }, 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 (global.document) { 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 (global.document) { 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').split(';')[0]; } catch (e) {} if (contentType === 'application/octet-stream') { data = this.xhr.response || this.xhr.responseText; } else { if (!this.supportsBinary) { data = this.xhr.responseText; } else { try { data = String.fromCharCode.apply(null, new Uint8Array(this.xhr.response)); } catch (e) { var ui8Arr = new Uint8Array(this.xhr.response); var dataArray = []; for (var idx = 0, length = ui8Arr.length; idx < length; idx++) { dataArray.push(ui8Arr[idx]); }
data = String.fromCharCode.apply(null, dataArray); } } } } catch (e) { this.onError(e); } if (null != data) { this.onData(data); } };
/** * Check if it has XDomainRequest. * * @api private */
Request.prototype.hasXDR = function () { return 'undefined' !== typeof global.XDomainRequest && !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 (global.document) { if (global.attachEvent) { global.attachEvent('onunload', unloadHandler); } else if (global.addEventListener) { global.addEventListener('beforeunload', unloadHandler, false); } }
function unloadHandler () { for (var i in Request.requests) { if (Request.requests.hasOwnProperty(i)) { Request.requests[i].abort(); } } }
|