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 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 == null"); 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 == null"); 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