/*
 * Copyright (c) 2005, 2006 Sendmail, Inc. and its suppliers.
 *	All rights reserved.
 *
 * By using this file, you agree to the terms and conditions set
 * forth in the LICENSE file which can be found at the top level of
 * the sendmail distribution.
 */

#include "sm/generic.h"
SM_RCSID("@(#)$Id: greyctl.c,v 1.19 2007/01/26 05:36:24 ca Exp $")

#include "sm/assert.h"
#include "sm/error.h"
#include "sm/memops.h"
#include "sm/heap.h"
#include "sm/queue.h"
#include "sm/net.h"
#include "sm/greyctl.h"
#include "sm/map.h"
#include "sm/bdb.h"
#if MTA_USE_PTHREADS
#include "sm/pthread.h"
#endif
#include "greyctl.h"

/*
**  SM_GREY_COMPARE -- compare two time stamps from DB (callback)
**
**	Parameters:
**		dbp -- ignored
**		d1 -- data 1
**		d2 -- data 2
**
**	Returns:
**		data 1 minus data 2 (as time_t)
*/

int
sm_grey_compare(DB *dbp, const DBT *d1, const DBT *d2)
{
	time_t t1, t2;

	sm_memcpy(&t1, d1->data, sizeof(time_t));
	sm_memcpy(&t2, d2->data, sizeof(time_t));
	return (t1 - t2);
}

/*
**  SM_GREY_SI -- get secondary index from main index (callback)
**
**	Parameters:
**		db -- ignored
**		pkey -- primary key
**		pdata -- primary data
**		skey -- secondary key (output)
**
**	Returns:
**		0
*/

int
sm_grey_si(DB *db, const DBT *pkey, const DBT *pdata, DBT *skey)
{
	SM_REQUIRE(skey != NULL);
	SM_REQUIRE(pdata != NULL);

	sm_memzero(skey, sizeof(skey));
	skey->data = (void *)&(((greyentry_P) pdata->data)->ge_expire);
	skey->size = sizeof(((greyentry_P) pdata->data)->ge_expire);
	return 0;
}

/*
**  SM_GREY_OPENDB -- open databases
**
**	Parameters:
**		db_name -- name of primary DB
**		db -- primary DB (output)
**		sdb_name -- name of secondary DB
**		sdb -- secondary DB (output)
**
**	Returns:
**		usual sm_error code
**
**	ToDo: error handling
*/

static sm_ret_T
sm_grey_opendb(const char *db_name, DB **pdb, const char *sdb_name, DB **psdb)
{
	int ret;
	DB *db, *sdb;
#define dbenv	NULL

	SM_REQUIRE(db_name != NULL);
	SM_REQUIRE(pdb != NULL);
	SM_REQUIRE(sdb_name != NULL);
	SM_REQUIRE(psdb != NULL);

	/* create/open primary */
	ret = db_create(&db, dbenv, 0);
	if (ret != 0)
		return BDB_ERR2RET(ret);
	ret = db->open(db, NULL, db_name, NULL, DB_BTREE, DB_CREATE, 0600);
	if (ret != 0)
		return BDB_ERR2RET(ret);

	/* create/open secondary */
	ret = db_create(&sdb, dbenv, 0);
	if (ret != 0)
		return BDB_ERR2RET(ret);
	ret = sdb->set_bt_compare(sdb, sm_grey_compare);
	if (ret != 0)
		return BDB_ERR2RET(ret);
	ret = sdb->set_flags(sdb, DB_DUP | DB_DUPSORT);
	if (ret != 0)
		return BDB_ERR2RET(ret);
	ret = sdb->open(sdb, NULL, sdb_name, NULL, DB_BTREE, DB_CREATE, 0600);
	if (ret != 0)
		return BDB_ERR2RET(ret);

	/* Associate the secondary with the primary. */
	ret = db->associate(db, NULL, sdb, sm_grey_si, 0);
	if (ret != 0)
		return BDB_ERR2RET(ret);

	*pdb = db;
	*psdb = sdb;
	return SM_SUCCESS;
}

/*
**  SM_GREYCTL_FREE -- free grey control context
**
**	Parameters:
**		greyctx -- connection control context
**
**	Returns:
**		usual sm_error code
*/

