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