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