/*
 *   This program is is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or (at
 *   your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

/**
 * $Id: 16ecd5feeb67bb62081a59612c438540d0bea824 $
 * @file rlm_cache.c
 * @brief Cache values and merge them back into future requests.
 *
 * @copyright 2012-2014 The FreeRADIUS server project
 */
RCSID("$Id: 16ecd5feeb67bb62081a59612c438540d0bea824 $")

#include <freeradius-devel/radiusd.h>
#include <freeradius-devel/modules.h>
#include <freeradius-devel/modpriv.h>
#include <freeradius-devel/modcall.h>
#include <freeradius-devel/rad_assert.h>

#include "rlm_cache.h"

/*
 *	A mapping of configuration file names to internal variables.
 *
 *	Note that the string is dynamically allocated, so it MUST
 *	be freed.  When the configuration file parse re-reads the string,
 *	it free's the old one, and strdup's the new one, placing the pointer
 *	to the strdup'd string into 'config.string'.  This gets around
 *	buffer over-flows.
 */
static const CONF_PARSER module_config[] = {
	{ "driver", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_cache_t, driver_name), "rlm_cache_rbtree" },
	{ "key", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED | PW_TYPE_XLAT, rlm_cache_t, key), NULL },
	{ "ttl", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_cache_t, ttl), "500" },
	{ "max_entries", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_cache_t, max_entries), "0" },

	/* Should be a type which matches time_t, @fixme before 2038 */
	{ "epoch", FR_CONF_OFFSET(PW_TYPE_SIGNED, rlm_cache_t, epoch), "0" },
	{ "add_stats", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_cache_t, stats), "no" },

	{ NULL, -1, 0, NULL, NULL }		/* end the list */
};

static int cache_acquire(rlm_cache_handle_t **out, rlm_cache_t *inst, REQUEST *request)
{
	if (!inst->module->acquire) return 0;

	return inst->module->acquire(out, inst, request);
}

static void cache_release(rlm_cache_t *inst, REQUEST *request, rlm_cache_handle_t **handle)
{
	if (!inst->module->release) return;
	if (!handle || !*handle) return;

	inst->module->release(inst, request, handle);
}

static int cache_reconnect(rlm_cache_t *inst, REQUEST *request, rlm_cache_handle_t **handle)
{
	rad_assert(inst->module->reconnect);

	return inst->module->reconnect(inst, request, handle);
}

/** Allocate a cache entry
 *
 *  This is used so that drivers may use their own allocation functions
 *  to allocate structures larger than the normal rlm_cache_entry_t.
 *
 *  If the driver doesn't specify a custom allocation function, the cache
 *  entry is talloced in the NULL ctx.
 */
static rlm_cache_entry_t *cache_alloc(rlm_cache_t *inst, REQUEST *request)
{
	if (inst->module->alloc) return inst->module->alloc(inst, request);

	return talloc_zero(NULL, rlm_cache_entry_t);
}

/** Free memory associated with a cache entry
 *
 * This does not necessarily remove the entry from the cache, cache_expire
 * should be used for that.
 *
 * This function should be called when an entry that is known to have been
 * retrieved or inserted into a data store successfully, is no longer needed.
 *
 * Some drivers (like rlm_cache_rbtree) don't register a free function.
 * This means that the cache entry never needs to be explicitly freed.
 *
 * @param c Cache entry to free.
 * @param inst Module instance.
 */
static void cache_free(rlm_cache_t *inst, rlm_cache_entry_t **c)
{
	if (!c || !*c || !inst->module->free) return;

	inst->module->free(*c);
	*c = NULL;
}

/*
 *	Merge a cached entry into a REQUEST.
 */
