ClientSessionContext.java revision 6b811c5daec1b28e6f63b57f98a032236f2c3cf7
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.util.Iterator;
20import java.util.LinkedHashMap;
21import java.util.Map;
22import java.util.HashMap;
23import java.util.ArrayList;
24import java.util.Arrays;
25
26import javax.net.ssl.SSLSession;
27
28/**
29 * Caches client sessions. Indexes by host and port. Users are typically
30 * looking to reuse any session for a given host and port. Users of the
31 * standard API are forced to iterate over the sessions semi-linearly as
32 * opposed to in constant time.
33 */
34public class ClientSessionContext extends AbstractSessionContext {
35
36    /*
37     * We don't care about timeouts in the client implementation. Trying
38     * to reuse an expired session and having to start a new one requires no
39     * more effort than starting a new one, so you might as well try to reuse
40     * one on the off chance it's still valid.
41     */
42
43    /** Sessions indexed by host and port in access order. */
44    final Map<HostAndPort, SSLSession> sessions
45            = new LinkedHashMap<HostAndPort, SSLSession>() {
46        @Override
47        protected boolean removeEldestEntry(
48                Map.Entry<HostAndPort, SSLSession> eldest) {
49            // Called while lock is held on sessions.
50            boolean remove = maximumSize > 0 && size() > maximumSize;
51            if (remove) {
52                removeById(eldest.getValue());
53            }
54            return remove;
55        }
56    };
57
58    /**
59     * Sessions indexed by ID. Initialized on demand. Protected from concurrent
60     * access by holding a lock on sessions.
61     */
62    Map<ByteArray, SSLSession> sessionsById;
63
64    final SSLClientSessionCache persistentCache;
65
66    public ClientSessionContext(int sslCtxNativePointer,
67                                SSLClientSessionCache persistentCache) {
68        super(sslCtxNativePointer, 10, 0);
69        this.persistentCache = persistentCache;
70    }
71
72    public final void setSessionTimeout(int seconds)
73            throws IllegalArgumentException {
74        if (seconds < 0) {
75            throw new IllegalArgumentException("seconds < 0");
76        }
77        timeout = seconds;
78    }
79
80    Iterator<SSLSession> sessionIterator() {
81        synchronized (sessions) {
82            SSLSession[] array = sessions.values().toArray(
83                    new SSLSession[sessions.size()]);
84            return Arrays.asList(array).iterator();
85        }
86    }
87
88    void trimToSize() {
89        synchronized (sessions) {
90            int size = sessions.size();
91            if (size > maximumSize) {
92                int removals = size - maximumSize;
93                Iterator<SSLSession> i = sessions.values().iterator();
94                do {
95                    removeById(i.next());
96                    i.remove();
97                } while (--removals > 0);
98            }
99        }
100    }
101
102    void removeById(SSLSession session) {
103        if (sessionsById != null) {
104            sessionsById.remove(new ByteArray(session.getId()));
105        }
106    }
107
108    /**
109     * {@inheritDoc}
110     *
111     * @see #getSession(String, int) for an implementation-specific but more
112     *  efficient approach
113     */
114    public SSLSession getSession(byte[] sessionId) {
115        /*
116         * This method is typically used in conjunction with getIds() to
117         * iterate over the sessions linearly, so it doesn't make sense for
118         * it to impact access order.
119         *
120         * It also doesn't load sessions from the persistent cache as doing
121         * so would likely force every session to load.
122         */
123
124        ByteArray id = new ByteArray(sessionId);
125        synchronized (sessions) {
126            indexById();
127            return sessionsById.get(id);
128        }
129    }
130
131    /**
132     * Ensures that the ID-based index is initialized.
133     */
134    private void indexById() {
135        if (sessionsById == null) {
136            sessionsById = new HashMap<ByteArray, SSLSession>();
137            for (SSLSession session : sessions.values()) {
138                sessionsById.put(new ByteArray(session.getId()), session);
139            }
140        }
141    }
142
143    /**
144     * Adds the given session to the ID-based index if the index has already
145     * been initialized.
146     */
147    private void indexById(byte[] id, SSLSession session) {
148        if (sessionsById != null) {
149            sessionsById.put(new ByteArray(id), session);
150        }
151    }
152
153    /**
154     * Finds a cached session for the given host name and port.
155     *
156     * @param host of server
157     * @param port of server
158     * @return cached session or null if none found
159     */
160    public SSLSession getSession(String host, int port) {
161        synchronized (sessions) {
162            SSLSession session = sessions.get(new HostAndPort(host, port));
163            if (session != null) {
164                return session;
165            }
166        }
167
168        // Look in persistent cache.
169        if (persistentCache != null) {
170            byte[] data = persistentCache.getSessionData(host, port);
171            if (data != null) {
172                SSLSession session = toSession(data, host, port);
173                if (session != null) {
174                    synchronized (sessions) {
175                        sessions.put(new HostAndPort(host, port), session);
176                        indexById(session.getId(), session);
177                    }
178                    return session;
179                }
180            }
181        }
182
183        return null;
184    }
185
186    @Override
187    void putSession(SSLSession session) {
188        byte[] id = session.getId();
189        if (id.length == 0) {
190            return;
191        }
192        HostAndPort key = new HostAndPort(session.getPeerHost(),
193                session.getPeerPort());
194        synchronized (sessions) {
195            sessions.put(key, session);
196            indexById(id, session);
197        }
198
199        // TODO: This in a background thread.
200        if (persistentCache != null) {
201            byte[] data = toBytes(session);
202            if (data != null) {
203                persistentCache.putSessionData(session, data);
204            }
205        }
206    }
207
208    static class HostAndPort {
209        final String host;
210        final int port;
211
212        HostAndPort(String host, int port) {
213            this.host = host;
214            this.port = port;
215        }
216
217        @Override
218        public int hashCode() {
219            return host.hashCode() * 31 + port;
220        }
221
222        @Override
223        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
224        public boolean equals(Object o) {
225            HostAndPort other = (HostAndPort) o;
226            return host.equals(other.host) && port == other.port;
227        }
228    }
229}
230