Home | History | Annotate | Download | only in conscrypt
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package org.conscrypt;
     18 
     19 import java.io.DataInputStream;
     20 import java.io.File;
     21 import java.io.FileInputStream;
     22 import java.io.FileNotFoundException;
     23 import java.io.FileOutputStream;
     24 import java.io.IOException;
     25 import java.util.Arrays;
     26 import java.util.HashMap;
     27 import java.util.Iterator;
     28 import java.util.LinkedHashMap;
     29 import java.util.Map;
     30 import java.util.Set;
     31 import java.util.TreeSet;
     32 import java.util.logging.Level;
     33 import java.util.logging.Logger;
     34 import javax.net.ssl.SSLSession;
     35 
     36 /**
     37  * File-based cache implementation. Only one process should access the
     38  * underlying directory at a time.
     39  *
     40  * @hide
     41  */
     42 @Internal
     43 public final class FileClientSessionCache {
     44     private static final Logger logger = Logger.getLogger(FileClientSessionCache.class.getName());
     45 
     46     public static final int MAX_SIZE = 12; // ~72k
     47 
     48     private FileClientSessionCache() {}
     49 
     50     /**
     51      * This cache creates one file per SSL session using "host.port" for
     52      * the file name. Files are created or replaced when session data is put
     53      * in the cache (see {@link #putSessionData}). Files are read on
     54      * cache hits, but not on cache misses.
     55      *
     56      * <p>When the number of session files exceeds MAX_SIZE, we delete the
     57      * least-recently-used file. We don't current persist the last access time,
     58      * so the ordering actually ends up being least-recently-modified in some
     59      * cases and even just "not accessed in this process" if the filesystem
     60      * doesn't track last modified times.
     61      */
     62     static class Impl implements SSLClientSessionCache {
     63         /** Directory to store session files in. */
     64         final File directory;
     65 
     66         /**
     67          * Map of name -> File. Keeps track of the order files were accessed in.
     68          */
     69         Map<String, File> accessOrder = newAccessOrder();
     70 
     71         /** The number of files on disk. */
     72         int size;
     73 
     74         /**
     75          * The initial set of files. We use this to defer adding information
     76          * about all files to accessOrder until necessary.
     77          */
     78         String[] initialFiles;
     79 
     80         /**
     81          * Constructs a new cache backed by the given directory.
     82          */
     83         Impl(File directory) throws IOException {
     84             boolean exists = directory.exists();
     85             if (exists && !directory.isDirectory()) {
     86                 throw new IOException(directory + " exists but is not a directory.");
     87             }
     88 
     89             if (exists) {
     90                 // Read and sort initial list of files. We defer adding
     91                 // information about these files to accessOrder until necessary
     92                 // (see indexFiles()). Sorting the list enables us to detect
     93                 // cache misses in getSessionData().
     94                 // Note: Sorting an array here was faster than creating a
     95                 // HashSet on Dalvik.
     96                 initialFiles = directory.list();
     97                 if (initialFiles == null) {
     98                     // File.list() will return null in error cases without throwing IOException
     99                     // http://b/3363561
    100                     throw new IOException(directory + " exists but cannot list contents.");
    101                 }
    102                 Arrays.sort(initialFiles);
    103                 size = initialFiles.length;
    104             } else {
    105                 // Create directory.
    106                 if (!directory.mkdirs()) {
    107                     throw new IOException("Creation of " + directory + " directory failed.");
    108                 }
    109                 size = 0;
    110             }
    111 
    112             this.directory = directory;
    113         }
    114 
    115         /**
    116          * Creates a new access-ordered linked hash map.
    117          */
    118         private static Map<String, File> newAccessOrder() {
    119             return new LinkedHashMap<String, File>(MAX_SIZE, 0.75f, true /* access order */);
    120         }
    121 
    122         /**
    123          * Gets the file name for the given host and port.
    124          */
    125         private static String fileName(String host, int port) {
    126             if (host == null) {
    127                 throw new NullPointerException("host == null");
    128             }
    129             return host + "." + port;
    130         }
    131 
    132         @Override
    133         public synchronized byte[] getSessionData(String host, int port) {
    134             /*
    135              * Note: This method is only called when the in-memory cache
    136              * in SSLSessionContext misses, so it would be unnecessarily
    137              * redundant for this cache to store data in memory.
    138              */
    139 
    140             String name = fileName(host, port);
    141             File file = accessOrder.get(name);
    142 
    143             if (file == null) {
    144                 // File wasn't in access order. Check initialFiles...
    145                 if (initialFiles == null) {
    146                     // All files are in accessOrder, so it doesn't exist.
    147                     return null;
    148                 }
    149 
    150                 // Look in initialFiles.
    151                 if (Arrays.binarySearch(initialFiles, name) < 0) {
    152                     // Not found.
    153                     return null;
    154                 }
    155 
    156                 // The file is on disk but not in accessOrder yet.
    157                 file = new File(directory, name);
    158                 accessOrder.put(name, file);
    159             }
    160 
    161             FileInputStream in;
    162             try {
    163                 in = new FileInputStream(file);
    164             } catch (FileNotFoundException e) {
    165                 logReadError(host, file, e);
    166                 return null;
    167             }
    168             try {
    169                 int size = (int) file.length();
    170                 byte[] data = new byte[size];
    171                 new DataInputStream(in).readFully(data);
    172                 return data;
    173             } catch (IOException e) {
    174                 logReadError(host, file, e);
    175                 return null;
    176             } finally {
    177                 if (in != null) {
    178                     try {
    179                         in.close();
    180                     } catch (Exception ignored) {
    181                     }
    182                 }
    183             }
    184         }
    185 
    186         static void logReadError(String host, File file, Throwable t) {
    187             logger.log(Level.WARNING,
    188                     "FileClientSessionCache: Error reading session data for " + host + " from "
    189                             + file + ".",
    190                     t);
    191         }
    192 
    193         @Override
    194         public synchronized void putSessionData(SSLSession session, byte[] sessionData) {
    195             String host = session.getPeerHost();
    196             if (sessionData == null) {
    197                 throw new NullPointerException("sessionData == null");
    198             }
    199 
    200             String name = fileName(host, session.getPeerPort());
    201             File file = new File(directory, name);
    202 
    203             // Used to keep track of whether or not we're expanding the cache.
    204             boolean existedBefore = file.exists();
    205 
    206             FileOutputStream out;
    207             try {
    208                 out = new FileOutputStream(file);
    209             } catch (FileNotFoundException e) {
    210                 // We can't write to the file.
    211                 logWriteError(host, file, e);
    212                 return;
    213             }
    214 
    215             // If we expanded the cache (by creating a new file)...
    216             if (!existedBefore) {
    217                 size++;
    218 
    219                 // Delete an old file if necessary.
    220                 makeRoom();
    221             }
    222 
    223             boolean writeSuccessful = false;
    224             try {
    225                 out.write(sessionData);
    226                 writeSuccessful = true;
    227             } catch (IOException e) {
    228                 logWriteError(host, file, e);
    229             } finally {
    230                 boolean closeSuccessful = false;
    231                 try {
    232                     out.close();
    233                     closeSuccessful = true;
    234                 } catch (IOException e) {
    235                     logWriteError(host, file, e);
    236                 } finally {
    237                     if (!writeSuccessful || !closeSuccessful) {
    238                         // Storage failed. Clean up.
    239                         delete(file);
    240                     } else {
    241                         // Success!
    242                         accessOrder.put(name, file);
    243                     }
    244                 }
    245             }
    246         }
    247 
    248         /**
    249          * Deletes old files if necessary.
    250          */
    251         private void makeRoom() {
    252             if (size <= MAX_SIZE) {
    253                 return;
    254             }
    255 
    256             indexFiles();
    257 
    258             // Delete LRUed files.
    259             int removals = size - MAX_SIZE;
    260             Iterator<File> i = accessOrder.values().iterator();
    261             do {
    262                 delete(i.next());
    263                 i.remove();
    264             } while (--removals > 0);
    265         }
    266 
    267         /**
    268          * Lazily updates accessOrder to know about all files as opposed to
    269          * just the files accessed since this process started.
    270          */
    271         private void indexFiles() {
    272             String[] initialFiles = this.initialFiles;
    273             if (initialFiles != null) {
    274                 this.initialFiles = null;
    275 
    276                 // Files on disk only, sorted by last modified time.
    277                 // TODO: Use last access time.
    278                 Set<CacheFile> diskOnly = new TreeSet<CacheFile>();
    279                 for (String name : initialFiles) {
    280                     // If the file hasn't been accessed in this process...
    281                     if (!accessOrder.containsKey(name)) {
    282                         diskOnly.add(new CacheFile(directory, name));
    283                     }
    284                 }
    285 
    286                 if (!diskOnly.isEmpty()) {
    287                     // Add files not accessed in this process to the beginning
    288                     // of accessOrder.
    289                     Map<String, File> newOrder = newAccessOrder();
    290                     for (CacheFile cacheFile : diskOnly) {
    291                         newOrder.put(cacheFile.name, cacheFile);
    292                     }
    293                     newOrder.putAll(accessOrder);
    294 
    295                     this.accessOrder = newOrder;
    296                 }
    297             }
    298         }
    299 
    300         @SuppressWarnings("ThrowableInstanceNeverThrown")
    301         private void delete(File file) {
    302             if (!file.delete()) {
    303                 Exception e =
    304                         new IOException("FileClientSessionCache: Failed to delete " + file + ".");
    305                 logger.log(Level.WARNING, e.getMessage(), e);
    306             }
    307             size--;
    308         }
    309 
    310         static void logWriteError(String host, File file, Throwable t) {
    311             logger.log(Level.WARNING,
    312                     "FileClientSessionCache: Error writing session data for " + host + " to " + file
    313                             + ".",
    314                     t);
    315         }
    316     }
    317 
    318     /**
    319      * Maps directories to the cache instances that are backed by those
    320      * directories. We synchronize access using the cache instance, so it's
    321      * important that everyone shares the same instance.
    322      */
    323     static final Map<File, FileClientSessionCache.Impl> caches =
    324             new HashMap<File, FileClientSessionCache.Impl>();
    325 
    326     /**
    327      * Returns a cache backed by the given directory. Creates the directory
    328      * (including parent directories) if necessary. This cache should have
    329      * exclusive access to the given directory.
    330      *
    331      * @param directory to store files in
    332      * @return a cache backed by the given directory
    333      * @throws IOException if the file exists and is not a directory or if
    334      *  creating the directories fails
    335      */
    336     public static synchronized SSLClientSessionCache usingDirectory(File directory)
    337             throws IOException {
    338         FileClientSessionCache.Impl cache = caches.get(directory);
    339         if (cache == null) {
    340             cache = new FileClientSessionCache.Impl(directory);
    341             caches.put(directory, cache);
    342         }
    343         return cache;
    344     }
    345 
    346     /** For testing. */
    347     static synchronized void reset() {
    348         caches.clear();
    349     }
    350 
    351     /** A file containing a piece of cached data. */
    352     @SuppressWarnings("serial")
    353     static class CacheFile extends File {
    354         final String name;
    355 
    356         CacheFile(File dir, String name) {
    357             super(dir, name);
    358             this.name = name;
    359         }
    360 
    361         long lastModified = -1;
    362 
    363         @Override
    364         public long lastModified() {
    365             long lastModified = this.lastModified;
    366             if (lastModified == -1) {
    367                 lastModified = this.lastModified = super.lastModified();
    368             }
    369             return lastModified;
    370         }
    371 
    372         @Override
    373         public int compareTo(File another) {
    374             // Sort by last modified time.
    375             long result = lastModified() - another.lastModified();
    376             if (result == 0) {
    377                 return super.compareTo(another);
    378             }
    379             return result < 0 ? -1 : 1;
    380         }
    381     }
    382 }
    383