sm_ret_T
sm_greyctl_free(greyctx_P greyctx)
{
#if MTA_USE_PTHREADS
	int r;
#endif

	if (NULL == greyctx)
		return SM_SUCCESS;

#if MTA_USE_PTHREADS
	r = pthread_mutex_lock(&greyctx->greyc_mutex);
	SM_LOCK_OK(r);
#endif
	if (greyctx->greyc_grey_sdb != NULL) {
		greyctx->greyc_grey_sdb->close(greyctx->greyc_grey_sdb, 0);
		greyctx->greyc_grey_sdb = NULL;
	}

	if (greyctx->greyc_grey_db != NULL) {
		greyctx->greyc_grey_db->close(greyctx->greyc_grey_db, 0);
		greyctx->greyc_grey_db = NULL;
	}

#if MTA_USE_PTHREADS
	r = pthread_mutex_destroy(&greyctx->greyc_mutex);
#endif
	sm_free_size(greyctx, sizeof(*greyctx));
#if SM_GREYCTL_CHECK
	greyctx->sm_magic = SM_MAGIC_NULL;
#endif
	return SM_SUCCESS;
}

/*
**  SM_GREYCTL_CRT -- create new grey control context
**
**	Parameters:
**		pgreyctx -- grey control context (output)
**
**	Returns:
**		usual sm_error code
*/

sm_ret_T
sm_greyctl_crt(greyctx_P *pgreyctx)
{
	sm_ret_T ret;
	greyctx_P greyctx;
#if MTA_USE_PTHREADS
	int r;
#endif

	SM_REQUIRE(pgreyctx != NULL);
	ret = SM_SUCCESS;

	greyctx = (greyctx_P) sm_zalloc(sizeof(*greyctx));
	if (NULL == greyctx)
		return sm_err_temp(ENOMEM);
#if MTA_USE_PTHREADS
	r = pthread_mutex_init(&greyctx->greyc_mutex, SM_PTHREAD_MUTEXATTR);
	if (r != 0) {
		ret = sm_err_perm(r);
		goto error;
	}
#endif

	greyctx->greyc_used = 0;
	greyctx->greyc_cnf.greycnf_limit = 10000;
	greyctx->greyc_cnf.greycnf_min_grey_wait = 60;
	greyctx->greyc_cnf.greycnf_max_grey_wait = 60 * 60 * 24;
	greyctx->greyc_cnf.greycnf_white_expire = 60 * 60 * 24 * 33;
	greyctx->greyc_cnf.greycnf_white_reconfirm = 60 * 60 * 24 * 40;

	/* CONF */
	greyctx->greyc_cnf.greycnf_grey_name = "grey_grey_m.db";
	greyctx->greyc_cnf.greycnf_grey_sname = "grey_grey_s.db";

#if SM_GREYCTL_CHECK
	greyctx->sm_magic = SM_GREYCTL_MAGIC;
#endif

	*pgreyctx = greyctx;
	return ret;

#if MTA_USE_PTHREADS
  error:
	SM_FREE_SIZE(greyctx, sizeof(*greyctx));
	return ret;
#endif
}

/*
**  SM_GREYCTL_OPEN -- open grey control context
**
**	Parameters:
**		greyctx -- grey control context
**
**	Returns:
**		usual sm_error code
*/

sm_ret_T
sm_greyctl_open(greyctx_P greyctx, greycnf_P greycnf)
{
	sm_ret_T ret;

	SM_IS_GREYCTX(greyctx);

	if (greycnf != NULL)
		sm_memcpy(&greyctx->greyc_cnf, greycnf, sizeof(greyctx->greyc_cnf));
	if (greyctx->greyc_cnf.greycnf_netmask == 0)
		greyctx->greyc_cnf.greycnf_netmask = (ipv4_T) 0xFFFFFFFF;

	ret = sm_grey_opendb(
		greyctx->greyc_cnf.greycnf_grey_name, &greyctx->greyc_grey_db,
		greyctx->greyc_cnf.greycnf_grey_sname, &greyctx->greyc_grey_sdb);
	return ret;
}

/*
**  SM_GREY_EXPIRE1 -- remove one entry
**
**	Parameters:
**		db -- main DB
**		sdb -- secondary DB
**		now -- time of connection
**
**	Returns:
**		usual sm_error code
*/

