appleSession.cpp   [plain text]


/*
 * Copyright (c) 2000-2001 Apple Computer, Inc. All Rights Reserved.
 * 
 * The contents of this file constitute Original Code as defined in and are
 * subject to the Apple Public Source License Version 1.2 (the 'License').
 * You may not use this file except in compliance with the License. Please obtain
 * a copy of the License at http://www.apple.com/publicsource and read it before
 * using this file.
 * 
 * This Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS
 * OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT
 * LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. Please see the License for the
 * specific language governing rights and limitations under the License.
 */


/*
	File:		appleSession.cpp

	Contains:	Session storage module, Apple CDSA version. 

	Written by:	Doug Mitchell

	Copyright: (c) 1999 by Apple Computer, Inc., all rights reserved.

*/

/* 
 * The current implementation stores sessions in a deque<>, a member of a 
 * SessionCache object for which we keep a ModuleNexus-ized instance. It is 
 * expected that at a given time, only a small number of sessions will be 
 * cached, so the random insertion access provided by a map<> is unnecessary. 
 * New entries are placed in the head of the queue, assuming a LIFO usage
 * tendency.  
 *
 * Entries in this cache have a time to live of SESSION_CACHE_TTL, currently 
 * ten minutes. Entries are tested for being stale upon lookup; also, the global
 * sslCleanupSession() tests all entries in the cache, deleting entries which 
 * are stale. This function is currently called whenever an SSLContext is deleted. 
 * The current design does not provide any asynchronous timed callouts to perform
 * further cache cleanup; it was decided that the thread overhead of this would 
 * outweight the benefits (again assuming a small number of entries in the 
 * cache). 
 *
 * When a session is added via sslAddSession, and a cache entry already
 * exists for the specifed key (sessionID), the sessionData for the existing
 * cache entry is updated with the new sessionData. The entry's expiration
 * time is unchanged (thus a given session entry can only be used for a finite
 * time no mattter how often it is re-used), 
 */
 
#include "ssl.h"
#include "sslMemory.h"
#include "sslDebug.h"
#include "appleSession.h"

#include <CoreServices/../Frameworks/CarbonCore.framework/Headers/MacErrors.h>

#include <deque>
#include <stdexcept>
#include <Security/threading.h>
#include <Security/globalizer.h>
#include <Security/timeflow.h>

/* time-to-live in cache, in seconds */
#define QUICK_CACHE_TEST	0
#if		QUICK_CACHE_TEST
#define SESSION_CACHE_TTL	((int)5)
#else
#define SESSION_CACHE_TTL	((int)(10 * 60))
#endif	/* QUICK_CACHE_TEST */

#define CACHE_PRINT			0
#if		CACHE_PRINT
#define DUMP_ALL_CACHE		0

static void cachePrint(
	const SSLBuffer *key, 
	const SSLBuffer *data)
{
	unsigned char *kd = key->data;
	if(data != NULL) {
		unsigned char *dd = data->data;
		printf("  key: %02X%02X%02X%02X%02X%02X%02X%02X"
			"  data: %02X%02X%02X%02X... (len %d)\n",
			kd[0],kd[1],kd[2],kd[3], kd[4],kd[5],kd[6],kd[7],
			dd[0],dd[1],dd[2],dd[3], (unsigned)data->length);
	}
	else {
		/* just print key */
		printf("  key: %02X%02X%02X%02X%02X%02X%02X%02X\n",
			kd[0],kd[1],kd[2],kd[3], kd[4],kd[5],kd[6],kd[7]);
	}
}
#else	/* !CACHE_PRINT */
#define cachePrint(k, d)
#define DUMP_ALL_CACHE	0
#endif	/* CACHE_PRINT */

#if 	DUMP_ALL_CACHE
static void dumpAllCache();
#else
#define dumpAllCache()
#endif

/*
 * One entry (value) in SessionCache.  
 */
