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