/* * Copyright (c) 1999-2001,2005-2008,2010-2014 Apple Inc. All Rights Reserved. * * @APPLE_LICENSE_HEADER_START@ * * This file contains Original Code and/or Modifications of Original Code * as defined in and that are subject to the Apple Public Source License * Version 2.0 (the 'License'). You may not use this file except in * compliance with the License. Please obtain a copy of the License at * http://www.opensource.apple.com/apsl/ and read it before using this * file. * * The 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. * * @APPLE_LICENSE_HEADER_END@ */ /* * appleSession.c - Session storage module, Apple CDSA version. */ /* * The current implementation stores sessions in a linked list, a member of a * SessionCache object for which we keep a single global 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 list, 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 #include #include #include /* default time-to-live in cache, in seconds */ #define QUICK_CACHE_TEST 0 #if QUICK_CACHE_TEST #define SESSION_CACHE_TTL ((CFTimeInterval)5) #else #define SESSION_CACHE_TTL ((CFTimeInterval)(10 * 60)) #endif /* QUICK_CACHE_TEST */ #define CACHE_PRINT 0 #if CACHE_PRINT #define DUMP_ALL_CACHE 0 static void cachePrint( const void *entry, const SSLBuffer *key, const SSLBuffer *data) { printf("entry: %p ", entry); 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(e, k, d) #define DUMP_ALL_CACHE 0 #endif /* CACHE_PRINT */ #if DUMP_ALL_CACHE static void dumpAllCache(void); #else #define dumpAllCache() #endif /* * One entry (value) in SessionCache. */ typedef struct SessionCacheEntry SessionCacheEntry; struct SessionCacheEntry { /* Linked list of SessionCacheEntries. */ SessionCacheEntry *next; SSLBuffer mKey; SSLBuffer mSessionData; /* this entry to be removed from session map at this time */ CFAbsoluteTime 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. */ /* * This constructor, the only one, allocs copies of the key and value * SSLBuffers. */ static SessionCacheEntry *SessionCacheEntryCreate( const SSLBuffer *key, const SSLBuffer *sessionData, CFAbsoluteTime expirationTime) { OSStatus serr; SessionCacheEntry *entry = sslMalloc(sizeof(SessionCacheEntry)); if (entry == NULL) return NULL; serr = SSLCopyBuffer(key, &entry->mKey); if(serr) { sslFree (entry); return NULL; } serr = SSLCopyBuffer(sessionData, &entry->mSessionData); if(serr) { SSLFreeBuffer(&entry->mKey); sslFree (entry); return NULL; } sslLogSessCacheDebug("SessionCacheEntryCreate(buf,buf) %p", entry); entry->mExpiration = expirationTime; return entry; } static void SessionCacheEntryDelete(SessionCacheEntry *entry) { sslLogSessCacheDebug("~SessionCacheEntryDelete() %p", entry); SSLFreeBuffer(&entry->mKey); // no SSLContext SSLFreeBuffer(&entry->mSessionData); sslFree(entry); } /* basic lookup/match function */ static bool SessionCacheEntryMatchKey(SessionCacheEntry *entry, const SSLBuffer *key) { if(key->length != entry->mKey.length) { return false; } if((key->data == NULL) || (entry->mKey.data == NULL)) { return false; } return (memcmp(key->data, entry->mKey.data, entry->mKey.length) == 0); } static bool SessionCacheEntryIsStale(SessionCacheEntry *entry, CFAbsoluteTime now) { return now > entry->mExpiration; } /* has this expired? */ static bool SessionCacheEntryIsStaleNow(SessionCacheEntry *entry) { return SessionCacheEntryIsStale(entry, CFAbsoluteTimeGetCurrent()); } /* replace existing mSessionData */ static OSStatus SessionCacheEntrySetSessionData(SessionCacheEntry *entry, const SSLBuffer *data) { SSLFreeBuffer(&entry->mSessionData); return SSLCopyBuffer(data, &entry->mSessionData); } /* * Global list of sessions and associated state. We maintain a singleton of * this. */ typedef struct SessionCache { SessionCacheEntry *head; CFTimeInterval mTimeToLive; /* default time-to-live in seconds */ } SessionCache; static pthread_mutex_t gSessionCacheLock = PTHREAD_MUTEX_INITIALIZER; static SessionCache *gSessionCache = NULL; static void SessionCacheInit(void) { gSessionCache = sslMalloc(sizeof(SessionCache)); gSessionCache->head = NULL; gSessionCache->mTimeToLive = SESSION_CACHE_TTL; } static SessionCache *SessionCacheGetLockedInstance(void) { pthread_mutex_lock(&gSessionCacheLock); if (!gSessionCache) { /* We could use pthread_once, but we already have a mutex for other reasons. */ SessionCacheInit(); } return gSessionCache; } /* these three correspond to the C functions exported by this file */ static OSStatus SessionCacheAddEntry( SessionCache *cache, const SSLBuffer *sessionKey, const SSLBuffer *sessionData, uint32_t timeToLive) /* optional time-to-live in seconds; 0 ==> default */ { SessionCacheEntry *entry = NULL; SessionCacheEntry **current; CFTimeInterval expireTime; for (current = &(cache->head); *current; current = &((*current)->next)) { entry = *current; if (SessionCacheEntryMatchKey(entry, sessionKey)) { /* cache hit - just update this entry's sessionData if necessary */ /* Note we leave expiration time and position in queue unchanged - OK? */ /* What if the entry has already expired? */ if((entry->mSessionData.length == sessionData->length) && (memcmp(entry->mSessionData.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", entry); return errSecSuccess; } else { sslLogSessCacheDebug("SessionCache::addEntry CACHE REPLACE " "entry = %p", entry); return SessionCacheEntrySetSessionData(entry, sessionData); } } } expireTime = CFAbsoluteTimeGetCurrent(); if(timeToLive) { /* caller-specified */ expireTime += (CFTimeInterval)timeToLive; } else { /* default */ expireTime += cache->mTimeToLive; } /* this allocs new copy of incoming sessionKey and sessionData */ entry = SessionCacheEntryCreate(sessionKey, sessionData, expireTime); sslLogSessCacheDebug("SessionCache::addEntry %p", entry); cachePrint(entry, sessionKey, sessionData); dumpAllCache(); /* add to head of queue for LIFO caching */ entry->next = cache->head; cache->head = entry; return errSecSuccess; } static OSStatus SessionCacheLookupEntry( SessionCache *cache, const SSLBuffer *sessionKey, SSLBuffer *sessionData) { SessionCacheEntry *entry = NULL; SessionCacheEntry **current; for (current = &(cache->head); *current; current = &((*current)->next)) { entry = *current; if (SessionCacheEntryMatchKey(entry, sessionKey)) break; } if (*current == NULL) return errSSLSessionNotFound; if (SessionCacheEntryIsStaleNow(entry)) { sslLogSessCacheDebug("SessionCache::lookupEntry %p: STALE " "entry, deleting; current %p, entry->next %p", entry, current, entry->next); cachePrint(entry, sessionKey, &entry->mSessionData); *current = entry->next; SessionCacheEntryDelete(entry); return errSSLSessionNotFound; } /* alloc/copy sessionData from existing entry (caller must free) */ return SSLCopyBuffer(&entry->mSessionData, sessionData); } static OSStatus SessionCacheDeleteEntry( SessionCache *cache, const SSLBuffer *sessionKey) { SessionCacheEntry **current; for (current = &(cache->head); *current; current = &((*current)->next)) { SessionCacheEntry *entry = *current; if (SessionCacheEntryMatchKey(entry, sessionKey)) { #ifndef DEBUG sslLogSessCacheDebug("...SessionCacheDeleteEntry: deleting " "cached session (%p)", entry); cachePrint(entry, &entry->mKey, &entry->mSessionData); #endif *current = entry->next; SessionCacheEntryDelete(entry); return errSecSuccess; } } return errSecSuccess; } /* cleanup, delete stale entries */ static bool SessionCacheCleanup(SessionCache *cache) { bool brtn = false; CFAbsoluteTime rightNow = CFAbsoluteTimeGetCurrent(); SessionCacheEntry **current; for (current = &(cache->head); *current;) { SessionCacheEntry *entry = *current; if(SessionCacheEntryIsStale(entry, rightNow)) { #ifndef DEBUG sslLogSessCacheDebug("...SessionCacheCleanup: deleting " "cached session (%p)", entry); cachePrint(entry, &entry->mKey, &entry->mSessionData); #endif *current = entry->next; SessionCacheEntryDelete(entry); } else { current = &((*current)->next); /* we're leaving one in the map */ brtn = true; } } return brtn; } #if DUMP_ALL_CACHE static void dumpAllCache(void) { SessionCache *cache = gSessionCache; SessionCacheEntry *entry; printf("Contents of sessionCache:\n"); for(entry = cache->head; entry; entry = entry->next) { cachePrint(entry, &entry->mKey, &entry->mSessionData); } } #endif /* DUMP_ALL_CACHE */ /* * Store opaque sessionData, associated with opaque sessionKey. */ OSStatus sslAddSession ( const SSLBuffer sessionKey, const SSLBuffer sessionData, uint32_t timeToLive) /* optional time-to-live in seconds; 0 ==> default */ { SessionCache *cache = SessionCacheGetLockedInstance(); OSStatus serr; if (!cache) serr = errSSLSessionNotFound; else { serr = SessionCacheAddEntry(cache, &sessionKey, &sessionData, timeToLive); dumpAllCache(); } pthread_mutex_unlock(&gSessionCacheLock); return serr; } /* * Given an opaque sessionKey, alloc & retrieve associated sessionData. */ OSStatus sslCopySession ( const SSLBuffer sessionKey, SSLBuffer *sessionData) { SessionCache *cache = SessionCacheGetLockedInstance(); OSStatus serr; if (!cache) serr = errSSLSessionNotFound; else { serr = SessionCacheLookupEntry(cache, &sessionKey, sessionData); sslLogSessCacheDebug("sslGetSession(%d, %p): %d", (int)sessionKey.length, sessionKey.data, (int)serr); if(!serr) { cachePrint(NULL, &sessionKey, sessionData); } else { cachePrint(NULL, &sessionKey, NULL); } dumpAllCache(); } pthread_mutex_unlock(&gSessionCacheLock); return serr; } OSStatus sslDeleteSession ( const SSLBuffer sessionKey) { SessionCache *cache = SessionCacheGetLockedInstance(); OSStatus serr; if (!cache) serr = errSSLSessionNotFound; else { serr = SessionCacheDeleteEntry(cache, &sessionKey); } pthread_mutex_unlock(&gSessionCacheLock); return serr; } /* cleanup up session cache, deleting stale entries. */ OSStatus sslCleanupSession(void) { SessionCache *cache = SessionCacheGetLockedInstance(); OSStatus serr = errSecSuccess; bool moreToGo = false; if (!cache) serr = errSSLSessionNotFound; else { moreToGo = SessionCacheCleanup(cache); } /* Possible TBD: if moreToGo, schedule a timed callback to this function */ pthread_mutex_unlock(&gSessionCacheLock); return serr; }