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 17 package org.conscrypt; 18 19 import java.io.ByteArrayInputStream; 20 import java.io.ByteArrayOutputStream; 21 import java.io.DataInputStream; 22 import java.io.DataOutputStream; 23 import java.io.IOException; 24 import java.security.cert.Certificate; 25 import java.security.cert.CertificateEncodingException; 26 import java.security.cert.X509Certificate; 27 import java.util.Arrays; 28 import java.util.Enumeration; 29 import java.util.Iterator; 30 import java.util.LinkedHashMap; 31 import java.util.Map; 32 import java.util.NoSuchElementException; 33 import javax.net.ssl.SSLSession; 34 import javax.net.ssl.SSLSessionContext; 35 36 /** 37 * Supports SSL session caches. 38 */ 39 abstract class AbstractSessionContext implements SSLSessionContext { 40 41 /** 42 * Maximum lifetime of a session (in seconds) after which it's considered invalid and should not 43 * be used to for new connections. 44 */ 45 private static final int DEFAULT_SESSION_TIMEOUT_SECONDS = 8 * 60 * 60; 46 47 volatile int maximumSize; 48 volatile int timeout = DEFAULT_SESSION_TIMEOUT_SECONDS; 49 50 final long sslCtxNativePointer = NativeCrypto.SSL_CTX_new(); 51 52 /** Identifies OpenSSL sessions. */ 53 static final int OPEN_SSL = 1; 54 55 private final Map<ByteArray, SSLSession> sessions 56 = new LinkedHashMap<ByteArray, SSLSession>() { 57 @Override 58 protected boolean removeEldestEntry( 59 Map.Entry<ByteArray, SSLSession> eldest) { 60 boolean remove = maximumSize > 0 && size() > maximumSize; 61 if (remove) { 62 remove(eldest.getKey()); 63 sessionRemoved(eldest.getValue()); 64 } 65 return false; 66 } 67 }; 68 69 /** 70 * Constructs a new session context. 71 * 72 * @param maximumSize of cache 73 */ AbstractSessionContext(int maximumSize)74 AbstractSessionContext(int maximumSize) { 75 this.maximumSize = maximumSize; 76 } 77 78 /** 79 * Returns the collection of sessions ordered from oldest to newest 80 */ sessionIterator()81 private Iterator<SSLSession> sessionIterator() { 82 synchronized (sessions) { 83 SSLSession[] array = sessions.values().toArray( 84 new SSLSession[sessions.size()]); 85 return Arrays.asList(array).iterator(); 86 } 87 } 88 89 @Override getIds()90 public final Enumeration<byte[]> getIds() { 91 final Iterator<SSLSession> i = sessionIterator(); 92 return new Enumeration<byte[]>() { 93 private SSLSession next; 94 95 @Override 96 public boolean hasMoreElements() { 97 if (next != null) { 98 return true; 99 } 100 while (i.hasNext()) { 101 SSLSession session = i.next(); 102 if (session.isValid()) { 103 next = session; 104 return true; 105 } 106 } 107 next = null; 108 return false; 109 } 110 111 @Override 112 public byte[] nextElement() { 113 if (hasMoreElements()) { 114 byte[] id = next.getId(); 115 next = null; 116 return id; 117 } 118 throw new NoSuchElementException(); 119 } 120 }; 121 } 122 123 @Override getSessionCacheSize()124 public final int getSessionCacheSize() { 125 return maximumSize; 126 } 127 128 @Override getSessionTimeout()129 public final int getSessionTimeout() { 130 return timeout; 131 } 132 133 /** 134 * Makes sure cache size is < maximumSize. 135 */ trimToSize()136 protected void trimToSize() { 137 synchronized (sessions) { 138 int size = sessions.size(); 139 if (size > maximumSize) { 140 int removals = size - maximumSize; 141 Iterator<SSLSession> i = sessions.values().iterator(); 142 do { 143 SSLSession session = i.next(); 144 i.remove(); 145 sessionRemoved(session); 146 } while (--removals > 0); 147 } 148 } 149 } 150 151 @Override setSessionTimeout(int seconds)152 public void setSessionTimeout(int seconds) 153 throws IllegalArgumentException { 154 if (seconds < 0) { 155 throw new IllegalArgumentException("seconds < 0"); 156 } 157 timeout = seconds; 158 159 synchronized (sessions) { 160 Iterator<SSLSession> i = sessions.values().iterator(); 161 while (i.hasNext()) { 162 SSLSession session = i.next(); 163 // SSLSession's know their context and consult the 164 // timeout as part of their validity condition. 165 if (!session.isValid()) { 166 i.remove(); 167 sessionRemoved(session); 168 } 169 } 170 } 171 } 172 173 /** 174 * Called when a session is removed. Used by ClientSessionContext 175 * to update its host-and-port based cache. 176 */ sessionRemoved(SSLSession session)177 protected abstract void sessionRemoved(SSLSession session); 178 179 @Override setSessionCacheSize(int size)180 public final void setSessionCacheSize(int size) 181 throws IllegalArgumentException { 182 if (size < 0) { 183 throw new IllegalArgumentException("size < 0"); 184 } 185 186 int oldMaximum = maximumSize; 187 maximumSize = size; 188 189 // Trim cache to size if necessary. 190 if (size < oldMaximum) { 191 trimToSize(); 192 } 193 } 194 195 /** 196 * Converts the given session to bytes. 197 * 198 * @return session data as bytes or null if the session can't be converted 199 */ toBytes(SSLSession session)200 byte[] toBytes(SSLSession session) { 201 // TODO: Support SSLSessionImpl, too. 202 if (!(session instanceof OpenSSLSessionImpl)) { 203 return null; 204 } 205 206 OpenSSLSessionImpl sslSession = (OpenSSLSessionImpl) session; 207 try { 208 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 209 DataOutputStream daos = new DataOutputStream(baos); 210 211 daos.writeInt(OPEN_SSL); // session type ID 212 213 // Session data. 214 byte[] data = sslSession.getEncoded(); 215 daos.writeInt(data.length); 216 daos.write(data); 217 218 // Certificates. 219 Certificate[] certs = session.getPeerCertificates(); 220 daos.writeInt(certs.length); 221 222 for (Certificate cert : certs) { 223 data = cert.getEncoded(); 224 daos.writeInt(data.length); 225 daos.write(data); 226 } 227 // TODO: local certificates? 228 229 return baos.toByteArray(); 230 } catch (IOException e) { 231 System.err.println("Failed to convert saved SSL Session: " + e.getMessage()); 232 return null; 233 } catch (CertificateEncodingException e) { 234 log(e); 235 return null; 236 } 237 } 238 239 /** 240 * Creates a session from the given bytes. 241 * 242 * @return a session or null if the session can't be converted 243 */ toSession(byte[] data, String host, int port)244 OpenSSLSessionImpl toSession(byte[] data, String host, int port) { 245 ByteArrayInputStream bais = new ByteArrayInputStream(data); 246 DataInputStream dais = new DataInputStream(bais); 247 try { 248 int type = dais.readInt(); 249 if (type != OPEN_SSL) { 250 log(new AssertionError("Unexpected type ID: " + type)); 251 return null; 252 } 253 254 int length = dais.readInt(); 255 byte[] sessionData = new byte[length]; 256 dais.readFully(sessionData); 257 258 int count = dais.readInt(); 259 X509Certificate[] certs = new X509Certificate[count]; 260 for (int i = 0; i < count; i++) { 261 length = dais.readInt(); 262 byte[] certData = new byte[length]; 263 dais.readFully(certData); 264 certs[i] = OpenSSLX509Certificate.fromX509Der(certData); 265 } 266 267 return new OpenSSLSessionImpl(sessionData, host, port, certs, this); 268 } catch (IOException e) { 269 log(e); 270 return null; 271 } 272 } 273 wrapSSLSessionIfNeeded(SSLSession session)274 protected SSLSession wrapSSLSessionIfNeeded(SSLSession session) { 275 if (session instanceof OpenSSLSessionImpl) { 276 return Platform.wrapSSLSession((OpenSSLSessionImpl) session); 277 } else { 278 return session; 279 } 280 } 281 282 @Override getSession(byte[] sessionId)283 public SSLSession getSession(byte[] sessionId) { 284 if (sessionId == null) { 285 throw new NullPointerException("sessionId == null"); 286 } 287 ByteArray key = new ByteArray(sessionId); 288 SSLSession session; 289 synchronized (sessions) { 290 session = sessions.get(key); 291 } 292 if (session != null && session.isValid()) { 293 if (session instanceof OpenSSLSessionImpl) { 294 return Platform.wrapSSLSession((OpenSSLSessionImpl) session); 295 } else { 296 return session; 297 } 298 } 299 return null; 300 } 301 putSession(SSLSession session)302 void putSession(SSLSession session) { 303 byte[] id = session.getId(); 304 if (id.length == 0) { 305 return; 306 } 307 ByteArray key = new ByteArray(id); 308 synchronized (sessions) { 309 sessions.put(key, session); 310 } 311 } 312 log(Throwable t)313 static void log(Throwable t) { 314 new Exception("Error converting session", t).printStackTrace(); 315 } 316 317 @Override finalize()318 protected void finalize() throws Throwable { 319 try { 320 NativeCrypto.SSL_CTX_free(sslCtxNativePointer); 321 } finally { 322 super.finalize(); 323 } 324 } 325 } 326