001: package net.matuschek.http.cookie;
002:
003: import java.net.URL;
004: import java.util.Date;
005: import java.util.Locale;
006: import java.util.StringTokenizer;
007:
008: /*********************************************
009: Copyright (c) 2001 by Daniel Matuschek
010: *********************************************/
011:
012: /**
013: * This object represents an HTTP cookie for a browser.
014: * It can interpret both Netscape and RFC cookies
015: *
016: * @author Daniel Matuschek
017: * @version $Id $
018: */
019: public class Cookie {
020: /** HTTP Set-Cookie response header (not case sensitive) */
021: final static String HEADER_SETCOOKIE = "Set-Cookie:";
022:
023: /** HTTP cookie response header (not case sensitive) */
024: final static String HEADER_COOKIE = "Cookie:";
025:
026: /** Cookie name */
027: private String name;
028:
029: /** Cookie value */
030: private String value = null;
031:
032: /**
033: * Life time in seconds, -1 means "expire, if browser exits" <br />
034: * this is only useful for RFC 2109 cookie, because Netscape cookies
035: * do not have a maxAge field. This value will only be used to
036: * create the internal expireDate value.
037: */
038: private long maxAge = -1;
039:
040: /** Comment */
041: private String comment = "";
042:
043: /** Domain */
044: private String domain = null;
045:
046: /** Path */
047: private String path = "/";
048:
049: /** Secure ? */
050: private boolean secure = false;
051:
052: /**
053: * expire date, default is "never" for cookies without explicit
054: * exipration date
055: */
056: private Date expireDate = new Date(Long.MAX_VALUE);
057:
058: /**
059: * Cookie version <br />
060: * version=0 refers to the Netscape cookie specification <br />
061: * version=1 refers to the RFC 2109 Cookie specification <br />
062: * <br />
063: * @see <a href="http://home.netscape.com/newsref/std/cookie_spec.html">
064: * Netscape Cookie specification</a><br />
065: */
066: private int version = 0;
067:
068: /**
069: * Default constructor, creates an empty cookie
070: */
071: public Cookie() {
072: }
073:
074: /**
075: * Constructor that initializes a cookie from a HTTP Set-Cookie: header
076: * @param setCookie a HTTP Set-Cookie: header line (including Set-Cookie)
077: * @param u there URL of the HTTP document where this cookie was set from
078: * this is needed, if no "domain" field is given in the cookie.
079: * It will be ignored otherwise
080: * @exception CookieException if the given setCookie String is not a valid
081: * HTTP Set-Cookie response header
082: */
083: public Cookie(String setCookie, URL u) throws CookieException {
084: this ();
085:
086: String cookieHeader = null;
087: String host = "";
088: StringTokenizer tokens = null;
089:
090: // does is start with "Set-Cookie" ?
091: if (setCookie.substring(0, HEADER_SETCOOKIE.length())
092: .equalsIgnoreCase(HEADER_SETCOOKIE)) {
093: cookieHeader = setCookie.substring(HEADER_SETCOOKIE
094: .length());
095: } else {
096: throw new CookieException("Not a Set-Cookie header");
097: }
098:
099: // set defaults from the URL
100: if (u != null) {
101: this .domain = u.getHost().toLowerCase();
102: host = this .domain;
103: } else {
104: this .domain = "";
105: }
106:
107: // tokenize setcookie request
108: tokens = new StringTokenizer(cookieHeader, ";");
109:
110: // there must be at least ONE token (name=value)
111: if (tokens.countTokens() < 1) {
112: throw new CookieException("Cookie contains no data");
113: } else {
114: String field = tokens.nextToken();
115: int pos = field.indexOf('=');
116: if (pos <= 0) {
117: throw new CookieException(
118: "First field not in the format NAME=VALUE"
119: + " but got " + field);
120: } else {
121: name = field.substring(0, pos).trim();
122: value = field.substring(pos + 1);
123: }
124: }
125:
126: // parse all other fields
127: while (tokens.hasMoreTokens()) {
128: String field = tokens.nextToken();
129: String fieldname = "";
130: String fieldvalue = "";
131:
132: int pos = field.indexOf('=');
133: if (pos <= 0) {
134: fieldname = field.trim();
135: fieldvalue = "";
136: } else {
137: fieldname = field.substring(0, pos).trim();
138: fieldvalue = field.substring(pos + 1).trim();
139: }
140:
141: if (fieldname.equalsIgnoreCase("comment")) {
142: //
143: // COMMENT
144: //
145: this .comment = fieldvalue;
146: } else if (fieldname.equalsIgnoreCase("domain")) {
147: //
148: // DOMAIN
149: //
150: String domainvalue = fieldvalue.toLowerCase();
151: // check if the domain is allowed for the current URL !
152: if ((host.equals("")) || (host.endsWith(domain))) {
153: this .domain = domainvalue;
154: } else {
155: throw new CookieException(
156: "Not allowed to set a cookie for domain "
157: + domainvalue + " from host "
158: + host);
159: }
160: } else if (fieldname.equalsIgnoreCase("jmfdomain")) {
161: //
162: // JMFDOMAIN
163: //
164: String domainvalue = fieldvalue.toLowerCase();
165: // check if the domain is allowed for the current URL !
166: if ((host.equals("")) || (host.endsWith(domain))) {
167: this .domain = domainvalue;
168: } else {
169: throw new CookieException(
170: "Not allowed to set a cookie for domain "
171: + domainvalue + " from host "
172: + host);
173: }
174: } else if (fieldname.equalsIgnoreCase("path")) {
175: //
176: // PATH
177: //
178: this .path = fieldvalue;
179: } else if (fieldname.equalsIgnoreCase("secure")) {
180: //
181: // SECURE
182: //
183: this .secure = true;
184: } else if (fieldname.equalsIgnoreCase("max-age")) {
185: //
186: // MAX-AGE
187: //
188: try {
189: this .maxAge = Integer.parseInt(fieldvalue);
190: } catch (NumberFormatException e) {
191: throw new CookieException(
192: "max-age must be integer, but is "
193: + fieldvalue);
194: }
195:
196: if (maxAge >= 0) {
197: this .expireDate = new Date(System
198: .currentTimeMillis()
199: + maxAge * 1000);
200: } else {
201: this .expireDate = new Date(Long.MAX_VALUE);
202: }
203: } else if (fieldname.equalsIgnoreCase("expires")) {
204: //
205: // EXPIRES
206: //
207: String dateStr = null;
208: java.text.SimpleDateFormat[] df = new java.text.SimpleDateFormat[2];
209:
210: // possible date formats
211: // thanks to Scott Woodson for the feedback
212: df[0] = new java.text.SimpleDateFormat(
213: "dd-MMM-yyyy HH:mm:ss z", Locale.US);
214: df[1] = new java.text.SimpleDateFormat(
215: "dd MMM yyyy HH:mm:ss z", Locale.US);
216:
217: int commapos = fieldvalue.indexOf(",");
218: if (commapos < 0) {
219: throw new CookieException(
220: "Expires field does not contain "
221: + "a comma, value is " + fieldvalue);
222: }
223: dateStr = fieldvalue.substring(commapos + 1).trim();
224: boolean foundDate = false;
225:
226: for (int i = 0; i < df.length; i++) {
227: try {
228: this .expireDate = df[i].parse(dateStr);
229: // if we got no exception, jump out of the loop
230: foundDate = true;
231: continue;
232: } catch (java.text.ParseException e) {
233: }
234: ;
235: }
236:
237: // found a valid date ?
238: if (!foundDate) {
239: throw new CookieException(
240: "Can't parse expires field as date, "
241: + "value is " + dateStr);
242: }
243:
244: } else if (fieldname.equalsIgnoreCase("version")) {
245: //
246: // VERSION
247: //
248: try {
249: this .version = Integer.parseInt(fieldvalue);
250: } catch (NumberFormatException e) {
251: throw new CookieException(
252: "Version must be integer, but is "
253: + fieldvalue);
254: }
255:
256: if (version > 1) {
257: throw new CookieException(
258: "Only version 0 and 1 supported yet, "
259: + "but cookie used version "
260: + version);
261: }
262: }
263: }
264:
265: }
266:
267: /**
268: * Initializes a cookie from an name-value pair and additional
269: * information. This constructor is useful to create cookies
270: * by yourself.
271: */
272: public Cookie(String name, String value, String domain, String path) {
273: this .name = name;
274: this .value = value;
275: this .domain = domain;
276: this .path = path;
277: }
278:
279: /**
280: * Is this cookie valid ?
281: * @return true if the cookie is valid, false if it is expired
282: */
283: public boolean isValid() {
284: Date current = new Date();
285: return current.before(expireDate);
286: }
287:
288: /**
289: * Is this cookie valid for the given URL ?
290: * That means, it is not expired and host and path matches the given URL
291: * @return true if this cookie is valid for the given URL, false otherwise
292: */
293: public boolean isValid(URL u) {
294: String urlhost = u.getHost().toLowerCase();
295: String urlpath = u.getPath();
296:
297: return (isValid() && urlhost.endsWith(this .domain) && urlpath
298: .startsWith(path));
299: }
300:
301: /**
302: * Does this Cookie overwrite another cookie ?
303: * A Cookie overwrites another one, if they have the same
304: * name, domain and path. It doesn't matter, if expireDate or value of
305: * the cookie are different !
306: */
307: public boolean overwrites(Cookie c) {
308: return (this .domain.equals(c.domain)
309: && this .path.equals(c.path) && this .name.equals(c.name));
310: }
311:
312: /**
313: * Gets the cookie name and value as NAME=VALUE pair
314: * @return a string in the format NAME=VALUE
315: */
316: public String getNameValuePair() {
317: return this .name + "=" + this .value;
318: }
319:
320: /**
321: * Convert the cookie to a String. Format is not defined and may change
322: * without notice. Use it for debugging and logging purposes only !
323: * @return a String representation of this cookie
324: */
325: public String toString() {
326: return this .name
327: + "="
328: + this .value
329: + " (Comment="
330: + this .comment
331: + ", Version="
332: + this .version
333: + ", domain="
334: + this .domain
335: + ", path="
336: + this .path
337: + ", expires "
338: + java.text.DateFormat.getDateTimeInstance().format(
339: this .expireDate) + ")";
340: }
341:
342: /**
343: */
344: public static Cookie[] cookieStringToCookies(String cookieStr,
345: String domain) throws CookieException {
346: String cookieHeader;
347:
348: // does is start with "Cookie" ?
349: if (cookieStr.substring(0, HEADER_COOKIE.length())
350: .equalsIgnoreCase(HEADER_COOKIE)) {
351: cookieHeader = cookieStr.substring(HEADER_COOKIE.length());
352: } else {
353: throw new CookieException("Not a Cookie header");
354: }
355:
356: // tokenize setcookie request
357: StringTokenizer tokens = new StringTokenizer(cookieHeader, ";");
358: Cookie[] cookies = new Cookie[tokens.countTokens()];
359: int i = 0;
360:
361: while (tokens.hasMoreTokens()) {
362: cookies[i] = null;
363: String field = tokens.nextToken();
364: int pos = field.indexOf('=');
365: if (pos <= 0) {
366: throw new CookieException(
367: "Cookie field not in the format NAME=VALUE"
368: + " but got " + field);
369: } else {
370: cookies[i] = new Cookie();
371: cookies[i].name = field.substring(0, pos).trim();
372: cookies[i].value = field.substring(pos + 1);
373: // we do not know this from a "Cookie" header, but from the
374: // argument of this function
375: cookies[i].domain = "." + domain;
376: }
377: i++;
378: }
379:
380: return cookies;
381:
382: }
383:
384: public boolean isSecure() {
385: return secure;
386: }
387:
388: }
|