1 /*
   2  * Copyright 2005-2008 Sun Microsystems, Inc.  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.  Sun designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
  22  * CA 95054 USA or visit www.sun.com if you need additional information or
  23  * have any questions.
  24  */
  25 
  26 package sun.net.www.protocol.http;
  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 public 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<HttpCookie>();
  66         domainIndex = new HashMap<String, List<HttpCookie>>();
  67         uriIndex = new HashMap<URI, List<HttpCookie>>();
  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                 // add it to uri index, too
  95                 addIndex(uriIndex, getEffectiveURI(uri), cookie);
  96             }
  97         } finally {
  98             lock.unlock();
  99         }
 100     }
 101 
 102 
 103     /**
 104      * Get all cookies, which:
 105      *  1) given uri domain-matches with, or, associated with
 106      *     given uri when added to the cookie store.
 107      *  3) not expired.
 108      * See RFC 2965 sec. 3.3.4 for more detail.
 109      */
 110     public List<HttpCookie> get(URI uri) {
 111         // argument can't be null
 112         if (uri == null) {
 113             throw new NullPointerException("uri is null");
 114         }
 115 
 116         List<HttpCookie> cookies = new ArrayList<HttpCookie>();
 117         boolean secureLink = "https".equalsIgnoreCase(uri.getScheme());
 118         lock.lock();
 119         try {
 120             // check domainIndex first
 121             getInternal1(cookies, domainIndex, uri.getHost(), secureLink);
 122             // check uriIndex then
 123             getInternal2(cookies, uriIndex, getEffectiveURI(uri), secureLink);
 124         } finally {
 125             lock.unlock();
 126         }
 127 
 128         return cookies;
 129     }
 130 
 131     /**
 132      * Get all cookies in cookie store, except those have expired
 133      */
 134     public List<HttpCookie> getCookies() {
 135         List<HttpCookie> rt;
 136 
 137         lock.lock();
 138         try {
 139             Iterator<HttpCookie> it = cookieJar.iterator();
 140             while (it.hasNext()) {
 141                 if (it.next().hasExpired()) {
 142                     it.remove();
 143                 }
 144             }
 145         } finally {
 146             rt = Collections.unmodifiableList(cookieJar);
 147             lock.unlock();
 148         }
 149 
 150         return rt;
 151     }
 152 
 153     /**
 154      * Get all URIs, which are associated with at least one cookie
 155      * of this cookie store.
 156      */
 157     public List<URI> getURIs() {
 158         List<URI> uris = new ArrayList<URI>();
 159 
 160         lock.lock();
 161         try {
 162             Iterator<URI> it = uriIndex.keySet().iterator();
 163             while (it.hasNext()) {
 164                 URI uri = it.next();
 165                 List<HttpCookie> cookies = uriIndex.get(uri);
 166                 if (cookies == null || cookies.size() == 0) {
 167                     // no cookies list or an empty list associated with
 168                     // this uri entry, delete it
 169                     it.remove();
 170                 }
 171             }
 172         } finally {
 173             uris.addAll(uriIndex.keySet());
 174             lock.unlock();
 175         }
 176 
 177         return uris;
 178     }
 179 
 180 
 181     /**
 182      * Remove a cookie from store
 183      */
 184     public boolean remove(URI uri, HttpCookie ck) {
 185         // argument can't be null
 186         if (ck == null) {
 187             throw new NullPointerException("cookie is null");
 188         }
 189 
 190         boolean modified = false;
 191         lock.lock();
 192         try {
 193             modified = cookieJar.remove(ck);
 194         } finally {
 195             lock.unlock();
 196         }
 197 
 198         return modified;
 199     }
 200 
 201 
 202     /**
 203      * Remove all cookies in this cookie store.
 204      */
 205     public boolean removeAll() {
 206         lock.lock();
 207         try {
 208             cookieJar.clear();
 209             domainIndex.clear();
 210             uriIndex.clear();
 211         } finally {
 212             lock.unlock();
 213         }
 214 
 215         return true;
 216     }
 217 
 218 
 219     /* ---------------- Private operations -------------- */
 220 
 221 
 222     /*
 223      * This is almost the same as HttpCookie.domainMatches except for
 224      * one difference: It won't reject cookies when the 'H' part of the
 225      * domain contains a dot ('.').
 226      * I.E.: RFC 2965 section 3.3.2 says that if host is x.y.domain.com
 227      * and the cookie domain is .domain.com, then it should be rejected.
 228      * However that's not how the real world works. Browsers don't reject and
 229      * some sites, like yahoo.com do actually expect these cookies to be
 230      * passed along.
 231      * And should be used for 'old' style cookies (aka Netscape type of cookies)
 232      */
 233     private boolean netscapeDomainMatches(String domain, String host)
 234     {
 235         if (domain == null || host == null) {
 236             return false;
 237         }
 238 
 239         // if there's no embedded dot in domain and domain is not .local
 240         boolean isLocalDomain = ".local".equalsIgnoreCase(domain);
 241         int embeddedDotInDomain = domain.indexOf('.');
 242         if (embeddedDotInDomain == 0) {
 243             embeddedDotInDomain = domain.indexOf('.', 1);
 244         }
 245         if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) {
 246             return false;
 247         }
 248 
 249         // if the host name contains no dot and the domain name is .local
 250         int firstDotInHost = host.indexOf('.');
 251         if (firstDotInHost == -1 && isLocalDomain) {
 252             return true;
 253         }
 254 
 255         int domainLength = domain.length();
 256         int lengthDiff = host.length() - domainLength;
 257         if (lengthDiff == 0) {
 258             // if the host name and the domain name are just string-compare euqal
 259             return host.equalsIgnoreCase(domain);
 260         } else if (lengthDiff > 0) {
 261             // need to check H & D component
 262             String H = host.substring(0, lengthDiff);
 263             String D = host.substring(lengthDiff);
 264 
 265             return (D.equalsIgnoreCase(domain));
 266         } else if (lengthDiff == -1) {
 267             // if domain is actually .host
 268             return (domain.charAt(0) == '.' &&
 269                     host.equalsIgnoreCase(domain.substring(1)));
 270         }
 271 
 272         return false;
 273     }
 274 
 275     private void getInternal1(List<HttpCookie> cookies, Map<String, List<HttpCookie>> cookieIndex,
 276             String host, boolean secureLink) {
 277         // Use a separate list to handle cookies that need to be removed so
 278         // that there is no conflict with iterators.
 279         ArrayList<HttpCookie> toRemove = new ArrayList<HttpCookie>();
 280         for (Map.Entry<String, List<HttpCookie>> entry : cookieIndex.entrySet()) {
 281             String domain = entry.getKey();
 282             List<HttpCookie> lst = entry.getValue();
 283             for (HttpCookie c : lst) {
 284                 if ((c.getVersion() == 0 && netscapeDomainMatches(domain, host)) ||
 285                         (c.getVersion() == 1 && HttpCookie.domainMatches(domain, host))) {
 286                     if ((cookieJar.indexOf(c) != -1)) {
 287                         // the cookie still in main cookie store
 288                         if (!c.hasExpired()) {
 289                             // don't add twice and make sure it's the proper
 290                             // security level
 291                             if ((secureLink || !c.getSecure()) &&
 292                                     !cookies.contains(c)) {
 293                                 cookies.add(c);
 294                             }
 295                         } else {
 296                             toRemove.add(c);
 297                         }
 298                     } else {
 299                         // the cookie has beed removed from main store,
 300                         // so also remove it from domain indexed store
 301                         toRemove.add(c);
 302                     }
 303                 }
 304             }
 305             // Clear up the cookies that need to be removed
 306             for (HttpCookie c : toRemove) {
 307                 lst.remove(c);
 308                 cookieJar.remove(c);
 309 
 310             }
 311             toRemove.clear();
 312         }
 313     }
 314 
 315     // @param cookies           [OUT] contains the found cookies
 316     // @param cookieIndex       the index
 317     // @param comparator        the prediction to decide whether or not
 318     //                          a cookie in index should be returned
 319     private <T> void getInternal2(List<HttpCookie> cookies,
 320                                 Map<T, List<HttpCookie>> cookieIndex,
 321                                 Comparable<T> comparator, boolean secureLink)
 322     {
 323         for (T index : cookieIndex.keySet()) {
 324             if (comparator.compareTo(index) == 0) {
 325                 List<HttpCookie> indexedCookies = cookieIndex.get(index);
 326                 // check the list of cookies associated with this domain
 327                 if (indexedCookies != null) {
 328                     Iterator<HttpCookie> it = indexedCookies.iterator();
 329                     while (it.hasNext()) {
 330                         HttpCookie ck = it.next();
 331                         if (cookieJar.indexOf(ck) != -1) {
 332                             // the cookie still in main cookie store
 333                             if (!ck.hasExpired()) {
 334                                 // don't add twice
 335                                 if ((secureLink || !ck.getSecure()) &&
 336                                         !cookies.contains(ck))
 337                                     cookies.add(ck);
 338                             } else {
 339                                 it.remove();
 340                                 cookieJar.remove(ck);
 341                             }
 342                         } else {
 343                             // the cookie has beed removed from main store,
 344                             // so also remove it from domain indexed store
 345                             it.remove();
 346                         }
 347                     }
 348                 } // end of indexedCookies != null
 349             } // end of comparator.compareTo(index) == 0
 350         } // end of cookieIndex iteration
 351     }
 352 
 353     // add 'cookie' indexed by 'index' into 'indexStore'
 354     private <T> void addIndex(Map<T, List<HttpCookie>> indexStore,
 355                               T index,
 356                               HttpCookie cookie)
 357     {
 358         if (index != null) {
 359             List<HttpCookie> cookies = indexStore.get(index);
 360             if (cookies != null) {
 361                 // there may already have the same cookie, so remove it first
 362                 cookies.remove(cookie);
 363 
 364                 cookies.add(cookie);
 365             } else {
 366                 cookies = new ArrayList<HttpCookie>();
 367                 cookies.add(cookie);
 368                 indexStore.put(index, cookies);
 369             }
 370         }
 371     }
 372 
 373 
 374     //
 375     // for cookie purpose, the effective uri should only be http://host
 376     // the path will be taken into account when path-match algorithm applied
 377     //
 378     private URI getEffectiveURI(URI uri) {
 379         URI effectiveURI = null;
 380         try {
 381             effectiveURI = new URI("http",
 382                                    uri.getHost(),
 383                                    null,  // path component
 384                                    null,  // query component
 385                                    null   // fragment component
 386                                   );
 387         } catch (URISyntaxException ignored) {
 388             effectiveURI = uri;
 389         }
 390 
 391         return effectiveURI;
 392     }
 393 }