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