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