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         name = name.trim();
 145         if (name.isEmpty() || !isToken(name) || name.charAt(0) == '$') {
 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.
 175      * @throws  NullPointerException
 176      *          if the header string is {@code null}
 177      */
 178     public static List<HttpCookie> parse(String header) {
 179         return parse(header, false);
 180     }
 181 
 182     // Private version of parse() that will store the original header used to
 183     // create the cookie, in the cookie itself. This can be useful for filtering
 184     // Set-Cookie[2] headers, using the internal parsing logic defined in this
 185     // class.
 186     private static List<HttpCookie> parse(String header, boolean retainHeader) {
 187 
 188         int version = guessCookieVersion(header);
 189 
 190         // if header start with set-cookie or set-cookie2, strip it off
 191         if (startsWithIgnoreCase(header, SET_COOKIE2)) {
 192             header = header.substring(SET_COOKIE2.length());
 193         } else if (startsWithIgnoreCase(header, SET_COOKIE)) {
 194             header = header.substring(SET_COOKIE.length());
 195         }
 196 
 197         List<HttpCookie> cookies = new java.util.ArrayList<>();
 198         // The Netscape cookie may have a comma in its expires attribute, while
 199         // the comma is the delimiter in rfc 2965/2109 cookie header string.
 200         // so the parse logic is slightly different
 201         if (version == 0) {
 202             // Netscape draft cookie
 203             HttpCookie cookie = parseInternal(header, retainHeader);
 204             cookie.setVersion(0);
 205             cookies.add(cookie);
 206         } else {
 207             // rfc2965/2109 cookie
 208             // if header string contains more than one cookie,
 209             // it'll separate them with comma
 210             List<String> cookieStrings = splitMultiCookies(header);
 211             for (String cookieStr : cookieStrings) {
 212                 HttpCookie cookie = parseInternal(cookieStr, retainHeader);
 213                 cookie.setVersion(1);
 214                 cookies.add(cookie);
 215             }
 216         }
 217 
 218         return cookies;
 219     }
 220 
 221     // ---------------- Public operations --------------
 222 
 223     /**
 224      * Reports whether this HTTP cookie has expired or not.
 225      *
 226      * @return  {@code true} to indicate this HTTP cookie has expired;
 227      *          otherwise, {@code false}
 228      */
 229     public boolean hasExpired() {
 230         if (maxAge == 0) return true;
 231 
 232         // if not specify max-age, this cookie should be
 233         // discarded when user agent is to be closed, but
 234         // it is not expired.
 235         if (maxAge < 0) return false;
 236 
 237         long deltaSecond = (System.currentTimeMillis() - whenCreated) / 1000;
 238         if (deltaSecond > maxAge)
 239             return true;
 240         else
 241             return false;
 242     }
 243 
 244     /**
 245      * Specifies a comment that describes a cookie's purpose.
 246      * The comment is useful if the browser presents the cookie
 247      * to the user. Comments are not supported by Netscape Version 0 cookies.
 248      *
 249      * @param  purpose
 250      *         a {@code String} specifying the comment to display to the user
 251      *
 252      * @see  #getComment
 253      */
 254     public void setComment(String purpose) {
 255         comment = purpose;
 256     }
 257 
 258     /**
 259      * Returns the comment describing the purpose of this cookie, or
 260      * {@code null} if the cookie has no comment.
 261      *
 262      * @return  a {@code String} containing the comment, or {@code null} if none
 263      *
 264      * @see  #setComment
 265      */
 266     public String getComment() {
 267         return comment;
 268     }
 269 
 270     /**
 271      * Specifies a comment URL that describes a cookie's purpose.
 272      * The comment URL is useful if the browser presents the cookie
 273      * to the user. Comment URL is RFC 2965 only.
 274      *
 275      * @param  purpose
 276      *         a {@code String} specifying the comment URL to display to the user
 277      *
 278      * @see  #getCommentURL
 279      */
 280     public void setCommentURL(String purpose) {
 281         commentURL = purpose;
 282     }
 283 
 284     /**
 285      * Returns the comment URL describing the purpose of this cookie, or
 286      * {@code null} if the cookie has no comment URL.
 287      *
 288      * @return  a {@code String} containing the comment URL, or {@code null}
 289      *          if none
 290      *
 291      * @see  #setCommentURL
 292      */
 293     public String getCommentURL() {
 294         return commentURL;
 295     }
 296 
 297     /**
 298      * Specify whether user agent should discard the cookie unconditionally.
 299      * This is RFC 2965 only attribute.
 300      *
 301      * @param  discard
 302      *         {@code true} indicates to discard cookie unconditionally
 303      *
 304      * @see  #getDiscard
 305      */
 306     public void setDiscard(boolean discard) {
 307         toDiscard = discard;
 308     }
 309 
 310     /**
 311      * Returns the discard attribute of the cookie
 312      *
 313      * @return  a {@code boolean} to represent this cookie's discard attribute
 314      *
 315      * @see  #setDiscard
 316      */
 317     public boolean getDiscard() {
 318         return toDiscard;
 319     }
 320 
 321     /**
 322      * Specify the portlist of the cookie, which restricts the port(s)
 323      * to which a cookie may be sent back in a Cookie header.
 324      *
 325      * @param  ports
 326      *         a {@code String} specify the port list, which is comma separated
 327      *         series of digits
 328      *
 329      * @see  #getPortlist
 330      */
 331     public void setPortlist(String ports) {
 332         portlist = ports;
 333     }
 334 
 335     /**
 336      * Returns the port list attribute of the cookie
 337      *
 338      * @return  a {@code String} contains the port list or {@code null} if none
 339      *
 340      * @see  #setPortlist
 341      */
 342     public String getPortlist() {
 343         return portlist;
 344     }
 345 
 346     /**
 347      * Specifies the domain within which this cookie should be presented.
 348      *
 349      * <p> The form of the domain name is specified by RFC 2965. A domain
 350      * name begins with a dot ({@code .foo.com}) and means that
 351      * the cookie is visible to servers in a specified Domain Name System
 352      * (DNS) zone (for example, {@code www.foo.com}, but not
 353      * {@code a.b.foo.com}). By default, cookies are only returned
 354      * to the server that sent them.
 355      *
 356      * @param  pattern
 357      *         a {@code String} containing the domain name within which this
 358      *         cookie is visible; form is according to RFC 2965
 359      *
 360      * @see  #getDomain
 361      */
 362     public void setDomain(String pattern) {
 363         if (pattern != null)
 364             domain = pattern.toLowerCase();
 365         else
 366             domain = pattern;
 367     }
 368 
 369     /**
 370      * Returns the domain name set for this cookie. The form of the domain name
 371      * is set by RFC 2965.
 372      *
 373      * @return  a {@code String} containing the domain name
 374      *
 375      * @see  #setDomain
 376      */
 377     public String getDomain() {
 378         return domain;
 379     }
 380 
 381     /**
 382      * Sets the maximum age of the cookie in seconds.
 383      *
 384      * <p> A positive value indicates that the cookie will expire
 385      * after that many seconds have passed. Note that the value is
 386      * the <i>maximum</i> age when the cookie will expire, not the cookie's
 387      * current age.
 388      *
 389      * <p> A negative value means that the cookie is not stored persistently
 390      * and will be deleted when the Web browser exits. A zero value causes the
 391      * cookie to be deleted.
 392      *
 393      * @param  expiry
 394      *         an integer specifying the maximum age of the cookie in seconds;
 395      *         if zero, the cookie should be discarded immediately; otherwise,
 396      *         the cookie's max age is unspecified.
 397      *
 398      * @see  #getMaxAge
 399      */
 400     public void setMaxAge(long expiry) {
 401         maxAge = expiry;
 402     }
 403 
 404     /**
 405      * Returns the maximum age of the cookie, specified in seconds. By default,
 406      * {@code -1} indicating the cookie will persist until browser shutdown.
 407      *
 408      * @return  an integer specifying the maximum age of the cookie in seconds
 409      *
 410      * @see  #setMaxAge
 411      */
 412     public long getMaxAge() {
 413         return maxAge;
 414     }
 415 
 416     /**
 417      * Specifies a path for the cookie to which the client should return
 418      * the cookie.
 419      *
 420      * <p> The cookie is visible to all the pages in the directory
 421      * you specify, and all the pages in that directory's subdirectories.
 422      * A cookie's path must include the servlet that set the cookie,
 423      * for example, <i>/catalog</i>, which makes the cookie
 424      * visible to all directories on the server under <i>/catalog</i>.
 425      *
 426      * <p> Consult RFC 2965 (available on the Internet) for more
 427      * information on setting path names for cookies.
 428      *
 429      * @param  uri
 430      *         a {@code String} specifying a path
 431      *
 432      * @see  #getPath
 433      */
 434     public void setPath(String uri) {
 435         path = uri;
 436     }
 437 
 438     /**
 439      * Returns the path on the server to which the browser returns this cookie.
 440      * The cookie is visible to all subpaths on the server.
 441      *
 442      * @return  a {@code String} specifying a path that contains a servlet name,
 443      *          for example, <i>/catalog</i>
 444      *
 445      * @see  #setPath
 446      */
 447     public String getPath() {
 448         return path;
 449     }
 450 
 451     /**
 452      * Indicates whether the cookie should only be sent using a secure protocol,
 453      * such as HTTPS or SSL.
 454      *
 455      * <p> The default value is {@code false}.
 456      *
 457      * @param  flag
 458      *         If {@code true}, the cookie can only be sent over a secure
 459      *         protocol like HTTPS. If {@code false}, it can be sent over
 460      *         any protocol.
 461      *
 462      * @see  #getSecure
 463      */
 464     public void setSecure(boolean flag) {
 465         secure = flag;
 466     }
 467 
 468     /**
 469      * Returns {@code true} if sending this cookie should be restricted to a
 470      * secure protocol, or {@code false} if the it can be sent using any
 471      * protocol.
 472      *
 473      * @return  {@code false} if the cookie can be sent over any standard
 474      *          protocol; otherwise, {@code true}
 475      *
 476      * @see  #setSecure
 477      */
 478     public boolean getSecure() {
 479         return secure;
 480     }
 481 
 482     /**
 483      * Returns the name of the cookie. The name cannot be changed after
 484      * creation.
 485      *
 486      * @return  a {@code String} specifying the cookie's name
 487      */
 488     public String getName() {
 489         return name;
 490     }
 491 
 492     /**
 493      * Assigns a new value to a cookie after the cookie is created.
 494      * If you use a binary value, you may want to use BASE64 encoding.
 495      *
 496      * <p> With Version 0 cookies, values should not contain white space,
 497      * brackets, parentheses, equals signs, commas, double quotes, slashes,
 498      * question marks, at signs, colons, and semicolons. Empty values may not
 499      * behave the same way on all browsers.
 500      *
 501      * @param  newValue
 502      *         a {@code String} specifying the new value
 503      *
 504      * @see  #getValue
 505      */
 506     public void setValue(String newValue) {
 507         value = newValue;
 508     }
 509 
 510     /**
 511      * Returns the value of the cookie.
 512      *
 513      * @return  a {@code String} containing the cookie's present value
 514      *
 515      * @see  #setValue
 516      */
 517     public String getValue() {
 518         return value;
 519     }
 520 
 521     /**
 522      * Returns the version of the protocol this cookie complies with. Version 1
 523      * complies with RFC 2965/2109, and version 0 complies with the original
 524      * cookie specification drafted by Netscape. Cookies provided by a browser
 525      * use and identify the browser's cookie version.
 526      *
 527      * @return  0 if the cookie complies with the original Netscape
 528      *          specification; 1 if the cookie complies with RFC 2965/2109
 529      *
 530      * @see  #setVersion
 531      */
 532     public int getVersion() {
 533         return version;
 534     }
 535 
 536     /**
 537      * Sets the version of the cookie protocol this cookie complies
 538      * with. Version 0 complies with the original Netscape cookie
 539      * specification. Version 1 complies with RFC 2965/2109.
 540      *
 541      * @param  v
 542      *         0 if the cookie should comply with the original Netscape
 543      *         specification; 1 if the cookie should comply with RFC 2965/2109
 544      *
 545      * @throws  IllegalArgumentException
 546      *          if {@code v} is neither 0 nor 1
 547      *
 548      * @see  #getVersion
 549      */
 550     public void setVersion(int v) {
 551         if (v != 0 && v != 1) {
 552             throw new IllegalArgumentException("cookie version should be 0 or 1");
 553         }
 554 
 555         version = v;
 556     }
 557 
 558     /**
 559      * Returns {@code true} if this cookie contains the <i>HttpOnly</i>
 560      * attribute. This means that the cookie should not be accessible to
 561      * scripting engines, like javascript.
 562      *
 563      * @return  {@code true} if this cookie should be considered HTTPOnly
 564      *
 565      * @see  #setHttpOnly(boolean)
 566      */
 567     public boolean isHttpOnly() {
 568         return httpOnly;
 569     }
 570 
 571     /**
 572      * Indicates whether the cookie should be considered HTTP Only. If set to
 573      * {@code true} it means the cookie should not be accessible to scripting
 574      * engines like javascript.
 575      *
 576      * @param  httpOnly
 577      *         if {@code true} make the cookie HTTP only, i.e. only visible as
 578      *         part of an HTTP request.
 579      *
 580      * @see  #isHttpOnly()
 581      */
 582     public void setHttpOnly(boolean httpOnly) {
 583         this.httpOnly = httpOnly;
 584     }
 585 
 586     /**
 587      * The utility method to check whether a host name is in a domain or not.
 588      *
 589      * <p> This concept is described in the cookie specification.
 590      * To understand the concept, some terminologies need to be defined first:
 591      * <blockquote>
 592      * effective host name = hostname if host name contains dot<br>
 593      * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
 594      * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;or = hostname.local if not
 595      * </blockquote>
 596      * <p>Host A's name domain-matches host B's if:
 597      * <blockquote><ul>
 598      *   <li>their host name strings string-compare equal; or</li>
 599      *   <li>A is a HDN string and has the form NB, where N is a non-empty
 600      *   name string, B has the form .B', and B' is a HDN string.  (So,
 601      *   x.y.com domain-matches .Y.com but not Y.com.)</li>
 602      * </ul></blockquote>
 603      *
 604      * <p>A host isn't in a domain (RFC 2965 sec. 3.3.2) if:
 605      * <blockquote><ul>
 606      *   <li>The value for the Domain attribute contains no embedded dots,
 607      *   and the value is not .local.</li>
 608      *   <li>The effective host name that derives from the request-host does
 609      *   not domain-match the Domain attribute.</li>
 610      *   <li>The request-host is a HDN (not IP address) and has the form HD,
 611      *   where D is the value of the Domain attribute, and H is a string
 612      *   that contains one or more dots.</li>
 613      * </ul></blockquote>
 614      *
 615      * <p>Examples:
 616      * <blockquote><ul>
 617      *   <li>A Set-Cookie2 from request-host y.x.foo.com for Domain=.foo.com
 618      *   would be rejected, because H is y.x and contains a dot.</li>
 619      *   <li>A Set-Cookie2 from request-host x.foo.com for Domain=.foo.com
 620      *   would be accepted.</li>
 621      *   <li>A Set-Cookie2 with Domain=.com or Domain=.com., will always be
 622      *   rejected, because there is no embedded dot.</li>
 623      *   <li>A Set-Cookie2 from request-host example for Domain=.local will
 624      *   be accepted, because the effective host name for the request-
 625      *   host is example.local, and example.local domain-matches .local.</li>
 626      * </ul></blockquote>
 627      *
 628      * @param  domain
 629      *         the domain name to check host name with
 630      *
 631      * @param  host
 632      *         the host name in question
 633      *
 634      * @return  {@code true} if they domain-matches; {@code false} if not
 635      */
 636     public static boolean domainMatches(String domain, String host) {
 637         if (domain == null || host == null)
 638             return false;
 639 
 640         // if there's no embedded dot in domain and domain is not .local
 641         boolean isLocalDomain = ".local".equalsIgnoreCase(domain);
 642         int embeddedDotInDomain = domain.indexOf('.');
 643         if (embeddedDotInDomain == 0)
 644             embeddedDotInDomain = domain.indexOf('.', 1);
 645         if (!isLocalDomain
 646             && (embeddedDotInDomain == -1 ||
 647                 embeddedDotInDomain == domain.length() - 1))
 648             return false;
 649 
 650         // if the host name contains no dot and the domain name
 651         // is .local or host.local
 652         int firstDotInHost = host.indexOf('.');
 653         if (firstDotInHost == -1 &&
 654             (isLocalDomain ||
 655              domain.equalsIgnoreCase(host + ".local"))) {
 656             return true;
 657         }
 658 
 659         int domainLength = domain.length();
 660         int lengthDiff = host.length() - domainLength;
 661         if (lengthDiff == 0) {
 662             // if the host name and the domain name are just string-compare euqal
 663             return host.equalsIgnoreCase(domain);
 664         }
 665         else if (lengthDiff > 0) {
 666             // need to check H & D component
 667             String H = host.substring(0, lengthDiff);
 668             String D = host.substring(lengthDiff);
 669 
 670             return (H.indexOf('.') == -1 && D.equalsIgnoreCase(domain));
 671         }
 672         else if (lengthDiff == -1) {
 673             // if domain is actually .host
 674             return (domain.charAt(0) == '.' &&
 675                         host.equalsIgnoreCase(domain.substring(1)));
 676         }
 677 
 678         return false;
 679     }
 680 
 681     /**
 682      * Constructs a cookie header string representation of this cookie,
 683      * which is in the format defined by corresponding cookie specification,
 684      * but without the leading "Cookie:" token.
 685      *
 686      * @return  a string form of the cookie. The string has the defined format
 687      */
 688     @Override
 689     public String toString() {
 690         if (getVersion() > 0) {
 691             return toRFC2965HeaderString();
 692         } else {
 693             return toNetscapeHeaderString();
 694         }
 695     }
 696 
 697     /**
 698      * Test the equality of two HTTP cookies.
 699      *
 700      * <p> The result is {@code true} only if two cookies come from same domain
 701      * (case-insensitive), have same name (case-insensitive), and have same path
 702      * (case-sensitive).
 703      *
 704      * @return  {@code true} if two HTTP cookies equal to each other;
 705      *          otherwise, {@code false}
 706      */
 707     @Override
 708     public boolean equals(Object obj) {
 709         if (obj == this)
 710             return true;
 711         if (!(obj instanceof HttpCookie))
 712             return false;
 713         HttpCookie other = (HttpCookie)obj;
 714 
 715         // One http cookie equals to another cookie (RFC 2965 sec. 3.3.3) if:
 716         //   1. they come from same domain (case-insensitive),
 717         //   2. have same name (case-insensitive),
 718         //   3. and have same path (case-sensitive).
 719         return equalsIgnoreCase(getName(), other.getName()) &&
 720                equalsIgnoreCase(getDomain(), other.getDomain()) &&
 721                Objects.equals(getPath(), other.getPath());
 722     }
 723 
 724     /**
 725      * Returns the hash code of this HTTP cookie. The result is the sum of
 726      * hash code value of three significant components of this cookie: name,
 727      * domain, and path. That is, the hash code is the value of the expression:
 728      * <blockquote>
 729      * getName().toLowerCase().hashCode()<br>
 730      * + getDomain().toLowerCase().hashCode()<br>
 731      * + getPath().hashCode()
 732      * </blockquote>
 733      *
 734      * @return  this HTTP cookie's hash code
 735      */
 736     @Override
 737     public int hashCode() {
 738         int h1 = name.toLowerCase().hashCode();
 739         int h2 = (domain!=null) ? domain.toLowerCase().hashCode() : 0;
 740         int h3 = (path!=null) ? path.hashCode() : 0;
 741 
 742         return h1 + h2 + h3;
 743     }
 744 
 745     /**
 746      * Create and return a copy of this object.
 747      *
 748      * @return  a clone of this HTTP cookie
 749      */
 750     @Override
 751     public Object clone() {
 752         try {
 753             return super.clone();
 754         } catch (CloneNotSupportedException e) {
 755             throw new RuntimeException(e.getMessage());
 756         }
 757     }
 758 
 759     // ---------------- Private operations --------------
 760 
 761     // Note -- disabled for now to allow full Netscape compatibility
 762     // from RFC 2068, token special case characters
 763     //
 764     // private static final String tspecials = "()<>@,;:\\\"/[]?={} \t";
 765     private static final String tspecials = ",; ";  // deliberately includes space
 766 
 767     /*
 768      * Tests a string and returns true if the string counts as a token.
 769      *
 770      * @param  value
 771      *         the {@code String} to be tested
 772      *
 773      * @return  {@code true} if the {@code String} is a token;
 774      *          {@code false} if it is not
 775      */
 776     private static boolean isToken(String value) {
 777         int len = value.length();
 778 
 779         for (int i = 0; i < len; i++) {
 780             char c = value.charAt(i);
 781 
 782             if (c < 0x20 || c >= 0x7f || tspecials.indexOf(c) != -1)
 783                 return false;
 784         }
 785         return true;
 786     }
 787 
 788     /*
 789      * Parse header string to cookie object.
 790      *
 791      * @param  header
 792      *         header string; should contain only one NAME=VALUE pair
 793      *
 794      * @return  an HttpCookie being extracted
 795      *
 796      * @throws  IllegalArgumentException
 797      *          if header string violates the cookie specification
 798      */
 799     private static HttpCookie parseInternal(String header,
 800                                             boolean retainHeader)
 801     {
 802         HttpCookie cookie = null;
 803         String namevaluePair = null;
 804 
 805         StringTokenizer tokenizer = new StringTokenizer(header, ";");
 806 
 807         // there should always have at least on name-value pair;
 808         // it's cookie's name
 809         try {
 810             namevaluePair = tokenizer.nextToken();
 811             int index = namevaluePair.indexOf('=');
 812             if (index != -1) {
 813                 String name = namevaluePair.substring(0, index).trim();
 814                 String value = namevaluePair.substring(index + 1).trim();
 815                 if (retainHeader)
 816                     cookie = new HttpCookie(name,
 817                                             stripOffSurroundingQuote(value),
 818                                             header);
 819                 else
 820                     cookie = new HttpCookie(name,
 821                                             stripOffSurroundingQuote(value));
 822             } else {
 823                 // no "=" in name-value pair; it's an error
 824                 throw new IllegalArgumentException("Invalid cookie name-value pair");
 825             }
 826         } catch (NoSuchElementException ignored) {
 827             throw new IllegalArgumentException("Empty cookie header string");
 828         }
 829 
 830         // remaining name-value pairs are cookie's attributes
 831         while (tokenizer.hasMoreTokens()) {
 832             namevaluePair = tokenizer.nextToken();
 833             int index = namevaluePair.indexOf('=');
 834             String name, value;
 835             if (index != -1) {
 836                 name = namevaluePair.substring(0, index).trim();
 837                 value = namevaluePair.substring(index + 1).trim();
 838             } else {
 839                 name = namevaluePair.trim();
 840                 value = null;
 841             }
 842 
 843             // assign attribute to cookie
 844             assignAttribute(cookie, name, value);
 845         }
 846 
 847         return cookie;
 848     }
 849 
 850     /*
 851      * assign cookie attribute value to attribute name;
 852      * use a map to simulate method dispatch
 853      */
 854     static interface CookieAttributeAssignor {
 855             public void assign(HttpCookie cookie,
 856                                String attrName,
 857                                String attrValue);
 858     }
 859     static final java.util.Map<String, CookieAttributeAssignor> assignors =
 860             new java.util.HashMap<>();
 861     static {
 862         assignors.put("comment", new CookieAttributeAssignor() {
 863                 public void assign(HttpCookie cookie,
 864                                    String attrName,
 865                                    String attrValue) {
 866                     if (cookie.getComment() == null)
 867                         cookie.setComment(attrValue);
 868                 }
 869             });
 870         assignors.put("commenturl", new CookieAttributeAssignor() {
 871                 public void assign(HttpCookie cookie,
 872                                    String attrName,
 873                                    String attrValue) {
 874                     if (cookie.getCommentURL() == null)
 875                         cookie.setCommentURL(attrValue);
 876                 }
 877             });
 878         assignors.put("discard", new CookieAttributeAssignor() {
 879                 public void assign(HttpCookie cookie,
 880                                    String attrName,
 881                                    String attrValue) {
 882                     cookie.setDiscard(true);
 883                 }
 884             });
 885         assignors.put("domain", new CookieAttributeAssignor(){
 886                 public void assign(HttpCookie cookie,
 887                                    String attrName,
 888                                    String attrValue) {
 889                     if (cookie.getDomain() == null)
 890                         cookie.setDomain(attrValue);
 891                 }
 892             });
 893         assignors.put("max-age", new CookieAttributeAssignor(){
 894                 public void assign(HttpCookie cookie,
 895                                    String attrName,
 896                                    String attrValue) {
 897                     try {
 898                         long maxage = Long.parseLong(attrValue);
 899                         if (cookie.getMaxAge() == MAX_AGE_UNSPECIFIED)
 900                             cookie.setMaxAge(maxage);
 901                     } catch (NumberFormatException ignored) {
 902                         throw new IllegalArgumentException(
 903                                 "Illegal cookie max-age attribute");
 904                     }
 905                 }
 906             });
 907         assignors.put("path", new CookieAttributeAssignor(){
 908                 public void assign(HttpCookie cookie,
 909                                    String attrName,
 910                                    String attrValue) {
 911                     if (cookie.getPath() == null)
 912                         cookie.setPath(attrValue);
 913                 }
 914             });
 915         assignors.put("port", new CookieAttributeAssignor(){
 916                 public void assign(HttpCookie cookie,
 917                                    String attrName,
 918                                    String attrValue) {
 919                     if (cookie.getPortlist() == null)
 920                         cookie.setPortlist(attrValue == null ? "" : attrValue);
 921                 }
 922             });
 923         assignors.put("secure", new CookieAttributeAssignor(){
 924                 public void assign(HttpCookie cookie,
 925                                    String attrName,
 926                                    String attrValue) {
 927                     cookie.setSecure(true);
 928                 }
 929             });
 930         assignors.put("httponly", new CookieAttributeAssignor(){
 931                 public void assign(HttpCookie cookie,
 932                                    String attrName,
 933                                    String attrValue) {
 934                     cookie.setHttpOnly(true);
 935                 }
 936             });
 937         assignors.put("version", new CookieAttributeAssignor(){
 938                 public void assign(HttpCookie cookie,
 939                                    String attrName,
 940                                    String attrValue) {
 941                     try {
 942                         int version = Integer.parseInt(attrValue);
 943                         cookie.setVersion(version);
 944                     } catch (NumberFormatException ignored) {
 945                         // Just ignore bogus version, it will default to 0 or 1
 946                     }
 947                 }
 948             });
 949         assignors.put("expires", new CookieAttributeAssignor(){ // Netscape only
 950                 public void assign(HttpCookie cookie,
 951                                    String attrName,
 952                                    String attrValue) {
 953                     if (cookie.getMaxAge() == MAX_AGE_UNSPECIFIED) {
 954                         long delta = cookie.expiryDate2DeltaSeconds(attrValue);
 955                         cookie.setMaxAge(delta > 0 ? delta : 0);
 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         SharedSecrets.setJavaNetHttpCookieAccess(
 977             new 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 constructed 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         for (int i = 0; i < COOKIE_DATE_FORMATS.length; i++) {
1035             SimpleDateFormat df = new SimpleDateFormat(COOKIE_DATE_FORMATS[i],
1036                                                        Locale.US);
1037             cal.set(1970, 0, 1, 0, 0, 0);
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<>();
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 }