FileClientSessionCache.java revision 86acc043d3334651ee26c65467d78d6cefedd397
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 javax.net.ssl.SSLSession;
33import libcore.io.IoUtils;
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    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