Home | History | Annotate | Download | only in net
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  * Copyright (c) 2005, 2012, 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 
     27 package java.net;
     28 
     29 import dalvik.system.VMRuntime;
     30 
     31 import java.util.List;
     32 import java.util.Map;
     33 import java.util.ArrayList;
     34 import java.util.HashMap;
     35 import java.util.Collections;
     36 import java.util.Iterator;
     37 import java.util.concurrent.locks.ReentrantLock;
     38 
     39 // Android-changed: App compat changes and bug fixes
     40 // b/26456024 Add targetSdkVersion based compatibility for domain matching
     41 // b/33034917 Support clearing cookies by adding it with "max-age=0"
     42 // b/25897688 InMemoryCookieStore ignores scheme (http/https) port and path of the cookie
     43 // Remove cookieJar and domainIndex. Use urlIndex as single Cookie storage
     44 // Fix InMemoryCookieStore#remove to verify cookie URI before removal
     45 // Fix InMemoryCookieStore#removeAll to return false if it's empty.
     46 /**
     47  * A simple in-memory java.net.CookieStore implementation
     48  *
     49  * @author Edward Wang
     50  * @since 1.6
     51  * @hide Visible for testing only.
     52  */
     53 public class InMemoryCookieStore implements CookieStore {
     54     // the in-memory representation of cookies
     55     // BEGIN Android-removed: Remove cookieJar and domainIndex
     56     /*
     57     private List<HttpCookie> cookieJar = null;
     58 
     59     // the cookies are indexed by its domain and associated uri (if present)
     60     // CAUTION: when a cookie removed from main data structure (i.e. cookieJar),
     61     //          it won't be cleared in domainIndex & uriIndex. Double-check the
     62     //          presence of cookie when retrieve one form index store.
     63     private Map<String, List<HttpCookie>> domainIndex = null;
     64     */
     65     // END Android-removed: Remove cookieJar and domainIndex
     66     private Map<URI, List<HttpCookie>> uriIndex = null;
     67 
     68     // use ReentrantLock instead of syncronized for scalability
     69     private ReentrantLock lock = null;
     70 
     71     // BEGIN Android-changed: Add targetSdkVersion and remove cookieJar and domainIndex
     72     private final boolean applyMCompatibility;
     73 
     74     /**
     75      * The default ctor
     76      */
     77     public InMemoryCookieStore() {
     78         this(VMRuntime.getRuntime().getTargetSdkVersion());
     79     }
     80 
     81     public InMemoryCookieStore(int targetSdkVersion) {
     82         uriIndex = new HashMap<>();
     83         lock = new ReentrantLock(false);
     84         applyMCompatibility = (targetSdkVersion <= 23);
     85     }
     86     // END Android-changed: Add targetSdkVersion and remove cookieJar and domainIndex
     87 
     88     /**
     89      * Add one cookie into cookie store.
     90      */
     91     public void add(URI uri, HttpCookie cookie) {
     92         // pre-condition : argument can't be null
     93         if (cookie == null) {
     94             throw new NullPointerException("cookie is null");
     95         }
     96 
     97         lock.lock();
     98         try {
     99             // Android-changed: http://b/33034917, android supports clearing cookies
    100             // by adding the cookie with max-age: 0.
    101             //if (cookie.getMaxAge() != 0) {
    102             addIndex(uriIndex, getEffectiveURI(uri), cookie);
    103             //}
    104         } finally {
    105             lock.unlock();
    106         }
    107     }
    108 
    109 
    110     /**
    111      * Get all cookies, which:
    112      *  1) given uri domain-matches with, or, associated with
    113      *     given uri when added to the cookie store.
    114      *  3) not expired.
    115      * See RFC 2965 sec. 3.3.4 for more detail.
    116      */
    117     public List<HttpCookie> get(URI uri) {
    118         // argument can't be null
    119         if (uri == null) {
    120             throw new NullPointerException("uri is null");
    121         }
    122 
    123         List<HttpCookie> cookies = new ArrayList<HttpCookie>();
    124         // BEGIN Android-changed: b/25897688 InMemoryCookieStore ignores scheme (http/https)
    125         lock.lock();
    126         try {
    127             // check domainIndex first
    128             getInternal1(cookies, uriIndex, uri.getHost());
    129             // check uriIndex then
    130             getInternal2(cookies, uriIndex, getEffectiveURI(uri));
    131         } finally {
    132             lock.unlock();
    133         }
    134         // END Android-changed: b/25897688 InMemoryCookieStore ignores scheme (http/https)
    135         return cookies;
    136     }
    137 
    138     /**
    139      * Get all cookies in cookie store, except those have expired
    140      */
    141     public List<HttpCookie> getCookies() {
    142         // BEGIN Android-changed: Remove cookieJar and domainIndex
    143         List<HttpCookie> rt = new ArrayList<HttpCookie>();
    144 
    145         lock.lock();
    146         try {
    147             for (List<HttpCookie> list : uriIndex.values()) {
    148                 Iterator<HttpCookie> it = list.iterator();
    149                 while (it.hasNext()) {
    150                     HttpCookie cookie = it.next();
    151                     if (cookie.hasExpired()) {
    152                         it.remove();
    153                     } else if (!rt.contains(cookie)) {
    154                         rt.add(cookie);
    155                     }
    156                 }
    157             }
    158         } finally {
    159             rt = Collections.unmodifiableList(rt);
    160             lock.unlock();
    161         }
    162         // END Android-changed: Remove cookieJar and domainIndex
    163 
    164         return rt;
    165     }
    166 
    167     /**
    168      * Get all URIs, which are associated with at least one cookie
    169      * of this cookie store.
    170      */
    171     public List<URI> getURIs() {
    172         List<URI> uris = new ArrayList<URI>();
    173 
    174         lock.lock();
    175         try {
    176             List<URI> result = new ArrayList<URI>(uriIndex.keySet());
    177             result.remove(null);
    178             return Collections.unmodifiableList(result);
    179         } finally {
    180             uris.addAll(uriIndex.keySet());
    181             lock.unlock();
    182         }
    183     }
    184 
    185 
    186     /**
    187      * Remove a cookie from store
    188      */
    189     public boolean remove(URI uri, HttpCookie ck) {
    190         // argument can't be null
    191         if (ck == null) {
    192             throw new NullPointerException("cookie is null");
    193         }
    194 
    195         // BEGIN Android-changed: Fix uri not being removed from uriIndex
    196         lock.lock();
    197         try {
    198             uri = getEffectiveURI(uri);
    199             if (uriIndex.get(uri) == null) {
    200                 return false;
    201             } else {
    202                 List<HttpCookie> cookies = uriIndex.get(uri);
    203                 if (cookies != null) {
    204                     return cookies.remove(ck);
    205                 } else {
    206                     return false;
    207                 }
    208             }
    209         } finally {
    210             lock.unlock();
    211         }
    212         // END Android-changed: Fix uri not being removed from uriIndex
    213     }
    214 
    215 
    216     /**
    217      * Remove all cookies in this cookie store.
    218      */
    219     public boolean removeAll() {
    220         lock.lock();
    221         // BEGIN Android-changed: Let removeAll() return false when there are no cookies.
    222         boolean result = false;
    223 
    224         try {
    225             result = !uriIndex.isEmpty();
    226             uriIndex.clear();
    227         } finally {
    228             lock.unlock();
    229         }
    230 
    231         return result;
    232         // END Android-changed: Let removeAll() return false when there are no cookies.
    233     }
    234 
    235 
    236     /* ---------------- Private operations -------------- */
    237 
    238 
    239     /*
    240      * This is almost the same as HttpCookie.domainMatches except for
    241      * one difference: It won't reject cookies when the 'H' part of the
    242      * domain contains a dot ('.').
    243      * I.E.: RFC 2965 section 3.3.2 says that if host is x.y.domain.com
    244      * and the cookie domain is .domain.com, then it should be rejected.
    245      * However that's not how the real world works. Browsers don't reject and
    246      * some sites, like yahoo.com do actually expect these cookies to be
    247      * passed along.
    248      * And should be used for 'old' style cookies (aka Netscape type of cookies)
    249      */
    250     private boolean netscapeDomainMatches(String domain, String host)
    251     {
    252         if (domain == null || host == null) {
    253             return false;
    254         }
    255 
    256         // if there's no embedded dot in domain and domain is not .local
    257         boolean isLocalDomain = ".local".equalsIgnoreCase(domain);
    258         int embeddedDotInDomain = domain.indexOf('.');
    259         if (embeddedDotInDomain == 0) {
    260             embeddedDotInDomain = domain.indexOf('.', 1);
    261         }
    262         if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) {
    263             return false;
    264         }
    265 
    266         // if the host name contains no dot and the domain name is .local
    267         int firstDotInHost = host.indexOf('.');
    268         if (firstDotInHost == -1 && isLocalDomain) {
    269             return true;
    270         }
    271 
    272         int domainLength = domain.length();
    273         int lengthDiff = host.length() - domainLength;
    274         if (lengthDiff == 0) {
    275             // if the host name and the domain name are just string-compare euqal
    276             return host.equalsIgnoreCase(domain);
    277         } else if (lengthDiff > 0) {
    278             // need to check H & D component
    279             String D = host.substring(lengthDiff);
    280 
    281             // Android-changed: b/26456024 targetSdkVersion based compatibility for domain matching
    282             // Android M and earlier: Cookies with domain "foo.com" would not match "bar.foo.com".
    283             // The RFC dictates that the user agent must treat those domains as if they had a
    284             // leading period and must therefore match "bar.foo.com".
    285             if (applyMCompatibility && !domain.startsWith(".")) {
    286                 return false;
    287             }
    288 
    289             return (D.equalsIgnoreCase(domain));
    290         } else if (lengthDiff == -1) {
    291             // if domain is actually .host
    292             return (domain.charAt(0) == '.' &&
    293                     host.equalsIgnoreCase(domain.substring(1)));
    294         }
    295 
    296         return false;
    297     }
    298 
    299     private void getInternal1(List<HttpCookie> cookies, Map<URI, List<HttpCookie>> cookieIndex,
    300             String host) {
    301         // BEGIN Android-changed: b/25897688 InMemoryCookieStore ignores scheme (http/https)
    302         // Use a separate list to handle cookies that need to be removed so
    303         // that there is no conflict with iterators.
    304         ArrayList<HttpCookie> toRemove = new ArrayList<HttpCookie>();
    305         for (Map.Entry<URI, List<HttpCookie>> entry : cookieIndex.entrySet()) {
    306             List<HttpCookie> lst = entry.getValue();
    307             for (HttpCookie c : lst) {
    308                 String domain = c.getDomain();
    309                 if ((c.getVersion() == 0 && netscapeDomainMatches(domain, host)) ||
    310                         (c.getVersion() == 1 && HttpCookie.domainMatches(domain, host))) {
    311 
    312                     // the cookie still in main cookie store
    313                     if (!c.hasExpired()) {
    314                         // don't add twice
    315                         if (!cookies.contains(c)) {
    316                             cookies.add(c);
    317                         }
    318                     } else {
    319                         toRemove.add(c);
    320                     }
    321                 }
    322             }
    323             // Clear up the cookies that need to be removed
    324             for (HttpCookie c : toRemove) {
    325                 lst.remove(c);
    326 
    327             }
    328             toRemove.clear();
    329         }
    330         // END Android-changed: b/25897688 InMemoryCookieStore ignores scheme (http/https)
    331     }
    332 
    333     // @param cookies           [OUT] contains the found cookies
    334     // @param cookieIndex       the index
    335     // @param comparator        the prediction to decide whether or not
    336     //                          a cookie in index should be returned
    337     private <T extends Comparable<T>>
    338         void getInternal2(List<HttpCookie> cookies, Map<T, List<HttpCookie>> cookieIndex,
    339                           T comparator)
    340     {
    341         // BEGIN Android-changed: b/25897688 InMemoryCookieStore ignores scheme (http/https)
    342         // Removed cookieJar
    343         for (T index : cookieIndex.keySet()) {
    344             if ((index == comparator) || (index != null && comparator.compareTo(index) == 0)) {
    345                 List<HttpCookie> indexedCookies = cookieIndex.get(index);
    346                 // check the list of cookies associated with this domain
    347                 if (indexedCookies != null) {
    348                     Iterator<HttpCookie> it = indexedCookies.iterator();
    349                     while (it.hasNext()) {
    350                         HttpCookie ck = it.next();
    351                         // the cookie still in main cookie store
    352                         if (!ck.hasExpired()) {
    353                             // don't add twice
    354                             if (!cookies.contains(ck))
    355                                 cookies.add(ck);
    356                         } else {
    357                             it.remove();
    358                         }
    359                     }
    360                 } // end of indexedCookies != null
    361             } // end of comparator.compareTo(index) == 0
    362         } // end of cookieIndex iteration
    363         // END Android-changed: b/25897688 InMemoryCookieStore ignores scheme (http/https)
    364     }
    365 
    366     // add 'cookie' indexed by 'index' into 'indexStore'
    367     private <T> void addIndex(Map<T, List<HttpCookie>> indexStore,
    368                               T index,
    369                               HttpCookie cookie)
    370     {
    371         // Android-changed: "index" can be null. We only use the URI based
    372         // index on Android and we want to support null URIs. The underlying
    373         // store is a HashMap which will support null keys anyway.
    374         // if (index != null) {
    375         List<HttpCookie> cookies = indexStore.get(index);
    376         if (cookies != null) {
    377             // there may already have the same cookie, so remove it first
    378             cookies.remove(cookie);
    379 
    380             cookies.add(cookie);
    381         } else {
    382             cookies = new ArrayList<HttpCookie>();
    383             cookies.add(cookie);
    384             indexStore.put(index, cookies);
    385         }
    386     }
    387 
    388 
    389     //
    390     // for cookie purpose, the effective uri should only be http://host
    391     // the path will be taken into account when path-match algorithm applied
    392     //
    393     private URI getEffectiveURI(URI uri) {
    394         URI effectiveURI = null;
    395         // Android-added: Fix NullPointerException
    396         if (uri == null) {
    397             return null;
    398         }
    399         try {
    400             effectiveURI = new URI("http",
    401                                    uri.getHost(),
    402                                    null,  // path component
    403                                    null,  // query component
    404                                    null   // fragment component
    405                                   );
    406         } catch (URISyntaxException ignored) {
    407             effectiveURI = uri;
    408         }
    409 
    410         return effectiveURI;
    411     }
    412 }
    413