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