static void CC_HINT(nonnull) cache_merge(rlm_cache_t *inst, REQUEST *request, rlm_cache_entry_t *c)
{
	VALUE_PAIR *vp;

	vp = pairfind(request->config, PW_CACHE_MERGE, 0, TAG_ANY);
	if (vp && (vp->vp_integer == 0)) {
		RDEBUG2("Told not to merge entry into request");
		return;
	}

	RDEBUG2("Merging cache entry into request");

	if (c->packet && request->packet) {
		rdebug_pair_list(L_DBG_LVL_2, request, c->packet, "&request:");
		radius_pairmove(request, &request->packet->vps, paircopy(request->packet, c->packet), false);
	}

	if (c->reply && request->reply) {
		rdebug_pair_list(L_DBG_LVL_2, request, c->reply, "&reply:");
		radius_pairmove(request, &request->reply->vps, paircopy(request->reply, c->reply), false);
	}

	if (c->control) {
		rdebug_pair_list(L_DBG_LVL_2, request, c->control, "&control:");
		radius_pairmove(request, &request->config, paircopy(request, c->control), false);
	}

	if (c->state) {
		rdebug_pair_list(L_DBG_LVL_2, request, c->state, "&session-state:");
		radius_pairmove(request, &request->state, paircopy(request->state, c->state), false);
	}

	if (inst->stats) {
		rad_assert(request->packet != NULL);
		vp = pairfind(request->packet->vps, PW_CACHE_ENTRY_HITS, 0, TAG_ANY);
		if (!vp) {
			vp = paircreate(request->packet, PW_CACHE_ENTRY_HITS, 0);
			rad_assert(vp != NULL);
			pairadd(&request->packet->vps, vp);
		}
		vp->vp_integer = c->hits;
	}
}


/** Find a cached entry.
 *
 * @return RLM_MODULE_OK on success, RLM_MODULE_FAIL on failure, RLM_MODULE_NOTFOUND if notfound.
 */
static rlm_rcode_t cache_find(rlm_cache_entry_t **out, rlm_cache_t *inst, REQUEST *request,
			      rlm_cache_handle_t **handle, char const *key)
{
	cache_status_t ret;

	rlm_cache_entry_t *c;

	*out = NULL;

	for (;;) {
		ret = inst->module->find(&c, inst, request, handle, key);
		switch (ret) {
		case CACHE_RECONNECT:
			RDEBUG("Reconnecting...");
			if (cache_reconnect(inst, request, handle) == 0) continue;
			return RLM_MODULE_FAIL;

		case CACHE_OK:
			break;

		case CACHE_MISS:
			RDEBUG("No cache entry found for \"%s\"", key);
			return RLM_MODULE_NOTFOUND;

		/* FALL-THROUGH */
		default:
			return RLM_MODULE_FAIL;

		}

		break;
	}

	/*
	 *	Yes, but it expired, OR the "forget all" epoch has
	 *	passed.  Delete it, and pretend it doesn't exist.
	 */
	if ((c->expires < request->timestamp) || (c->created < inst->epoch)) {
		RDEBUG("Removing expired entry");

		inst->module->expire(inst, request, handle, c);
		cache_free(inst, &c);
		return RLM_MODULE_NOTFOUND;	/* Couldn't find a non-expired entry */
	}

	RDEBUG("Found entry for \"%s\"", key);

	c->hits++;
	*out = c;

	return RLM_MODULE_OK;
}

/** Expire a cache entry (removing it from the datastore)
 *
 */
static void cache_expire(rlm_cache_t *inst, REQUEST *request, rlm_cache_handle_t **handle, rlm_cache_entry_t **c)
{
	rad_assert(*c);

	for (;;) switch (inst->module->expire(inst, request, handle, *c)) {
	case CACHE_RECONNECT:
		if (cache_reconnect(inst, request, handle) == 0) continue;

	/* FALL-THROUGH */
	default:
		cache_free(inst, c);
		*c = NULL;
		return;
	}
}

/** Create and insert a cache entry.
 *
 * @return RLM_MODULE_OK on success, RLM_MODULE_UPDATED if we merged the cache entry and RLM_MODULE_FAIL on failure.
 */
