CookieManager.java revision dbdad99813be49f2a64b876507ae18db42db7bbc
1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.webkit;
18
19import android.net.ParseException;
20import android.net.WebAddress;
21import android.util.Log;
22
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.Collection;
26import java.util.Comparator;
27import java.util.Iterator;
28import java.util.LinkedHashMap;
29import java.util.Map;
30import java.util.SortedSet;
31import java.util.TreeSet;
32
33/**
34 * CookieManager manages cookies according to RFC2109 spec.
35 */
36public final class CookieManager {
37
38    private static CookieManager sRef;
39
40    private static final String LOGTAG = "webkit";
41
42    private static final String DOMAIN = "domain";
43
44    private static final String PATH = "path";
45
46    private static final String EXPIRES = "expires";
47
48    private static final String SECURE = "secure";
49
50    private static final String MAX_AGE = "max-age";
51
52    private static final String HTTP_ONLY = "httponly";
53
54    private static final String HTTPS = "https";
55
56    private static final char PERIOD = '.';
57
58    private static final char COMMA = ',';
59
60    private static final char SEMICOLON = ';';
61
62    private static final char EQUAL = '=';
63
64    private static final char PATH_DELIM = '/';
65
66    private static final char QUESTION_MARK = '?';
67
68    private static final char WHITE_SPACE = ' ';
69
70    private static final char QUOTATION = '\"';
71
72    private static final int SECURE_LENGTH = SECURE.length();
73
74    private static final int HTTP_ONLY_LENGTH = HTTP_ONLY.length();
75
76    // RFC2109 defines 4k as maximum size of a cookie
77    private static final int MAX_COOKIE_LENGTH = 4 * 1024;
78
79    // RFC2109 defines 20 as max cookie count per domain. As we track with base
80    // domain, we allow 50 per base domain
81    private static final int MAX_COOKIE_COUNT_PER_BASE_DOMAIN = 50;
82
83    // RFC2109 defines 300 as max count of domains. As we track with base
84    // domain, we set 200 as max base domain count
85    private static final int MAX_DOMAIN_COUNT = 200;
86
87    // max cookie count to limit RAM cookie takes less than 100k, it is based on
88    // average cookie entry size is less than 100 bytes
89    private static final int MAX_RAM_COOKIES_COUNT = 1000;
90
91    //  max domain count to limit RAM cookie takes less than 100k,
92    private static final int MAX_RAM_DOMAIN_COUNT = 15;
93
94    private Map<String, ArrayList<Cookie>> mCookieMap = new LinkedHashMap
95            <String, ArrayList<Cookie>>(MAX_DOMAIN_COUNT, 0.75f, true);
96
97    private boolean mAcceptCookie = true;
98
99    /**
100     * This contains a list of 2nd-level domains that aren't allowed to have
101     * wildcards when combined with country-codes. For example: [.co.uk].
102     */
103    private final static String[] BAD_COUNTRY_2LDS =
104          { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
105            "lg", "ne", "net", "or", "org" };
106
107    static {
108        Arrays.sort(BAD_COUNTRY_2LDS);
109    }
110
111    /**
112     * Package level class to be accessed by cookie sync manager
113     */
114    static class Cookie {
115        static final byte MODE_NEW = 0;
116
117        static final byte MODE_NORMAL = 1;
118
119        static final byte MODE_DELETED = 2;
120
121        static final byte MODE_REPLACED = 3;
122
123        String domain;
124
125        String path;
126
127        String name;
128
129        String value;
130
131        long expires;
132
133        long lastAcessTime;
134
135        long lastUpdateTime;
136
137        boolean secure;
138
139        byte mode;
140
141        Cookie() {
142        }
143
144        Cookie(String defaultDomain, String defaultPath) {
145            domain = defaultDomain;
146            path = defaultPath;
147            expires = -1;
148        }
149
150        boolean exactMatch(Cookie in) {
151            return domain.equals(in.domain) && path.equals(in.path) &&
152                    name.equals(in.name);
153        }
154
155        boolean domainMatch(String urlHost) {
156            if (domain.startsWith(".")) {
157                if (urlHost.endsWith(domain.substring(1))) {
158                    int len = domain.length();
159                    int urlLen = urlHost.length();
160                    if (urlLen > len - 1) {
161                        // make sure bar.com doesn't match .ar.com
162                        return urlHost.charAt(urlLen - len) == PERIOD;
163                    }
164                    return true;
165                }
166                return false;
167            } else {
168                // exact match if domain is not leading w/ dot
169                return urlHost.equals(domain);
170            }
171        }
172
173        boolean pathMatch(String urlPath) {
174            if (urlPath.startsWith(path)) {
175                int len = path.length();
176                if (len == 0) {
177                    Log.w(LOGTAG, "Empty cookie path");
178                    return false;
179                }
180                int urlLen = urlPath.length();
181                if (path.charAt(len-1) != PATH_DELIM && urlLen > len) {
182                    // make sure /wee doesn't match /we
183                    return urlPath.charAt(len) == PATH_DELIM;
184                }
185                return true;
186            }
187            return false;
188        }
189
190        public String toString() {
191            return "domain: " + domain + "; path: " + path + "; name: " + name
192                    + "; value: " + value;
193        }
194    }
195
196    private static final CookieComparator COMPARATOR = new CookieComparator();
197
198    private static final class CookieComparator implements Comparator<Cookie> {
199        public int compare(Cookie cookie1, Cookie cookie2) {
200            // According to RFC 2109, multiple cookies are ordered in a way such
201            // that those with more specific Path attributes precede those with
202            // less specific. Ordering with respect to other attributes (e.g.,
203            // Domain) is unspecified.
204            // As Set is not modified if the two objects are same, we do want to
205            // assign different value for each cookie.
206            int diff = cookie2.path.length() - cookie1.path.length();
207            if (diff == 0) {
208                diff = cookie2.domain.length() - cookie1.domain.length();
209                if (diff == 0) {
210                    diff = cookie2.name.hashCode() - cookie1.name.hashCode();
211                    if (diff == 0) {
212                        Log.w(LOGTAG, "Found two cookies with the same value." +
213                                "cookie1=" + cookie1 + " , cookie2=" + cookie2);
214                    }
215                }
216            }
217            return diff;
218        }
219    }
220
221    private CookieManager() {
222    }
223
224    protected Object clone() throws CloneNotSupportedException {
225        throw new CloneNotSupportedException("doesn't implement Cloneable");
226    }
227
228    /**
229     * Get a singleton CookieManager. If this is called before any
230     * {@link WebView} is created or outside of {@link WebView} context, the
231     * caller needs to call {@link CookieSyncManager#createInstance(Context)}
232     * first.
233     *
234     * @return CookieManager
235=     */
236    public static synchronized CookieManager getInstance() {
237        if (sRef == null) {
238            sRef = new CookieManager();
239        }
240        return sRef;
241    }
242
243    /**
244     * Control whether cookie is enabled or disabled
245     * @param accept TRUE if accept cookie
246     */
247    public synchronized void setAcceptCookie(boolean accept) {
248        mAcceptCookie = accept;
249    }
250
251    /**
252     * Return whether cookie is enabled
253     * @return TRUE if accept cookie
254     */
255    public synchronized boolean acceptCookie() {
256        return mAcceptCookie;
257    }
258
259    /**
260     * Set cookie for a given url. The old cookie with same host/path/name will
261     * be removed. The new cookie will be added if it is not expired or it does
262     * not have expiration which implies it is session cookie.
263     * @param url The url which cookie is set for
264     * @param value The value for set-cookie: in http response header
265     */
266    public void setCookie(String url, String value) {
267        WebAddress uri;
268        try {
269            uri = new WebAddress(url);
270        } catch (ParseException ex) {
271            Log.e(LOGTAG, "Bad address: " + url);
272            return;
273        }
274        setCookie(uri, value);
275    }
276
277    /**
278     * Set cookie for a given uri. The old cookie with same host/path/name will
279     * be removed. The new cookie will be added if it is not expired or it does
280     * not have expiration which implies it is session cookie.
281     * @param uri The uri which cookie is set for
282     * @param value The value for set-cookie: in http response header
283     * @hide - hide this because it takes in a parameter of type WebAddress,
284     * a system private class.
285     */
286    public synchronized void setCookie(WebAddress uri, String value) {
287        if (value != null && value.length() > MAX_COOKIE_LENGTH) {
288            return;
289        }
290        if (!mAcceptCookie || uri == null) {
291            return;
292        }
293        if (DebugFlags.COOKIE_MANAGER) {
294            Log.v(LOGTAG, "setCookie: uri: " + uri + " value: " + value);
295        }
296
297        String[] hostAndPath = getHostAndPath(uri);
298        if (hostAndPath == null) {
299            return;
300        }
301
302        // For default path, when setting a cookie, the spec says:
303        //Path:   Defaults to the path of the request URL that generated the
304        // Set-Cookie response, up to, but not including, the
305        // right-most /.
306        if (hostAndPath[1].length() > 1) {
307            int index = hostAndPath[1].lastIndexOf(PATH_DELIM);
308            hostAndPath[1] = hostAndPath[1].substring(0,
309                    index > 0 ? index : index + 1);
310        }
311
312        ArrayList<Cookie> cookies = null;
313        try {
314            cookies = parseCookie(hostAndPath[0], hostAndPath[1], value);
315        } catch (RuntimeException ex) {
316            Log.e(LOGTAG, "parse cookie failed for: " + value);
317        }
318
319        if (cookies == null || cookies.size() == 0) {
320            return;
321        }
322
323        String baseDomain = getBaseDomain(hostAndPath[0]);
324        ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain);
325        if (cookieList == null) {
326            cookieList = CookieSyncManager.getInstance()
327                    .getCookiesForDomain(baseDomain);
328            mCookieMap.put(baseDomain, cookieList);
329        }
330
331        long now = System.currentTimeMillis();
332        int size = cookies.size();
333        for (int i = 0; i < size; i++) {
334            Cookie cookie = cookies.get(i);
335
336            boolean done = false;
337            Iterator<Cookie> iter = cookieList.iterator();
338            while (iter.hasNext()) {
339                Cookie cookieEntry = iter.next();
340                if (cookie.exactMatch(cookieEntry)) {
341                    // expires == -1 means no expires defined. Otherwise
342                    // negative means far future
343                    if (cookie.expires < 0 || cookie.expires > now) {
344                        // secure cookies can't be overwritten by non-HTTPS url
345                        if (!cookieEntry.secure || HTTPS.equals(uri.mScheme)) {
346                            cookieEntry.value = cookie.value;
347                            cookieEntry.expires = cookie.expires;
348                            cookieEntry.secure = cookie.secure;
349                            cookieEntry.lastAcessTime = now;
350                            cookieEntry.lastUpdateTime = now;
351                            cookieEntry.mode = Cookie.MODE_REPLACED;
352                        }
353                    } else {
354                        cookieEntry.lastUpdateTime = now;
355                        cookieEntry.mode = Cookie.MODE_DELETED;
356                    }
357                    done = true;
358                    break;
359                }
360            }
361
362            // expires == -1 means no expires defined. Otherwise negative means
363            // far future
364            if (!done && (cookie.expires < 0 || cookie.expires > now)) {
365                cookie.lastAcessTime = now;
366                cookie.lastUpdateTime = now;
367                cookie.mode = Cookie.MODE_NEW;
368                if (cookieList.size() > MAX_COOKIE_COUNT_PER_BASE_DOMAIN) {
369                    Cookie toDelete = new Cookie();
370                    toDelete.lastAcessTime = now;
371                    Iterator<Cookie> iter2 = cookieList.iterator();
372                    while (iter2.hasNext()) {
373                        Cookie cookieEntry2 = iter2.next();
374                        if ((cookieEntry2.lastAcessTime < toDelete.lastAcessTime)
375                                && cookieEntry2.mode != Cookie.MODE_DELETED) {
376                            toDelete = cookieEntry2;
377                        }
378                    }
379                    toDelete.mode = Cookie.MODE_DELETED;
380                }
381                cookieList.add(cookie);
382            }
383        }
384    }
385
386    /**
387     * Get cookie(s) for a given url so that it can be set to "cookie:" in http
388     * request header.
389     * @param url The url needs cookie
390     * @return The cookies in the format of NAME=VALUE [; NAME=VALUE]
391     */
392    public String getCookie(String url) {
393        WebAddress uri;
394        try {
395            uri = new WebAddress(url);
396        } catch (ParseException ex) {
397            Log.e(LOGTAG, "Bad address: " + url);
398            return null;
399        }
400        return getCookie(uri);
401    }
402
403    /**
404     * Get cookie(s) for a given uri so that it can be set to "cookie:" in http
405     * request header.
406     * @param uri The uri needs cookie
407     * @return The cookies in the format of NAME=VALUE [; NAME=VALUE]
408     * @hide - hide this because it has a parameter of type WebAddress, which
409     * is a system private class.
410     */
411    public synchronized String getCookie(WebAddress uri) {
412        if (!mAcceptCookie || uri == null) {
413            return null;
414        }
415
416        String[] hostAndPath = getHostAndPath(uri);
417        if (hostAndPath == null) {
418            return null;
419        }
420
421        String baseDomain = getBaseDomain(hostAndPath[0]);
422        ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain);
423        if (cookieList == null) {
424            cookieList = CookieSyncManager.getInstance()
425                    .getCookiesForDomain(baseDomain);
426            mCookieMap.put(baseDomain, cookieList);
427        }
428
429        long now = System.currentTimeMillis();
430        boolean secure = HTTPS.equals(uri.mScheme);
431        Iterator<Cookie> iter = cookieList.iterator();
432
433        SortedSet<Cookie> cookieSet = new TreeSet<Cookie>(COMPARATOR);
434        while (iter.hasNext()) {
435            Cookie cookie = iter.next();
436            if (cookie.domainMatch(hostAndPath[0]) &&
437                    cookie.pathMatch(hostAndPath[1])
438                    // expires == -1 means no expires defined. Otherwise
439                    // negative means far future
440                    && (cookie.expires < 0 || cookie.expires > now)
441                    && (!cookie.secure || secure)
442                    && cookie.mode != Cookie.MODE_DELETED) {
443                cookie.lastAcessTime = now;
444                cookieSet.add(cookie);
445            }
446        }
447
448        StringBuilder ret = new StringBuilder(256);
449        Iterator<Cookie> setIter = cookieSet.iterator();
450        while (setIter.hasNext()) {
451            Cookie cookie = setIter.next();
452            if (ret.length() > 0) {
453                ret.append(SEMICOLON);
454                // according to RC2109, SEMICOLON is official separator,
455                // but when log in yahoo.com, it needs WHITE_SPACE too.
456                ret.append(WHITE_SPACE);
457            }
458
459            ret.append(cookie.name);
460            ret.append(EQUAL);
461            ret.append(cookie.value);
462        }
463
464        if (ret.length() > 0) {
465            if (DebugFlags.COOKIE_MANAGER) {
466                Log.v(LOGTAG, "getCookie: uri: " + uri + " value: " + ret);
467            }
468            return ret.toString();
469        } else {
470            if (DebugFlags.COOKIE_MANAGER) {
471                Log.v(LOGTAG, "getCookie: uri: " + uri
472                        + " But can't find cookie.");
473            }
474            return null;
475        }
476    }
477
478    /**
479     * Remove all session cookies, which are cookies without expiration date
480     */
481    public void removeSessionCookie() {
482        final Runnable clearCache = new Runnable() {
483            public void run() {
484                synchronized(CookieManager.this) {
485                    Collection<ArrayList<Cookie>> cookieList = mCookieMap.values();
486                    Iterator<ArrayList<Cookie>> listIter = cookieList.iterator();
487                    while (listIter.hasNext()) {
488                        ArrayList<Cookie> list = listIter.next();
489                        Iterator<Cookie> iter = list.iterator();
490                        while (iter.hasNext()) {
491                            Cookie cookie = iter.next();
492                            if (cookie.expires == -1) {
493                                iter.remove();
494                            }
495                        }
496                    }
497                    CookieSyncManager.getInstance().clearSessionCookies();
498                }
499            }
500        };
501        new Thread(clearCache).start();
502    }
503
504    /**
505     * Remove all cookies
506     */
507    public void removeAllCookie() {
508        final Runnable clearCache = new Runnable() {
509            public void run() {
510                synchronized(CookieManager.this) {
511                    mCookieMap = new LinkedHashMap<String, ArrayList<Cookie>>(
512                            MAX_DOMAIN_COUNT, 0.75f, true);
513                    CookieSyncManager.getInstance().clearAllCookies();
514                }
515            }
516        };
517        new Thread(clearCache).start();
518    }
519
520    /**
521     *  Return true if there are stored cookies.
522     */
523    public synchronized boolean hasCookies() {
524        return CookieSyncManager.getInstance().hasCookies();
525    }
526
527    /**
528     * Remove all expired cookies
529     */
530    public void removeExpiredCookie() {
531        final Runnable clearCache = new Runnable() {
532            public void run() {
533                synchronized(CookieManager.this) {
534                    long now = System.currentTimeMillis();
535                    Collection<ArrayList<Cookie>> cookieList = mCookieMap.values();
536                    Iterator<ArrayList<Cookie>> listIter = cookieList.iterator();
537                    while (listIter.hasNext()) {
538                        ArrayList<Cookie> list = listIter.next();
539                        Iterator<Cookie> iter = list.iterator();
540                        while (iter.hasNext()) {
541                            Cookie cookie = iter.next();
542                            // expires == -1 means no expires defined. Otherwise
543                            // negative means far future
544                            if (cookie.expires > 0 && cookie.expires < now) {
545                                iter.remove();
546                            }
547                        }
548                    }
549                    CookieSyncManager.getInstance().clearExpiredCookies(now);
550                }
551            }
552        };
553        new Thread(clearCache).start();
554    }
555
556    /**
557     * Package level api, called from CookieSyncManager
558     *
559     * Get a list of cookies which are updated since a given time.
560     * @param last The given time in millisec
561     * @return A list of cookies
562     */
563    synchronized ArrayList<Cookie> getUpdatedCookiesSince(long last) {
564        ArrayList<Cookie> cookies = new ArrayList<Cookie>();
565        Collection<ArrayList<Cookie>> cookieList = mCookieMap.values();
566        Iterator<ArrayList<Cookie>> listIter = cookieList.iterator();
567        while (listIter.hasNext()) {
568            ArrayList<Cookie> list = listIter.next();
569            Iterator<Cookie> iter = list.iterator();
570            while (iter.hasNext()) {
571                Cookie cookie = iter.next();
572                if (cookie.lastUpdateTime > last) {
573                    cookies.add(cookie);
574                }
575            }
576        }
577        return cookies;
578    }
579
580    /**
581     * Package level api, called from CookieSyncManager
582     *
583     * Delete a Cookie in the RAM
584     * @param cookie Cookie to be deleted
585     */
586    synchronized void deleteACookie(Cookie cookie) {
587        if (cookie.mode == Cookie.MODE_DELETED) {
588            String baseDomain = getBaseDomain(cookie.domain);
589            ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain);
590            if (cookieList != null) {
591                cookieList.remove(cookie);
592                if (cookieList.isEmpty()) {
593                    mCookieMap.remove(baseDomain);
594                }
595            }
596        }
597    }
598
599    /**
600     * Package level api, called from CookieSyncManager
601     *
602     * Called after a cookie is synced to FLASH
603     * @param cookie Cookie to be synced
604     */
605    synchronized void syncedACookie(Cookie cookie) {
606        cookie.mode = Cookie.MODE_NORMAL;
607    }
608
609    /**
610     * Package level api, called from CookieSyncManager
611     *
612     * Delete the least recent used domains if the total cookie count in RAM
613     * exceeds the limit
614     * @return A list of cookies which are removed from RAM
615     */
616    synchronized ArrayList<Cookie> deleteLRUDomain() {
617        int count = 0;
618        int byteCount = 0;
619        int mapSize = mCookieMap.size();
620
621        if (mapSize < MAX_RAM_DOMAIN_COUNT) {
622            Collection<ArrayList<Cookie>> cookieLists = mCookieMap.values();
623            Iterator<ArrayList<Cookie>> listIter = cookieLists.iterator();
624            while (listIter.hasNext() && count < MAX_RAM_COOKIES_COUNT) {
625                ArrayList<Cookie> list = listIter.next();
626                if (DebugFlags.COOKIE_MANAGER) {
627                    Iterator<Cookie> iter = list.iterator();
628                    while (iter.hasNext() && count < MAX_RAM_COOKIES_COUNT) {
629                        Cookie cookie = iter.next();
630                        // 14 is 3 * sizeof(long) + sizeof(boolean)
631                        // + sizeof(byte)
632                        byteCount += cookie.domain.length()
633                                + cookie.path.length()
634                                + cookie.name.length()
635                                + cookie.value.length() + 14;
636                        count++;
637                    }
638                } else {
639                    count += list.size();
640                }
641            }
642        }
643
644        ArrayList<Cookie> retlist = new ArrayList<Cookie>();
645        if (mapSize >= MAX_RAM_DOMAIN_COUNT || count >= MAX_RAM_COOKIES_COUNT) {
646            if (DebugFlags.COOKIE_MANAGER) {
647                Log.v(LOGTAG, count + " cookies used " + byteCount
648                        + " bytes with " + mapSize + " domains");
649            }
650            Object[] domains = mCookieMap.keySet().toArray();
651            int toGo = mapSize / 10 + 1;
652            while (toGo-- > 0){
653                String domain = domains[toGo].toString();
654                if (DebugFlags.COOKIE_MANAGER) {
655                    Log.v(LOGTAG, "delete domain: " + domain
656                            + " from RAM cache");
657                }
658                retlist.addAll(mCookieMap.get(domain));
659                mCookieMap.remove(domain);
660            }
661        }
662        return retlist;
663    }
664
665    /**
666     * Extract the host and path out of a uri
667     * @param uri The given WebAddress
668     * @return The host and path in the format of String[], String[0] is host
669     *          which has at least two periods, String[1] is path which always
670     *          ended with "/"
671     */
672    private String[] getHostAndPath(WebAddress uri) {
673        if (uri.mHost != null && uri.mPath != null) {
674            String[] ret = new String[2];
675            ret[0] = uri.mHost;
676            ret[1] = uri.mPath;
677
678            int index = ret[0].indexOf(PERIOD);
679            if (index == -1) {
680                if (uri.mScheme.equalsIgnoreCase("file")) {
681                    // There is a potential bug where a local file path matches
682                    // another file in the local web server directory. Still
683                    // "localhost" is the best pseudo domain name.
684                    ret[0] = "localhost";
685                }
686            } else if (index == ret[0].lastIndexOf(PERIOD)) {
687                // cookie host must have at least two periods
688                ret[0] = PERIOD + ret[0];
689            }
690
691            if (ret[1].charAt(0) != PATH_DELIM) {
692                return null;
693            }
694
695            /*
696             * find cookie path, e.g. for http://www.google.com, the path is "/"
697             * for http://www.google.com/lab/, the path is "/lab"
698             * for http://www.google.com/lab/foo, the path is "/lab/foo"
699             * for http://www.google.com/lab?hl=en, the path is "/lab"
700             * for http://www.google.com/lab.asp?hl=en, the path is "/lab.asp"
701             * Note: the path from URI has at least one "/"
702             * See:
703             * http://www.unix.com.ua/rfc/rfc2109.html
704             */
705            index = ret[1].indexOf(QUESTION_MARK);
706            if (index != -1) {
707                ret[1] = ret[1].substring(0, index);
708            }
709            return ret;
710        } else
711            return null;
712    }
713
714    /**
715     * Get the base domain for a give host. E.g. mail.google.com will return
716     * google.com
717     * @param host The give host
718     * @return the base domain
719     */
720    private String getBaseDomain(String host) {
721        int startIndex = 0;
722        int nextIndex = host.indexOf(PERIOD);
723        int lastIndex = host.lastIndexOf(PERIOD);
724        while (nextIndex < lastIndex) {
725            startIndex = nextIndex + 1;
726            nextIndex = host.indexOf(PERIOD, startIndex);
727        }
728        if (startIndex > 0) {
729            return host.substring(startIndex);
730        } else {
731            return host;
732        }
733    }
734
735    /**
736     * parseCookie() parses the cookieString which is a comma-separated list of
737     * one or more cookies in the format of "NAME=VALUE; expires=DATE;
738     * path=PATH; domain=DOMAIN_NAME; secure httponly" to a list of Cookies.
739     * Here is a sample: IGDND=1, IGPC=ET=UB8TSNwtDmQ:AF=0; expires=Sun,
740     * 17-Jan-2038 19:14:07 GMT; path=/ig; domain=.google.com, =,
741     * PREF=ID=408909b1b304593d:TM=1156459854:LM=1156459854:S=V-vCAU6Sh-gobCfO;
742     * expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com which
743     * contains 3 cookies IGDND, IGPC, PREF and an empty cookie
744     * @param host The default host
745     * @param path The default path
746     * @param cookieString The string coming from "Set-Cookie:"
747     * @return A list of Cookies
748     */
749    private ArrayList<Cookie> parseCookie(String host, String path,
750            String cookieString) {
751        ArrayList<Cookie> ret = new ArrayList<Cookie>();
752
753        int index = 0;
754        int length = cookieString.length();
755        while (true) {
756            Cookie cookie = null;
757
758            // done
759            if (index < 0 || index >= length) {
760                break;
761            }
762
763            // skip white space
764            if (cookieString.charAt(index) == WHITE_SPACE) {
765                index++;
766                continue;
767            }
768
769            /*
770             * get NAME=VALUE; pair. detecting the end of a pair is tricky, it
771             * can be the end of a string, like "foo=bluh", it can be semicolon
772             * like "foo=bluh;path=/"; or it can be enclosed by \", like
773             * "foo=\"bluh bluh\";path=/"
774             *
775             * Note: in the case of "foo=bluh, bar=bluh;path=/", we interpret
776             * it as one cookie instead of two cookies.
777             */
778            int semicolonIndex = cookieString.indexOf(SEMICOLON, index);
779            int equalIndex = cookieString.indexOf(EQUAL, index);
780            if (equalIndex == -1) {
781                // bad format, force return
782                break;
783            }
784            if (semicolonIndex > -1 && semicolonIndex < equalIndex) {
785                // empty cookie, like "; path=/", return
786                break;
787            }
788            cookie = new Cookie(host, path);
789            cookie.name = cookieString.substring(index, equalIndex);
790            if (cookieString.charAt(equalIndex + 1) == QUOTATION) {
791                index = cookieString.indexOf(QUOTATION, equalIndex + 2);
792                if (index == -1) {
793                    // bad format, force return
794                    break;
795                }
796            }
797            semicolonIndex = cookieString.indexOf(SEMICOLON, index);
798            if (semicolonIndex == -1) {
799                semicolonIndex = length;
800            }
801            if (semicolonIndex - equalIndex > MAX_COOKIE_LENGTH) {
802                // cookie is too big, trim it
803                cookie.value = cookieString.substring(equalIndex + 1,
804                        equalIndex + MAX_COOKIE_LENGTH);
805            } else if (equalIndex + 1 == semicolonIndex
806                    || semicolonIndex < equalIndex) {
807                // these are unusual case like foo=; and foo; path=/
808                cookie.value = "";
809            } else {
810                cookie.value = cookieString.substring(equalIndex + 1,
811                        semicolonIndex);
812            }
813            // get attributes
814            index = semicolonIndex;
815            while (true) {
816                // done
817                if (index < 0 || index >= length) {
818                    break;
819                }
820
821                // skip white space and semicolon
822                if (cookieString.charAt(index) == WHITE_SPACE
823                        || cookieString.charAt(index) == SEMICOLON) {
824                    index++;
825                    continue;
826                }
827
828                // comma means next cookie
829                if (cookieString.charAt(index) == COMMA) {
830                    index++;
831                    break;
832                }
833
834                // "secure" is a known attribute doesn't use "=";
835                // while sites like live.com uses "secure="
836                if (length - index >= SECURE_LENGTH
837                        && cookieString.substring(index, index + SECURE_LENGTH).
838                        equalsIgnoreCase(SECURE)) {
839                    index += SECURE_LENGTH;
840                    cookie.secure = true;
841                    if (index == length) break;
842                    if (cookieString.charAt(index) == EQUAL) index++;
843                    continue;
844                }
845
846                // "httponly" is a known attribute doesn't use "=";
847                // while sites like live.com uses "httponly="
848                if (length - index >= HTTP_ONLY_LENGTH
849                        && cookieString.substring(index,
850                            index + HTTP_ONLY_LENGTH).
851                        equalsIgnoreCase(HTTP_ONLY)) {
852                    index += HTTP_ONLY_LENGTH;
853                    if (index == length) break;
854                    if (cookieString.charAt(index) == EQUAL) index++;
855                    // FIXME: currently only parse the attribute
856                    continue;
857                }
858                equalIndex = cookieString.indexOf(EQUAL, index);
859                if (equalIndex > 0) {
860                    String name = cookieString.substring(index, equalIndex)
861                            .toLowerCase();
862                    if (name.equals(EXPIRES)) {
863                        int comaIndex = cookieString.indexOf(COMMA, equalIndex);
864
865                        // skip ',' in (Wdy, DD-Mon-YYYY HH:MM:SS GMT) or
866                        // (Weekday, DD-Mon-YY HH:MM:SS GMT) if it applies.
867                        // "Wednesday" is the longest Weekday which has length 9
868                        if ((comaIndex != -1) &&
869                                (comaIndex - equalIndex <= 10)) {
870                            index = comaIndex + 1;
871                        }
872                    }
873                    semicolonIndex = cookieString.indexOf(SEMICOLON, index);
874                    int commaIndex = cookieString.indexOf(COMMA, index);
875                    if (semicolonIndex == -1 && commaIndex == -1) {
876                        index = length;
877                    } else if (semicolonIndex == -1) {
878                        index = commaIndex;
879                    } else if (commaIndex == -1) {
880                        index = semicolonIndex;
881                    } else {
882                        index = Math.min(semicolonIndex, commaIndex);
883                    }
884                    String value =
885                            cookieString.substring(equalIndex + 1, index);
886
887                    // Strip quotes if they exist
888                    if (value.length() > 2 && value.charAt(0) == QUOTATION) {
889                        int endQuote = value.indexOf(QUOTATION, 1);
890                        if (endQuote > 0) {
891                            value = value.substring(1, endQuote);
892                        }
893                    }
894                    if (name.equals(EXPIRES)) {
895                        try {
896                            cookie.expires = HttpDateTime.parse(value);
897                        } catch (IllegalArgumentException ex) {
898                            Log.e(LOGTAG,
899                                    "illegal format for expires: " + value);
900                        }
901                    } else if (name.equals(MAX_AGE)) {
902                        try {
903                            cookie.expires = System.currentTimeMillis() + 1000
904                                    * Long.parseLong(value);
905                        } catch (NumberFormatException ex) {
906                            Log.e(LOGTAG,
907                                    "illegal format for max-age: " + value);
908                        }
909                    } else if (name.equals(PATH)) {
910                        // only allow non-empty path value
911                        if (value.length() > 0) {
912                            cookie.path = value;
913                        }
914                    } else if (name.equals(DOMAIN)) {
915                        int lastPeriod = value.lastIndexOf(PERIOD);
916                        if (lastPeriod == 0) {
917                            // disallow cookies set for TLDs like [.com]
918                            cookie.domain = null;
919                            continue;
920                        }
921                        try {
922                            Integer.parseInt(value.substring(lastPeriod + 1));
923                            // no wildcard for ip address match
924                            if (!value.equals(host)) {
925                                // no cross-site cookie
926                                cookie.domain = null;
927                            }
928                            continue;
929                        } catch (NumberFormatException ex) {
930                            // ignore the exception, value is a host name
931                        }
932                        value = value.toLowerCase();
933                        if (value.charAt(0) != PERIOD) {
934                            // pre-pended dot to make it as a domain cookie
935                            value = PERIOD + value;
936                            lastPeriod++;
937                        }
938                        if (host.endsWith(value.substring(1))) {
939                            int len = value.length();
940                            int hostLen = host.length();
941                            if (hostLen > (len - 1)
942                                    && host.charAt(hostLen - len) != PERIOD) {
943                                // make sure the bar.com doesn't match .ar.com
944                                cookie.domain = null;
945                                continue;
946                            }
947                            // disallow cookies set on ccTLDs like [.co.uk]
948                            if ((len == lastPeriod + 3)
949                                    && (len >= 6 && len <= 8)) {
950                                String s = value.substring(1, lastPeriod);
951                                if (Arrays.binarySearch(BAD_COUNTRY_2LDS, s) >= 0) {
952                                    cookie.domain = null;
953                                    continue;
954                                }
955                            }
956                            cookie.domain = value;
957                        } else {
958                            // no cross-site or more specific sub-domain cookie
959                            cookie.domain = null;
960                        }
961                    }
962                } else {
963                    // bad format, force return
964                    index = length;
965                }
966            }
967            if (cookie != null && cookie.domain != null) {
968                ret.add(cookie);
969            }
970        }
971        return ret;
972    }
973}
974