• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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