static rlm_rcode_t cache_insert(rlm_cache_t *inst, REQUEST *request, rlm_cache_handle_t **handle,
				char const *key, int ttl)
{
	VALUE_PAIR *vp, *to_cache;
	vp_cursor_t src_list, packet, reply, control, state;

	vp_map_t const *map;

	bool merge = true;
	rlm_cache_entry_t *c;

	if ((inst->max_entries > 0) && inst->module->count &&
	    (inst->module->count(inst, request, handle) > inst->max_entries)) {
		RWDEBUG("Cache is full: %d entries", inst->max_entries);
		return RLM_MODULE_FAIL;
	}

	c = cache_alloc(inst, request);
	if (!c) return RLM_MODULE_FAIL;

	c->key = talloc_typed_strdup(c, key);
	c->created = c->expires = request->timestamp;
	c->expires += ttl;

	RDEBUG("Creating new cache entry");

	fr_cursor_init(&packet, &c->packet);
	fr_cursor_init(&reply, &c->reply);
	fr_cursor_init(&control, &c->control);
	fr_cursor_init(&state, &c->state);

	for (map = inst->maps; map != NULL; map = map->next) {
		rad_assert(map->lhs && map->rhs);

		if (map_to_vp(c, &to_cache, request, map, NULL) < 0) {
			RDEBUG("Skipping %s", map->rhs->name);
			continue;
		}

		for (vp = fr_cursor_init(&src_list, &to_cache);
		     vp;
		     vp = fr_cursor_next(&src_list)) {
			VERIFY_VP(vp);

			/*
			 *	Prevent people from accidentally caching
			 *	cache control attributes.
			 */
			if (map->rhs->type == TMPL_TYPE_LIST) switch (vp->da->attr) {
			case PW_CACHE_TTL:
			case PW_CACHE_STATUS_ONLY:
			case PW_CACHE_READ_ONLY:
			case PW_CACHE_MERGE:
			case PW_CACHE_ENTRY_HITS:
				RDEBUG2("Skipping %s", vp->da->name);
				continue;

			default:
				break;
			}

			RINDENT();
			if (RDEBUG_ENABLED2) map_debug_log(request, map, vp);
			REXDENT();

			vp->op = map->op;

			switch (map->lhs->tmpl_list) {
			case PAIR_LIST_REQUEST:
				fr_cursor_insert(&packet, vp);
				break;

			case PAIR_LIST_REPLY:
				fr_cursor_insert(&reply, vp);
				break;

			case PAIR_LIST_CONTROL:
				fr_cursor_insert(&control, vp);
				break;

			case PAIR_LIST_STATE:
				fr_cursor_insert(&state, vp);
				break;

			default:
				rad_assert(0);	/* should have been caught by validation */
			}
		}
	}

	/*
	 *	Check to see if we need to merge the entry into the request
	 */
	vp = pairfind(request->config, PW_CACHE_MERGE, 0, TAG_ANY);
	if (vp && (vp->vp_integer == 0)) merge = false;

	if (merge) cache_merge(inst, request, c);

	for (;;) {
		cache_status_t ret;

		ret = inst->module->insert(inst, request, handle, c);
		switch (ret) {
		case CACHE_RECONNECT:
			if (cache_reconnect(inst, request, handle) == 0) continue;
			return RLM_MODULE_FAIL;

		case CACHE_OK:
			RDEBUG("Commited entry, TTL %d seconds", ttl);
			cache_free(inst, &c);
			return RLM_MODULE_UPDATED;

		default:
			talloc_free(c);	/* Failed insertion - use talloc_free not the driver free */
			return RLM_MODULE_FAIL;
		}
	}
}

/** Verify that a map in the cache section makes sense
 *
 */
static int cache_verify(vp_map_t *map, void *ctx)
{
	if (modcall_fixup_update(map, ctx) < 0) return -1;

	if ((map->lhs->type != TMPL_TYPE_ATTR) &&
	    (map->lhs->type != TMPL_TYPE_LIST)) {
		cf_log_err(map->ci, "Destination must be an attribute ref or a list");
		return -1;
	}

	switch (map->lhs->tmpl_list) {
	case PAIR_LIST_REQUEST:
	case PAIR_LIST_REPLY:
	case PAIR_LIST_CONTROL:
	case PAIR_LIST_STATE:
		break;

	default:
		cf_log_err(map->ci, "Destination list must be one of request, reply, control or session-state");
		return -1;
	}

	if (map->lhs->tmpl_request != REQUEST_CURRENT) {
		cf_log_err(map->ci, "Cached attributes can only be inserted into the current request");
		return -1;
	}

	switch (map->rhs->type) {
	case TMPL_TYPE_EXEC:
		cf_log_err(map->ci, "Exec values are not allowed");
		return -1;
	/*
	 *	Only =, :=, += and -= operators are supported for
	 *	cache entries.
	 */
	case TMPL_TYPE_LITERAL:
	case TMPL_TYPE_XLAT:
	case TMPL_TYPE_ATTR:
		switch (map->op) {
		case T_OP_SET:
		case T_OP_EQ:
		case T_OP_SUB:
		case T_OP_ADD:
			break;

		default:
			cf_log_err(map->ci, "Operator \"%s\" not allowed for %s values",
				   fr_int2str(fr_tokens, map->op, "<INVALID>"),
				   fr_int2str(tmpl_names, map->rhs->type, "<INVALID>"));
			return -1;
		}
	default:
		break;
	}

	return 0;
}

