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