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