/*
 *	Do caching checks.  Since we can update ANY VP list, we do
 *	exactly the same thing for all sections (autz / auth / etc.)
 *
 *	If you want to cache something different in different sections,
 *	configure another cache module.
 */
static rlm_rcode_t CC_HINT(nonnull) mod_cache_it(void *instance, REQUEST *request)
{
	rlm_cache_entry_t *c;
	rlm_cache_t *inst = instance;

	rlm_cache_handle_t *handle;

	vp_cursor_t cursor;
	VALUE_PAIR *vp;
	char buffer[1024];
	rlm_rcode_t rcode;

	int ttl = inst->ttl;

	if (radius_xlat(buffer, sizeof(buffer), request, inst->key, NULL, NULL) < 0) return RLM_MODULE_FAIL;

	if (buffer[0] == '\0') {
		REDEBUG("Zero length key string is invalid");
		return RLM_MODULE_INVALID;
	}

	if (cache_acquire(&handle, inst, request) < 0) return RLM_MODULE_FAIL;

	rcode = cache_find(&c, inst, request, &handle, buffer);
	if (rcode == RLM_MODULE_FAIL) goto finish;
	rad_assert(handle);

	/*
	 *	If Cache-Status-Only == yes, only return whether we found a
	 *	valid cache entry
	 */
	vp = pairfind(request->config, PW_CACHE_STATUS_ONLY, 0, TAG_ANY);
	if (vp && vp->vp_integer) {
		rcode = c ? RLM_MODULE_OK:
			    RLM_MODULE_NOTFOUND;
		goto finish;
	}

	/*
	 *	Update the expiry time based on the TTL.
	 *	A TTL of 0 means "delete from the cache".
	 *	A TTL < 0 means "delete from the cache and recreate the entry".
	 */
	vp = pairfind(request->config, PW_CACHE_TTL, 0, TAG_ANY);
	if (vp) ttl = vp->vp_signed;

	/*
	 *	If there's no existing cache entry, go and create a new one.
	 */
	if (!c) {
		if (ttl <= 0) ttl = inst->ttl;
		goto insert;
	}

	/*
	 *	Expire the entry if requested to do so
	 */
	if (vp) {
		if (ttl == 0) {
			cache_expire(inst, request, &handle, &c);
			RDEBUG("Forcing expiry of entry");
			rcode = RLM_MODULE_OK;
			goto finish;
		}

		if (ttl < 0) {
			RDEBUG("Forcing expiry of existing entry");
			cache_expire(inst, request, &handle, &c);
			ttl *= -1;
			goto insert;
		}
		c->expires = request->timestamp + ttl;
		RDEBUG("Setting TTL to %d", ttl);
	}

	/*
	 *	Cache entry was still valid, so we merge it into the request
	 *	and return. No need to add a new entry.
	 */
	cache_merge(inst, request, c);
	rcode = RLM_MODULE_OK;

	goto finish;

insert:
	/*
	 *	If Cache-Read-Only == yes, then we only allow already cached entries
	 *	to be merged into the request
	 */
	vp = pairfind(request->config, PW_CACHE_READ_ONLY, 0, TAG_ANY);
	if (vp && vp->vp_integer) {
		rcode = RLM_MODULE_NOTFOUND;
		goto finish;
	}

	/*
	 *	Create a new entry.
	 */
	rcode = cache_insert(inst, request, &handle, buffer, ttl);
	rad_assert(handle);

finish:
	cache_free(inst, &c);
	cache_release(inst, request, &handle);

	/*
	 *	Clear control attributes
	 */
	for (vp = fr_cursor_init(&cursor, &request->config);
	     vp;
	     vp = fr_cursor_next(&cursor)) {
		if (vp->da->vendor == 0) switch (vp->da->attr) {
		case PW_CACHE_TTL:
		case PW_CACHE_STATUS_ONLY:
		case PW_CACHE_READ_ONLY:
		case PW_CACHE_MERGE:
			vp = fr_cursor_remove(&cursor);
			talloc_free(vp);
			break;
		}
	}

	return rcode;
}

