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