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