static ssize_t CC_HINT(nonnull) cache_xlat(void *instance, REQUEST *request,
					   char const *fmt, char *out, size_t freespace);

/*
 *	Allow single attribute values to be retrieved from the cache.
 */
static ssize_t cache_xlat(void *instance, REQUEST *request,
			  char const *fmt, char *out, size_t freespace)
{
	rlm_cache_entry_t 	*c = NULL;
	rlm_cache_t		*inst = instance;
	rlm_cache_handle_t	*handle = NULL;

	VALUE_PAIR		*vp, *vps;
	pair_lists_t		list;
	DICT_ATTR const		*target;
	char const		*p = fmt;
	size_t			len;
	int			ret = 0;

	p += radius_list_name(&list, p, PAIR_LIST_REQUEST);
	if (list == PAIR_LIST_UNKNOWN) {
		REDEBUG("Unknown list qualifier in \"%s\"", fmt);
		ret = -1;
		goto finish;
	}

	target = dict_attrbyname(p);
	if (!target) {
		REDEBUG("Unknown attribute \"%s\"", p);
		return -1;
	}

	if (cache_acquire(&handle, inst, request) < 0) return -1;

	switch (cache_find(&c, inst, request, handle, fmt)) {
	case RLM_MODULE_OK:		/* found */
		break;

	case RLM_MODULE_NOTFOUND:	/* not found */
		*out = '\0';
		return 0;

	default:
		return -1;
	}

	switch (list) {
	case PAIR_LIST_REQUEST:
		vps = c->packet;
		break;

	case PAIR_LIST_REPLY:
		vps = c->reply;
		break;

	case PAIR_LIST_CONTROL:
		vps = c->control;
		break;

	case PAIR_LIST_STATE:
		vps = c->state;
		break;

	default:
		REDEBUG("Unsupported list \"%s\"", fr_int2str(pair_lists, list, "<UNKNOWN>"));
		ret = -1;
		goto finish;
	}

	vp = pairfind(vps, target->attr, target->vendor, TAG_ANY);
	if (!vp) {
		RDEBUG("No instance of this attribute has been cached");
		*out = '\0';
		goto finish;
	}

	len = vp_prints_value(out, freespace, vp, 0);
	if (is_truncated(len, freespace)) {
		REDEBUG("Insufficient buffer space to write cached value");
		ret = -1;
		goto finish;
	}

finish:
	cache_free(inst, &c);
	cache_release(inst, request, &handle);

	return ret;
}

/*
 *	Only free memory we allocated.  The strings allocated via
 *	cf_section_parse() do not need to be freed.
 */
static int mod_detach(void *instance)
{
	rlm_cache_t *inst = instance;

	talloc_free(inst->maps);

	/*
	 *  We need to explicitly free all children, so if the driver
	 *  parented any memory off the instance, their destructors
	 *  run before we unload the bytecode for them.
	 *
	 *  If we don't do this, we get a SEGV deep inside the talloc code
	 *  when it tries to call a destructor that no longer exists.
	 */
	talloc_free_children(inst);

	/*
	 *  Decrements the reference count. The driver object won't be unloaded
	 *  until all instances of rlm_cache that use it have been destroyed.
	 */
	if (inst->handle) dlclose(inst->handle);

	return 0;
}


static int mod_bootstrap(CONF_SECTION *conf, void *instance)
{
	rlm_cache_t *inst = instance;

	inst->cs = conf;

	inst->name = cf_section_name2(conf);
	if (!inst->name) inst->name = cf_section_name1(conf);

	/*
	 *	Register the cache xlat function
	 */
	xlat_register(inst->name, cache_xlat, NULL, inst);

	return 0;
}


