1 /*
   2  * Copyright (c) 2011, 2018, 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 com.sun.webkit.network;
  27 
  28 import com.sun.javafx.logging.PlatformLogger;
  29 import com.sun.javafx.logging.PlatformLogger.Level;
  30 
  31 import java.util.LinkedHashMap;
  32 import java.util.Comparator;
  33 import java.util.Collections;
  34 import java.util.ArrayList;
  35 import java.util.HashMap;
  36 import java.util.Iterator;
  37 import java.util.List;
  38 import java.util.Map;
  39 import java.util.PriorityQueue;
  40 import java.util.Queue;
  41 
  42 /**
  43  * A cookie store.
  44  */
  45 final class CookieStore {
  46 
  47     private static final PlatformLogger logger =
  48             PlatformLogger.getLogger(CookieStore.class.getName());
  49 
  50     private static final int MAX_BUCKET_SIZE = 50;
  51     private static final int TOTAL_COUNT_LOWER_THRESHOLD = 3000;
  52     private static final int TOTAL_COUNT_UPPER_THRESHOLD = 4000;
  53 
  54 
  55     /**
  56      * The mapping from domain names to cookie buckets.
  57      * Each cookie bucket stores the cookies associated with the
  58      * corresponding domain. Each cookie bucket is represented
  59      * by a Map<Cookie,Cookie> to facilitate retrieval of a cookie
  60      * by another cookie with the same name, domain, and path.
  61      */
  62     private final Map<String,Map<Cookie,Cookie>> buckets =
  63             new HashMap<String,Map<Cookie,Cookie>>();
  64 
  65     /**
  66      * The total number of cookies currently in the store.
  67      */
  68     private int totalCount = 0;
  69 
  70 
  71     /**
  72      * Creates a new {@code CookieStore}.
  73      */
  74     CookieStore() {
  75     }
  76 
  77 
  78     /**
  79      * Returns the currently stored cookie with the same name, domain, and
  80      * path as the given cookie.
  81      */
  82     Cookie get(Cookie cookie) {
  83         Map<Cookie,Cookie> bucket = buckets.get(cookie.getDomain());
  84         if (bucket == null) {
  85             return null;
  86         }
  87         Cookie storedCookie = bucket.get(cookie);
  88         if (storedCookie == null) {
  89             return null;
  90         }
  91         if (storedCookie.hasExpired()) {
  92             bucket.remove(storedCookie);
  93             totalCount--;
  94             log("Expired cookie removed by get", storedCookie, bucket);
  95             return null;
  96         }
  97         return storedCookie;
  98     }
  99 
 100 
 101     /**
 102      * Returns all the currently stored cookies that match the given query.
 103      */
 104     List<Cookie> get(String hostname, String path, boolean secureProtocol,
 105             boolean httpApi)
 106     {
 107         if (logger.isLoggable(Level.FINEST)) {
 108             logger.finest("hostname: [{0}], path: [{1}], "
 109                     + "secureProtocol: [{2}], httpApi: [{3}]", new Object[] {
 110                     hostname, path, secureProtocol, httpApi});
 111         }
 112 
 113         ArrayList<Cookie> result = new ArrayList<Cookie>();
 114 
 115         String domain = hostname;
 116         while (domain.length() > 0) {
 117             Map<Cookie,Cookie> bucket = buckets.get(domain);
 118             if (bucket != null) {
 119                 find(result, bucket, hostname, path, secureProtocol, httpApi);
 120             }
 121             int nextPoint = domain.indexOf('.');
 122             if (nextPoint != -1) {
 123                 domain = domain.substring(nextPoint + 1);
 124             } else {
 125                 break;
 126             }
 127         }
 128 
 129         Collections.sort(result, new GetComparator());
 130 
 131         long currentTime = System.currentTimeMillis();
 132         for (Cookie cookie : result) {
 133             cookie.setLastAccessTime(currentTime);
 134         }
 135 
 136         logger.finest("result: {0}", result);
 137         return result;
 138     }
 139 
 140     /**
 141      * Finds all the cookies that are stored in the given bucket and
 142      * match the given query.
 143      */
 144     private void find(List<Cookie> list, Map<Cookie,Cookie> bucket,
 145             String hostname, String path, boolean secureProtocol,
 146             boolean httpApi)
 147     {
 148         Iterator<Cookie> it = bucket.values().iterator();
 149         while (it.hasNext()) {
 150             Cookie cookie = it.next();
 151             if (cookie.hasExpired()) {
 152                 it.remove();
 153                 totalCount--;
 154                 log("Expired cookie removed by find", cookie, bucket);
 155                 continue;
 156             }
 157 
 158             if (cookie.getHostOnly()) {
 159                 if (!hostname.equalsIgnoreCase(cookie.getDomain())) {
 160                     continue;
 161                 }
 162             } else {
 163                 if (!Cookie.domainMatches(hostname, cookie.getDomain())) {
 164                     continue;
 165                 }
 166             }
 167 
 168             if (!Cookie.pathMatches(path, cookie.getPath())) {
 169                 continue;
 170             }
 171 
 172             if (cookie.getSecureOnly() && !secureProtocol) {
 173                 continue;
 174             }
 175 
 176             if (cookie.getHttpOnly() && !httpApi) {
 177                 continue;
 178             }
 179 
 180             list.add(cookie);
 181         }
 182     }
 183 
 184     private static final class GetComparator implements Comparator<Cookie> {
 185         @Override
 186         public int compare(Cookie c1, Cookie c2) {
 187             int d = c2.getPath().length() - c1.getPath().length();
 188             if (d != 0) {
 189                 return d;
 190             }
 191             return c1.getCreationTime().compareTo(c2.getCreationTime());
 192         }
 193     }
 194 
 195     /**
 196      * Stores the given cookie.
 197      */
 198     void put(Cookie cookie) {
 199         Map<Cookie,Cookie> bucket = buckets.get(cookie.getDomain());
 200         if (bucket == null) {
 201             bucket = new LinkedHashMap<Cookie,Cookie>(20);
 202             buckets.put(cookie.getDomain(), bucket);
 203         }
 204         if (cookie.hasExpired()) {
 205             log("Cookie expired", cookie, bucket);
 206             if (bucket.remove(cookie) != null) {
 207                 totalCount--;
 208                 log("Expired cookie removed by put", cookie, bucket);
 209             }
 210         } else {
 211             if (bucket.put(cookie, cookie) == null) {
 212                 totalCount++;
 213                 log("Cookie added", cookie, bucket);
 214                 if (bucket.size() > MAX_BUCKET_SIZE) {
 215                     purge(bucket);
 216                 }
 217                 if (totalCount > TOTAL_COUNT_UPPER_THRESHOLD) {
 218                     purge();
 219                 }
 220             } else {
 221                 log("Cookie updated", cookie, bucket);
 222             }
 223         }
 224     }
 225 
 226     /**
 227      * Removes excess cookies from a given bucket.
 228      */
 229     private void purge(Map<Cookie,Cookie> bucket) {
 230         logger.finest("Purging bucket: {0}", bucket.values());
 231 
 232         Cookie earliestCookie = null;
 233         Iterator<Cookie> it = bucket.values().iterator();
 234         while (it.hasNext()) {
 235             Cookie cookie = it.next();
 236             if (cookie.hasExpired()) {
 237                 it.remove();
 238                 totalCount--;
 239                 log("Expired cookie removed", cookie, bucket);
 240             } else {
 241                 if (earliestCookie == null || cookie.getLastAccessTime()
 242                         < earliestCookie.getLastAccessTime())
 243                 {
 244                     earliestCookie = cookie;
 245                 }
 246             }
 247         }
 248         if (bucket.size() > MAX_BUCKET_SIZE) {
 249             bucket.remove(earliestCookie);
 250             totalCount--;
 251             log("Excess cookie removed", earliestCookie, bucket);
 252         }
 253     }
 254 
 255     /**
 256      * Removes excess cookies globally.
 257      */
 258     private void purge() {
 259         logger.finest("Purging store");
 260 
 261         Queue<Cookie> removalQueue = new PriorityQueue<Cookie>(totalCount / 2,
 262                 new RemovalComparator());
 263 
 264         for (Map.Entry<String,Map<Cookie,Cookie>> entry : buckets.entrySet()) {
 265             Map<Cookie,Cookie> bucket = entry.getValue();
 266             Iterator<Cookie> it = bucket.values().iterator();
 267             while (it.hasNext()) {
 268                 Cookie cookie = it.next();
 269                 if (cookie.hasExpired()) {
 270                     it.remove();
 271                     totalCount--;
 272                     log("Expired cookie removed", cookie, bucket);
 273                 } else {
 274                     removalQueue.add(cookie);
 275                 }
 276             }
 277         }
 278 
 279         while (totalCount > TOTAL_COUNT_LOWER_THRESHOLD) {
 280             Cookie cookie = removalQueue.remove();
 281             Map<Cookie,Cookie> bucket = buckets.get(cookie.getDomain());
 282             if (bucket != null) {
 283                 bucket.remove(cookie);
 284                 totalCount--;
 285                 log("Excess cookie removed", cookie, bucket);
 286             }
 287         }
 288     }
 289 
 290     private static final class RemovalComparator implements Comparator<Cookie> {
 291         @Override
 292         public int compare(Cookie c1, Cookie c2) {
 293             return (int) (c1.getLastAccessTime() - c2.getLastAccessTime());
 294         }
 295     }
 296 
 297     /**
 298      * Logs a cookie event.
 299      */
 300     private void log(String message, Cookie cookie,
 301             Map<Cookie,Cookie> bucket)
 302     {
 303         if (logger.isLoggable(Level.FINEST)) {
 304             logger.finest("{0}: {1}, bucket size: {2}, total count: {3}",
 305                     new Object[] {message, cookie, bucket.size(), totalCount});
 306         }
 307     }
 308 }