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