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