1/*
2 * Copyright (c) 1999-2001,2005-2008,2010-2012 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24/*
25 *  appleSession.c - Session storage module, Apple CDSA version.
26 */
27
28/*
29 * The current implementation stores sessions in a linked list, a member of a
30 * SessionCache object for which we keep a single global instance. It is
31 * expected that at a given time, only a small number of sessions will be
32 * cached, so the random insertion access provided by a map<> is unnecessary.
33 * New entries are placed in the head of the list, assuming a LIFO usage
34 * tendency.
35 *
36 * Entries in this cache have a time to live of SESSION_CACHE_TTL, currently
37 * ten minutes. Entries are tested for being stale upon lookup; also, the global
38 * sslCleanupSession() tests all entries in the cache, deleting entries which
39 * are stale. This function is currently called whenever an SSLContext is deleted.
40 * The current design does not provide any asynchronous timed callouts to perform
41 * further cache cleanup; it was decided that the thread overhead of this would
42 * outweight the benefits (again assuming a small number of entries in the
43 * cache).
44 *
45 * When a session is added via sslAddSession, and a cache entry already
46 * exists for the specifed key (sessionID), the sessionData for the existing
47 * cache entry is updated with the new sessionData. The entry's expiration
48 * time is unchanged (thus a given session entry can only be used for a finite
49 * time no mattter how often it is re-used),
50 */
51
52#include "ssl.h"
53#include "sslMemory.h"
54#include "sslDebug.h"
55#include "appleSession.h"
56
57#include <CoreFoundation/CFDate.h>
58#include <pthread.h>
59#include <string.h>
60
61#include <utilities/SecIOFormat.h>
62
63/* default time-to-live in cache, in seconds */
64#define QUICK_CACHE_TEST	0
65#if		QUICK_CACHE_TEST
66#define SESSION_CACHE_TTL	((CFTimeInterval)5)
67#else
68#define SESSION_CACHE_TTL	((CFTimeInterval)(10 * 60))
69#endif	/* QUICK_CACHE_TEST */
70
71#define CACHE_PRINT			0
72#if		CACHE_PRINT
73#define DUMP_ALL_CACHE		0
74
75static void cachePrint(
76	const void *entry,
77	const SSLBuffer *key,
78	const SSLBuffer *data)
79{
80	printf("entry: %p ", entry);
81	unsigned char *kd = key->data;
82	if(data != NULL) {
83		unsigned char *dd = data->data;
84		printf("  key: %02X%02X%02X%02X%02X%02X%02X%02X"
85			"  data: %02X%02X%02X%02X... (len %d)\n",
86			kd[0],kd[1],kd[2],kd[3], kd[4],kd[5],kd[6],kd[7],
87			dd[0],dd[1],dd[2],dd[3], (unsigned)data->length);
88	}
89	else {
90		/* just print key */
91		printf("  key: %02X%02X%02X%02X%02X%02X%02X%02X\n",
92			kd[0],kd[1],kd[2],kd[3], kd[4],kd[5],kd[6],kd[7]);
93	}
94}
95#else	/* !CACHE_PRINT */
96#define cachePrint(e, k, d)
97#define DUMP_ALL_CACHE	0
98#endif	/* CACHE_PRINT */
99
100#if 	DUMP_ALL_CACHE
101static void dumpAllCache(void);
102#else
103#define dumpAllCache()
104#endif
105
106/*
107 * One entry (value) in SessionCache.
108 */
109typedef struct SessionCacheEntry SessionCacheEntry;
110struct SessionCacheEntry {
111    /* Linked list of SessionCacheEntries. */
112    SessionCacheEntry *next;
113
114	SSLBuffer		mKey;
115	SSLBuffer		mSessionData;
116
117	/* this entry to be removed from session map at this time */
118	CFAbsoluteTime	mExpiration;
119};
120
121/*
122 * Note: the caller passes in the expiration time solely to accomodate the
123 * instantiation of a single const Time::Interval for use in calculating
124 * TTL. This const, SessionCache.mTimeToLive, is in the singleton gSession Cache.
125 */
126/*
127 * This constructor, the only one, allocs copies of the key and value
128 * SSLBuffers.
129 */
130static SessionCacheEntry *SessionCacheEntryCreate(
131	const SSLBuffer *key,
132	const SSLBuffer *sessionData,
133	CFAbsoluteTime expirationTime)
134{
135    OSStatus serr;
136
137    SessionCacheEntry *entry = sslMalloc(sizeof(SessionCacheEntry));
138    if (entry == NULL)
139        return NULL;
140
141	serr = SSLCopyBuffer(key, &entry->mKey);
142	if(serr) {
143        sslFree (entry);
144        return NULL;
145	}
146	serr = SSLCopyBuffer(sessionData, &entry->mSessionData);
147	if(serr) {
148        SSLFreeBuffer(&entry->mKey);
149        sslFree (entry);
150        return NULL;
151	}
152
153	sslLogSessCacheDebug("SessionCacheEntryCreate(buf,buf) %p", entry);
154	entry->mExpiration = expirationTime;
155
156    return entry;
157}
158
159static void SessionCacheEntryDelete(SessionCacheEntry *entry)
160{
161	sslLogSessCacheDebug("~SessionCacheEntryDelete() %p", entry);
162	SSLFreeBuffer(&entry->mKey);		// no SSLContext
163	SSLFreeBuffer(&entry->mSessionData);
164    sslFree(entry);
165}
166
167/* basic lookup/match function */
168static bool SessionCacheEntryMatchKey(SessionCacheEntry *entry,
169    const SSLBuffer *key)
170{
171	if(key->length != entry->mKey.length) {
172		return false;
173	}
174	if((key->data == NULL) || (entry->mKey.data == NULL)) {
175		return false;
176	}
177	return (memcmp(key->data, entry->mKey.data, entry->mKey.length) == 0);
178}
179
180static bool SessionCacheEntryIsStale(SessionCacheEntry *entry,
181    CFAbsoluteTime now)
182{
183	return now > entry->mExpiration;
184}
185
186/* has this expired? */
187static bool SessionCacheEntryIsStaleNow(SessionCacheEntry *entry)
188{
189	return SessionCacheEntryIsStale(entry, CFAbsoluteTimeGetCurrent());
190}
191
192/* replace existing mSessionData */
193static OSStatus SessionCacheEntrySetSessionData(SessionCacheEntry *entry,
194	const SSLBuffer *data)
195{
196	SSLFreeBuffer(&entry->mSessionData);
197	return SSLCopyBuffer(data, &entry->mSessionData);
198}
199
200/*
201 * Global list of sessions and associated state. We maintain a singleton of
202 * this.
203 */
204typedef struct SessionCache {
205    SessionCacheEntry *head;
206    CFTimeInterval mTimeToLive;		/* default time-to-live in seconds */
207} SessionCache;
208
209static pthread_mutex_t gSessionCacheLock = PTHREAD_MUTEX_INITIALIZER;
210static SessionCache *gSessionCache = NULL;
211
212static void SessionCacheInit(void) {
213    gSessionCache = sslMalloc(sizeof(SessionCache));
214    gSessionCache->head = NULL;
215    gSessionCache->mTimeToLive = SESSION_CACHE_TTL;
216}
217
218static SessionCache *SessionCacheGetLockedInstance(void) {
219    pthread_mutex_lock(&gSessionCacheLock);
220    if (!gSessionCache) {
221        /* We could use pthread_once, but we already have a mutex for other
222           reasons. */
223        SessionCacheInit();
224    }
225
226    return gSessionCache;
227}
228
229/* these three correspond to the C functions exported by this file */
230static OSStatus SessionCacheAddEntry(
231    SessionCache *cache,
232	const SSLBuffer *sessionKey,
233	const SSLBuffer *sessionData,
234	uint32_t timeToLive)			/* optional time-to-live in seconds; 0 ==> default */
235{
236    SessionCacheEntry *entry = NULL;
237    SessionCacheEntry **current;
238	CFTimeInterval expireTime;
239
240	for (current = &(cache->head); *current; current = &((*current)->next)) {
241        entry = *current;
242		if (SessionCacheEntryMatchKey(entry, sessionKey)) {
243            /* cache hit - just update this entry's sessionData if necessary */
244            /* Note we leave expiration time and position in queue unchanged
245               - OK? */
246            /* What if the entry has already expired? */
247            if((entry->mSessionData.length == sessionData->length) &&
248               (memcmp(entry->mSessionData.data, sessionData->data,
249                sessionData->length) == 0)) {
250                /*
251                 * These usually match, and a memcmp is a lot cheaper than
252                 * a malloc and a free, hence this quick optimization.....
253                 */
254                sslLogSessCacheDebug("SessionCache::addEntry CACHE HIT "
255                    "entry = %p", entry);
256                return errSecSuccess;
257            }
258            else {
259                sslLogSessCacheDebug("SessionCache::addEntry CACHE REPLACE "
260                    "entry = %p", entry);
261                return SessionCacheEntrySetSessionData(entry, sessionData);
262            }
263        }
264    }
265
266	expireTime = CFAbsoluteTimeGetCurrent();
267	if(timeToLive) {
268		/* caller-specified */
269		expireTime += (CFTimeInterval)timeToLive;
270	}
271	else {
272		/* default */
273		expireTime += cache->mTimeToLive;
274	}
275	/* this allocs new copy of incoming sessionKey and sessionData */
276	entry = SessionCacheEntryCreate(sessionKey, sessionData, expireTime);
277
278	sslLogSessCacheDebug("SessionCache::addEntry %p", entry);
279	cachePrint(entry, sessionKey, sessionData);
280	dumpAllCache();
281
282	/* add to head of queue for LIFO caching */
283    entry->next = cache->head;
284    cache->head = entry;
285
286	return errSecSuccess;
287}
288
289static OSStatus SessionCacheLookupEntry(
290    SessionCache *cache,
291	const SSLBuffer *sessionKey,
292	SSLBuffer *sessionData)
293{
294    SessionCacheEntry *entry = NULL;
295    SessionCacheEntry **current;
296	for (current = &(cache->head); *current; current = &((*current)->next)) {
297        entry = *current;
298		if (SessionCacheEntryMatchKey(entry, sessionKey))
299            break;
300    }
301
302	if (*current == NULL)
303		return errSSLSessionNotFound;
304
305	if (SessionCacheEntryIsStaleNow(entry)) {
306		sslLogSessCacheDebug("SessionCache::lookupEntry %p: STALE "
307			"entry, deleting; current %p, entry->next %p",
308			entry, current, entry->next);
309		cachePrint(entry, sessionKey, &entry->mSessionData);
310        *current = entry->next;
311        SessionCacheEntryDelete(entry);
312		return errSSLSessionNotFound;
313	}
314
315	/* alloc/copy sessionData from existing entry (caller must free) */
316	return SSLCopyBuffer(&entry->mSessionData, sessionData);
317}
318
319static OSStatus SessionCacheDeleteEntry(
320    SessionCache *cache,
321	const SSLBuffer *sessionKey)
322{
323	SessionCacheEntry **current;
324
325	for (current = &(cache->head); *current; current = &((*current)->next)) {
326		SessionCacheEntry *entry = *current;
327		if (SessionCacheEntryMatchKey(entry, sessionKey)) {
328			#ifndef	DEBUG
329			sslLogSessCacheDebug("...SessionCacheDeleteEntry: deleting "
330				"cached session (%p)", entry);
331			cachePrint(entry, &entry->mKey, &entry->mSessionData);
332			#endif
333            *current = entry->next;
334            SessionCacheEntryDelete(entry);
335            return errSecSuccess;
336		}
337	}
338
339    return errSecSuccess;
340}
341
342/* cleanup, delete stale entries */
343static bool SessionCacheCleanup(SessionCache *cache)
344{
345	bool brtn = false;
346	CFAbsoluteTime rightNow = CFAbsoluteTimeGetCurrent();
347	SessionCacheEntry **current;
348
349	for (current = &(cache->head); *current;) {
350		SessionCacheEntry *entry = *current;
351		if(SessionCacheEntryIsStale(entry, rightNow)) {
352			#ifndef	DEBUG
353			sslLogSessCacheDebug("...SessionCacheCleanup: deleting "
354				"cached session (%p)", entry);
355			cachePrint(entry, &entry->mKey, &entry->mSessionData);
356			#endif
357            *current = entry->next;
358            SessionCacheEntryDelete(entry);
359		}
360		else {
361			current = &((*current)->next);
362			/* we're leaving one in the map */
363			brtn = true;
364		}
365	}
366	return brtn;
367}
368
369#if		DUMP_ALL_CACHE
370static void dumpAllCache(void)
371{
372	SessionCache *cache = gSessionCache;
373	SessionCacheEntry *entry;
374
375	printf("Contents of sessionCache:\n");
376	for(entry = cache->head; entry; entry = entry->next) {
377		cachePrint(entry, &entry->mKey, &entry->mSessionData);
378	}
379}
380#endif	/* DUMP_ALL_CACHE */
381
382/*
383 * Store opaque sessionData, associated with opaque sessionKey.
384 */
385OSStatus sslAddSession (
386	const SSLBuffer sessionKey,
387	const SSLBuffer sessionData,
388	uint32_t timeToLive)			/* optional time-to-live in seconds; 0 ==> default */
389{
390    SessionCache *cache = SessionCacheGetLockedInstance();
391	OSStatus serr;
392    if (!cache)
393        serr = errSSLSessionNotFound;
394    else
395    {
396        serr = SessionCacheAddEntry(cache, &sessionKey, &sessionData, timeToLive);
397
398        dumpAllCache();
399    }
400
401    pthread_mutex_unlock(&gSessionCacheLock);
402    return serr;
403}
404
405/*
406 * Given an opaque sessionKey, alloc & retrieve associated sessionData.
407 */
408OSStatus sslGetSession (
409	const SSLBuffer sessionKey,
410	SSLBuffer *sessionData)
411{
412    SessionCache *cache = SessionCacheGetLockedInstance();
413	OSStatus serr;
414    if (!cache)
415        serr = errSSLSessionNotFound;
416    else
417    {
418        serr = SessionCacheLookupEntry(cache, &sessionKey, sessionData);
419
420        sslLogSessCacheDebug("sslGetSession(%d, %p): %d",
421            (int)sessionKey.length, sessionKey.data,
422            (int)serr);
423        if(!serr) {
424            cachePrint(NULL, &sessionKey, sessionData);
425        }
426        else {
427            cachePrint(NULL, &sessionKey, NULL);
428        }
429        dumpAllCache();
430    }
431
432    pthread_mutex_unlock(&gSessionCacheLock);
433
434	return serr;
435}
436
437OSStatus sslDeleteSession (
438	const SSLBuffer sessionKey)
439{
440    SessionCache *cache = SessionCacheGetLockedInstance();
441	OSStatus serr;
442    if (!cache)
443        serr = errSSLSessionNotFound;
444    else
445    {
446        serr = SessionCacheDeleteEntry(cache, &sessionKey);
447    }
448
449    pthread_mutex_unlock(&gSessionCacheLock);
450    return serr;
451}
452
453/* cleanup up session cache, deleting stale entries. */
454OSStatus sslCleanupSession(void)
455{
456    SessionCache *cache = SessionCacheGetLockedInstance();
457	OSStatus serr = errSecSuccess;
458	bool moreToGo = false;
459
460    if (!cache)
461        serr = errSSLSessionNotFound;
462    else
463    {
464        moreToGo = SessionCacheCleanup(cache);
465    }
466	/* Possible TBD: if moreToGo, schedule a timed callback to this function */
467
468    pthread_mutex_unlock(&gSessionCacheLock);
469    return serr;
470}
471