class SessionCacheEntry {
public:
	/*
	 * This constructor, the only one, allocs copies of the key and value
	 * SSLBuffers.
	 */
	SessionCacheEntry(
		const SSLBuffer &key, 
		const SSLBuffer &sessionData,
		const Time::Absolute &expirationTime);
	~SessionCacheEntry();
		
	/* basic lookup/match function */
	bool			matchKey(const SSLBuffer &key) const;
	
	/* has this expired? */
	bool			isStale();							// calculates "now" 
	bool			isStale(const Time::Absolute &now);	// when you know it
	
	/* key/data accessors */
	SSLBuffer		&key()			{ return mKey; }
	SSLBuffer		&sessionData()	{ return mSessionData; }
	
	/* replace existing mSessionData */
	OSStatus			sessionData(const SSLBuffer &data);
	
private:
	SSLBuffer		mKey;
	SSLBuffer		mSessionData;

	/* this entry to be removed from session map at this time */
	Time::Absolute	mExpiration;
};

/*
 * Note: the caller passes in the expiration time solely to accomodate the 
 * instantiation of a single const Time::Interval for use in calculating
 * TTL. This const, SessionCache.mTimeToLive, is in the singleton gSession Cache.
 */
SessionCacheEntry::SessionCacheEntry(
	const SSLBuffer &key, 
	const SSLBuffer &sessionData,
	const Time::Absolute &expirationTime)
		: mExpiration(expirationTime)
{
	OSStatus serr;
	
	serr = SSLCopyBuffer(key, mKey);
	if(serr) {
		throw runtime_error("memory error");
	}
	serr = SSLCopyBuffer(sessionData, mSessionData);
	if(serr) {
		throw runtime_error("memory error");
	}
	sslLogSessCacheDebug("SessionCacheEntry(buf,buf) this %p", this);
	mExpiration += Time::Interval(SESSION_CACHE_TTL);
}

SessionCacheEntry::~SessionCacheEntry()
{
	sslLogSessCacheDebug("~SessionCacheEntry() this %p", this);
	SSLFreeBuffer(mKey, NULL);		// no SSLContext
	SSLFreeBuffer(mSessionData, NULL);
}

/* basic lookup/match function */
bool SessionCacheEntry::matchKey(const SSLBuffer &key) const
{
	if(key.length != mKey.length) {
		return false;
	}
	if((key.data == NULL) || (mKey.data == NULL)) {
		return false;
	}
	return (memcmp(key.data, mKey.data, mKey.length) == 0);
}
	
/* has this expired? */
bool SessionCacheEntry::isStale()
{
	return isStale(Time::now());
}

bool SessionCacheEntry::isStale(const Time::Absolute &now)
{
	if(now > mExpiration) {
		return true;
	}
	else {
		return false;
	}
}

/* replace existing mSessionData */
OSStatus SessionCacheEntry::sessionData(
	const SSLBuffer &data)
{
	SSLFreeBuffer(mSessionData, NULL);
	return SSLCopyBuffer(data, mSessionData);
}

/* Types for the actual deque and its iterator */
typedef std::deque<SessionCacheEntry *> SessionCacheType;
typedef SessionCacheType::iterator SessionCacheIter;

/* 
 * Global map and associated state. We maintain a singleton of this.
 */
class SessionCache
{
public:
	SessionCache()
	  : mTimeToLive(SESSION_CACHE_TTL) {}
	~SessionCache();
	
	/* these correspond to the C functions exported by this file */
	OSStatus addEntry(
		const SSLBuffer sessionKey, 
		const SSLBuffer sessionData);
	OSStatus lookupEntry(
		const SSLBuffer sessionKey, 
		SSLBuffer *sessionData); 
	OSStatus deleteEntry(
		const SSLBuffer sessionKey);
		
	/* cleanup, delete stale entries */
	bool cleanup();
	SessionCacheType		&sessMap() { return mSessionCache; }
	
private:
	SessionCacheIter lookupPriv(
		const SSLBuffer *sessionKey);
	void deletePriv(
		const SSLBuffer *sessionKey);
	SessionCacheIter deletePriv(
		SessionCacheIter iter);
	SessionCacheType		mSessionCache;
	Mutex					mSessionLock;
	const Time::Interval	mTimeToLive;
};

