1 /*
   2  * Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package java.net;
  27 
  28 import java.util.List;
  29 import java.util.StringTokenizer;
  30 import java.util.NoSuchElementException;
  31 import java.text.SimpleDateFormat;
  32 import java.util.TimeZone;
  33 import java.util.Date;
  34 
  35 import java.lang.NullPointerException;  // for javadoc
  36 import java.util.Locale;
  37 
  38 /**
  39  * An HttpCookie object represents an http cookie, which carries state
  40  * information between server and user agent. Cookie is widely adopted
  41  * to create stateful sessions.
  42  *
  43  * <p>There are 3 http cookie specifications:
  44  * <blockquote>
  45  *   Netscape draft<br>
  46  *   RFC 2109 - <a href="http://www.ietf.org/rfc/rfc2109.txt">
  47  * <i>http://www.ietf.org/rfc/rfc2109.txt</i></a><br>
  48  *   RFC 2965 - <a href="http://www.ietf.org/rfc/rfc2965.txt">
  49  * <i>http://www.ietf.org/rfc/rfc2965.txt</i></a>
  50  * </blockquote>
  51  *
  52  * <p>HttpCookie class can accept all these 3 forms of syntax.
  53  *
  54  * @author Edward Wang
  55  * @since 1.6
  56  */
  57 public final class HttpCookie implements Cloneable {
  58     /* ---------------- Fields -------------- */
  59 
  60     //
  61     // The value of the cookie itself.
  62     //
  63 
  64     private String name;        // NAME= ... "$Name" style is reserved
  65     private String value;       // value of NAME
  66 
  67     //
  68     // Attributes encoded in the header's cookie fields.
  69     //
  70 
  71     private String comment;     // Comment=VALUE ... describes cookie's use
  72     private String commentURL;  // CommentURL="http URL" ... describes cookie's use
  73     private boolean toDiscard;  // Discard ... discard cookie unconditionally
  74     private String domain;      // Domain=VALUE ... domain that sees cookie
  75     private long maxAge = MAX_AGE_UNSPECIFIED;  // Max-Age=VALUE ... cookies auto-expire
  76     private String path;        // Path=VALUE ... URLs that see the cookie
  77     private String portlist;    // Port[="portlist"] ... the port cookie may be returned to
  78     private boolean secure;     // Secure ... e.g. use SSL
  79     private boolean httpOnly;   // HttpOnly ... i.e. not accessible to scripts
  80     private int version = 1;    // Version=1 ... RFC 2965 style
  81 
  82     //
  83     // Hold the creation time (in seconds) of the http cookie for later
  84     // expiration calculation
  85     //
  86     private long whenCreated = 0;
  87 
  88 
  89     //
  90     // Since the positive and zero max-age have their meanings,
  91     // this value serves as a hint as 'not specify max-age'
  92     //
  93     private final static long MAX_AGE_UNSPECIFIED = -1;
  94 
  95 
  96     //
  97     // date formats used by Netscape's cookie draft
  98     // as well as formats seen on various sites
  99     //
 100     private final static String[] COOKIE_DATE_FORMATS = {
 101         "EEE',' dd-MMM-yyyy HH:mm:ss 'GMT'",
 102         "EEE',' dd MMM yyyy HH:mm:ss 'GMT'",
 103         "EEE MMM dd yyyy HH:mm:ss 'GMT'Z"
 104     };
 105 
 106     //
 107     // constant strings represent set-cookie header token
 108     //
 109     private final static String SET_COOKIE = "set-cookie:";
 110     private final static String SET_COOKIE2 = "set-cookie2:";
 111 
 112 
 113     /* ---------------- Ctors -------------- */
 114 
 115     /**
 116      * Constructs a cookie with a specified name and value.
 117      *
 118      * <p>The name must conform to RFC 2965. That means it can contain
 119      * only ASCII alphanumeric characters and cannot contain commas,
 120      * semicolons, or white space or begin with a $ character. The cookie's
 121      * name cannot be changed after creation.
 122      *
 123      * <p>The value can be anything the server chooses to send. Its
 124      * value is probably of interest only to the server. The cookie's
 125      * value can be changed after creation with the
 126      * <code>setValue</code> method.
 127      *
 128      * <p>By default, cookies are created according to the RFC 2965
 129      * cookie specification. The version can be changed with the
 130      * <code>setVersion</code> method.
 131      *
 132      *
 133      * @param name                      a <code>String</code> specifying the name of the cookie
 134      *
 135      * @param value                     a <code>String</code> specifying the value of the cookie
 136      *
 137      * @throws IllegalArgumentException if the cookie name contains illegal characters
 138      *                                  or it is one of the tokens reserved for use
 139      *                                  by the cookie protocol
 140      * @throws NullPointerException     if <tt>name</tt> is <tt>null</tt>
 141      * @see #setValue
 142      * @see #setVersion
 143      *
 144      */
 145 
 146     public HttpCookie(String name, String value) {
 147         name = name.trim();
 148         if (name.length() == 0 || !isToken(name) || isReserved(name)) {
 149             throw new IllegalArgumentException("Illegal cookie name");
 150         }
 151 
 152         this.name = name;
 153         this.value = value;
 154         toDiscard = false;
 155         secure = false;
 156 
 157         whenCreated = System.currentTimeMillis();
 158         portlist = null;
 159     }
 160 
 161 
 162     /**
 163      * Constructs cookies from set-cookie or set-cookie2 header string.
 164      * RFC 2965 section 3.2.2 set-cookie2 syntax indicates that one header line
 165      * may contain more than one cookie definitions, so this is a static
 166      * utility method instead of another constructor.
 167      *
 168      * @param header    a <tt>String</tt> specifying the set-cookie header.
 169      *                  The header should start with "set-cookie", or "set-cookie2"
 170      *                  token; or it should have no leading token at all.
 171      * @return          a List of cookie parsed from header line string
 172      * @throws IllegalArgumentException if header string violates the cookie
 173      *                                  specification's syntax, or the cookie
 174      *                                  name contains llegal characters, or
 175      *                                  the cookie name is one of the tokens
 176      *                                  reserved for use by the cookie protocol
 177      * @throws NullPointerException     if the header string is <tt>null</tt>
 178      */
 179     public static List<HttpCookie> parse(String header) {
 180         int version = guessCookieVersion(header);
 181 
 182         // if header start with set-cookie or set-cookie2, strip it off
 183         if (startsWithIgnoreCase(header, SET_COOKIE2)) {
 184             header = header.substring(SET_COOKIE2.length());
 185         } else if (startsWithIgnoreCase(header, SET_COOKIE)) {
 186             header = header.substring(SET_COOKIE.length());
 187         }
 188 
 189 
 190         List<HttpCookie> cookies = new java.util.ArrayList<HttpCookie>();
 191         // The Netscape cookie may have a comma in its expires attribute,
 192         // while the comma is the delimiter in rfc 2965/2109 cookie header string.
 193         // so the parse logic is slightly different
 194         if (version == 0) {
 195             // Netscape draft cookie
 196             HttpCookie cookie = parseInternal(header);
 197             cookie.setVersion(0);
 198             cookies.add(cookie);
 199         } else {
 200             // rfc2965/2109 cookie
 201             // if header string contains more than one cookie,
 202             // it'll separate them with comma
 203             List<String> cookieStrings = splitMultiCookies(header);
 204             for (String cookieStr : cookieStrings) {
 205                 HttpCookie cookie = parseInternal(cookieStr);
 206                 cookie.setVersion(1);
 207                 cookies.add(cookie);
 208             }
 209         }
 210 
 211         return cookies;
 212     }
 213 
 214 
 215 
 216 
 217     /* ---------------- Public operations -------------- */
 218 
 219 
 220     /**
 221      * Reports whether this http cookie has expired or not.
 222      *
 223      * @return  <tt>true</tt> to indicate this http cookie has expired;
 224      *          otherwise, <tt>false</tt>
 225      */
 226     public boolean hasExpired() {
 227         if (maxAge == 0) return true;
 228 
 229         // if not specify max-age, this cookie should be
 230         // discarded when user agent is to be closed, but
 231         // it is not expired.
 232         if (maxAge == MAX_AGE_UNSPECIFIED) return false;
 233 
 234         long deltaSecond = (System.currentTimeMillis() - whenCreated) / 1000;
 235         if (deltaSecond > maxAge)
 236             return true;
 237         else
 238             return false;
 239     }
 240 
 241     /**
 242      *
 243      * Specifies a comment that describes a cookie's purpose.
 244      * The comment is useful if the browser presents the cookie
 245      * to the user. Comments
 246      * are not supported by Netscape Version 0 cookies.
 247      *
 248      * @param purpose           a <code>String</code> specifying the comment
 249      *                          to display to the user
 250      *
 251      * @see #getComment
 252      *
 253      */
 254 
 255     public void setComment(String purpose) {
 256         comment = purpose;
 257     }
 258 
 259 
 260 
 261 
 262     /**
 263      * Returns the comment describing the purpose of this cookie, or
 264      * <code>null</code> if the cookie has no comment.
 265      *
 266      * @return                  a <code>String</code> containing the comment,
 267      *                          or <code>null</code> if none
 268      *
 269      * @see #setComment
 270      *
 271      */
 272 
 273     public String getComment() {
 274         return comment;
 275     }
 276 
 277 
 278     /**
 279      *
 280      * Specifies a comment url that describes a cookie's purpose.
 281      * The comment url is useful if the browser presents the cookie
 282      * to the user. Comment url is RFC 2965 only.
 283      *
 284      * @param purpose           a <code>String</code> specifying the comment url
 285      *                          to display to the user
 286      *
 287      * @see #getCommentURL
 288      *
 289      */
 290 
 291     public void setCommentURL(String purpose) {
 292         commentURL = purpose;
 293     }
 294 
 295 
 296 
 297 
 298     /**
 299      * Returns the comment url describing the purpose of this cookie, or
 300      * <code>null</code> if the cookie has no comment url.
 301      *
 302      * @return                  a <code>String</code> containing the comment url,
 303      *                          or <code>null</code> if none
 304      *
 305      * @see #setCommentURL
 306      *
 307      */
 308 
 309     public String getCommentURL() {
 310         return commentURL;
 311     }
 312 
 313 
 314     /**
 315      * Specify whether user agent should discard the cookie unconditionally.
 316      * This is RFC 2965 only attribute.
 317      *
 318      * @param discard   <tt>true</tt> indicates to discard cookie unconditionally
 319      *
 320      * @see #getDiscard
 321      */
 322 
 323     public void setDiscard(boolean discard) {
 324         toDiscard = discard;
 325     }
 326 
 327 
 328 
 329 
 330     /**
 331      * Return the discard attribute of the cookie
 332      *
 333      * @return  a <tt>boolean</tt> to represent this cookie's discard attribute
 334      *
 335      * @see #setDiscard
 336      */
 337 
 338     public boolean getDiscard() {
 339         return toDiscard;
 340     }
 341 
 342 
 343     /**
 344      * Specify the portlist of the cookie, which restricts the port(s)
 345      * to which a cookie may be sent back in a Cookie header.
 346      *
 347      * @param ports     a <tt>String</tt> specify the port list, which is
 348      *                  comma seperated series of digits
 349      * @see #getPortlist
 350      */
 351 
 352     public void setPortlist(String ports) {
 353         portlist = ports;
 354     }
 355 
 356 
 357 
 358 
 359     /**
 360      * Return the port list attribute of the cookie
 361      *
 362      * @return  a <tt>String</tt> contains the port list
 363      *          or <tt>null</tt> if none
 364      * @see #setPortlist
 365      */
 366 
 367     public String getPortlist() {
 368         return portlist;
 369     }
 370 
 371     /**
 372      *
 373      * Specifies the domain within which this cookie should be presented.
 374      *
 375      * <p>The form of the domain name is specified by RFC 2965. A domain
 376      * name begins with a dot (<code>.foo.com</code>) and means that
 377      * the cookie is visible to servers in a specified Domain Name System
 378      * (DNS) zone (for example, <code>www.foo.com</code>, but not
 379      * <code>a.b.foo.com</code>). By default, cookies are only returned
 380      * to the server that sent them.
 381      *
 382      *
 383      * @param pattern           a <code>String</code> containing the domain name
 384      *                          within which this cookie is visible;
 385      *                          form is according to RFC 2965
 386      *
 387      * @see #getDomain
 388      *
 389      */
 390 
 391     public void setDomain(String pattern) {
 392         if (pattern != null)
 393             domain = pattern.toLowerCase();
 394         else
 395             domain = pattern;
 396     }
 397 
 398 
 399 
 400 
 401 
 402     /**
 403      * Returns the domain name set for this cookie. The form of
 404      * the domain name is set by RFC 2965.
 405      *
 406      * @return                  a <code>String</code> containing the domain name
 407      *
 408      * @see #setDomain
 409      *
 410      */
 411 
 412     public String getDomain() {
 413         return domain;
 414     }
 415 
 416 
 417     /**
 418      * Sets the maximum age of the cookie in seconds.
 419      *
 420      * <p>A positive value indicates that the cookie will expire
 421      * after that many seconds have passed. Note that the value is
 422      * the <i>maximum</i> age when the cookie will expire, not the cookie's
 423      * current age.
 424      *
 425      * <p>A negative value means
 426      * that the cookie is not stored persistently and will be deleted
 427      * when the Web browser exits. A zero value causes the cookie
 428      * to be deleted.
 429      *
 430      * @param expiry            an integer specifying the maximum age of the
 431      *                          cookie in seconds; if zero, the cookie
 432      *                          should be discarded immediately;
 433      *                          otherwise, the cookie's max age is unspecified.
 434      *
 435      * @see #getMaxAge
 436      *
 437      */
 438     public void setMaxAge(long expiry) {
 439         maxAge = expiry;
 440     }
 441 
 442 
 443 
 444 
 445     /**
 446      * Returns the maximum age of the cookie, specified in seconds.
 447      * By default, <code>-1</code> indicating the cookie will persist
 448      * until browser shutdown.
 449      *
 450      *
 451      * @return                  an integer specifying the maximum age of the
 452      *                          cookie in seconds
 453      *
 454      *
 455      * @see #setMaxAge
 456      *
 457      */
 458 
 459     public long getMaxAge() {
 460         return maxAge;
 461     }
 462 
 463 
 464 
 465 
 466     /**
 467      * Specifies a path for the cookie
 468      * to which the client should return the cookie.
 469      *
 470      * <p>The cookie is visible to all the pages in the directory
 471      * you specify, and all the pages in that directory's subdirectories.
 472      * A cookie's path must include the servlet that set the cookie,
 473      * for example, <i>/catalog</i>, which makes the cookie
 474      * visible to all directories on the server under <i>/catalog</i>.
 475      *
 476      * <p>Consult RFC 2965 (available on the Internet) for more
 477      * information on setting path names for cookies.
 478      *
 479      *
 480      * @param uri               a <code>String</code> specifying a path
 481      *
 482      *
 483      * @see #getPath
 484      *
 485      */
 486 
 487     public void setPath(String uri) {
 488         path = uri;
 489     }
 490 
 491 
 492 
 493 
 494     /**
 495      * Returns the path on the server
 496      * to which the browser returns this cookie. The
 497      * cookie is visible to all subpaths on the server.
 498      *
 499      *
 500      * @return          a <code>String</code> specifying a path that contains
 501      *                  a servlet name, for example, <i>/catalog</i>
 502      *
 503      * @see #setPath
 504      *
 505      */
 506 
 507     public String getPath() {
 508         return path;
 509     }
 510 
 511 
 512 
 513 
 514 
 515     /**
 516      * Indicates whether the cookie should only be sent using a secure protocol,
 517      * such as HTTPS or SSL.
 518      *
 519      * <p>The default value is <code>false</code>.
 520      *
 521      * @param flag      If <code>true</code>, the cookie can only be sent over
 522      *                  a secure protocol like https.
 523      *                  If <code>false</code>, it can be sent over any protocol.
 524      *
 525      * @see #getSecure
 526      *
 527      */
 528 
 529     public void setSecure(boolean flag) {
 530         secure = flag;
 531     }
 532 
 533 
 534 
 535 
 536     /**
 537      * Returns <code>true</code> if sending this cookie should be
 538      * restricted to a secure protocol, or <code>false</code> if the
 539      * it can be sent using any protocol.
 540      *
 541      * @return          <code>false</code> if the cookie can be sent over
 542      *                  any standard protocol; otherwise, <code>true</code>
 543      *
 544      * @see #setSecure
 545      *
 546      */
 547 
 548     public boolean getSecure() {
 549         return secure;
 550     }
 551 
 552 
 553 
 554 
 555 
 556     /**
 557      * Returns the name of the cookie. The name cannot be changed after
 558      * creation.
 559      *
 560      * @return          a <code>String</code> specifying the cookie's name
 561      *
 562      */
 563 
 564     public String getName() {
 565         return name;
 566     }
 567 
 568 
 569 
 570 
 571 
 572     /**
 573      *
 574      * Assigns a new value to a cookie after the cookie is created.
 575      * If you use a binary value, you may want to use BASE64 encoding.
 576      *
 577      * <p>With Version 0 cookies, values should not contain white
 578      * space, brackets, parentheses, equals signs, commas,
 579      * double quotes, slashes, question marks, at signs, colons,
 580      * and semicolons. Empty values may not behave the same way
 581      * on all browsers.
 582      *
 583      * @param newValue          a <code>String</code> specifying the new value
 584      *
 585      *
 586      * @see #getValue
 587      *
 588      */
 589 
 590     public void setValue(String newValue) {
 591         value = newValue;
 592     }
 593 
 594 
 595 
 596 
 597     /**
 598      * Returns the value of the cookie.
 599      *
 600      * @return                  a <code>String</code> containing the cookie's
 601      *                          present value
 602      *
 603      * @see #setValue
 604      *
 605      */
 606 
 607     public String getValue() {
 608         return value;
 609     }
 610 
 611 
 612 
 613 
 614     /**
 615      * Returns the version of the protocol this cookie complies
 616      * with. Version 1 complies with RFC 2965/2109,
 617      * and version 0 complies with the original
 618      * cookie specification drafted by Netscape. Cookies provided
 619      * by a browser use and identify the browser's cookie version.
 620      *
 621      *
 622      * @return                  0 if the cookie complies with the
 623      *                          original Netscape specification; 1
 624      *                          if the cookie complies with RFC 2965/2109
 625      *
 626      * @see #setVersion
 627      *
 628      */
 629 
 630     public int getVersion() {
 631         return version;
 632     }
 633 
 634 
 635 
 636 
 637     /**
 638      * Sets the version of the cookie protocol this cookie complies
 639      * with. Version 0 complies with the original Netscape cookie
 640      * specification. Version 1 complies with RFC 2965/2109.
 641      *
 642      *
 643      * @param v                 0 if the cookie should comply with
 644      *                          the original Netscape specification;
 645      *                          1 if the cookie should comply with RFC 2965/2109
 646      *
 647      * @throws IllegalArgumentException if <tt>v</tt> is neither 0 nor 1
 648      *
 649      * @see #getVersion
 650      *
 651      */
 652 
 653     public void setVersion(int v) {
 654         if (v != 0 && v != 1) {
 655             throw new IllegalArgumentException("cookie version should be 0 or 1");
 656         }
 657 
 658         version = v;
 659     }
 660 
 661     /**
 662      * Returns {@code true} if this cookie contains the <i>HttpOnly</i>
 663      * attribute. This means that the cookie should not be accessible to
 664      * scripting engines, like javascript.
 665      *
 666      * @return {@code true} if this cookie should be considered http only.
 667      * @see #setHttpOnly(boolean)
 668      */
 669     public boolean isHttpOnly()
 670     {
 671         return httpOnly;
 672     }
 673 
 674     /**
 675      * Indicates whether the cookie should be considered HTTP Only. If set to
 676      * {@code true} it means the cookie should not be accessible to scripting
 677      * engines like javascript.
 678      *
 679      * @param httpOnly if {@code true} make the cookie HTTP only, i.e.
 680      *                 only visible as part of an HTTP request.
 681      * @see #isHttpOnly()
 682      */
 683     public void setHttpOnly(boolean httpOnly)
 684     {
 685         this.httpOnly = httpOnly;
 686     }
 687 
 688     /**
 689      * The utility method to check whether a host name is in a domain
 690      * or not.
 691      *
 692      * <p>This concept is described in the cookie specification.
 693      * To understand the concept, some terminologies need to be defined first:
 694      * <blockquote>
 695      * effective host name = hostname if host name contains dot<br>
 696      * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;or = hostname.local if not
 697      * </blockquote>
 698      * <p>Host A's name domain-matches host B's if:
 699      * <blockquote><ul>
 700      *   <li>their host name strings string-compare equal; or</li>
 701      *   <li>A is a HDN string and has the form NB, where N is a non-empty
 702      *   name string, B has the form .B', and B' is a HDN string.  (So,
 703      *   x.y.com domain-matches .Y.com but not Y.com.)</li>
 704      * </ul></blockquote>
 705      *
 706      * <p>A host isn't in a domain (RFC 2965 sec. 3.3.2) if:
 707      * <blockquote><ul>
 708      *   <li>The value for the Domain attribute contains no embedded dots,
 709      *   and the value is not .local.</li>
 710      *   <li>The effective host name that derives from the request-host does
 711      *   not domain-match the Domain attribute.</li>
 712      *   <li>The request-host is a HDN (not IP address) and has the form HD,
 713      *   where D is the value of the Domain attribute, and H is a string
 714      *   that contains one or more dots.</li>
 715      * </ul></blockquote>
 716      *
 717      * <p>Examples:
 718      * <blockquote><ul>
 719      *   <li>A Set-Cookie2 from request-host y.x.foo.com for Domain=.foo.com
 720      *   would be rejected, because H is y.x and contains a dot.</li>
 721      *   <li>A Set-Cookie2 from request-host x.foo.com for Domain=.foo.com
 722      *   would be accepted.</li>
 723      *   <li>A Set-Cookie2 with Domain=.com or Domain=.com., will always be
 724      *   rejected, because there is no embedded dot.</li>
 725      *   <li>A Set-Cookie2 with Domain=ajax.com will be accepted, and the
 726      *   value for Domain will be taken to be .ajax.com, because a dot
 727      *   gets prepended to the value.</li>
 728      *   <li>A Set-Cookie2 from request-host example for Domain=.local will
 729      *   be accepted, because the effective host name for the request-
 730      *   host is example.local, and example.local domain-matches .local.</li>
 731      * </ul></blockquote>
 732      *
 733      * @param domain    the domain name to check host name with
 734      * @param host      the host name in question
 735      * @return          <tt>true</tt> if they domain-matches; <tt>false</tt> if not
 736      */
 737     public static boolean domainMatches(String domain, String host) {
 738         if (domain == null || host == null)
 739             return false;
 740 
 741         // if there's no embedded dot in domain and domain is not .local
 742         boolean isLocalDomain = ".local".equalsIgnoreCase(domain);
 743         int embeddedDotInDomain = domain.indexOf('.');
 744         if (embeddedDotInDomain == 0)
 745             embeddedDotInDomain = domain.indexOf('.', 1);
 746         if (!isLocalDomain
 747             && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1))
 748             return false;
 749 
 750         // if the host name contains no dot and the domain name is .local
 751         int firstDotInHost = host.indexOf('.');
 752         if (firstDotInHost == -1 && isLocalDomain)
 753             return true;
 754 
 755         int domainLength = domain.length();
 756         int lengthDiff = host.length() - domainLength;
 757         if (lengthDiff == 0) {
 758             // if the host name and the domain name are just string-compare euqal
 759             return host.equalsIgnoreCase(domain);
 760         }
 761         else if (lengthDiff > 0) {
 762             // need to check H & D component
 763             String H = host.substring(0, lengthDiff);
 764             String D = host.substring(lengthDiff);
 765 
 766             return (H.indexOf('.') == -1 && D.equalsIgnoreCase(domain));
 767         }
 768         else if (lengthDiff == -1) {
 769             // if domain is actually .host
 770             return (domain.charAt(0) == '.' &&
 771                         host.equalsIgnoreCase(domain.substring(1)));
 772         }
 773 
 774         return false;
 775     }
 776 
 777 
 778     /**
 779      * Constructs a cookie header string representation of this cookie,
 780      * which is in the format defined by corresponding cookie specification,
 781      * but without the leading "Cookie:" token.
 782      *
 783      * @return  a string form of the cookie. The string has the defined format
 784      */
 785     @Override
 786     public String toString() {
 787         if (getVersion() > 0) {
 788             return toRFC2965HeaderString();
 789         } else {
 790             return toNetscapeHeaderString();
 791         }
 792     }
 793 
 794 
 795     /**
 796      * Test the equality of two http cookies.
 797      *
 798      * <p> The result is <tt>true</tt> only if two cookies
 799      * come from same domain (case-insensitive),
 800      * have same name (case-insensitive),
 801      * and have same path (case-sensitive).
 802      *
 803      * @return          <tt>true</tt> if 2 http cookies equal to each other;
 804      *                  otherwise, <tt>false</tt>
 805      */
 806     @Override
 807     public boolean equals(Object obj) {
 808         if (obj == this)
 809             return true;
 810         if (!(obj instanceof HttpCookie))
 811             return false;
 812         HttpCookie other = (HttpCookie)obj;
 813 
 814         // One http cookie equals to another cookie (RFC 2965 sec. 3.3.3) if:
 815         //   1. they come from same domain (case-insensitive),
 816         //   2. have same name (case-insensitive),
 817         //   3. and have same path (case-sensitive).
 818         return equalsIgnoreCase(getName(), other.getName()) &&
 819                equalsIgnoreCase(getDomain(), other.getDomain()) &&
 820                equals(getPath(), other.getPath());
 821     }
 822 
 823 
 824     /**
 825      * Return hash code of this http cookie. The result is the sum of
 826      * hash code value of three significant components of this cookie:
 827      * name, domain, and path.
 828      * That is, the hash code is the value of the expression:
 829      * <blockquote>
 830      * getName().toLowerCase().hashCode()<br>
 831      * + getDomain().toLowerCase().hashCode()<br>
 832      * + getPath().hashCode()
 833      * </blockquote>
 834      *
 835      * @return          this http cookie's hash code
 836      */
 837     @Override
 838     public int hashCode() {
 839         int h1 = name.toLowerCase().hashCode();
 840         int h2 = (domain!=null) ? domain.toLowerCase().hashCode() : 0;
 841         int h3 = (path!=null) ? path.hashCode() : 0;
 842 
 843         return h1 + h2 + h3;
 844     }
 845 
 846     /**
 847      * Create and return a copy of this object.
 848      *
 849      * @return          a clone of this http cookie
 850      */
 851     @Override
 852     public Object clone() {
 853         try {
 854             return super.clone();
 855         } catch (CloneNotSupportedException e) {
 856             throw new RuntimeException(e.getMessage());
 857         }
 858     }
 859 
 860 
 861     /* ---------------- Private operations -------------- */
 862 
 863     // Note -- disabled for now to allow full Netscape compatibility
 864     // from RFC 2068, token special case characters
 865     //
 866     // private static final String tspecials = "()<>@,;:\\\"/[]?={} \t";
 867     private static final String tspecials = ",;";
 868 
 869     /*
 870      * Tests a string and returns true if the string counts as a
 871      * token.
 872      *
 873      * @param value             the <code>String</code> to be tested
 874      *
 875      * @return                  <code>true</code> if the <code>String</code> is
 876      *                          a token; <code>false</code> if it is not
 877      */
 878 
 879     private static boolean isToken(String value) {
 880         int len = value.length();
 881 
 882         for (int i = 0; i < len; i++) {
 883             char c = value.charAt(i);
 884 
 885             if (c < 0x20 || c >= 0x7f || tspecials.indexOf(c) != -1)
 886                 return false;
 887         }
 888         return true;
 889     }
 890 
 891 
 892     /*
 893      * @param name      the name to be tested
 894      * @return          <tt>true</tt> if the name is reserved by cookie
 895      *                  specification, <tt>false</tt> if it is not
 896      */
 897     private static boolean isReserved(String name) {
 898         if (name.equalsIgnoreCase("Comment")
 899             || name.equalsIgnoreCase("CommentURL")      // rfc2965 only
 900             || name.equalsIgnoreCase("Discard")         // rfc2965 only
 901             || name.equalsIgnoreCase("Domain")
 902             || name.equalsIgnoreCase("Expires")         // netscape draft only
 903             || name.equalsIgnoreCase("Max-Age")
 904             || name.equalsIgnoreCase("Path")
 905             || name.equalsIgnoreCase("Port")            // rfc2965 only
 906             || name.equalsIgnoreCase("Secure")
 907             || name.equalsIgnoreCase("Version")
 908             || name.equalsIgnoreCase("HttpOnly")
 909             || name.charAt(0) == '$')
 910         {
 911             return true;
 912         }
 913 
 914         return false;
 915     }
 916 
 917 
 918     /*
 919      * Parse header string to cookie object.
 920      *
 921      * @param header    header string; should contain only one NAME=VALUE pair
 922      *
 923      * @return          an HttpCookie being extracted
 924      *
 925      * @throws IllegalArgumentException if header string violates the cookie
 926      *                                  specification
 927      */
 928     private static HttpCookie parseInternal(String header)
 929     {
 930         HttpCookie cookie = null;
 931         String namevaluePair = null;
 932 
 933         StringTokenizer tokenizer = new StringTokenizer(header, ";");
 934 
 935         // there should always have at least on name-value pair;
 936         // it's cookie's name
 937         try {
 938             namevaluePair = tokenizer.nextToken();
 939             int index = namevaluePair.indexOf('=');
 940             if (index != -1) {
 941                 String name = namevaluePair.substring(0, index).trim();
 942                 String value = namevaluePair.substring(index + 1).trim();
 943                 cookie = new HttpCookie(name, stripOffSurroundingQuote(value));
 944             } else {
 945                 // no "=" in name-value pair; it's an error
 946                 throw new IllegalArgumentException("Invalid cookie name-value pair");
 947             }
 948         } catch (NoSuchElementException ignored) {
 949             throw new IllegalArgumentException("Empty cookie header string");
 950         }
 951 
 952         // remaining name-value pairs are cookie's attributes
 953         while (tokenizer.hasMoreTokens()) {
 954             namevaluePair = tokenizer.nextToken();
 955             int index = namevaluePair.indexOf('=');
 956             String name, value;
 957             if (index != -1) {
 958                 name = namevaluePair.substring(0, index).trim();
 959                 value = namevaluePair.substring(index + 1).trim();
 960             } else {
 961                 name = namevaluePair.trim();
 962                 value = null;
 963             }
 964 
 965             // assign attribute to cookie
 966             assignAttribute(cookie, name, value);
 967         }
 968 
 969         return cookie;
 970     }
 971 
 972 
 973     /*
 974      * assign cookie attribute value to attribute name;
 975      * use a map to simulate method dispatch
 976      */
 977     static interface CookieAttributeAssignor {
 978             public void assign(HttpCookie cookie, String attrName, String attrValue);
 979     }
 980     static java.util.Map<String, CookieAttributeAssignor> assignors = null;
 981     static {
 982         assignors = new java.util.HashMap<String, CookieAttributeAssignor>();
 983         assignors.put("comment", new CookieAttributeAssignor(){
 984                 public void assign(HttpCookie cookie, String attrName, String attrValue) {
 985                     if (cookie.getComment() == null) cookie.setComment(attrValue);
 986                 }
 987             });
 988         assignors.put("commenturl", new CookieAttributeAssignor(){
 989                 public void assign(HttpCookie cookie, String attrName, String attrValue) {
 990                     if (cookie.getCommentURL() == null) cookie.setCommentURL(attrValue);
 991                 }
 992             });
 993         assignors.put("discard", new CookieAttributeAssignor(){
 994                 public void assign(HttpCookie cookie, String attrName, String attrValue) {
 995                     cookie.setDiscard(true);
 996                 }
 997             });
 998         assignors.put("domain", new CookieAttributeAssignor(){
 999                 public void assign(HttpCookie cookie, String attrName, String attrValue) {
1000                     if (cookie.getDomain() == null) cookie.setDomain(attrValue);
1001                 }
1002             });
1003         assignors.put("max-age", new CookieAttributeAssignor(){
1004                 public void assign(HttpCookie cookie, String attrName, String attrValue) {
1005                     try {
1006                         long maxage = Long.parseLong(attrValue);
1007                         if (cookie.getMaxAge() == MAX_AGE_UNSPECIFIED) cookie.setMaxAge(maxage);
1008                     } catch (NumberFormatException ignored) {
1009                         throw new IllegalArgumentException("Illegal cookie max-age attribute");
1010                     }
1011                 }
1012             });
1013         assignors.put("path", new CookieAttributeAssignor(){
1014                 public void assign(HttpCookie cookie, String attrName, String attrValue) {
1015                     if (cookie.getPath() == null) cookie.setPath(attrValue);
1016                 }
1017             });
1018         assignors.put("port", new CookieAttributeAssignor(){
1019                 public void assign(HttpCookie cookie, String attrName, String attrValue) {
1020                     if (cookie.getPortlist() == null) cookie.setPortlist(attrValue == null ? "" : attrValue);
1021                 }
1022             });
1023         assignors.put("secure", new CookieAttributeAssignor(){
1024                 public void assign(HttpCookie cookie, String attrName, String attrValue) {
1025                     cookie.setSecure(true);
1026                 }
1027             });
1028         assignors.put("httponly", new CookieAttributeAssignor(){
1029                 public void assign(HttpCookie cookie, String attrName, String attrValue) {
1030                     cookie.setHttpOnly(true);
1031                 }
1032             });
1033         assignors.put("version", new CookieAttributeAssignor(){
1034                 public void assign(HttpCookie cookie, String attrName, String attrValue) {
1035                     try {
1036                         int version = Integer.parseInt(attrValue);
1037                         cookie.setVersion(version);
1038                     } catch (NumberFormatException ignored) {
1039                         // Just ignore bogus version, it will default to 0 or 1
1040                     }
1041                 }
1042             });
1043         assignors.put("expires", new CookieAttributeAssignor(){ // Netscape only
1044                 public void assign(HttpCookie cookie, String attrName, String attrValue) {
1045                     if (cookie.getMaxAge() == MAX_AGE_UNSPECIFIED) {
1046                         cookie.setMaxAge(cookie.expiryDate2DeltaSeconds(attrValue));
1047                     }
1048                 }
1049             });
1050     }
1051     private static void assignAttribute(HttpCookie cookie,
1052                                        String attrName,
1053                                        String attrValue)
1054     {
1055         // strip off the surrounding "-sign if there's any
1056         attrValue = stripOffSurroundingQuote(attrValue);
1057 
1058         CookieAttributeAssignor assignor = assignors.get(attrName.toLowerCase());
1059         if (assignor != null) {
1060             assignor.assign(cookie, attrName, attrValue);
1061         } else {
1062             // Ignore the attribute as per RFC 2965
1063         }
1064     }
1065 
1066     /*
1067      * Constructs a string representation of this cookie. The string format is
1068      * as Netscape spec, but without leading "Cookie:" token.
1069      */
1070     private String toNetscapeHeaderString() {
1071         StringBuilder sb = new StringBuilder();
1072 
1073         sb.append(getName() + "=" + getValue());
1074 
1075         return sb.toString();
1076     }
1077 
1078     /*
1079      * Constructs a string representation of this cookie. The string format is
1080      * as RFC 2965/2109, but without leading "Cookie:" token.
1081      */
1082     private String toRFC2965HeaderString() {
1083         StringBuilder sb = new StringBuilder();
1084 
1085         sb.append(getName()).append("=\"").append(getValue()).append('"');
1086         if (getPath() != null)
1087             sb.append(";$Path=\"").append(getPath()).append('"');
1088         if (getDomain() != null)
1089             sb.append(";$Domain=\"").append(getDomain()).append('"');
1090         if (getPortlist() != null)
1091             sb.append(";$Port=\"").append(getPortlist()).append('"');
1092 
1093         return sb.toString();
1094     }
1095 
1096     static final TimeZone GMT = TimeZone.getTimeZone("GMT");
1097 
1098     /*
1099      * @param dateString        a date string in one of the formats
1100      *                          defined in Netscape cookie spec
1101      *
1102      * @return                  delta seconds between this cookie's creation
1103      *                          time and the time specified by dateString
1104      */
1105     private long expiryDate2DeltaSeconds(String dateString) {
1106         for (int i = 0; i < COOKIE_DATE_FORMATS.length; i++) {
1107             SimpleDateFormat df = new SimpleDateFormat(COOKIE_DATE_FORMATS[i], Locale.US);
1108             df.setTimeZone(GMT);
1109             try {
1110                 Date date = df.parse(dateString);
1111                 return (date.getTime() - whenCreated) / 1000;
1112             } catch (Exception e) {
1113                 // Ignore, try the next date format
1114             }
1115         }
1116         return 0;
1117     }
1118 
1119 
1120 
1121     /*
1122      * try to guess the cookie version through set-cookie header string
1123      */
1124     private static int guessCookieVersion(String header) {
1125         int version = 0;
1126 
1127         header = header.toLowerCase();
1128         if (header.indexOf("expires=") != -1) {
1129             // only netscape cookie using 'expires'
1130             version = 0;
1131         } else if (header.indexOf("version=") != -1) {
1132             // version is mandatory for rfc 2965/2109 cookie
1133             version = 1;
1134         } else if (header.indexOf("max-age") != -1) {
1135             // rfc 2965/2109 use 'max-age'
1136             version = 1;
1137         } else if (startsWithIgnoreCase(header, SET_COOKIE2)) {
1138             // only rfc 2965 cookie starts with 'set-cookie2'
1139             version = 1;
1140         }
1141 
1142         return version;
1143     }
1144 
1145     private static String stripOffSurroundingQuote(String str) {
1146         if (str != null && str.length() > 2 &&
1147             str.charAt(0) == '"' && str.charAt(str.length() - 1) == '"') {
1148             return str.substring(1, str.length() - 1);
1149         }
1150         if (str != null && str.length() > 2 &&
1151             str.charAt(0) == '\'' && str.charAt(str.length() - 1) == '\'') {
1152             return str.substring(1, str.length() - 1);
1153         }
1154         return str;
1155     }
1156 
1157     private static boolean equalsIgnoreCase(String s, String t) {
1158         if (s == t) return true;
1159         if ((s != null) && (t != null)) {
1160             return s.equalsIgnoreCase(t);
1161         }
1162         return false;
1163     }
1164 
1165     private static boolean equals(String s, String t) {
1166         if (s == t) return true;
1167         if ((s != null) && (t != null)) {
1168             return s.equals(t);
1169         }
1170         return false;
1171     }
1172 
1173     private static boolean startsWithIgnoreCase(String s, String start) {
1174         if (s == null || start == null) return false;
1175 
1176         if (s.length() >= start.length() &&
1177                 start.equalsIgnoreCase(s.substring(0, start.length()))) {
1178             return true;
1179         }
1180 
1181         return false;
1182     }
1183 
1184     /*
1185      * Split cookie header string according to rfc 2965:
1186      *   1) split where it is a comma;
1187      *   2) but not the comma surrounding by double-quotes, which is the comma
1188      *      inside port list or embeded URIs.
1189      *
1190      * @param header            the cookie header string to split
1191      *
1192      * @return                  list of strings; never null
1193      *
1194      */
1195     private static List<String> splitMultiCookies(String header) {
1196         List<String> cookies = new java.util.ArrayList<String>();
1197         int quoteCount = 0;
1198         int p, q;
1199 
1200         for (p = 0, q = 0; p < header.length(); p++) {
1201             char c = header.charAt(p);
1202             if (c == '"') quoteCount++;
1203             if (c == ',' && (quoteCount % 2 == 0)) {      // it is comma and not surrounding by double-quotes
1204                 cookies.add(header.substring(q, p));
1205                 q = p + 1;
1206             }
1207         }
1208 
1209         cookies.add(header.substring(q));
1210 
1211         return cookies;
1212     }
1213 }