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 17 package android.webkit; 18 19 import android.net.ParseException; 20 import android.net.WebAddress; 21 import android.net.http.AndroidHttpClient; 22 import android.util.Log; 23 24 25 import java.util.ArrayList; 26 import java.util.Arrays; 27 import java.util.Collection; 28 import java.util.Comparator; 29 import java.util.Iterator; 30 import java.util.LinkedHashMap; 31 import java.util.Map; 32 import java.util.SortedSet; 33 import java.util.TreeSet; 34 35 /** 36 * CookieManager manages cookies according to RFC2109 spec. 37 */ 38 public 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 697 /* 698 * The domain (i.e. host) portion of the cookie is supposed to be 699 * case-insensitive. We will consistently return the domain in lower 700 * case, which allows us to do the more efficient equals comparison 701 * instead of equalIgnoreCase. 702 * 703 * See: http://www.ieft.org/rfc/rfc2965.txt (Section 3.3.3) 704 */ 705 String[] ret = new String[2]; 706 ret[0] = uri.mHost.toLowerCase(); 707 ret[1] = uri.mPath; 708 709 int index = ret[0].indexOf(PERIOD); 710 if (index == -1) { 711 if (uri.mScheme.equalsIgnoreCase("file")) { 712 // There is a potential bug where a local file path matches 713 // another file in the local web server directory. Still 714 // "localhost" is the best pseudo domain name. 715 ret[0] = "localhost"; 716 } 717 } else if (index == ret[0].lastIndexOf(PERIOD)) { 718 // cookie host must have at least two periods 719 ret[0] = PERIOD + ret[0]; 720 } 721 722 if (ret[1].charAt(0) != PATH_DELIM) { 723 return null; 724 } 725 726 /* 727 * find cookie path, e.g. for http://www.google.com, the path is "/" 728 * for http://www.google.com/lab/, the path is "/lab" 729 * for http://www.google.com/lab/foo, the path is "/lab/foo" 730 * for http://www.google.com/lab?hl=en, the path is "/lab" 731 * for http://www.google.com/lab.asp?hl=en, the path is "/lab.asp" 732 * Note: the path from URI has at least one "/" 733 * See: 734 * http://www.unix.com.ua/rfc/rfc2109.html 735 */ 736 index = ret[1].indexOf(QUESTION_MARK); 737 if (index != -1) { 738 ret[1] = ret[1].substring(0, index); 739 } 740 741 return ret; 742 } else 743 return null; 744 } 745 746 /** 747 * Get the base domain for a give host. E.g. mail.google.com will return 748 * google.com 749 * @param host The give host 750 * @return the base domain 751 */ 752 private String getBaseDomain(String host) { 753 int startIndex = 0; 754 int nextIndex = host.indexOf(PERIOD); 755 int lastIndex = host.lastIndexOf(PERIOD); 756 while (nextIndex < lastIndex) { 757 startIndex = nextIndex + 1; 758 nextIndex = host.indexOf(PERIOD, startIndex); 759 } 760 if (startIndex > 0) { 761 return host.substring(startIndex); 762 } else { 763 return host; 764 } 765 } 766 767 /** 768 * parseCookie() parses the cookieString which is a comma-separated list of 769 * one or more cookies in the format of "NAME=VALUE; expires=DATE; 770 * path=PATH; domain=DOMAIN_NAME; secure httponly" to a list of Cookies. 771 * Here is a sample: IGDND=1, IGPC=ET=UB8TSNwtDmQ:AF=0; expires=Sun, 772 * 17-Jan-2038 19:14:07 GMT; path=/ig; domain=.google.com, =, 773 * PREF=ID=408909b1b304593d:TM=1156459854:LM=1156459854:S=V-vCAU6Sh-gobCfO; 774 * expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com which 775 * contains 3 cookies IGDND, IGPC, PREF and an empty cookie 776 * @param host The default host 777 * @param path The default path 778 * @param cookieString The string coming from "Set-Cookie:" 779 * @return A list of Cookies 780 */ 781 private ArrayList<Cookie> parseCookie(String host, String path, 782 String cookieString) { 783 ArrayList<Cookie> ret = new ArrayList<Cookie>(); 784 785 int index = 0; 786 int length = cookieString.length(); 787 while (true) { 788 Cookie cookie = null; 789 790 // done 791 if (index < 0 || index >= length) { 792 break; 793 } 794 795 // skip white space 796 if (cookieString.charAt(index) == WHITE_SPACE) { 797 index++; 798 continue; 799 } 800 801 /* 802 * get NAME=VALUE; pair. detecting the end of a pair is tricky, it 803 * can be the end of a string, like "foo=bluh", it can be semicolon 804 * like "foo=bluh;path=/"; or it can be enclosed by \", like 805 * "foo=\"bluh bluh\";path=/" 806 * 807 * Note: in the case of "foo=bluh, bar=bluh;path=/", we interpret 808 * it as one cookie instead of two cookies. 809 */ 810 int semicolonIndex = cookieString.indexOf(SEMICOLON, index); 811 int equalIndex = cookieString.indexOf(EQUAL, index); 812 cookie = new Cookie(host, path); 813 814 // Cookies like "testcookie; path=/;" are valid and used 815 // (lovefilm.se). 816 // Look for 2 cases: 817 // 1. "foo" or "foo;" where equalIndex is -1 818 // 2. "foo; path=..." where the first semicolon is before an equal 819 // and a semicolon exists. 820 if ((semicolonIndex != -1 && (semicolonIndex < equalIndex)) || 821 equalIndex == -1) { 822 // Fix up the index in case we have a string like "testcookie" 823 if (semicolonIndex == -1) { 824 semicolonIndex = length; 825 } 826 cookie.name = cookieString.substring(index, semicolonIndex); 827 cookie.value = null; 828 } else { 829 cookie.name = cookieString.substring(index, equalIndex); 830 // Make sure we do not throw an exception if the cookie is like 831 // "foo=" 832 if ((equalIndex < length - 1) && 833 (cookieString.charAt(equalIndex + 1) == QUOTATION)) { 834 index = cookieString.indexOf(QUOTATION, equalIndex + 2); 835 if (index == -1) { 836 // bad format, force return 837 break; 838 } 839 } 840 // Get the semicolon index again in case it was contained within 841 // the quotations. 842 semicolonIndex = cookieString.indexOf(SEMICOLON, index); 843 if (semicolonIndex == -1) { 844 semicolonIndex = length; 845 } 846 if (semicolonIndex - equalIndex > MAX_COOKIE_LENGTH) { 847 // cookie is too big, trim it 848 cookie.value = cookieString.substring(equalIndex + 1, 849 equalIndex + 1 + MAX_COOKIE_LENGTH); 850 } else if (equalIndex + 1 == semicolonIndex 851 || semicolonIndex < equalIndex) { 852 // this is an unusual case like "foo=;" or "foo=" 853 cookie.value = ""; 854 } else { 855 cookie.value = cookieString.substring(equalIndex + 1, 856 semicolonIndex); 857 } 858 } 859 // get attributes 860 index = semicolonIndex; 861 while (true) { 862 // done 863 if (index < 0 || index >= length) { 864 break; 865 } 866 867 // skip white space and semicolon 868 if (cookieString.charAt(index) == WHITE_SPACE 869 || cookieString.charAt(index) == SEMICOLON) { 870 index++; 871 continue; 872 } 873 874 // comma means next cookie 875 if (cookieString.charAt(index) == COMMA) { 876 index++; 877 break; 878 } 879 880 // "secure" is a known attribute doesn't use "="; 881 // while sites like live.com uses "secure=" 882 if (length - index >= SECURE_LENGTH 883 && cookieString.substring(index, index + SECURE_LENGTH). 884 equalsIgnoreCase(SECURE)) { 885 index += SECURE_LENGTH; 886 cookie.secure = true; 887 if (index == length) break; 888 if (cookieString.charAt(index) == EQUAL) index++; 889 continue; 890 } 891 892 // "httponly" is a known attribute doesn't use "="; 893 // while sites like live.com uses "httponly=" 894 if (length - index >= HTTP_ONLY_LENGTH 895 && cookieString.substring(index, 896 index + HTTP_ONLY_LENGTH). 897 equalsIgnoreCase(HTTP_ONLY)) { 898 index += HTTP_ONLY_LENGTH; 899 if (index == length) break; 900 if (cookieString.charAt(index) == EQUAL) index++; 901 // FIXME: currently only parse the attribute 902 continue; 903 } 904 equalIndex = cookieString.indexOf(EQUAL, index); 905 if (equalIndex > 0) { 906 String name = cookieString.substring(index, equalIndex) 907 .toLowerCase(); 908 if (name.equals(EXPIRES)) { 909 int comaIndex = cookieString.indexOf(COMMA, equalIndex); 910 911 // skip ',' in (Wdy, DD-Mon-YYYY HH:MM:SS GMT) or 912 // (Weekday, DD-Mon-YY HH:MM:SS GMT) if it applies. 913 // "Wednesday" is the longest Weekday which has length 9 914 if ((comaIndex != -1) && 915 (comaIndex - equalIndex <= 10)) { 916 index = comaIndex + 1; 917 } 918 } 919 semicolonIndex = cookieString.indexOf(SEMICOLON, index); 920 int commaIndex = cookieString.indexOf(COMMA, index); 921 if (semicolonIndex == -1 && commaIndex == -1) { 922 index = length; 923 } else if (semicolonIndex == -1) { 924 index = commaIndex; 925 } else if (commaIndex == -1) { 926 index = semicolonIndex; 927 } else { 928 index = Math.min(semicolonIndex, commaIndex); 929 } 930 String value = 931 cookieString.substring(equalIndex + 1, index); 932 933 // Strip quotes if they exist 934 if (value.length() > 2 && value.charAt(0) == QUOTATION) { 935 int endQuote = value.indexOf(QUOTATION, 1); 936 if (endQuote > 0) { 937 value = value.substring(1, endQuote); 938 } 939 } 940 if (name.equals(EXPIRES)) { 941 try { 942 cookie.expires = AndroidHttpClient.parseDate(value); 943 } catch (IllegalArgumentException ex) { 944 Log.e(LOGTAG, 945 "illegal format for expires: " + value); 946 } 947 } else if (name.equals(MAX_AGE)) { 948 try { 949 cookie.expires = System.currentTimeMillis() + 1000 950 * Long.parseLong(value); 951 } catch (NumberFormatException ex) { 952 Log.e(LOGTAG, 953 "illegal format for max-age: " + value); 954 } 955 } else if (name.equals(PATH)) { 956 // only allow non-empty path value 957 if (value.length() > 0) { 958 cookie.path = value; 959 } 960 } else if (name.equals(DOMAIN)) { 961 int lastPeriod = value.lastIndexOf(PERIOD); 962 if (lastPeriod == 0) { 963 // disallow cookies set for TLDs like [.com] 964 cookie.domain = null; 965 continue; 966 } 967 try { 968 Integer.parseInt(value.substring(lastPeriod + 1)); 969 // no wildcard for ip address match 970 if (!value.equals(host)) { 971 // no cross-site cookie 972 cookie.domain = null; 973 } 974 continue; 975 } catch (NumberFormatException ex) { 976 // ignore the exception, value is a host name 977 } 978 value = value.toLowerCase(); 979 if (value.charAt(0) != PERIOD) { 980 // pre-pended dot to make it as a domain cookie 981 value = PERIOD + value; 982 lastPeriod++; 983 } 984 if (host.endsWith(value.substring(1))) { 985 int len = value.length(); 986 int hostLen = host.length(); 987 if (hostLen > (len - 1) 988 && host.charAt(hostLen - len) != PERIOD) { 989 // make sure the bar.com doesn't match .ar.com 990 cookie.domain = null; 991 continue; 992 } 993 // disallow cookies set on ccTLDs like [.co.uk] 994 if ((len == lastPeriod + 3) 995 && (len >= 6 && len <= 8)) { 996 String s = value.substring(1, lastPeriod); 997 if (Arrays.binarySearch(BAD_COUNTRY_2LDS, s) >= 0) { 998 cookie.domain = null; 999 continue; 1000 } 1001 } 1002 cookie.domain = value; 1003 } else { 1004 // no cross-site or more specific sub-domain cookie 1005 cookie.domain = null; 1006 } 1007 } 1008 } else { 1009 // bad format, force return 1010 index = length; 1011 } 1012 } 1013 if (cookie != null && cookie.domain != null) { 1014 ret.add(cookie); 1015 } 1016 } 1017 return ret; 1018 } 1019 } 1020