| /* |
| * Copyright (C) 2006 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.webkit; |
| |
| import android.net.ParseException; |
| import android.net.WebAddress; |
| import android.net.http.AndroidHttpClient; |
| import android.util.Log; |
| |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Comparator; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.SortedSet; |
| import java.util.TreeSet; |
| |
| /** |
| * CookieManager manages cookies according to RFC2109 spec. |
| */ |
| public final class CookieManager { |
| |
| private static CookieManager sRef; |
| |
| private static final String LOGTAG = "webkit"; |
| |
| private static final String DOMAIN = "domain"; |
| |
| private static final String PATH = "path"; |
| |
| private static final String EXPIRES = "expires"; |
| |
| private static final String SECURE = "secure"; |
| |
| private static final String MAX_AGE = "max-age"; |
| |
| private static final String HTTP_ONLY = "httponly"; |
| |
| private static final String HTTPS = "https"; |
| |
| private static final char PERIOD = '.'; |
| |
| private static final char COMMA = ','; |
| |
| private static final char SEMICOLON = ';'; |
| |
| private static final char EQUAL = '='; |
| |
| private static final char PATH_DELIM = '/'; |
| |
| private static final char QUESTION_MARK = '?'; |
| |
| private static final char WHITE_SPACE = ' '; |
| |
| private static final char QUOTATION = '\"'; |
| |
| private static final int SECURE_LENGTH = SECURE.length(); |
| |
| private static final int HTTP_ONLY_LENGTH = HTTP_ONLY.length(); |
| |
| // RFC2109 defines 4k as maximum size of a cookie |
| private static final int MAX_COOKIE_LENGTH = 4 * 1024; |
| |
| // RFC2109 defines 20 as max cookie count per domain. As we track with base |
| // domain, we allow 50 per base domain |
| private static final int MAX_COOKIE_COUNT_PER_BASE_DOMAIN = 50; |
| |
| // RFC2109 defines 300 as max count of domains. As we track with base |
| // domain, we set 200 as max base domain count |
| private static final int MAX_DOMAIN_COUNT = 200; |
| |
| // max cookie count to limit RAM cookie takes less than 100k, it is based on |
| // average cookie entry size is less than 100 bytes |
| private static final int MAX_RAM_COOKIES_COUNT = 1000; |
| |
| // max domain count to limit RAM cookie takes less than 100k, |
| private static final int MAX_RAM_DOMAIN_COUNT = 15; |
| |
| private Map<String, ArrayList<Cookie>> mCookieMap = new LinkedHashMap |
| <String, ArrayList<Cookie>>(MAX_DOMAIN_COUNT, 0.75f, true); |
| |
| private boolean mAcceptCookie = true; |
| |
| /** |
| * This contains a list of 2nd-level domains that aren't allowed to have |
| * wildcards when combined with country-codes. For example: [.co.uk]. |
| */ |
| private final static String[] BAD_COUNTRY_2LDS = |
| { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info", |
| "lg", "ne", "net", "or", "org" }; |
| |
| static { |
| Arrays.sort(BAD_COUNTRY_2LDS); |
| } |
| |
| /** |
| * Package level class to be accessed by cookie sync manager |
| */ |
| static class Cookie { |
| static final byte MODE_NEW = 0; |
| |
| static final byte MODE_NORMAL = 1; |
| |
| static final byte MODE_DELETED = 2; |
| |
| static final byte MODE_REPLACED = 3; |
| |
| String domain; |
| |
| String path; |
| |
| String name; |
| |
| String value; |
| |
| long expires; |
| |
| long lastAcessTime; |
| |
| long lastUpdateTime; |
| |
| boolean secure; |
| |
| byte mode; |
| |
| Cookie() { |
| } |
| |
| Cookie(String defaultDomain, String defaultPath) { |
| domain = defaultDomain; |
| path = defaultPath; |
| expires = -1; |
| } |
| |
| boolean exactMatch(Cookie in) { |
| // An exact match means that domain, path, and name are equal. If |
| // both values are null, the cookies match. If both values are |
| // non-null, the cookies match. If one value is null and the other |
| // is non-null, the cookies do not match (i.e. "foo=;" and "foo;") |
| boolean valuesMatch = !((value == null) ^ (in.value == null)); |
| return domain.equals(in.domain) && path.equals(in.path) && |
| name.equals(in.name) && valuesMatch; |
| } |
| |
| boolean domainMatch(String urlHost) { |
| if (domain.startsWith(".")) { |
| if (urlHost.endsWith(domain.substring(1))) { |
| int len = domain.length(); |
| int urlLen = urlHost.length(); |
| if (urlLen > len - 1) { |
| // make sure bar.com doesn't match .ar.com |
| return urlHost.charAt(urlLen - len) == PERIOD; |
| } |
| return true; |
| } |
| return false; |
| } else { |
| // exact match if domain is not leading w/ dot |
| return urlHost.equals(domain); |
| } |
| } |
| |
| boolean pathMatch(String urlPath) { |
| if (urlPath.startsWith(path)) { |
| int len = path.length(); |
| if (len == 0) { |
| Log.w(LOGTAG, "Empty cookie path"); |
| return false; |
| } |
| int urlLen = urlPath.length(); |
| if (path.charAt(len-1) != PATH_DELIM && urlLen > len) { |
| // make sure /wee doesn't match /we |
| return urlPath.charAt(len) == PATH_DELIM; |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| public String toString() { |
| return "domain: " + domain + "; path: " + path + "; name: " + name |
| + "; value: " + value; |
| } |
| } |
| |
| private static final CookieComparator COMPARATOR = new CookieComparator(); |
| |
| private static final class CookieComparator implements Comparator<Cookie> { |
| public int compare(Cookie cookie1, Cookie cookie2) { |
| // According to RFC 2109, multiple cookies are ordered in a way such |
| // that those with more specific Path attributes precede those with |
| // less specific. Ordering with respect to other attributes (e.g., |
| // Domain) is unspecified. |
| // As Set is not modified if the two objects are same, we do want to |
| // assign different value for each cookie. |
| int diff = cookie2.path.length() - cookie1.path.length(); |
| if (diff != 0) return diff; |
| |
| diff = cookie2.domain.length() - cookie1.domain.length(); |
| if (diff != 0) return diff; |
| |
| // If cookie2 has a null value, it should come later in |
| // the list. |
| if (cookie2.value == null) { |
| // If both cookies have null values, fall back to using the name |
| // difference. |
| if (cookie1.value != null) { |
| return -1; |
| } |
| } else if (cookie1.value == null) { |
| // Now we know that cookie2 does not have a null value, if |
| // cookie1 has a null value, place it later in the list. |
| return 1; |
| } |
| |
| // Fallback to comparing the name to ensure consistent order. |
| return cookie1.name.compareTo(cookie2.name); |
| } |
| } |
| |
| private CookieManager() { |
| } |
| |
| protected Object clone() throws CloneNotSupportedException { |
| throw new CloneNotSupportedException("doesn't implement Cloneable"); |
| } |
| |
| /** |
| * Get a singleton CookieManager. If this is called before any |
| * {@link WebView} is created or outside of {@link WebView} context, the |
| * caller needs to call {@link CookieSyncManager#createInstance(Context)} |
| * first. |
| * |
| * @return CookieManager |
| */ |
| public static synchronized CookieManager getInstance() { |
| if (sRef == null) { |
| sRef = new CookieManager(); |
| } |
| return sRef; |
| } |
| |
| /** |
| * Control whether cookie is enabled or disabled |
| * @param accept TRUE if accept cookie |
| */ |
| public synchronized void setAcceptCookie(boolean accept) { |
| mAcceptCookie = accept; |
| } |
| |
| /** |
| * Return whether cookie is enabled |
| * @return TRUE if accept cookie |
| */ |
| public synchronized boolean acceptCookie() { |
| return mAcceptCookie; |
| } |
| |
| /** |
| * Set cookie for a given url. The old cookie with same host/path/name will |
| * be removed. The new cookie will be added if it is not expired or it does |
| * not have expiration which implies it is session cookie. |
| * @param url The url which cookie is set for |
| * @param value The value for set-cookie: in http response header |
| */ |
| public void setCookie(String url, String value) { |
| WebAddress uri; |
| try { |
| uri = new WebAddress(url); |
| } catch (ParseException ex) { |
| Log.e(LOGTAG, "Bad address: " + url); |
| return; |
| } |
| setCookie(uri, value); |
| } |
| |
| /** |
| * Set cookie for a given uri. The old cookie with same host/path/name will |
| * be removed. The new cookie will be added if it is not expired or it does |
| * not have expiration which implies it is session cookie. |
| * @param uri The uri which cookie is set for |
| * @param value The value for set-cookie: in http response header |
| * @hide - hide this because it takes in a parameter of type WebAddress, |
| * a system private class. |
| */ |
| public synchronized void setCookie(WebAddress uri, String value) { |
| if (value != null && value.length() > MAX_COOKIE_LENGTH) { |
| return; |
| } |
| if (!mAcceptCookie || uri == null) { |
| return; |
| } |
| if (DebugFlags.COOKIE_MANAGER) { |
| Log.v(LOGTAG, "setCookie: uri: " + uri + " value: " + value); |
| } |
| |
| String[] hostAndPath = getHostAndPath(uri); |
| if (hostAndPath == null) { |
| return; |
| } |
| |
| // For default path, when setting a cookie, the spec says: |
| //Path: Defaults to the path of the request URL that generated the |
| // Set-Cookie response, up to, but not including, the |
| // right-most /. |
| if (hostAndPath[1].length() > 1) { |
| int index = hostAndPath[1].lastIndexOf(PATH_DELIM); |
| hostAndPath[1] = hostAndPath[1].substring(0, |
| index > 0 ? index : index + 1); |
| } |
| |
| ArrayList<Cookie> cookies = null; |
| try { |
| cookies = parseCookie(hostAndPath[0], hostAndPath[1], value); |
| } catch (RuntimeException ex) { |
| Log.e(LOGTAG, "parse cookie failed for: " + value); |
| } |
| |
| if (cookies == null || cookies.size() == 0) { |
| return; |
| } |
| |
| String baseDomain = getBaseDomain(hostAndPath[0]); |
| ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain); |
| if (cookieList == null) { |
| cookieList = CookieSyncManager.getInstance() |
| .getCookiesForDomain(baseDomain); |
| mCookieMap.put(baseDomain, cookieList); |
| } |
| |
| long now = System.currentTimeMillis(); |
| int size = cookies.size(); |
| for (int i = 0; i < size; i++) { |
| Cookie cookie = cookies.get(i); |
| |
| boolean done = false; |
| Iterator<Cookie> iter = cookieList.iterator(); |
| while (iter.hasNext()) { |
| Cookie cookieEntry = iter.next(); |
| if (cookie.exactMatch(cookieEntry)) { |
| // expires == -1 means no expires defined. Otherwise |
| // negative means far future |
| if (cookie.expires < 0 || cookie.expires > now) { |
| // secure cookies can't be overwritten by non-HTTPS url |
| if (!cookieEntry.secure || HTTPS.equals(uri.mScheme)) { |
| cookieEntry.value = cookie.value; |
| cookieEntry.expires = cookie.expires; |
| cookieEntry.secure = cookie.secure; |
| cookieEntry.lastAcessTime = now; |
| cookieEntry.lastUpdateTime = now; |
| cookieEntry.mode = Cookie.MODE_REPLACED; |
| } |
| } else { |
| cookieEntry.lastUpdateTime = now; |
| cookieEntry.mode = Cookie.MODE_DELETED; |
| } |
| done = true; |
| break; |
| } |
| } |
| |
| // expires == -1 means no expires defined. Otherwise negative means |
| // far future |
| if (!done && (cookie.expires < 0 || cookie.expires > now)) { |
| cookie.lastAcessTime = now; |
| cookie.lastUpdateTime = now; |
| cookie.mode = Cookie.MODE_NEW; |
| if (cookieList.size() > MAX_COOKIE_COUNT_PER_BASE_DOMAIN) { |
| Cookie toDelete = new Cookie(); |
| toDelete.lastAcessTime = now; |
| Iterator<Cookie> iter2 = cookieList.iterator(); |
| while (iter2.hasNext()) { |
| Cookie cookieEntry2 = iter2.next(); |
| if ((cookieEntry2.lastAcessTime < toDelete.lastAcessTime) |
| && cookieEntry2.mode != Cookie.MODE_DELETED) { |
| toDelete = cookieEntry2; |
| } |
| } |
| toDelete.mode = Cookie.MODE_DELETED; |
| } |
| cookieList.add(cookie); |
| } |
| } |
| } |
| |
| /** |
| * Get cookie(s) for a given url so that it can be set to "cookie:" in http |
| * request header. |
| * @param url The url needs cookie |
| * @return The cookies in the format of NAME=VALUE [; NAME=VALUE] |
| */ |
| public String getCookie(String url) { |
| WebAddress uri; |
| try { |
| uri = new WebAddress(url); |
| } catch (ParseException ex) { |
| Log.e(LOGTAG, "Bad address: " + url); |
| return null; |
| } |
| return getCookie(uri); |
| } |
| |
| /** |
| * Get cookie(s) for a given uri so that it can be set to "cookie:" in http |
| * request header. |
| * @param uri The uri needs cookie |
| * @return The cookies in the format of NAME=VALUE [; NAME=VALUE] |
| * @hide - hide this because it has a parameter of type WebAddress, which |
| * is a system private class. |
| */ |
| public synchronized String getCookie(WebAddress uri) { |
| if (!mAcceptCookie || uri == null) { |
| return null; |
| } |
| |
| String[] hostAndPath = getHostAndPath(uri); |
| if (hostAndPath == null) { |
| return null; |
| } |
| |
| String baseDomain = getBaseDomain(hostAndPath[0]); |
| ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain); |
| if (cookieList == null) { |
| cookieList = CookieSyncManager.getInstance() |
| .getCookiesForDomain(baseDomain); |
| mCookieMap.put(baseDomain, cookieList); |
| } |
| |
| long now = System.currentTimeMillis(); |
| boolean secure = HTTPS.equals(uri.mScheme); |
| Iterator<Cookie> iter = cookieList.iterator(); |
| |
| SortedSet<Cookie> cookieSet = new TreeSet<Cookie>(COMPARATOR); |
| while (iter.hasNext()) { |
| Cookie cookie = iter.next(); |
| if (cookie.domainMatch(hostAndPath[0]) && |
| cookie.pathMatch(hostAndPath[1]) |
| // expires == -1 means no expires defined. Otherwise |
| // negative means far future |
| && (cookie.expires < 0 || cookie.expires > now) |
| && (!cookie.secure || secure) |
| && cookie.mode != Cookie.MODE_DELETED) { |
| cookie.lastAcessTime = now; |
| cookieSet.add(cookie); |
| } |
| } |
| |
| StringBuilder ret = new StringBuilder(256); |
| Iterator<Cookie> setIter = cookieSet.iterator(); |
| while (setIter.hasNext()) { |
| Cookie cookie = setIter.next(); |
| if (ret.length() > 0) { |
| ret.append(SEMICOLON); |
| // according to RC2109, SEMICOLON is official separator, |
| // but when log in yahoo.com, it needs WHITE_SPACE too. |
| ret.append(WHITE_SPACE); |
| } |
| |
| ret.append(cookie.name); |
| if (cookie.value != null) { |
| ret.append(EQUAL); |
| ret.append(cookie.value); |
| } |
| } |
| |
| if (ret.length() > 0) { |
| if (DebugFlags.COOKIE_MANAGER) { |
| Log.v(LOGTAG, "getCookie: uri: " + uri + " value: " + ret); |
| } |
| return ret.toString(); |
| } else { |
| if (DebugFlags.COOKIE_MANAGER) { |
| Log.v(LOGTAG, "getCookie: uri: " + uri |
| + " But can't find cookie."); |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * Remove all session cookies, which are cookies without expiration date |
| */ |
| public void removeSessionCookie() { |
| final Runnable clearCache = new Runnable() { |
| public void run() { |
| synchronized(CookieManager.this) { |
| Collection<ArrayList<Cookie>> cookieList = mCookieMap.values(); |
| Iterator<ArrayList<Cookie>> listIter = cookieList.iterator(); |
| while (listIter.hasNext()) { |
| ArrayList<Cookie> list = listIter.next(); |
| Iterator<Cookie> iter = list.iterator(); |
| while (iter.hasNext()) { |
| Cookie cookie = iter.next(); |
| if (cookie.expires == -1) { |
| iter.remove(); |
| } |
| } |
| } |
| CookieSyncManager.getInstance().clearSessionCookies(); |
| } |
| } |
| }; |
| new Thread(clearCache).start(); |
| } |
| |
| /** |
| * Remove all cookies |
| */ |
| public void removeAllCookie() { |
| final Runnable clearCache = new Runnable() { |
| public void run() { |
| synchronized(CookieManager.this) { |
| mCookieMap = new LinkedHashMap<String, ArrayList<Cookie>>( |
| MAX_DOMAIN_COUNT, 0.75f, true); |
| CookieSyncManager.getInstance().clearAllCookies(); |
| } |
| } |
| }; |
| new Thread(clearCache).start(); |
| } |
| |
| /** |
| * Return true if there are stored cookies. |
| */ |
| public synchronized boolean hasCookies() { |
| return CookieSyncManager.getInstance().hasCookies(); |
| } |
| |
| /** |
| * Remove all expired cookies |
| */ |
| public void removeExpiredCookie() { |
| final Runnable clearCache = new Runnable() { |
| public void run() { |
| synchronized(CookieManager.this) { |
| long now = System.currentTimeMillis(); |
| Collection<ArrayList<Cookie>> cookieList = mCookieMap.values(); |
| Iterator<ArrayList<Cookie>> listIter = cookieList.iterator(); |
| while (listIter.hasNext()) { |
| ArrayList<Cookie> list = listIter.next(); |
| Iterator<Cookie> iter = list.iterator(); |
| while (iter.hasNext()) { |
| Cookie cookie = iter.next(); |
| // expires == -1 means no expires defined. Otherwise |
| // negative means far future |
| if (cookie.expires > 0 && cookie.expires < now) { |
| iter.remove(); |
| } |
| } |
| } |
| CookieSyncManager.getInstance().clearExpiredCookies(now); |
| } |
| } |
| }; |
| new Thread(clearCache).start(); |
| } |
| |
| /** |
| * Package level api, called from CookieSyncManager |
| * |
| * Get a list of cookies which are updated since a given time. |
| * @param last The given time in millisec |
| * @return A list of cookies |
| */ |
| synchronized ArrayList<Cookie> getUpdatedCookiesSince(long last) { |
| ArrayList<Cookie> cookies = new ArrayList<Cookie>(); |
| Collection<ArrayList<Cookie>> cookieList = mCookieMap.values(); |
| Iterator<ArrayList<Cookie>> listIter = cookieList.iterator(); |
| while (listIter.hasNext()) { |
| ArrayList<Cookie> list = listIter.next(); |
| Iterator<Cookie> iter = list.iterator(); |
| while (iter.hasNext()) { |
| Cookie cookie = iter.next(); |
| if (cookie.lastUpdateTime > last) { |
| cookies.add(cookie); |
| } |
| } |
| } |
| return cookies; |
| } |
| |
| /** |
| * Package level api, called from CookieSyncManager |
| * |
| * Delete a Cookie in the RAM |
| * @param cookie Cookie to be deleted |
| */ |
| synchronized void deleteACookie(Cookie cookie) { |
| if (cookie.mode == Cookie.MODE_DELETED) { |
| String baseDomain = getBaseDomain(cookie.domain); |
| ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain); |
| if (cookieList != null) { |
| cookieList.remove(cookie); |
| if (cookieList.isEmpty()) { |
| mCookieMap.remove(baseDomain); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Package level api, called from CookieSyncManager |
| * |
| * Called after a cookie is synced to FLASH |
| * @param cookie Cookie to be synced |
| */ |
| synchronized void syncedACookie(Cookie cookie) { |
| cookie.mode = Cookie.MODE_NORMAL; |
| } |
| |
| /** |
| * Package level api, called from CookieSyncManager |
| * |
| * Delete the least recent used domains if the total cookie count in RAM |
| * exceeds the limit |
| * @return A list of cookies which are removed from RAM |
| */ |
| synchronized ArrayList<Cookie> deleteLRUDomain() { |
| int count = 0; |
| int byteCount = 0; |
| int mapSize = mCookieMap.size(); |
| |
| if (mapSize < MAX_RAM_DOMAIN_COUNT) { |
| Collection<ArrayList<Cookie>> cookieLists = mCookieMap.values(); |
| Iterator<ArrayList<Cookie>> listIter = cookieLists.iterator(); |
| while (listIter.hasNext() && count < MAX_RAM_COOKIES_COUNT) { |
| ArrayList<Cookie> list = listIter.next(); |
| if (DebugFlags.COOKIE_MANAGER) { |
| Iterator<Cookie> iter = list.iterator(); |
| while (iter.hasNext() && count < MAX_RAM_COOKIES_COUNT) { |
| Cookie cookie = iter.next(); |
| // 14 is 3 * sizeof(long) + sizeof(boolean) |
| // + sizeof(byte) |
| byteCount += cookie.domain.length() |
| + cookie.path.length() |
| + cookie.name.length() |
| + (cookie.value != null |
| ? cookie.value.length() |
| : 0) |
| + 14; |
| count++; |
| } |
| } else { |
| count += list.size(); |
| } |
| } |
| } |
| |
| ArrayList<Cookie> retlist = new ArrayList<Cookie>(); |
| if (mapSize >= MAX_RAM_DOMAIN_COUNT || count >= MAX_RAM_COOKIES_COUNT) { |
| if (DebugFlags.COOKIE_MANAGER) { |
| Log.v(LOGTAG, count + " cookies used " + byteCount |
| + " bytes with " + mapSize + " domains"); |
| } |
| Object[] domains = mCookieMap.keySet().toArray(); |
| int toGo = mapSize / 10 + 1; |
| while (toGo-- > 0){ |
| String domain = domains[toGo].toString(); |
| if (DebugFlags.COOKIE_MANAGER) { |
| Log.v(LOGTAG, "delete domain: " + domain |
| + " from RAM cache"); |
| } |
| retlist.addAll(mCookieMap.get(domain)); |
| mCookieMap.remove(domain); |
| } |
| } |
| return retlist; |
| } |
| |
| /** |
| * Extract the host and path out of a uri |
| * @param uri The given WebAddress |
| * @return The host and path in the format of String[], String[0] is host |
| * which has at least two periods, String[1] is path which always |
| * ended with "/" |
| */ |
| private String[] getHostAndPath(WebAddress uri) { |
| if (uri.mHost != null && uri.mPath != null) { |
| |
| /* |
| * The domain (i.e. host) portion of the cookie is supposed to be |
| * case-insensitive. We will consistently return the domain in lower |
| * case, which allows us to do the more efficient equals comparison |
| * instead of equalIgnoreCase. |
| * |
| * See: http://www.ieft.org/rfc/rfc2965.txt (Section 3.3.3) |
| */ |
| String[] ret = new String[2]; |
| ret[0] = uri.mHost.toLowerCase(); |
| ret[1] = uri.mPath; |
| |
| int index = ret[0].indexOf(PERIOD); |
| if (index == -1) { |
| if (uri.mScheme.equalsIgnoreCase("file")) { |
| // There is a potential bug where a local file path matches |
| // another file in the local web server directory. Still |
| // "localhost" is the best pseudo domain name. |
| ret[0] = "localhost"; |
| } |
| } else if (index == ret[0].lastIndexOf(PERIOD)) { |
| // cookie host must have at least two periods |
| ret[0] = PERIOD + ret[0]; |
| } |
| |
| if (ret[1].charAt(0) != PATH_DELIM) { |
| return null; |
| } |
| |
| /* |
| * find cookie path, e.g. for http://www.google.com, the path is "/" |
| * for http://www.google.com/lab/, the path is "/lab" |
| * for http://www.google.com/lab/foo, the path is "/lab/foo" |
| * for http://www.google.com/lab?hl=en, the path is "/lab" |
| * for http://www.google.com/lab.asp?hl=en, the path is "/lab.asp" |
| * Note: the path from URI has at least one "/" |
| * See: |
| * http://www.unix.com.ua/rfc/rfc2109.html |
| */ |
| index = ret[1].indexOf(QUESTION_MARK); |
| if (index != -1) { |
| ret[1] = ret[1].substring(0, index); |
| } |
| |
| return ret; |
| } else |
| return null; |
| } |
| |
| /** |
| * Get the base domain for a give host. E.g. mail.google.com will return |
| * google.com |
| * @param host The give host |
| * @return the base domain |
| */ |
| private String getBaseDomain(String host) { |
| int startIndex = 0; |
| int nextIndex = host.indexOf(PERIOD); |
| int lastIndex = host.lastIndexOf(PERIOD); |
| while (nextIndex < lastIndex) { |
| startIndex = nextIndex + 1; |
| nextIndex = host.indexOf(PERIOD, startIndex); |
| } |
| if (startIndex > 0) { |
| return host.substring(startIndex); |
| } else { |
| return host; |
| } |
| } |
| |
| /** |
| * parseCookie() parses the cookieString which is a comma-separated list of |
| * one or more cookies in the format of "NAME=VALUE; expires=DATE; |
| * path=PATH; domain=DOMAIN_NAME; secure httponly" to a list of Cookies. |
| * Here is a sample: IGDND=1, IGPC=ET=UB8TSNwtDmQ:AF=0; expires=Sun, |
| * 17-Jan-2038 19:14:07 GMT; path=/ig; domain=.google.com, =, |
| * PREF=ID=408909b1b304593d:TM=1156459854:LM=1156459854:S=V-vCAU6Sh-gobCfO; |
| * expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com which |
| * contains 3 cookies IGDND, IGPC, PREF and an empty cookie |
| * @param host The default host |
| * @param path The default path |
| * @param cookieString The string coming from "Set-Cookie:" |
| * @return A list of Cookies |
| */ |
| private ArrayList<Cookie> parseCookie(String host, String path, |
| String cookieString) { |
| ArrayList<Cookie> ret = new ArrayList<Cookie>(); |
| |
| int index = 0; |
| int length = cookieString.length(); |
| while (true) { |
| Cookie cookie = null; |
| |
| // done |
| if (index < 0 || index >= length) { |
| break; |
| } |
| |
| // skip white space |
| if (cookieString.charAt(index) == WHITE_SPACE) { |
| index++; |
| continue; |
| } |
| |
| /* |
| * get NAME=VALUE; pair. detecting the end of a pair is tricky, it |
| * can be the end of a string, like "foo=bluh", it can be semicolon |
| * like "foo=bluh;path=/"; or it can be enclosed by \", like |
| * "foo=\"bluh bluh\";path=/" |
| * |
| * Note: in the case of "foo=bluh, bar=bluh;path=/", we interpret |
| * it as one cookie instead of two cookies. |
| */ |
| int semicolonIndex = cookieString.indexOf(SEMICOLON, index); |
| int equalIndex = cookieString.indexOf(EQUAL, index); |
| cookie = new Cookie(host, path); |
| |
| // Cookies like "testcookie; path=/;" are valid and used |
| // (lovefilm.se). |
| // Look for 2 cases: |
| // 1. "foo" or "foo;" where equalIndex is -1 |
| // 2. "foo; path=..." where the first semicolon is before an equal |
| // and a semicolon exists. |
| if ((semicolonIndex != -1 && (semicolonIndex < equalIndex)) || |
| equalIndex == -1) { |
| // Fix up the index in case we have a string like "testcookie" |
| if (semicolonIndex == -1) { |
| semicolonIndex = length; |
| } |
| cookie.name = cookieString.substring(index, semicolonIndex); |
| cookie.value = null; |
| } else { |
| cookie.name = cookieString.substring(index, equalIndex); |
| // Make sure we do not throw an exception if the cookie is like |
| // "foo=" |
| if ((equalIndex < length - 1) && |
| (cookieString.charAt(equalIndex + 1) == QUOTATION)) { |
| index = cookieString.indexOf(QUOTATION, equalIndex + 2); |
| if (index == -1) { |
| // bad format, force return |
| break; |
| } |
| } |
| // Get the semicolon index again in case it was contained within |
| // the quotations. |
| semicolonIndex = cookieString.indexOf(SEMICOLON, index); |
| if (semicolonIndex == -1) { |
| semicolonIndex = length; |
| } |
| if (semicolonIndex - equalIndex > MAX_COOKIE_LENGTH) { |
| // cookie is too big, trim it |
| cookie.value = cookieString.substring(equalIndex + 1, |
| equalIndex + 1 + MAX_COOKIE_LENGTH); |
| } else if (equalIndex + 1 == semicolonIndex |
| || semicolonIndex < equalIndex) { |
| // this is an unusual case like "foo=;" or "foo=" |
| cookie.value = ""; |
| } else { |
| cookie.value = cookieString.substring(equalIndex + 1, |
| semicolonIndex); |
| } |
| } |
| // get attributes |
| index = semicolonIndex; |
| while (true) { |
| // done |
| if (index < 0 || index >= length) { |
| break; |
| } |
| |
| // skip white space and semicolon |
| if (cookieString.charAt(index) == WHITE_SPACE |
| || cookieString.charAt(index) == SEMICOLON) { |
| index++; |
| continue; |
| } |
| |
| // comma means next cookie |
| if (cookieString.charAt(index) == COMMA) { |
| index++; |
| break; |
| } |
| |
| // "secure" is a known attribute doesn't use "="; |
| // while sites like live.com uses "secure=" |
| if (length - index >= SECURE_LENGTH |
| && cookieString.substring(index, index + SECURE_LENGTH). |
| equalsIgnoreCase(SECURE)) { |
| index += SECURE_LENGTH; |
| cookie.secure = true; |
| if (index == length) break; |
| if (cookieString.charAt(index) == EQUAL) index++; |
| continue; |
| } |
| |
| // "httponly" is a known attribute doesn't use "="; |
| // while sites like live.com uses "httponly=" |
| if (length - index >= HTTP_ONLY_LENGTH |
| && cookieString.substring(index, |
| index + HTTP_ONLY_LENGTH). |
| equalsIgnoreCase(HTTP_ONLY)) { |
| index += HTTP_ONLY_LENGTH; |
| if (index == length) break; |
| if (cookieString.charAt(index) == EQUAL) index++; |
| // FIXME: currently only parse the attribute |
| continue; |
| } |
| equalIndex = cookieString.indexOf(EQUAL, index); |
| if (equalIndex > 0) { |
| String name = cookieString.substring(index, equalIndex) |
| .toLowerCase(); |
| if (name.equals(EXPIRES)) { |
| int comaIndex = cookieString.indexOf(COMMA, equalIndex); |
| |
| // skip ',' in (Wdy, DD-Mon-YYYY HH:MM:SS GMT) or |
| // (Weekday, DD-Mon-YY HH:MM:SS GMT) if it applies. |
| // "Wednesday" is the longest Weekday which has length 9 |
| if ((comaIndex != -1) && |
| (comaIndex - equalIndex <= 10)) { |
| index = comaIndex + 1; |
| } |
| } |
| semicolonIndex = cookieString.indexOf(SEMICOLON, index); |
| int commaIndex = cookieString.indexOf(COMMA, index); |
| if (semicolonIndex == -1 && commaIndex == -1) { |
| index = length; |
| } else if (semicolonIndex == -1) { |
| index = commaIndex; |
| } else if (commaIndex == -1) { |
| index = semicolonIndex; |
| } else { |
| index = Math.min(semicolonIndex, commaIndex); |
| } |
| String value = |
| cookieString.substring(equalIndex + 1, index); |
| |
| // Strip quotes if they exist |
| if (value.length() > 2 && value.charAt(0) == QUOTATION) { |
| int endQuote = value.indexOf(QUOTATION, 1); |
| if (endQuote > 0) { |
| value = value.substring(1, endQuote); |
| } |
| } |
| if (name.equals(EXPIRES)) { |
| try { |
| cookie.expires = AndroidHttpClient.parseDate(value); |
| } catch (IllegalArgumentException ex) { |
| Log.e(LOGTAG, |
| "illegal format for expires: " + value); |
| } |
| } else if (name.equals(MAX_AGE)) { |
| try { |
| cookie.expires = System.currentTimeMillis() + 1000 |
| * Long.parseLong(value); |
| } catch (NumberFormatException ex) { |
| Log.e(LOGTAG, |
| "illegal format for max-age: " + value); |
| } |
| } else if (name.equals(PATH)) { |
| // only allow non-empty path value |
| if (value.length() > 0) { |
| cookie.path = value; |
| } |
| } else if (name.equals(DOMAIN)) { |
| int lastPeriod = value.lastIndexOf(PERIOD); |
| if (lastPeriod == 0) { |
| // disallow cookies set for TLDs like [.com] |
| cookie.domain = null; |
| continue; |
| } |
| try { |
| Integer.parseInt(value.substring(lastPeriod + 1)); |
| // no wildcard for ip address match |
| if (!value.equals(host)) { |
| // no cross-site cookie |
| cookie.domain = null; |
| } |
| continue; |
| } catch (NumberFormatException ex) { |
| // ignore the exception, value is a host name |
| } |
| value = value.toLowerCase(); |
| if (value.charAt(0) != PERIOD) { |
| // pre-pended dot to make it as a domain cookie |
| value = PERIOD + value; |
| lastPeriod++; |
| } |
| if (host.endsWith(value.substring(1))) { |
| int len = value.length(); |
| int hostLen = host.length(); |
| if (hostLen > (len - 1) |
| && host.charAt(hostLen - len) != PERIOD) { |
| // make sure the bar.com doesn't match .ar.com |
| cookie.domain = null; |
| continue; |
| } |
| // disallow cookies set on ccTLDs like [.co.uk] |
| if ((len == lastPeriod + 3) |
| && (len >= 6 && len <= 8)) { |
| String s = value.substring(1, lastPeriod); |
| if (Arrays.binarySearch(BAD_COUNTRY_2LDS, s) >= 0) { |
| cookie.domain = null; |
| continue; |
| } |
| } |
| cookie.domain = value; |
| } else { |
| // no cross-site or more specific sub-domain cookie |
| cookie.domain = null; |
| } |
| } |
| } else { |
| // bad format, force return |
| index = length; |
| } |
| } |
| if (cookie != null && cookie.domain != null) { |
| ret.add(cookie); |
| } |
| } |
| return ret; |
| } |
| } |