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