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 /** 40 * A simple in-memory java.net.CookieStore implementation 41 * 42 * @author Edward Wang 43 * @since 1.6 44 * @hide Visible for testing only. 45 */ 46 public class InMemoryCookieStore implements CookieStore { 47 // the in-memory representation of cookies 48 private Map<URI, List<HttpCookie>> uriIndex = null; 49 50 // use ReentrantLock instead of syncronized for scalability 51 private ReentrantLock lock = null; 52 53 private final boolean applyMCompatibility; 54 55 /** 56 * The default ctor 57 */ InMemoryCookieStore()58 public InMemoryCookieStore() { 59 this(VMRuntime.getRuntime().getTargetSdkVersion()); 60 } 61 InMemoryCookieStore(int targetSdkVersion)62 public InMemoryCookieStore(int targetSdkVersion) { 63 uriIndex = new HashMap<>(); 64 lock = new ReentrantLock(false); 65 applyMCompatibility = (targetSdkVersion <= 23); 66 } 67 68 /** 69 * Add one cookie into cookie store. 70 */ add(URI uri, HttpCookie cookie)71 public void add(URI uri, HttpCookie cookie) { 72 // pre-condition : argument can't be null 73 if (cookie == null) { 74 throw new NullPointerException("cookie is null"); 75 } 76 77 lock.lock(); 78 try { 79 // Android-changed: http://b/33034917, android supports clearing cookies 80 // by adding the cookie with max-age: 0. 81 //if (cookie.getMaxAge() != 0) { 82 addIndex(uriIndex, getEffectiveURI(uri), cookie); 83 //} 84 } finally { 85 lock.unlock(); 86 } 87 } 88 89 90 /** 91 * Get all cookies, which: 92 * 1) given uri domain-matches with, or, associated with 93 * given uri when added to the cookie store. 94 * 3) not expired. 95 * See RFC 2965 sec. 3.3.4 for more detail. 96 */ get(URI uri)97 public List<HttpCookie> get(URI uri) { 98 // argument can't be null 99 if (uri == null) { 100 throw new NullPointerException("uri is null"); 101 } 102 103 List<HttpCookie> cookies = new ArrayList<HttpCookie>(); 104 lock.lock(); 105 try { 106 // check domainIndex first 107 getInternal1(cookies, uriIndex, uri.getHost()); 108 // check uriIndex then 109 getInternal2(cookies, uriIndex, getEffectiveURI(uri)); 110 } finally { 111 lock.unlock(); 112 } 113 114 return cookies; 115 } 116 117 /** 118 * Get all cookies in cookie store, except those have expired 119 */ getCookies()120 public List<HttpCookie> getCookies() { 121 List<HttpCookie> rt = new ArrayList<HttpCookie>(); 122 123 lock.lock(); 124 try { 125 for (List<HttpCookie> list : uriIndex.values()) { 126 Iterator<HttpCookie> it = list.iterator(); 127 while (it.hasNext()) { 128 HttpCookie cookie = it.next(); 129 if (cookie.hasExpired()) { 130 it.remove(); 131 } else if (!rt.contains(cookie)) { 132 rt.add(cookie); 133 } 134 } 135 } 136 } finally { 137 rt = Collections.unmodifiableList(rt); 138 lock.unlock(); 139 } 140 141 return rt; 142 } 143 144 /** 145 * Get all URIs, which are associated with at least one cookie 146 * of this cookie store. 147 */ getURIs()148 public List<URI> getURIs() { 149 List<URI> uris = new ArrayList<URI>(); 150 151 lock.lock(); 152 try { 153 List<URI> result = new ArrayList<URI>(uriIndex.keySet()); 154 result.remove(null); 155 return Collections.unmodifiableList(result); 156 } finally { 157 uris.addAll(uriIndex.keySet()); 158 lock.unlock(); 159 } 160 } 161 162 163 /** 164 * Remove a cookie from store 165 */ remove(URI uri, HttpCookie ck)166 public boolean remove(URI uri, HttpCookie ck) { 167 // argument can't be null 168 if (ck == null) { 169 throw new NullPointerException("cookie is null"); 170 } 171 172 lock.lock(); 173 try { 174 uri = getEffectiveURI(uri); 175 if (uriIndex.get(uri) == null) { 176 return false; 177 } else { 178 List<HttpCookie> cookies = uriIndex.get(uri); 179 if (cookies != null) { 180 return cookies.remove(ck); 181 } else { 182 return false; 183 } 184 } 185 } finally { 186 lock.unlock(); 187 } 188 } 189 190 191 /** 192 * Remove all cookies in this cookie store. 193 */ removeAll()194 public boolean removeAll() { 195 lock.lock(); 196 boolean result = false; 197 198 try { 199 result = !uriIndex.isEmpty(); 200 uriIndex.clear(); 201 } finally { 202 lock.unlock(); 203 } 204 205 return result; 206 } 207 208 209 /* ---------------- Private operations -------------- */ 210 211 212 /* 213 * This is almost the same as HttpCookie.domainMatches except for 214 * one difference: It won't reject cookies when the 'H' part of the 215 * domain contains a dot ('.'). 216 * I.E.: RFC 2965 section 3.3.2 says that if host is x.y.domain.com 217 * and the cookie domain is .domain.com, then it should be rejected. 218 * However that's not how the real world works. Browsers don't reject and 219 * some sites, like yahoo.com do actually expect these cookies to be 220 * passed along. 221 * And should be used for 'old' style cookies (aka Netscape type of cookies) 222 */ netscapeDomainMatches(String domain, String host)223 private boolean netscapeDomainMatches(String domain, String host) 224 { 225 if (domain == null || host == null) { 226 return false; 227 } 228 229 // if there's no embedded dot in domain and domain is not .local 230 boolean isLocalDomain = ".local".equalsIgnoreCase(domain); 231 int embeddedDotInDomain = domain.indexOf('.'); 232 if (embeddedDotInDomain == 0) { 233 embeddedDotInDomain = domain.indexOf('.', 1); 234 } 235 if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) { 236 return false; 237 } 238 239 // if the host name contains no dot and the domain name is .local 240 int firstDotInHost = host.indexOf('.'); 241 if (firstDotInHost == -1 && isLocalDomain) { 242 return true; 243 } 244 245 int domainLength = domain.length(); 246 int lengthDiff = host.length() - domainLength; 247 if (lengthDiff == 0) { 248 // if the host name and the domain name are just string-compare euqal 249 return host.equalsIgnoreCase(domain); 250 } else if (lengthDiff > 0) { 251 // need to check H & D component 252 String D = host.substring(lengthDiff); 253 254 // Android M and earlier: Cookies with domain "foo.com" would not match "bar.foo.com". 255 // The RFC dictates that the user agent must treat those domains as if they had a 256 // leading period and must therefore match "bar.foo.com". 257 if (applyMCompatibility && !domain.startsWith(".")) { 258 return false; 259 } 260 261 return (D.equalsIgnoreCase(domain)); 262 } else if (lengthDiff == -1) { 263 // if domain is actually .host 264 return (domain.charAt(0) == '.' && 265 host.equalsIgnoreCase(domain.substring(1))); 266 } 267 268 return false; 269 } 270 getInternal1(List<HttpCookie> cookies, Map<URI, List<HttpCookie>> cookieIndex, String host)271 private void getInternal1(List<HttpCookie> cookies, Map<URI, List<HttpCookie>> cookieIndex, 272 String host) { 273 // Use a separate list to handle cookies that need to be removed so 274 // that there is no conflict with iterators. 275 ArrayList<HttpCookie> toRemove = new ArrayList<HttpCookie>(); 276 for (Map.Entry<URI, List<HttpCookie>> entry : cookieIndex.entrySet()) { 277 List<HttpCookie> lst = entry.getValue(); 278 for (HttpCookie c : lst) { 279 String domain = c.getDomain(); 280 if ((c.getVersion() == 0 && netscapeDomainMatches(domain, host)) || 281 (c.getVersion() == 1 && HttpCookie.domainMatches(domain, host))) { 282 283 // the cookie still in main cookie store 284 if (!c.hasExpired()) { 285 // don't add twice 286 if (!cookies.contains(c)) { 287 cookies.add(c); 288 } 289 } else { 290 toRemove.add(c); 291 } 292 } 293 } 294 // Clear up the cookies that need to be removed 295 for (HttpCookie c : toRemove) { 296 lst.remove(c); 297 298 } 299 toRemove.clear(); 300 } 301 } 302 303 // @param cookies [OUT] contains the found cookies 304 // @param cookieIndex the index 305 // @param comparator the prediction to decide whether or not 306 // a cookie in index should be returned 307 private <T extends Comparable<T>> getInternal2(List<HttpCookie> cookies, Map<T, List<HttpCookie>> cookieIndex, T comparator)308 void getInternal2(List<HttpCookie> cookies, Map<T, List<HttpCookie>> cookieIndex, 309 T comparator) 310 { 311 // Removed cookieJar 312 for (T index : cookieIndex.keySet()) { 313 if ((index == comparator) || (index != null && comparator.compareTo(index) == 0)) { 314 List<HttpCookie> indexedCookies = cookieIndex.get(index); 315 // check the list of cookies associated with this domain 316 if (indexedCookies != null) { 317 Iterator<HttpCookie> it = indexedCookies.iterator(); 318 while (it.hasNext()) { 319 HttpCookie ck = it.next(); 320 // the cookie still in main cookie store 321 if (!ck.hasExpired()) { 322 // don't add twice 323 if (!cookies.contains(ck)) 324 cookies.add(ck); 325 } else { 326 it.remove(); 327 } 328 } 329 } // end of indexedCookies != null 330 } // end of comparator.compareTo(index) == 0 331 } // end of cookieIndex iteration 332 } 333 334 // add 'cookie' indexed by 'index' into 'indexStore' addIndex(Map<T, List<HttpCookie>> indexStore, T index, HttpCookie cookie)335 private <T> void addIndex(Map<T, List<HttpCookie>> indexStore, 336 T index, 337 HttpCookie cookie) 338 { 339 // Android-changed: "index" can be null. We only use the URI based 340 // index on Android and we want to support null URIs. The underlying 341 // store is a HashMap which will support null keys anyway. 342 List<HttpCookie> cookies = indexStore.get(index); 343 if (cookies != null) { 344 // there may already have the same cookie, so remove it first 345 cookies.remove(cookie); 346 347 cookies.add(cookie); 348 } else { 349 cookies = new ArrayList<HttpCookie>(); 350 cookies.add(cookie); 351 indexStore.put(index, cookies); 352 } 353 } 354 355 356 // 357 // for cookie purpose, the effective uri should only be http://host 358 // the path will be taken into account when path-match algorithm applied 359 // getEffectiveURI(URI uri)360 private URI getEffectiveURI(URI uri) { 361 URI effectiveURI = null; 362 if (uri == null) { 363 return null; 364 } 365 try { 366 effectiveURI = new URI("http", 367 uri.getHost(), 368 null, // path component 369 null, // query component 370 null // fragment component 371 ); 372 } catch (URISyntaxException ignored) { 373 effectiveURI = uri; 374 } 375 376 return effectiveURI; 377 } 378 } 379