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