source: frontend/node_modules/node-forge/lib/xhr.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

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