diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
commit | 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch) | |
tree | d88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/webkit/CookieManager.java | |
parent | d83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff) | |
download | frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.zip frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.gz frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'core/java/android/webkit/CookieManager.java')
-rw-r--r-- | core/java/android/webkit/CookieManager.java | 934 |
1 files changed, 934 insertions, 0 deletions
diff --git a/core/java/android/webkit/CookieManager.java b/core/java/android/webkit/CookieManager.java new file mode 100644 index 0000000..07c1a5d --- /dev/null +++ b/core/java/android/webkit/CookieManager.java @@ -0,0 +1,934 @@ +/* + * 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.util.Config; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 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) { + return domain.equals(in.domain) && path.equals(in.path) && + name.equals(in.name); + } + + 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 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 (Config.LOGV) { + 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(); + StringBuilder ret = new StringBuilder(256); + + 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; + + if (ret.length() > 0) { + ret.append(SEMICOLON); + // according to RC2109, SEMICOLON is office separator, + // but when log in yahoo.com, it needs WHITE_SPACE too. + ret.append(WHITE_SPACE); + } + + ret.append(cookie.name); + ret.append(EQUAL); + ret.append(cookie.value); + } + } + if (ret.length() > 0) { + if (Config.LOGV) { + Log.v(LOGTAG, "getCookie: uri: " + uri + " value: " + ret); + } + return ret.toString(); + } else { + if (Config.LOGV) { + 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 (Config.DEBUG) { + 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.length() + 14; + count++; + } + } else { + count += list.size(); + } + } + } + + ArrayList<Cookie> retlist = new ArrayList<Cookie>(); + if (mapSize >= MAX_RAM_DOMAIN_COUNT || count >= MAX_RAM_COOKIES_COUNT) { + if (Config.DEBUG) { + 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 (Config.LOGV) { + 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) { + String[] ret = new String[2]; + ret[0] = uri.mHost; + 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 (!ret[0].equals("localhost")) { + return null; + } + } 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 equalIndex = cookieString.indexOf(EQUAL, index); + if (equalIndex == -1) { + // bad format, force return + break; + } + cookie = new Cookie(host, path); + cookie.name = cookieString.substring(index, equalIndex); + if (cookieString.charAt(equalIndex + 1) == QUOTATION) { + index = cookieString.indexOf(QUOTATION, equalIndex + 2); + if (index == -1) { + // bad format, force return + break; + } + } + int 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 + MAX_COOKIE_LENGTH); + } else if (equalIndex + 1 == semicolonIndex + || semicolonIndex < equalIndex) { + // these are unusual case like foo=; and foo; path=/ + 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 (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 (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 = HttpDateTime.parse(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; + } +} |