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.

736 lines
22 KiB

4 years ago
  1. /**
  2. * XmlHttpRequest implementation that uses TLS and flash SocketPool.
  3. *
  4. * @author Dave Longley
  5. *
  6. * Copyright (c) 2010-2013 Digital Bazaar, Inc.
  7. */
  8. var forge = require('./forge');
  9. require('./socket');
  10. require('./http');
  11. /* XHR API */
  12. var xhrApi = module.exports = forge.xhr = forge.xhr || {};
  13. (function($) {
  14. // logging category
  15. var cat = 'forge.xhr';
  16. /*
  17. XMLHttpRequest interface definition from:
  18. http://www.w3.org/TR/XMLHttpRequest
  19. interface XMLHttpRequest {
  20. // event handler
  21. attribute EventListener onreadystatechange;
  22. // state
  23. const unsigned short UNSENT = 0;
  24. const unsigned short OPENED = 1;
  25. const unsigned short HEADERS_RECEIVED = 2;
  26. const unsigned short LOADING = 3;
  27. const unsigned short DONE = 4;
  28. readonly attribute unsigned short readyState;
  29. // request
  30. void open(in DOMString method, in DOMString url);
  31. void open(in DOMString method, in DOMString url, in boolean async);
  32. void open(in DOMString method, in DOMString url,
  33. in boolean async, in DOMString user);
  34. void open(in DOMString method, in DOMString url,
  35. in boolean async, in DOMString user, in DOMString password);
  36. void setRequestHeader(in DOMString header, in DOMString value);
  37. void send();
  38. void send(in DOMString data);
  39. void send(in Document data);
  40. void abort();
  41. // response
  42. DOMString getAllResponseHeaders();
  43. DOMString getResponseHeader(in DOMString header);
  44. readonly attribute DOMString responseText;
  45. readonly attribute Document responseXML;
  46. readonly attribute unsigned short status;
  47. readonly attribute DOMString statusText;
  48. };
  49. */
  50. // readyStates
  51. var UNSENT = 0;
  52. var OPENED = 1;
  53. var HEADERS_RECEIVED = 2;
  54. var LOADING = 3;
  55. var DONE = 4;
  56. // exceptions
  57. var INVALID_STATE_ERR = 11;
  58. var SYNTAX_ERR = 12;
  59. var SECURITY_ERR = 18;
  60. var NETWORK_ERR = 19;
  61. var ABORT_ERR = 20;
  62. // private flash socket pool vars
  63. var _sp = null;
  64. var _policyPort = 0;
  65. var _policyUrl = null;
  66. // default client (used if no special URL provided when creating an XHR)
  67. var _client = null;
  68. // all clients including the default, key'd by full base url
  69. // (multiple cross-domain http clients are permitted so there may be more
  70. // than one client in this map)
  71. // TODO: provide optional clean up API for non-default clients
  72. var _clients = {};
  73. // the default maximum number of concurrents connections per client
  74. var _maxConnections = 10;
  75. var net = forge.net;
  76. var http = forge.http;
  77. /**
  78. * Initializes flash XHR support.
  79. *
  80. * @param options:
  81. * url: the default base URL to connect to if xhr URLs are relative,
  82. * ie: https://myserver.com.
  83. * flashId: the dom ID of the flash SocketPool.
  84. * policyPort: the port that provides the server's flash policy, 0 to use
  85. * the flash default.
  86. * policyUrl: the policy file URL to use instead of a policy port.
  87. * msie: true if browser is internet explorer, false if not.
  88. * connections: the maximum number of concurrent connections.
  89. * caCerts: a list of PEM-formatted certificates to trust.
  90. * cipherSuites: an optional array of cipher suites to use,
  91. * see forge.tls.CipherSuites.
  92. * verify: optional TLS certificate verify callback to use (see forge.tls
  93. * for details).
  94. * getCertificate: an optional callback used to get a client-side
  95. * certificate (see forge.tls for details).
  96. * getPrivateKey: an optional callback used to get a client-side private
  97. * key (see forge.tls for details).
  98. * getSignature: an optional callback used to get a client-side signature
  99. * (see forge.tls for details).
  100. * persistCookies: true to use persistent cookies via flash local storage,
  101. * false to only keep cookies in javascript.
  102. * primeTlsSockets: true to immediately connect TLS sockets on their
  103. * creation so that they will cache TLS sessions for reuse.
  104. */
  105. xhrApi.init = function(options) {
  106. forge.log.debug(cat, 'initializing', options);
  107. // update default policy port and max connections
  108. _policyPort = options.policyPort || _policyPort;
  109. _policyUrl = options.policyUrl || _policyUrl;
  110. _maxConnections = options.connections || _maxConnections;
  111. // create the flash socket pool
  112. _sp = net.createSocketPool({
  113. flashId: options.flashId,
  114. policyPort: _policyPort,
  115. policyUrl: _policyUrl,
  116. msie: options.msie || false
  117. });
  118. // create default http client
  119. _client = http.createClient({
  120. url: options.url || (
  121. window.location.protocol + '//' + window.location.host),
  122. socketPool: _sp,
  123. policyPort: _policyPort,
  124. policyUrl: _policyUrl,
  125. connections: options.connections || _maxConnections,
  126. caCerts: options.caCerts,
  127. cipherSuites: options.cipherSuites,
  128. persistCookies: options.persistCookies || true,
  129. primeTlsSockets: options.primeTlsSockets || false,
  130. verify: options.verify,
  131. getCertificate: options.getCertificate,
  132. getPrivateKey: options.getPrivateKey,
  133. getSignature: options.getSignature
  134. });
  135. _clients[_client.url.full] = _client;
  136. forge.log.debug(cat, 'ready');
  137. };
  138. /**
  139. * Called to clean up the clients and socket pool.
  140. */
  141. xhrApi.cleanup = function() {
  142. // destroy all clients
  143. for(var key in _clients) {
  144. _clients[key].destroy();
  145. }
  146. _clients = {};
  147. _client = null;
  148. // destroy socket pool
  149. _sp.destroy();
  150. _sp = null;
  151. };
  152. /**
  153. * Sets a cookie.
  154. *
  155. * @param cookie the cookie with parameters:
  156. * name: the name of the cookie.
  157. * value: the value of the cookie.
  158. * comment: an optional comment string.
  159. * maxAge: the age of the cookie in seconds relative to created time.
  160. * secure: true if the cookie must be sent over a secure protocol.
  161. * httpOnly: true to restrict access to the cookie from javascript
  162. * (inaffective since the cookies are stored in javascript).
  163. * path: the path for the cookie.
  164. * domain: optional domain the cookie belongs to (must start with dot).
  165. * version: optional version of the cookie.
  166. * created: creation time, in UTC seconds, of the cookie.
  167. */
  168. xhrApi.setCookie = function(cookie) {
  169. // default cookie expiration to never
  170. cookie.maxAge = cookie.maxAge || -1;
  171. // if the cookie's domain is set, use the appropriate client
  172. if(cookie.domain) {
  173. // add the cookies to the applicable domains
  174. for(var key in _clients) {
  175. var client = _clients[key];
  176. if(http.withinCookieDomain(client.url, cookie) &&
  177. client.secure === cookie.secure) {
  178. client.setCookie(cookie);
  179. }
  180. }
  181. } else {
  182. // use the default domain
  183. // FIXME: should a null domain cookie be added to all clients? should
  184. // this be an option?
  185. _client.setCookie(cookie);
  186. }
  187. };
  188. /**
  189. * Gets a cookie.
  190. *
  191. * @param name the name of the cookie.
  192. * @param path an optional path for the cookie (if there are multiple cookies
  193. * with the same name but different paths).
  194. * @param domain an optional domain for the cookie (if not using the default
  195. * domain).
  196. *
  197. * @return the cookie, cookies (if multiple matches), or null if not found.
  198. */
  199. xhrApi.getCookie = function(name, path, domain) {
  200. var rval = null;
  201. if(domain) {
  202. // get the cookies from the applicable domains
  203. for(var key in _clients) {
  204. var client = _clients[key];
  205. if(http.withinCookieDomain(client.url, domain)) {
  206. var cookie = client.getCookie(name, path);
  207. if(cookie !== null) {
  208. if(rval === null) {
  209. rval = cookie;
  210. } else if(!forge.util.isArray(rval)) {
  211. rval = [rval, cookie];
  212. } else {
  213. rval.push(cookie);
  214. }
  215. }
  216. }
  217. }
  218. } else {
  219. // get cookie from default domain
  220. rval = _client.getCookie(name, path);
  221. }
  222. return rval;
  223. };
  224. /**
  225. * Removes a cookie.
  226. *
  227. * @param name the name of the cookie.
  228. * @param path an optional path for the cookie (if there are multiple cookies
  229. * with the same name but different paths).
  230. * @param domain an optional domain for the cookie (if not using the default
  231. * domain).
  232. *
  233. * @return true if a cookie was removed, false if not.
  234. */
  235. xhrApi.removeCookie = function(name, path, domain) {
  236. var rval = false;
  237. if(domain) {
  238. // remove the cookies from the applicable domains
  239. for(var key in _clients) {
  240. var client = _clients[key];
  241. if(http.withinCookieDomain(client.url, domain)) {
  242. if(client.removeCookie(name, path)) {
  243. rval = true;
  244. }
  245. }
  246. }
  247. } else {
  248. // remove cookie from default domain
  249. rval = _client.removeCookie(name, path);
  250. }
  251. return rval;
  252. };
  253. /**
  254. * Creates a new XmlHttpRequest. By default the base URL, flash policy port,
  255. * etc, will be used. However, an XHR can be created to point at another
  256. * cross-domain URL.
  257. *
  258. * @param options:
  259. * logWarningOnError: If true and an HTTP error status code is received then
  260. * log a warning, otherwise log a verbose message.
  261. * verbose: If true be very verbose in the output including the response
  262. * event and response body, otherwise only include status, timing, and
  263. * data size.
  264. * logError: a multi-var log function for warnings that takes the log
  265. * category as the first var.
  266. * logWarning: a multi-var log function for warnings that takes the log
  267. * category as the first var.
  268. * logDebug: a multi-var log function for warnings that takes the log
  269. * category as the first var.
  270. * logVerbose: a multi-var log function for warnings that takes the log
  271. * category as the first var.
  272. * url: the default base URL to connect to if xhr URLs are relative,
  273. * eg: https://myserver.com, and note that the following options will be
  274. * ignored if the URL is absent or the same as the default base URL.
  275. * policyPort: the port that provides the server's flash policy, 0 to use
  276. * the flash default.
  277. * policyUrl: the policy file URL to use instead of a policy port.
  278. * connections: the maximum number of concurrent connections.
  279. * caCerts: a list of PEM-formatted certificates to trust.
  280. * cipherSuites: an optional array of cipher suites to use, see
  281. * forge.tls.CipherSuites.
  282. * verify: optional TLS certificate verify callback to use (see forge.tls
  283. * for details).
  284. * getCertificate: an optional callback used to get a client-side
  285. * certificate.
  286. * getPrivateKey: an optional callback used to get a client-side private key.
  287. * getSignature: an optional callback used to get a client-side signature.
  288. * persistCookies: true to use persistent cookies via flash local storage,
  289. * false to only keep cookies in javascript.
  290. * primeTlsSockets: true to immediately connect TLS sockets on their
  291. * creation so that they will cache TLS sessions for reuse.
  292. *
  293. * @return the XmlHttpRequest.
  294. */
  295. xhrApi.create = function(options) {
  296. // set option defaults
  297. options = $.extend({
  298. logWarningOnError: true,
  299. verbose: false,
  300. logError: function() {},
  301. logWarning: function() {},
  302. logDebug: function() {},
  303. logVerbose: function() {},
  304. url: null
  305. }, options || {});
  306. // private xhr state
  307. var _state = {
  308. // the http client to use
  309. client: null,
  310. // request storage
  311. request: null,
  312. // response storage
  313. response: null,
  314. // asynchronous, true if doing asynchronous communication
  315. asynchronous: true,
  316. // sendFlag, true if send has been called
  317. sendFlag: false,
  318. // errorFlag, true if a network error occurred
  319. errorFlag: false
  320. };
  321. // private log functions
  322. var _log = {
  323. error: options.logError || forge.log.error,
  324. warning: options.logWarning || forge.log.warning,
  325. debug: options.logDebug || forge.log.debug,
  326. verbose: options.logVerbose || forge.log.verbose
  327. };
  328. // create public xhr interface
  329. var xhr = {
  330. // an EventListener
  331. onreadystatechange: null,
  332. // readonly, the current readyState
  333. readyState: UNSENT,
  334. // a string with the response entity-body
  335. responseText: '',
  336. // a Document for response entity-bodies that are XML
  337. responseXML: null,
  338. // readonly, returns the HTTP status code (i.e. 404)
  339. status: 0,
  340. // readonly, returns the HTTP status message (i.e. 'Not Found')
  341. statusText: ''
  342. };
  343. // determine which http client to use
  344. if(options.url === null) {
  345. // use default
  346. _state.client = _client;
  347. } else {
  348. var url = http.parseUrl(options.url);
  349. if(!url) {
  350. var error = new Error('Invalid url.');
  351. error.details = {
  352. url: options.url
  353. };
  354. }
  355. // find client
  356. if(url.full in _clients) {
  357. // client found
  358. _state.client = _clients[url.full];
  359. } else {
  360. // create client
  361. _state.client = http.createClient({
  362. url: options.url,
  363. socketPool: _sp,
  364. policyPort: options.policyPort || _policyPort,
  365. policyUrl: options.policyUrl || _policyUrl,
  366. connections: options.connections || _maxConnections,
  367. caCerts: options.caCerts,
  368. cipherSuites: options.cipherSuites,
  369. persistCookies: options.persistCookies || true,
  370. primeTlsSockets: options.primeTlsSockets || false,
  371. verify: options.verify,
  372. getCertificate: options.getCertificate,
  373. getPrivateKey: options.getPrivateKey,
  374. getSignature: options.getSignature
  375. });
  376. _clients[url.full] = _state.client;
  377. }
  378. }
  379. /**
  380. * Opens the request. This method will create the HTTP request to send.
  381. *
  382. * @param method the HTTP method (i.e. 'GET').
  383. * @param url the relative url (the HTTP request path).
  384. * @param async always true, ignored.
  385. * @param user always null, ignored.
  386. * @param password always null, ignored.
  387. */
  388. xhr.open = function(method, url, async, user, password) {
  389. // 1. validate Document if one is associated
  390. // TODO: not implemented (not used yet)
  391. // 2. validate method token
  392. // 3. change method to uppercase if it matches a known
  393. // method (here we just require it to be uppercase, and
  394. // we do not allow the standard methods)
  395. // 4. disallow CONNECT, TRACE, or TRACK with a security error
  396. switch(method) {
  397. case 'DELETE':
  398. case 'GET':
  399. case 'HEAD':
  400. case 'OPTIONS':
  401. case 'PATCH':
  402. case 'POST':
  403. case 'PUT':
  404. // valid method
  405. break;
  406. case 'CONNECT':
  407. case 'TRACE':
  408. case 'TRACK':
  409. throw new Error('CONNECT, TRACE and TRACK methods are disallowed');
  410. default:
  411. throw new Error('Invalid method: ' + method);
  412. }
  413. // TODO: other validation steps in algorithm are not implemented
  414. // 19. set send flag to false
  415. // set response body to null
  416. // empty list of request headers
  417. // set request method to given method
  418. // set request URL
  419. // set username, password
  420. // set asychronous flag
  421. _state.sendFlag = false;
  422. xhr.responseText = '';
  423. xhr.responseXML = null;
  424. // custom: reset status and statusText
  425. xhr.status = 0;
  426. xhr.statusText = '';
  427. // create the HTTP request
  428. _state.request = http.createRequest({
  429. method: method,
  430. path: url
  431. });
  432. // 20. set state to OPENED
  433. xhr.readyState = OPENED;
  434. // 21. dispatch onreadystatechange
  435. if(xhr.onreadystatechange) {
  436. xhr.onreadystatechange();
  437. }
  438. };
  439. /**
  440. * Adds an HTTP header field to the request.
  441. *
  442. * @param header the name of the header field.
  443. * @param value the value of the header field.
  444. */
  445. xhr.setRequestHeader = function(header, value) {
  446. // 1. if state is not OPENED or send flag is true, raise exception
  447. if(xhr.readyState != OPENED || _state.sendFlag) {
  448. throw new Error('XHR not open or sending');
  449. }
  450. // TODO: other validation steps in spec aren't implemented
  451. // set header
  452. _state.request.setField(header, value);
  453. };
  454. /**
  455. * Sends the request and any associated data.
  456. *
  457. * @param data a string or Document object to send, null to send no data.
  458. */
  459. xhr.send = function(data) {
  460. // 1. if state is not OPENED or 2. send flag is true, raise
  461. // an invalid state exception
  462. if(xhr.readyState != OPENED || _state.sendFlag) {
  463. throw new Error('XHR not open or sending');
  464. }
  465. // 3. ignore data if method is GET or HEAD
  466. if(data &&
  467. _state.request.method !== 'GET' &&
  468. _state.request.method !== 'HEAD') {
  469. // handle non-IE case
  470. if(typeof(XMLSerializer) !== 'undefined') {
  471. if(data instanceof Document) {
  472. var xs = new XMLSerializer();
  473. _state.request.body = xs.serializeToString(data);
  474. } else {
  475. _state.request.body = data;
  476. }
  477. } else {
  478. // poorly implemented IE case
  479. if(typeof(data.xml) !== 'undefined') {
  480. _state.request.body = data.xml;
  481. } else {
  482. _state.request.body = data;
  483. }
  484. }
  485. }
  486. // 4. release storage mutex (not used)
  487. // 5. set error flag to false
  488. _state.errorFlag = false;
  489. // 6. if asynchronous is true (must be in this implementation)
  490. // 6.1 set send flag to true
  491. _state.sendFlag = true;
  492. // 6.2 dispatch onreadystatechange
  493. if(xhr.onreadystatechange) {
  494. xhr.onreadystatechange();
  495. }
  496. // create send options
  497. var options = {};
  498. options.request = _state.request;
  499. options.headerReady = function(e) {
  500. // make cookies available for ease of use/iteration
  501. xhr.cookies = _state.client.cookies;
  502. // TODO: update document.cookie with any cookies where the
  503. // script's domain matches
  504. // headers received
  505. xhr.readyState = HEADERS_RECEIVED;
  506. xhr.status = e.response.code;
  507. xhr.statusText = e.response.message;
  508. _state.response = e.response;
  509. if(xhr.onreadystatechange) {
  510. xhr.onreadystatechange();
  511. }
  512. if(!_state.response.aborted) {
  513. // now loading body
  514. xhr.readyState = LOADING;
  515. if(xhr.onreadystatechange) {
  516. xhr.onreadystatechange();
  517. }
  518. }
  519. };
  520. options.bodyReady = function(e) {
  521. xhr.readyState = DONE;
  522. var ct = e.response.getField('Content-Type');
  523. // Note: this null/undefined check is done outside because IE
  524. // dies otherwise on a "'null' is null" error
  525. if(ct) {
  526. if(ct.indexOf('text/xml') === 0 ||
  527. ct.indexOf('application/xml') === 0 ||
  528. ct.indexOf('+xml') !== -1) {
  529. try {
  530. var doc = new ActiveXObject('MicrosoftXMLDOM');
  531. doc.async = false;
  532. doc.loadXML(e.response.body);
  533. xhr.responseXML = doc;
  534. } catch(ex) {
  535. var parser = new DOMParser();
  536. xhr.responseXML = parser.parseFromString(ex.body, 'text/xml');
  537. }
  538. }
  539. }
  540. var length = 0;
  541. if(e.response.body !== null) {
  542. xhr.responseText = e.response.body;
  543. length = e.response.body.length;
  544. }
  545. // build logging output
  546. var req = _state.request;
  547. var output =
  548. req.method + ' ' + req.path + ' ' +
  549. xhr.status + ' ' + xhr.statusText + ' ' +
  550. length + 'B ' +
  551. (e.request.connectTime + e.request.time + e.response.time) +
  552. 'ms';
  553. var lFunc;
  554. if(options.verbose) {
  555. lFunc = (xhr.status >= 400 && options.logWarningOnError) ?
  556. _log.warning : _log.verbose;
  557. lFunc(cat, output,
  558. e, e.response.body ? '\n' + e.response.body : '\nNo content');
  559. } else {
  560. lFunc = (xhr.status >= 400 && options.logWarningOnError) ?
  561. _log.warning : _log.debug;
  562. lFunc(cat, output);
  563. }
  564. if(xhr.onreadystatechange) {
  565. xhr.onreadystatechange();
  566. }
  567. };
  568. options.error = function(e) {
  569. var req = _state.request;
  570. _log.error(cat, req.method + ' ' + req.path, e);
  571. // 1. set response body to null
  572. xhr.responseText = '';
  573. xhr.responseXML = null;
  574. // 2. set error flag to true (and reset status)
  575. _state.errorFlag = true;
  576. xhr.status = 0;
  577. xhr.statusText = '';
  578. // 3. set state to done
  579. xhr.readyState = DONE;
  580. // 4. asyc flag is always true, so dispatch onreadystatechange
  581. if(xhr.onreadystatechange) {
  582. xhr.onreadystatechange();
  583. }
  584. };
  585. // 7. send request
  586. _state.client.send(options);
  587. };
  588. /**
  589. * Aborts the request.
  590. */
  591. xhr.abort = function() {
  592. // 1. abort send
  593. // 2. stop network activity
  594. _state.request.abort();
  595. // 3. set response to null
  596. xhr.responseText = '';
  597. xhr.responseXML = null;
  598. // 4. set error flag to true (and reset status)
  599. _state.errorFlag = true;
  600. xhr.status = 0;
  601. xhr.statusText = '';
  602. // 5. clear user headers
  603. _state.request = null;
  604. _state.response = null;
  605. // 6. if state is DONE or UNSENT, or if OPENED and send flag is false
  606. if(xhr.readyState === DONE || xhr.readyState === UNSENT ||
  607. (xhr.readyState === OPENED && !_state.sendFlag)) {
  608. // 7. set ready state to unsent
  609. xhr.readyState = UNSENT;
  610. } else {
  611. // 6.1 set state to DONE
  612. xhr.readyState = DONE;
  613. // 6.2 set send flag to false
  614. _state.sendFlag = false;
  615. // 6.3 dispatch onreadystatechange
  616. if(xhr.onreadystatechange) {
  617. xhr.onreadystatechange();
  618. }
  619. // 7. set state to UNSENT
  620. xhr.readyState = UNSENT;
  621. }
  622. };
  623. /**
  624. * Gets all response headers as a string.
  625. *
  626. * @return the HTTP-encoded response header fields.
  627. */
  628. xhr.getAllResponseHeaders = function() {
  629. var rval = '';
  630. if(_state.response !== null) {
  631. var fields = _state.response.fields;
  632. $.each(fields, function(name, array) {
  633. $.each(array, function(i, value) {
  634. rval += name + ': ' + value + '\r\n';
  635. });
  636. });
  637. }
  638. return rval;
  639. };
  640. /**
  641. * Gets a single header field value or, if there are multiple
  642. * fields with the same name, a comma-separated list of header
  643. * values.
  644. *
  645. * @return the header field value(s) or null.
  646. */
  647. xhr.getResponseHeader = function(header) {
  648. var rval = null;
  649. if(_state.response !== null) {
  650. if(header in _state.response.fields) {
  651. rval = _state.response.fields[header];
  652. if(forge.util.isArray(rval)) {
  653. rval = rval.join();
  654. }
  655. }
  656. }
  657. return rval;
  658. };
  659. return xhr;
  660. };
  661. })(jQuery);