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