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