HttpCookie.java revision fd6bb3510c2f94d636f3572dcf5f7f4dcd1a2726
1/* Licensed to the Apache Software Foundation (ASF) under one or more 2 * contributor license agreements. See the NOTICE file distributed with 3 * this work for additional information regarding copyright ownership. 4 * The ASF licenses this file to You under the Apache License, Version 2.0 5 * (the "License"); you may not use this file except in compliance with 6 * the License. You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package java.net; 18 19import java.util.ArrayList; 20import java.util.Date; 21import java.util.HashMap; 22import java.util.List; 23import java.util.Locale; 24import java.util.regex.Matcher; 25import java.util.regex.Pattern; 26 27import org.apache.harmony.luni.util.Msg; 28 29/** 30 * This class represents a http cookie, which indicates the status information 31 * between the client agent side and the server side. According to RFC, there 32 * are 3 http cookie specifications. This class is compatible with all the three 33 * forms. HttpCookie class can accept all these 3 forms of syntax. 34 * 35 * @since 1.6 36 */ 37public final class HttpCookie implements Cloneable { 38 39 private abstract static class Setter { 40 boolean set; 41 42 Setter() { 43 set = false; 44 } 45 46 boolean isSet() { 47 return set; 48 } 49 50 void set(boolean isSet) { 51 set = isSet; 52 } 53 54 abstract void setValue(String value, HttpCookie cookie); 55 56 void validate(String value, HttpCookie cookie) { 57 if (cookie.getVersion() == 1 && value != null 58 && value.contains(COMMA_STR)) { 59 throw new IllegalArgumentException(); 60 } 61 } 62 } 63 64 private static final String DOT_STR = "."; 65 66 private static final String LOCAL_STR = ".local"; 67 68 private static final String QUOTE_STR = "\""; 69 70 private static final String COMMA_STR = ","; 71 72 private static Pattern HEAD_PATTERN = Pattern.compile("Set-Cookie2?:", 73 Pattern.CASE_INSENSITIVE); 74 75 private static Pattern NAME_PATTERN = Pattern 76 .compile( 77 "([^$=,\u0085\u2028\u2029][^,\n\t\r\r\n\u0085\u2028\u2029]*?)=([^;]*)(;)?", 78 Pattern.DOTALL | Pattern.CASE_INSENSITIVE); 79 80 private static Pattern ATTR_PATTERN0 = Pattern 81 .compile("([^;=]*)(?:=([^;]*))?"); 82 83 private static Pattern ATTR_PATTERN1 = Pattern 84 .compile("(,?[^;=]*)(?:=([^;,]*))?((?=.))?"); 85 86 private HashMap<String, Setter> attributeSet = new HashMap<String, Setter>(); 87 88 /** 89 * A utility method used to check whether the host name is in a domain or 90 * not. 91 * 92 * @param domain 93 * the domain to be checked against 94 * @param host 95 * the host to be checked 96 * @return true if the host is in the domain, false otherwise 97 */ 98 public static boolean domainMatches(String domain, String host) { 99 if (domain == null || host == null) { 100 return false; 101 } 102 String newDomain = domain.toLowerCase(); 103 String newHost = host.toLowerCase(); 104 return isValidDomain(newDomain) && effDomainMatches(newDomain, newHost) 105 && isValidHost(newDomain, newHost); 106 } 107 108 private static boolean effDomainMatches(String domain, String host) { 109 // calculate effective host name 110 String effHost = host.indexOf(DOT_STR) != -1 ? host 111 : (host + LOCAL_STR); 112 113 // Rule 2: domain and host are string-compare equal, or A = NB, B = .B' 114 // and N is a non-empty name string 115 boolean inDomain = domain.equals(effHost); 116 inDomain = inDomain 117 || (effHost.endsWith(domain) 118 && effHost.length() > domain.length() && domain 119 .startsWith(DOT_STR)); 120 return inDomain; 121 } 122 123 private static boolean isCommaDelim(HttpCookie cookie) { 124 String value = cookie.getValue(); 125 if (value.startsWith(QUOTE_STR) && value.endsWith(QUOTE_STR)) { 126 cookie.setValue(value.substring(1, value.length() - 1)); 127 return false; 128 } 129 if (cookie.getVersion() == 1 && value.contains(COMMA_STR)) { 130 cookie.setValue(value.substring(0, value.indexOf(COMMA_STR))); 131 return true; 132 } 133 return false; 134 } 135 136 private static boolean isValidDomain(String domain) { 137 // Rule 1: The value for Domain contains embedded dots, or is .local 138 if (domain.length() <= 2) { 139 return false; 140 } 141 return domain.substring(1, domain.length() - 1).indexOf(DOT_STR) != -1 142 || domain.equals(LOCAL_STR); 143 } 144 145 private static boolean isValidHost(String domain, String host) { 146 // Rule 3: host does not end with domain, or the remainder does not 147 // contain "." 148 boolean matches = !host.endsWith(domain); 149 if (!matches) { 150 String hostSub = host.substring(0, host.length() - domain.length()); 151 matches = hostSub.indexOf(DOT_STR) == -1; 152 } 153 return matches; 154 } 155 156 /** 157 * Constructs a cookie from a string. The string should comply with 158 * set-cookie or set-cookie2 header format as specified in RFC 2965. Since 159 * set-cookies2 syntax allows more than one cookie definitions in one 160 * header, the returned object is a list. 161 * 162 * @param header 163 * a set-cookie or set-cookie2 header. 164 * @return a list of constructed cookies 165 * @throws IllegalArgumentException 166 * if the string does not comply with cookie specification, or 167 * the cookie name contains illegal characters, or reserved 168 * tokens of cookie specification appears 169 * @throws NullPointerException 170 * if header is null 171 */ 172 public static List<HttpCookie> parse(String header) { 173 Matcher matcher = HEAD_PATTERN.matcher(header); 174 // Parse cookie name & value 175 List<HttpCookie> list = null; 176 HttpCookie cookie = null; 177 String headerString = header; 178 int version = 0; 179 // process set-cookie | set-cookie2 head 180 if (matcher.find()) { 181 String cookieHead = matcher.group(); 182 if ("set-cookie2:".equalsIgnoreCase(cookieHead)) { 183 version = 1; 184 } 185 headerString = header.substring(cookieHead.length()); 186 } 187 188 // parse cookie name/value pair 189 matcher = NAME_PATTERN.matcher(headerString); 190 if (matcher.lookingAt()) { 191 list = new ArrayList<HttpCookie>(); 192 cookie = new HttpCookie(matcher.group(1), matcher.group(2)); 193 cookie.setVersion(version); 194 195 /* 196 * Comma is a delimiter in cookie spec 1.1. If find comma in version 197 * 1 cookie header, part of matched string need to be spitted out. 198 */ 199 String nameGroup = matcher.group(); 200 if (isCommaDelim(cookie)) { 201 headerString = headerString.substring(nameGroup 202 .indexOf(COMMA_STR)); 203 } else { 204 headerString = headerString.substring(nameGroup.length()); 205 } 206 list.add(cookie); 207 } else { 208 throw new IllegalArgumentException(); 209 } 210 211 // parse cookie headerString 212 while (!(headerString.length() == 0)) { 213 matcher = cookie.getVersion() == 1 ? ATTR_PATTERN1 214 .matcher(headerString) : ATTR_PATTERN0 215 .matcher(headerString); 216 217 if (matcher.lookingAt()) { 218 String attrName = matcher.group(1).trim(); 219 220 // handle special situation like: <..>;;<..> 221 if (attrName.length() == 0) { 222 headerString = headerString.substring(1); 223 continue; 224 } 225 226 // If port is the attribute, then comma will not be used as a 227 // delimiter 228 if (attrName.equalsIgnoreCase("port") 229 || attrName.equalsIgnoreCase("expires")) { 230 int start = matcher.regionStart(); 231 matcher = ATTR_PATTERN0.matcher(headerString); 232 matcher.region(start, headerString.length()); 233 matcher.lookingAt(); 234 } else if (cookie.getVersion() == 1 235 && attrName.startsWith(COMMA_STR)) { 236 // If the last encountered token is comma, and the parsed 237 // attribute is not port, then this attribute/value pair 238 // ends. 239 headerString = headerString.substring(1); 240 matcher = NAME_PATTERN.matcher(headerString); 241 if (matcher.lookingAt()) { 242 cookie = new HttpCookie(matcher.group(1), matcher 243 .group(2)); 244 list.add(cookie); 245 headerString = headerString.substring(matcher.group() 246 .length()); 247 continue; 248 } 249 } 250 251 Setter setter = cookie.attributeSet.get(attrName.toLowerCase()); 252 if (null == setter) { 253 throw new IllegalArgumentException(); 254 } 255 if (!setter.isSet()) { 256 String attrValue = matcher.group(2); 257 setter.validate(attrValue, cookie); 258 setter.setValue(matcher.group(2), cookie); 259 } 260 headerString = headerString.substring(matcher.end()); 261 } 262 } 263 264 return list; 265 } 266 267 private String comment; 268 269 private String commentURL; 270 271 private boolean discard; 272 273 private String domain; 274 275 private long maxAge = -1l; 276 277 private String name; 278 279 private String path; 280 281 private String portList; 282 283 private boolean secure; 284 285 private String value; 286 287 private int version = 1; 288 289 { 290 attributeSet.put("comment", new Setter() { 291 @Override 292 void setValue(String value, HttpCookie cookie) { 293 cookie.setComment(value); 294 if (cookie.getComment() != null) { 295 set(true); 296 } 297 } 298 }); 299 attributeSet.put("commenturl", new Setter() { 300 @Override 301 void setValue(String value, HttpCookie cookie) { 302 cookie.setCommentURL(value); 303 if (cookie.getCommentURL() != null) { 304 set(true); 305 } 306 } 307 }); 308 attributeSet.put("discard", new Setter() { 309 @Override 310 void setValue(String value, HttpCookie cookie) { 311 cookie.setDiscard(true); 312 set(true); 313 } 314 }); 315 attributeSet.put("domain", new Setter() { 316 @Override 317 void setValue(String value, HttpCookie cookie) { 318 cookie.setDomain(value); 319 if (cookie.getDomain() != null) { 320 set(true); 321 } 322 } 323 }); 324 attributeSet.put("max-age", new Setter() { 325 @Override 326 void setValue(String value, HttpCookie cookie) { 327 try { 328 cookie.setMaxAge(Long.parseLong(value)); 329 } catch (NumberFormatException e) { 330 throw new IllegalArgumentException(Msg.getString( 331 "KB001", "max-age")); 332 } 333 set(true); 334 335 if (!attributeSet.get("version").isSet()) { 336 cookie.setVersion(1); 337 } 338 } 339 }); 340 341 attributeSet.put("path", new Setter() { 342 @Override 343 void setValue(String value, HttpCookie cookie) { 344 cookie.setPath(value); 345 if (cookie.getPath() != null) { 346 set(true); 347 } 348 } 349 }); 350 attributeSet.put("port", new Setter() { 351 @Override 352 void setValue(String value, HttpCookie cookie) { 353 cookie.setPortlist(value); 354 if (cookie.getPortlist() != null) { 355 set(true); 356 } 357 } 358 359 @Override 360 void validate(String v, HttpCookie cookie) { 361 return; 362 } 363 }); 364 attributeSet.put("secure", new Setter() { 365 @Override 366 void setValue(String value, HttpCookie cookie) { 367 cookie.setSecure(true); 368 set(true); 369 } 370 }); 371 attributeSet.put("version", new Setter() { 372 @Override 373 void setValue(String value, HttpCookie cookie) { 374 try { 375 int v = Integer.parseInt(value); 376 if (v > cookie.getVersion()) { 377 cookie.setVersion(v); 378 } 379 } catch (NumberFormatException e) { 380 throw new IllegalArgumentException(Msg.getString( 381 "KB001", "version")); 382 } 383 if (cookie.getVersion() != 0) { 384 set(true); 385 } 386 } 387 }); 388 389 attributeSet.put("expires", new Setter() { 390 @Override 391 void setValue(String value, HttpCookie cookie) { 392 cookie.setVersion(0); 393 attributeSet.get("version").set(true); 394 if (!attributeSet.get("max-age").isSet()) { 395 attributeSet.get("max-age").set(true); 396 if (!"en".equalsIgnoreCase(Locale.getDefault() 397 .getLanguage())) { 398 cookie.setMaxAge(0); 399 return; 400 } 401 try { 402 cookie.setMaxAge((Date.parse(value) - System 403 .currentTimeMillis()) / 1000); 404 } catch (IllegalArgumentException e) { 405 cookie.setMaxAge(0); 406 } 407 } 408 } 409 410 @Override 411 void validate(String v, HttpCookie cookie) { 412 return; 413 } 414 }); 415 } 416 417 /** 418 * Initializes a cookie with the specified name and value. 419 * 420 * The name attribute can just contain ASCII characters, which is immutable 421 * after creation. Commas, white space and semicolons are not allowed. The $ 422 * character is also not allowed to be the beginning of the name. 423 * 424 * The value attribute depends on what the server side is interested. The 425 * setValue method can be used to change it. 426 * 427 * RFC 2965 is the default cookie specification of this class. If one wants 428 * to change the version of the cookie, the setVersion method is available. 429 * 430 * @param name - 431 * the specific name of the cookie 432 * @param value - 433 * the specific value of the cookie 434 * 435 * @throws IllegalArgumentException - 436 * if the name contains not-allowed or reserved characters 437 * 438 * @throws NullPointerException 439 * if the value of name is null 440 */ 441 public HttpCookie(String name, String value) { 442 String ntrim = name.trim(); // erase leading and trailing whitespaces 443 if (!isValidName(ntrim)) { 444 throw new IllegalArgumentException(Msg.getString("KB002")); 445 } 446 447 this.name = ntrim; 448 this.value = value; 449 } 450 451 private void attrToString(StringBuilder builder, String attrName, 452 String attrValue) { 453 if (attrValue != null && builder != null) { 454 builder.append(";"); 455 builder.append("$"); 456 builder.append(attrName); 457 builder.append("=\""); 458 builder.append(attrValue); 459 builder.append(QUOTE_STR); 460 } 461 } 462 463 @Override 464 public Object clone() { 465 try { 466 HttpCookie obj = (HttpCookie) super.clone(); 467 return obj; 468 } catch (CloneNotSupportedException e) { 469 return null; 470 } 471 } 472 473 /** 474 * Returns true if {@code obj} equals this cookie. Two cookies are equal if they have 475 * the same domain and name in a case-insensitive mode and path in a 476 * case-sensitive mode. 477 */ 478 @Override 479 public boolean equals(Object obj) { 480 if (obj == this) { 481 return true; 482 } 483 if (obj instanceof HttpCookie) { 484 HttpCookie anotherCookie = (HttpCookie) obj; 485 if (name.equalsIgnoreCase(anotherCookie.getName())) { 486 String anotherDomain = anotherCookie.getDomain(); 487 boolean equals = domain == null ? anotherDomain == null 488 : domain.equalsIgnoreCase(anotherDomain); 489 if (equals) { 490 String anotherPath = anotherCookie.getPath(); 491 return path == null ? anotherPath == null : path 492 .equals(anotherPath); 493 } 494 } 495 } 496 return false; 497 } 498 499 /** 500 * Returns the value of the "comment" attribute (specified in RFC 2965) of this cookie. 501 */ 502 public String getComment() { 503 return comment; 504 } 505 506 /** 507 * Returns the value of the "commentURL" attribute (specified in RFC 2965) of this cookie. 508 */ 509 public String getCommentURL() { 510 return commentURL; 511 } 512 513 /** 514 * Returns the value of the "discard" attribute (specified in RFC 2965) of this cookie. 515 */ 516 public boolean getDiscard() { 517 return discard; 518 } 519 520 /** 521 * Returns the domain name for this cookie in the format specified in RFC 2965. 522 */ 523 public String getDomain() { 524 return domain; 525 } 526 527 /** 528 * Returns the Max-Age value for this cookie as specified in RFC 2965. 529 */ 530 public long getMaxAge() { 531 return maxAge; 532 } 533 534 /** 535 * Returns the name of this cookie. 536 */ 537 public String getName() { 538 return name; 539 } 540 541 /** 542 * Returns the path part of a request URL to which this cookie is returned. 543 * This cookie is visible to all subpaths. 544 */ 545 public String getPath() { 546 return path; 547 } 548 549 /** 550 * Returns the value of the "portlist" attribute (specified in RFC 2965) of this cookie. 551 */ 552 public String getPortlist() { 553 return portList; 554 } 555 556 /** 557 * Returns true if the browser only sends cookies over a secure protocol. 558 * False if it can send cookies through any protocols. 559 */ 560 public boolean getSecure() { 561 return secure; 562 } 563 564 /** 565 * Returns the value of this cookie. 566 */ 567 public String getValue() { 568 return value; 569 } 570 571 /** 572 * Get the version of this cookie 573 * 574 * @return 0 indicates the original Netscape cookie specification, while 1 575 * indicates RFC 2965/2109 specification. 576 */ 577 public int getVersion() { 578 return version; 579 } 580 581 /** 582 * Returns true if this cookie has expired. 583 */ 584 public boolean hasExpired() { 585 // -1 indicates the cookie will persist until browser shutdown 586 // so the cookie is not expired. 587 if (maxAge == -1l) { 588 return false; 589 } 590 591 boolean expired = false; 592 if (maxAge <= 0l) { 593 expired = true; 594 } 595 return expired; 596 } 597 598 /** 599 * Returns the hash code of this HTTP cookie. The hash code is 600 * {@code getName().toLowerCase().hashCode() + getDomain().toLowerCase().hashCode() + getPath().hashCode()}. 601 */ 602 @Override 603 public int hashCode() { 604 int hashCode = name.toLowerCase().hashCode(); 605 hashCode += domain == null ? 0 : domain.toLowerCase().hashCode(); 606 hashCode += path == null ? 0 : path.hashCode(); 607 return hashCode; 608 } 609 610 private boolean isValidName(String n) { 611 // name cannot be empty or begin with '$' or equals the reserved 612 // attributes (case-insensitive) 613 boolean isValid = !(n.length() == 0 || n.startsWith("$") || attributeSet.containsKey(n.toLowerCase())); 614 if (isValid) { 615 for (int i = 0; i < n.length(); i++) { 616 char nameChar = n.charAt(i); 617 // name must be ASCII characters and cannot contain ';', ',' and 618 // whitespace 619 if (nameChar < 0 620 || nameChar >= 127 621 || nameChar == ';' 622 || nameChar == ',' 623 || (Character.isWhitespace(nameChar) && nameChar != ' ')) { 624 isValid = false; 625 break; 626 } 627 } 628 } 629 return isValid; 630 } 631 632 /** 633 * Set the value of comment attribute(specified in RFC 2965) of this cookie. 634 * 635 * @param purpose 636 * the comment value to be set 637 */ 638 public void setComment(String purpose) { 639 comment = purpose; 640 } 641 642 /** 643 * Set the value of commentURL attribute(specified in RFC 2965) of this 644 * cookie. 645 * 646 * @param purpose 647 * the value of commentURL attribute to be set 648 */ 649 public void setCommentURL(String purpose) { 650 commentURL = purpose; 651 } 652 653 /** 654 * Set the value of discard attribute(specified in RFC 2965) of this cookie. 655 * 656 * @param discard 657 * the value for discard attribute 658 */ 659 public void setDiscard(boolean discard) { 660 this.discard = discard; 661 } 662 663 /** 664 * Set the domain value for this cookie. Browsers send the cookie to the 665 * domain specified by this value. The form of the domain is specified in 666 * RFC 2965. 667 * 668 * @param pattern 669 * the domain pattern 670 */ 671 public void setDomain(String pattern) { 672 domain = pattern == null ? null : pattern.toLowerCase(); 673 } 674 675 /** 676 * Sets the Max-Age value as specified in RFC 2965 of this cookie to expire. 677 * 678 * @param expiry 679 * the value used to set the Max-Age value of this cookie 680 */ 681 public void setMaxAge(long expiry) { 682 maxAge = expiry; 683 } 684 685 /** 686 * Set the path to which this cookie is returned. This cookie is visible to 687 * all the pages under the path and all subpaths. 688 * 689 * @param path 690 * the path to which this cookie is returned 691 */ 692 public void setPath(String path) { 693 this.path = path; 694 } 695 696 /** 697 * Set the value of port attribute(specified in RFC 2965) of this cookie. 698 * 699 * @param ports 700 * the value for port attribute 701 */ 702 public void setPortlist(String ports) { 703 portList = ports; 704 } 705 706 /* 707 * Handle 2 special cases: 1. value is wrapped by a quotation 2. value 708 * contains comma 709 */ 710 711 /** 712 * Tells the browser whether the cookies should be sent to server through 713 * secure protocols. 714 * 715 * @param flag 716 * tells browser to send cookie to server only through secure 717 * protocol if flag is true 718 */ 719 public void setSecure(boolean flag) { 720 secure = flag; 721 } 722 723 /** 724 * Sets the value for this cookie after it has been instantiated. String 725 * newValue can be in BASE64 form. If the version of the cookie is 0, 726 * special value as: white space, brackets, parentheses, equals signs, 727 * commas, double quotes, slashes, question marks, at signs, colons, and 728 * semicolons are not recommended. Empty values may lead to different 729 * behavior on different browsers. 730 * 731 * @param newValue 732 * the value for this cookie 733 */ 734 public void setValue(String newValue) { 735 // FIXME: According to spec, version 0 cookie value does not allow many 736 // symbols. But RI does not implement it. Follow RI temporarily. 737 value = newValue; 738 } 739 740 /** 741 * Sets the version of the cookie. 0 indicates the original Netscape cookie 742 * specification, while 1 indicates RFC 2965/2109 specification. 743 * 744 * @param v 745 * 0 or 1 as stated above 746 * @throws IllegalArgumentException 747 * if v is neither 0 nor 1 748 */ 749 public void setVersion(int v) { 750 if (v != 0 && v != 1) { 751 throw new IllegalArgumentException(Msg.getString("KB003")); 752 } 753 version = v; 754 } 755 756 /** 757 * Returns a string to represent the cookie. The format of string follows 758 * the cookie specification. The leading token "Cookie" is not included 759 * 760 * @return the string format of the cookie object 761 */ 762 @Override 763 public String toString() { 764 StringBuilder cookieStr = new StringBuilder(); 765 cookieStr.append(name); 766 cookieStr.append("="); 767 if (version == 0) { 768 cookieStr.append(value); 769 } else if (version == 1) { 770 cookieStr.append(QUOTE_STR); 771 cookieStr.append(value); 772 cookieStr.append(QUOTE_STR); 773 774 attrToString(cookieStr, "Path", path); 775 attrToString(cookieStr, "Domain", domain); 776 attrToString(cookieStr, "Port", portList); 777 } 778 return cookieStr.toString(); 779 } 780} 781