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 
26 package java.net;
27 
28 import java.net.URI;
29 import java.net.CookieStore;
30 import java.net.HttpCookie;
31 import java.net.URISyntaxException;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.ArrayList;
35 import java.util.HashMap;
36 import java.util.Collections;
37 import java.util.Iterator;
38 import 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  */
46 class 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      */
InMemoryCookieStore()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      */
add(URI uri, HttpCookie cookie)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      */
get(URI uri)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      */
getCookies()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      */
getURIs()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      */
remove(URI uri, HttpCookie ck)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      */
removeAll()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      */
netscapeDomainMatches(String domain, String host)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 
getInternal1(List<HttpCookie> cookies, Map<String, List<HttpCookie>> cookieIndex, String host, boolean secureLink)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
getInternal2(List<HttpCookie> cookies, Map<T, List<HttpCookie>> cookieIndex, Comparable<T> comparator, boolean secureLink)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'
addIndex(Map<T, List<HttpCookie>> indexStore, T index, HttpCookie cookie)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     //
getEffectiveURI(URI uri)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