// Copyright 2009 The Android Open Source Project
package com.android.internal.net;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import org.apache.commons.codec.binary.Base64;
import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache;
import java.util.HashMap;
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 DbSSLSessionCache implements SSLClientSessionCache {
private static final String TAG = "DbSSLSessionCache";
/**
* 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 = 2;
/** 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;
public static final int MAX_CACHE_SIZE = 256;
private final Map<String, byte[]> mExternalCache =
new HashMap<String, byte[]>();
private DatabaseHelper mDatabaseHelper;
private boolean mNeedsCacheLoad = true;
public static final String[] PROJECTION = new String[] {
SSL_CACHE_ID,
SSL_CACHE_HOSTPORT,
SSL_CACHE_SESSION,
SSL_CACHE_TIME_SEC
};
private static final Map<String,DbSSLSessionCache> sInstances =
new HashMap<String,DbSSLSessionCache>();
/**
* Returns a singleton instance of the DbSSLSessionCache that should be used for this
* context's package.
*
* @param context The context that should be used for getting/creating the singleton instance.
* @return The singleton instance for the context's package.
*/
public static synchronized DbSSLSessionCache getInstanceForPackage(Context context) {
String packageName = context.getPackageName();
if (sInstances.containsKey(packageName)) {
return sInstances.get(packageName);
}
DbSSLSessionCache cache = new DbSSLSessionCache(context);
sInstances.put(packageName, cache);
return cache;
}
/**
* Create a SslSessionCache instance, using the specified context to
* initialize the database.
*
* This constructor will use the default database - created for the application
* context.
*
* @param activityContext
*/
private DbSSLSessionCache(Context activityContext) {
Context appContext = activityContext.getApplicationContext();
mDatabaseHelper = new DatabaseHelper(appContext);
}
/**
* Create a SslSessionCache that uses a specific database.
*
*
* @param database
*/
public DbSSLSessionCache(DatabaseHelper database) {
this.mDatabaseHelper = database;
}
public void putSessionData(SSLSession session, byte[] der) {
if (mDatabaseHelper == null) {
return;
}
synchronized (this.getClass()) {
SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
if (mExternalCache.size() == MAX_CACHE_SIZE) {
// remove oldest.
// TODO: check if the new one is in cached already ( i.e. update ).
Cursor byTime = mDatabaseHelper.getReadableDatabase().query(SSL_CACHE_TABLE,
PROJECTION, null, null, null, null, SSL_CACHE_TIME_SEC);
if (byTime.moveToFirst()) {
// TODO: can I do byTime.deleteRow() ?
String hostPort = byTime.getString(SSL_CACHE_HOSTPORT_COL);
db.delete(SSL_CACHE_TABLE,
SSL_CACHE_HOSTPORT + "= ?" , new String[] { hostPort });
mExternalCache.remove(hostPort);
} else {
Log.w(TAG, "No rows found");
// something is wrong, clear it
clear();
}
}
// 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);
mExternalCache.put(key, der);
try {
db.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);
}
}
/**
* Reset the database and internal state.
* Used for testing or to free space.
*/
public void clear() {
synchronized(this) {
try {
mExternalCache.clear();
mNeedsCacheLoad = true;
mDatabaseHelper.getWritableDatabase().delete(SSL_CACHE_TABLE,
null, null);
} catch (SQLException ex) {
Log.d(TAG, "Error removing SSL cached entries ", ex);
// ignore - nothing we can do about it
}
}
}
public byte[] getSessionData(byte[] id) {
// We support client side only - the cache will do nothing for
// server-side sessions.
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" +
");");
// No index - we load on startup, index would slow down inserts.
// If we want to scale this to lots of rows - we could use
// index, but then we'll hit DB a bit too often ( including
// negative hits )
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + SSL_CACHE_TABLE );
onCreate(db);
}
}
}
|