001 // Copyright (c) 2001 Hursh Jain (http://www.mollypages.org)
002 // The Molly framework is freely distributable under the terms of an
003 // MIT-style license. For details, see the molly pages web site at:
004 // http://www.mollypages.org/. Use, modify, have fun !
005
006 package fc.web.servlet;
007
008 import java.util.*;
009 import java.io.*;
010 import java.security.*;
011 import javax.servlet.*;
012 import javax.servlet.http.*;
013
014 import fc.io.*;
015
016 /**
017 Misc utility methods for sessions and cookies.
018
019 @author hursh jain
020 **/
021 public class SessionUtil
022 {
023 static final String TestCookieName = "eikooc";
024 static final Random random = new java.util.Random();
025 static MessageDigest digester = null;
026
027 static {
028 try {
029 digester = MessageDigest.getInstance("SHA-1");
030 }
031 catch (Exception e) {
032 e.printStackTrace();
033 }
034 }
035
036 /**
037 Sets a test cookies. The user should be redirected to this or some other
038 page after calling this method and the presence of this cookie can be
039 checked via {@link hasTestCookie} from that page. The lifetime of the
040 test cookie is set to the browsers lifetime (will be lost when the
041 browser exits).
042 */
043 public static void setTestCookie(
044 HttpServletRequest req, HttpServletResponse res)
045 {
046 Cookie c = new Cookie(TestCookieName, "scotchAndsoda");
047 c.setMaxAge(-1);
048 res.addCookie(c);
049 }
050
051 /**
052 Returns true if the browser accepts cookies. The {@link setTestCookie()}
053 method must have been invoked by some page prior to calling this method
054 AND the user must have been either redirected to that or some other page
055 AND this method must be invoked from that redirected page.
056 <p>
057 <b>Note:</b>
058 Test cookies are entirely self contained. They are meant to figure out if
059 a browser accepts cookies. Their presence or absence does not mean that
060 <b>other</b> cookies will not be sent by the browser. These other cookies
061 may point to a expired session in memory or database and a seperate check
062 for that should be made.
063 */
064 public static boolean hasTestCookie(
065 HttpServletRequest req, HttpServletResponse res)
066 {
067 Cookie[] cookies = req.getCookies();
068 if (cookies == null)
069 return false;
070
071 for (int n = 0; n < cookies.length; n++) {
072 if (cookies[n].getName().equals(TestCookieName))
073 return true;
074 }
075
076 return false;
077 }
078
079 /**
080 Removes the test cookies if previously set. Normally, the
081 test cookie set by the {@link #setTestCookie} method is
082 automatically removed when the browser exists, but this
083 method allows removing it before such time.
084 */
085 public static void deleteTestCookie(
086 HttpServletRequest req, HttpServletResponse res)
087 {
088 Cookie[] cookies = req.getCookies();
089 if (cookies == null)
090 return;
091
092 for (int n = 0; n < cookies.length; n++)
093 {
094 if (cookies[n].getName().equals(TestCookieName))
095 {
096 cookies[n].setMaxAge(0);
097 res.addCookie(cookies[n]);
098 break;
099 }
100 }
101 }
102
103 /**
104 Returns a new session identifier.
105 <pre>
106 See: cookies.lcs.mit.edu
107 See: www.across.si
108
109 There are 2 issues with generating sessionid's.
110
111 1) uniqueness - 2 or more sessionid's should not end up being
112 the same.
113 2) hard-to-guess - For example, sequential values like
114 1, 2, 3 are unique but easy to guess and therefore easy
115 to session hijack.
116
117 Our sessionid's have 2 parts:
118 a) a timestamp for guaranteed uniqueness (easy to guess)
119 b) random data (hard to guess)
120 </pre>
121 */
122 public static String newSessionID()
123 {
124 //although this function has been written for some time,
125 //i recently looked this over with webscarab (around aug 2007)
126 //and it visually appears fairly scattered/random. (9999 sid's)
127 //dunno, maybe hotbits retrieved/cached every minute via a background
128 //thread is good if one wants to be hardcore but it's pretty good
129 //as-is
130
131 //there is also no visual difference between nanos & millis. intuitively
132 //nanos should have more differences in the lower bits but after
133 //all the bits get churned/digested, there is no diff that i can tell,
134 //at least visually via webscarab. so i've left this as millis.
135
136 long time = System.currentTimeMillis();
137
138 //String remoteip = req.getRemoteAddr();
139 int rand = random.nextInt(); //presuming thread safe ?
140 String cookie = String.valueOf(time) + rand;
141
142 byte[] digestbuf = null;
143
144 //digests cookiestring to 16 bytes
145 synchronized (SessionUtil.class)
146 {
147 digestbuf = digester.digest(cookie.getBytes());
148 }
149
150 /*
151 we simply convert bytes toHex. note, we don't use Base64
152 because '/' and '+' are valid Base64 chars and they can
153 mess up url's.
154 */
155
156 char[] encoded = null;
157 encoded = encode(digestbuf);
158 return new String(encoded);
159
160 //note, we don't ever need to use the decode() method
161 //but it's there for testing foo->encode->decode->foo
162 }
163
164 //well, i was very...young...when I wrote this... :-]
165 //
166 // 1..............16 [or 0-15, 16 things]
167 // 0123456789ABCDEF
168 static final char[] lookup="cUntSNIFfER21690".toCharArray();
169
170 /*
171 base16 encoding
172 0 -> some ascii code
173 1 -> some ascii code
174 ...
175 15-> some ascii code
176
177 when ascii codes are for 0...F it's called "hex" encoding.
178 In this case, the set of ascii codes chosen together happen
179 to spell out a word and this sequence is henceforth known as
180 "hursh" encoding.
181 */
182
183 //we need a reverse lookup table since are ain't
184 //using straight hexadecimal to encode.
185 static final int[] reverselookup = new int[128];
186
187 static
188 {
189 for (int n = 0; n < 16; n++) {
190 int x = lookup[n] & 0xFF; //x ='c'('c'&0xff->99), 'n'(110) etc
191 reverselookup[x] = n; //this is why reverselookup[128] above, not 16
192 }
193 }
194
195 private static final char[] encode(byte[] buf)
196 {
197 final int len = buf.length;
198
199 char[] tmp = new char[len * 2];
200
201 for (int n = 0, i = 0; n < len ; n++, i+=2)
202 {
203 int high4 = (buf[n] >> 4) & 0x0F;
204 int low4 = buf[n] & 0x0F;
205
206 tmp[i] = lookup [high4];
207 tmp[i+1] = lookup [low4];
208 }
209 return tmp;
210 }
211
212 private static final byte[] decode(char[] buf)
213 {
214 final int len = buf.length;
215
216 byte[] tmp = new byte[(len/2) + (len%2)];
217
218 for (int n = 0, tmp_n = 0; n < len; n+=2, tmp_n++)
219 {
220 //0xFF makes it an int. we could cast to (int)
221 //too but then that would sign extend the char
222 //in the general case [but since all are chars
223 //are < 127], it would be ok to do so in this
224 //case. Used 0xFF because that's the right thing
225 //generally speaking
226
227 int c1 = buf[n] & 0xFF; //'c', 'u', 'n', 't' etc..
228 int c2 = buf[n+1] & 0xFF;
229
230 if ( c1 > 127 || c2 > 127)
231 throw new IllegalArgumentException("malformed bufing, encoded with characters I don't understand [" + c1 + "] and [" + c2 + "]");
232
233 int i = (reverselookup[c1] << 4 | reverselookup [c2]);
234 //System.out.println( (reverselookup[c1] << 4) + ", " + reverselookup[c2] + ",->" + i);
235
236 tmp[tmp_n] = (byte) i;
237 }
238 return tmp;
239 }
240
241 }