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