/* * SessionFilter.java * * Brazil project web application toolkit, * export version: 2.3 * Copyright (c) 2000-2008 Sun Microsystems, Inc. * * Sun Public License Notice * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is included as the file "license.terms", * and also available at http://www.sun.com/ * * The Original Code is from: * Brazil project web application toolkit release 2.3. * The Initial Developer of the Original Code is: suhler. * Portions created by suhler are Copyright (C) Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): suhler. * * Version: 2.6 * Created by suhler on 00/11/04 * Last modified by suhler on 08/12/19 10:45:52 * * Version Histories: * * 2.6 08/12/19-10:45:52 (suhler) * remove unused instance variable * * 2.5 08/02/15-16:17:57 (suhler) * Accommodate the filterHandler dealing with suffixes now * * 2.4 05/07/12-10:36:42 (suhler) * add "https" support * * 2.3 04/11/30-15:19:39 (suhler) * fixed sccs version string * * 2.2 02/11/05-10:43:59 (suhler) * Merged changes between child workspace "/home/suhler/brazil/naws" and * parent workspace "/net/mack.eng/export/ws/brazil/naws". * * 1.17.1.1 02/11/05-10:42:33 (suhler) * remove purious diagnostic output * * 2.1 02/10/01-16:39:05 (suhler) * version change * * 1.17 02/07/24-10:44:14 (suhler) * doc updates * * 1.16 02/06/24-15:51:31 (suhler) * change prefix to cookiePrefix * * 1.15 02/06/23-20:57:02 (suhler) * add cookie prefix * * 1.14 02/05/10-15:14:32 (suhler) * use util.Guid for cookie name generation * * 1.13 02/05/09-09:53:16 (suhler) * Make invented session-i's more unique * * 1.12 02/01/29-14:39:40 (suhler) * doc lint * * 1.11 01/12/10-13:54:54 (suhler) * lint * * 1.10 01/07/20-10:38:30 (suhler) * don't ses session if it already exists * * 1.9 01/01/14-14:52:53 (suhler) * lint * * 1.8 00/12/11-13:26:30 (suhler) * add class=props for automatic property extraction * * 1.7 00/11/27-22:36:47 (suhler) * make another attempt at suffix matching. * * 1.6 00/11/20-13:21:16 (suhler) * doc fixes * * 1.5 00/11/17-09:32:40 (suhler) * remove diagnostics * * 1.4 00/11/15-09:47:24 (suhler) * temporary fix for suffix matching * * 1.3 00/11/05-20:34:12 (suhler) * Reorganized the code. Handler redirects better, so URL's look better * * 1.2 00/11/05-17:00:34 (suhler) * checkpoint * * 1.2 00/11/04-19:28:40 (Codemgr) * SunPro Code Manager data about conflicts, renames, etc... * Name history : 1 0 filter/SessionFilter.java * * 1.1 00/11/04-19:28:39 (suhler) * date and time created 00/11/04 19:28:39 by suhler * */ package sunlabs.brazil.filter; import java.io.IOException; import java.util.Properties; import sunlabs.brazil.handler.MapPage; import sunlabs.brazil.server.Request; import sunlabs.brazil.server.Server; import sunlabs.brazil.util.Guid; import sunlabs.brazil.util.http.MimeHeaders; import sunlabs.brazil.util.regexp.Regexp; /** * Filter to manage browser sessions using browser cookies or URL * rewriting as needed. * This should be used as the last filter in the filter chain. * It attempts to use browser cookies. If they don't work, * it rewrites the URL's instead, tacking the session info onto * the end of the URL. *

* This Filter works by first examining the request as a handler. * If the request contains an ID, either in the "browser cookie" or * written into the URL, the session ID is extracted. * In the id-in-the-url case, the ID is removed from the URL. When called * later as a filter, the SessionFilter rewrites all relevent URL's in the page * to incorporate the ID. *

* If an ID can't be found either in the cookie or URL, a couple * the session creation sequence starts. * First, the browser is send a "set-cookie" request along with a redirect * that contains the cookie value encoded into the redirected URL. * When the browser follows the redirect, the request is examined to se * if the cookie value was sent. If so, the browser is redirected back * to the original URL, and normal "cookie" processing takes place. * If no cookie is found, the browser is redirected back to the * original URL, modified to embed the ID into it, and normal URL * session rewriting takes place. *

* The following server properties are used: *

*
cookie *
The name of the cookie to use (defaults to "cookie"). * If the name is "none", then no cookies are used. Instead, * session rewriting will occur for every session. *
session *
The name of the request property that the Session ID will be stored * in, to be passed to downstream handler. The default value is * "SessionID". If the session property is set, and not empty, * then no processing is done. *
persist
If set, cookies persist across browser sessions. * If cookies are disabled, no persistence is available. *
cookiePrefix
The URL prefix for which the cookie applies. * Defaults to "/". *
suffix
A regular expression that matches url suffix we process. * Defaults to html|xml|txt, or to the directory * default (e.g. a URL that ends in "/"). Note, this overrides * the suffix handling done by MatchString() in the FilterHandler. *
* The Following request properties are set: *
*
gotCookie *
An id was retrieved out of a cookie header *
UrlID *
Set to the string tacked onto the end of each URL, if * session ID's are managed by URL rewriting. If cookies are used, this * is set to the empty string. *
* @author Stephen Uhler * @version 2.6 */ public class SessionFilter implements Filter { private static final String SESSION = "session"; private static final String COOKIE = "cookie"; private static final String PERSIST = "persist"; private static final String SUFFIX = "suffix"; private static final String URLPREFIX = "cookiePrefix"; public String session = "SessionID"; public String cookieName = "cookie"; public String urlSep = ",id="; // delimeter between url and ID public String redirectToken; // magic to put in url to redirect public String encoding = "UrlID"; // set if url encoding is in effect public boolean persist; // if true, make cookies last. private String propsPrefix; // properties prefix Regexp cookieExp; // exp to match cookie in mime header Regexp suffixExp; // exp to match suffix String urlPrefix; // url prefix for cookie to match on public boolean init(Server server, String propsPrefix) { this.propsPrefix = propsPrefix; Properties props = server.props; session = props.getProperty(propsPrefix + SESSION, session); cookieName = props.getProperty(propsPrefix + COOKIE, cookieName); persist = props.getProperty(propsPrefix + PERSIST) != null; urlPrefix = props.getProperty(propsPrefix + URLPREFIX, "/"); /* XXX * The code should be re-arranged so the url id is stripped * off before we do the suffix check. Then this won't look * quite so ugly. */ String suffix = props.getProperty(propsPrefix + SUFFIX,"html|txt|xml"); cookieExp = new Regexp("(^|; *)" + cookieName + "=([^;]*)"); suffixExp = new Regexp("(\\.(" + suffix + ")|/)$"); redirectToken = cookieName + ","; server.log(Server.LOG_DIAGNOSTIC, propsPrefix, "suffix:" + suffix); /* XXX * Suffixes are now handled by MatchString() in the FilterHandler, * but the behaviour here is different, so for backward compatibility, * turn off the MatchString suffix handling, and do it here instead. */ props.put(propsPrefix + SUFFIX, ""); return true; } /** * This is called by the filterHandler before the content generation * step. It is responsible for extracting the session information, * then (if required) restoring the URL's to their original form. * It tries relatively hard to use cookies if they are available * through a series or redirects. */ public boolean respond(Request request) throws IOException { String current = request.props.getProperty(session); if (current != null && !current.equals("")) { request.log(Server.LOG_INFORMATIONAL, propsPrefix, session + " already exists, skipping"); return false; } /* * See if the query contains our redirect token. if so, strip it * off, and set the "redirected" flag. */ boolean haveRedirect = request.query.startsWith(redirectToken); if (haveRedirect) { request.query = request.query.substring(redirectToken.length()); request.log(Server.LOG_DIAGNOSTIC, propsPrefix, "Found and removing Redirect token"); } String id = fetchCookie(request); /* * If we got the cookie during the redirect, redirect back * to clean up the url. */ if (haveRedirect && id != null) { redirect(request); return true; } /* * If we have the cookie, set the session id and return. */ if (id != null) { request.props.put("gotCookie", "true"); request.props.put(session, id); request.log(Server.LOG_DIAGNOSTIC, propsPrefix, "Found cookie (" + cookieName + ") = " + id); return false; } /* * No cookie, see if the id is encoded into the URL. * For now, we'll encode the session info into the URL so * that /foo/bar.html becomes /foo/bar.html,id=nnn. * If we found the id in the url, and the redirect token, redirect * again to remove the redirect token, but leave in the session info. * We could try to prevent spoofing; Later. */ int sepIndex = request.url.indexOf(urlSep); if (sepIndex > 0) { id = request.url.substring(sepIndex + urlSep.length()); request.url = request.url.substring(0, sepIndex); request.props.put(encoding, urlSep + id); request.props.put(session, id); request.log(Server.LOG_DIAGNOSTIC, propsPrefix, "Found id in url=" + id); return false; } /* * If this isn't a suffix that needs an ID, then don't bother */ if (suffixExp.match(request.url) == null) { request.log(Server.LOG_DIAGNOSTIC, propsPrefix, "Invalid suffix for " + request.url); request.props.remove(encoding); return false; } /* * No id in either cookie or Url, so we need to make one up. * If this is the first use, try to set a cookie and redirect * back here. We'll add some magic onto the end of the Url * as part of the query string so we know we tried to set a cookie. * If we already tried that, then tack the session id onto the * url, and redirect again. */ id = Guid.getString(); if (haveRedirect) { request.log(Server.LOG_DIAGNOSTIC, propsPrefix, "refusing cookies, set url encoding=" + id); request.url += urlSep + id; } else { String expire = ""; if (persist) { expire="; EXPIRES=Fri, 01-Jan-25 00:00:00 GMT"; } if (!cookieName.equals("none")) { request.addHeader("Set-Cookie", cookieName + "=" + id + "; PATH=" + urlPrefix + expire); } if (request.query.equals("")) { request.query = redirectToken; } else { request.query = redirectToken + request.query; } } redirect(request); return true; } /** * Do a redirect back to ourselves. */ private void redirect(Request request) throws IOException { String target; if (request.method.equals("GET") && !request.query.equals("")) { target = request.url + "?" + request.query; } else { target = request.url; } request.log(Server.LOG_DIAGNOSTIC, propsPrefix, "Redirect to: " + target); request.redirect(target, null); } /** * Try to Get the cookie out of the http header. Cookies can be * on separate lines or all combined on one line. * When multiple cookies are on one line, they are separated * by ';' characters. * * @return The cookie value, or null if not found. */ String fetchCookie(Request request) { String id = null; String[] subs = new String[3]; MimeHeaders headers = request.headers; for (int i = headers.size(); --i >= 0; ) { if (headers.getKey(i).equalsIgnoreCase("Cookie") == false) { continue; } request.log(Server.LOG_DIAGNOSTIC, propsPrefix, "Examining cookie header: " + headers.get(i)); if (cookieExp.match(headers.get(i), subs)) { id = subs[2]; if (id.length() == 0) { id = null; } request.log(Server.LOG_DIAGNOSTIC, propsPrefix, "Got cookie id: " + id); break; } } return id; } /** * We have the results, only filter if html and we're rewriting */ public boolean shouldFilter(Request request, MimeHeaders headers) { String type = headers.get("content-type"); boolean shouldEncode = (request.props.getProperty(encoding) != null); return (shouldEncode && type != null && type.toLowerCase().startsWith("text/")); } /** * Rewrite all the url's, adding the session id to the end */ public byte[] filter(Request request, MimeHeaders headers, byte[] content) { String id = request.props.getProperty(session); if (id == null) { return content; } String type = headers.get("content-type"); if (!type.toLowerCase().startsWith("text/html")) { return content; } Map map = new Map(urlSep + id, suffixExp); return map.convertHtml(new String(content)).getBytes(); } /** * The mapPage class was designed for virtual web page re-mapping, * but it knows which entity attributes are URL's. * We use it for session attachment instead. */ private static class Map extends MapPage { public static boolean debug = false; // turn on for diagnostics Regexp re; Map(String s, Regexp re) { super(s); this.re = re; } /** * If the url doesn't start with http://, tack the session info * on to the end of the URL. This can fail if the absolute * URL resolves back to us XXX. */ public String convertString(String fix) { if (fix.startsWith("http://") || fix.startsWith("https://")) { return null; } int index = fix.indexOf('#'); if (index < 0) { index = fix.indexOf('?'); } if (index == 0) { return null; } else if (index < 0) { index = fix.length(); } String urlPart = fix.substring(0, index); if (re.match(urlPart) != null) { String result = urlPart + prefix + fix.substring(index); debug(" converting: " + fix + " => " + result); return result; } else { debug(" Not converting: " + urlPart); return null; } } private void debug(String s) { if (debug) { System.err.println(s); } } } }