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