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