1 /* distcache, Distributed Session Caching technology
2  * Copyright (C) 2000-2003  Geoff Thorpe, and Cryptographic Appliances, Inc.
3  * Copyright (C) 2004       The Distcache.org project
4  *
5  * This library is free software; you can redistribute it and/or modify it under
6  * the terms of the GNU Lesser General Public License as published by the Free
7  * Software Foundation; using version 2.1 of the License. The copyright holders
8  * may elect to allow the application of later versions of the License to this
9  * software, please contact the author (geoff@distcache.org) if you wish us to
10  * review any later version released by the Free Software Foundation.
11  *
12  * This library is distributed in the hope that it will be useful, but WITHOUT
13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14  * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
15  * details.
16  *
17  * You should have received a copy of the GNU Lesser General Public License
18  * along with this library; if not, write to the Free Software Foundation, Inc.,
19  * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20  */
21 
22 #define SYS_GENERATING_LIB
23 
24 #include <libsys/pre.h>
25 #include <libnal/nal.h>
26 #include <distcache/dc_server.h>
27 #include <distcache/dc_plug.h>
28 #include <distcache/dc_internal.h>
29 #include <libsys/post.h>
30 
31 /* If you define DC_CACHE_DEBUG, the cached-lookup code in this default
32  * implementation will have extra debugging; it will print to stderr each time
33  * either "cached_removes", "cached_hits", or "cached_misses" hits a multiple of
34  * DC_CACHE_DEBUG_INTERVAL. */
35 /* #define DC_CACHE_DEBUG */
36 /* #define DC_CACHE_DEBUG_INTERVAL	100 */
37 
38 /******************************************/
39 /* The "DC_CACHE" structure details */
40 
41 /* stores a single cache item */
42 typedef struct st_DC_ITEM {
43 	/* The time at which we will expire this session (calculated locally -
44 	 * the client sends us a number of milli-seconds, and the server adds
45 	 * that to the local time when the "add" operation is processed). */
46 	struct timeval expiry;
47 	/* The length of the session_id and the encoded session respectively */
48 	unsigned int id_len, data_len;
49 	/* A block of memory containing the session_id followed by the encoded
50 	 * session. */
51 	unsigned char *ptr;
52 } DC_ITEM;
53 
54 struct st_DC_CACHE {
55 	/* Our session storage */
56 	DC_ITEM *items;
57 	unsigned int items_used, items_size;
58 	unsigned int expire_delta;
59 	/* Cached lookups. Mostly used so that a call to "DC_CACHE_get" with a
60 	 * NULL 'store' (to find the size of the session to be copied before
61 	 * finding room to copy it to) followed by another call with a non-NULL
62 	 * 'store' doesn't do two searches. It also helps if an "add" operation
63 	 * is followed immediately by a "get" or "remove" (eg. session
64 	 * resumption and renegotiation, respectively). */
65 	unsigned char cached_id[DC_MAX_ID_LEN];
66 	unsigned int cached_id_len;
67 	int cached_idx; /* -1 means a cached lookup for a session that doesn't
68 			   exist (so, don't bother looking) */
69 };
70 
71 /**********************************************************/
72 /* Internal functions to manage the "cached_lookup" stuff */
73 
74 #ifdef DC_CACHE_DEBUG
75 #ifndef DC_CACHE_DEBUG_INTERVAL
76 #error "Error, must define DC_CACHE_DEBUG_INTERVAL when DC_CACHE_DEBUG is defined"
77 #endif
78 static unsigned int cached_removes = 0;
79 static unsigned int cached_misses = 0;
80 static unsigned int cached_hits = 0;
81 
82 #define CACHED_REMOVE {if((++cached_removes % DC_CACHE_DEBUG_INTERVAL) == 0) \
83 		SYS_fprintf(SYS_stderr, "DC_CACHE_DEBUG: cached_removes = %u\n", \
84 				cached_removes); }
85 #define CACHED_MISS {if((++cached_misses % DC_CACHE_DEBUG_INTERVAL) == 0) \
86 		SYS_fprintf(SYS_stderr, "DC_CACHE_DEBUG: cached_misses = %u\n", \
87 				cached_misses); }
88 #define CACHED_HIT {if((++cached_hits % DC_CACHE_DEBUG_INTERVAL) == 0) \
89 		SYS_fprintf(SYS_stderr, "DC_CACHE_DEBUG: cached_hits = %u\n", \
90 				cached_hits); }
91 #else
92 #define CACHED_REMOVE
93 #define CACHED_MISS
94 #define CACHED_HIT
95 #endif
96 
97 /* 'num' entries have been scrolled off the front of the cache, take the
98  * appropriate action with the cached-lookup */
int_lookup_expired(DC_CACHE * cache,unsigned int num)99 static void int_lookup_expired(DC_CACHE *cache, unsigned int num)
100 {
101 	/* The whole list slipped 'num' to the left */
102 	cache->cached_idx -= num;
103 	/* If that put cached_idx into the red, it means the cached item has
104 	 * been removed. */
105 	if(cache->cached_idx < 0) {
106 		cache->cached_idx = -1;
107 		CACHED_REMOVE
108 	}
109 }
110 
111 /* A specific index in the cache has been removed - check if this affects the
112  * cached-lookup. */
int_lookup_removed(DC_CACHE * cache,unsigned int idx)113 static void int_lookup_removed(DC_CACHE *cache, unsigned int idx)
114 {
115 	/* At worst case, the last item in the array was deleted and
116 	 * cache->items_used has already been decremented so idx is the first
117 	 * unused spot in the array. */
118 	assert(idx <= cache->items_used);
119 	if(cache->cached_idx == (int)idx) {
120 		/* Our cached item was the one removed */
121 		cache->cached_idx = -1;
122 		CACHED_REMOVE
123 	} else if(cache->cached_idx > (int)idx)
124 		/* Our cached item was after the removed item, so the index has
125 		 * moved left */
126 		cache->cached_idx--;
127 }
128 
129 /* This function is to use the cached-lookup prior to doing an actual search */
int_lookup_check(DC_CACHE * cache,const unsigned char * session_id,unsigned int session_id_len,unsigned int * idx)130 static int int_lookup_check(DC_CACHE *cache,
131 			const unsigned char *session_id,
132 			unsigned int session_id_len,
133 			unsigned int *idx)
134 {
135 	if((cache->cached_idx < 0) ||
136 			(session_id_len != cache->cached_id_len) ||
137 			(memcmp(session_id, cache->cached_id,
138 				session_id_len) != 0)) {
139 		CACHED_MISS
140 		return 0;
141 	}
142 	*idx = (unsigned int)cache->cached_idx;
143 	CACHED_HIT
144 	return 1;
145 }
146 
147 /* An operation (eg. an 'add', a 'remove', or a 'find' for something that wasn't
148  * already cached) wants to specify a session to cache (or to mark the absence
149  * of any session corresponding to a given id). */
int_lookup_set(DC_CACHE * cache,const unsigned char * session_id,unsigned int session_id_len,int idx)150 static void int_lookup_set(DC_CACHE *cache,
151 			const unsigned char *session_id,
152 			unsigned int session_id_len,
153 			int idx)
154 {
155 	cache->cached_id_len = session_id_len;
156 	if(session_id_len)
157 		SYS_memcpy_n(unsigned char, cache->cached_id,
158 				session_id, session_id_len);
159 	cache->cached_idx = idx;
160 }
161 
162 /**************************************************************/
163 /* Internal functions to manage the session items in a server */
164 
int_pre_remove_DC_ITEM(DC_ITEM * item)165 static void int_pre_remove_DC_ITEM(DC_ITEM *item)
166 {
167 	SYS_free(unsigned char, item->ptr);
168 	item->ptr = NULL;
169 }
170 
int_force_expire(DC_CACHE * cache,unsigned int num)171 static void int_force_expire(DC_CACHE *cache, unsigned int num)
172 {
173 	assert((num > 0) && (num <= cache->items_used));
174 	/* Only "memmove" if we're not expiring everything */
175 	if(num < cache->items_used)
176 		SYS_memmove_n(DC_ITEM, cache->items, cache->items + num,
177 				cache->items_used - num);
178 	cache->items_used -= num;
179 	/* How does this affect cached lookups? */
180 	int_lookup_expired(cache, num);
181 }
182 
int_expire(DC_CACHE * cache,const struct timeval * now)183 static void int_expire(DC_CACHE *cache, const struct timeval *now)
184 {
185 	unsigned int idx = 0, toexpire = 0;
186 	DC_ITEM *item = cache->items;
187 	while((idx < cache->items_used) && (SYS_timecmp(now,
188 				&(item->expiry)) > 0)) {
189 		/* Do pre-remove cleanup but don't do the remove, this is
190 		 * because we can do one giant scroll in int_force_expire()
191 		 * rather than lots of little ones by calling
192 		 * int_remove_DC_ITEM(), for example. */
193 		int_pre_remove_DC_ITEM(item);
194 		idx++;
195 		item++;
196 		toexpire++;
197 	}
198 	if(toexpire)
199 		int_force_expire(cache, toexpire);
200 }
201 
int_find_DC_ITEM(DC_CACHE * cache,const unsigned char * ptr,unsigned int len,const struct timeval * now)202 static int int_find_DC_ITEM(DC_CACHE *cache, const unsigned char *ptr,
203 				unsigned int len, const struct timeval *now)
204 {
205 	unsigned int idx;
206 	DC_ITEM *item = cache->items;
207 	/* First flush out expired entries */
208 	int_expire(cache, now);
209 	/* See if we have a lookup for this session cached */
210 	if(int_lookup_check(cache, ptr, len, &idx))
211 		/* Yes! */
212 		return idx;
213 	idx = 0;
214 	while(idx < cache->items_used) {
215 		if((item->id_len == len) && (memcmp(item->ptr, ptr, len) == 0)) {
216 			/* Found the session - cache it and return the idx */
217 			int_lookup_set(cache, ptr, len, idx);
218 			return (int)idx;
219 		}
220 		idx++;
221 		item++;
222 	}
223 	return -1;
224 }
225 
int_add_DC_ITEM(DC_CACHE * cache,unsigned int idx,const struct timeval * expiry,const unsigned char * session_id,unsigned int session_id_len,const unsigned char * data,unsigned int data_len)226 static int int_add_DC_ITEM(DC_CACHE *cache, unsigned int idx,
227 		const struct timeval *expiry,
228 		const unsigned char *session_id, unsigned int session_id_len,
229 		const unsigned char *data, unsigned int data_len)
230 {
231 	unsigned char *ptr;
232 	DC_ITEM *item;
233 
234 	/* So we'll definitely insert - take care of the one remaining error
235 	 * possibility first, malloc. */
236 	ptr = SYS_malloc(unsigned char, session_id_len + data_len);
237 	if(!ptr)
238 		return 0;
239 	item = cache->items + idx;
240 	/* Do we need to shuffle existing items? */
241 	if(idx < cache->items_used)
242 		/* This is a genuine insertion rather than an append */
243 		SYS_memmove_n(DC_ITEM, item + 1, item,
244 				cache->items_used - idx);
245 	/* Populate the entry */
246 	SYS_timecpy(&item->expiry, expiry);
247 	item->ptr = ptr;
248 	item->id_len = session_id_len;
249 	item->data_len = data_len;
250 	SYS_memcpy_n(unsigned char, item->ptr, session_id, session_id_len);
251 	SYS_memcpy_n(unsigned char, item->ptr + item->id_len, data, data_len);
252 	cache->items_used++;
253 	/* Cache this item as a lookup */
254 	int_lookup_set(cache, session_id, session_id_len, idx);
255 	return 1;
256 }
257 
int_remove_DC_ITEM(DC_CACHE * cache,unsigned int idx)258 static void int_remove_DC_ITEM(DC_CACHE *cache, unsigned int idx)
259 {
260 	DC_ITEM *item;
261 	assert(idx < cache->items_used);
262 	item = cache->items + idx;
263 	int_pre_remove_DC_ITEM(item);
264 	cache->items_used--;
265 	if(idx < cache->items_used)
266 		SYS_memmove_n(DC_ITEM, cache->items + idx,
267 				cache->items + (idx + 1),
268 				cache->items_used - idx);
269 	int_lookup_removed(cache, idx);
270 }
271 
272 /*********************************************************/
273 /* Our high-level cache implementation handler functions */
274 
cache_new(unsigned int max_sessions)275 static DC_CACHE *cache_new(unsigned int max_sessions)
276 {
277 	DC_CACHE *toret;
278 	if((max_sessions < DC_CACHE_MIN_SIZE) ||
279 			(max_sessions > DC_CACHE_MAX_SIZE))
280 		return NULL;
281 	toret = SYS_malloc(DC_CACHE, 1);
282 	if(!toret)
283 		return NULL;
284 	toret->items = SYS_malloc(DC_ITEM, max_sessions);
285 	if(!toret->items) {
286 		SYS_free(DC_CACHE, toret);
287 		return NULL;
288 	}
289 	toret->items_used = 0;
290 	toret->items_size = max_sessions;
291 	/* Choose a "delta" for forced expiries. When making room for new
292 	 * sessions (ie. when full), how many do we force out at a time? */
293 	toret->expire_delta = max_sessions / 30;
294 	if(!toret->expire_delta)
295 		toret->expire_delta = 1;
296 	/* Make sure we have no weird cached-lookup state */
297 	int_lookup_set(toret, NULL, 0, -1);
298 	return toret;
299 }
300 
cache_free(DC_CACHE * cache)301 static void cache_free(DC_CACHE *cache)
302 {
303 	while(cache->items_used)
304 		int_remove_DC_ITEM(cache, cache->items_used - 1);
305 	SYS_free(DC_ITEM, cache->items);
306 	SYS_free(DC_CACHE, cache);
307 }
308 
cache_add_session(DC_CACHE * cache,const struct timeval * now,unsigned long timeout_msecs,const unsigned char * session_id,unsigned int session_id_len,const unsigned char * data,unsigned int data_len)309 static int cache_add_session(DC_CACHE *cache,
310 			const struct timeval *now,
311 			unsigned long timeout_msecs,
312 			const unsigned char *session_id,
313 			unsigned int session_id_len,
314 			const unsigned char *data,
315 			unsigned int data_len)
316 {
317 	/* Use 'idx' to search for the insertion point based on 'expiry' */
318 	DC_ITEM *item;
319 	int idx;
320 	struct timeval expiry;
321 
322 	/* The caller should already be making these checks */
323 	assert(session_id_len && data_len &&
324 			(session_id_len <= DC_MAX_ID_LEN) &&
325 			(data_len <= DC_MAX_DATA_LEN));
326 	/* Check if we already have this session (NB: this also flushes expired
327 	 * sessions out automatically). */
328 	idx = int_find_DC_ITEM(cache, session_id, session_id_len, now);
329 	if(idx >= 0)
330 		return 0;
331 	/* Do we need to forcibly expire entries to make room? */
332 	if(cache->items_used == cache->items_size)
333 		/* Yes, make room. We clear out 'expire_delta' sessions from the
334 		 * front of the queue. If this value is one we get logical
335 		 * behaviour but it's inefficient (we keep scrolling the entire
336 		 * array to the left one space). If it's greater than one, it's
337 		 * a little unfair on some extra sessions (expiring them when
338 		 * it's not strictly necessary), but we don't do nearly as many
339 		 * memmove operations. */
340 		int_force_expire(cache, cache->expire_delta);
341 	/* Set the time that the new session will expire */
342 	SYS_timeadd(&expiry, now, timeout_msecs);
343 	/* Find the insertion point based on expiry time */
344 	idx = cache->items_used;
345 	item = cache->items + idx;
346 	while(idx > 0)  {
347 		idx--;
348 		item--;
349 		/* So, if 'item' will expiry before or at the same time, we can
350 		 * insert immediately after it. */
351 		if(SYS_timecmp(&item->expiry, &expiry) <= 0) {
352 			idx++;
353 			item++;
354 			goto found;
355 		}
356 	}
357 	/* So, strangely, this item expires before all others (in reality, we're
358 	 * probably inserting into an empty list!). But because of the "idx++"
359 	 * logic, 'idx' and 'item' match the insertion/append point in all cases
360 	 * at this point. */
361 found:
362 	return int_add_DC_ITEM(cache, idx, &expiry, session_id,
363 					session_id_len, data, data_len);
364 }
365 
cache_get_session(DC_CACHE * cache,const struct timeval * now,const unsigned char * session_id,unsigned int session_id_len,unsigned char * store,unsigned int store_len)366 static unsigned int cache_get_session(DC_CACHE *cache,
367 			const struct timeval *now,
368 			const unsigned char *session_id,
369 			unsigned int session_id_len,
370 			unsigned char *store,
371 			unsigned int store_len)
372 {
373 	DC_ITEM *item;
374 	int idx = int_find_DC_ITEM(cache,
375 			session_id, session_id_len, now);
376 	if(idx < 0)
377 		return 0;
378 	item = cache->items + idx;
379 	if(store) {
380 		unsigned int towrite = item->data_len;
381 		assert(store_len > 0); /* no reason to accept store_len == 0 */
382 		if(towrite > store_len)
383 			towrite = store_len;
384 		SYS_memcpy_n(unsigned char, store,
385 			item->ptr + item->id_len, towrite);
386 	}
387 	return item->data_len;
388 }
389 
cache_remove_session(DC_CACHE * cache,const struct timeval * now,const unsigned char * session_id,unsigned int session_id_len)390 static int cache_remove_session(DC_CACHE *cache,
391 			const struct timeval *now,
392 			const unsigned char *session_id,
393 			unsigned int session_id_len)
394 {
395 	int idx = int_find_DC_ITEM(cache, session_id, session_id_len, now);
396 	if(idx < 0)
397 		return 0;
398 	int_remove_DC_ITEM(cache, idx);
399 	return 1;
400 }
401 
cache_have_session(DC_CACHE * cache,const struct timeval * now,const unsigned char * session_id,unsigned int session_id_len)402 static int cache_have_session(DC_CACHE *cache,
403 			const struct timeval *now,
404 			const unsigned char *session_id,
405 			unsigned int session_id_len)
406 {
407 	return (int_find_DC_ITEM(cache, session_id,
408 				session_id_len, now) < 0 ? 0 : 1);
409 }
410 
cache_items_stored(DC_CACHE * cache,const struct timeval * now)411 static unsigned int cache_items_stored(DC_CACHE *cache,
412 			const struct timeval *now)
413 {
414 	/* sneak in a quick flush of expired items. This actually makes this API
415 	 * function a convenient (low-overhead) way for the application to flush
416 	 * the cache. */
417 	int_expire(cache, now);
418 	return cache->items_used;
419 }
420 
421 /********************************************/
422 /* The only external function in this file! */
423 
424 /* First the static structure used in this hook function */
425 static const DC_CACHE_cb our_implementation = {
426 	cache_new,
427 	cache_free,
428 	cache_add_session,
429 	cache_get_session,
430 	cache_remove_session,
431 	cache_have_session,
432 	cache_items_stored
433 };
434 
DC_SERVER_set_default_cache(void)435 int DC_SERVER_set_default_cache(void)
436 {
437 	return DC_SERVER_set_cache(&our_implementation);
438 }
439