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