/* ** Copyright (c) 2007 Sendmail, Inc. and its suppliers. ** All rights reserved. */ #ifdef QUERY_CACHE #ifndef lint static char dkim_cache_c_id[] = "@(#)$Id: dkim-cache.c,v 1.20 2007/10/24 06:32:56 msk Exp $"; #endif /* !lint */ /* system includes */ #include #include #include #include #include #include #include #include /* libsm includes */ #include /* libdb includes */ #include /* libdkim includes */ #include "dkim.h" #include "dkim-cache.h" /* limits, macros, etc. */ #define BUFRSZ 1024 #define DB_MODE (S_IRUSR|S_IWUSR) #ifndef DB_NOTFOUND # define DB_NOTFOUND 1 #endif /* ! DB_NOTFOUND */ #ifndef DB_VERSION_MAJOR # define DB_VERSION_MAJOR 1 #endif /* ! DB_VERSION_MAJOR */ #define DB_VERSION_CHECK(x,y,z) ((DB_VERSION_MAJOR == (x) && \ DB_VERSION_MINOR >= (y)) || \ DB_VERSION_MAJOR > (x)) /* data types */ struct dkim_cache_entry { int cache_ttl; time_t cache_when; char cache_data[BUFRSZ + 1]; }; /* globals */ static pthread_mutex_t cache_stats_lock; /* stats lock */ static u_int c_hits = 0; /* cache hits */ static u_int c_queries = 0; /* cache queries */ static u_int c_expired = 0; /* expired cache hits */ #if DB_VERSION_MAJOR == 1 static pthread_mutex_t cache_lock; /* cache lock */ #else /* DB_VERSION_MAJOR == 1 */ # ifdef PTHREAD_RWLOCK_INITIALIZER static pthread_rwlock_t cache_rwlock; /* cache lock */ # else /* PTHREAD_RWLOCK_INITIALIZER */ static pthread_mutex_t cache_lock; /* cache lock */ static u_int r_wait; /* readers waiting */ static u_int r_active; /* readers active */ static u_int w_wait; /* writers waiting */ static u_int w_active; /* writers waiting */ static pthread_cond_t writer_go; /* writer should enter */ static pthread_cond_t reader_go; /* reader should enter */ # endif /* PTHREAD_RWLOCK_INITIALIZER */ #endif /* DB_VERSION_MAJOR == 1 */ /* ** DKIM_CACHE_READ_LOCK -- acquire a read lock ** ** Parameters: ** None. ** ** Return value: ** None. */ static void dkim_cache_read_lock(void) { #if DB_VERSION_CHECK(2,0,0) # ifdef PTHREAD_RWLOCK_INITIALIZER (void) pthread_rwlock_rdlock(&cache_rwlock); # else /* PTHREAD_RWLOCK_INITIALIZER */ (void) pthread_mutex_lock(&cache_lock); if (w_wait > 0 || w_active > 0) { r_wait++; while (w_wait > 0 || w_active > 0) (void) pthread_cond_wait(&reader_go, &cache_lock); r_wait--; } r_active++; (void) pthread_mutex_unlock(&cache_lock); # endif /* PTHREAD_RWLOCK_INITIALIZER */ #else /* DB_VERSION_CHECK(2,0,0) */ (void) pthread_mutex_lock(&cache_lock); #endif /* DB_VERSION_CHECK(2,0,0) */ } /* ** DKIM_CACHE_READ_UNLOCK -- release a read lock ** ** Parameters: ** None. ** ** Return value: ** None. */ static void dkim_cache_read_unlock(void) { #if DB_VERSION_CHECK(2,0,0) # ifdef PTHREAD_RWLOCK_INITIALIZER (void) pthread_rwlock_unlock(&cache_rwlock); # else /* PTHREAD_RWLOCK_INITIALIZER */ (void) pthread_mutex_lock(&cache_lock); r_active--; if (r_active == 0 && w_wait > 0) (void) pthread_cond_signal(&writer_go); (void) pthread_mutex_unlock(&cache_lock); # endif /* PTHREAD_RWLOCK_INITIALIZER */ #else /* DB_VERSION_CHECK(2,0,0) */ (void) pthread_mutex_unlock(&cache_lock); #endif /* DB_VERSION_CHECK(2,0,0) */ } /* ** DKIM_CACHE_WRITE_LOCK -- acquire a write lock ** ** Parameters: ** None. ** ** Return value: ** None. */ static void dkim_cache_write_lock(void) { #if DB_VERSION_CHECK(2,0,0) # ifdef PTHREAD_RWLOCK_INITIALIZER (void) pthread_rwlock_wrlock(&cache_rwlock); # else /* PTHREAD_RWLOCK_INITIALIZER */ (void) pthread_mutex_lock(&cache_lock); if (w_active > 0 || r_active > 0) { w_wait++; while (w_active > 0 || r_active > 0) (void) pthread_cond_wait(&writer_go, &cache_lock); w_wait--; } w_active++; assert(w_active == 1); (void) pthread_mutex_unlock(&cache_lock); # endif /* PTHREAD_RWLOCK_INITIALIZER */ #else /* DB_VERSION_CHECK(2,0,0) */ (void) pthread_mutex_lock(&cache_lock); #endif /* DB_VERSION_CHECK(2,0,0) */ } /* ** DKIM_CACHE_WRITE_UNLOCK -- release a write lock ** ** Parameters: ** None. ** ** Return value: ** None. */ static void dkim_cache_write_unlock(void) { #if DB_VERSION_CHECK(2,0,0) # ifdef PTHREAD_RWLOCK_INITIALIZER (void) pthread_rwlock_unlock(&cache_rwlock); # else /* PTHREAD_RWLOCK_INITIALIZER */ (void) pthread_mutex_lock(&cache_lock); w_active--; assert(w_active == 0); if (w_wait > 0) (void) pthread_cond_broadcast(&writer_go); else if (r_wait > 0) (void) pthread_cond_broadcast(&reader_go); (void) pthread_mutex_unlock(&cache_lock); # endif /* PTHREAD_RWLOCK_INITIALIZER */ #else /* DB_VERSION_CHECK(2,0,0) */ (void) pthread_mutex_unlock(&cache_lock); #endif /* DB_VERSION_CHECK(2,0,0) */ } /* ** DKIM_CACHE_INIT -- initialize an on-disk cache of entries ** ** Parameters: ** err -- error code (returned) ** tmpdir -- temporary directory to use (may be NULL) ** ** Return value: ** A DB handle referring to the cache, or NULL on error. */ DB * dkim_cache_init(int *err, char *tmpdir) { int status = 0; DB *cache = NULL; c_hits = 0; c_queries = 0; c_expired = 0; (void) pthread_mutex_init(&cache_stats_lock, NULL); #if DB_VERSION_MAJOR == 1 (void) pthread_mutex_init(&cache_lock, NULL); #else /* DB_VERSION_MAJOR == 1 */ # ifdef PTHREAD_RWLOCK_INITIALIZER (void) pthread_rwlock_init(&cache_rwlock, NULL); # else /* PTHREAD_RWLOCK_INITIALIZER */ (void) pthread_mutex_init(&cache_lock, NULL); (void) pthread_cond_init(&reader_go, NULL); (void) pthread_cond_init(&writer_go, NULL); r_wait = 0; r_active = 0; w_wait = 0; # endif /* PTHREAD_RWLOCK_INITIALIZER */ #endif /* DB_VERSION_MAJOR == 1 */ #if DB_VERSION_CHECK(3,0,0) status = db_create(&cache, NULL, 0); if (status == 0) { # if DB_VERSION_CHECK(4,0,0) # if DB_VERSION_CHECK(4,2,0) if (tmpdir != NULL && tmpdir[0] != '\0') { DB_ENV *env = NULL; # if DB_VERSION_CHECK(4,3,0) env = cache->get_env(cache); # else /* DB_VERSION_CHECK(4,3,0) */ (void) cache->get_env(cache, &env); # endif /* DB_VERISON_CHECK(4,3,0) */ if (env != NULL) (void) env->set_tmp_dir(env, tmpdir); } # endif /* DB_VERISON_CHECK(4,2,0) */ status = cache->open(cache, NULL, NULL, NULL, DB_HASH, (DB_CREATE|DB_THREAD), DB_MODE); # else /* DB_VERSION_CHECK(4,0,0) */ status = cache->open(cache, NULL, NULL, DB_HASH, (DB_CREATE|DB_THREAD), DB_MODE); # endif /* DB_VERSION_CHECK(4,0,0) */ } #elif DB_VERSION_CHECK(2,0,0) status = db_open(NULL, DB_HASH, (DB_CREATE|DB_THREAD), DB_MODE, NULL, NULL, &cache); #else /* ! DB_VERSION_CHECK(2,0,0) */ cache = dbopen(NULL, (O_CREAT|O_RDWR), DB_MODE, DB_HASH, NULL); if (cache == NULL) status = errno; #endif /* DB_VERSION_CHECK */ if (status != 0) { if (err != NULL) *err = status; return NULL; } #ifdef DEBUG printf("libdkim cache initialized\n"); #endif /* DEBUG */ return cache; } /* ** DKIM_CACHE_QUERY -- query an on-disk cache of entries ** ** Parameters: ** db -- DB handle referring to the cache ** str -- key to query ** ttl -- time-to-live; ignore any record older than this; if 0, apply ** the TTL in the record ** buf -- buffer into which to write any cached data found ** buflen -- number of bytes at "buffer" (returned); caller should set ** this to the maximum space available and use the returned ** value as the length of the data returned ** err -- error code (returned) ** ** Return value: ** -1 -- error; caller should check "err" ** 0 -- no error; record found and data returned ** 1 -- no data found or data has expired */ int dkim_cache_query(DB *db, char *str, int ttl, char *buf, size_t *buflen, int *err) { int status; time_t now; DBT q; DBT d; struct dkim_cache_entry ce; assert(db != NULL); assert(str != NULL); assert(buf != NULL); assert(err != NULL); memset(&q, '\0', sizeof q); memset(&d, '\0', sizeof d); q.data = str; q.size = strlen(q.data); #ifdef DEBUG printf("libdkim cache query for `%s'\n", str); #endif /* DEBUG */ #if DB_VERSION_CHECK(3,0,0) d.flags = DB_DBT_USERMEM; d.data = (void *) &ce; d.ulen = sizeof ce; #endif /* DB_VERSION_CHECK(3,0,0) */ (void) time(&now); pthread_mutex_lock(&cache_stats_lock); c_queries++; pthread_mutex_unlock(&cache_stats_lock); dkim_cache_read_lock(); #if DB_VERSION_CHECK(2,0,0) status = db->get(db, NULL, &q, &d, 0); #else /* DB_VERSION_CHECK(2,0,0) */ status = db->get(db, &q, &d, 0); #endif /* DB_VERSION_CHECK(2,0,0) */ dkim_cache_read_unlock(); if (status == 0) { #if !DB_VERSION_CHECK(2,0,0) memset(&ce, '\0', sizeof ce); memcpy(&ce, d.data, MIN(sizeof ce, d.size)); #endif /* ! DB_VERSION_CHECK(2,0,0) */ if (ttl != 0) ce.cache_ttl = ttl; if (ce.cache_when + ce.cache_ttl < now) { #ifdef DEBUG printf("libdkim cache query for `%s' expired\n", str); #endif /* DEBUG */ pthread_mutex_lock(&cache_stats_lock); c_expired++; pthread_mutex_unlock(&cache_stats_lock); return 1; } pthread_mutex_lock(&cache_stats_lock); c_hits++; pthread_mutex_unlock(&cache_stats_lock); sm_strlcpy(buf, ce.cache_data, *buflen); *buflen = strlen(ce.cache_data); #ifdef DEBUG printf("libdkim cache query for `%s' found: `%s'\n", str, buf); #endif /* DEBUG */ return 0; } else if (status != DB_NOTFOUND) { *err = status; #ifdef DEBUG printf("libdkim cache query for `%s' error\n"); #endif /* DEBUG */ return -1; } else { #ifdef DEBUG printf("libdkim cache query for `%s' not found\n", str); #endif /* DEBUG */ return 1; } } /* ** DKIM_CACHE_INSERT -- insert data into an on-disk cache of entries ** ** Parameters: ** db -- DB handle referring to the cache ** str -- key to insert ** data -- data to insert ** ttl -- time-to-live ** err -- error code (returned) ** ** Return value: ** -1 -- error; caller should check "err" ** 0 -- cache updated */ int dkim_cache_insert(DB *db, char *str, char *data, int ttl, int *err) { int status; time_t now; DBT q; DBT d; struct dkim_cache_entry ce; assert(db != NULL); assert(str != NULL); assert(data != NULL); assert(err != NULL); (void) time(&now); memset(&q, '\0', sizeof q); memset(&d, '\0', sizeof d); q.data = str; q.size = strlen(str); d.data = (void *) &ce; d.size = sizeof ce; ce.cache_when = now; ce.cache_ttl = ttl; sm_strlcpy(ce.cache_data, data, sizeof ce.cache_data); dkim_cache_write_lock(); #if DB_VERSION_CHECK(2,0,0) status = db->put(db, NULL, &q, &d, 0); #else /* DB_VERSION_CHECK(2,0,0) */ status = db->put(db, &q, &d, 0); #endif /* DB_VERSION_CHECK(2,0,0) */ dkim_cache_write_unlock(); if (status == 0) { #ifdef DEBUG printf("libdkim cache insert for `%s': `%s'\n", str, data); #endif /* DEBUG */ return 0; } else { *err = status; #ifdef DEBUG printf("libdkim cache insert for `%s' error\n"); #endif /* DEBUG */ return -1; } } /* ** DKIM_CACHE_EXPIRE -- expire records in an on-disk cache of entries ** ** Parameters: ** db -- DB handle referring to the cache ** ttl -- time-to-live; delete any record older than this; if 0, apply ** the TTL in the record ** err -- error code (returned) ** ** Return value: ** -1 -- error; caller should check "err" ** otherwise -- count of deleted records */ int dkim_cache_expire(DB *db, int ttl, int *err) { #if !DB_VERSION_CHECK(2,0,0) bool first = TRUE; #endif /* ! DB_VERSION_CHECK(2,0,0) */ bool delete; int deleted = 0; int status; time_t now; #if DB_VERSION_CHECK(2,0,0) DBC *dbc; #endif /* DB_VERSION_CHECK(2,0,0) */ DBT q; DBT d; char name[DKIM_MAXHOSTNAMELEN + 1]; struct dkim_cache_entry ce; assert(db != NULL); assert(err != NULL); memset(&q, '\0', sizeof q); memset(&d, '\0', sizeof d); (void) time(&now); dkim_cache_write_lock(); #if DB_VERSION_CHECK(2,0,0) status = db->cursor(db, NULL, &dbc, 0); if (status != 0) { *err = status; dkim_cache_write_unlock(); return -1; } #endif /* DB_VERSION_CHECK(2,0,0) */ for (;;) { memset(name, '\0', sizeof name); memset(&ce, '\0', sizeof ce); #if DB_VERSION_CHECK(3,0,0) q.data = name; q.flags = DB_DBT_USERMEM; q.ulen = sizeof name; #endif /* DB_VERSION_CHECK(3,0,0) */ #if DB_VERSION_CHECK(3,0,0) d.data = (void *) &ce; d.flags = DB_DBT_USERMEM; d.ulen = sizeof ce; #endif /* DB_VERSION_CHECK(3,0,0) */ #if DB_VERSION_CHECK(2,0,0) status = dbc->c_get(dbc, &q, &d, DB_NEXT); if (status == DB_NOTFOUND) { break; } else if (status != 0) { *err = status; break; } #else /* DB_VERSION_CHECK(2,0,0) */ status = db->seq(db, &q, &d, first ? R_FIRST : R_NEXT); if (status == DB_NOTFOUND) { break; } else if (status != 0) { *err = status; break; } first = FALSE; memcpy(name, q.data, MIN(sizeof name, q.size)); memcpy((void *) &ce, d.data, MIN(sizeof ce, d.size)); #endif /* DB_VERSION_CHECK(2,0,0) */ delete = FALSE; if (ttl == 0) { if (ce.cache_when + ce.cache_ttl < now) delete = TRUE; } else { if (ce.cache_when + ttl < now) delete = TRUE; } if (delete) { #if DB_VERSION_CHECK(2,0,0) status = dbc->c_del(dbc, 0); #else /* DB_VERSION_CHECK(2,0,0) */ status = db->del(db, &q, R_CURSOR); #endif /* DB_VERSION_CHECK(2,0,0) */ if (status != 0) { *err = status; deleted = -1; break; } deleted++; } } #if DB_VERSION_CHECK(2,0,0) (void) dbc->c_close(dbc); #endif /* DB_VERSION_CHECK(2,0,0) */ dkim_cache_write_unlock(); return deleted; } /* ** DKIM_CACHE_CLOSE -- close a cache database ** ** Parameters: ** db -- cache DB handle ** ** Return value: ** None. */ void dkim_cache_close(DB *db) { assert(db != NULL); #if DB_VERSION_CHECK(2,0,0) (void) db->close(db, 0); #else /* DB_VERSION_CHECK(2,0,0) */ (void) db->close(db); #endif /* DB_VERSION_CHECK(2,0,0) */ #if DB_VERSION_MAJOR == 1 (void) pthread_mutex_destroy(&cache_lock); #else /* DB_VERSION_MAJOR == 1 */ # ifdef PTHREAD_RWLOCK_INITIALIZER (void) pthread_rwlock_destroy(&cache_rwlock); # else /* PTHREAD_RWLOCK_INITIALIZER */ (void) pthread_mutex_destroy(&cache_lock); (void) pthread_cond_destroy(&reader_go); (void) pthread_cond_destroy(&writer_go); # endif /* PTHREAD_RWLOCK_INITIALIZER */ #endif /* DB_VERSION_MAJOR == 1 */ } /* ** DKIM_CACHE_STATS -- retrieve cache performance statistics ** ** Parameters: ** queries -- number of queries handled (returned) ** hits -- number of cache hits (returned) ** expired -- number of expired hits (returned) ** ** Return value: ** None. ** ** Notes: ** Any of the parameters may be NULL if the corresponding datum ** is not of interest. */ void dkim_cache_stats(u_int *queries, u_int *hits, u_int *expired) { pthread_mutex_lock(&cache_stats_lock); if (queries != NULL) *queries = c_queries; if (hits != NULL) *hits = c_hits; if (expired != NULL) *expired = c_expired; pthread_mutex_unlock(&cache_stats_lock); } #endif /* QUERY_CACHE */