| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.conscrypt; |
| |
| import java.io.DataInputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import javax.net.ssl.SSLSession; |
| |
| /** |
| * File-based cache implementation. Only one process should access the |
| * underlying directory at a time. |
| */ |
| public class FileClientSessionCache { |
| public static final int MAX_SIZE = 12; // ~72k |
| |
| private FileClientSessionCache() {} |
| |
| /** |
| * This cache creates one file per SSL session using "host.port" for |
| * the file name. Files are created or replaced when session data is put |
| * in the cache (see {@link #putSessionData}). Files are read on |
| * cache hits, but not on cache misses. |
| * |
| * <p>When the number of session files exceeds MAX_SIZE, we delete the |
| * least-recently-used file. We don't current persist the last access time, |
| * so the ordering actually ends up being least-recently-modified in some |
| * cases and even just "not accessed in this process" if the filesystem |
| * doesn't track last modified times. |
| */ |
| static class Impl implements SSLClientSessionCache { |
| |
| /** Directory to store session files in. */ |
| final File directory; |
| |
| /** |
| * Map of name -> File. Keeps track of the order files were accessed in. |
| */ |
| Map<String, File> accessOrder = newAccessOrder(); |
| |
| /** The number of files on disk. */ |
| int size; |
| |
| /** |
| * The initial set of files. We use this to defer adding information |
| * about all files to accessOrder until necessary. |
| */ |
| String[] initialFiles; |
| |
| /** |
| * Constructs a new cache backed by the given directory. |
| */ |
| Impl(File directory) throws IOException { |
| boolean exists = directory.exists(); |
| if (exists && !directory.isDirectory()) { |
| throw new IOException(directory + " exists but is not a directory."); |
| } |
| |
| if (exists) { |
| // Read and sort initial list of files. We defer adding |
| // information about these files to accessOrder until necessary |
| // (see indexFiles()). Sorting the list enables us to detect |
| // cache misses in getSessionData(). |
| // Note: Sorting an array here was faster than creating a |
| // HashSet on Dalvik. |
| initialFiles = directory.list(); |
| if (initialFiles == null) { |
| // File.list() will return null in error cases without throwing IOException |
| // http://b/3363561 |
| throw new IOException(directory + " exists but cannot list contents."); |
| } |
| Arrays.sort(initialFiles); |
| size = initialFiles.length; |
| } else { |
| // Create directory. |
| if (!directory.mkdirs()) { |
| throw new IOException("Creation of " + directory + " directory failed."); |
| } |
| size = 0; |
| } |
| |
| this.directory = directory; |
| } |
| |
| /** |
| * Creates a new access-ordered linked hash map. |
| */ |
| private static Map<String, File> newAccessOrder() { |
| return new LinkedHashMap<String, File>( |
| MAX_SIZE, 0.75f, true /* access order */); |
| } |
| |
| /** |
| * Gets the file name for the given host and port. |
| */ |
| private static String fileName(String host, int port) { |
| if (host == null) { |
| throw new NullPointerException("host == null"); |
| } |
| return host + "." + port; |
| } |
| |
| @Override |
| public synchronized byte[] getSessionData(String host, int port) { |
| /* |
| * Note: This method is only called when the in-memory cache |
| * in SSLSessionContext misses, so it would be unnecessarily |
| * redundant for this cache to store data in memory. |
| */ |
| |
| String name = fileName(host, port); |
| File file = accessOrder.get(name); |
| |
| if (file == null) { |
| // File wasn't in access order. Check initialFiles... |
| if (initialFiles == null) { |
| // All files are in accessOrder, so it doesn't exist. |
| return null; |
| } |
| |
| // Look in initialFiles. |
| if (Arrays.binarySearch(initialFiles, name) < 0) { |
| // Not found. |
| return null; |
| } |
| |
| // The file is on disk but not in accessOrder yet. |
| file = new File(directory, name); |
| accessOrder.put(name, file); |
| } |
| |
| FileInputStream in; |
| try { |
| in = new FileInputStream(file); |
| } catch (FileNotFoundException e) { |
| logReadError(host, file, e); |
| return null; |
| } |
| try { |
| int size = (int) file.length(); |
| byte[] data = new byte[size]; |
| new DataInputStream(in).readFully(data); |
| return data; |
| } catch (IOException e) { |
| logReadError(host, file, e); |
| return null; |
| } finally { |
| if (in != null) { |
| try { |
| in.close(); |
| } catch (RuntimeException rethrown) { |
| throw rethrown; |
| } catch (Exception ignored) { |
| } |
| } |
| } |
| } |
| |
| static void logReadError(String host, File file, Throwable t) { |
| System.err.println("FileClientSessionCache: Error reading session data for " + host + " from " + file + "."); |
| t.printStackTrace(); |
| } |
| |
| @Override |
| public synchronized void putSessionData(SSLSession session, |
| byte[] sessionData) { |
| String host = session.getPeerHost(); |
| if (sessionData == null) { |
| throw new NullPointerException("sessionData == null"); |
| } |
| |
| String name = fileName(host, session.getPeerPort()); |
| File file = new File(directory, name); |
| |
| // Used to keep track of whether or not we're expanding the cache. |
| boolean existedBefore = file.exists(); |
| |
| FileOutputStream out; |
| try { |
| out = new FileOutputStream(file); |
| } catch (FileNotFoundException e) { |
| // We can't write to the file. |
| logWriteError(host, file, e); |
| return; |
| } |
| |
| // If we expanded the cache (by creating a new file)... |
| if (!existedBefore) { |
| size++; |
| |
| // Delete an old file if necessary. |
| makeRoom(); |
| } |
| |
| boolean writeSuccessful = false; |
| try { |
| out.write(sessionData); |
| writeSuccessful = true; |
| } catch (IOException e) { |
| logWriteError(host, file, e); |
| } finally { |
| boolean closeSuccessful = false; |
| try { |
| out.close(); |
| closeSuccessful = true; |
| } catch (IOException e) { |
| logWriteError(host, file, e); |
| } finally { |
| if (!writeSuccessful || !closeSuccessful) { |
| // Storage failed. Clean up. |
| delete(file); |
| } else { |
| // Success! |
| accessOrder.put(name, file); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Deletes old files if necessary. |
| */ |
| private void makeRoom() { |
| if (size <= MAX_SIZE) { |
| return; |
| } |
| |
| indexFiles(); |
| |
| // Delete LRUed files. |
| int removals = size - MAX_SIZE; |
| Iterator<File> i = accessOrder.values().iterator(); |
| do { |
| delete(i.next()); |
| i.remove(); |
| } while (--removals > 0); |
| } |
| |
| /** |
| * Lazily updates accessOrder to know about all files as opposed to |
| * just the files accessed since this process started. |
| */ |
| private void indexFiles() { |
| String[] initialFiles = this.initialFiles; |
| if (initialFiles != null) { |
| this.initialFiles = null; |
| |
| // Files on disk only, sorted by last modified time. |
| // TODO: Use last access time. |
| Set<CacheFile> diskOnly = new TreeSet<CacheFile>(); |
| for (String name : initialFiles) { |
| // If the file hasn't been accessed in this process... |
| if (!accessOrder.containsKey(name)) { |
| diskOnly.add(new CacheFile(directory, name)); |
| } |
| } |
| |
| if (!diskOnly.isEmpty()) { |
| // Add files not accessed in this process to the beginning |
| // of accessOrder. |
| Map<String, File> newOrder = newAccessOrder(); |
| for (CacheFile cacheFile : diskOnly) { |
| newOrder.put(cacheFile.name, cacheFile); |
| } |
| newOrder.putAll(accessOrder); |
| |
| this.accessOrder = newOrder; |
| } |
| } |
| } |
| |
| @SuppressWarnings("ThrowableInstanceNeverThrown") |
| private void delete(File file) { |
| if (!file.delete()) { |
| new IOException("FileClientSessionCache: Failed to delete " + file + ".").printStackTrace(); |
| } |
| size--; |
| } |
| |
| static void logWriteError(String host, File file, Throwable t) { |
| System.err.println("FileClientSessionCache: Error writing session data for " + host + " to " + file + "."); |
| t.printStackTrace(); |
| } |
| } |
| |
| /** |
| * Maps directories to the cache instances that are backed by those |
| * directories. We synchronize access using the cache instance, so it's |
| * important that everyone shares the same instance. |
| */ |
| static final Map<File, FileClientSessionCache.Impl> caches |
| = new HashMap<File, FileClientSessionCache.Impl>(); |
| |
| /** |
| * Returns a cache backed by the given directory. Creates the directory |
| * (including parent directories) if necessary. This cache should have |
| * exclusive access to the given directory. |
| * |
| * @param directory to store files in |
| * @return a cache backed by the given directory |
| * @throws IOException if the file exists and is not a directory or if |
| * creating the directories fails |
| */ |
| public static synchronized SSLClientSessionCache usingDirectory( |
| File directory) throws IOException { |
| FileClientSessionCache.Impl cache = caches.get(directory); |
| if (cache == null) { |
| cache = new FileClientSessionCache.Impl(directory); |
| caches.put(directory, cache); |
| } |
| return cache; |
| } |
| |
| /** For testing. */ |
| static synchronized void reset() { |
| caches.clear(); |
| } |
| |
| /** A file containing a piece of cached data. */ |
| static class CacheFile extends File { |
| |
| final String name; |
| |
| CacheFile(File dir, String name) { |
| super(dir, name); |
| this.name = name; |
| } |
| |
| long lastModified = -1; |
| |
| @Override |
| public long lastModified() { |
| long lastModified = this.lastModified; |
| if (lastModified == -1) { |
| lastModified = this.lastModified = super.lastModified(); |
| } |
| return lastModified; |
| } |
| |
| @Override |
| public int compareTo(File another) { |
| // Sort by last modified time. |
| long result = lastModified() - another.lastModified(); |
| if (result == 0) { |
| return super.compareTo(another); |
| } |
| return result < 0 ? -1 : 1; |
| } |
| } |
| } |