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 17package org.apache.harmony.xnet.provider.jsse; 18 19import javax.net.ssl.SSLSession; 20import java.util.Map; 21import java.util.HashMap; 22import java.util.LinkedHashMap; 23import java.util.Set; 24import java.util.TreeSet; 25import java.util.Iterator; 26import java.util.Arrays; 27import java.util.logging.Level; 28import java.io.DataInputStream; 29import java.io.File; 30import java.io.FileInputStream; 31import java.io.FileNotFoundException; 32import java.io.FileOutputStream; 33import java.io.IOException; 34 35/** 36 * File-based cache implementation. Only one process should access the 37 * underlying directory at a time. 38 */ 39public 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}