1 // Copyright 2002 Google, Inc.
3 package com
.google
.appengine
.tools
.util
;
5 import java
.io
.Serializable
;
7 import java
.util
.ArrayList
;
9 import java
.util
.logging
.Logger
;
12 * A client-side cookie.
14 * @see com.google.yans.ClientCookieManager
16 public class ClientCookie
implements Comparable
<ClientCookie
>, Serializable
{
18 private static final long serialVersionUID
= 1L;
20 private static final Logger logger
= Logger
.getLogger(
21 ClientCookie
.class.getName());
24 * Cookie name (never null).
27 private String name_
= null;
29 * Cookie value (never null).
32 private String value_
= null;
34 * Cookie comment (always null for V0 cookies).
37 private String comment_
= null;
39 * Cookie domain, as seen in the HTTP header (can be null).
42 private String domain_
= null;
44 * Effective cookie domain, used in matches (never null).
47 private String effectiveDomain_
= null;
49 * Cookie path, as seen in the HTTP header (can be null).
52 private String path_
= null;
54 * Effective cookie path, used in matches (never null).
57 private String effectivePath_
= null;
59 * Is this cookie secure? This field is set but currently unused.
62 private boolean secure_
= false;
64 * Absolute cookie expiration time, in milliseconds since the epoch.
67 private long expires_
= Long
.MAX_VALUE
;
69 * Cookie version, as seen in the HTTP header (zero if not in header).
72 private int version_
= 0;
74 * Effective cookie version, as determined by the parser.
77 private int effectiveVersion_
= 0;
79 * Is this cookie only available via HTTP? This field is set but currently unused.
82 private boolean httponly_
= false;
84 private static final String
[] SPECIAL_DOMAINS
= {
85 ".com", ".edu", ".net", ".org", ".gov", ".mil", ".int"
90 * @return cookie name, never null.
92 public String
getName() { return name_
; }
96 * @return cookie value, never null.
98 public String
getValue() { return value_
; }
101 * Get cookie comment (RFC 2109 and RFC 2965 cookies only).
102 * @return cookie comment, or null if not specified in the header.
104 public String
getComment() { return comment_
; }
108 * @return cookie domain, or null if not specified in the header.
110 public String
getDomain() { return domain_
; }
113 * Get effective cookie domain.
114 * @return effective domain, never null.
116 public String
getEffectiveDomain() { return effectiveDomain_
; }
120 * @return cookie path, or null if not specified in the header.
122 public String
getPath() { return path_
; }
125 * Get effective cookie path.
126 * @return effective path, never null.
128 public String
getEffectivePath() { return effectivePath_
; }
131 * Is this cookie secure?
132 * @return true if the cookie is secure, false if not.
134 public boolean isSecure() { return secure_
; }
137 * Is this cookie only available via HTTP?
138 * @return true if the cookie is only available via HTTP, false if not.
140 public boolean isHttpOnly() { return httponly_
; }
143 * Get cookie expiration time.
144 * @return absolute expiration time, in milliseconds, or large value if none.
146 public long getExpirationTime() { return expires_
; }
149 * Get cookie version.
150 * @return cookie version, or zero if not in the header.
152 public int getVersion() { return version_
; }
155 * Get effective cookie version.
156 * @return effective version: 0, 1, or 2.
158 public int getEffectiveVersion() { return effectiveVersion_
; }
161 * Match the cookie against a request.
163 * <p>See V0, RFC 2109 sec. 2, RFC 2965 sec. 1.
165 * @param url request URL.
166 * @return true if the cookie matches this request, false if not.
168 public boolean match(URL url
) {
169 final String requestHost
= url
.getHost().toLowerCase();
170 final String requestPath
= url
.getPath();
171 if (effectiveDomain_
.startsWith(".")) {
172 if (!requestHost
.endsWith(effectiveDomain_
) &&
173 !requestHost
.equals(effectiveDomain_
.substring(1))) {
177 if (!requestHost
.equals(effectiveDomain_
)) {
181 if (!requestPath
.startsWith(effectivePath_
)) {
188 * Encode the cookie for transmission to the server.
190 * <p>See V0, RFC 2109 sec. 4.3.4, RFC 2965 sec. 3.3.4.
192 * @return properly encoded cookie.
194 public StringBuffer
encode() {
195 if (effectiveVersion_
== 0) {
196 final StringBuffer result
= new StringBuffer(name_
);
198 result
.append(value_
);
201 final StringBuffer result
=
202 HttpHeaderParser
.makeAttributeValuePair(name_
, value_
);
205 result
.append(HttpHeaderParser
.makeAttributeValuePair("$Path", path_
));
207 if (domain_
!= null) {
210 HttpHeaderParser
.makeAttributeValuePair("$Domain", domain_
));
217 * Compare two cookies.
219 * <p>See V0, RFC 2109 sec. 4.3.4, RFC 2965 sec. 3.3.3.
221 * @param o object to compare this cookie to.
222 * @return true if the object is equal to this cookie, false otherwise.
224 @Override public boolean equals(Object o
) {
225 if (!(o
instanceof ClientCookie
)) {
228 ClientCookie cookie
= (ClientCookie
)o
;
229 return cookie
.name_
.equals(name_
) &&
230 cookie
.effectiveDomain_
.equals(effectiveDomain_
) &&
231 cookie
.effectivePath_
.equals(effectivePath_
);
236 * Compare two cookies.
238 * <p>See V0, RFC 2109 sec. 4.3.4, RFC 2965 sec. 3.3.4.
240 * @param cookie object to compare this cookie to, must be non-null.
241 * @return a negative integer, zero, or positive integer, as per Comparable.
243 public int compareTo(ClientCookie cookie
) {
244 int result
= cookie
.effectivePath_
.length() - effectivePath_
.length();
248 result
= cookie
.effectivePath_
.compareTo(effectivePath_
);
252 result
= cookie
.effectiveDomain_
.compareTo(effectiveDomain_
);
256 return cookie
.name_
.compareTo(name_
);
260 * Count occurrences of a character in a string.
261 * @param str string to examine.
262 * @param ch character to look for.
263 * @return the number of occurrences of ch in str.
265 private static int countOccurrences(String str
, char ch
) {
266 int count
= 0, index
= 0;
267 while ((index
= str
.indexOf(ch
, index
)) >= 0) {
275 * No externally visible default constructor, see <code>parseSetCookie</code>.
277 private ClientCookie() {
281 * Get all cookies from a Set-Cookie header.
282 * @param setCookieHeader value of the header, never null.
283 * @param url request URL, never null.
284 * @return a list of cookies in the header, can be empty.
285 * @exception HttpHeaderParseException if the header is misformatted.
287 public static List
<ClientCookie
> parseSetCookie(String setCookieHeader
,
289 throws HttpHeaderParseException
{
291 return parseSetCookie1(setCookieHeader
, url
);
292 } catch (HttpHeaderParseException e
) {
293 return parseSetCookie0(setCookieHeader
, url
);
298 * Get all cookies from a Set-Cookie header, assume RFC 2109.
300 * <p>See RFC 2109 sec. 4.2.2, 4.3.1, 4.3.2.
302 * @param setCookie1Header value of the header.
303 * @param url request URL.
304 * @return a list of cookies in the header, can be empty.
305 * @exception HttpHeaderParseException if the header is misformatted.
307 private static List
<ClientCookie
> parseSetCookie1(String setCookie1Header
,
309 throws HttpHeaderParseException
{
310 final HttpHeaderParser parser
= new HttpHeaderParser(setCookie1Header
);
311 final ArrayList
<ClientCookie
> results
= new ArrayList
<ClientCookie
>();
314 final ClientCookie cookie
= new ClientCookie();
315 cookie
.effectiveVersion_
= 1;
316 cookie
.name_
= parser
.eatToken();
320 cookie
.value_
= parser
.eatTokenOrQuotedString();
323 while (parser
.peek() == ';') {
326 final String name
= parser
.eatToken().toLowerCase();
328 if (name
.equals("secure")) {
329 cookie
.secure_
= true;
330 } else if (name
.equals("httponly")) {
331 cookie
.httponly_
= true;
335 final String value
= parser
.eatTokenOrQuotedString();
336 if (name
.equals("comment")) {
337 cookie
.comment_
= value
;
338 } else if (name
.equals("domain")) {
339 cookie
.domain_
= value
.toLowerCase();
340 } else if (name
.equals("max-age")) {
343 maxAge
= Integer
.parseInt(value
);
345 throw new NumberFormatException(value
);
347 } catch (NumberFormatException e
) {
348 throw new HttpHeaderParseException("invalid max-age: " +
351 cookie
.expires_
= System
.currentTimeMillis() + 1000 * maxAge
;
352 } else if (name
.equals("path")) {
353 cookie
.path_
= value
;
354 } else if (name
.equals("version")) {
356 cookie
.version_
= Integer
.parseInt(value
);
357 if (cookie
.version_
<= 0) {
358 throw new NumberFormatException(value
);
360 } catch (NumberFormatException e
) {
361 throw new HttpHeaderParseException("invalid version: " +
364 } else if (name
.equals("expires")) {
365 throw new HttpHeaderParseException("this is a v0 cookie");
367 logger
.info("unrecognized v1 cookie attribute: " +
374 boolean valid
= true;
375 final String requestHost
= url
.getHost().toLowerCase();
376 final String requestPath
= url
.getPath();
377 if (cookie
.domain_
== null) {
378 cookie
.effectiveDomain_
= requestHost
;
380 if (!cookie
.domain_
.startsWith(".") ||
381 cookie
.domain_
.substring(1, cookie
.domain_
.length() - 1)
383 logger
.info("rejecting v1 cookie [bad domain - no periods]: " +
386 } else if (!requestHost
.endsWith(cookie
.domain_
)) {
387 logger
.info("rejecting v1 cookie [bad domain - no match]: " +
390 } else if (requestHost
391 .substring(0, requestHost
.length() - cookie
.domain_
.length())
392 .indexOf('.') >= 0) {
393 logger
.info("rejecting v1 cookie [bad domain - extra periods]: " +
397 cookie
.effectiveDomain_
= cookie
.domain_
;
400 if (cookie
.path_
== null) {
401 int index
= requestPath
.lastIndexOf('/');
403 cookie
.effectivePath_
= requestPath
;
405 cookie
.effectivePath_
= requestPath
.substring(0, index
);
408 if (!requestPath
.startsWith(cookie
.path_
)) {
409 logger
.info("rejecting v1 cookie [bad path]: " +
413 cookie
.effectivePath_
= cookie
.path_
;
420 if (!parser
.isEnd()) {
430 * Get all cookies from a Set-Cookie header, assume Netscape V0 cookies.
431 * @param setCookie0Header value of the header.
432 * @param url request URL.
433 * @return a list of cookies in the header, can be empty.
434 * @exception HttpHeaderParseException if the header is misformatted.
436 private static List
<ClientCookie
> parseSetCookie0(String setCookie0Header
,
438 throws HttpHeaderParseException
{
439 final HttpHeaderParser parser
= new HttpHeaderParser(setCookie0Header
);
440 final ArrayList
<ClientCookie
> results
= new ArrayList
<ClientCookie
>();
443 final ClientCookie cookie
= new ClientCookie();
444 cookie
.effectiveVersion_
= 0;
445 cookie
.name_
= parser
.eatV0CookieToken();
449 cookie
.value_
= parser
.eatV0CookieValue();
452 while (!parser
.isEnd()) {
455 final String name
= parser
.eatV0CookieToken().toLowerCase();
456 if (name
.equals("secure")) {
457 cookie
.secure_
= true;
458 } else if (name
.equals("httponly")) {
459 cookie
.httponly_
= true;
464 if (name
.equals("expires")) {
465 cookie
.expires_
= parser
.eatV0CookieDate().getTime();
467 final String value
= parser
.eatV0CookieValue();
468 if (name
.equals("domain")) {
469 cookie
.domain_
= value
.toLowerCase();
470 } else if (name
.equals("path")) {
471 cookie
.path_
= value
;
473 logger
.info("unrecognized v0 cookie attribute: " +
481 final String requestHost
= url
.getHost().toLowerCase();
482 final String requestPath
= url
.getPath();
483 boolean valid
= true;
484 if (cookie
.domain_
== null) {
485 cookie
.effectiveDomain_
= '.' + requestHost
;
487 if (!requestHost
.equals(cookie
.domain_
)) {
488 if (!cookie
.domain_
.startsWith(".")) {
489 cookie
.effectiveDomain_
= '.' + cookie
.domain_
;
491 cookie
.effectiveDomain_
= cookie
.domain_
;
493 if (!requestHost
.endsWith(cookie
.effectiveDomain_
)) {
494 logger
.info("rejecting v0 cookie [bad domain - no match]: " +
498 final int numPeriods
=
499 countOccurrences(cookie
.effectiveDomain_
, '.');
500 boolean special
= false;
501 for (int i
= 0; i
< SPECIAL_DOMAINS
.length
; i
++) {
502 if (cookie
.effectiveDomain_
.endsWith(SPECIAL_DOMAINS
[i
])) {
507 if (special ?
(numPeriods
< 2) : (numPeriods
< 3)) {
508 logger
.info("rejecting v0 cookie [bad domain - no periods]: " +
514 cookie
.effectiveDomain_
= '.' + cookie
.domain_
;
517 if (cookie
.path_
== null) {
518 cookie
.effectivePath_
= requestPath
;
520 cookie
.effectivePath_
= cookie
.path_
;