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