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