static sm_ret_T
sm_grey_expire1(DB *db, DB *sdb, time_t now)
{
	sm_ret_T ret;
	DBC *dbcp;
	DBT db_data, db_key;
	greyentry_T ge;

	ret = db->cursor(sdb, NULL, &dbcp, 0);
	if (ret != 0)
		return ret;

	sm_memzero(&db_key, sizeof(db_key));
	sm_memzero(&db_data, sizeof(db_data));
	db_data.flags = DB_DBT_USERMEM;
	db_data.data = ≥
	db_data.ulen = sizeof(ge);

	/* this could be done in a loop for some elements */
	ret = dbcp->c_get(dbcp, &db_key, &db_data, DB_LAST);
	if (0 == ret) {
		if (ge.ge_time > now) {
			ret = dbcp->c_del(dbcp, 0);
			if (0 == ret)
				ret = 1;
		}
	}
	ret = dbcp->c_close(dbcp);
	return ret;
}

/*
**  SM_GREY_EXPIRE -- remove some entries
**
**	Parameters:
**		greyctx -- connection control context
**		t -- time of connection
**
**	Returns:
**		usual sm_error code
*/

static sm_ret_T
sm_grey_expire(greyctx_P greyctx, time_t t)
{
	sm_ret_T ret;

	ret = sm_grey_expire1(greyctx->greyc_grey_db, greyctx->greyc_grey_sdb, t);
	return ret;
}

#define GE_KEY(ge)    ((ge)->ge_key)
#define GE_LEN(ge)    ((ge)->ge_len)

/*
**  SM_GREY_FIND -- find an entry
**
**	Parameters:
**
**	Returns:
**		usual sm_error code
*/

static sm_ret_T
sm_grey_find(DB *db, void *key, uint len, greyentry_P ge)
{
	sm_ret_T ret;
	DBT db_data, db_key;

	sm_memzero(&db_key, sizeof(db_key));
	sm_memzero(&db_data, sizeof(db_data));
	db_key.data = key;
	db_key.size = len;
	db_data.flags = DB_DBT_USERMEM;
	db_data.data = ge;
	db_data.ulen = sizeof(*ge);
	ret = db->get(db, NULL, &db_key, &db_data, 0);
	return ret;
}

/*
**  SM_GREY_ADD -- add an entry
**
**	Parameters:
**		db -- main DB
**		ge -- grey_entry to be added
**
**	Returns:
**		usual sm_error code
*/

static sm_ret_T
sm_grey_add(DB *db, greyentry_P ge)
{
	sm_ret_T ret;
	DBT db_data, db_key;

	sm_memzero(&db_key, sizeof(db_key));
	sm_memzero(&db_data, sizeof(db_data));
	db_key.data = GE_KEY(ge);
	db_key.size = GE_LEN(ge);
	db_data.data = ge;
	db_data.size = sizeof(*ge);
	ret = db->put(db, NULL, &db_key, &db_data, 0);
	return ret;
}

/*
**  SM_GREY_RM -- remove an entry
**
**	Parameters:
**		db -- main DB
**		key -- key to be removed
**		len -- length of key
**
**	Returns:
**		usual sm_error code
*/

sm_ret_T
sm_grey_rm(DB *db, void *key, uint len)
{
	sm_ret_T ret;
	DBT db_key;

	sm_memzero(&db_key, sizeof(db_key));
	db_key.data = key;
	db_key.size = len;
	ret = db->del(db, NULL, &db_key, 0);
	return ret;
}

/*
**  SM_GREYCTL -- perform greylisting checks
**
**	Parameters:
**		greyctx -- connection control context
**		key -- key
**		keylen -- length of key
**		t -- time of connection
**
**	Returns:
**		SM_GREY_OK: ok to accept now (just moved from grey to white)
**		SM_GREY_WHITE: entry is whitelisted by now
**		SM_GREY_FIRST: new entry
**		SM_GREY_WAIT: need to wait
**		SM_GREY_AGAIN: was whitelisted, but expired
**		<0: usual sm_error code
**
**	Question: how to handle overflows of the table?
**		just return an error if hash table limit is reached.
*/

#define GREL_REMOVE_WHITE(greyctx, ge)	do { \
		ret = sm_grey_rm((greyctx)->greyc_grey_db, GE_KEY(ge), GE_LEN(ge));\
		if (sm_is_err(ret))					\
			goto error;					\
	} while (0)
#define GREL_REMOVE_GREY(greyctx, ge)	do { \
		ret = sm_grey_rm((greyctx)->greyc_grey_db, GE_KEY(ge), GE_LEN(ge));\
		if (sm_is_err(ret))					\
			goto error;					\
	} while (0)
#define GREL_APPEND_WHITE(greyctx, ge)	do { \
		ret = sm_grey_add((greyctx)->greyc_grey_db, (ge));	\
		if (sm_is_err(ret))					\
			goto error;					\
	} while (0)
