Merge "Remove SSLPerformanceTest and DatabaseSessionCache."

This commit is contained in:
Jesse Wilson
2011-02-10 13:44:56 -08:00
committed by Android (Google) Code Review
2 changed files with 0 additions and 744 deletions

View File

@@ -1,312 +0,0 @@
// Copyright 2009 The Android Open Source Project
package android.core;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import android.content.ContentValues;
import android.content.Context;
import org.apache.commons.codec.binary.Base64;
import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.net.ssl.SSLSession;
/**
* Hook into harmony SSL cache to persist the SSL sessions.
*
* Current implementation is suitable for saving a small number of hosts -
* like google services. It can be extended with expiration and more features
* to support more hosts.
*
* {@hide}
*/
public class DatabaseSessionCache implements SSLClientSessionCache {
private static final String TAG = "SslSessionCache";
static DatabaseHelper sDefaultDatabaseHelper;
private DatabaseHelper mDatabaseHelper;
/**
* Table where sessions are stored.
*/
public static final String SSL_CACHE_TABLE = "ssl_sessions";
private static final String SSL_CACHE_ID = "_id";
/**
* Key is host:port - port is not optional.
*/
private static final String SSL_CACHE_HOSTPORT = "hostport";
/**
* Base64-encoded DER value of the session.
*/
private static final String SSL_CACHE_SESSION = "session";
/**
* Time when the record was added - should be close to the time
* of the initial session negotiation.
*/
private static final String SSL_CACHE_TIME_SEC = "time_sec";
public static final String DATABASE_NAME = "ssl_sessions.db";
public static final int DATABASE_VERSION = 1;
/** public for testing
*/
public static final int SSL_CACHE_ID_COL = 0;
public static final int SSL_CACHE_HOSTPORT_COL = 1;
public static final int SSL_CACHE_SESSION_COL = 2;
public static final int SSL_CACHE_TIME_SEC_COL = 3;
private static final String SAVE_ON_ADD = "save_on_add";
static boolean sHookInitializationDone = false;
public static final int MAX_CACHE_SIZE = 256;
private static final Map<String, byte[]> mExternalCache =
new LinkedHashMap<String, byte[]>(MAX_CACHE_SIZE, 0.75f, true) {
@Override
public boolean removeEldestEntry(
Map.Entry<String, byte[]> eldest) {
boolean shouldDelete = this.size() > MAX_CACHE_SIZE;
// TODO: delete from DB
return shouldDelete;
}
};
static boolean mNeedsCacheLoad = true;
public static final String[] PROJECTION = new String[] {
SSL_CACHE_ID,
SSL_CACHE_HOSTPORT,
SSL_CACHE_SESSION,
SSL_CACHE_TIME_SEC
};
/**
* This class needs to be installed as a hook, if the security property
* is set. Getting the right classloader may be fun since we don't use
* Provider to get its classloader, but in android this is in same
* loader with AndroidHttpClient.
*
* This constructor will use the default database. You must
* call init() before to specify the context used for the database and
* check settings.
*/
public DatabaseSessionCache() {
Log.v(TAG, "Instance created.");
// May be null if caching is disabled - no sessions will be persisted.
this.mDatabaseHelper = sDefaultDatabaseHelper;
}
/**
* Create a SslSessionCache instance, using the specified context to
* initialize the database.
*
* This constructor will use the default database - created the first
* time.
*
* @param activityContext
*/
public DatabaseSessionCache(Context activityContext) {
// Static init - only one initialization will happen.
// Each SslSessionCache is using the same DB.
init(activityContext);
// May be null if caching is disabled - no sessions will be persisted.
this.mDatabaseHelper = sDefaultDatabaseHelper;
}
/**
* Create a SslSessionCache that uses a specific database.
*
* @param database
*/
public DatabaseSessionCache(DatabaseHelper database) {
this.mDatabaseHelper = database;
}
// public static boolean enabled(Context androidContext) {
// String sslCache = Settings.Secure.getString(androidContext.getContentResolver(),
// Settings.Secure.SSL_SESSION_CACHE);
//
// if (Log.isLoggable(TAG, Log.DEBUG)) {
// Log.d(TAG, "enabled " + sslCache + " " + androidContext.getPackageName());
// }
//
// return SAVE_ON_ADD.equals(sslCache);
// }
/**
* You must call this method to enable SSL session caching for an app.
*/
public synchronized static void init(Context activityContext) {
// It is possible that multiple provider will try to install this hook.
// We want a single db per VM.
if (sHookInitializationDone) {
return;
}
// // More values can be added in future to provide different
// // behaviours, like 'batch save'.
// if (enabled(activityContext)) {
Context appContext = activityContext.getApplicationContext();
sDefaultDatabaseHelper = new DatabaseHelper(appContext);
// Set default SSLSocketFactory
// The property is defined in the javadocs for javax.net.SSLSocketFactory
// (no constant defined there)
// This should cover all code using SSLSocketFactory.getDefault(),
// including native http client and apache httpclient.
// MCS is using its own custom factory - will need special code.
// Security.setProperty("ssl.SocketFactory.provider",
// SslSocketFactoryWithCache.class.getName());
// }
// Won't try again.
sHookInitializationDone = true;
}
public void putSessionData(SSLSession session, byte[] der) {
if (mDatabaseHelper == null) {
return;
}
if (mExternalCache.size() > MAX_CACHE_SIZE) {
// remove oldest.
Cursor byTime = mDatabaseHelper.getWritableDatabase().query(SSL_CACHE_TABLE,
PROJECTION, null, null, null, null, SSL_CACHE_TIME_SEC);
byTime.moveToFirst();
// TODO: can I do byTime.deleteRow() ?
String hostPort = byTime.getString(SSL_CACHE_HOSTPORT_COL);
mDatabaseHelper.getWritableDatabase().delete(SSL_CACHE_TABLE,
SSL_CACHE_HOSTPORT + "= ?" , new String[] { hostPort });
}
// Serialize native session to standard DER encoding
long t0 = System.currentTimeMillis();
String b64 = new String(Base64.encodeBase64(der));
String key = session.getPeerHost() + ":" + session.getPeerPort();
ContentValues values = new ContentValues();
values.put(SSL_CACHE_HOSTPORT, key);
values.put(SSL_CACHE_SESSION, b64);
values.put(SSL_CACHE_TIME_SEC, System.currentTimeMillis() / 1000);
synchronized (this.getClass()) {
mExternalCache.put(key, der);
try {
mDatabaseHelper.getWritableDatabase().insert(SSL_CACHE_TABLE, null /*nullColumnHack */ , values);
} catch(SQLException ex) {
// Ignore - nothing we can do to recover, and caller shouldn't
// be affected.
Log.w(TAG, "Ignoring SQL exception when caching session", ex);
}
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
long t1 = System.currentTimeMillis();
Log.d(TAG, "New SSL session " + session.getPeerHost() +
" DER len: " + der.length + " " + (t1 - t0));
}
}
public byte[] getSessionData(String host, int port) {
// Current (simple) implementation does a single lookup to DB, then saves
// all entries to the cache.
// This works for google services - i.e. small number of certs.
// If we extend this to all processes - we should hold a separate cache
// or do lookups to DB each time.
if (mDatabaseHelper == null) {
return null;
}
synchronized(this.getClass()) {
if (mNeedsCacheLoad) {
// Don't try to load again, if something is wrong on the first
// request it'll likely be wrong each time.
mNeedsCacheLoad = false;
long t0 = System.currentTimeMillis();
Cursor cur = null;
try {
cur = mDatabaseHelper.getReadableDatabase().query(SSL_CACHE_TABLE, PROJECTION, null,
null, null, null, null);
if (cur.moveToFirst()) {
do {
String hostPort = cur.getString(SSL_CACHE_HOSTPORT_COL);
String value = cur.getString(SSL_CACHE_SESSION_COL);
if (hostPort == null || value == null) {
continue;
}
// TODO: blob support ?
byte[] der = Base64.decodeBase64(value.getBytes());
mExternalCache.put(hostPort, der);
} while (cur.moveToNext());
}
} catch (SQLException ex) {
Log.d(TAG, "Error loading SSL cached entries ", ex);
} finally {
if (cur != null) {
cur.close();
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
long t1 = System.currentTimeMillis();
Log.d(TAG, "LOADED CACHED SSL " + (t1 - t0) + " ms");
}
}
}
String key = host + ":" + port;
return mExternalCache.get(key);
}
}
public byte[] getSessionData(byte[] id) {
// We support client side only - the cache will do nothing on client.
return null;
}
/** Visible for testing.
*/
public static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + SSL_CACHE_TABLE + " (" +
SSL_CACHE_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
SSL_CACHE_HOSTPORT + " TEXT UNIQUE ON CONFLICT REPLACE," +
SSL_CACHE_SESSION + " TEXT," +
SSL_CACHE_TIME_SEC + " INTEGER" +
");");
db.execSQL("CREATE INDEX ssl_sessions_idx1 ON ssl_sessions (" +
SSL_CACHE_HOSTPORT + ");");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + SSL_CACHE_TABLE );
onCreate(db);
}
}
}

