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