SessionCache::~SessionCache()
{
	/* free all entries */
	StLock<Mutex> _(mSessionLock);
	for(SessionCacheIter iter = mSessionCache.begin(); iter != mSessionCache.end(); ) {
		iter = deletePriv(iter);
	}
}

/* these three correspond to the C functions exported by this file */
OSStatus SessionCache::addEntry(
	const SSLBuffer sessionKey, 
	const SSLBuffer sessionData)
{
	StLock<Mutex> _(mSessionLock);
	
	SessionCacheIter existIter = lookupPriv(&sessionKey);
	if(existIter != mSessionCache.end()) {
		/* cache hit - just update this entry's sessionData if necessary */
		/* Note we leave expiration time and position in deque unchanged - OK? */
		SessionCacheEntry *existEntry = *existIter;
		SSLBuffer &existBuf = existEntry->sessionData();
		if((existBuf.length == sessionData.length) &&
		   (memcmp(existBuf.data, sessionData.data, sessionData.length) == 0)) {
			/* 
			 * These usually match, and a memcmp is a lot cheaper than 
			 * a malloc and a free, hence this quick optimization.....
			 */
			sslLogSessCacheDebug("SessionCache::addEntry CACHE HIT "
				"entry = %p", existEntry);
			return noErr;
		}
		else {
			sslLogSessCacheDebug("SessionCache::addEntry CACHE REPLACE "
				"entry = %p", existEntry);
			return existEntry->sessionData(sessionData);
		}
	}
	
	/* this allocs new copy of incoming sessionKey and sessionData */
	SessionCacheEntry *entry = new SessionCacheEntry(sessionKey, 
		sessionData,
		Time::now() + mTimeToLive);

	sslLogSessCacheDebug("SessionCache::addEntry %p", entry);
	cachePrint(&sessionKey, &sessionData);
	dumpAllCache();

	/* add to head of queue for LIFO caching */
	mSessionCache.push_front(entry);
	assert(lookupPriv(&sessionKey) != mSessionCache.end());
	return noErr;
}

OSStatus SessionCache::lookupEntry(
	const SSLBuffer sessionKey, 
	SSLBuffer *sessionData)
{
	StLock<Mutex> _(mSessionLock);
	
	SessionCacheIter existIter = lookupPriv(&sessionKey);
	if(existIter == mSessionCache.end()) {
		return errSSLSessionNotFound;
	}
	SessionCacheEntry *entry = *existIter;
	if(entry->isStale()) {
		sslLogSessCacheDebug("SessionCache::lookupEntry %p: STALE "
			"entry, deleting", entry);
		cachePrint(&sessionKey, &entry->sessionData());
		deletePriv(existIter);
		return errSSLSessionNotFound;
	}
	/* alloc/copy sessionData from existing entry (caller must free) */
	return SSLCopyBuffer(entry->sessionData(), *sessionData);
}

OSStatus SessionCache::deleteEntry(
	const SSLBuffer sessionKey)
{
	StLock<Mutex> _(mSessionLock);
	deletePriv(&sessionKey);
	return noErr;
}
	
/* cleanup, delete stale entries */
bool SessionCache::cleanup()
{
	StLock<Mutex> _(mSessionLock);
	bool brtn = false;
	Time::Absolute rightNow = Time::now();
	SessionCacheIter iter;
	
	for(iter = mSessionCache.begin(); iter != mSessionCache.end(); ) {
		SessionCacheEntry *entry = *iter;
		if(entry->isStale(rightNow)) {
			#ifndef	DEBUG
			sslLogSessCacheDebug("...SessionCache::cleanup: deleting "
				"cached session (%p)", entry);
			cachePrint(&entry->key(), &entry->sessionData());
			#endif
			iter = deletePriv(iter);
		}
		else {
			iter++;
			/* we're leaving one in the map */
			brtn = true;
		}
	}
	return brtn;
}

