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