#define GREL_APPEND_GREY(greyctx, ge)	do { \
		ret = sm_grey_add((greyctx)->greyc_grey_db, (ge));	\
		if (sm_is_err(ret))					\
			goto error;					\
	} while (0)

sm_ret_T
sm_greyctl(greyctx_P greyctx, uchar *key, uint keylen, time_t t)
{
	sm_ret_T ret;
	greyentry_T ges;
	greyentry_P ge;
	size_t len;
#if MTA_USE_PTHREADS
	int r;
#endif

	if (NULL == greyctx)
		return sm_err_perm(SM_E_NOMAP);

	SM_IS_GREYCTX(greyctx);

#if MTA_USE_PTHREADS
	r = pthread_mutex_lock(&greyctx->greyc_mutex);
	SM_LOCK_OK(r);
	if (r != 0) {
		/* LOG? */
		return sm_err_perm(r);
	}
#endif

	ge = &ges;
	ret = sm_grey_find(greyctx->greyc_grey_db, key, keylen, ge);
	if (ret != 0) {
		/* does not exist: add it and return "wait" */
		if (greyctx->greyc_used >= greyctx->greyc_cnf.greycnf_limit)
			ret = sm_grey_expire(greyctx, t);
		++greyctx->greyc_used;

		len = keylen;
		if (len >= sizeof(ge->ge_key))
			len = sizeof(ge->ge_key);
		sm_memcpy(ge->ge_key, key, len);
		ge->ge_len = len;
		ge->ge_time = t;
		ge->ge_expire = t + greyctx->greyc_cnf.greycnf_max_grey_wait;
		ge->ge_type = GREY_TYPE_GREY;

		ret = sm_grey_add(greyctx->greyc_grey_db, ge);
		if (sm_is_err(ret)) {
			SM_ASSERT(greyctx->greyc_used > 0);
			--greyctx->greyc_used;
			goto error;
		}
		ret = SM_GREY_FIRST;
	}
	else if (GREY_TYPE_WHITE == ge->ge_type)
	{
		if (ge->ge_time + greyctx->greyc_cnf.greycnf_white_reconfirm <= t) {
			/* it's too old: need to reconfirm */
			GREL_REMOVE_WHITE(greyctx, ge);
			if (sm_is_err(ret))
				goto error;
			ge->ge_time = t;
			ge->ge_expire = t + greyctx->greyc_cnf.greycnf_max_grey_wait;
			ge->ge_type = GREY_TYPE_GREY;
			GREL_APPEND_GREY(greyctx, ge);
			ret = SM_GREY_AGAIN;
		}
		else if (ge->ge_time != t) {
			/* update time stamps */
			GREL_REMOVE_WHITE(greyctx, ge);
			ge->ge_time = t;
			ge->ge_expire = t + greyctx->greyc_cnf.greycnf_white_expire;
			GREL_APPEND_WHITE(greyctx, ge);
			ret = SM_GREY_WHITE;
		}
		else
			ret = SM_GREY_WHITE;
	}
	else if (GREY_TYPE_GREY == ge->ge_type) {
		if (ge->ge_time + greyctx->greyc_cnf.greycnf_max_grey_wait <= t) {
			/* confirmation window passed; restart it */
			GREL_REMOVE_GREY(greyctx, ge);
			ge->ge_time = t;
			ge->ge_expire = t + greyctx->greyc_cnf.greycnf_max_grey_wait;
			GREL_APPEND_GREY(greyctx, ge);
			ret = SM_GREY_WAIT;
		}
		else if (ge->ge_time + greyctx->greyc_cnf.greycnf_min_grey_wait <= t)
		{
			/* waiting time expired, within confirmation window */
			GREL_REMOVE_GREY(greyctx, ge);
			ge->ge_type = GREY_TYPE_WHITE;
			ge->ge_time = t;
			ge->ge_expire = t + greyctx->greyc_cnf.greycnf_white_expire;
			GREL_APPEND_WHITE(greyctx, ge);
			ret = SM_GREY_OK;
		}
		else
			ret = SM_GREY_WAIT;
	}
	else
		ret = sm_err_perm(SM_E_UNEXPECTED);

	/* fall through for unlocking */
  error:
#if MTA_USE_PTHREADS
	r = pthread_mutex_unlock(&greyctx->greyc_mutex);
	SM_ASSERT(0 == r);
	/* r isn't checked further; will fail on next iteration */
#endif
	return ret;
}


syntax highlighted by Code2HTML, v. 0.9.1