/* private methods, mSessionLock held on entry and exit */
SessionCacheIter SessionCache::lookupPriv(
	const SSLBuffer *sessionKey)
{
	SessionCacheIter it;
	
	for(it = mSessionCache.begin(); it != mSessionCache.end(); it++) {
		SessionCacheEntry *entry = *it;
		if(entry->matchKey(*sessionKey)) {
			return it;
		}
	}
	/* returning map.end() */
	return it;
}

void SessionCache::deletePriv(
	const SSLBuffer *sessionKey)
{
	SessionCacheIter iter = lookupPriv(sessionKey);
	if(iter != mSessionCache.end()) {
		/* 
		 * delete from map 
		 * free underlying SSLBuffer.data pointers
		 * destruct the stored map entry 
		 */
		#if	CACHE_PRINT
		SessionCacheEntry *entry = *iter;
		sslLogSessCacheDebug("SessionCache::deletePriv %p", entry);
		cachePrint(sessionKey, &entry->sessionData());
		dumpAllCache();
		#endif
		deletePriv(iter);
	}
	assert(lookupPriv(sessionKey) == mSessionCache.end());
}

/* common erase, given a SessionCacheIter; returns next iter */
SessionCacheIter SessionCache::deletePriv(
	SessionCacheIter iter)
{
	assert(iter != mSessionCache.end());
	SessionCacheEntry *entry = *iter;
	SessionCacheIter nextIter = mSessionCache.erase(iter);
	delete entry;
	return nextIter;
}

/* the single global thing */
static ModuleNexus<SessionCache> gSessionCache;

#if		DUMP_ALL_CACHE
static void dumpAllCache()
{
	SessionCacheIter it;
	SessionCacheType &smap = gSessionCache().sessMap();
	
	printf("Contents of sessionCache:\n");
	for(it = smap.begin(); it != smap.end(); it++) {
		SessionCacheEntry *entry = *it;
		cachePrint(&entry->key(), &entry->sessionData());
	}
}
#endif	/* DUMP_ALL_CACHE */

/*
 * Store opaque sessionData, associated with opaque sessionKey.
 */
OSStatus sslAddSession (
	const SSLBuffer sessionKey, 
	const SSLBuffer sessionData)
{
	OSStatus serr;
	try {
		serr = gSessionCache().addEntry(sessionKey, sessionData);
	}
	catch(...) {
		serr = unimpErr;
	}
	dumpAllCache();
	return serr;
}

/*
 * Given an opaque sessionKey, alloc & retrieve associated sessionData.
 */
OSStatus sslGetSession (
	const SSLBuffer sessionKey, 
	SSLBuffer *sessionData)
{
	OSStatus serr;
	try {
		serr = gSessionCache().lookupEntry(sessionKey, sessionData);
	}
	catch(...) {
		serr = errSSLSessionNotFound;
	}
	sslLogSessCacheDebug("sslGetSession(%d, %p): %ld", 
		(int)sessionKey.length, sessionKey.data,
		serr);
	if(serr == noErr) {
		cachePrint(&sessionKey, sessionData);
	}
	else {
		cachePrint(&sessionKey, NULL);
	}
	dumpAllCache();
	return serr;
}

OSStatus sslDeleteSession (
	const SSLBuffer sessionKey)
{
	OSStatus serr;
	try {
		serr = gSessionCache().deleteEntry(sessionKey);
	}
	catch(...) {
		serr = errSSLSessionNotFound;
	}
	return serr;
}

/* cleanup up session cache, deleting stale entries. */
OSStatus sslCleanupSession ()
{
	OSStatus serr = noErr;
	bool moreToGo = false;
	try {
		moreToGo = gSessionCache().cleanup();
	}
	catch(...) {
		serr = errSSLSessionNotFound;
	}
	/* Possible TBD: if moreToGo, schedule a timed callback to this function */
	return serr;
}