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      */
  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 }