/*
 *	Instantiate the module.
 */
static int mod_instantiate(CONF_SECTION *conf, void *instance)
{
	rlm_cache_t *inst = instance;
	CONF_SECTION *update;

	inst->cs = conf;

	/*
	 *	Sanity check for crazy people.
	 */
	if (strncmp(inst->driver_name, "rlm_cache_", 8) != 0) {
		cf_log_err_cs(conf, "\"%s\" is NOT an Cache driver!", inst->driver_name);
		return -1;
	}

	/*
	 *	Load the appropriate driver for our database
	 */
	inst->handle = lt_dlopenext(inst->driver_name);
	if (!inst->handle) {
		cf_log_err_cs(conf, "Could not link driver %s: %s", inst->driver_name, dlerror());
		cf_log_err_cs(conf, "Make sure it (and all its dependent libraries!) are in the search path"
			      "of your system's ld");
		return -1;
	}

	inst->module = (cache_module_t *) dlsym(inst->handle, inst->driver_name);
	if (!inst->module) {
		cf_log_err_cs(conf, "Could not link symbol %s: %s", inst->driver_name, dlerror());
		return -1;
	}

	DEBUG("rlm_cache (%s): Driver %s (module %s) loaded and linked", inst->name,
	      inst->driver_name, inst->module->name);

	/*
	 *	Non optional fields and callbacks
	 */
	rad_assert(inst->module->name);
	rad_assert(inst->module->find);
	rad_assert(inst->module->insert);
	rad_assert(inst->module->expire);

	if (inst->module->instantiate) {
		CONF_SECTION *cs;
		char const *name;

		name = strrchr(inst->driver_name, '_');
		if (!name) {
			name = inst->driver_name;
		} else {
			name++;
		}

		cs = cf_section_sub_find(conf, name);
		if (!cs) {
			cs = cf_section_alloc(conf, name, NULL);
			if (!cs) return -1;
		}

		/*
		 *	It's up to the driver to register a destructor (using talloc)
		 *
		 *	Should write its instance data in inst->driver,
		 *	and parent it off of inst.
		 */
		if (inst->module->instantiate(cs, inst) < 0) return -1;
	}

	rad_assert(inst->key && *inst->key);

	if (inst->ttl == 0) {
		cf_log_err_cs(conf, "Must set 'ttl' to non-zero");
		return -1;
	}

	if (inst->epoch != 0) {
		cf_log_err_cs(conf, "Must not set 'epoch' in the configuration files");
		return -1;
	}

	update = cf_section_sub_find(inst->cs, "update");
	if (!update) {
		cf_log_err_cs(conf, "Must have an 'update' section in order to cache anything.");
		return -1;
	}

	/*
	 *	Make sure the users don't screw up too badly.
	 */
	if (map_afrom_cs(&inst->maps, update,
			 PAIR_LIST_REQUEST, PAIR_LIST_REQUEST, cache_verify, NULL, MAX_ATTRMAP) < 0) {
		return -1;
	}

	if (!inst->maps) {
		cf_log_err_cs(inst->cs, "Cache config must contain an update section, and "
			      "that section must not be empty");

		return -1;
	}
	return 0;
}

/*
 *	The module name should be the only globally exported symbol.
 *	That is, everything else should be 'static'.
 *
 *	If the module needs to temporarily modify it's instantiation
 *	data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
 *	The server will then take care of ensuring that the module
 *	is single-threaded.
 */
extern module_t rlm_cache;
module_t rlm_cache = {
	.magic		= RLM_MODULE_INIT,
	.name		= "cache",
	.inst_size	= sizeof(rlm_cache_t),
	.config		= module_config,
	.bootstrap	= mod_bootstrap,
	.instantiate	= mod_instantiate,
	.detach		= mod_detach,
	.methods = {
		[MOD_AUTHORIZE]		= mod_cache_it,
		[MOD_PREACCT]		= mod_cache_it,
		[MOD_ACCOUNTING]	= mod_cache_it,
		[MOD_PRE_PROXY]		= mod_cache_it,
		[MOD_POST_PROXY]	= mod_cache_it,
		[MOD_POST_AUTH]		= mod_cache_it
	},
};
