source: trip-planner-front/node_modules/node-forge/lib/http.js@ eed0bf8

Last change on this file since eed0bf8 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 38.5 KB
RevLine 
[6a3a178]1/**
2 * HTTP client-side implementation that uses forge.net sockets.
3 *
4 * @author Dave Longley
5 *
6 * Copyright (c) 2010-2014 Digital Bazaar, Inc. All rights reserved.
7 */
8var forge = require('./forge');
9require('./debug');
10require('./tls');
11require('./util');
12
13// define http namespace
14var http = module.exports = forge.http = forge.http || {};
15
16// logging category
17var cat = 'forge.http';
18
19// add array of clients to debug storage
20if(forge.debug) {
21 forge.debug.set('forge.http', 'clients', []);
22}
23
24// normalizes an http header field name
25var _normalize = function(name) {
26 return name.toLowerCase().replace(/(^.)|(-.)/g,
27 function(a) {return a.toUpperCase();});
28};
29
30/**
31 * Gets the local storage ID for the given client.
32 *
33 * @param client the client to get the local storage ID for.
34 *
35 * @return the local storage ID to use.
36 */
37var _getStorageId = function(client) {
38 // TODO: include browser in ID to avoid sharing cookies between
39 // browsers (if this is undesirable)
40 // navigator.userAgent
41 return 'forge.http.' +
42 client.url.scheme + '.' +
43 client.url.host + '.' +
44 client.url.port;
45};
46
47/**
48 * Loads persistent cookies from disk for the given client.
49 *
50 * @param client the client.
51 */
52var _loadCookies = function(client) {
53 if(client.persistCookies) {
54 try {
55 var cookies = forge.util.getItem(
56 client.socketPool.flashApi,
57 _getStorageId(client), 'cookies');
58 client.cookies = cookies || {};
59 } catch(ex) {
60 // no flash storage available, just silently fail
61 // TODO: i assume we want this logged somewhere or
62 // should it actually generate an error
63 //forge.log.error(cat, ex);
64 }
65 }
66};
67
68/**
69 * Saves persistent cookies on disk for the given client.
70 *
71 * @param client the client.
72 */
73var _saveCookies = function(client) {
74 if(client.persistCookies) {
75 try {
76 forge.util.setItem(
77 client.socketPool.flashApi,
78 _getStorageId(client), 'cookies', client.cookies);
79 } catch(ex) {
80 // no flash storage available, just silently fail
81 // TODO: i assume we want this logged somewhere or
82 // should it actually generate an error
83 //forge.log.error(cat, ex);
84 }
85 }
86
87 // FIXME: remove me
88 _loadCookies(client);
89};
90
91/**
92 * Clears persistent cookies on disk for the given client.
93 *
94 * @param client the client.
95 */
96var _clearCookies = function(client) {
97 if(client.persistCookies) {
98 try {
99 // only thing stored is 'cookies', so clear whole storage
100 forge.util.clearItems(
101 client.socketPool.flashApi,
102 _getStorageId(client));
103 } catch(ex) {
104 // no flash storage available, just silently fail
105 // TODO: i assume we want this logged somewhere or
106 // should it actually generate an error
107 //forge.log.error(cat, ex);
108 }
109 }
110};
111
112/**
113 * Connects and sends a request.
114 *
115 * @param client the http client.
116 * @param socket the socket to use.
117 */
118var _doRequest = function(client, socket) {
119 if(socket.isConnected()) {
120 // already connected
121 socket.options.request.connectTime = +new Date();
122 socket.connected({
123 type: 'connect',
124 id: socket.id
125 });
126 } else {
127 // connect
128 socket.options.request.connectTime = +new Date();
129 socket.connect({
130 host: client.url.host,
131 port: client.url.port,
132 policyPort: client.policyPort,
133 policyUrl: client.policyUrl
134 });
135 }
136};
137
138/**
139 * Handles the next request or marks a socket as idle.
140 *
141 * @param client the http client.
142 * @param socket the socket.
143 */
144var _handleNextRequest = function(client, socket) {
145 // clear buffer
146 socket.buffer.clear();
147
148 // get pending request
149 var pending = null;
150 while(pending === null && client.requests.length > 0) {
151 pending = client.requests.shift();
152 if(pending.request.aborted) {
153 pending = null;
154 }
155 }
156
157 // mark socket idle if no pending requests
158 if(pending === null) {
159 if(socket.options !== null) {
160 socket.options = null;
161 }
162 client.idle.push(socket);
163 } else {
164 // handle pending request, allow 1 retry
165 socket.retries = 1;
166 socket.options = pending;
167 _doRequest(client, socket);
168 }
169};
170
171/**
172 * Sets up a socket for use with an http client.
173 *
174 * @param client the parent http client.
175 * @param socket the socket to set up.
176 * @param tlsOptions if the socket must use TLS, the TLS options.
177 */
178var _initSocket = function(client, socket, tlsOptions) {
179 // no socket options yet
180 socket.options = null;
181
182 // set up handlers
183 socket.connected = function(e) {
184 // socket primed by caching TLS session, handle next request
185 if(socket.options === null) {
186 _handleNextRequest(client, socket);
187 } else {
188 // socket in use
189 var request = socket.options.request;
190 request.connectTime = +new Date() - request.connectTime;
191 e.socket = socket;
192 socket.options.connected(e);
193 if(request.aborted) {
194 socket.close();
195 } else {
196 var out = request.toString();
197 if(request.body) {
198 out += request.body;
199 }
200 request.time = +new Date();
201 socket.send(out);
202 request.time = +new Date() - request.time;
203 socket.options.response.time = +new Date();
204 socket.sending = true;
205 }
206 }
207 };
208 socket.closed = function(e) {
209 if(socket.sending) {
210 socket.sending = false;
211 if(socket.retries > 0) {
212 --socket.retries;
213 _doRequest(client, socket);
214 } else {
215 // error, closed during send
216 socket.error({
217 id: socket.id,
218 type: 'ioError',
219 message: 'Connection closed during send. Broken pipe.',
220 bytesAvailable: 0
221 });
222 }
223 } else {
224 // handle unspecified content-length transfer
225 var response = socket.options.response;
226 if(response.readBodyUntilClose) {
227 response.time = +new Date() - response.time;
228 response.bodyReceived = true;
229 socket.options.bodyReady({
230 request: socket.options.request,
231 response: response,
232 socket: socket
233 });
234 }
235 socket.options.closed(e);
236 _handleNextRequest(client, socket);
237 }
238 };
239 socket.data = function(e) {
240 socket.sending = false;
241 var request = socket.options.request;
242 if(request.aborted) {
243 socket.close();
244 } else {
245 // receive all bytes available
246 var response = socket.options.response;
247 var bytes = socket.receive(e.bytesAvailable);
248 if(bytes !== null) {
249 // receive header and then body
250 socket.buffer.putBytes(bytes);
251 if(!response.headerReceived) {
252 response.readHeader(socket.buffer);
253 if(response.headerReceived) {
254 socket.options.headerReady({
255 request: socket.options.request,
256 response: response,
257 socket: socket
258 });
259 }
260 }
261 if(response.headerReceived && !response.bodyReceived) {
262 response.readBody(socket.buffer);
263 }
264 if(response.bodyReceived) {
265 socket.options.bodyReady({
266 request: socket.options.request,
267 response: response,
268 socket: socket
269 });
270 // close connection if requested or by default on http/1.0
271 var value = response.getField('Connection') || '';
272 if(value.indexOf('close') != -1 ||
273 (response.version === 'HTTP/1.0' &&
274 response.getField('Keep-Alive') === null)) {
275 socket.close();
276 } else {
277 _handleNextRequest(client, socket);
278 }
279 }
280 }
281 }
282 };
283 socket.error = function(e) {
284 // do error callback, include request
285 socket.options.error({
286 type: e.type,
287 message: e.message,
288 request: socket.options.request,
289 response: socket.options.response,
290 socket: socket
291 });
292 socket.close();
293 };
294
295 // wrap socket for TLS
296 if(tlsOptions) {
297 socket = forge.tls.wrapSocket({
298 sessionId: null,
299 sessionCache: {},
300 caStore: tlsOptions.caStore,
301 cipherSuites: tlsOptions.cipherSuites,
302 socket: socket,
303 virtualHost: tlsOptions.virtualHost,
304 verify: tlsOptions.verify,
305 getCertificate: tlsOptions.getCertificate,
306 getPrivateKey: tlsOptions.getPrivateKey,
307 getSignature: tlsOptions.getSignature,
308 deflate: tlsOptions.deflate || null,
309 inflate: tlsOptions.inflate || null
310 });
311
312 socket.options = null;
313 socket.buffer = forge.util.createBuffer();
314 client.sockets.push(socket);
315 if(tlsOptions.prime) {
316 // prime socket by connecting and caching TLS session, will do
317 // next request from there
318 socket.connect({
319 host: client.url.host,
320 port: client.url.port,
321 policyPort: client.policyPort,
322 policyUrl: client.policyUrl
323 });
324 } else {
325 // do not prime socket, just add as idle
326 client.idle.push(socket);
327 }
328 } else {
329 // no need to prime non-TLS sockets
330 socket.buffer = forge.util.createBuffer();
331 client.sockets.push(socket);
332 client.idle.push(socket);
333 }
334};
335
336/**
337 * Checks to see if the given cookie has expired. If the cookie's max-age
338 * plus its created time is less than the time now, it has expired, unless
339 * its max-age is set to -1 which indicates it will never expire.
340 *
341 * @param cookie the cookie to check.
342 *
343 * @return true if it has expired, false if not.
344 */
345var _hasCookieExpired = function(cookie) {
346 var rval = false;
347
348 if(cookie.maxAge !== -1) {
349 var now = _getUtcTime(new Date());
350 var expires = cookie.created + cookie.maxAge;
351 if(expires <= now) {
352 rval = true;
353 }
354 }
355
356 return rval;
357};
358
359/**
360 * Adds cookies in the given client to the given request.
361 *
362 * @param client the client.
363 * @param request the request.
364 */
365var _writeCookies = function(client, request) {
366 var expired = [];
367 var url = client.url;
368 var cookies = client.cookies;
369 for(var name in cookies) {
370 // get cookie paths
371 var paths = cookies[name];
372 for(var p in paths) {
373 var cookie = paths[p];
374 if(_hasCookieExpired(cookie)) {
375 // store for clean up
376 expired.push(cookie);
377 } else if(request.path.indexOf(cookie.path) === 0) {
378 // path or path's ancestor must match cookie.path
379 request.addCookie(cookie);
380 }
381 }
382 }
383
384 // clean up expired cookies
385 for(var i = 0; i < expired.length; ++i) {
386 var cookie = expired[i];
387 client.removeCookie(cookie.name, cookie.path);
388 }
389};
390
391/**
392 * Gets cookies from the given response and adds the to the given client.
393 *
394 * @param client the client.
395 * @param response the response.
396 */
397var _readCookies = function(client, response) {
398 var cookies = response.getCookies();
399 for(var i = 0; i < cookies.length; ++i) {
400 try {
401 client.setCookie(cookies[i]);
402 } catch(ex) {
403 // ignore failure to add other-domain, etc. cookies
404 }
405 }
406};
407
408/**
409 * Creates an http client that uses forge.net sockets as a backend and
410 * forge.tls for security.
411 *
412 * @param options:
413 * url: the url to connect to (scheme://host:port).
414 * socketPool: the flash socket pool to use.
415 * policyPort: the flash policy port to use (if other than the
416 * socket pool default), use 0 for flash default.
417 * policyUrl: the flash policy file URL to use (if provided will
418 * be used instead of a policy port).
419 * connections: number of connections to use to handle requests.
420 * caCerts: an array of certificates to trust for TLS, certs may
421 * be PEM-formatted or cert objects produced via forge.pki.
422 * cipherSuites: an optional array of cipher suites to use,
423 * see forge.tls.CipherSuites.
424 * virtualHost: the virtual server name to use in a TLS SNI
425 * extension, if not provided the url host will be used.
426 * verify: a custom TLS certificate verify callback to use.
427 * getCertificate: an optional callback used to get a client-side
428 * certificate (see forge.tls for details).
429 * getPrivateKey: an optional callback used to get a client-side
430 * private key (see forge.tls for details).
431 * getSignature: an optional callback used to get a client-side
432 * signature (see forge.tls for details).
433 * persistCookies: true to use persistent cookies via flash local
434 * storage, false to only keep cookies in javascript.
435 * primeTlsSockets: true to immediately connect TLS sockets on
436 * their creation so that they will cache TLS sessions for reuse.
437 *
438 * @return the client.
439 */
440http.createClient = function(options) {
441 // create CA store to share with all TLS connections
442 var caStore = null;
443 if(options.caCerts) {
444 caStore = forge.pki.createCaStore(options.caCerts);
445 }
446
447 // get scheme, host, and port from url
448 options.url = (options.url ||
449 window.location.protocol + '//' + window.location.host);
450 var url = http.parseUrl(options.url);
451 if(!url) {
452 var error = new Error('Invalid url.');
453 error.details = {url: options.url};
454 throw error;
455 }
456
457 // default to 1 connection
458 options.connections = options.connections || 1;
459
460 // create client
461 var sp = options.socketPool;
462 var client = {
463 // url
464 url: url,
465 // socket pool
466 socketPool: sp,
467 // the policy port to use
468 policyPort: options.policyPort,
469 // policy url to use
470 policyUrl: options.policyUrl,
471 // queue of requests to service
472 requests: [],
473 // all sockets
474 sockets: [],
475 // idle sockets
476 idle: [],
477 // whether or not the connections are secure
478 secure: (url.scheme === 'https'),
479 // cookie jar (key'd off of name and then path, there is only 1 domain
480 // and one setting for secure per client so name+path is unique)
481 cookies: {},
482 // default to flash storage of cookies
483 persistCookies: (typeof(options.persistCookies) === 'undefined') ?
484 true : options.persistCookies
485 };
486
487 // add client to debug storage
488 if(forge.debug) {
489 forge.debug.get('forge.http', 'clients').push(client);
490 }
491
492 // load cookies from disk
493 _loadCookies(client);
494
495 /**
496 * A default certificate verify function that checks a certificate common
497 * name against the client's URL host.
498 *
499 * @param c the TLS connection.
500 * @param verified true if cert is verified, otherwise alert number.
501 * @param depth the chain depth.
502 * @param certs the cert chain.
503 *
504 * @return true if verified and the common name matches the host, error
505 * otherwise.
506 */
507 var _defaultCertificateVerify = function(c, verified, depth, certs) {
508 if(depth === 0 && verified === true) {
509 // compare common name to url host
510 var cn = certs[depth].subject.getField('CN');
511 if(cn === null || client.url.host !== cn.value) {
512 verified = {
513 message: 'Certificate common name does not match url host.'
514 };
515 }
516 }
517 return verified;
518 };
519
520 // determine if TLS is used
521 var tlsOptions = null;
522 if(client.secure) {
523 tlsOptions = {
524 caStore: caStore,
525 cipherSuites: options.cipherSuites || null,
526 virtualHost: options.virtualHost || url.host,
527 verify: options.verify || _defaultCertificateVerify,
528 getCertificate: options.getCertificate || null,
529 getPrivateKey: options.getPrivateKey || null,
530 getSignature: options.getSignature || null,
531 prime: options.primeTlsSockets || false
532 };
533
534 // if socket pool uses a flash api, then add deflate support to TLS
535 if(sp.flashApi !== null) {
536 tlsOptions.deflate = function(bytes) {
537 // strip 2 byte zlib header and 4 byte trailer
538 return forge.util.deflate(sp.flashApi, bytes, true);
539 };
540 tlsOptions.inflate = function(bytes) {
541 return forge.util.inflate(sp.flashApi, bytes, true);
542 };
543 }
544 }
545
546 // create and initialize sockets
547 for(var i = 0; i < options.connections; ++i) {
548 _initSocket(client, sp.createSocket(), tlsOptions);
549 }
550
551 /**
552 * Sends a request. A method 'abort' will be set on the request that
553 * can be called to attempt to abort the request.
554 *
555 * @param options:
556 * request: the request to send.
557 * connected: a callback for when the connection is open.
558 * closed: a callback for when the connection is closed.
559 * headerReady: a callback for when the response header arrives.
560 * bodyReady: a callback for when the response body arrives.
561 * error: a callback for if an error occurs.
562 */
563 client.send = function(options) {
564 // add host header if not set
565 if(options.request.getField('Host') === null) {
566 options.request.setField('Host', client.url.fullHost);
567 }
568
569 // set default dummy handlers
570 var opts = {};
571 opts.request = options.request;
572 opts.connected = options.connected || function() {};
573 opts.closed = options.close || function() {};
574 opts.headerReady = function(e) {
575 // read cookies
576 _readCookies(client, e.response);
577 if(options.headerReady) {
578 options.headerReady(e);
579 }
580 };
581 opts.bodyReady = options.bodyReady || function() {};
582 opts.error = options.error || function() {};
583
584 // create response
585 opts.response = http.createResponse();
586 opts.response.time = 0;
587 opts.response.flashApi = client.socketPool.flashApi;
588 opts.request.flashApi = client.socketPool.flashApi;
589
590 // create abort function
591 opts.request.abort = function() {
592 // set aborted, clear handlers
593 opts.request.aborted = true;
594 opts.connected = function() {};
595 opts.closed = function() {};
596 opts.headerReady = function() {};
597 opts.bodyReady = function() {};
598 opts.error = function() {};
599 };
600
601 // add cookies to request
602 _writeCookies(client, opts.request);
603
604 // queue request options if there are no idle sockets
605 if(client.idle.length === 0) {
606 client.requests.push(opts);
607 } else {
608 // use an idle socket, prefer an idle *connected* socket first
609 var socket = null;
610 var len = client.idle.length;
611 for(var i = 0; socket === null && i < len; ++i) {
612 socket = client.idle[i];
613 if(socket.isConnected()) {
614 client.idle.splice(i, 1);
615 } else {
616 socket = null;
617 }
618 }
619 // no connected socket available, get unconnected socket
620 if(socket === null) {
621 socket = client.idle.pop();
622 }
623 socket.options = opts;
624 _doRequest(client, socket);
625 }
626 };
627
628 /**
629 * Destroys this client.
630 */
631 client.destroy = function() {
632 // clear pending requests, close and destroy sockets
633 client.requests = [];
634 for(var i = 0; i < client.sockets.length; ++i) {
635 client.sockets[i].close();
636 client.sockets[i].destroy();
637 }
638 client.socketPool = null;
639 client.sockets = [];
640 client.idle = [];
641 };
642
643 /**
644 * Sets a cookie for use with all connections made by this client. Any
645 * cookie with the same name will be replaced. If the cookie's value
646 * is undefined, null, or the blank string, the cookie will be removed.
647 *
648 * If the cookie's domain doesn't match this client's url host or the
649 * cookie's secure flag doesn't match this client's url scheme, then
650 * setting the cookie will fail with an exception.
651 *
652 * @param cookie the cookie with parameters:
653 * name: the name of the cookie.
654 * value: the value of the cookie.
655 * comment: an optional comment string.
656 * maxAge: the age of the cookie in seconds relative to created time.
657 * secure: true if the cookie must be sent over a secure protocol.
658 * httpOnly: true to restrict access to the cookie from javascript
659 * (inaffective since the cookies are stored in javascript).
660 * path: the path for the cookie.
661 * domain: optional domain the cookie belongs to (must start with dot).
662 * version: optional version of the cookie.
663 * created: creation time, in UTC seconds, of the cookie.
664 */
665 client.setCookie = function(cookie) {
666 var rval;
667 if(typeof(cookie.name) !== 'undefined') {
668 if(cookie.value === null || typeof(cookie.value) === 'undefined' ||
669 cookie.value === '') {
670 // remove cookie
671 rval = client.removeCookie(cookie.name, cookie.path);
672 } else {
673 // set cookie defaults
674 cookie.comment = cookie.comment || '';
675 cookie.maxAge = cookie.maxAge || 0;
676 cookie.secure = (typeof(cookie.secure) === 'undefined') ?
677 true : cookie.secure;
678 cookie.httpOnly = cookie.httpOnly || true;
679 cookie.path = cookie.path || '/';
680 cookie.domain = cookie.domain || null;
681 cookie.version = cookie.version || null;
682 cookie.created = _getUtcTime(new Date());
683
684 // do secure check
685 if(cookie.secure !== client.secure) {
686 var error = new Error('Http client url scheme is incompatible ' +
687 'with cookie secure flag.');
688 error.url = client.url;
689 error.cookie = cookie;
690 throw error;
691 }
692 // make sure url host is within cookie.domain
693 if(!http.withinCookieDomain(client.url, cookie)) {
694 var error = new Error('Http client url scheme is incompatible ' +
695 'with cookie secure flag.');
696 error.url = client.url;
697 error.cookie = cookie;
698 throw error;
699 }
700
701 // add new cookie
702 if(!(cookie.name in client.cookies)) {
703 client.cookies[cookie.name] = {};
704 }
705 client.cookies[cookie.name][cookie.path] = cookie;
706 rval = true;
707
708 // save cookies
709 _saveCookies(client);
710 }
711 }
712
713 return rval;
714 };
715
716 /**
717 * Gets a cookie by its name.
718 *
719 * @param name the name of the cookie to retrieve.
720 * @param path an optional path for the cookie (if there are multiple
721 * cookies with the same name but different paths).
722 *
723 * @return the cookie or null if not found.
724 */
725 client.getCookie = function(name, path) {
726 var rval = null;
727 if(name in client.cookies) {
728 var paths = client.cookies[name];
729
730 // get path-specific cookie
731 if(path) {
732 if(path in paths) {
733 rval = paths[path];
734 }
735 } else {
736 // get first cookie
737 for(var p in paths) {
738 rval = paths[p];
739 break;
740 }
741 }
742 }
743 return rval;
744 };
745
746 /**
747 * Removes a cookie.
748 *
749 * @param name the name of the cookie to remove.
750 * @param path an optional path for the cookie (if there are multiple
751 * cookies with the same name but different paths).
752 *
753 * @return true if a cookie was removed, false if not.
754 */
755 client.removeCookie = function(name, path) {
756 var rval = false;
757 if(name in client.cookies) {
758 // delete the specific path
759 if(path) {
760 var paths = client.cookies[name];
761 if(path in paths) {
762 rval = true;
763 delete client.cookies[name][path];
764 // clean up entry if empty
765 var empty = true;
766 for(var i in client.cookies[name]) {
767 empty = false;
768 break;
769 }
770 if(empty) {
771 delete client.cookies[name];
772 }
773 }
774 } else {
775 // delete all cookies with the given name
776 rval = true;
777 delete client.cookies[name];
778 }
779 }
780 if(rval) {
781 // save cookies
782 _saveCookies(client);
783 }
784 return rval;
785 };
786
787 /**
788 * Clears all cookies stored in this client.
789 */
790 client.clearCookies = function() {
791 client.cookies = {};
792 _clearCookies(client);
793 };
794
795 if(forge.log) {
796 forge.log.debug('forge.http', 'created client', options);
797 }
798
799 return client;
800};
801
802/**
803 * Trims the whitespace off of the beginning and end of a string.
804 *
805 * @param str the string to trim.
806 *
807 * @return the trimmed string.
808 */
809var _trimString = function(str) {
810 return str.replace(/^\s*/, '').replace(/\s*$/, '');
811};
812
813/**
814 * Creates an http header object.
815 *
816 * @return the http header object.
817 */
818var _createHeader = function() {
819 var header = {
820 fields: {},
821 setField: function(name, value) {
822 // normalize field name, trim value
823 header.fields[_normalize(name)] = [_trimString('' + value)];
824 },
825 appendField: function(name, value) {
826 name = _normalize(name);
827 if(!(name in header.fields)) {
828 header.fields[name] = [];
829 }
830 header.fields[name].push(_trimString('' + value));
831 },
832 getField: function(name, index) {
833 var rval = null;
834 name = _normalize(name);
835 if(name in header.fields) {
836 index = index || 0;
837 rval = header.fields[name][index];
838 }
839 return rval;
840 }
841 };
842 return header;
843};
844
845/**
846 * Gets the time in utc seconds given a date.
847 *
848 * @param d the date to use.
849 *
850 * @return the time in utc seconds.
851 */
852var _getUtcTime = function(d) {
853 var utc = +d + d.getTimezoneOffset() * 60000;
854 return Math.floor(+new Date() / 1000);
855};
856
857/**
858 * Creates an http request.
859 *
860 * @param options:
861 * version: the version.
862 * method: the method.
863 * path: the path.
864 * body: the body.
865 * headers: custom header fields to add,
866 * eg: [{'Content-Length': 0}].
867 *
868 * @return the http request.
869 */
870http.createRequest = function(options) {
871 options = options || {};
872 var request = _createHeader();
873 request.version = options.version || 'HTTP/1.1';
874 request.method = options.method || null;
875 request.path = options.path || null;
876 request.body = options.body || null;
877 request.bodyDeflated = false;
878 request.flashApi = null;
879
880 // add custom headers
881 var headers = options.headers || [];
882 if(!forge.util.isArray(headers)) {
883 headers = [headers];
884 }
885 for(var i = 0; i < headers.length; ++i) {
886 for(var name in headers[i]) {
887 request.appendField(name, headers[i][name]);
888 }
889 }
890
891 /**
892 * Adds a cookie to the request 'Cookie' header.
893 *
894 * @param cookie a cookie to add.
895 */
896 request.addCookie = function(cookie) {
897 var value = '';
898 var field = request.getField('Cookie');
899 if(field !== null) {
900 // separate cookies by semi-colons
901 value = field + '; ';
902 }
903
904 // get current time in utc seconds
905 var now = _getUtcTime(new Date());
906
907 // output cookie name and value
908 value += cookie.name + '=' + cookie.value;
909 request.setField('Cookie', value);
910 };
911
912 /**
913 * Converts an http request into a string that can be sent as an
914 * HTTP request. Does not include any data.
915 *
916 * @return the string representation of the request.
917 */
918 request.toString = function() {
919 /* Sample request header:
920 GET /some/path/?query HTTP/1.1
921 Host: www.someurl.com
922 Connection: close
923 Accept-Encoding: deflate
924 Accept: image/gif, text/html
925 User-Agent: Mozilla 4.0
926 */
927
928 // set default headers
929 if(request.getField('User-Agent') === null) {
930 request.setField('User-Agent', 'forge.http 1.0');
931 }
932 if(request.getField('Accept') === null) {
933 request.setField('Accept', '*/*');
934 }
935 if(request.getField('Connection') === null) {
936 request.setField('Connection', 'keep-alive');
937 request.setField('Keep-Alive', '115');
938 }
939
940 // add Accept-Encoding if not specified
941 if(request.flashApi !== null &&
942 request.getField('Accept-Encoding') === null) {
943 request.setField('Accept-Encoding', 'deflate');
944 }
945
946 // if the body isn't null, deflate it if its larger than 100 bytes
947 if(request.flashApi !== null && request.body !== null &&
948 request.getField('Content-Encoding') === null &&
949 !request.bodyDeflated && request.body.length > 100) {
950 // use flash to compress data
951 request.body = forge.util.deflate(request.flashApi, request.body);
952 request.bodyDeflated = true;
953 request.setField('Content-Encoding', 'deflate');
954 request.setField('Content-Length', request.body.length);
955 } else if(request.body !== null) {
956 // set content length for body
957 request.setField('Content-Length', request.body.length);
958 }
959
960 // build start line
961 var rval =
962 request.method.toUpperCase() + ' ' + request.path + ' ' +
963 request.version + '\r\n';
964
965 // add each header
966 for(var name in request.fields) {
967 var fields = request.fields[name];
968 for(var i = 0; i < fields.length; ++i) {
969 rval += name + ': ' + fields[i] + '\r\n';
970 }
971 }
972 // final terminating CRLF
973 rval += '\r\n';
974
975 return rval;
976 };
977
978 return request;
979};
980
981/**
982 * Creates an empty http response header.
983 *
984 * @return the empty http response header.
985 */
986http.createResponse = function() {
987 // private vars
988 var _first = true;
989 var _chunkSize = 0;
990 var _chunksFinished = false;
991
992 // create response
993 var response = _createHeader();
994 response.version = null;
995 response.code = 0;
996 response.message = null;
997 response.body = null;
998 response.headerReceived = false;
999 response.bodyReceived = false;
1000 response.flashApi = null;
1001
1002 /**
1003 * Reads a line that ends in CRLF from a byte buffer.
1004 *
1005 * @param b the byte buffer.
1006 *
1007 * @return the line or null if none was found.
1008 */
1009 var _readCrlf = function(b) {
1010 var line = null;
1011 var i = b.data.indexOf('\r\n', b.read);
1012 if(i != -1) {
1013 // read line, skip CRLF
1014 line = b.getBytes(i - b.read);
1015 b.getBytes(2);
1016 }
1017 return line;
1018 };
1019
1020 /**
1021 * Parses a header field and appends it to the response.
1022 *
1023 * @param line the header field line.
1024 */
1025 var _parseHeader = function(line) {
1026 var tmp = line.indexOf(':');
1027 var name = line.substring(0, tmp++);
1028 response.appendField(
1029 name, (tmp < line.length) ? line.substring(tmp) : '');
1030 };
1031
1032 /**
1033 * Reads an http response header from a buffer of bytes.
1034 *
1035 * @param b the byte buffer to parse the header from.
1036 *
1037 * @return true if the whole header was read, false if not.
1038 */
1039 response.readHeader = function(b) {
1040 // read header lines (each ends in CRLF)
1041 var line = '';
1042 while(!response.headerReceived && line !== null) {
1043 line = _readCrlf(b);
1044 if(line !== null) {
1045 // parse first line
1046 if(_first) {
1047 _first = false;
1048 var tmp = line.split(' ');
1049 if(tmp.length >= 3) {
1050 response.version = tmp[0];
1051 response.code = parseInt(tmp[1], 10);
1052 response.message = tmp.slice(2).join(' ');
1053 } else {
1054 // invalid header
1055 var error = new Error('Invalid http response header.');
1056 error.details = {'line': line};
1057 throw error;
1058 }
1059 } else if(line.length === 0) {
1060 // handle final line, end of header
1061 response.headerReceived = true;
1062 } else {
1063 _parseHeader(line);
1064 }
1065 }
1066 }
1067
1068 return response.headerReceived;
1069 };
1070
1071 /**
1072 * Reads some chunked http response entity-body from the given buffer of
1073 * bytes.
1074 *
1075 * @param b the byte buffer to read from.
1076 *
1077 * @return true if the whole body was read, false if not.
1078 */
1079 var _readChunkedBody = function(b) {
1080 /* Chunked transfer-encoding sends data in a series of chunks,
1081 followed by a set of 0-N http trailers.
1082 The format is as follows:
1083
1084 chunk-size (in hex) CRLF
1085 chunk data (with "chunk-size" many bytes) CRLF
1086 ... (N many chunks)
1087 chunk-size (of 0 indicating the last chunk) CRLF
1088 N many http trailers followed by CRLF
1089 blank line + CRLF (terminates the trailers)
1090
1091 If there are no http trailers, then after the chunk-size of 0,
1092 there is still a single CRLF (indicating the blank line + CRLF
1093 that terminates the trailers). In other words, you always terminate
1094 the trailers with blank line + CRLF, regardless of 0-N trailers. */
1095
1096 /* From RFC-2616, section 3.6.1, here is the pseudo-code for
1097 implementing chunked transfer-encoding:
1098
1099 length := 0
1100 read chunk-size, chunk-extension (if any) and CRLF
1101 while (chunk-size > 0) {
1102 read chunk-data and CRLF
1103 append chunk-data to entity-body
1104 length := length + chunk-size
1105 read chunk-size and CRLF
1106 }
1107 read entity-header
1108 while (entity-header not empty) {
1109 append entity-header to existing header fields
1110 read entity-header
1111 }
1112 Content-Length := length
1113 Remove "chunked" from Transfer-Encoding
1114 */
1115
1116 var line = '';
1117 while(line !== null && b.length() > 0) {
1118 // if in the process of reading a chunk
1119 if(_chunkSize > 0) {
1120 // if there are not enough bytes to read chunk and its
1121 // trailing CRLF, we must wait for more data to be received
1122 if(_chunkSize + 2 > b.length()) {
1123 break;
1124 }
1125
1126 // read chunk data, skip CRLF
1127 response.body += b.getBytes(_chunkSize);
1128 b.getBytes(2);
1129 _chunkSize = 0;
1130 } else if(!_chunksFinished) {
1131 // more chunks, read next chunk-size line
1132 line = _readCrlf(b);
1133 if(line !== null) {
1134 // parse chunk-size (ignore any chunk extension)
1135 _chunkSize = parseInt(line.split(';', 1)[0], 16);
1136 _chunksFinished = (_chunkSize === 0);
1137 }
1138 } else {
1139 // chunks finished, read next trailer
1140 line = _readCrlf(b);
1141 while(line !== null) {
1142 if(line.length > 0) {
1143 // parse trailer
1144 _parseHeader(line);
1145 // read next trailer
1146 line = _readCrlf(b);
1147 } else {
1148 // body received
1149 response.bodyReceived = true;
1150 line = null;
1151 }
1152 }
1153 }
1154 }
1155
1156 return response.bodyReceived;
1157 };
1158
1159 /**
1160 * Reads an http response body from a buffer of bytes.
1161 *
1162 * @param b the byte buffer to read from.
1163 *
1164 * @return true if the whole body was read, false if not.
1165 */
1166 response.readBody = function(b) {
1167 var contentLength = response.getField('Content-Length');
1168 var transferEncoding = response.getField('Transfer-Encoding');
1169 if(contentLength !== null) {
1170 contentLength = parseInt(contentLength);
1171 }
1172
1173 // read specified length
1174 if(contentLength !== null && contentLength >= 0) {
1175 response.body = response.body || '';
1176 response.body += b.getBytes(contentLength);
1177 response.bodyReceived = (response.body.length === contentLength);
1178 } else if(transferEncoding !== null) {
1179 // read chunked encoding
1180 if(transferEncoding.indexOf('chunked') != -1) {
1181 response.body = response.body || '';
1182 _readChunkedBody(b);
1183 } else {
1184 var error = new Error('Unknown Transfer-Encoding.');
1185 error.details = {'transferEncoding': transferEncoding};
1186 throw error;
1187 }
1188 } else if((contentLength !== null && contentLength < 0) ||
1189 (contentLength === null &&
1190 response.getField('Content-Type') !== null)) {
1191 // read all data in the buffer
1192 response.body = response.body || '';
1193 response.body += b.getBytes();
1194 response.readBodyUntilClose = true;
1195 } else {
1196 // no body
1197 response.body = null;
1198 response.bodyReceived = true;
1199 }
1200
1201 if(response.bodyReceived) {
1202 response.time = +new Date() - response.time;
1203 }
1204
1205 if(response.flashApi !== null &&
1206 response.bodyReceived && response.body !== null &&
1207 response.getField('Content-Encoding') === 'deflate') {
1208 // inflate using flash api
1209 response.body = forge.util.inflate(
1210 response.flashApi, response.body);
1211 }
1212
1213 return response.bodyReceived;
1214 };
1215
1216 /**
1217 * Parses an array of cookies from the 'Set-Cookie' field, if present.
1218 *
1219 * @return the array of cookies.
1220 */
1221 response.getCookies = function() {
1222 var rval = [];
1223
1224 // get Set-Cookie field
1225 if('Set-Cookie' in response.fields) {
1226 var field = response.fields['Set-Cookie'];
1227
1228 // get current local time in seconds
1229 var now = +new Date() / 1000;
1230
1231 // regex for parsing 'name1=value1; name2=value2; name3'
1232 var regex = /\s*([^=]*)=?([^;]*)(;|$)/g;
1233
1234 // examples:
1235 // Set-Cookie: cookie1_name=cookie1_value; max-age=0; path=/
1236 // Set-Cookie: c2=v2; expires=Thu, 21-Aug-2008 23:47:25 GMT; path=/
1237 for(var i = 0; i < field.length; ++i) {
1238 var fv = field[i];
1239 var m;
1240 regex.lastIndex = 0;
1241 var first = true;
1242 var cookie = {};
1243 do {
1244 m = regex.exec(fv);
1245 if(m !== null) {
1246 var name = _trimString(m[1]);
1247 var value = _trimString(m[2]);
1248
1249 // cookie_name=value
1250 if(first) {
1251 cookie.name = name;
1252 cookie.value = value;
1253 first = false;
1254 } else {
1255 // property_name=value
1256 name = name.toLowerCase();
1257 switch(name) {
1258 case 'expires':
1259 // replace hyphens w/spaces so date will parse
1260 value = value.replace(/-/g, ' ');
1261 var secs = Date.parse(value) / 1000;
1262 cookie.maxAge = Math.max(0, secs - now);
1263 break;
1264 case 'max-age':
1265 cookie.maxAge = parseInt(value, 10);
1266 break;
1267 case 'secure':
1268 cookie.secure = true;
1269 break;
1270 case 'httponly':
1271 cookie.httpOnly = true;
1272 break;
1273 default:
1274 if(name !== '') {
1275 cookie[name] = value;
1276 }
1277 }
1278 }
1279 }
1280 } while(m !== null && m[0] !== '');
1281 rval.push(cookie);
1282 }
1283 }
1284
1285 return rval;
1286 };
1287
1288 /**
1289 * Converts an http response into a string that can be sent as an
1290 * HTTP response. Does not include any data.
1291 *
1292 * @return the string representation of the response.
1293 */
1294 response.toString = function() {
1295 /* Sample response header:
1296 HTTP/1.0 200 OK
1297 Host: www.someurl.com
1298 Connection: close
1299 */
1300
1301 // build start line
1302 var rval =
1303 response.version + ' ' + response.code + ' ' + response.message + '\r\n';
1304
1305 // add each header
1306 for(var name in response.fields) {
1307 var fields = response.fields[name];
1308 for(var i = 0; i < fields.length; ++i) {
1309 rval += name + ': ' + fields[i] + '\r\n';
1310 }
1311 }
1312 // final terminating CRLF
1313 rval += '\r\n';
1314
1315 return rval;
1316 };
1317
1318 return response;
1319};
1320
1321/**
1322 * Parses the scheme, host, and port from an http(s) url.
1323 *
1324 * @param str the url string.
1325 *
1326 * @return the parsed url object or null if the url is invalid.
1327 */
1328http.parseUrl = forge.util.parseUrl;
1329
1330/**
1331 * Returns true if the given url is within the given cookie's domain.
1332 *
1333 * @param url the url to check.
1334 * @param cookie the cookie or cookie domain to check.
1335 */
1336http.withinCookieDomain = function(url, cookie) {
1337 var rval = false;
1338
1339 // cookie may be null, a cookie object, or a domain string
1340 var domain = (cookie === null || typeof cookie === 'string') ?
1341 cookie : cookie.domain;
1342
1343 // any domain will do
1344 if(domain === null) {
1345 rval = true;
1346 } else if(domain.charAt(0) === '.') {
1347 // ensure domain starts with a '.'
1348 // parse URL as necessary
1349 if(typeof url === 'string') {
1350 url = http.parseUrl(url);
1351 }
1352
1353 // add '.' to front of URL host to match against domain
1354 var host = '.' + url.host;
1355
1356 // if the host ends with domain then it falls within it
1357 var idx = host.lastIndexOf(domain);
1358 if(idx !== -1 && (idx + domain.length === host.length)) {
1359 rval = true;
1360 }
1361 }
1362
1363 return rval;
1364};
Note: See TracBrowser for help on using the repository browser.