/*
**  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 <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <assert.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>

/* libsm includes */
#include <sm/string.h>

/* libdb includes */
#include <db.h>

/* 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 */


syntax highlighted by Code2HTML, v. 0.9.1