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