001: /*
002: * CookieFilter.java
003: *
004: * Brazil project web application Framework,
005: * export version: 1.1
006: * Copyright (c) 1999-2000 Sun Microsystems, Inc.
007: *
008: * Sun Public License Notice
009: *
010: * The contents of this file are subject to the Sun Public License Version
011: * 1.0 (the "License"). You may not use this file except in compliance with
012: * the License. A copy of the License is included as the file "license.terms",
013: * and also available at http://www.sun.com/
014: *
015: * The Original Code is from:
016: * Brazil project web application Framework release 1.1.
017: * The Initial Developer of the Original Code is: suhler.
018: * Portions created by suhler are Copyright (C) Sun Microsystems, Inc.
019: * All Rights Reserved.
020: *
021: * Contributor(s): cstevens, suhler.
022: *
023: * Version: 1.22
024: * Created by suhler on 99/01/27
025: * Last modified by suhler on 00/12/11 13:31:29
026: */
027:
028: package sunlabs.brazil.proxy;
029:
030: import sunlabs.brazil.filter.Filter;
031: import sunlabs.brazil.server.Request;
032: import sunlabs.brazil.server.Server;
033: import sunlabs.brazil.session.SessionManager;
034: import sunlabs.brazil.util.http.MimeHeaders;
035:
036: import java.text.SimpleDateFormat;
037: import java.text.ParseException;
038: import java.util.Date;
039: import java.io.IOException;
040: import java.io.Serializable;
041: import java.util.Enumeration;
042: import java.util.Hashtable;
043: import java.util.Properties;
044: import java.util.NoSuchElementException;
045: import java.util.StringTokenizer;
046: import java.util.Vector;
047:
048: /**
049: * The <code>CookieFilter</code> keeps a
050: * record of all the browser <i>cookies</i> associated with a given session.
051: * This can be
052: * used to make the user's cookies "mobile" as follows. A user's cookies
053: * are normally stored with the browser being used, on the user's machine.
054: * If the user runs a different browser or goes to a different machine, the
055: * user's cookies will not be there. Instead, the user can access the web
056: * via a proxy that keeps all their cookies. No matter which browser the
057: * user chooses or machine the user is at, a proxy running with the
058: * <code>CookieFilter</code> will automatically remember and add in their
059: * appropriate cookies. The <code>CookieFilter</code> also supports
060: * multiple, concurrent users, keeping each user's cookies separate.
061: * <ul>
062: * <li> All "Set-Cookie" HTTP response headers are filtered out and saved in
063: * the local storage. "Set-Cookie" headers are not transmitted back to the
064: * client.
065: *
066: * <li> Requests from the client have the appropriate "Cookie" headers added.
067: *
068: * <li> Users can retrieve, edit, and delete their own cookies.
069: *
070: * <li> JavaScript code that sets cookies is <b>not</b> handled by this
071: * filter, since the code only runs on the client computer, not on the proxy.
072: * For instance: <code>document.cookie = "userid=778287312"</code>. Any and
073: * all Javascript is passed unchanged by this filter.
074: *
075: * <li> This filter works in both a session-based and a non-session-based
076: * fashion. If sessions are used, cookies are kept with respect to the
077: * session associated with a user. If sessions are not used, all cookies
078: * are kept in one pile for all users. The latter case is valid if, say,
079: * only one user is using the proxy running the <code>CookieFilter</code>.
080: * </ul>
081: * Properties:
082: * <dl class=props>
083: * <dt>session <dd>The request property to find the session id.
084: * Defaults to "SessionID"
085: * <dt>nosession <dd>The name of the session to use if no session
086: * id is found. defaults to "common".
087: * <dt>admin <dd>A URL prefix that causes status information to
088: * be placed in the request properties.
089: * </dl>
090: *
091: * @author Stephen Uhler (stephen.uhler@sun.com)
092: * @author Colin Stevens (colin.stevens@sun.com)
093: * @version 1.22, 00/12/11
094: */
095:
096: public class CookieFilter implements Filter {
097: private static final String SESSION = "session";
098: private static final String NOSESSION = "nosession";
099: private static final String ADMIN = "admin";
100:
101: public String session = "SessionID";
102: public String nosession = "common";
103: public String admin = "";
104:
105: String prefix;
106:
107: public boolean init(Server server, String prefix) {
108: this .prefix = prefix;
109:
110: Properties props = server.props;
111: session = props.getProperty(prefix + SESSION, session);
112: nosession = props.getProperty(prefix + NOSESSION, nosession);
113: admin = props.getProperty(prefix + ADMIN, admin);
114:
115: return true;
116: }
117:
118: public boolean respond(Request request) throws IOException {
119: if ((admin != null) && request.url.startsWith(admin)) {
120: return getInfo(request);
121: }
122:
123: if (request.url.startsWith("http://")) {
124: MimeHeaders headers = request.headers;
125: for (int i = headers.size(); --i >= 0;) {
126: if ("Cookie".equalsIgnoreCase(headers.getKey(i))) {
127: headers.remove(i);
128: }
129: }
130:
131: insertCookies(request);
132: }
133: return false;
134: }
135:
136: /**
137: * Saves all "Set-Cookie" headers from the target in the client's local
138: * storage, then removes those headers before allowing the response to
139: * go back to the client. The client never sees cookies on their local
140: * machine.
141: */
142: public boolean shouldFilter(Request request, MimeHeaders headers) {
143: for (int i = headers.size(); --i >= 0;) {
144: String key = headers.getKey(i);
145: if (key.equalsIgnoreCase("Set-Cookie")) {
146: rememberCookie(request, headers.get(i));
147: headers.remove(i);
148: }
149: }
150:
151: return false;
152: }
153:
154: /**
155: * Returns the original content, since this filter does not change
156: * content; it changes the headers.
157: */
158: public byte[] filter(Request request, MimeHeaders headers,
159: byte[] content) {
160: return content;
161: }
162:
163: boolean getInfo(Request request) {
164: Hashtable h = request.getQueryData();
165:
166: Hashtable cookies = getSessionCookies(request);
167: Enumeration e = cookies.keys();
168:
169: Hashtable sites = new Hashtable();
170:
171: Properties props = request.props;
172: String pfx = prefix + "domains.";
173:
174: while (e.hasMoreElements()) {
175: Vector list = (Vector) cookies.get(e.nextElement());
176: for (int i = 0; i < list.size(); i++) {
177: CookieInfo cookie = (CookieInfo) list.elementAt(i);
178: String site = cookie.domain + cookie.path;
179:
180: int index = cookie.cookie.indexOf('=');
181:
182: String name, value;
183: if (index < 0) {
184: name = cookie.cookie;
185: value = "";
186: } else {
187: name = cookie.cookie.substring(0, index);
188: value = cookie.cookie.substring(index + 1);
189: }
190:
191: sites.put(site, site);
192:
193: site = pfx + site;
194: props.put(site, props.getProperty(site, "") + name
195: + " ");
196: props.put(site + "." + name, value);
197: }
198: }
199:
200: e = sites.keys();
201: StringBuffer sb = new StringBuffer();
202: for (int i = 0; e.hasMoreElements(); i++) {
203: sb.append(e.nextElement()).append(' ');
204: }
205: props.put(prefix + "domains", sb.toString());
206: return false;
207: }
208:
209: /*
210: * Remembers a set-cookie request from the content server
211: *
212: * look for set-cookie requests from origin server, and remember them.
213: * They will be of the form:
214: * Set-cookie: <cookie>; expires=<date>; path=<path>; domain=<domain>
215: * - ';'s are delimeters
216: * - all but <cookie> are optional
217: * - Cookies have at least on embedded '=' used for id matching
218: * - paths a prefix matches on urls (default is /)
219: * - domain is a suffix match on hostname field (default origin host)
220: * Cookies are hashed via domains, eith all cookies for a domain
221: * stored in a vector, in order of decreasing path size
222: *
223: * @param request The request object from the browser
224: * @param line the http header line containing "set-cookie".
225: */
226:
227: void rememberCookie(Request request, String line) {
228: Hashtable cookies = getSessionCookies(request);
229:
230: Url url = new Url(request.url);
231: String domain = url.domain();
232: if (domain.equals("")) {
233: domain = url.host();
234: }
235:
236: CookieInfo cookie = new CookieInfo(url, line);
237: Vector list = (Vector) cookies.get(domain);
238: if (list == null) {
239: list = new Vector();
240: cookies.put(domain, list);
241: request.log(Server.LOG_DIAGNOSTIC,
242: "Creating vector for domain: " + domain);
243: }
244:
245: int myLength = cookie.path.length();
246:
247: int i; // cookie index;
248: for (i = 0; i < list.size(); i++) {
249: CookieInfo match = (CookieInfo) list.elementAt(i);
250: if (myLength < match.path.length()) {
251: break;
252: }
253: if (cookie.isEquals(match)) {
254: list.removeElementAt(i);
255: request.log(Server.LOG_DIAGNOSTIC, "Replacing Cookie: "
256: + match);
257: break;
258: }
259: }
260: request.log(Server.LOG_DIAGNOSTIC, "Inserting cookie: "
261: + cookie);
262: list.insertElementAt(cookie, i);
263: // System.out.println("Inserting cookie (" + cookie + ") into: " + cookieList);
264: }
265:
266: void insertCookies(Request request) {
267: Url url = new Url(request.url);
268: String domain = url.domain();
269:
270: Hashtable cookies = getSessionCookies(request);
271: Vector list = (Vector) cookies.get(domain);
272: if (list == null) {
273: return;
274: }
275:
276: /*
277: * Theory: Multiple cookies that need to be sent to the same site
278: * can be send as multiple "Cookie" lines. Makes sense, since
279: * multiple "Set-Cookie" headers are sent to the client as separate
280: * lines.
281: *
282: * Practice: Some HTTP servers instead require that all the cookies
283: * are sent in a single "Cookie" line, with each cookie separated
284: * by "; ". Otherwise they don't see anything but the first or last
285: * "Cookie" line.
286: */
287: StringBuffer sb = new StringBuffer();
288:
289: Enumeration e = list.elements();
290: while (e.hasMoreElements()) {
291: CookieInfo match = (CookieInfo) e.nextElement();
292: if (match.isMatch(url)) {
293: sb.append(match.cookie);
294: sb.append("; ");
295: match.update();
296: }
297: }
298: if (sb.length() > 0) {
299: /*
300: * Remove final "; " at end of "Cookie" HTTP header. If present,
301: * it causes some HTTP servers get confused and generate an error
302: * response. Specifically, this seemed to happen to Microsoft-IIS
303: * HTTP servers when issuing a POST to an .asp file.
304: */
305:
306: sb.setLength(sb.length() - 2);
307:
308: request.headers.add("Cookie", sb.toString());
309: }
310: }
311:
312: /**
313: * Get the session's cookie table
314: */
315:
316: Hashtable getSessionCookies(Request request) {
317: String id = request.props.getProperty(session, nosession);
318: return (Hashtable) SessionManager.getSession(id,
319: CookieFilter.class, Hashtable.class);
320: }
321:
322: /**
323: * Store information about a cookie
324: */
325:
326: static class CookieInfo implements Serializable {
327: long ctime; // cookie creation time
328: long mtime; // time of last cookie use
329: long exptime; // cookie expiration time
330: int uses; // number of uses
331: String path; // the path prefix for this cookie
332: String domain; // the domain of the cookie (including port)
333: String cookie; // the value of the cookie
334: String host; // Host setting this cookie (including port)
335:
336: /**
337: * Create a cookie object.
338: * @param url: The url for this request, to fill out default stuff
339: * @param line The http line containing the cookie request
340: */
341:
342: public CookieInfo(Url url, String line) {
343: ctime = mtime = System.currentTimeMillis();
344: exptime = 0;
345: uses = 0;
346: path = "/";
347: domain = null;
348: host = null;
349: cookie = null;
350:
351: // Convert the cookie data into a hash table
352:
353: StringTokenizer st = new StringTokenizer(line, ";");
354: Hashtable cookieData = new Hashtable(5);
355: cookieData.put("value", st.nextToken().trim());
356: while (st.hasMoreTokens()) {
357: String param = st.nextToken().trim();
358: int index = param.indexOf('=');
359: if (index > 0) {
360: cookieData.put(param.substring(0, index)
361: .toLowerCase(), param.substring(index + 1));
362: } else {
363: cookieData.put(param, "");
364: }
365: }
366:
367: // validate data
368:
369: if (cookieData.containsKey("expires")) {
370: exptime = expTime((String) cookieData.get("expires"));
371: }
372:
373: String path = (String) cookieData.get("path");
374: if (path != null && !path.startsWith("/")) {
375: path = "/" + path;
376: }
377: if (path != null) {
378: this .path = path;
379: }
380:
381: // compute domain
382:
383: host = url.host();
384: String domain = (String) cookieData.get("domain");
385: if (domain != null) {
386: if (domain.indexOf(":") < 0) {
387: domain += host.substring(host.indexOf(":"));
388: }
389: if (url.domain().indexOf(domain) >= 0) {
390: this .domain = domain;
391: } else {
392: // System.out.println("Domain error, using host: " + domain + " !~ " + url.domain());
393: this .domain = host;
394: }
395: } else {
396: this .domain = host;
397: }
398: cookie = (String) cookieData.get("value");
399: }
400:
401: /**
402: * Generate the cookie info from the string representation
403: * This is called with the output of toString(). No error checking
404: */
405:
406: public CookieInfo(String value) {
407: try {
408: StringTokenizer st = new StringTokenizer(cookie);
409: host = st.nextToken();
410: domain = st.nextToken();
411: path = st.nextToken();
412: cookie = st.nextToken();
413: host = st.nextToken();
414: ctime = Long.parseLong(st.nextToken());
415: mtime = Long.parseLong(st.nextToken());
416: exptime = Long.parseLong(st.nextToken());
417: uses = Integer.parseInt(st.nextToken());
418: } catch (NumberFormatException e) {
419: throw new RuntimeException(e.toString());
420: } catch (NoSuchElementException e) {
421: throw new RuntimeException(e.toString());
422: }
423: }
424:
425: /**
426: * Generate string representation - convertable back into a cookie
427: */
428:
429: public String toString() {
430: return "<" + cookie + "> " + uses + " " + domain + path
431: + "[from " + host + "]";
432: /*
433: return host + "" + domain + " " + path + " " + cookie + " " +
434: host + " " + ctime + " " + mtime + " " + exptime + " " +
435: uses;
436: */
437: }
438:
439: /**
440: * See if url matches this cookie
441: */
442:
443: public boolean isMatch(Url url) {
444: return url.path().startsWith(path)
445: && url.host().endsWith(domain);
446: }
447:
448: public void update() {
449: mtime = System.currentTimeMillis();
450: uses++;
451: }
452:
453: /**
454: * See if cookies match
455: */
456:
457: public boolean isEquals(CookieInfo cookie) {
458: if (!cookie.domain.equals(domain)
459: || !cookie.path.equals(path)) {
460: return false;
461: }
462: int index = cookie.cookie.indexOf("=");
463: if (index >= 0
464: && index == this .cookie.indexOf("=")
465: && cookie.cookie.substring(0, index).equals(
466: this .cookie.substring(0, index))) {
467: return true;
468: } else {
469: return false;
470: }
471: }
472:
473: /**
474: * Convert cookie dates into time-stamps
475: */
476:
477: static SimpleDateFormat df = new SimpleDateFormat(
478: "EEE, dd-MMM-yy HH:mm:ss z");
479:
480: public static long expTime(String dateString) {
481: try {
482: return (df.parse(dateString)).getTime();
483: } catch (ParseException e) {
484: return 0;
485: }
486: }
487: }
488:
489: /**
490: * This does special stuff, base on our needs
491: * host: the host + port
492: * path: the path (starting with /)
493: * domain: The minimum legal cookie domain for this host
494: */
495:
496: static class Url {
497: private String host = null; // the host part (excluding port)
498: private String domain = null; // minumum domain for cookies
499: private String path = null; // path part
500: private String port; // the port #
501:
502: /**
503: * Create a url object from a url string
504: * @param url The http url string
505: * ("" if none)
506: *
507: */
508:
509: public Url(String url) {
510: String host = url.substring("http://".length());
511: int index = host.indexOf("/");
512: if (index >= 0) {
513: this .path = host.substring(index);
514: this .host = host.substring(0, index).toLowerCase();
515: } else {
516: this .path = "/";
517: this .host = host.toLowerCase();
518: }
519: index = host.indexOf(":");
520: if (index > 0) {
521: this .host = host.substring(0, index);
522: port = host.substring(index + 1);
523: } else {
524: port = "80";
525: }
526: }
527:
528: public String host() {
529: return host + ":" + port;
530: }
531:
532: public String path() {
533: return path;
534: }
535:
536: /**
537: * Retrieve the minimum domain spec for a host. Must have port info
538: * @param host The host name (e.g. foo.x.y.com:80)
539: * @returns min. cookie domain (e.g. y.com:80)
540: */
541:
542: public static Vector specialDomains = new Vector();
543: static {
544: specialDomains.addElement("com");
545: specialDomains.addElement("edu");
546: specialDomains.addElement("net");
547: specialDomains.addElement("org");
548: specialDomains.addElement("gov");
549: specialDomains.addElement("mil");
550: specialDomains.addElement("int");
551: }
552:
553: /**
554: * Compute the Minimum cookie domain.
555: * @param default The string to tack onto the host name,
556: * if it only has 1 dot
557: */
558:
559: public String domain() {
560: if (domain != null) {
561: return domain;
562: }
563: StringTokenizer st = new StringTokenizer(host, ".");
564: int dots = st.countTokens() - 1;
565: // System.out.println("COMPUTING DOMAIN FOR: " + host + " (" + dots + ") dots");
566: switch (dots) {
567: case 0: // no dots - invalid domain
568: case 1: // 1 dot - invalid domain
569: domain = "";
570: System.out.println("Domain error: " + host);
571: break;
572: case 2: // OK if special suffix
573: st.nextToken();
574: String middle = st.nextToken();
575: String right = st.nextToken();
576: if (specialDomains.contains(right)) {
577: domain = "." + middle + "." + right + ":" + port;
578: } else {
579: System.out.println("Domain error: " + host);
580: domain = "";
581: }
582: break;
583: default: // 3 or more dots, always ok, leave .xxx.yyy.zzz
584: while (dots-- > 2) {
585: st.nextToken();
586: }
587: domain = "";
588: String maybe = st.nextToken();
589: String next = "";
590: while (st.hasMoreTokens()) {
591: next = st.nextToken();
592: domain += "." + next;
593: }
594: domain += ":" + port;
595: if (!specialDomains.contains(next)) {
596: domain = "." + maybe + domain;
597: }
598: }
599: return domain;
600: }
601:
602: public String toString() {
603: return host + path + " (" + domain + ")";
604: }
605: }
606: }
607:
608: /*
609:
610: Initial sign-on stragegy
611: 1) have proxy-authentication
612: a) have hashtable entry - > ok
613: b) don't have hash-table entry ->ask for re-authentication
614: 2) go to web-server sign-on page
615: - fill out form userid/password
616: a) unused ID - make entry in cookie table, explain how to do proxy
617: b) used ID - make-em use another one
618:
619: Misc
620: - keep used id's in a separate hash table
621:
622: Investigate: application/x-ns-proxy-autoconfig for setting
623: proxy automatically
624:
625: function FindProxyForURL(url, host) {
626: return "PROXY ???.Sun.COM:8080;"
627: }
628: */
|