001: /*
002: * SessionFilter.java
003: *
004: * Brazil project web application Framework,
005: * export version: 1.1
006: * Copyright (c) 2000-2001 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): suhler.
022: *
023: * Version: 1.9
024: * Created by suhler on 00/11/04
025: * Last modified by suhler on 01/01/14 14:52:53
026: */
027:
028: package sunlabs.brazil.filter;
029:
030: import java.io.IOException;
031: import java.util.Properties;
032: import java.util.Random;
033: import sunlabs.brazil.handler.MapPage;
034: import sunlabs.brazil.server.Request;
035: import sunlabs.brazil.server.Server;
036: import sunlabs.brazil.util.http.MimeHeaders;
037: import sunlabs.brazil.util.regexp.Regexp;
038:
039: /**
040: * Filter to manage browser sessions.
041: * This should be used as the last filter in the filter chain.
042: * It attempts to use browser cookies. If they don't work,
043: * it rewrites the URL's instead, tacking the session info onto
044: * the end of the URL.
045: * <p>
046: * The following server properties are used:
047: * <dl class=props>
048: * <dt> cookie
049: * <dd> The name of the cookie to use (defaults to "cookie").
050: * If the name is "none", then no cookies are used. Instead,
051: * session rewriting will occur for every session.
052: * <dt> session
053: * <dd> The name of the request property that the Session ID will be stored
054: * in, to be passed to downstream handler. The default value is
055: * "SessionID".
056: * <dt>persist <dd> If set, cookies persist across browser sessions.
057: * If cookies are disabled, no persistence is available.
058: * <dt>suffix <dd> A regular expression that matches url suffix we process.
059: * Defaults to <code>html|xml|txt</code>.
060: * </dl>
061: * The Following request properties are set:
062: * <dl>
063: * <dt>gotCookie
064: * <dd>An id was retrieved out of a cookie header
065: * <dt>UrlID
066: * <dd>Set to the string tacked onto the end of each URL, if
067: * session ID's are managed by URL rewriting.
068: * </dl>
069: * @author Stephen Uhler
070: * @version 1.9, 01/01/14
071: */
072:
073: public class SessionFilter implements Filter {
074: private static final String SESSION = "session";
075: private static final String COOKIE = "cookie";
076: private static final String PERSIST = "persist";
077: private static final String SUFFIX = "suffix";
078:
079: public String session = "SessionID";
080: public String cookieName = "cookie";
081: public String urlSep = ",id="; // delimeter between url and ID
082: public String redirectToken; // magic to put in url to redirect
083: public String encoding = "UrlID"; // set if url encoding is in effect
084: public boolean persist; // if true, make cookies last.
085:
086: private String propsPrefix; // properties prefix
087: private boolean haveRedirect; // we have a redirect response
088: private static Random random = new Random();
089:
090: Regexp cookieExp; // exp to match cookie in mime header
091: Regexp suffixExp; // exp to match suffix
092:
093: public boolean init(Server server, String propsPrefix) {
094: this .propsPrefix = propsPrefix;
095: Properties props = server.props;
096: session = props.getProperty(propsPrefix + SESSION, session);
097: cookieName = props
098: .getProperty(propsPrefix + COOKIE, cookieName);
099: persist = props.getProperty(propsPrefix + PERSIST) != null;
100:
101: /* XXX
102: * The code should be re-arranged so the url id is stripped
103: * off before we do the suffix check. Then this won't look
104: * quite so ugly.
105: */
106:
107: String suffix = props.getProperty(propsPrefix + SUFFIX,
108: "html|txt|xml");
109: cookieExp = new Regexp("(^|; *)" + cookieName + "=([^;]*)");
110: suffixExp = new Regexp("(\\.(" + suffix + ")|/)$");
111: redirectToken = cookieName + ",";
112: server.log(Server.LOG_DIAGNOSTIC, propsPrefix, "suffix:"
113: + suffix);
114: return true;
115: }
116:
117: /**
118: * This is called by the filterHandler before the content generation
119: * step. It is responsible for extracting the session information,
120: * then (ifd required) restoring the URL's to their original form.
121: * It tries relatively hard to use cookies if they are available
122: * through a series or redirects.
123: */
124:
125: public boolean respond(Request request) throws IOException {
126:
127: /*
128: * See if the query contains our redirect token. if so, strip it
129: * off, and set the "redirected" flag.
130: */
131:
132: boolean haveRedirect = request.query.startsWith(redirectToken);
133: if (haveRedirect) {
134: request.query = request.query.substring(redirectToken
135: .length());
136: request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
137: "Found and removing Redirect token");
138: }
139:
140: String id = fetchCookie(request);
141:
142: /*
143: * If we got the cookie during the redirect, redirect back
144: * to clean up the url.
145: */
146:
147: if (haveRedirect && id != null) {
148: redirect(request);
149: return true;
150: }
151:
152: /*
153: * If we have the cookie, set the session id and return.
154: */
155:
156: if (id != null) {
157: request.props.put("gotCookie", "true");
158: request.props.put(session, id);
159: request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
160: "Found cookie (" + cookieName + ") = " + id);
161: return false;
162: }
163:
164: /*
165: * No cookie, see if the id is encoded into the URL.
166: * For now, we'll encode the session info into the URL so
167: * that /foo/bar.html becomes /foo/bar.html,id=nnn.
168: * If we found the id in the url, and the redirect token, redirect
169: * again to remove the redirect token, but leave in the session info.
170: * We could try to prevent spoofing; Later.
171: */
172:
173: int sepIndex = request.url.indexOf(urlSep);
174: if (sepIndex > 0) {
175: id = request.url.substring(sepIndex + urlSep.length());
176: request.url = request.url.substring(0, sepIndex);
177: request.props.put(encoding, urlSep + id);
178: request.props.put(session, id);
179: request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
180: "Found id in url=" + id);
181: return false;
182: }
183:
184: /*
185: * If this isn't a suffix that needs an ID, then don't bother
186: */
187:
188: if (suffixExp.match(request.url) == null) {
189: request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
190: "Invalid suffix for " + request.url);
191: request.props.remove(encoding);
192: return false;
193: }
194:
195: /*
196: * No id in either cookie or Url, so we need to make one up.
197: * If this is the first use, try to set a cookie and redirect
198: * back here. We'll add some magic onto the end of the Url
199: * as part of the query string so we know we tried to set a cookie.
200: * If we already tried that, then tack the session id onto the
201: * url, and redirect again.
202: */
203:
204: id = Long.toHexString(random.nextLong());
205: if (haveRedirect) {
206: request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
207: "refusing cookies, set url encoding=" + id);
208: request.url += urlSep + id;
209: } else {
210: String expire = "";
211: if (persist) {
212: expire = "; EXPIRES=Fri, 01-Jan-10 00:00:00 GMT";
213: }
214: if (!cookieName.equals("none")) {
215: request.addHeader("Set-Cookie", cookieName + "=" + id
216: + "; PATH=/" + expire);
217: }
218: if (request.query.equals("")) {
219: request.query = redirectToken;
220: } else {
221: request.query = redirectToken + request.query;
222: }
223: }
224: redirect(request);
225: return true;
226: }
227:
228: /**
229: * Do a redirect back to ourselves.
230: */
231:
232: private void redirect(Request request) throws IOException {
233: String target;
234: if (request.method.equals("GET") && !request.query.equals("")) {
235: target = request.url + "?" + request.query;
236: } else {
237: target = request.url;
238: }
239: request.log(Server.LOG_DIAGNOSTIC, propsPrefix, "Redirect to: "
240: + target);
241: request.redirect(target, null);
242: }
243:
244: /**
245: * Try to Get the cookie out of the http header. Cookies can be
246: * on separate lines or all combined on one line.
247: * When multiple cookies are on one line, they are separated
248: * by ';' characters.
249: *
250: * @return The cookie value, or null if not found.
251: */
252:
253: String fetchCookie(Request request) {
254: String id = null;
255: String[] subs = new String[3];
256: MimeHeaders headers = request.headers;
257: for (int i = headers.size(); --i >= 0;) {
258: if (headers.getKey(i).equalsIgnoreCase("Cookie") == false) {
259: continue;
260: }
261: request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
262: "Examining cookie header: " + headers.get(i));
263: if (cookieExp.match(headers.get(i), subs)) {
264: id = subs[2];
265: if (id.length() == 0) {
266: id = null;
267: }
268: break;
269: }
270: }
271: return id;
272: }
273:
274: /**
275: * We have the results, only filter if html and we're rewriting
276: */
277:
278: public boolean shouldFilter(Request request, MimeHeaders headers) {
279: String type = headers.get("content-type");
280: boolean shouldEncode = (request.props.getProperty(encoding) != null);
281: return (shouldEncode && type != null && type.toLowerCase()
282: .startsWith("text/"));
283: }
284:
285: /**
286: * Rewrite all the url's, adding the session id to the end
287: */
288:
289: public byte[] filter(Request request, MimeHeaders headers,
290: byte[] content) {
291: String id = request.props.getProperty(session);
292: if (id == null) {
293: return content;
294: }
295: String type = headers.get("content-type");
296: if (!type.toLowerCase().startsWith("text/html")) {
297: return content;
298: }
299: Map map = new Map(urlSep + id, suffixExp);
300: return map.convertHtml(new String(content)).getBytes();
301: }
302:
303: /**
304: * The mapPage class was designed for virtual web page re-mapping,
305: * but it knows which entity attributes are URL's.
306: * We use it for session attachment instead.
307: */
308:
309: private static class Map extends MapPage {
310: Regexp re;
311:
312: Map(String s, Regexp re) {
313: super (s);
314: this .re = re;
315: }
316:
317: /**
318: * If the url doesn't start with http://, tack the session info
319: * on to the end of the URL. This can fail if the absolute
320: * URL resolves back to us XXX.
321: */
322:
323: public String convertString(String fix) {
324: if (fix.startsWith("http://")) {
325: return null;
326: }
327: int index = fix.indexOf('#');
328: if (index < 0) {
329: index = fix.indexOf('?');
330: }
331: if (index == 0) {
332: return null;
333: } else if (index < 0) {
334: index = fix.length();
335: }
336: String urlPart = fix.substring(0, index);
337: if (re.match(urlPart) != null) {
338: String result = urlPart + prefix + fix.substring(index);
339: // System.out.println(" converting: " + fix + " => " + result);
340: return result;
341: }
342: // System.out.println(" Not converting: " + urlPart);
343: return null;
344: }
345: }
346: }
|