FileClientSessionCache.java revision 7365de1056414750d0a7d1fdd26025fd247f0d04
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 java.io.DataInputStream;
20import java.io.File;
21import java.io.FileInputStream;
22import java.io.FileNotFoundException;
23import java.io.FileOutputStream;
24import java.io.IOException;
25import java.util.Arrays;
26import java.util.HashMap;
27import java.util.Iterator;
28import java.util.LinkedHashMap;
29import java.util.Map;
30import java.util.Set;
31import java.util.TreeSet;
32import java.util.logging.Level;
33import javax.net.ssl.SSLSession;
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}
375