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

Commit ed6d936e authored by Jesse Wilson's avatar Jesse Wilson Committed by Android (Google) Code Review
Browse files

Merge "Remove SSLPerformanceTest and DatabaseSessionCache."

parents 360e40ef 74815d33
Loading
Loading
Loading
Loading
+0 −312
Original line number Original line 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 Original line 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();
    }
}