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.
412 lines
8.6 KiB
412 lines
8.6 KiB
/** |
|
* 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 (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; |
|
} |
|
} |
|
|
|
/** |
|
* 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 && 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 = 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 (xhr.readyState === 2) { |
|
try { |
|
var contentType = xhr.getResponseHeader('Content-Type'); |
|
if (self.supportsBinary && contentType === 'application/octet-stream') { |
|
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(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'); |
|
} catch (e) {} |
|
if (contentType === 'application/octet-stream') { |
|
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 '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(); |
|
} |
|
} |
|
}
|
|
|