1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * Copyright (c) 2005, 2012, Oracle and/or its affiliates. All rights reserved. 4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 * 6 * This code is free software; you can redistribute it and/or modify it 7 * under the terms of the GNU General Public License version 2 only, as 8 * published by the Free Software Foundation. Oracle designates this 9 * particular file as subject to the "Classpath" exception as provided 10 * by Oracle in the LICENSE file that accompanied this code. 11 * 12 * This code is distributed in the hope that it will be useful, but WITHOUT 13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 15 * version 2 for more details (a copy is included in the LICENSE file that 16 * accompanied this code). 17 * 18 * You should have received a copy of the GNU General Public License version 19 * 2 along with this work; if not, write to the Free Software Foundation, 20 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 21 * 22 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 23 * or visit www.oracle.com if you need additional information or have any 24 * questions. 25 */ 26 27 package java.net; 28 29 import dalvik.system.VMRuntime; 30 31 import java.util.List; 32 import java.util.Map; 33 import java.util.ArrayList; 34 import java.util.HashMap; 35 import java.util.Collections; 36 import java.util.Iterator; 37 import java.util.concurrent.locks.ReentrantLock; 38 39 // Android-changed: App compat changes and bug fixes. 40 // b/26456024 Add targetSdkVersion based compatibility for domain matching 41 // b/33034917 Support clearing cookies by adding it with "max-age=0" 42 // b/25897688 InMemoryCookieStore ignores scheme (http/https) port and path of the cookie 43 // Remove cookieJar and domainIndex. Use urlIndex as single Cookie storage 44 // Fix InMemoryCookieStore#remove to verify cookie URI before removal 45 // Fix InMemoryCookieStore#removeAll to return false if it's empty. 46 /** 47 * A simple in-memory java.net.CookieStore implementation 48 * 49 * @author Edward Wang 50 * @since 1.6 51 * @hide Visible for testing only. 52 */ 53 public class InMemoryCookieStore implements CookieStore { 54 // the in-memory representation of cookies 55 // BEGIN Android-removed: Remove cookieJar and domainIndex. 56 /* 57 private List<HttpCookie> cookieJar = null; 58 59 // the cookies are indexed by its domain and associated uri (if present) 60 // CAUTION: when a cookie removed from main data structure (i.e. cookieJar), 61 // it won't be cleared in domainIndex & uriIndex. Double-check the 62 // presence of cookie when retrieve one form index store. 63 private Map<String, List<HttpCookie>> domainIndex = null; 64 */ 65 // END Android-removed: Remove cookieJar and domainIndex. 66 private Map<URI, List<HttpCookie>> uriIndex = null; 67 68 // use ReentrantLock instead of syncronized for scalability 69 private ReentrantLock lock = null; 70 71 // BEGIN Android-changed: Add targetSdkVersion and remove cookieJar and domainIndex. 72 private final boolean applyMCompatibility; 73 74 /** 75 * The default ctor 76 */ InMemoryCookieStore()77 public InMemoryCookieStore() { 78 this(VMRuntime.getRuntime().getTargetSdkVersion()); 79 } 80 InMemoryCookieStore(int targetSdkVersion)81 public InMemoryCookieStore(int targetSdkVersion) { 82 uriIndex = new HashMap<>(); 83 lock = new ReentrantLock(false); 84 applyMCompatibility = (targetSdkVersion <= 23); 85 } 86 // END Android-changed: Add targetSdkVersion and remove cookieJar and domainIndex. 87 88 /** 89 * Add one cookie into cookie store. 90 */ add(URI uri, HttpCookie cookie)91 public void add(URI uri, HttpCookie cookie) { 92 // pre-condition : argument can't be null 93 if (cookie == null) { 94 throw new NullPointerException("cookie is null"); 95 } 96 97 lock.lock(); 98 try { 99 // Android-changed: Android supports clearing cookies. http://b/33034917 100 // They are cleared by adding the cookie with max-age: 0. 101 //if (cookie.getMaxAge() != 0) { 102 addIndex(uriIndex, getEffectiveURI(uri), cookie); 103 //} 104 } finally { 105 lock.unlock(); 106 } 107 } 108 109 110 /** 111 * Get all cookies, which: 112 * 1) given uri domain-matches with, or, associated with 113 * given uri when added to the cookie store. 114 * 3) not expired. 115 * See RFC 2965 sec. 3.3.4 for more detail. 116 */ get(URI uri)117 public List<HttpCookie> get(URI uri) { 118 // argument can't be null 119 if (uri == null) { 120 throw new NullPointerException("uri is null"); 121 } 122 123 List<HttpCookie> cookies = new ArrayList<HttpCookie>(); 124 // BEGIN Android-changed: InMemoryCookieStore ignores scheme (http/https). b/25897688 125 lock.lock(); 126 try { 127 // check domainIndex first 128 getInternal1(cookies, uriIndex, uri.getHost()); 129 // check uriIndex then 130 getInternal2(cookies, uriIndex, getEffectiveURI(uri)); 131 } finally { 132 lock.unlock(); 133 } 134 // END Android-changed: InMemoryCookieStore ignores scheme (http/https). b/25897688 135 return cookies; 136 } 137 138 /** 139 * Get all cookies in cookie store, except those have expired 140 */ getCookies()141 public List<HttpCookie> getCookies() { 142 // BEGIN Android-changed: Remove cookieJar and domainIndex. 143 List<HttpCookie> rt = new ArrayList<HttpCookie>(); 144 145 lock.lock(); 146 try { 147 for (List<HttpCookie> list : uriIndex.values()) { 148 Iterator<HttpCookie> it = list.iterator(); 149 while (it.hasNext()) { 150 HttpCookie cookie = it.next(); 151 if (cookie.hasExpired()) { 152 it.remove(); 153 } else if (!rt.contains(cookie)) { 154 rt.add(cookie); 155 } 156 } 157 } 158 } finally { 159 rt = Collections.unmodifiableList(rt); 160 lock.unlock(); 161 } 162 // END Android-changed: Remove cookieJar and domainIndex. 163 164 return rt; 165 } 166 167 /** 168 * Get all URIs, which are associated with at least one cookie 169 * of this cookie store. 170 */ getURIs()171 public List<URI> getURIs() { 172 // BEGIN Android-changed: App compat. Return URI with no cookies. http://b/65538736 173 /* 174 List<URI> uris = new ArrayList<>(); 175 176 lock.lock(); 177 try { 178 Iterator<URI> it = uriIndex.keySet().iterator(); 179 while (it.hasNext()) { 180 URI uri = it.next(); 181 List<HttpCookie> cookies = uriIndex.get(uri); 182 if (cookies == null || cookies.size() == 0) { 183 // no cookies list or an empty list associated with 184 // this uri entry, delete it 185 it.remove(); 186 } 187 } 188 } finally { 189 uris.addAll(uriIndex.keySet()); 190 lock.unlock(); 191 } 192 193 return uris; 194 */ 195 lock.lock(); 196 try { 197 List<URI> result = new ArrayList<URI>(uriIndex.keySet()); 198 result.remove(null); 199 return Collections.unmodifiableList(result); 200 } finally { 201 lock.unlock(); 202 } 203 // END Android-changed: App compat. Return URI with no cookies. http://b/65538736 204 } 205 206 207 /** 208 * Remove a cookie from store 209 */ remove(URI uri, HttpCookie ck)210 public boolean remove(URI uri, HttpCookie ck) { 211 // argument can't be null 212 if (ck == null) { 213 throw new NullPointerException("cookie is null"); 214 } 215 216 // BEGIN Android-changed: Fix uri not being removed from uriIndex. 217 lock.lock(); 218 try { 219 uri = getEffectiveURI(uri); 220 if (uriIndex.get(uri) == null) { 221 return false; 222 } else { 223 List<HttpCookie> cookies = uriIndex.get(uri); 224 if (cookies != null) { 225 return cookies.remove(ck); 226 } else { 227 return false; 228 } 229 } 230 } finally { 231 lock.unlock(); 232 } 233 // END Android-changed: Fix uri not being removed from uriIndex. 234 } 235 236 237 /** 238 * Remove all cookies in this cookie store. 239 */ removeAll()240 public boolean removeAll() { 241 lock.lock(); 242 // BEGIN Android-changed: Let removeAll() return false when there are no cookies. 243 boolean result = false; 244 245 try { 246 result = !uriIndex.isEmpty(); 247 uriIndex.clear(); 248 } finally { 249 lock.unlock(); 250 } 251 252 return result; 253 // END Android-changed: Let removeAll() return false when there are no cookies. 254 } 255 256 257 /* ---------------- Private operations -------------- */ 258 259 260 /* 261 * This is almost the same as HttpCookie.domainMatches except for 262 * one difference: It won't reject cookies when the 'H' part of the 263 * domain contains a dot ('.'). 264 * I.E.: RFC 2965 section 3.3.2 says that if host is x.y.domain.com 265 * and the cookie domain is .domain.com, then it should be rejected. 266 * However that's not how the real world works. Browsers don't reject and 267 * some sites, like yahoo.com do actually expect these cookies to be 268 * passed along. 269 * And should be used for 'old' style cookies (aka Netscape type of cookies) 270 */ netscapeDomainMatches(String domain, String host)271 private boolean netscapeDomainMatches(String domain, String host) 272 { 273 if (domain == null || host == null) { 274 return false; 275 } 276 277 // if there's no embedded dot in domain and domain is not .local 278 boolean isLocalDomain = ".local".equalsIgnoreCase(domain); 279 int embeddedDotInDomain = domain.indexOf('.'); 280 if (embeddedDotInDomain == 0) { 281 embeddedDotInDomain = domain.indexOf('.', 1); 282 } 283 if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) { 284 return false; 285 } 286 287 // if the host name contains no dot and the domain name is .local 288 int firstDotInHost = host.indexOf('.'); 289 if (firstDotInHost == -1 && isLocalDomain) { 290 return true; 291 } 292 293 int domainLength = domain.length(); 294 int lengthDiff = host.length() - domainLength; 295 if (lengthDiff == 0) { 296 // if the host name and the domain name are just string-compare euqal 297 return host.equalsIgnoreCase(domain); 298 } else if (lengthDiff > 0) { 299 // need to check H & D component 300 String D = host.substring(lengthDiff); 301 302 // Android-changed: b/26456024 targetSdkVersion based compatibility for domain matching. 303 // Android M and earlier: Cookies with domain "foo.com" would not match "bar.foo.com". 304 // The RFC dictates that the user agent must treat those domains as if they had a 305 // leading period and must therefore match "bar.foo.com". 306 if (applyMCompatibility && !domain.startsWith(".")) { 307 return false; 308 } 309 310 return (D.equalsIgnoreCase(domain)); 311 } else if (lengthDiff == -1) { 312 // if domain is actually .host 313 return (domain.charAt(0) == '.' && 314 host.equalsIgnoreCase(domain.substring(1))); 315 } 316 317 return false; 318 } 319 getInternal1(List<HttpCookie> cookies, Map<URI, List<HttpCookie>> cookieIndex, String host)320 private void getInternal1(List<HttpCookie> cookies, Map<URI, List<HttpCookie>> cookieIndex, 321 String host) { 322 // BEGIN Android-changed: InMemoryCookieStore ignores scheme (http/https). b/25897688 323 // Use a separate list to handle cookies that need to be removed so 324 // that there is no conflict with iterators. 325 ArrayList<HttpCookie> toRemove = new ArrayList<HttpCookie>(); 326 for (Map.Entry<URI, List<HttpCookie>> entry : cookieIndex.entrySet()) { 327 List<HttpCookie> lst = entry.getValue(); 328 for (HttpCookie c : lst) { 329 String domain = c.getDomain(); 330 if ((c.getVersion() == 0 && netscapeDomainMatches(domain, host)) || 331 (c.getVersion() == 1 && HttpCookie.domainMatches(domain, host))) { 332 333 // the cookie still in main cookie store 334 if (!c.hasExpired()) { 335 // don't add twice 336 if (!cookies.contains(c)) { 337 cookies.add(c); 338 } 339 } else { 340 toRemove.add(c); 341 } 342 } 343 } 344 // Clear up the cookies that need to be removed 345 for (HttpCookie c : toRemove) { 346 lst.remove(c); 347 348 } 349 toRemove.clear(); 350 } 351 // END Android-changed: InMemoryCookieStore ignores scheme (http/https). b/25897688 352 } 353 354 // @param cookies [OUT] contains the found cookies 355 // @param cookieIndex the index 356 // @param comparator the prediction to decide whether or not 357 // a cookie in index should be returned 358 private <T extends Comparable<T>> getInternal2(List<HttpCookie> cookies, Map<T, List<HttpCookie>> cookieIndex, T comparator)359 void getInternal2(List<HttpCookie> cookies, Map<T, List<HttpCookie>> cookieIndex, 360 T comparator) 361 { 362 // BEGIN Android-changed: InMemoryCookieStore ignores scheme (http/https). b/25897688 363 // Removed cookieJar 364 for (T index : cookieIndex.keySet()) { 365 if ((index == comparator) || (index != null && comparator.compareTo(index) == 0)) { 366 List<HttpCookie> indexedCookies = cookieIndex.get(index); 367 // check the list of cookies associated with this domain 368 if (indexedCookies != null) { 369 Iterator<HttpCookie> it = indexedCookies.iterator(); 370 while (it.hasNext()) { 371 HttpCookie ck = it.next(); 372 // the cookie still in main cookie store 373 if (!ck.hasExpired()) { 374 // don't add twice 375 if (!cookies.contains(ck)) 376 cookies.add(ck); 377 } else { 378 it.remove(); 379 } 380 } 381 } // end of indexedCookies != null 382 } // end of comparator.compareTo(index) == 0 383 } // end of cookieIndex iteration 384 // END Android-changed: InMemoryCookieStore ignores scheme (http/https). b/25897688 385 } 386 387 // add 'cookie' indexed by 'index' into 'indexStore' addIndex(Map<T, List<HttpCookie>> indexStore, T index, HttpCookie cookie)388 private <T> void addIndex(Map<T, List<HttpCookie>> indexStore, 389 T index, 390 HttpCookie cookie) 391 { 392 // Android-changed: "index" can be null. 393 // We only use the URI based index on Android and we want to support null URIs. The 394 // underlying store is a HashMap which will support null keys anyway. 395 // if (index != null) { 396 List<HttpCookie> cookies = indexStore.get(index); 397 if (cookies != null) { 398 // there may already have the same cookie, so remove it first 399 cookies.remove(cookie); 400 401 cookies.add(cookie); 402 } else { 403 cookies = new ArrayList<>(); 404 cookies.add(cookie); 405 indexStore.put(index, cookies); 406 } 407 } 408 409 410 // 411 // for cookie purpose, the effective uri should only be http://host 412 // the path will be taken into account when path-match algorithm applied 413 // getEffectiveURI(URI uri)414 private URI getEffectiveURI(URI uri) { 415 URI effectiveURI = null; 416 // Android-added: Fix NullPointerException. 417 if (uri == null) { 418 return null; 419 } 420 try { 421 effectiveURI = new URI("http", 422 uri.getHost(), 423 null, // path component 424 null, // query component 425 null // fragment component 426 ); 427 } catch (URISyntaxException ignored) { 428 effectiveURI = uri; 429 } 430 431 return effectiveURI; 432 } 433 } 434