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