Spaces:
Configuration error
Configuration error
/** | |
* 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.origin] = _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; | |
try { | |
url = new URL(options.url); | |
} catch(e) { | |
var error = new Error('Invalid url.'); | |
error.details = { | |
url: options.url | |
}; | |
} | |
// find client | |
if(url.origin in _clients) { | |
// client found | |
_state.client = _clients[url.origin]; | |
} 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.origin] = _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); | |