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