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