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