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