View File

@@ -1,432 +0,0 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.core;
import android.test.AndroidTestCase;
import android.os.Debug;
import org.apache.harmony.xnet.provider.jsse.FileClientSessionCache;
import org.apache.harmony.xnet.provider.jsse.OpenSSLContextImpl;
import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.conn.SingleClientConnManager;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.HttpResponse;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.security.cert.Certificate;
import java.security.Principal;
import java.security.KeyManagementException;
import java.util.Arrays;
public class SSLPerformanceTest extends AndroidTestCase {
static final byte[] SESSION_DATA = new byte[6000];
static {
for (int i = 0; i < SESSION_DATA.length; i++) {
SESSION_DATA[i] = (byte) i;
}
}
static final File dataDir = new File("/data/data/android.core/");
static final File filesDir = new File(dataDir, "files");
static final File dbDir = new File(dataDir, "databases");
static final String CACHE_DIR
= SSLPerformanceTest.class.getName() + "/cache";
static final int ITERATIONS = 10;
public void testCreateNewEmptyDatabase() {
deleteDatabase();
Stopwatch stopwatch = new Stopwatch();
DatabaseSessionCache cache = new DatabaseSessionCache(getContext());
cache.getSessionData("crazybob.org", 443);
stopwatch.stop();
}
public void testCreateNewEmptyDirectory() throws IOException {
deleteDirectory();
Stopwatch stopwatch = new Stopwatch();
SSLClientSessionCache cache = FileClientSessionCache.usingDirectory(
getCacheDirectory());
cache.getSessionData("crazybob.org", 443);
stopwatch.stop();
}
public void testOpenDatabaseWith10Sessions() {
deleteDatabase();
DatabaseSessionCache cache = new DatabaseSessionCache(getContext());
putSessionsIn(cache);
closeDatabase();
System.err.println("Size of ssl_sessions.db w/ 10 sessions: "
+ new File(dbDir, "ssl_sessions.db").length());
Stopwatch stopwatch = new Stopwatch();
cache = new DatabaseSessionCache(getContext());
cache.getSessionData("crazybob.org", 443);
stopwatch.stop();
}
public void testOpenDirectoryWith10Sessions() throws IOException {
deleteDirectory();
SSLClientSessionCache cache = FileClientSessionCache.usingDirectory(
getCacheDirectory());
putSessionsIn(cache);
closeDirectoryCache();
Stopwatch stopwatch = new Stopwatch();
cache = FileClientSessionCache.usingDirectory(
getCacheDirectory());
cache.getSessionData("crazybob.org", 443);
stopwatch.stop();
}
public void testGetSessionFromDatabase() {
deleteDatabase();
DatabaseSessionCache cache = new DatabaseSessionCache(getContext());
cache.putSessionData(new FakeSession("foo"), SESSION_DATA);
closeDatabase();
cache = new DatabaseSessionCache(getContext());
cache.getSessionData("crazybob.org", 443);
Stopwatch stopwatch = new Stopwatch();
byte[] sessionData = cache.getSessionData("foo", 443);
stopwatch.stop();
assertTrue(Arrays.equals(SESSION_DATA, sessionData));
}
public void testGetSessionFromDirectory() throws IOException {
deleteDirectory();
SSLClientSessionCache cache = FileClientSessionCache.usingDirectory(
getCacheDirectory());
cache.putSessionData(new FakeSession("foo"), SESSION_DATA);
closeDirectoryCache();
cache = FileClientSessionCache.usingDirectory(
getCacheDirectory());
cache.getSessionData("crazybob.org", 443);
Stopwatch stopwatch = new Stopwatch();
byte[] sessionData = cache.getSessionData("foo", 443);
stopwatch.stop();
assertTrue(Arrays.equals(SESSION_DATA, sessionData));
}
public void testPutSessionIntoDatabase() {
deleteDatabase();
DatabaseSessionCache cache = new DatabaseSessionCache(getContext());
cache.getSessionData("crazybob.org", 443);
Stopwatch stopwatch = new Stopwatch();
cache.putSessionData(new FakeSession("foo"), SESSION_DATA);
stopwatch.stop();
}
public void testPutSessionIntoDirectory() throws IOException {
deleteDirectory();
SSLClientSessionCache cache = FileClientSessionCache.usingDirectory(
getCacheDirectory());
cache.getSessionData("crazybob.org", 443);
Stopwatch stopwatch = new Stopwatch();
cache.putSessionData(new FakeSession("foo"), SESSION_DATA);
stopwatch.stop();
}
public void testEngineInit() throws IOException, KeyManagementException {
Stopwatch stopwatch = new Stopwatch();
new OpenSSLContextImpl().engineInit(null, null, null);
stopwatch.stop();
}
public void testWebRequestWithoutCache() throws IOException,
KeyManagementException {
OpenSSLContextImpl sslContext = new OpenSSLContextImpl();
sslContext.engineInit(null, null, null);
Stopwatch stopwatch = new Stopwatch();
getVerisignDotCom(sslContext);
stopwatch.stop();
}
public void testWebRequestWithFileCache() throws IOException,
KeyManagementException {
deleteDirectory();
OpenSSLContextImpl sslContext = new OpenSSLContextImpl();
sslContext.engineInit(null, null, null);
sslContext.engineGetClientSessionContext().setPersistentCache(
FileClientSessionCache.usingDirectory(getCacheDirectory()));
// Make sure www.google.com is in the cache.
getVerisignDotCom(sslContext);
// Re-initialize so we hit the file cache.
sslContext.engineInit(null, null, null);
sslContext.engineGetClientSessionContext().setPersistentCache(
FileClientSessionCache.usingDirectory(getCacheDirectory()));
Stopwatch stopwatch = new Stopwatch();
getVerisignDotCom(sslContext);
stopwatch.stop();
}
public void testWebRequestWithInMemoryCache() throws IOException,
KeyManagementException {
deleteDirectory();
OpenSSLContextImpl sslContext = new OpenSSLContextImpl();
sslContext.engineInit(null, null, null);
// Make sure www.google.com is in the cache.
getVerisignDotCom(sslContext);
Stopwatch stopwatch = new Stopwatch();
getVerisignDotCom(sslContext);
stopwatch.stop();
}
private void getVerisignDotCom(OpenSSLContextImpl sslContext)
throws IOException {
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("https",
new SSLSocketFactory(sslContext.engineGetSocketFactory()),
443));
ClientConnectionManager manager =
new SingleClientConnManager(null, schemeRegistry);
new DefaultHttpClient(manager, null).execute(
new HttpGet("https://www.verisign.com"),
new ResponseHandler<Object>() {
public Object handleResponse(HttpResponse response)
throws ClientProtocolException, IOException {
return null;
}
});
}
private void putSessionsIn(SSLClientSessionCache cache) {
for (int i = 0; i < 10; i++) {
cache.putSessionData(new FakeSession("host" + i), SESSION_DATA);
}
}
private void deleteDatabase() {
closeDatabase();
if (!new File(dbDir, "ssl_sessions.db").delete()) {
System.err.println("Failed to delete database.");
}
}
private void closeDatabase() {
if (DatabaseSessionCache.sDefaultDatabaseHelper != null) {
DatabaseSessionCache.sDefaultDatabaseHelper.close();
}
DatabaseSessionCache.sDefaultDatabaseHelper = null;
DatabaseSessionCache.sHookInitializationDone = false;
DatabaseSessionCache.mNeedsCacheLoad = true;
}
private void deleteDirectory() {
closeDirectoryCache();
File dir = getCacheDirectory();
if (!dir.exists()) {
return;
}
for (File file : dir.listFiles()) {
file.delete();
}
if (!dir.delete()) {
System.err.println("Failed to delete directory.");
}
}
private void closeDirectoryCache() {
try {
Method reset = FileClientSessionCache.class
.getDeclaredMethod("reset");
reset.setAccessible(true);
reset.invoke(null);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
private File getCacheDirectory() {
return new File(getContext().getFilesDir(), CACHE_DIR);
}
class Stopwatch {
{
Debug.startAllocCounting();
}
long start = System.nanoTime();
void stop() {
long elapsed = (System.nanoTime() - start) / 1000;
Debug.stopAllocCounting();
System.err.println(getName() + ": " + elapsed + "us, "
+ Debug.getThreadAllocCount() + " allocations, "
+ Debug.getThreadAllocSize() + " bytes");
}
}
}
class FakeSession implements SSLSession {
final String host;
FakeSession(String host) {
this.host = host;
}
public int getApplicationBufferSize() {
throw new UnsupportedOperationException();
}
public String getCipherSuite() {
throw new UnsupportedOperationException();
}
public long getCreationTime() {
throw new UnsupportedOperationException();
}
public byte[] getId() {
return host.getBytes();
}
public long getLastAccessedTime() {
throw new UnsupportedOperationException();
}
public Certificate[] getLocalCertificates() {
throw new UnsupportedOperationException();
}
public Principal getLocalPrincipal() {
throw new UnsupportedOperationException();
}
public int getPacketBufferSize() {
throw new UnsupportedOperationException();
}
public javax.security.cert.X509Certificate[] getPeerCertificateChain() {
throw new UnsupportedOperationException();
}
public Certificate[] getPeerCertificates() {
throw new UnsupportedOperationException();
}
public String getPeerHost() {
return host;
}
public int getPeerPort() {
return 443;
}
public Principal getPeerPrincipal() {
throw new UnsupportedOperationException();
}
public String getProtocol() {
throw new UnsupportedOperationException();
}
public SSLSessionContext getSessionContext() {
throw new UnsupportedOperationException();
}
public Object getValue(String name) {
throw new UnsupportedOperationException();
}
public String[] getValueNames() {
throw new UnsupportedOperationException();
}
public void invalidate() {
throw new UnsupportedOperationException();
}
public boolean isValid() {
throw new UnsupportedOperationException();
}
public void putValue(String name, Object value) {
throw new UnsupportedOperationException();
}
public void removeValue(String name) {
throw new UnsupportedOperationException();
}
}