| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736 | /** * XmlHttpRequest implementation that uses TLS and flash SocketPool. * * @author Dave Longley * * Copyright (c) 2010-2013 Digital Bazaar, Inc. */var forge = require('./forge');require('./socket');require('./http');/* XHR API */var xhrApi = module.exports = forge.xhr = forge.xhr || {};(function($) {// logging categoryvar cat = 'forge.xhr';/*XMLHttpRequest interface definition from:http://www.w3.org/TR/XMLHttpRequestinterface XMLHttpRequest {  // event handler  attribute EventListener onreadystatechange;  // state  const unsigned short UNSENT = 0;  const unsigned short OPENED = 1;  const unsigned short HEADERS_RECEIVED = 2;  const unsigned short LOADING = 3;  const unsigned short DONE = 4;  readonly attribute unsigned short readyState;  // request  void open(in DOMString method, in DOMString url);  void open(in DOMString method, in DOMString url, in boolean async);  void open(in DOMString method, in DOMString url,            in boolean async, in DOMString user);  void open(in DOMString method, in DOMString url,            in boolean async, in DOMString user, in DOMString password);  void setRequestHeader(in DOMString header, in DOMString value);  void send();  void send(in DOMString data);  void send(in Document data);  void abort();  // response  DOMString getAllResponseHeaders();  DOMString getResponseHeader(in DOMString header);  readonly attribute DOMString responseText;  readonly attribute Document responseXML;  readonly attribute unsigned short status;  readonly attribute DOMString statusText;};*/// readyStatesvar UNSENT = 0;var OPENED = 1;var HEADERS_RECEIVED = 2;var LOADING = 3;var DONE = 4;// exceptionsvar INVALID_STATE_ERR = 11;var SYNTAX_ERR = 12;var SECURITY_ERR = 18;var NETWORK_ERR = 19;var ABORT_ERR = 20;// private flash socket pool varsvar _sp = null;var _policyPort = 0;var _policyUrl = null;// default client (used if no special URL provided when creating an XHR)var _client = null;// all clients including the default, key'd by full base url// (multiple cross-domain http clients are permitted so there may be more// than one client in this map)// TODO: provide optional clean up API for non-default clientsvar _clients = {};// the default maximum number of concurrents connections per clientvar _maxConnections = 10;var net = forge.net;var http = forge.http;/** * Initializes flash XHR support. * * @param options: *   url: the default base URL to connect to if xhr URLs are relative, *     ie: https://myserver.com. *   flashId: the dom ID of the flash SocketPool. *   policyPort: the port that provides the server's flash policy, 0 to use *     the flash default. *   policyUrl: the policy file URL to use instead of a policy port. *   msie: true if browser is internet explorer, false if not. *   connections: the maximum number of concurrent connections. *   caCerts: a list of PEM-formatted certificates to trust. *   cipherSuites: an optional array of cipher suites to use, *     see forge.tls.CipherSuites. *   verify: optional TLS certificate verify callback to use (see forge.tls *     for details). *   getCertificate: an optional callback used to get a client-side *     certificate (see forge.tls for details). *   getPrivateKey: an optional callback used to get a client-side private *     key (see forge.tls for details). *   getSignature: an optional callback used to get a client-side signature *     (see forge.tls for details). *   persistCookies: true to use persistent cookies via flash local storage, *     false to only keep cookies in javascript. *   primeTlsSockets: true to immediately connect TLS sockets on their *     creation so that they will cache TLS sessions for reuse. */xhrApi.init = function(options) {  forge.log.debug(cat, 'initializing', options);  // update default policy port and max connections  _policyPort = options.policyPort || _policyPort;  _policyUrl = options.policyUrl || _policyUrl;  _maxConnections = options.connections || _maxConnections;  // create the flash socket pool  _sp = net.createSocketPool({    flashId: options.flashId,    policyPort: _policyPort,    policyUrl: _policyUrl,    msie: options.msie || false  });  // create default http client  _client = http.createClient({    url: options.url || (      window.location.protocol + '//' + window.location.host),    socketPool: _sp,    policyPort: _policyPort,    policyUrl: _policyUrl,    connections: options.connections || _maxConnections,    caCerts: options.caCerts,    cipherSuites: options.cipherSuites,    persistCookies: options.persistCookies || true,    primeTlsSockets: options.primeTlsSockets || false,    verify: options.verify,    getCertificate: options.getCertificate,    getPrivateKey: options.getPrivateKey,    getSignature: options.getSignature  });  _clients[_client.url.full] = _client;  forge.log.debug(cat, 'ready');};/** * Called to clean up the clients and socket pool. */xhrApi.cleanup = function() {  // destroy all clients  for(var key in _clients) {    _clients[key].destroy();  }  _clients = {};  _client = null;  // destroy socket pool  _sp.destroy();  _sp = null;};/** * Sets a cookie. * * @param cookie the cookie with parameters: *   name: the name of the cookie. *   value: the value of the cookie. *   comment: an optional comment string. *   maxAge: the age of the cookie in seconds relative to created time. *   secure: true if the cookie must be sent over a secure protocol. *   httpOnly: true to restrict access to the cookie from javascript *     (inaffective since the cookies are stored in javascript). *   path: the path for the cookie. *   domain: optional domain the cookie belongs to (must start with dot). *   version: optional version of the cookie. *   created: creation time, in UTC seconds, of the cookie. */xhrApi.setCookie = function(cookie) {  // default cookie expiration to never  cookie.maxAge = cookie.maxAge || -1;  // if the cookie's domain is set, use the appropriate client  if(cookie.domain) {    // add the cookies to the applicable domains    for(var key in _clients) {      var client = _clients[key];      if(http.withinCookieDomain(client.url, cookie) &&        client.secure === cookie.secure) {        client.setCookie(cookie);      }    }  } else {    // use the default domain    // FIXME: should a null domain cookie be added to all clients? should    // this be an option?    _client.setCookie(cookie);  }};/** * Gets a cookie. * * @param name the name of the cookie. * @param path an optional path for the cookie (if there are multiple cookies *          with the same name but different paths). * @param domain an optional domain for the cookie (if not using the default *          domain). * * @return the cookie, cookies (if multiple matches), or null if not found. */xhrApi.getCookie = function(name, path, domain) {  var rval = null;  if(domain) {    // get the cookies from the applicable domains    for(var key in _clients) {      var client = _clients[key];      if(http.withinCookieDomain(client.url, domain)) {        var cookie = client.getCookie(name, path);        if(cookie !== null) {          if(rval === null) {            rval = cookie;          } else if(!forge.util.isArray(rval)) {            rval = [rval, cookie];          } else {            rval.push(cookie);          }        }      }    }  } else {    // get cookie from default domain    rval = _client.getCookie(name, path);  }  return rval;};/** * Removes a cookie. * * @param name the name of the cookie. * @param path an optional path for the cookie (if there are multiple cookies *          with the same name but different paths). * @param domain an optional domain for the cookie (if not using the default *          domain). * * @return true if a cookie was removed, false if not. */xhrApi.removeCookie = function(name, path, domain) {  var rval = false;  if(domain) {    // remove the cookies from the applicable domains    for(var key in _clients) {      var client = _clients[key];      if(http.withinCookieDomain(client.url, domain)) {        if(client.removeCookie(name, path)) {           rval = true;        }      }    }  } else {    // remove cookie from default domain    rval = _client.removeCookie(name, path);  }  return rval;};/** * Creates a new XmlHttpRequest. By default the base URL, flash policy port, * etc, will be used. However, an XHR can be created to point at another * cross-domain URL. * * @param options: *   logWarningOnError: If true and an HTTP error status code is received then *     log a warning, otherwise log a verbose message. *   verbose: If true be very verbose in the output including the response *     event and response body, otherwise only include status, timing, and *     data size. *   logError: a multi-var log function for warnings that takes the log *     category as the first var. *   logWarning: a multi-var log function for warnings that takes the log *     category as the first var. *   logDebug: a multi-var log function for warnings that takes the log *     category as the first var. *   logVerbose: a multi-var log function for warnings that takes the log *     category as the first var. *   url: the default base URL to connect to if xhr URLs are relative, *     eg: https://myserver.com, and note that the following options will be *     ignored if the URL is absent or the same as the default base URL. *   policyPort: the port that provides the server's flash policy, 0 to use *     the flash default. *   policyUrl: the policy file URL to use instead of a policy port. *   connections: the maximum number of concurrent connections. *   caCerts: a list of PEM-formatted certificates to trust. *   cipherSuites: an optional array of cipher suites to use, see *     forge.tls.CipherSuites. *   verify: optional TLS certificate verify callback to use (see forge.tls *     for details). *   getCertificate: an optional callback used to get a client-side *     certificate. *   getPrivateKey: an optional callback used to get a client-side private key. *   getSignature: an optional callback used to get a client-side signature. *   persistCookies: true to use persistent cookies via flash local storage, *     false to only keep cookies in javascript. *   primeTlsSockets: true to immediately connect TLS sockets on their *     creation so that they will cache TLS sessions for reuse. * * @return the XmlHttpRequest. */xhrApi.create = function(options) {  // set option defaults  options = $.extend({    logWarningOnError: true,    verbose: false,    logError: function() {},    logWarning: function() {},    logDebug: function() {},    logVerbose: function() {},    url: null  }, options || {});  // private xhr state  var _state = {    // the http client to use    client: null,    // request storage    request: null,    // response storage    response: null,    // asynchronous, true if doing asynchronous communication    asynchronous: true,    // sendFlag, true if send has been called    sendFlag: false,    // errorFlag, true if a network error occurred    errorFlag: false  };  // private log functions  var _log = {    error: options.logError || forge.log.error,    warning: options.logWarning || forge.log.warning,    debug: options.logDebug || forge.log.debug,    verbose: options.logVerbose || forge.log.verbose  };  // create public xhr interface  var xhr = {    // an EventListener    onreadystatechange: null,    // readonly, the current readyState    readyState: UNSENT,    // a string with the response entity-body    responseText: '',    // a Document for response entity-bodies that are XML    responseXML: null,    // readonly, returns the HTTP status code (i.e. 404)    status: 0,    // readonly, returns the HTTP status message (i.e. 'Not Found')    statusText: ''  };  // determine which http client to use  if(options.url === null) {    // use default    _state.client = _client;  } else {    var url = http.parseUrl(options.url);    if(!url) {      var error = new Error('Invalid url.');      error.details = {        url: options.url      };    }    // find client    if(url.full in _clients) {      // client found      _state.client = _clients[url.full];    } else {      // create client      _state.client = http.createClient({        url: options.url,        socketPool: _sp,        policyPort: options.policyPort || _policyPort,        policyUrl: options.policyUrl || _policyUrl,        connections: options.connections || _maxConnections,        caCerts: options.caCerts,        cipherSuites: options.cipherSuites,        persistCookies: options.persistCookies || true,        primeTlsSockets: options.primeTlsSockets || false,        verify: options.verify,        getCertificate: options.getCertificate,        getPrivateKey: options.getPrivateKey,        getSignature: options.getSignature      });      _clients[url.full] = _state.client;    }  }  /**   * Opens the request. This method will create the HTTP request to send.   *   * @param method the HTTP method (i.e. 'GET').   * @param url the relative url (the HTTP request path).   * @param async always true, ignored.   * @param user always null, ignored.   * @param password always null, ignored.   */  xhr.open = function(method, url, async, user, password) {    // 1. validate Document if one is associated    // TODO: not implemented (not used yet)    // 2. validate method token    // 3. change method to uppercase if it matches a known    // method (here we just require it to be uppercase, and    // we do not allow the standard methods)    // 4. disallow CONNECT, TRACE, or TRACK with a security error    switch(method) {    case 'DELETE':    case 'GET':    case 'HEAD':    case 'OPTIONS':    case 'PATCH':    case 'POST':    case 'PUT':      // valid method      break;    case 'CONNECT':    case 'TRACE':    case 'TRACK':      throw new Error('CONNECT, TRACE and TRACK methods are disallowed');    default:      throw new Error('Invalid method: ' + method);    }    // TODO: other validation steps in algorithm are not implemented    // 19. set send flag to false    // set response body to null    // empty list of request headers    // set request method to given method    // set request URL    // set username, password    // set asychronous flag    _state.sendFlag = false;    xhr.responseText = '';    xhr.responseXML = null;    // custom: reset status and statusText    xhr.status = 0;    xhr.statusText = '';    // create the HTTP request    _state.request = http.createRequest({      method: method,      path: url    });    // 20. set state to OPENED    xhr.readyState = OPENED;    // 21. dispatch onreadystatechange    if(xhr.onreadystatechange) {       xhr.onreadystatechange();    }  };  /**   * Adds an HTTP header field to the request.   *   * @param header the name of the header field.   * @param value the value of the header field.   */  xhr.setRequestHeader = function(header, value) {    // 1. if state is not OPENED or send flag is true, raise exception    if(xhr.readyState != OPENED || _state.sendFlag) {      throw new Error('XHR not open or sending');    }    // TODO: other validation steps in spec aren't implemented    // set header    _state.request.setField(header, value);  };  /**   * Sends the request and any associated data.   *   * @param data a string or Document object to send, null to send no data.   */  xhr.send = function(data) {    // 1. if state is not OPENED or 2. send flag is true, raise    // an invalid state exception    if(xhr.readyState != OPENED || _state.sendFlag) {      throw new Error('XHR not open or sending');    }    // 3. ignore data if method is GET or HEAD    if(data &&      _state.request.method !== 'GET' &&      _state.request.method !== 'HEAD') {      // handle non-IE case      if(typeof(XMLSerializer) !== 'undefined') {        if(data instanceof Document) {          var xs = new XMLSerializer();          _state.request.body = xs.serializeToString(data);        } else {          _state.request.body = data;        }      } else {        // poorly implemented IE case        if(typeof(data.xml) !== 'undefined') {          _state.request.body = data.xml;        } else {          _state.request.body = data;        }      }    }    // 4. release storage mutex (not used)    // 5. set error flag to false    _state.errorFlag = false;    // 6. if asynchronous is true (must be in this implementation)    // 6.1 set send flag to true    _state.sendFlag = true;    // 6.2 dispatch onreadystatechange    if(xhr.onreadystatechange) {      xhr.onreadystatechange();    }    // create send options    var options = {};    options.request = _state.request;    options.headerReady = function(e) {      // make cookies available for ease of use/iteration      xhr.cookies = _state.client.cookies;      // TODO: update document.cookie with any cookies where the      // script's domain matches      // headers received      xhr.readyState = HEADERS_RECEIVED;      xhr.status = e.response.code;      xhr.statusText = e.response.message;      _state.response = e.response;      if(xhr.onreadystatechange) {        xhr.onreadystatechange();      }      if(!_state.response.aborted) {        // now loading body        xhr.readyState = LOADING;        if(xhr.onreadystatechange) {           xhr.onreadystatechange();        }      }    };    options.bodyReady = function(e) {      xhr.readyState = DONE;      var ct = e.response.getField('Content-Type');      // Note: this null/undefined check is done outside because IE      // dies otherwise on a "'null' is null" error      if(ct) {        if(ct.indexOf('text/xml') === 0 ||          ct.indexOf('application/xml') === 0 ||          ct.indexOf('+xml') !== -1) {          try {            var doc = new ActiveXObject('MicrosoftXMLDOM');            doc.async = false;            doc.loadXML(e.response.body);            xhr.responseXML = doc;          } catch(ex) {            var parser = new DOMParser();            xhr.responseXML = parser.parseFromString(ex.body, 'text/xml');          }        }      }      var length = 0;      if(e.response.body !== null) {        xhr.responseText = e.response.body;        length = e.response.body.length;      }      // build logging output      var req = _state.request;      var output =        req.method + ' ' + req.path + ' ' +        xhr.status + ' ' + xhr.statusText + ' ' +        length + 'B ' +        (e.request.connectTime + e.request.time + e.response.time) +        'ms';      var lFunc;      if(options.verbose) {        lFunc = (xhr.status >= 400 && options.logWarningOnError) ?          _log.warning : _log.verbose;        lFunc(cat, output,          e, e.response.body ? '\n' + e.response.body : '\nNo content');      } else {        lFunc = (xhr.status >= 400 && options.logWarningOnError) ?          _log.warning : _log.debug;        lFunc(cat, output);      }      if(xhr.onreadystatechange) {        xhr.onreadystatechange();      }    };    options.error = function(e) {      var req = _state.request;      _log.error(cat, req.method + ' ' + req.path, e);      // 1. set response body to null      xhr.responseText = '';      xhr.responseXML = null;      // 2. set error flag to true (and reset status)      _state.errorFlag = true;      xhr.status = 0;      xhr.statusText = '';      // 3. set state to done      xhr.readyState = DONE;      // 4. asyc flag is always true, so dispatch onreadystatechange      if(xhr.onreadystatechange) {        xhr.onreadystatechange();      }    };    // 7. send request    _state.client.send(options);  };  /**   * Aborts the request.   */  xhr.abort = function() {    // 1. abort send    // 2. stop network activity    _state.request.abort();    // 3. set response to null    xhr.responseText = '';    xhr.responseXML = null;    // 4. set error flag to true (and reset status)    _state.errorFlag = true;    xhr.status = 0;    xhr.statusText = '';    // 5. clear user headers    _state.request = null;    _state.response = null;    // 6. if state is DONE or UNSENT, or if OPENED and send flag is false    if(xhr.readyState === DONE || xhr.readyState === UNSENT ||     (xhr.readyState === OPENED && !_state.sendFlag)) {      // 7. set ready state to unsent      xhr.readyState = UNSENT;    } else {      // 6.1 set state to DONE      xhr.readyState = DONE;      // 6.2 set send flag to false      _state.sendFlag = false;      // 6.3 dispatch onreadystatechange      if(xhr.onreadystatechange) {        xhr.onreadystatechange();      }      // 7. set state to UNSENT      xhr.readyState = UNSENT;    }  };  /**   * Gets all response headers as a string.   *   * @return the HTTP-encoded response header fields.   */  xhr.getAllResponseHeaders = function() {    var rval = '';    if(_state.response !== null) {      var fields = _state.response.fields;      $.each(fields, function(name, array) {        $.each(array, function(i, value) {          rval += name + ': ' + value + '\r\n';        });      });    }    return rval;  };  /**   * Gets a single header field value or, if there are multiple   * fields with the same name, a comma-separated list of header   * values.   *   * @return the header field value(s) or null.   */  xhr.getResponseHeader = function(header) {    var rval = null;    if(_state.response !== null) {      if(header in _state.response.fields) {        rval = _state.response.fields[header];        if(forge.util.isArray(rval)) {          rval = rval.join();        }      }    }    return rval;  };  return xhr;};})(jQuery);
 |