|
|
- /**
- * 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 category
- var cat = 'forge.xhr';
-
- /*
- XMLHttpRequest interface definition from:
- http://www.w3.org/TR/XMLHttpRequest
-
- interface 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;
- };
- */
-
- // readyStates
- var UNSENT = 0;
- var OPENED = 1;
- var HEADERS_RECEIVED = 2;
- var LOADING = 3;
- var DONE = 4;
-
- // exceptions
- var INVALID_STATE_ERR = 11;
- var SYNTAX_ERR = 12;
- var SECURITY_ERR = 18;
- var NETWORK_ERR = 19;
- var ABORT_ERR = 20;
-
- // private flash socket pool vars
- var _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 clients
- var _clients = {};
-
- // the default maximum number of concurrents connections per client
- var _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);
|