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.

424 lines
9.0 KiB

8 years ago
  1. /**
  2. * Module requirements.
  3. */
  4. var XMLHttpRequest = require('xmlhttprequest-ssl');
  5. var Polling = require('./polling');
  6. var Emitter = require('component-emitter');
  7. var inherit = require('component-inherit');
  8. var debug = require('debug')('engine.io-client:polling-xhr');
  9. /**
  10. * Module exports.
  11. */
  12. module.exports = XHR;
  13. module.exports.Request = Request;
  14. /**
  15. * Empty function
  16. */
  17. function empty () {}
  18. /**
  19. * XHR Polling constructor.
  20. *
  21. * @param {Object} opts
  22. * @api public
  23. */
  24. function XHR (opts) {
  25. Polling.call(this, opts);
  26. this.requestTimeout = opts.requestTimeout;
  27. if (global.location) {
  28. var isSSL = 'https:' === location.protocol;
  29. var port = location.port;
  30. // some user agents have empty `location.port`
  31. if (!port) {
  32. port = isSSL ? 443 : 80;
  33. }
  34. this.xd = opts.hostname !== global.location.hostname ||
  35. port !== opts.port;
  36. this.xs = opts.secure !== isSSL;
  37. } else {
  38. this.extraHeaders = opts.extraHeaders;
  39. }
  40. }
  41. /**
  42. * Inherits from Polling.
  43. */
  44. inherit(XHR, Polling);
  45. /**
  46. * XHR supports binary
  47. */
  48. XHR.prototype.supportsBinary = true;
  49. /**
  50. * Creates a request.
  51. *
  52. * @param {String} method
  53. * @api private
  54. */
  55. XHR.prototype.request = function (opts) {
  56. opts = opts || {};
  57. opts.uri = this.uri();
  58. opts.xd = this.xd;
  59. opts.xs = this.xs;
  60. opts.agent = this.agent || false;
  61. opts.supportsBinary = this.supportsBinary;
  62. opts.enablesXDR = this.enablesXDR;
  63. // SSL options for Node.js client
  64. opts.pfx = this.pfx;
  65. opts.key = this.key;
  66. opts.passphrase = this.passphrase;
  67. opts.cert = this.cert;
  68. opts.ca = this.ca;
  69. opts.ciphers = this.ciphers;
  70. opts.rejectUnauthorized = this.rejectUnauthorized;
  71. opts.requestTimeout = this.requestTimeout;
  72. // other options for Node.js client
  73. opts.extraHeaders = this.extraHeaders;
  74. return new Request(opts);
  75. };
  76. /**
  77. * Sends data.
  78. *
  79. * @param {String} data to send.
  80. * @param {Function} called upon flush.
  81. * @api private
  82. */
  83. XHR.prototype.doWrite = function (data, fn) {
  84. var isBinary = typeof data !== 'string' && data !== undefined;
  85. var req = this.request({ method: 'POST', data: data, isBinary: isBinary });
  86. var self = this;
  87. req.on('success', fn);
  88. req.on('error', function (err) {
  89. self.onError('xhr post error', err);
  90. });
  91. this.sendXhr = req;
  92. };
  93. /**
  94. * Starts a poll cycle.
  95. *
  96. * @api private
  97. */
  98. XHR.prototype.doPoll = function () {
  99. debug('xhr poll');
  100. var req = this.request();
  101. var self = this;
  102. req.on('data', function (data) {
  103. self.onData(data);
  104. });
  105. req.on('error', function (err) {
  106. self.onError('xhr poll error', err);
  107. });
  108. this.pollXhr = req;
  109. };
  110. /**
  111. * Request constructor
  112. *
  113. * @param {Object} options
  114. * @api public
  115. */
  116. function Request (opts) {
  117. this.method = opts.method || 'GET';
  118. this.uri = opts.uri;
  119. this.xd = !!opts.xd;
  120. this.xs = !!opts.xs;
  121. this.async = false !== opts.async;
  122. this.data = undefined !== opts.data ? opts.data : null;
  123. this.agent = opts.agent;
  124. this.isBinary = opts.isBinary;
  125. this.supportsBinary = opts.supportsBinary;
  126. this.enablesXDR = opts.enablesXDR;
  127. this.requestTimeout = opts.requestTimeout;
  128. // SSL options for Node.js client
  129. this.pfx = opts.pfx;
  130. this.key = opts.key;
  131. this.passphrase = opts.passphrase;
  132. this.cert = opts.cert;
  133. this.ca = opts.ca;
  134. this.ciphers = opts.ciphers;
  135. this.rejectUnauthorized = opts.rejectUnauthorized;
  136. // other options for Node.js client
  137. this.extraHeaders = opts.extraHeaders;
  138. this.create();
  139. }
  140. /**
  141. * Mix in `Emitter`.
  142. */
  143. Emitter(Request.prototype);
  144. /**
  145. * Creates the XHR object and sends the request.
  146. *
  147. * @api private
  148. */
  149. Request.prototype.create = function () {
  150. var opts = { agent: this.agent, xdomain: this.xd, xscheme: this.xs, enablesXDR: this.enablesXDR };
  151. // SSL options for Node.js client
  152. opts.pfx = this.pfx;
  153. opts.key = this.key;
  154. opts.passphrase = this.passphrase;
  155. opts.cert = this.cert;
  156. opts.ca = this.ca;
  157. opts.ciphers = this.ciphers;
  158. opts.rejectUnauthorized = this.rejectUnauthorized;
  159. var xhr = this.xhr = new XMLHttpRequest(opts);
  160. var self = this;
  161. try {
  162. debug('xhr open %s: %s', this.method, this.uri);
  163. xhr.open(this.method, this.uri, this.async);
  164. try {
  165. if (this.extraHeaders) {
  166. xhr.setDisableHeaderCheck(true);
  167. for (var i in this.extraHeaders) {
  168. if (this.extraHeaders.hasOwnProperty(i)) {
  169. xhr.setRequestHeader(i, this.extraHeaders[i]);
  170. }
  171. }
  172. }
  173. } catch (e) {}
  174. if (this.supportsBinary) {
  175. // This has to be done after open because Firefox is stupid
  176. // http://stackoverflow.com/questions/13216903/get-binary-data-with-xmlhttprequest-in-a-firefox-extension
  177. xhr.responseType = 'arraybuffer';
  178. }
  179. if ('POST' === this.method) {
  180. try {
  181. if (this.isBinary) {
  182. xhr.setRequestHeader('Content-type', 'application/octet-stream');
  183. } else {
  184. xhr.setRequestHeader('Content-type', 'text/plain;charset=UTF-8');
  185. }
  186. } catch (e) {}
  187. }
  188. try {
  189. xhr.setRequestHeader('Accept', '*/*');
  190. } catch (e) {}
  191. // ie6 check
  192. if ('withCredentials' in xhr) {
  193. xhr.withCredentials = true;
  194. }
  195. if (this.requestTimeout) {
  196. xhr.timeout = this.requestTimeout;
  197. }
  198. if (this.hasXDR()) {
  199. xhr.onload = function () {
  200. self.onLoad();
  201. };
  202. xhr.onerror = function () {
  203. self.onError(xhr.responseText);
  204. };
  205. } else {
  206. xhr.onreadystatechange = function () {
  207. if (4 !== xhr.readyState) return;
  208. if (200 === xhr.status || 1223 === xhr.status) {
  209. self.onLoad();
  210. } else {
  211. // make sure the `error` event handler that's user-set
  212. // does not throw in the same tick and gets caught here
  213. setTimeout(function () {
  214. self.onError(xhr.status);
  215. }, 0);
  216. }
  217. };
  218. }
  219. debug('xhr data %s', this.data);
  220. xhr.send(this.data);
  221. } catch (e) {
  222. // Need to defer since .create() is called directly fhrom the constructor
  223. // and thus the 'error' event can only be only bound *after* this exception
  224. // occurs. Therefore, also, we cannot throw here at all.
  225. setTimeout(function () {
  226. self.onError(e);
  227. }, 0);
  228. return;
  229. }
  230. if (global.document) {
  231. this.index = Request.requestsCount++;
  232. Request.requests[this.index] = this;
  233. }
  234. };
  235. /**
  236. * Called upon successful response.
  237. *
  238. * @api private
  239. */
  240. Request.prototype.onSuccess = function () {
  241. this.emit('success');
  242. this.cleanup();
  243. };
  244. /**
  245. * Called if we have data.
  246. *
  247. * @api private
  248. */
  249. Request.prototype.onData = function (data) {
  250. this.emit('data', data);
  251. this.onSuccess();
  252. };
  253. /**
  254. * Called upon error.
  255. *
  256. * @api private
  257. */
  258. Request.prototype.onError = function (err) {
  259. this.emit('error', err);
  260. this.cleanup(true);
  261. };
  262. /**
  263. * Cleans up house.
  264. *
  265. * @api private
  266. */
  267. Request.prototype.cleanup = function (fromError) {
  268. if ('undefined' === typeof this.xhr || null === this.xhr) {
  269. return;
  270. }
  271. // xmlhttprequest
  272. if (this.hasXDR()) {
  273. this.xhr.onload = this.xhr.onerror = empty;
  274. } else {
  275. this.xhr.onreadystatechange = empty;
  276. }
  277. if (fromError) {
  278. try {
  279. this.xhr.abort();
  280. } catch (e) {}
  281. }
  282. if (global.document) {
  283. delete Request.requests[this.index];
  284. }
  285. this.xhr = null;
  286. };
  287. /**
  288. * Called upon load.
  289. *
  290. * @api private
  291. */
  292. Request.prototype.onLoad = function () {
  293. var data;
  294. try {
  295. var contentType;
  296. try {
  297. contentType = this.xhr.getResponseHeader('Content-Type').split(';')[0];
  298. } catch (e) {}
  299. if (contentType === 'application/octet-stream') {
  300. data = this.xhr.response || this.xhr.responseText;
  301. } else {
  302. if (!this.supportsBinary) {
  303. data = this.xhr.responseText;
  304. } else {
  305. try {
  306. data = String.fromCharCode.apply(null, new Uint8Array(this.xhr.response));
  307. } catch (e) {
  308. var ui8Arr = new Uint8Array(this.xhr.response);
  309. var dataArray = [];
  310. for (var idx = 0, length = ui8Arr.length; idx < length; idx++) {
  311. dataArray.push(ui8Arr[idx]);
  312. }
  313. data = String.fromCharCode.apply(null, dataArray);
  314. }
  315. }
  316. }
  317. } catch (e) {
  318. this.onError(e);
  319. }
  320. if (null != data) {
  321. this.onData(data);
  322. }
  323. };
  324. /**
  325. * Check if it has XDomainRequest.
  326. *
  327. * @api private
  328. */
  329. Request.prototype.hasXDR = function () {
  330. return 'undefined' !== typeof global.XDomainRequest && !this.xs && this.enablesXDR;
  331. };
  332. /**
  333. * Aborts the request.
  334. *
  335. * @api public
  336. */
  337. Request.prototype.abort = function () {
  338. this.cleanup();
  339. };
  340. /**
  341. * Aborts pending requests when unloading the window. This is needed to prevent
  342. * memory leaks (e.g. when using IE) and to ensure that no spurious error is
  343. * emitted.
  344. */
  345. Request.requestsCount = 0;
  346. Request.requests = {};
  347. if (global.document) {
  348. if (global.attachEvent) {
  349. global.attachEvent('onunload', unloadHandler);
  350. } else if (global.addEventListener) {
  351. global.addEventListener('beforeunload', unloadHandler, false);
  352. }
  353. }
  354. function unloadHandler () {
  355. for (var i in Request.requests) {
  356. if (Request.requests.hasOwnProperty(i)) {
  357. Request.requests[i].abort();
  358. }
  359. }
  360. }