xhr.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  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.origin] = _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;
  349. try {
  350. url = new URL(options.url);
  351. } catch(e) {
  352. var error = new Error('Invalid url.');
  353. error.details = {
  354. url: options.url
  355. };
  356. }
  357. // find client
  358. if(url.origin in _clients) {
  359. // client found
  360. _state.client = _clients[url.origin];
  361. } else {
  362. // create client
  363. _state.client = http.createClient({
  364. url: options.url,
  365. socketPool: _sp,
  366. policyPort: options.policyPort || _policyPort,
  367. policyUrl: options.policyUrl || _policyUrl,
  368. connections: options.connections || _maxConnections,
  369. caCerts: options.caCerts,
  370. cipherSuites: options.cipherSuites,
  371. persistCookies: options.persistCookies || true,
  372. primeTlsSockets: options.primeTlsSockets || false,
  373. verify: options.verify,
  374. getCertificate: options.getCertificate,
  375. getPrivateKey: options.getPrivateKey,
  376. getSignature: options.getSignature
  377. });
  378. _clients[url.origin] = _state.client;
  379. }
  380. }
  381. /**
  382. * Opens the request. This method will create the HTTP request to send.
  383. *
  384. * @param method the HTTP method (i.e. 'GET').
  385. * @param url the relative url (the HTTP request path).
  386. * @param async always true, ignored.
  387. * @param user always null, ignored.
  388. * @param password always null, ignored.
  389. */
  390. xhr.open = function(method, url, async, user, password) {
  391. // 1. validate Document if one is associated
  392. // TODO: not implemented (not used yet)
  393. // 2. validate method token
  394. // 3. change method to uppercase if it matches a known
  395. // method (here we just require it to be uppercase, and
  396. // we do not allow the standard methods)
  397. // 4. disallow CONNECT, TRACE, or TRACK with a security error
  398. switch(method) {
  399. case 'DELETE':
  400. case 'GET':
  401. case 'HEAD':
  402. case 'OPTIONS':
  403. case 'PATCH':
  404. case 'POST':
  405. case 'PUT':
  406. // valid method
  407. break;
  408. case 'CONNECT':
  409. case 'TRACE':
  410. case 'TRACK':
  411. throw new Error('CONNECT, TRACE and TRACK methods are disallowed');
  412. default:
  413. throw new Error('Invalid method: ' + method);
  414. }
  415. // TODO: other validation steps in algorithm are not implemented
  416. // 19. set send flag to false
  417. // set response body to null
  418. // empty list of request headers
  419. // set request method to given method
  420. // set request URL
  421. // set username, password
  422. // set asychronous flag
  423. _state.sendFlag = false;
  424. xhr.responseText = '';
  425. xhr.responseXML = null;
  426. // custom: reset status and statusText
  427. xhr.status = 0;
  428. xhr.statusText = '';
  429. // create the HTTP request
  430. _state.request = http.createRequest({
  431. method: method,
  432. path: url
  433. });
  434. // 20. set state to OPENED
  435. xhr.readyState = OPENED;
  436. // 21. dispatch onreadystatechange
  437. if(xhr.onreadystatechange) {
  438. xhr.onreadystatechange();
  439. }
  440. };
  441. /**
  442. * Adds an HTTP header field to the request.
  443. *
  444. * @param header the name of the header field.
  445. * @param value the value of the header field.
  446. */
  447. xhr.setRequestHeader = function(header, value) {
  448. // 1. if state is not OPENED or send flag is true, raise exception
  449. if(xhr.readyState != OPENED || _state.sendFlag) {
  450. throw new Error('XHR not open or sending');
  451. }
  452. // TODO: other validation steps in spec aren't implemented
  453. // set header
  454. _state.request.setField(header, value);
  455. };
  456. /**
  457. * Sends the request and any associated data.
  458. *
  459. * @param data a string or Document object to send, null to send no data.
  460. */
  461. xhr.send = function(data) {
  462. // 1. if state is not OPENED or 2. send flag is true, raise
  463. // an invalid state exception
  464. if(xhr.readyState != OPENED || _state.sendFlag) {
  465. throw new Error('XHR not open or sending');
  466. }
  467. // 3. ignore data if method is GET or HEAD
  468. if(data &&
  469. _state.request.method !== 'GET' &&
  470. _state.request.method !== 'HEAD') {
  471. // handle non-IE case
  472. if(typeof(XMLSerializer) !== 'undefined') {
  473. if(data instanceof Document) {
  474. var xs = new XMLSerializer();
  475. _state.request.body = xs.serializeToString(data);
  476. } else {
  477. _state.request.body = data;
  478. }
  479. } else {
  480. // poorly implemented IE case
  481. if(typeof(data.xml) !== 'undefined') {
  482. _state.request.body = data.xml;
  483. } else {
  484. _state.request.body = data;
  485. }
  486. }
  487. }
  488. // 4. release storage mutex (not used)
  489. // 5. set error flag to false
  490. _state.errorFlag = false;
  491. // 6. if asynchronous is true (must be in this implementation)
  492. // 6.1 set send flag to true
  493. _state.sendFlag = true;
  494. // 6.2 dispatch onreadystatechange
  495. if(xhr.onreadystatechange) {
  496. xhr.onreadystatechange();
  497. }
  498. // create send options
  499. var options = {};
  500. options.request = _state.request;
  501. options.headerReady = function(e) {
  502. // make cookies available for ease of use/iteration
  503. xhr.cookies = _state.client.cookies;
  504. // TODO: update document.cookie with any cookies where the
  505. // script's domain matches
  506. // headers received
  507. xhr.readyState = HEADERS_RECEIVED;
  508. xhr.status = e.response.code;
  509. xhr.statusText = e.response.message;
  510. _state.response = e.response;
  511. if(xhr.onreadystatechange) {
  512. xhr.onreadystatechange();
  513. }
  514. if(!_state.response.aborted) {
  515. // now loading body
  516. xhr.readyState = LOADING;
  517. if(xhr.onreadystatechange) {
  518. xhr.onreadystatechange();
  519. }
  520. }
  521. };
  522. options.bodyReady = function(e) {
  523. xhr.readyState = DONE;
  524. var ct = e.response.getField('Content-Type');
  525. // Note: this null/undefined check is done outside because IE
  526. // dies otherwise on a "'null' is null" error
  527. if(ct) {
  528. if(ct.indexOf('text/xml') === 0 ||
  529. ct.indexOf('application/xml') === 0 ||
  530. ct.indexOf('+xml') !== -1) {
  531. try {
  532. var doc = new ActiveXObject('MicrosoftXMLDOM');
  533. doc.async = false;
  534. doc.loadXML(e.response.body);
  535. xhr.responseXML = doc;
  536. } catch(ex) {
  537. var parser = new DOMParser();
  538. xhr.responseXML = parser.parseFromString(ex.body, 'text/xml');
  539. }
  540. }
  541. }
  542. var length = 0;
  543. if(e.response.body !== null) {
  544. xhr.responseText = e.response.body;
  545. length = e.response.body.length;
  546. }
  547. // build logging output
  548. var req = _state.request;
  549. var output =
  550. req.method + ' ' + req.path + ' ' +
  551. xhr.status + ' ' + xhr.statusText + ' ' +
  552. length + 'B ' +
  553. (e.request.connectTime + e.request.time + e.response.time) +
  554. 'ms';
  555. var lFunc;
  556. if(options.verbose) {
  557. lFunc = (xhr.status >= 400 && options.logWarningOnError) ?
  558. _log.warning : _log.verbose;
  559. lFunc(cat, output,
  560. e, e.response.body ? '\n' + e.response.body : '\nNo content');
  561. } else {
  562. lFunc = (xhr.status >= 400 && options.logWarningOnError) ?
  563. _log.warning : _log.debug;
  564. lFunc(cat, output);
  565. }
  566. if(xhr.onreadystatechange) {
  567. xhr.onreadystatechange();
  568. }
  569. };
  570. options.error = function(e) {
  571. var req = _state.request;
  572. _log.error(cat, req.method + ' ' + req.path, e);
  573. // 1. set response body to null
  574. xhr.responseText = '';
  575. xhr.responseXML = null;
  576. // 2. set error flag to true (and reset status)
  577. _state.errorFlag = true;
  578. xhr.status = 0;
  579. xhr.statusText = '';
  580. // 3. set state to done
  581. xhr.readyState = DONE;
  582. // 4. asyc flag is always true, so dispatch onreadystatechange
  583. if(xhr.onreadystatechange) {
  584. xhr.onreadystatechange();
  585. }
  586. };
  587. // 7. send request
  588. _state.client.send(options);
  589. };
  590. /**
  591. * Aborts the request.
  592. */
  593. xhr.abort = function() {
  594. // 1. abort send
  595. // 2. stop network activity
  596. _state.request.abort();
  597. // 3. set response to null
  598. xhr.responseText = '';
  599. xhr.responseXML = null;
  600. // 4. set error flag to true (and reset status)
  601. _state.errorFlag = true;
  602. xhr.status = 0;
  603. xhr.statusText = '';
  604. // 5. clear user headers
  605. _state.request = null;
  606. _state.response = null;
  607. // 6. if state is DONE or UNSENT, or if OPENED and send flag is false
  608. if(xhr.readyState === DONE || xhr.readyState === UNSENT ||
  609. (xhr.readyState === OPENED && !_state.sendFlag)) {
  610. // 7. set ready state to unsent
  611. xhr.readyState = UNSENT;
  612. } else {
  613. // 6.1 set state to DONE
  614. xhr.readyState = DONE;
  615. // 6.2 set send flag to false
  616. _state.sendFlag = false;
  617. // 6.3 dispatch onreadystatechange
  618. if(xhr.onreadystatechange) {
  619. xhr.onreadystatechange();
  620. }
  621. // 7. set state to UNSENT
  622. xhr.readyState = UNSENT;
  623. }
  624. };
  625. /**
  626. * Gets all response headers as a string.
  627. *
  628. * @return the HTTP-encoded response header fields.
  629. */
  630. xhr.getAllResponseHeaders = function() {
  631. var rval = '';
  632. if(_state.response !== null) {
  633. var fields = _state.response.fields;
  634. $.each(fields, function(name, array) {
  635. $.each(array, function(i, value) {
  636. rval += name + ': ' + value + '\r\n';
  637. });
  638. });
  639. }
  640. return rval;
  641. };
  642. /**
  643. * Gets a single header field value or, if there are multiple
  644. * fields with the same name, a comma-separated list of header
  645. * values.
  646. *
  647. * @return the header field value(s) or null.
  648. */
  649. xhr.getResponseHeader = function(header) {
  650. var rval = null;
  651. if(_state.response !== null) {
  652. if(header in _state.response.fields) {
  653. rval = _state.response.fields[header];
  654. if(forge.util.isArray(rval)) {
  655. rval = rval.join();
  656. }
  657. }
  658. }
  659. return rval;
  660. };
  661. return xhr;
  662. };
  663. })(jQuery);