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