1/*
2 * Copyright (c) 2005, 2012, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package java.net;
27
28import java.net.URI;
29import java.net.CookieStore;
30import java.net.HttpCookie;
31import java.net.URISyntaxException;
32import java.util.List;
33import java.util.Map;
34import java.util.ArrayList;
35import java.util.HashMap;
36import java.util.Collections;
37import java.util.Iterator;
38import java.util.concurrent.locks.ReentrantLock;
39
40/**
41 * A simple in-memory java.net.CookieStore implementation
42 *
43 * @author Edward Wang
44 * @since 1.6
45 */
46class InMemoryCookieStore implements CookieStore {
47    // the in-memory representation of cookies
48    private List<HttpCookie> cookieJar = null;
49
50    // the cookies are indexed by its domain and associated uri (if present)
51    // CAUTION: when a cookie removed from main data structure (i.e. cookieJar),
52    //          it won't be cleared in domainIndex & uriIndex. Double-check the
53    //          presence of cookie when retrieve one form index store.
54    private Map<String, List<HttpCookie>> domainIndex = null;
55    private Map<URI, List<HttpCookie>> uriIndex = null;
56
57    // use ReentrantLock instead of syncronized for scalability
58    private ReentrantLock lock = null;
59
60
61    /**
62     * The default ctor
63     */
64    public InMemoryCookieStore() {
65        cookieJar = new ArrayList<>();
66        domainIndex = new HashMap<>();
67        uriIndex = new HashMap<>();
68
69        lock = new ReentrantLock(false);
70    }
71
72    /**
73     * Add one cookie into cookie store.
74     */
75    public void add(URI uri, HttpCookie cookie) {
76        // pre-condition : argument can't be null
77        if (cookie == null) {
78            throw new NullPointerException("cookie is null");
79        }
80
81
82        lock.lock();
83        try {
84            // remove the ole cookie if there has had one
85            cookieJar.remove(cookie);
86
87            // add new cookie if it has a non-zero max-age
88            if (cookie.getMaxAge() != 0) {
89                cookieJar.add(cookie);
90                // and add it to domain index
91                if (cookie.getDomain() != null) {
92                    addIndex(domainIndex, cookie.getDomain(), cookie);
93                }
94                if (uri != null) {
95                    // add it to uri index, too
96                    addIndex(uriIndex, getEffectiveURI(uri), cookie);
97                }
98            }
99        } finally {
100            lock.unlock();
101        }
102    }
103
104
105    /**
106     * Get all cookies, which:
107     *  1) given uri domain-matches with, or, associated with
108     *     given uri when added to the cookie store.
109     *  3) not expired.
110     * See RFC 2965 sec. 3.3.4 for more detail.
111     */
112    public List<HttpCookie> get(URI uri) {
113        // argument can't be null
114        if (uri == null) {
115            throw new NullPointerException("uri is null");
116        }
117
118        List<HttpCookie> cookies = new ArrayList<>();
119        boolean secureLink = "https".equalsIgnoreCase(uri.getScheme());
120        lock.lock();
121        try {
122            // check domainIndex first
123            getInternal1(cookies, domainIndex, uri.getHost(), secureLink);
124            // check uriIndex then
125            getInternal2(cookies, uriIndex, getEffectiveURI(uri), secureLink);
126        } finally {
127            lock.unlock();
128        }
129
130        return cookies;
131    }
132
133    /**
134     * Get all cookies in cookie store, except those have expired
135     */
136    public List<HttpCookie> getCookies() {
137        List<HttpCookie> rt;
138
139        lock.lock();
140        try {
141            Iterator<HttpCookie> it = cookieJar.iterator();
142            while (it.hasNext()) {
143                if (it.next().hasExpired()) {
144                    it.remove();
145                }
146            }
147        } finally {
148            rt = Collections.unmodifiableList(cookieJar);
149            lock.unlock();
150        }
151
152        return rt;
153    }
154
155    /**
156     * Get all URIs, which are associated with at least one cookie
157     * of this cookie store.
158     */
159    public List<URI> getURIs() {
160        List<URI> uris = new ArrayList<>();
161
162        lock.lock();
163        try {
164            Iterator<URI> it = uriIndex.keySet().iterator();
165            while (it.hasNext()) {
166                URI uri = it.next();
167                List<HttpCookie> cookies = uriIndex.get(uri);
168                if (cookies == null || cookies.size() == 0) {
169                    // no cookies list or an empty list associated with
170                    // this uri entry, delete it
171                    it.remove();
172                }
173            }
174        } finally {
175            uris.addAll(uriIndex.keySet());
176            lock.unlock();
177        }
178
179        return uris;
180    }
181
182
183    /**
184     * Remove a cookie from store
185     */
186    public boolean remove(URI uri, HttpCookie ck) {
187        // argument can't be null
188        if (ck == null) {
189            throw new NullPointerException("cookie is null");
190        }
191
192        boolean modified = false;
193        lock.lock();
194        try {
195            modified = cookieJar.remove(ck);
196        } finally {
197            lock.unlock();
198        }
199
200        return modified;
201    }
202
203
204    /**
205     * Remove all cookies in this cookie store.
206     */
207    public boolean removeAll() {
208        lock.lock();
209        try {
210            if (cookieJar.isEmpty()) {
211                return false;
212            }
213            cookieJar.clear();
214            domainIndex.clear();
215            uriIndex.clear();
216        } finally {
217            lock.unlock();
218        }
219
220        return true;
221    }
222
223
224    /* ---------------- Private operations -------------- */
225
226
227    /*
228     * This is almost the same as HttpCookie.domainMatches except for
229     * one difference: It won't reject cookies when the 'H' part of the
230     * domain contains a dot ('.').
231     * I.E.: RFC 2965 section 3.3.2 says that if host is x.y.domain.com
232     * and the cookie domain is .domain.com, then it should be rejected.
233     * However that's not how the real world works. Browsers don't reject and
234     * some sites, like yahoo.com do actually expect these cookies to be
235     * passed along.
236     * And should be used for 'old' style cookies (aka Netscape type of cookies)
237     */
238    private boolean netscapeDomainMatches(String domain, String host)
239    {
240        if (domain == null || host == null) {
241            return false;
242        }
243
244        // if there's no embedded dot in domain and domain is not .local
245        boolean isLocalDomain = ".local".equalsIgnoreCase(domain);
246        int embeddedDotInDomain = domain.indexOf('.');
247        if (embeddedDotInDomain == 0) {
248            embeddedDotInDomain = domain.indexOf('.', 1);
249        }
250        if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) {
251            return false;
252        }
253
254        // if the host name contains no dot and the domain name is .local
255        int firstDotInHost = host.indexOf('.');
256        if (firstDotInHost == -1 && isLocalDomain) {
257            return true;
258        }
259
260        int domainLength = domain.length();
261        int lengthDiff = host.length() - domainLength;
262        if (lengthDiff == 0) {
263            // if the host name and the domain name are just string-compare euqal
264            return host.equalsIgnoreCase(domain);
265        } else if (lengthDiff > 0) {
266            // need to check H & D component
267            String H = host.substring(0, lengthDiff);
268            String D = host.substring(lengthDiff);
269
270            return (D.equalsIgnoreCase(domain));
271        } else if (lengthDiff == -1) {
272            // if domain is actually .host
273            return (domain.charAt(0) == '.' &&
274                    host.equalsIgnoreCase(domain.substring(1)));
275        }
276
277        return false;
278    }
279
280    private void getInternal1(List<HttpCookie> cookies, Map<String, List<HttpCookie>> cookieIndex,
281            String host, boolean secureLink) {
282        // Use a separate list to handle cookies that need to be removed so
283        // that there is no conflict with iterators.
284        ArrayList<HttpCookie> toRemove = new ArrayList<>();
285        for (Map.Entry<String, List<HttpCookie>> entry : cookieIndex.entrySet()) {
286            String domain = entry.getKey();
287            List<HttpCookie> lst = entry.getValue();
288            for (HttpCookie c : lst) {
289                if ((c.getVersion() == 0 && netscapeDomainMatches(domain, host)) ||
290                        (c.getVersion() == 1 && HttpCookie.domainMatches(domain, host))) {
291                    if ((cookieJar.indexOf(c) != -1)) {
292                        // the cookie still in main cookie store
293                        if (!c.hasExpired()) {
294                            // don't add twice and make sure it's the proper
295                            // security level
296                            if ((secureLink || !c.getSecure()) &&
297                                    !cookies.contains(c)) {
298                                cookies.add(c);
299                            }
300                        } else {
301                            toRemove.add(c);
302                        }
303                    } else {
304                        // the cookie has beed removed from main store,
305                        // so also remove it from domain indexed store
306                        toRemove.add(c);
307                    }
308                }
309            }
310            // Clear up the cookies that need to be removed
311            for (HttpCookie c : toRemove) {
312                lst.remove(c);
313                cookieJar.remove(c);
314
315            }
316            toRemove.clear();
317        }
318    }
319
320    // @param cookies           [OUT] contains the found cookies
321    // @param cookieIndex       the index
322    // @param comparator        the prediction to decide whether or not
323    //                          a cookie in index should be returned
324    private <T> void getInternal2(List<HttpCookie> cookies,
325                                Map<T, List<HttpCookie>> cookieIndex,
326                                Comparable<T> comparator, boolean secureLink)
327    {
328        for (T index : cookieIndex.keySet()) {
329            if (comparator.compareTo(index) == 0) {
330                List<HttpCookie> indexedCookies = cookieIndex.get(index);
331                // check the list of cookies associated with this domain
332                if (indexedCookies != null) {
333                    Iterator<HttpCookie> it = indexedCookies.iterator();
334                    while (it.hasNext()) {
335                        HttpCookie ck = it.next();
336                        if (cookieJar.indexOf(ck) != -1) {
337                            // the cookie still in main cookie store
338                            if (!ck.hasExpired()) {
339                                // don't add twice
340                                if ((secureLink || !ck.getSecure()) &&
341                                        !cookies.contains(ck))
342                                    cookies.add(ck);
343                            } else {
344                                it.remove();
345                                cookieJar.remove(ck);
346                            }
347                        } else {
348                            // the cookie has beed removed from main store,
349                            // so also remove it from domain indexed store
350                            it.remove();
351                        }
352                    }
353                } // end of indexedCookies != null
354            } // end of comparator.compareTo(index) == 0
355        } // end of cookieIndex iteration
356    }
357
358    // add 'cookie' indexed by 'index' into 'indexStore'
359    private <T> void addIndex(Map<T, List<HttpCookie>> indexStore,
360                              T index,
361                              HttpCookie cookie)
362    {
363        if (index != null) {
364            List<HttpCookie> cookies = indexStore.get(index);
365            if (cookies != null) {
366                // there may already have the same cookie, so remove it first
367                cookies.remove(cookie);
368
369                cookies.add(cookie);
370            } else {
371                cookies = new ArrayList<>();
372                cookies.add(cookie);
373                indexStore.put(index, cookies);
374            }
375        }
376    }
377
378
379    //
380    // for cookie purpose, the effective uri should only be http://host
381    // the path will be taken into account when path-match algorithm applied
382    //
383    private URI getEffectiveURI(URI uri) {
384        URI effectiveURI = null;
385        try {
386            effectiveURI = new URI("http",
387                                   uri.getHost(),
388                                   null,  // path component
389                                   null,  // query component
390                                   null   // fragment component
391                                  );
392        } catch (URISyntaxException ignored) {
393            effectiveURI = uri;
394        }
395
396        return effectiveURI;
397    }
398}
399