Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 74815d33 authored by Jesse Wilson's avatar Jesse Wilson
Browse files

Remove SSLPerformanceTest and DatabaseSessionCache.

These aren't being run.

Change-Id: I9afc617a424c675578185ac66a4f6ac3af9afacf
parent 83a7b963
Loading
Loading
Loading
Loading
+0 −312
Original line number Diff line number Diff line
// 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);
        }

    }

}
+0 −432
Original line number Diff line number Diff line
/*
 * 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();
    }
}