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