1.9.30 sync.
[gae.git] / java / src / main / com / google / appengine / tools / util / ClientCookie.java
blob579fe8aedafcb09924de55f55f37dc9a482d20ab
1 // Copyright 2002 Google, Inc.
3 package com.google.appengine.tools.util;
5 import java.io.Serializable;
6 import java.net.URL;
7 import java.util.ArrayList;
8 import java.util.List;
9 import java.util.logging.Logger;
11 /**
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());
23 /**
24 * Cookie name (never null).
25 * @serial
27 private String name_ = null;
28 /**
29 * Cookie value (never null).
30 * @serial
32 private String value_ = null;
33 /**
34 * Cookie comment (always null for V0 cookies).
35 * @serial
37 private String comment_ = null;
38 /**
39 * Cookie domain, as seen in the HTTP header (can be null).
40 * @serial
42 private String domain_ = null;
43 /**
44 * Effective cookie domain, used in matches (never null).
45 * @serial
47 private String effectiveDomain_ = null;
48 /**
49 * Cookie path, as seen in the HTTP header (can be null).
50 * @serial
52 private String path_ = null;
53 /**
54 * Effective cookie path, used in matches (never null).
55 * @serial
57 private String effectivePath_ = null;
58 /**
59 * Is this cookie secure? This field is set but currently unused.
60 * @serial
62 private boolean secure_ = false;
63 /**
64 * Absolute cookie expiration time, in milliseconds since the epoch.
65 * @serial
67 private long expires_ = Long.MAX_VALUE;
68 /**
69 * Cookie version, as seen in the HTTP header (zero if not in header).
70 * @serial
72 private int version_ = 0;
73 /**
74 * Effective cookie version, as determined by the parser.
75 * @serial
77 private int effectiveVersion_ = 0;
78 /**
79 * Is this cookie only available via HTTP? This field is set but currently unused.
80 * @serial
82 private boolean httponly_ = false;
84 private static final String[] SPECIAL_DOMAINS = {
85 ".com", ".edu", ".net", ".org", ".gov", ".mil", ".int"
88 /**
89 * Get cookie name.
90 * @return cookie name, never null.
92 public String getName() { return name_; }
94 /**
95 * Get cookie value.
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_; }
107 * Get cookie domain.
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_; }
119 * Get cookie path.
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))) {
174 return false;
176 } else {
177 if (!requestHost.equals(effectiveDomain_)) {
178 return false;
181 if (!requestPath.startsWith(effectivePath_)) {
182 return false;
184 return true;
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_);
197 result.append('=');
198 result.append(value_);
199 return result;
200 } else {
201 final StringBuffer result =
202 HttpHeaderParser.makeAttributeValuePair(name_, value_);
203 if (path_ != null) {
204 result.append("; ");
205 result.append(HttpHeaderParser.makeAttributeValuePair("$Path", path_));
207 if (domain_ != null) {
208 result.append("; ");
209 result.append(
210 HttpHeaderParser.makeAttributeValuePair("$Domain", domain_));
212 return result;
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)) {
226 return false;
227 } else {
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();
245 if (result != 0) {
246 return result;
248 result = cookie.effectivePath_.compareTo(effectivePath_);
249 if (result != 0) {
250 return result;
252 result = cookie.effectiveDomain_.compareTo(effectiveDomain_);
253 if (result != 0) {
254 return result;
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) {
268 count++;
269 index++;
271 return count;
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,
288 URL url)
289 throws HttpHeaderParseException {
290 try {
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&nbsp;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,
308 URL url)
309 throws HttpHeaderParseException {
310 final HttpHeaderParser parser = new HttpHeaderParser(setCookie1Header);
311 final ArrayList<ClientCookie> results = new ArrayList<ClientCookie>();
312 parser.eatLWS();
313 while (true) {
314 final ClientCookie cookie = new ClientCookie();
315 cookie.effectiveVersion_ = 1;
316 cookie.name_ = parser.eatToken();
317 parser.eatLWS();
318 parser.eatChar('=');
319 parser.eatLWS();
320 cookie.value_ = parser.eatTokenOrQuotedString();
321 parser.eatLWS();
323 while (parser.peek() == ';') {
324 parser.eatChar(';');
325 parser.eatLWS();
326 final String name = parser.eatToken().toLowerCase();
327 parser.eatLWS();
328 if (name.equals("secure")) {
329 cookie.secure_ = true;
330 } else if (name.equals("httponly")) {
331 cookie.httponly_ = true;
332 } else {
333 parser.eatChar('=');
334 parser.eatLWS();
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")) {
341 final int maxAge;
342 try {
343 maxAge = Integer.parseInt(value);
344 if (maxAge < 0) {
345 throw new NumberFormatException(value);
347 } catch (NumberFormatException e) {
348 throw new HttpHeaderParseException("invalid max-age: " +
349 setCookie1Header);
351 cookie.expires_ = System.currentTimeMillis() + 1000 * maxAge;
352 } else if (name.equals("path")) {
353 cookie.path_ = value;
354 } else if (name.equals("version")) {
355 try {
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: " +
362 setCookie1Header);
364 } else if (name.equals("expires")) {
365 throw new HttpHeaderParseException("this is a v0 cookie");
366 } else {
367 logger.info("unrecognized v1 cookie attribute: " +
368 name + "=" + value);
371 parser.eatLWS();
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;
379 } else {
380 if (!cookie.domain_.startsWith(".") ||
381 cookie.domain_.substring(1, cookie.domain_.length() - 1)
382 .indexOf('.') < 0) {
383 logger.info("rejecting v1 cookie [bad domain - no periods]: " +
384 setCookie1Header);
385 valid = false;
386 } else if (!requestHost.endsWith(cookie.domain_)) {
387 logger.info("rejecting v1 cookie [bad domain - no match]: " +
388 setCookie1Header);
389 valid = false;
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]: " +
394 setCookie1Header);
395 valid = false;
396 } else {
397 cookie.effectiveDomain_ = cookie.domain_;
400 if (cookie.path_ == null) {
401 int index = requestPath.lastIndexOf('/');
402 if (index < 0) {
403 cookie.effectivePath_ = requestPath;
404 } else {
405 cookie.effectivePath_ = requestPath.substring(0, index);
407 } else {
408 if (!requestPath.startsWith(cookie.path_)) {
409 logger.info("rejecting v1 cookie [bad path]: " +
410 setCookie1Header);
411 valid = false;
412 } else {
413 cookie.effectivePath_ = cookie.path_;
416 if (valid) {
417 results.add(cookie);
420 if (!parser.isEnd()) {
421 parser.eatChar(',');
422 } else {
423 break;
426 return results;
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,
437 URL url)
438 throws HttpHeaderParseException {
439 final HttpHeaderParser parser = new HttpHeaderParser(setCookie0Header);
440 final ArrayList<ClientCookie> results = new ArrayList<ClientCookie>();
442 parser.eatLWS();
443 final ClientCookie cookie = new ClientCookie();
444 cookie.effectiveVersion_ = 0;
445 cookie.name_ = parser.eatV0CookieToken();
446 parser.eatLWS();
447 parser.eatChar('=');
448 parser.eatLWS();
449 cookie.value_ = parser.eatV0CookieValue();
450 parser.eatLWS();
452 while (!parser.isEnd()) {
453 parser.eatChar(';');
454 parser.eatLWS();
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;
460 } else {
461 parser.eatLWS();
462 parser.eatChar('=');
463 parser.eatLWS();
464 if (name.equals("expires")) {
465 cookie.expires_ = parser.eatV0CookieDate().getTime();
466 } else {
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;
472 } else {
473 logger.info("unrecognized v0 cookie attribute: " +
474 name + "=" + value);
478 parser.eatLWS();
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;
486 } else {
487 if (!requestHost.equals(cookie.domain_)) {
488 if (!cookie.domain_.startsWith(".")) {
489 cookie.effectiveDomain_ = '.' + cookie.domain_;
490 } else {
491 cookie.effectiveDomain_ = cookie.domain_;
493 if (!requestHost.endsWith(cookie.effectiveDomain_)) {
494 logger.info("rejecting v0 cookie [bad domain - no match]: " +
495 setCookie0Header);
496 valid = false;
497 } else {
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])) {
503 special = true;
504 break;
507 if (special ? (numPeriods < 2) : (numPeriods < 3)) {
508 logger.info("rejecting v0 cookie [bad domain - no periods]: " +
509 setCookie0Header);
510 valid = false;
513 } else {
514 cookie.effectiveDomain_ = '.' + cookie.domain_;
517 if (cookie.path_ == null) {
518 cookie.effectivePath_ = requestPath;
519 } else {
520 cookie.effectivePath_ = cookie.path_;
522 if (valid) {
523 results.add(cookie);
526 return results;