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