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