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 */ 77 public InMemoryCookieStore() { 78 this(VMRuntime.getRuntime().getTargetSdkVersion()); 79 } 80 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 */ 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: http://b/33034917, android supports clearing cookies 100 // 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 */ 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: b/25897688 InMemoryCookieStore ignores scheme (http/https) 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: b/25897688 InMemoryCookieStore ignores scheme (http/https) 135 return cookies; 136 } 137 138 /** 139 * Get all cookies in cookie store, except those have expired 140 */ 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 */ 171 public List<URI> getURIs() { 172 List<URI> uris = new ArrayList<URI>(); 173 174 lock.lock(); 175 try { 176 List<URI> result = new ArrayList<URI>(uriIndex.keySet()); 177 result.remove(null); 178 return Collections.unmodifiableList(result); 179 } finally { 180 uris.addAll(uriIndex.keySet()); 181 lock.unlock(); 182 } 183 } 184 185 186 /** 187 * Remove a cookie from store 188 */ 189 public boolean remove(URI uri, HttpCookie ck) { 190 // argument can't be null 191 if (ck == null) { 192 throw new NullPointerException("cookie is null"); 193 } 194 195 // BEGIN Android-changed: Fix uri not being removed from uriIndex 196 lock.lock(); 197 try { 198 uri = getEffectiveURI(uri); 199 if (uriIndex.get(uri) == null) { 200 return false; 201 } else { 202 List<HttpCookie> cookies = uriIndex.get(uri); 203 if (cookies != null) { 204 return cookies.remove(ck); 205 } else { 206 return false; 207 } 208 } 209 } finally { 210 lock.unlock(); 211 } 212 // END Android-changed: Fix uri not being removed from uriIndex 213 } 214 215 216 /** 217 * Remove all cookies in this cookie store. 218 */ 219 public boolean removeAll() { 220 lock.lock(); 221 // BEGIN Android-changed: Let removeAll() return false when there are no cookies. 222 boolean result = false; 223 224 try { 225 result = !uriIndex.isEmpty(); 226 uriIndex.clear(); 227 } finally { 228 lock.unlock(); 229 } 230 231 return result; 232 // END Android-changed: Let removeAll() return false when there are no cookies. 233 } 234 235 236 /* ---------------- Private operations -------------- */ 237 238 239 /* 240 * This is almost the same as HttpCookie.domainMatches except for 241 * one difference: It won't reject cookies when the 'H' part of the 242 * domain contains a dot ('.'). 243 * I.E.: RFC 2965 section 3.3.2 says that if host is x.y.domain.com 244 * and the cookie domain is .domain.com, then it should be rejected. 245 * However that's not how the real world works. Browsers don't reject and 246 * some sites, like yahoo.com do actually expect these cookies to be 247 * passed along. 248 * And should be used for 'old' style cookies (aka Netscape type of cookies) 249 */ 250 private boolean netscapeDomainMatches(String domain, String host) 251 { 252 if (domain == null || host == null) { 253 return false; 254 } 255 256 // if there's no embedded dot in domain and domain is not .local 257 boolean isLocalDomain = ".local".equalsIgnoreCase(domain); 258 int embeddedDotInDomain = domain.indexOf('.'); 259 if (embeddedDotInDomain == 0) { 260 embeddedDotInDomain = domain.indexOf('.', 1); 261 } 262 if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) { 263 return false; 264 } 265 266 // if the host name contains no dot and the domain name is .local 267 int firstDotInHost = host.indexOf('.'); 268 if (firstDotInHost == -1 && isLocalDomain) { 269 return true; 270 } 271 272 int domainLength = domain.length(); 273 int lengthDiff = host.length() - domainLength; 274 if (lengthDiff == 0) { 275 // if the host name and the domain name are just string-compare euqal 276 return host.equalsIgnoreCase(domain); 277 } else if (lengthDiff > 0) { 278 // need to check H & D component 279 String D = host.substring(lengthDiff); 280 281 // Android-changed: b/26456024 targetSdkVersion based compatibility for domain matching 282 // Android M and earlier: Cookies with domain "foo.com" would not match "bar.foo.com". 283 // The RFC dictates that the user agent must treat those domains as if they had a 284 // leading period and must therefore match "bar.foo.com". 285 if (applyMCompatibility && !domain.startsWith(".")) { 286 return false; 287 } 288 289 return (D.equalsIgnoreCase(domain)); 290 } else if (lengthDiff == -1) { 291 // if domain is actually .host 292 return (domain.charAt(0) == '.' && 293 host.equalsIgnoreCase(domain.substring(1))); 294 } 295 296 return false; 297 } 298 299 private void getInternal1(List<HttpCookie> cookies, Map<URI, List<HttpCookie>> cookieIndex, 300 String host) { 301 // BEGIN Android-changed: b/25897688 InMemoryCookieStore ignores scheme (http/https) 302 // Use a separate list to handle cookies that need to be removed so 303 // that there is no conflict with iterators. 304 ArrayList<HttpCookie> toRemove = new ArrayList<HttpCookie>(); 305 for (Map.Entry<URI, List<HttpCookie>> entry : cookieIndex.entrySet()) { 306 List<HttpCookie> lst = entry.getValue(); 307 for (HttpCookie c : lst) { 308 String domain = c.getDomain(); 309 if ((c.getVersion() == 0 && netscapeDomainMatches(domain, host)) || 310 (c.getVersion() == 1 && HttpCookie.domainMatches(domain, host))) { 311 312 // the cookie still in main cookie store 313 if (!c.hasExpired()) { 314 // don't add twice 315 if (!cookies.contains(c)) { 316 cookies.add(c); 317 } 318 } else { 319 toRemove.add(c); 320 } 321 } 322 } 323 // Clear up the cookies that need to be removed 324 for (HttpCookie c : toRemove) { 325 lst.remove(c); 326 327 } 328 toRemove.clear(); 329 } 330 // END Android-changed: b/25897688 InMemoryCookieStore ignores scheme (http/https) 331 } 332 333 // @param cookies [OUT] contains the found cookies 334 // @param cookieIndex the index 335 // @param comparator the prediction to decide whether or not 336 // a cookie in index should be returned 337 private <T extends Comparable<T>> 338 void getInternal2(List<HttpCookie> cookies, Map<T, List<HttpCookie>> cookieIndex, 339 T comparator) 340 { 341 // BEGIN Android-changed: b/25897688 InMemoryCookieStore ignores scheme (http/https) 342 // Removed cookieJar 343 for (T index : cookieIndex.keySet()) { 344 if ((index == comparator) || (index != null && comparator.compareTo(index) == 0)) { 345 List<HttpCookie> indexedCookies = cookieIndex.get(index); 346 // check the list of cookies associated with this domain 347 if (indexedCookies != null) { 348 Iterator<HttpCookie> it = indexedCookies.iterator(); 349 while (it.hasNext()) { 350 HttpCookie ck = it.next(); 351 // the cookie still in main cookie store 352 if (!ck.hasExpired()) { 353 // don't add twice 354 if (!cookies.contains(ck)) 355 cookies.add(ck); 356 } else { 357 it.remove(); 358 } 359 } 360 } // end of indexedCookies != null 361 } // end of comparator.compareTo(index) == 0 362 } // end of cookieIndex iteration 363 // END Android-changed: b/25897688 InMemoryCookieStore ignores scheme (http/https) 364 } 365 366 // add 'cookie' indexed by 'index' into 'indexStore' 367 private <T> void addIndex(Map<T, List<HttpCookie>> indexStore, 368 T index, 369 HttpCookie cookie) 370 { 371 // Android-changed: "index" can be null. We only use the URI based 372 // index on Android and we want to support null URIs. The underlying 373 // store is a HashMap which will support null keys anyway. 374 // if (index != null) { 375 List<HttpCookie> cookies = indexStore.get(index); 376 if (cookies != null) { 377 // there may already have the same cookie, so remove it first 378 cookies.remove(cookie); 379 380 cookies.add(cookie); 381 } else { 382 cookies = new ArrayList<HttpCookie>(); 383 cookies.add(cookie); 384 indexStore.put(index, cookies); 385 } 386 } 387 388 389 // 390 // for cookie purpose, the effective uri should only be http://host 391 // the path will be taken into account when path-match algorithm applied 392 // 393 private URI getEffectiveURI(URI uri) { 394 URI effectiveURI = null; 395 // Android-added: Fix NullPointerException 396 if (uri == null) { 397 return null; 398 } 399 try { 400 effectiveURI = new URI("http", 401 uri.getHost(), 402 null, // path component 403 null, // query component 404 null // fragment component 405 ); 406 } catch (URISyntaxException ignored) { 407 effectiveURI = uri; 408 } 409 410 return effectiveURI; 411 } 412 } 413