Home | History | Annotate | Download | only in net
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  * Copyright (c) 2005, 2013, 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 java.util.Map;
     30 import java.util.List;
     31 import java.util.Collections;
     32 import java.util.Comparator;
     33 import java.io.IOException;
     34 import sun.util.logging.PlatformLogger;
     35 
     36 /**
     37  * CookieManager provides a concrete implementation of {@link CookieHandler},
     38  * which separates the storage of cookies from the policy surrounding accepting
     39  * and rejecting cookies. A CookieManager is initialized with a {@link CookieStore}
     40  * which manages storage, and a {@link CookiePolicy} object, which makes
     41  * policy decisions on cookie acceptance/rejection.
     42  *
     43  * <p> The HTTP cookie management in java.net package looks like:
     44  * <blockquote>
     45  * <pre>{@code
     46  *                  use
     47  * CookieHandler <------- HttpURLConnection
     48  *       ^
     49  *       | impl
     50  *       |         use
     51  * CookieManager -------> CookiePolicy
     52  *             |   use
     53  *             |--------> HttpCookie
     54  *             |              ^
     55  *             |              | use
     56  *             |   use        |
     57  *             |--------> CookieStore
     58  *                            ^
     59  *                            | impl
     60  *                            |
     61  *                  Internal in-memory implementation
     62  * }</pre>
     63  * <ul>
     64  *   <li>
     65  *     CookieHandler is at the core of cookie management. User can call
     66  *     CookieHandler.setDefault to set a concrete CookieHanlder implementation
     67  *     to be used.
     68  *   </li>
     69  *   <li>
     70  *     CookiePolicy.shouldAccept will be called by CookieManager.put to see whether
     71  *     or not one cookie should be accepted and put into cookie store. User can use
     72  *     any of three pre-defined CookiePolicy, namely ACCEPT_ALL, ACCEPT_NONE and
     73  *     ACCEPT_ORIGINAL_SERVER, or user can define his own CookiePolicy implementation
     74  *     and tell CookieManager to use it.
     75  *   </li>
     76  *   <li>
     77  *     CookieStore is the place where any accepted HTTP cookie is stored in.
     78  *     If not specified when created, a CookieManager instance will use an internal
     79  *     in-memory implementation. Or user can implements one and tell CookieManager
     80  *     to use it.
     81  *   </li>
     82  *   <li>
     83  *     Currently, only CookieStore.add(URI, HttpCookie) and CookieStore.get(URI)
     84  *     are used by CookieManager. Others are for completeness and might be needed
     85  *     by a more sophisticated CookieStore implementation, e.g. a NetscapeCookieSotre.
     86  *   </li>
     87  * </ul>
     88  * </blockquote>
     89  *
     90  * <p>There're various ways user can hook up his own HTTP cookie management behavior, e.g.
     91  * <blockquote>
     92  * <ul>
     93  *   <li>Use CookieHandler.setDefault to set a brand new {@link CookieHandler} implementation
     94  *   <li>Let CookieManager be the default {@link CookieHandler} implementation,
     95  *       but implement user's own {@link CookieStore} and {@link CookiePolicy}
     96  *       and tell default CookieManager to use them:
     97  *     <blockquote><pre>
     98  *       // this should be done at the beginning of an HTTP session
     99  *       CookieHandler.setDefault(new CookieManager(new MyCookieStore(), new MyCookiePolicy()));
    100  *     </pre></blockquote>
    101  *   <li>Let CookieManager be the default {@link CookieHandler} implementation, but
    102  *       use customized {@link CookiePolicy}:
    103  *     <blockquote><pre>
    104  *       // this should be done at the beginning of an HTTP session
    105  *       CookieHandler.setDefault(new CookieManager());
    106  *       // this can be done at any point of an HTTP session
    107  *       ((CookieManager)CookieHandler.getDefault()).setCookiePolicy(new MyCookiePolicy());
    108  *     </pre></blockquote>
    109  * </ul>
    110  * </blockquote>
    111  *
    112  * <p>The implementation conforms to <a href="http://www.ietf.org/rfc/rfc2965.txt">RFC 2965</a>, section 3.3.
    113  *
    114  * @see CookiePolicy
    115  * @author Edward Wang
    116  * @since 1.6
    117  */
    118 public class CookieManager extends CookieHandler
    119 {
    120     /* ---------------- Fields -------------- */
    121 
    122     private CookiePolicy policyCallback;
    123 
    124 
    125     private CookieStore cookieJar = null;
    126 
    127 
    128     /* ---------------- Ctors -------------- */
    129 
    130     /**
    131      * Create a new cookie manager.
    132      *
    133      * <p>This constructor will create new cookie manager with default
    134      * cookie store and accept policy. The effect is same as
    135      * {@code CookieManager(null, null)}.
    136      */
    137     public CookieManager() {
    138         this(null, null);
    139     }
    140 
    141 
    142     /**
    143      * Create a new cookie manager with specified cookie store and cookie policy.
    144      *
    145      * @param store     a {@code CookieStore} to be used by cookie manager.
    146      *                  if {@code null}, cookie manager will use a default one,
    147      *                  which is an in-memory CookieStore implementation.
    148      * @param cookiePolicy      a {@code CookiePolicy} instance
    149      *                          to be used by cookie manager as policy callback.
    150      *                          if {@code null}, ACCEPT_ORIGINAL_SERVER will
    151      *                          be used.
    152      */
    153     public CookieManager(CookieStore store,
    154                          CookiePolicy cookiePolicy)
    155     {
    156         // use default cookie policy if not specify one
    157         policyCallback = (cookiePolicy == null) ? CookiePolicy.ACCEPT_ORIGINAL_SERVER
    158                                                 : cookiePolicy;
    159 
    160         // if not specify CookieStore to use, use default one
    161         if (store == null) {
    162             cookieJar = new InMemoryCookieStore();
    163         } else {
    164             cookieJar = store;
    165         }
    166     }
    167 
    168 
    169     /* ---------------- Public operations -------------- */
    170 
    171     /**
    172      * To set the cookie policy of this cookie manager.
    173      *
    174      * <p> A instance of {@code CookieManager} will have
    175      * cookie policy ACCEPT_ORIGINAL_SERVER by default. Users always
    176      * can call this method to set another cookie policy.
    177      *
    178      * @param cookiePolicy      the cookie policy. Can be {@code null}, which
    179      *                          has no effects on current cookie policy.
    180      */
    181     public void setCookiePolicy(CookiePolicy cookiePolicy) {
    182         if (cookiePolicy != null) policyCallback = cookiePolicy;
    183     }
    184 
    185 
    186     /**
    187      * To retrieve current cookie store.
    188      *
    189      * @return  the cookie store currently used by cookie manager.
    190      */
    191     public CookieStore getCookieStore() {
    192         return cookieJar;
    193     }
    194 
    195 
    196     public Map<String, List<String>>
    197         get(URI uri, Map<String, List<String>> requestHeaders)
    198         throws IOException
    199     {
    200         // pre-condition check
    201         if (uri == null || requestHeaders == null) {
    202             throw new IllegalArgumentException("Argument is null");
    203         }
    204 
    205         Map<String, List<String>> cookieMap =
    206                         new java.util.HashMap<String, List<String>>();
    207         // if there's no default CookieStore, no way for us to get any cookie
    208         if (cookieJar == null)
    209             return Collections.unmodifiableMap(cookieMap);
    210 
    211         boolean secureLink = "https".equalsIgnoreCase(uri.getScheme());
    212         List<HttpCookie> cookies = new java.util.ArrayList<HttpCookie>();
    213         // BEGIN Android-removed: The logic of converting null path is moved into pathMatches
    214         /*
    215         String path = uri.getPath();
    216         if (path == null || path.isEmpty()) {
    217             path = "/";
    218         }
    219         */
    220         // BEGIN Android-removed: The logic of converting null path is moved into pathMatches
    221         for (HttpCookie cookie : cookieJar.get(uri)) {
    222             // apply path-matches rule (RFC 2965 sec. 3.3.4)
    223             // and check for the possible "secure" tag (i.e. don't send
    224             // 'secure' cookies over unsecure links)
    225             if (pathMatches(uri, cookie) &&
    226                     (secureLink || !cookie.getSecure())) {
    227                 // BEGIN Android-removed: App compat: b/25897688 InMemoryCookieStore ignores scheme.
    228                 /*
    229                 if (cookie.isHttpOnly()) {
    230                     String s = uri.getScheme();
    231                     if (!"http".equalsIgnoreCase(s) && !"https".equalsIgnoreCase(s)) {
    232                         continue;
    233                     }
    234                 }
    235                 */
    236                 // END Android-removed: App compat: b/25897688 InMemoryCookieStore ignores scheme.
    237 
    238                 // Let's check the authorize port list if it exists
    239                 String ports = cookie.getPortlist();
    240                 if (ports != null && !ports.isEmpty()) {
    241                     int port = uri.getPort();
    242                     if (port == -1) {
    243                         port = "https".equals(uri.getScheme()) ? 443 : 80;
    244                     }
    245                     if (isInPortList(ports, port)) {
    246                         cookies.add(cookie);
    247                     }
    248                 } else {
    249                     cookies.add(cookie);
    250                 }
    251             }
    252         }
    253         // Android-added: b/25897688 A fix to return empty map if cookies list is empty
    254         if (cookies.isEmpty()) {
    255             return Collections.emptyMap();
    256         }
    257 
    258         // apply sort rule (RFC 2965 sec. 3.3.4)
    259         List<String> cookieHeader = sortByPath(cookies);
    260 
    261         cookieMap.put("Cookie", cookieHeader);
    262         return Collections.unmodifiableMap(cookieMap);
    263     }
    264 
    265     public void
    266         put(URI uri, Map<String, List<String>> responseHeaders)
    267         throws IOException
    268     {
    269         // pre-condition check
    270         if (uri == null || responseHeaders == null) {
    271             throw new IllegalArgumentException("Argument is null");
    272         }
    273 
    274 
    275         // if there's no default CookieStore, no need to remember any cookie
    276         if (cookieJar == null)
    277             return;
    278 
    279     PlatformLogger logger = PlatformLogger.getLogger("java.net.CookieManager");
    280         for (String headerKey : responseHeaders.keySet()) {
    281             // RFC 2965 3.2.2, key must be 'Set-Cookie2'
    282             // we also accept 'Set-Cookie' here for backward compatibility
    283             if (headerKey == null
    284                 || !(headerKey.equalsIgnoreCase("Set-Cookie2")
    285                      || headerKey.equalsIgnoreCase("Set-Cookie")
    286                     )
    287                 )
    288             {
    289                 continue;
    290             }
    291 
    292             for (String headerValue : responseHeaders.get(headerKey)) {
    293                 try {
    294                     List<HttpCookie> cookies;
    295                     try {
    296                         cookies = HttpCookie.parse(headerValue);
    297                     } catch (IllegalArgumentException e) {
    298                         // Bogus header, make an empty list and log the error
    299                         cookies = java.util.Collections.emptyList();
    300                         if (logger.isLoggable(PlatformLogger.Level.SEVERE)) {
    301                             logger.severe("Invalid cookie for " + uri + ": " + headerValue);
    302                         }
    303                     }
    304                     for (HttpCookie cookie : cookies) {
    305                         if (cookie.getPath() == null) {
    306                             // If no path is specified, then by default
    307                             // the path is the directory of the page/doc
    308                             String path = uri.getPath();
    309                             if (!path.endsWith("/")) {
    310                                 int i = path.lastIndexOf("/");
    311                                 if (i > 0) {
    312                                     path = path.substring(0, i + 1);
    313                                 } else {
    314                                     path = "/";
    315                                 }
    316                             }
    317                             cookie.setPath(path);
    318                         // Android-added: b/25763487 A fix to verify cookie URI before removal
    319                         } else {
    320                             // Validate existing path
    321                             if (!pathMatches(uri, cookie)) {
    322                                 continue;
    323                             }
    324                         }
    325 
    326                         // As per RFC 2965, section 3.3.1:
    327                         // Domain  Defaults to the effective request-host.  (Note that because
    328                         // there is no dot at the beginning of effective request-host,
    329                         // the default Domain can only domain-match itself.)
    330                         if (cookie.getDomain() == null) {
    331                             String host = uri.getHost();
    332                             if (host != null && !host.contains("."))
    333                                 host += ".local";
    334                             cookie.setDomain(host);
    335                         }
    336                         String ports = cookie.getPortlist();
    337                         if (ports != null) {
    338                             int port = uri.getPort();
    339                             if (port == -1) {
    340                                 port = "https".equals(uri.getScheme()) ? 443 : 80;
    341                             }
    342                             if (ports.isEmpty()) {
    343                                 // Empty port list means this should be restricted
    344                                 // to the incoming URI port
    345                                 cookie.setPortlist("" + port );
    346                                 if (shouldAcceptInternal(uri, cookie)) {
    347                                     cookieJar.add(uri, cookie);
    348                                 }
    349                             } else {
    350                                 // Only store cookies with a port list
    351                                 // IF the URI port is in that list, as per
    352                                 // RFC 2965 section 3.3.2
    353                                 if (isInPortList(ports, port) &&
    354                                         shouldAcceptInternal(uri, cookie)) {
    355                                     cookieJar.add(uri, cookie);
    356                                 }
    357                             }
    358                         } else {
    359                             if (shouldAcceptInternal(uri, cookie)) {
    360                                 cookieJar.add(uri, cookie);
    361                             }
    362                         }
    363                     }
    364                 } catch (IllegalArgumentException e) {
    365                     // invalid set-cookie header string
    366                     // no-op
    367                 }
    368             }
    369         }
    370     }
    371 
    372 
    373     /* ---------------- Private operations -------------- */
    374 
    375     // to determine whether or not accept this cookie
    376     private boolean shouldAcceptInternal(URI uri, HttpCookie cookie) {
    377         try {
    378             return policyCallback.shouldAccept(uri, cookie);
    379         } catch (Exception ignored) { // pretect against malicious callback
    380             return false;
    381         }
    382     }
    383 
    384 
    385     static private boolean isInPortList(String lst, int port) {
    386         int i = lst.indexOf(",");
    387         int val = -1;
    388         while (i > 0) {
    389             try {
    390                 val = Integer.parseInt(lst.substring(0, i));
    391                 if (val == port) {
    392                     return true;
    393                 }
    394             } catch (NumberFormatException numberFormatException) {
    395             }
    396             lst = lst.substring(i+1);
    397             i = lst.indexOf(",");
    398         }
    399         if (!lst.isEmpty()) {
    400             try {
    401                 val = Integer.parseInt(lst);
    402                 if (val == port) {
    403                     return true;
    404                 }
    405             } catch (NumberFormatException numberFormatException) {
    406             }
    407         }
    408         return false;
    409     }
    410 
    411     // Android-changed: b/25763487 Cookie path matching logic in OpenJDK was wrong
    412     /**
    413      * Return true iff. the path from {@code cookie} matches the path from {@code uri}.
    414      */
    415     private static boolean pathMatches(URI uri, HttpCookie cookie) {
    416         return normalizePath(uri.getPath()).startsWith(normalizePath(cookie.getPath()));
    417     }
    418 
    419     private static String normalizePath(String path) {
    420         if (path == null) {
    421             path = "";
    422         }
    423 
    424         if (!path.endsWith("/")) {
    425             path = path + "/";
    426         }
    427 
    428         return path;
    429     }
    430 
    431 
    432     /*
    433      * sort cookies with respect to their path: those with more specific Path attributes
    434      * precede those with less specific, as defined in RFC 2965 sec. 3.3.4
    435      */
    436     private List<String> sortByPath(List<HttpCookie> cookies) {
    437         Collections.sort(cookies, new CookiePathComparator());
    438 
    439         // BEGIN Android-changed: Netscape cookie spec and RFC 2965 have different format
    440         // of Cookie header; RFC 2965 requires a leading $Version="1" string while Netscape does not
    441         // The workaround here is to add a $Version="1" string in advance
    442         final StringBuilder result = new StringBuilder();
    443         int minVersion = 1;
    444         for (HttpCookie cookie : cookies) {
    445             if (cookie.getVersion() < minVersion) {
    446                 minVersion = cookie.getVersion();
    447             }
    448         }
    449 
    450         if (minVersion == 1) {
    451             result.append("$Version=\"1\"; ");
    452         }
    453 
    454         for (int i = 0; i < cookies.size(); ++i) {
    455             if (i != 0) {
    456                 result.append("; ");
    457             }
    458 
    459             result.append(cookies.get(i).toString());
    460         }
    461 
    462         List<String> cookieHeader = new java.util.ArrayList<String>();
    463         cookieHeader.add(result.toString());
    464         // END Android-changed: Netscape cookie spec and RFC 2965 have different format
    465         return cookieHeader;
    466     }
    467 
    468 
    469     static class CookiePathComparator implements Comparator<HttpCookie> {
    470         public int compare(HttpCookie c1, HttpCookie c2) {
    471             if (c1 == c2) return 0;
    472             if (c1 == null) return -1;
    473             if (c2 == null) return 1;
    474 
    475             // path rule only applies to the cookies with same name
    476             if (!c1.getName().equals(c2.getName())) return 0;
    477 
    478             // Android-changed: normalize before comparison
    479             final String c1Path = normalizePath(c1.getPath());
    480             final String c2Path = normalizePath(c2.getPath());
    481 
    482             // those with more specific Path attributes precede those with less specific
    483             if (c1Path.startsWith(c2Path))
    484                 return -1;
    485             else if (c2Path.startsWith(c1Path))
    486                 return 1;
    487             else
    488                 return 0;
    489         }
    490     }
    491 }
    492