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
27package java.net;
28
29import dalvik.system.VMRuntime;
30
31import java.util.List;
32import java.util.Map;
33import java.util.ArrayList;
34import java.util.HashMap;
35import java.util.Collections;
36import java.util.Iterator;
37import 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 */
53public 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