Home | History | Annotate | Download | only in webkit
      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 
     17 package android.webkit;
     18 
     19 import android.net.ParseException;
     20 import android.net.WebAddress;
     21 import android.net.http.AndroidHttpClient;
     22 import android.os.AsyncTask;
     23 import android.util.Log;
     24 
     25 
     26 import java.util.ArrayList;
     27 import java.util.Arrays;
     28 import java.util.Collection;
     29 import java.util.Comparator;
     30 import java.util.Iterator;
     31 import java.util.LinkedHashMap;
     32 import java.util.Map;
     33 import java.util.SortedSet;
     34 import java.util.TreeSet;
     35 
     36 /**
     37  * CookieManager manages cookies according to RFC2109 spec.
     38  */
     39 public 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         if (JniUtil.useChromiumHttpStack()) {
    297             setCookie(url, value, false);
    298             return;
    299         }
    300 
    301         WebAddress uri;
    302         try {
    303             uri = new WebAddress(url);
    304         } catch (ParseException ex) {
    305             Log.e(LOGTAG, "Bad address: " + url);
    306             return;
    307         }
    308 
    309         setCookie(uri, value);
    310     }
    311 
    312     /**
    313      * Set cookie for a given url. The old cookie with same host/path/name will
    314      * be removed. The new cookie will be added if it is not expired or it does
    315      * not have expiration which implies it is session cookie.
    316      * @param url The url which cookie is set for
    317      * @param value The value for set-cookie: in http response header
    318      * @param privateBrowsing cookie jar to use
    319      * @hide hiding private browsing
    320      */
    321     public void setCookie(String url, String value, boolean privateBrowsing) {
    322         if (!JniUtil.useChromiumHttpStack()) {
    323             setCookie(url, value);
    324             return;
    325         }
    326 
    327         WebAddress uri;
    328         try {
    329             uri = new WebAddress(url);
    330         } catch (ParseException ex) {
    331             Log.e(LOGTAG, "Bad address: " + url);
    332             return;
    333         }
    334 
    335         nativeSetCookie(uri.toString(), value, privateBrowsing);
    336     }
    337 
    338     /**
    339      * Set cookie for a given uri. The old cookie with same host/path/name will
    340      * be removed. The new cookie will be added if it is not expired or it does
    341      * not have expiration which implies it is session cookie.
    342      * @param uri The uri which cookie is set for
    343      * @param value The value for set-cookie: in http response header
    344      * @hide - hide this because it takes in a parameter of type WebAddress,
    345      * a system private class.
    346      */
    347     public synchronized void setCookie(WebAddress uri, String value) {
    348         if (JniUtil.useChromiumHttpStack()) {
    349             nativeSetCookie(uri.toString(), value, false);
    350             return;
    351         }
    352 
    353         if (value != null && value.length() > MAX_COOKIE_LENGTH) {
    354             return;
    355         }
    356         if (!mAcceptCookie || uri == null) {
    357             return;
    358         }
    359         if (DebugFlags.COOKIE_MANAGER) {
    360             Log.v(LOGTAG, "setCookie: uri: " + uri + " value: " + value);
    361         }
    362 
    363         String[] hostAndPath = getHostAndPath(uri);
    364         if (hostAndPath == null) {
    365             return;
    366         }
    367 
    368         // For default path, when setting a cookie, the spec says:
    369         //Path:   Defaults to the path of the request URL that generated the
    370         // Set-Cookie response, up to, but not including, the
    371         // right-most /.
    372         if (hostAndPath[1].length() > 1) {
    373             int index = hostAndPath[1].lastIndexOf(PATH_DELIM);
    374             hostAndPath[1] = hostAndPath[1].substring(0,
    375                     index > 0 ? index : index + 1);
    376         }
    377 
    378         ArrayList<Cookie> cookies = null;
    379         try {
    380             cookies = parseCookie(hostAndPath[0], hostAndPath[1], value);
    381         } catch (RuntimeException ex) {
    382             Log.e(LOGTAG, "parse cookie failed for: " + value);
    383         }
    384 
    385         if (cookies == null || cookies.size() == 0) {
    386             return;
    387         }
    388 
    389         String baseDomain = getBaseDomain(hostAndPath[0]);
    390         ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain);
    391         if (cookieList == null) {
    392             cookieList = CookieSyncManager.getInstance()
    393                     .getCookiesForDomain(baseDomain);
    394             mCookieMap.put(baseDomain, cookieList);
    395         }
    396 
    397         long now = System.currentTimeMillis();
    398         int size = cookies.size();
    399         for (int i = 0; i < size; i++) {
    400             Cookie cookie = cookies.get(i);
    401 
    402             boolean done = false;
    403             Iterator<Cookie> iter = cookieList.iterator();
    404             while (iter.hasNext()) {
    405                 Cookie cookieEntry = iter.next();
    406                 if (cookie.exactMatch(cookieEntry)) {
    407                     // expires == -1 means no expires defined. Otherwise
    408                     // negative means far future
    409                     if (cookie.expires < 0 || cookie.expires > now) {
    410                         // secure cookies can't be overwritten by non-HTTPS url
    411                         if (!cookieEntry.secure || HTTPS.equals(uri.getScheme())) {
    412                             cookieEntry.value = cookie.value;
    413                             cookieEntry.expires = cookie.expires;
    414                             cookieEntry.secure = cookie.secure;
    415                             cookieEntry.lastAcessTime = now;
    416                             cookieEntry.lastUpdateTime = now;
    417                             cookieEntry.mode = Cookie.MODE_REPLACED;
    418                         }
    419                     } else {
    420                         cookieEntry.lastUpdateTime = now;
    421                         cookieEntry.mode = Cookie.MODE_DELETED;
    422                     }
    423                     done = true;
    424                     break;
    425                 }
    426             }
    427 
    428             // expires == -1 means no expires defined. Otherwise negative means
    429             // far future
    430             if (!done && (cookie.expires < 0 || cookie.expires > now)) {
    431                 cookie.lastAcessTime = now;
    432                 cookie.lastUpdateTime = now;
    433                 cookie.mode = Cookie.MODE_NEW;
    434                 if (cookieList.size() > MAX_COOKIE_COUNT_PER_BASE_DOMAIN) {
    435                     Cookie toDelete = new Cookie();
    436                     toDelete.lastAcessTime = now;
    437                     Iterator<Cookie> iter2 = cookieList.iterator();
    438                     while (iter2.hasNext()) {
    439                         Cookie cookieEntry2 = iter2.next();
    440                         if ((cookieEntry2.lastAcessTime < toDelete.lastAcessTime)
    441                                 && cookieEntry2.mode != Cookie.MODE_DELETED) {
    442                             toDelete = cookieEntry2;
    443                         }
    444                     }
    445                     toDelete.mode = Cookie.MODE_DELETED;
    446                 }
    447                 cookieList.add(cookie);
    448             }
    449         }
    450     }
    451 
    452     /**
    453      * Get cookie(s) for a given url so that it can be set to "cookie:" in http
    454      * request header.
    455      * @param url The url needs cookie
    456      * @return The cookies in the format of NAME=VALUE [; NAME=VALUE]
    457      */
    458     public String getCookie(String url) {
    459         if (JniUtil.useChromiumHttpStack()) {
    460             return getCookie(url, false);
    461         }
    462 
    463         WebAddress uri;
    464         try {
    465             uri = new WebAddress(url);
    466         } catch (ParseException ex) {
    467             Log.e(LOGTAG, "Bad address: " + url);
    468             return null;
    469         }
    470 
    471         return getCookie(uri);
    472     }
    473 
    474     /**
    475      * Get cookie(s) for a given url so that it can be set to "cookie:" in http
    476      * request header.
    477      * @param url The url needs cookie
    478      * @param privateBrowsing cookie jar to use
    479      * @return The cookies in the format of NAME=VALUE [; NAME=VALUE]
    480      * @hide Private mode is not very well exposed for now
    481      */
    482     public String getCookie(String url, boolean privateBrowsing) {
    483         if (!JniUtil.useChromiumHttpStack()) {
    484             // Just redirect to regular get cookie for android stack
    485             return getCookie(url);
    486         }
    487 
    488         WebAddress uri;
    489         try {
    490             uri = new WebAddress(url);
    491         } catch (ParseException ex) {
    492             Log.e(LOGTAG, "Bad address: " + url);
    493             return null;
    494         }
    495 
    496         return nativeGetCookie(uri.toString(), privateBrowsing);
    497     }
    498 
    499     /**
    500      * Get cookie(s) for a given uri so that it can be set to "cookie:" in http
    501      * request header.
    502      * @param uri The uri needs cookie
    503      * @return The cookies in the format of NAME=VALUE [; NAME=VALUE]
    504      * @hide - hide this because it has a parameter of type WebAddress, which
    505      * is a system private class.
    506      */
    507     public synchronized String getCookie(WebAddress uri) {
    508         if (JniUtil.useChromiumHttpStack()) {
    509             return nativeGetCookie(uri.toString(), false);
    510         }
    511 
    512         if (!mAcceptCookie || uri == null) {
    513             return null;
    514         }
    515 
    516         String[] hostAndPath = getHostAndPath(uri);
    517         if (hostAndPath == null) {
    518             return null;
    519         }
    520 
    521         String baseDomain = getBaseDomain(hostAndPath[0]);
    522         ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain);
    523         if (cookieList == null) {
    524             cookieList = CookieSyncManager.getInstance()
    525                     .getCookiesForDomain(baseDomain);
    526             mCookieMap.put(baseDomain, cookieList);
    527         }
    528 
    529         long now = System.currentTimeMillis();
    530         boolean secure = HTTPS.equals(uri.getScheme());
    531         Iterator<Cookie> iter = cookieList.iterator();
    532 
    533         SortedSet<Cookie> cookieSet = new TreeSet<Cookie>(COMPARATOR);
    534         while (iter.hasNext()) {
    535             Cookie cookie = iter.next();
    536             if (cookie.domainMatch(hostAndPath[0]) &&
    537                     cookie.pathMatch(hostAndPath[1])
    538                     // expires == -1 means no expires defined. Otherwise
    539                     // negative means far future
    540                     && (cookie.expires < 0 || cookie.expires > now)
    541                     && (!cookie.secure || secure)
    542                     && cookie.mode != Cookie.MODE_DELETED) {
    543                 cookie.lastAcessTime = now;
    544                 cookieSet.add(cookie);
    545             }
    546         }
    547 
    548         StringBuilder ret = new StringBuilder(256);
    549         Iterator<Cookie> setIter = cookieSet.iterator();
    550         while (setIter.hasNext()) {
    551             Cookie cookie = setIter.next();
    552             if (ret.length() > 0) {
    553                 ret.append(SEMICOLON);
    554                 // according to RC2109, SEMICOLON is official separator,
    555                 // but when log in yahoo.com, it needs WHITE_SPACE too.
    556                 ret.append(WHITE_SPACE);
    557             }
    558 
    559             ret.append(cookie.name);
    560             if (cookie.value != null) {
    561                 ret.append(EQUAL);
    562                 ret.append(cookie.value);
    563             }
    564         }
    565 
    566         if (ret.length() > 0) {
    567             if (DebugFlags.COOKIE_MANAGER) {
    568                 Log.v(LOGTAG, "getCookie: uri: " + uri + " value: " + ret);
    569             }
    570             return ret.toString();
    571         } else {
    572             if (DebugFlags.COOKIE_MANAGER) {
    573                 Log.v(LOGTAG, "getCookie: uri: " + uri
    574                         + " But can't find cookie.");
    575             }
    576             return null;
    577         }
    578     }
    579 
    580     /**
    581      * Waits for pending operations to completed.
    582      * {@hide}  Too late to release publically.
    583      */
    584     public void waitForCookieOperationsToComplete() {
    585         // Note that this function is applicable for both the java
    586         // and native http stacks, and works correctly with either.
    587         synchronized (this) {
    588             while (pendingCookieOperations > 0) {
    589                 try {
    590                     wait();
    591                 } catch (InterruptedException e) { }
    592             }
    593         }
    594     }
    595 
    596     private synchronized void signalCookieOperationsComplete() {
    597         pendingCookieOperations--;
    598         assert pendingCookieOperations > -1;
    599         notify();
    600     }
    601 
    602     private synchronized void signalCookieOperationsStart() {
    603         pendingCookieOperations++;
    604     }
    605 
    606     /**
    607      * Remove all session cookies, which are cookies without expiration date
    608      */
    609     public void removeSessionCookie() {
    610         signalCookieOperationsStart();
    611         if (JniUtil.useChromiumHttpStack()) {
    612             new AsyncTask<Void, Void, Void>() {
    613                 protected Void doInBackground(Void... none) {
    614                     nativeRemoveSessionCookie();
    615                     signalCookieOperationsComplete();
    616                     return null;
    617                 }
    618             }.execute();
    619             return;
    620         }
    621 
    622         final Runnable clearCache = new Runnable() {
    623             public void run() {
    624                 synchronized(CookieManager.this) {
    625                     Collection<ArrayList<Cookie>> cookieList = mCookieMap.values();
    626                     Iterator<ArrayList<Cookie>> listIter = cookieList.iterator();
    627                     while (listIter.hasNext()) {
    628                         ArrayList<Cookie> list = listIter.next();
    629                         Iterator<Cookie> iter = list.iterator();
    630                         while (iter.hasNext()) {
    631                             Cookie cookie = iter.next();
    632                             if (cookie.expires == -1) {
    633                                 iter.remove();
    634                             }
    635                         }
    636                     }
    637                     CookieSyncManager.getInstance().clearSessionCookies();
    638                     signalCookieOperationsComplete();
    639                 }
    640             }
    641         };
    642         new Thread(clearCache).start();
    643     }
    644 
    645     /**
    646      * Remove all cookies
    647      */
    648     public void removeAllCookie() {
    649         if (JniUtil.useChromiumHttpStack()) {
    650             nativeRemoveAllCookie();
    651             return;
    652         }
    653 
    654         final Runnable clearCache = new Runnable() {
    655             public void run() {
    656                 synchronized(CookieManager.this) {
    657                     mCookieMap = new LinkedHashMap<String, ArrayList<Cookie>>(
    658                             MAX_DOMAIN_COUNT, 0.75f, true);
    659                     CookieSyncManager.getInstance().clearAllCookies();
    660                 }
    661             }
    662         };
    663         new Thread(clearCache).start();
    664     }
    665 
    666     /**
    667      *  Return true if there are stored cookies.
    668      */
    669     public synchronized boolean hasCookies() {
    670         if (JniUtil.useChromiumHttpStack()) {
    671             return hasCookies(false);
    672         }
    673 
    674         return CookieSyncManager.getInstance().hasCookies();
    675     }
    676 
    677     /**
    678      *  Return true if there are stored cookies.
    679      *  @param privateBrowsing cookie jar to use
    680      *  @hide Hiding private mode
    681      */
    682     public synchronized boolean hasCookies(boolean privateBrowsing) {
    683         if (!JniUtil.useChromiumHttpStack()) {
    684             return hasCookies();
    685         }
    686 
    687         return nativeHasCookies(privateBrowsing);
    688     }
    689 
    690     /**
    691      * Remove all expired cookies
    692      */
    693     public void removeExpiredCookie() {
    694         if (JniUtil.useChromiumHttpStack()) {
    695             nativeRemoveExpiredCookie();
    696             return;
    697         }
    698 
    699         final Runnable clearCache = new Runnable() {
    700             public void run() {
    701                 synchronized(CookieManager.this) {
    702                     long now = System.currentTimeMillis();
    703                     Collection<ArrayList<Cookie>> cookieList = mCookieMap.values();
    704                     Iterator<ArrayList<Cookie>> listIter = cookieList.iterator();
    705                     while (listIter.hasNext()) {
    706                         ArrayList<Cookie> list = listIter.next();
    707                         Iterator<Cookie> iter = list.iterator();
    708                         while (iter.hasNext()) {
    709                             Cookie cookie = iter.next();
    710                             // expires == -1 means no expires defined. Otherwise
    711                             // negative means far future
    712                             if (cookie.expires > 0 && cookie.expires < now) {
    713                                 iter.remove();
    714                             }
    715                         }
    716                     }
    717                     CookieSyncManager.getInstance().clearExpiredCookies(now);
    718                 }
    719             }
    720         };
    721         new Thread(clearCache).start();
    722     }
    723 
    724     /**
    725      * Package level api, called from CookieSyncManager
    726      *
    727      * Flush all cookies managed by the Chrome HTTP stack to flash.
    728      */
    729     void flushCookieStore() {
    730         if (JniUtil.useChromiumHttpStack()) {
    731             nativeFlushCookieStore();
    732         }
    733     }
    734 
    735     /**
    736      * Whether cookies are accepted for file scheme URLs.
    737      */
    738     public static boolean allowFileSchemeCookies() {
    739         if (JniUtil.useChromiumHttpStack()) {
    740             return nativeAcceptFileSchemeCookies();
    741         } else {
    742             return true;
    743         }
    744     }
    745 
    746     /**
    747      * Sets whether cookies are accepted for file scheme URLs.
    748      *
    749      * Use of cookies with file scheme URLs is potentially insecure. Do not use this feature unless
    750      * you can be sure that no unintentional sharing of cookie data can take place.
    751      * <p>
    752      * Note that calls to this method will have no effect if made after a WebView or CookieManager
    753      * instance has been created.
    754      */
    755     public static void setAcceptFileSchemeCookies(boolean accept) {
    756         if (JniUtil.useChromiumHttpStack()) {
    757             nativeSetAcceptFileSchemeCookies(accept);
    758         }
    759     }
    760 
    761     /**
    762      * Package level api, called from CookieSyncManager
    763      *
    764      * Get a list of cookies which are updated since a given time.
    765      * @param last The given time in millisec
    766      * @return A list of cookies
    767      */
    768     synchronized ArrayList<Cookie> getUpdatedCookiesSince(long last) {
    769         ArrayList<Cookie> cookies = new ArrayList<Cookie>();
    770         Collection<ArrayList<Cookie>> cookieList = mCookieMap.values();
    771         Iterator<ArrayList<Cookie>> listIter = cookieList.iterator();
    772         while (listIter.hasNext()) {
    773             ArrayList<Cookie> list = listIter.next();
    774             Iterator<Cookie> iter = list.iterator();
    775             while (iter.hasNext()) {
    776                 Cookie cookie = iter.next();
    777                 if (cookie.lastUpdateTime > last) {
    778                     cookies.add(cookie);
    779                 }
    780             }
    781         }
    782         return cookies;
    783     }
    784 
    785     /**
    786      * Package level api, called from CookieSyncManager
    787      *
    788      * Delete a Cookie in the RAM
    789      * @param cookie Cookie to be deleted
    790      */
    791     synchronized void deleteACookie(Cookie cookie) {
    792         if (cookie.mode == Cookie.MODE_DELETED) {
    793             String baseDomain = getBaseDomain(cookie.domain);
    794             ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain);
    795             if (cookieList != null) {
    796                 cookieList.remove(cookie);
    797                 if (cookieList.isEmpty()) {
    798                     mCookieMap.remove(baseDomain);
    799                 }
    800             }
    801         }
    802     }
    803 
    804     /**
    805      * Package level api, called from CookieSyncManager
    806      *
    807      * Called after a cookie is synced to FLASH
    808      * @param cookie Cookie to be synced
    809      */
    810     synchronized void syncedACookie(Cookie cookie) {
    811         cookie.mode = Cookie.MODE_NORMAL;
    812     }
    813 
    814     /**
    815      * Package level api, called from CookieSyncManager
    816      *
    817      * Delete the least recent used domains if the total cookie count in RAM
    818      * exceeds the limit
    819      * @return A list of cookies which are removed from RAM
    820      */
    821     synchronized ArrayList<Cookie> deleteLRUDomain() {
    822         int count = 0;
    823         int byteCount = 0;
    824         int mapSize = mCookieMap.size();
    825 
    826         if (mapSize < MAX_RAM_DOMAIN_COUNT) {
    827             Collection<ArrayList<Cookie>> cookieLists = mCookieMap.values();
    828             Iterator<ArrayList<Cookie>> listIter = cookieLists.iterator();
    829             while (listIter.hasNext() && count < MAX_RAM_COOKIES_COUNT) {
    830                 ArrayList<Cookie> list = listIter.next();
    831                 if (DebugFlags.COOKIE_MANAGER) {
    832                     Iterator<Cookie> iter = list.iterator();
    833                     while (iter.hasNext() && count < MAX_RAM_COOKIES_COUNT) {
    834                         Cookie cookie = iter.next();
    835                         // 14 is 3 * sizeof(long) + sizeof(boolean)
    836                         // + sizeof(byte)
    837                         byteCount += cookie.domain.length()
    838                                 + cookie.path.length()
    839                                 + cookie.name.length()
    840                                 + (cookie.value != null
    841                                         ? cookie.value.length()
    842                                         : 0)
    843                                 + 14;
    844                         count++;
    845                     }
    846                 } else {
    847                     count += list.size();
    848                 }
    849             }
    850         }
    851 
    852         ArrayList<Cookie> retlist = new ArrayList<Cookie>();
    853         if (mapSize >= MAX_RAM_DOMAIN_COUNT || count >= MAX_RAM_COOKIES_COUNT) {
    854             if (DebugFlags.COOKIE_MANAGER) {
    855                 Log.v(LOGTAG, count + " cookies used " + byteCount
    856                         + " bytes with " + mapSize + " domains");
    857             }
    858             Object[] domains = mCookieMap.keySet().toArray();
    859             int toGo = mapSize / 10 + 1;
    860             while (toGo-- > 0){
    861                 String domain = domains[toGo].toString();
    862                 if (DebugFlags.COOKIE_MANAGER) {
    863                     Log.v(LOGTAG, "delete domain: " + domain
    864                             + " from RAM cache");
    865                 }
    866                 retlist.addAll(mCookieMap.get(domain));
    867                 mCookieMap.remove(domain);
    868             }
    869         }
    870         return retlist;
    871     }
    872 
    873     /**
    874      * Extract the host and path out of a uri
    875      * @param uri The given WebAddress
    876      * @return The host and path in the format of String[], String[0] is host
    877      *          which has at least two periods, String[1] is path which always
    878      *          ended with "/"
    879      */
    880     private String[] getHostAndPath(WebAddress uri) {
    881         if (uri.getHost() != null && uri.getPath() != null) {
    882 
    883             /*
    884              * The domain (i.e. host) portion of the cookie is supposed to be
    885              * case-insensitive. We will consistently return the domain in lower
    886              * case, which allows us to do the more efficient equals comparison
    887              * instead of equalIgnoreCase.
    888              *
    889              * See: http://www.ieft.org/rfc/rfc2965.txt (Section 3.3.3)
    890              */
    891             String[] ret = new String[2];
    892             ret[0] = uri.getHost().toLowerCase();
    893             ret[1] = uri.getPath();
    894 
    895             int index = ret[0].indexOf(PERIOD);
    896             if (index == -1) {
    897                 if (uri.getScheme().equalsIgnoreCase("file")) {
    898                     // There is a potential bug where a local file path matches
    899                     // another file in the local web server directory. Still
    900                     // "localhost" is the best pseudo domain name.
    901                     ret[0] = "localhost";
    902                 }
    903             } else if (index == ret[0].lastIndexOf(PERIOD)) {
    904                 // cookie host must have at least two periods
    905                 ret[0] = PERIOD + ret[0];
    906             }
    907 
    908             if (ret[1].charAt(0) != PATH_DELIM) {
    909                 return null;
    910             }
    911 
    912             /*
    913              * find cookie path, e.g. for http://www.google.com, the path is "/"
    914              * for http://www.google.com/lab/, the path is "/lab"
    915              * for http://www.google.com/lab/foo, the path is "/lab/foo"
    916              * for http://www.google.com/lab?hl=en, the path is "/lab"
    917              * for http://www.google.com/lab.asp?hl=en, the path is "/lab.asp"
    918              * Note: the path from URI has at least one "/"
    919              * See:
    920              * http://www.unix.com.ua/rfc/rfc2109.html
    921              */
    922             index = ret[1].indexOf(QUESTION_MARK);
    923             if (index != -1) {
    924                 ret[1] = ret[1].substring(0, index);
    925             }
    926 
    927             return ret;
    928         } else
    929             return null;
    930     }
    931 
    932     /**
    933      * Get the base domain for a give host. E.g. mail.google.com will return
    934      * google.com
    935      * @param host The give host
    936      * @return the base domain
    937      */
    938     private String getBaseDomain(String host) {
    939         int startIndex = 0;
    940         int nextIndex = host.indexOf(PERIOD);
    941         int lastIndex = host.lastIndexOf(PERIOD);
    942         while (nextIndex < lastIndex) {
    943             startIndex = nextIndex + 1;
    944             nextIndex = host.indexOf(PERIOD, startIndex);
    945         }
    946         if (startIndex > 0) {
    947             return host.substring(startIndex);
    948         } else {
    949             return host;
    950         }
    951     }
    952 
    953     /**
    954      * parseCookie() parses the cookieString which is a comma-separated list of
    955      * one or more cookies in the format of "NAME=VALUE; expires=DATE;
    956      * path=PATH; domain=DOMAIN_NAME; secure httponly" to a list of Cookies.
    957      * Here is a sample: IGDND=1, IGPC=ET=UB8TSNwtDmQ:AF=0; expires=Sun,
    958      * 17-Jan-2038 19:14:07 GMT; path=/ig; domain=.google.com, =,
    959      * PREF=ID=408909b1b304593d:TM=1156459854:LM=1156459854:S=V-vCAU6Sh-gobCfO;
    960      * expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com which
    961      * contains 3 cookies IGDND, IGPC, PREF and an empty cookie
    962      * @param host The default host
    963      * @param path The default path
    964      * @param cookieString The string coming from "Set-Cookie:"
    965      * @return A list of Cookies
    966      */
    967     private ArrayList<Cookie> parseCookie(String host, String path,
    968             String cookieString) {
    969         ArrayList<Cookie> ret = new ArrayList<Cookie>();
    970 
    971         int index = 0;
    972         int length = cookieString.length();
    973         while (true) {
    974             Cookie cookie = null;
    975 
    976             // done
    977             if (index < 0 || index >= length) {
    978                 break;
    979             }
    980 
    981             // skip white space
    982             if (cookieString.charAt(index) == WHITE_SPACE) {
    983                 index++;
    984                 continue;
    985             }
    986 
    987             /*
    988              * get NAME=VALUE; pair. detecting the end of a pair is tricky, it
    989              * can be the end of a string, like "foo=bluh", it can be semicolon
    990              * like "foo=bluh;path=/"; or it can be enclosed by \", like
    991              * "foo=\"bluh bluh\";path=/"
    992              *
    993              * Note: in the case of "foo=bluh, bar=bluh;path=/", we interpret
    994              * it as one cookie instead of two cookies.
    995              */
    996             int semicolonIndex = cookieString.indexOf(SEMICOLON, index);
    997             int equalIndex = cookieString.indexOf(EQUAL, index);
    998             cookie = new Cookie(host, path);
    999 
   1000             // Cookies like "testcookie; path=/;" are valid and used
   1001             // (lovefilm.se).
   1002             // Look for 2 cases:
   1003             // 1. "foo" or "foo;" where equalIndex is -1
   1004             // 2. "foo; path=..." where the first semicolon is before an equal
   1005             //    and a semicolon exists.
   1006             if ((semicolonIndex != -1 && (semicolonIndex < equalIndex)) ||
   1007                     equalIndex == -1) {
   1008                 // Fix up the index in case we have a string like "testcookie"
   1009                 if (semicolonIndex == -1) {
   1010                     semicolonIndex = length;
   1011                 }
   1012                 cookie.name = cookieString.substring(index, semicolonIndex);
   1013                 cookie.value = null;
   1014             } else {
   1015                 cookie.name = cookieString.substring(index, equalIndex);
   1016                 // Make sure we do not throw an exception if the cookie is like
   1017                 // "foo="
   1018                 if ((equalIndex < length - 1) &&
   1019                         (cookieString.charAt(equalIndex + 1) == QUOTATION)) {
   1020                     index = cookieString.indexOf(QUOTATION, equalIndex + 2);
   1021                     if (index == -1) {
   1022                         // bad format, force return
   1023                         break;
   1024                     }
   1025                 }
   1026                 // Get the semicolon index again in case it was contained within
   1027                 // the quotations.
   1028                 semicolonIndex = cookieString.indexOf(SEMICOLON, index);
   1029                 if (semicolonIndex == -1) {
   1030                     semicolonIndex = length;
   1031                 }
   1032                 if (semicolonIndex - equalIndex > MAX_COOKIE_LENGTH) {
   1033                     // cookie is too big, trim it
   1034                     cookie.value = cookieString.substring(equalIndex + 1,
   1035                             equalIndex + 1 + MAX_COOKIE_LENGTH);
   1036                 } else if (equalIndex + 1 == semicolonIndex
   1037                         || semicolonIndex < equalIndex) {
   1038                     // this is an unusual case like "foo=;" or "foo="
   1039                     cookie.value = "";
   1040                 } else {
   1041                     cookie.value = cookieString.substring(equalIndex + 1,
   1042                             semicolonIndex);
   1043                 }
   1044             }
   1045             // get attributes
   1046             index = semicolonIndex;
   1047             while (true) {
   1048                 // done
   1049                 if (index < 0 || index >= length) {
   1050                     break;
   1051                 }
   1052 
   1053                 // skip white space and semicolon
   1054                 if (cookieString.charAt(index) == WHITE_SPACE
   1055                         || cookieString.charAt(index) == SEMICOLON) {
   1056                     index++;
   1057                     continue;
   1058                 }
   1059 
   1060                 // comma means next cookie
   1061                 if (cookieString.charAt(index) == COMMA) {
   1062                     index++;
   1063                     break;
   1064                 }
   1065 
   1066                 // "secure" is a known attribute doesn't use "=";
   1067                 // while sites like live.com uses "secure="
   1068                 if (length - index >= SECURE_LENGTH
   1069                         && cookieString.substring(index, index + SECURE_LENGTH).
   1070                         equalsIgnoreCase(SECURE)) {
   1071                     index += SECURE_LENGTH;
   1072                     cookie.secure = true;
   1073                     if (index == length) break;
   1074                     if (cookieString.charAt(index) == EQUAL) index++;
   1075                     continue;
   1076                 }
   1077 
   1078                 // "httponly" is a known attribute doesn't use "=";
   1079                 // while sites like live.com uses "httponly="
   1080                 if (length - index >= HTTP_ONLY_LENGTH
   1081                         && cookieString.substring(index,
   1082                             index + HTTP_ONLY_LENGTH).
   1083                         equalsIgnoreCase(HTTP_ONLY)) {
   1084                     index += HTTP_ONLY_LENGTH;
   1085                     if (index == length) break;
   1086                     if (cookieString.charAt(index) == EQUAL) index++;
   1087                     // FIXME: currently only parse the attribute
   1088                     continue;
   1089                 }
   1090                 equalIndex = cookieString.indexOf(EQUAL, index);
   1091                 if (equalIndex > 0) {
   1092                     String name = cookieString.substring(index, equalIndex).toLowerCase();
   1093                     int valueIndex = equalIndex + 1;
   1094                     while (valueIndex < length && cookieString.charAt(valueIndex) == WHITE_SPACE) {
   1095                         valueIndex++;
   1096                     }
   1097 
   1098                     if (name.equals(EXPIRES)) {
   1099                         int comaIndex = cookieString.indexOf(COMMA, equalIndex);
   1100 
   1101                         // skip ',' in (Wdy, DD-Mon-YYYY HH:MM:SS GMT) or
   1102                         // (Weekday, DD-Mon-YY HH:MM:SS GMT) if it applies.
   1103                         // "Wednesday" is the longest Weekday which has length 9
   1104                         if ((comaIndex != -1) &&
   1105                                 (comaIndex - valueIndex <= 10)) {
   1106                             index = comaIndex + 1;
   1107                         }
   1108                     }
   1109                     semicolonIndex = cookieString.indexOf(SEMICOLON, index);
   1110                     int commaIndex = cookieString.indexOf(COMMA, index);
   1111                     if (semicolonIndex == -1 && commaIndex == -1) {
   1112                         index = length;
   1113                     } else if (semicolonIndex == -1) {
   1114                         index = commaIndex;
   1115                     } else if (commaIndex == -1) {
   1116                         index = semicolonIndex;
   1117                     } else {
   1118                         index = Math.min(semicolonIndex, commaIndex);
   1119                     }
   1120                     String value = cookieString.substring(valueIndex, index);
   1121 
   1122                     // Strip quotes if they exist
   1123                     if (value.length() > 2 && value.charAt(0) == QUOTATION) {
   1124                         int endQuote = value.indexOf(QUOTATION, 1);
   1125                         if (endQuote > 0) {
   1126                             value = value.substring(1, endQuote);
   1127                         }
   1128                     }
   1129                     if (name.equals(EXPIRES)) {
   1130                         try {
   1131                             cookie.expires = AndroidHttpClient.parseDate(value);
   1132                         } catch (IllegalArgumentException ex) {
   1133                             Log.e(LOGTAG,
   1134                                     "illegal format for expires: " + value);
   1135                         }
   1136                     } else if (name.equals(MAX_AGE)) {
   1137                         try {
   1138                             cookie.expires = System.currentTimeMillis() + 1000
   1139                                     * Long.parseLong(value);
   1140                         } catch (NumberFormatException ex) {
   1141                             Log.e(LOGTAG,
   1142                                     "illegal format for max-age: " + value);
   1143                         }
   1144                     } else if (name.equals(PATH)) {
   1145                         // only allow non-empty path value
   1146                         if (value.length() > 0) {
   1147                             cookie.path = value;
   1148                         }
   1149                     } else if (name.equals(DOMAIN)) {
   1150                         int lastPeriod = value.lastIndexOf(PERIOD);
   1151                         if (lastPeriod == 0) {
   1152                             // disallow cookies set for TLDs like [.com]
   1153                             cookie.domain = null;
   1154                             continue;
   1155                         }
   1156                         try {
   1157                             Integer.parseInt(value.substring(lastPeriod + 1));
   1158                             // no wildcard for ip address match
   1159                             if (!value.equals(host)) {
   1160                                 // no cross-site cookie
   1161                                 cookie.domain = null;
   1162                             }
   1163                             continue;
   1164                         } catch (NumberFormatException ex) {
   1165                             // ignore the exception, value is a host name
   1166                         }
   1167                         value = value.toLowerCase();
   1168                         if (value.charAt(0) != PERIOD) {
   1169                             // pre-pended dot to make it as a domain cookie
   1170                             value = PERIOD + value;
   1171                             lastPeriod++;
   1172                         }
   1173                         if (host.endsWith(value.substring(1))) {
   1174                             int len = value.length();
   1175                             int hostLen = host.length();
   1176                             if (hostLen > (len - 1)
   1177                                     && host.charAt(hostLen - len) != PERIOD) {
   1178                                 // make sure the bar.com doesn't match .ar.com
   1179                                 cookie.domain = null;
   1180                                 continue;
   1181                             }
   1182                             // disallow cookies set on ccTLDs like [.co.uk]
   1183                             if ((len == lastPeriod + 3)
   1184                                     && (len >= 6 && len <= 8)) {
   1185                                 String s = value.substring(1, lastPeriod);
   1186                                 if (Arrays.binarySearch(BAD_COUNTRY_2LDS, s) >= 0) {
   1187                                     cookie.domain = null;
   1188                                     continue;
   1189                                 }
   1190                             }
   1191                             cookie.domain = value;
   1192                         } else {
   1193                             // no cross-site or more specific sub-domain cookie
   1194                             cookie.domain = null;
   1195                         }
   1196                     }
   1197                 } else {
   1198                     // bad format, force return
   1199                     index = length;
   1200                 }
   1201             }
   1202             if (cookie != null && cookie.domain != null) {
   1203                 ret.add(cookie);
   1204             }
   1205         }
   1206         return ret;
   1207     }
   1208 
   1209     // Native functions
   1210     private static native boolean nativeAcceptCookie();
   1211     private static native String nativeGetCookie(String url, boolean privateBrowsing);
   1212     private static native boolean nativeHasCookies(boolean privateBrowsing);
   1213     private static native void nativeRemoveAllCookie();
   1214     private static native void nativeRemoveExpiredCookie();
   1215     private static native void nativeRemoveSessionCookie();
   1216     private static native void nativeSetAcceptCookie(boolean accept);
   1217     private static native void nativeSetCookie(String url, String value, boolean privateBrowsing);
   1218     private static native void nativeFlushCookieStore();
   1219     private static native boolean nativeAcceptFileSchemeCookies();
   1220     private static native void nativeSetAcceptFileSchemeCookies(boolean accept);
   1221 }
   1222