001: /*
002: * @(#)Cookie.java 0.3-2 18/06/1999
003: *
004: * This file is part of the HTTPClient package
005: * Copyright (C) 1996-1999 Ronald Tschalär
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this library; if not, write to the Free
019: * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
020: * MA 02111-1307, USA
021: *
022: * For questions, suggestions, bug-reports, enhancement-requests etc.
023: * I may be contacted at:
024: *
025: * ronald@innovation.ch
026: *
027: */
028:
029: package HTTPClient;
030:
031: import java.io.File;
032: import java.net.ProtocolException;
033: import java.util.Date;
034: import java.util.Hashtable;
035:
036: /**
037: * This class represents an http cookie as specified in
038: * <a href="http://home.netscape.com/newsref/std/cookie_spec.html">Netscape's
039: * cookie spec</a>
040: *
041: * @version 0.3-2 18/06/1999
042: * @author Ronald Tschalär
043: * @since V0.3
044: */
045:
046: public class Cookie implements java.io.Serializable {
047: protected String name;
048: protected String value;
049: protected Date expires;
050: protected String domain;
051: protected String path;
052: protected boolean secure;
053:
054: /**
055: * Create a cookie.
056: *
057: * @param name the cookie name
058: * @param value the cookie value
059: * @param domain the host this cookie will be sent to
060: * @param path the path prefix for which this cookie will be sent
061: * @param epxires the Date this cookie expires, null if at end of
062: * session
063: * @param secure if true this cookie will only be over secure connections
064: * @exception NullPointerException if <var>name</var>, <var>value</var>,
065: * <var>domain</var>, or <var>path</var>
066: * is null
067: * @since V0.3-1
068: */
069: public Cookie(String name, String value, String domain,
070: String path, Date expires, boolean secure) {
071: if (name == null)
072: throw new NullPointerException("missing name");
073: if (value == null)
074: throw new NullPointerException("missing value");
075: if (domain == null)
076: throw new NullPointerException("missing domain");
077: if (path == null)
078: throw new NullPointerException("missing path");
079:
080: this .name = name;
081: this .value = value;
082: this .domain = domain.toLowerCase();
083: this .path = path;
084: this .expires = expires;
085: this .secure = secure;
086:
087: if (this .domain.indexOf('.') == -1)
088: this .domain += ".local";
089: }
090:
091: /**
092: * Use <code>parse()</code> to create cookies.
093: *
094: * @see #parse(java.lang.String, HTTPClient.RoRequest)
095: */
096: protected Cookie(RoRequest req) {
097: name = null;
098: value = null;
099: expires = null;
100: domain = req.getConnection().getHost();
101: if (domain.indexOf('.') == -1)
102: domain += ".local";
103: path = Util.getPath(req.getRequestURI());
104:
105: String prot = req.getConnection().getProtocol();
106: if (prot.equals("https") || prot.equals("shttp"))
107: secure = true;
108: else
109: secure = false;
110: }
111:
112: /**
113: * Parses the Set-Cookie header into an array of Cookies.
114: *
115: * @param set_cookie the Set-Cookie header received from the server
116: * @param req the request used
117: * @return an array of Cookies as parsed from the Set-Cookie header
118: * @exception ProtocolException if an error occurs during parsing
119: */
120: protected static Cookie[] parse(String set_cookie, RoRequest req)
121: throws ProtocolException {
122: int beg = 0, end = 0, start = 0;
123: char[] buf = set_cookie.toCharArray();
124: int len = buf.length;
125:
126: Cookie cookie_arr[] = new Cookie[0], curr;
127:
128: cookies: while (true) // get all cookies
129: {
130: beg = Util.skipSpace(buf, beg);
131: if (beg >= len)
132: break; // no more left
133: if (buf[beg] == ',') // empty header
134: {
135: beg++;
136: continue;
137: }
138:
139: curr = new Cookie(req);
140: start = beg;
141:
142: boolean legal = true;
143:
144: parts: while (true) // parse all parts
145: {
146: if (beg >= len || buf[beg] == ',')
147: break;
148:
149: // skip empty fields
150: if (buf[beg] == ';') {
151: beg = Util.skipSpace(buf, beg + 1);
152: continue;
153: }
154:
155: // first check for secure, as this is the only one w/o a '='
156: if ((beg + 6 <= len)
157: && set_cookie.regionMatches(true, beg,
158: "secure", 0, 6)) {
159: curr.secure = true;
160: beg += 6;
161:
162: beg = Util.skipSpace(buf, beg);
163: if (beg < len && buf[beg] == ';') // consume ";"
164: beg = Util.skipSpace(buf, beg + 1);
165: else if (beg < len && buf[beg] != ',')
166: throw new ProtocolException(
167: "Bad Set-Cookie header: " + set_cookie
168: + "\nExpected "
169: + "';' or ',' at position "
170: + beg);
171:
172: continue;
173: }
174:
175: // alright, must now be of the form x=y
176: end = set_cookie.indexOf('=', beg);
177: if (end == -1)
178: throw new ProtocolException(
179: "Bad Set-Cookie header: " + set_cookie
180: + "\nNo '=' found "
181: + "for token starting at "
182: + "position " + beg);
183:
184: String name = set_cookie.substring(beg, end).trim();
185: beg = Util.skipSpace(buf, end + 1);
186:
187: if (name.equalsIgnoreCase("expires")) {
188: /* cut off the weekday if it is there. This is a little
189: * tricky because the comma is also used between cookies
190: * themselves. To make sure we don't inadvertantly
191: * mistake a date for a weekday we only skip letters.
192: */
193: int pos = beg;
194: while (buf[pos] >= 'a' && buf[pos] <= 'z'
195: || buf[pos] >= 'A' && buf[pos] <= 'Z')
196: pos++;
197: pos = Util.skipSpace(buf, pos);
198: if (buf[pos] == ',')
199: beg = pos + 1;
200: }
201:
202: int comma = set_cookie.indexOf(',', beg);
203: int semic = set_cookie.indexOf(';', beg);
204: if (comma == -1 && semic == -1)
205: end = len;
206: else if (comma == -1)
207: end = semic;
208: else if (semic == -1)
209: end = comma;
210: else
211: end = Math.min(comma, semic);
212:
213: String value = set_cookie.substring(beg, end).trim();
214:
215: if (name.equalsIgnoreCase("expires")) {
216: try {
217: curr.expires = new Date(value);
218: } catch (IllegalArgumentException iae) {
219: /* More broken servers to deal with... Ignore expires
220: * if it's invalid
221: throw new ProtocolException("Bad Set-Cookie header: "+
222: set_cookie + "\nInvalid date found at "+
223: "position " + beg);
224: */
225: }
226: } else if (name.equalsIgnoreCase("domain")) {
227: // domains are case insensitive.
228: value = value.toLowerCase();
229:
230: // add leading dot, if missing
231: if (value.charAt(0) != '.'
232: && !value.equals(curr.domain))
233: value = '.' + value;
234:
235: // must be the same domain as in the url
236: if (!curr.domain.endsWith(value))
237: legal = false;
238:
239: /* Netscape's original 2-/3-dot rule really doesn't work
240: * because many countries use a shallow hierarchy (similar
241: * to the special TLDs defined in the spec). While the
242: * rules in draft-ietf-http-state-man-mec-08 aren't
243: * perfect either, they are better. OTOH, some sites
244: * use a domain so that the host name minus the domain
245: * name contains a dot (e.g. host x.x.yahoo.com and
246: * domain .yahoo.com). So, for the seven special TLDs we
247: * use the 2-dot rule, and for all others we use the
248: * rules in the state-man draft instead.
249: */
250:
251: // domain must be either .local or must contain at least
252: // two dots
253: if (!value.equals(".local")
254: && value.indexOf('.', 1) == -1)
255: legal = false;
256:
257: // If TLD not special then host minus domain may not
258: // contain any dots
259: String top = null;
260: if (value.length() > 3)
261: top = value.substring(value.length() - 4);
262: if (top == null
263: || !(top.equalsIgnoreCase(".com")
264: || top.equalsIgnoreCase(".edu")
265: || top.equalsIgnoreCase(".net")
266: || top.equalsIgnoreCase(".org")
267: || top.equalsIgnoreCase(".gov")
268: || top.equalsIgnoreCase(".mil") || top
269: .equalsIgnoreCase(".int"))) {
270: int dl = curr.domain.length(), vl = value
271: .length();
272: if (dl > vl
273: && curr.domain.substring(0, dl - vl)
274: .indexOf('.') != -1)
275: legal = false;
276: }
277:
278: curr.domain = value;
279: } else if (name.equalsIgnoreCase("path"))
280: curr.path = value;
281: else {
282: curr.name = name;
283: curr.value = value;
284: }
285:
286: beg = end;
287: if (beg < len && buf[beg] == ';') // consume ";"
288: beg = Util.skipSpace(buf, beg + 1);
289: }
290:
291: if (curr.name == null || curr.value == null)
292: throw new ProtocolException("Bad Set-Cookie header: "
293: + set_cookie + "\nNo Name=Value found"
294: + " for cookie starting at " + "posibition "
295: + start);
296:
297: if (legal) {
298: cookie_arr = Util.resizeArray(cookie_arr,
299: cookie_arr.length + 1);
300: cookie_arr[cookie_arr.length - 1] = curr;
301: }
302: }
303:
304: return cookie_arr;
305: }
306:
307: /**
308: * Return the name of this cookie.
309: */
310: public String getName() {
311: return name;
312: }
313:
314: /**
315: * Return the value of this cookie.
316: */
317: public String getValue() {
318: return value;
319: }
320:
321: /**
322: * @return the expiry date of this cookie, or null if none set.
323: */
324: public Date expires() {
325: return expires;
326: }
327:
328: /**
329: * @return true if the cookie should be discarded at the end of the
330: * session; false otherwise
331: */
332: public boolean discard() {
333: return (expires == null);
334: }
335:
336: /**
337: * Return the domain this cookie is valid in.
338: */
339: public String getDomain() {
340: return domain;
341: }
342:
343: /**
344: * Return the path this cookie is associated with.
345: */
346: public String getPath() {
347: return path;
348: }
349:
350: /**
351: * Return whether this cookie should only be sent over secure connections.
352: */
353: public boolean isSecure() {
354: return secure;
355: }
356:
357: /**
358: * @return true if this cookie has expired
359: */
360: public boolean hasExpired() {
361: return (expires != null && expires.getTime() <= System
362: .currentTimeMillis());
363: }
364:
365: /**
366: * @param req the request to be sent
367: * @return true if this cookie should be sent with the request
368: */
369: protected boolean sendWith(RoRequest req) {
370: HTTPConnection con = req.getConnection();
371: String eff_host = con.getHost();
372: if (eff_host.indexOf('.') == -1)
373: eff_host += ".local";
374:
375: return ((domain.charAt(0) == '.' && eff_host.endsWith(domain) || domain
376: .charAt(0) != '.'
377: && eff_host.equals(domain))
378: && Util.getPath(req.getRequestURI()).startsWith(path) && (!secure
379: || con.getProtocol().equals("https") || con
380: .getProtocol().equals("shttp")));
381: }
382:
383: /**
384: * Hash up name, path and domain into new hash.
385: */
386: public int hashCode() {
387: return (name.hashCode() + path.hashCode() + domain.hashCode());
388: }
389:
390: /**
391: * Two cookies match if the name, path and domain match.
392: */
393: public boolean equals(Object obj) {
394: if ((obj != null) && (obj instanceof Cookie)) {
395: Cookie other = (Cookie) obj;
396: return (this .name.equals(other.name)
397: && this .path.equals(other.path) && this .domain
398: .equals(other.domain));
399: }
400: return false;
401: }
402:
403: /**
404: * @return a string suitable for sending in a Cookie header.
405: */
406: protected String toExternalForm() {
407: return name + "=" + value;
408: }
409:
410: /**
411: * Create a string containing all the cookie fields. The format is that
412: * used in the Set-Cookie header.
413: */
414: public String toString() {
415: String string = name + "=" + value;
416: if (expires != null)
417: string += "; expires=" + expires;
418: if (path != null)
419: string += "; path=" + path;
420: if (domain != null)
421: string += "; domain=" + domain;
422: if (secure)
423: string += "; secure";
424: return string;
425: }
426: }
|