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