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