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