Added rlm_cache
authorAlan T. DeKok <aland@freeradius.org>
Thu, 23 Aug 2012 12:24:57 +0000 (14:24 +0200)
committerAlan T. DeKok <aland@freeradius.org>
Thu, 23 Aug 2012 12:25:14 +0000 (14:25 +0200)
raddb/modules/cache [new file with mode: 0644]
src/modules/rlm_cache/Makefile [new file with mode: 0644]
src/modules/rlm_cache/rlm_cache.c [new file with mode: 0644]

diff --git a/raddb/modules/cache b/raddb/modules/cache
new file mode 100644 (file)
index 0000000..edc8239
--- /dev/null
@@ -0,0 +1,60 @@
+# -*- text -*-
+#
+#  $Id$
+
+#
+#      A module to cache attributes.  The idea is that you can look
+#      up information in a database, and then cache it.  Repeated
+#      requests for the same information will then have the cached
+#      values added to the request.
+#
+#      The module can cache a fixed set of attributes per key.
+#      It can be listed in "authorize", "post-auth", "pre-proxy"
+#      and "post-proxy".
+#
+#      If you want different things cached for authorize and post-auth,
+#      you will need to define two instances of the "cache" module.
+#
+#      The module returns "updated" if it found a cache entry.
+#      The module returns "ok" if it added a new cache entry.
+#      The module returns "noop" if it did nothing.
+#
+cache {
+       #  The key used to index the cache.  It is dynamically expanded
+       #  at run time.
+       key = "%{User-Name}"
+
+       #  The TTL of cache entries, in seconds.  Entries older than this
+       #  will be expired.
+       #
+       #  You can set the TTL per cache entry, but adding a control
+       #  variable "Cache-TTL".  The value there will over-ride this one.
+       #  Setting a Cache-TTL of 0 means "delete this entry".
+       #
+       #  This value should be between 10 and 86400.
+       ttl = 10
+
+       #  A timestamp used to flush the cache, via
+       #
+       #       radmin -e "set module config cache epoch 123456789"
+       #
+       #  Where last value is a 32-bit Unix timestamp.  Cache entries
+       #  older than this are expired, and new entries added.
+       #
+       #  You should ALWAYS leave it as "epoch = 0" here.
+       epoch = 0
+
+       #  The list of attributes to cache for a particular key.
+       #  Each key gets the same set of cached attributes.
+       #  The attributes are dynamically expanded at run time.
+       #
+       #  You can specify which list the attribute goes into by
+       #  prefixing the attribute name with the list.  This allows
+       #  you to update multiple lists with one configuration.
+       attributes {
+               # list:Attr-Name
+               reply:Filter-Id := "%l"
+
+               control:Class := 0x010203
+       }
+}
diff --git a/src/modules/rlm_cache/Makefile b/src/modules/rlm_cache/Makefile
new file mode 100644 (file)
index 0000000..5af8183
--- /dev/null
@@ -0,0 +1,11 @@
+TARGET         = rlm_cache
+SRCS           = rlm_cache.c
+HEADERS                = 
+RLM_CFLAGS     =
+RLM_LIBS       = 
+
+include ../rules.mak
+
+$(STATIC_OBJS): $(HEADERS)
+
+$(DYNAMIC_OBJS): $(HEADERS)
diff --git a/src/modules/rlm_cache/rlm_cache.c b/src/modules/rlm_cache/rlm_cache.c
new file mode 100644 (file)
index 0000000..ef24f61
--- /dev/null
@@ -0,0 +1,510 @@
+/*
+ * rlm_cache.c
+ *
+ * Version:    $Id$
+ *
+ *   This program 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
+ *
+ * Copyright 2000,2006  The FreeRADIUS server project
+ * Copyright 2000  your name <your address>
+ */
+
+#include <freeradius-devel/ident.h>
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/heap.h>
+#include <freeradius-devel/rad_assert.h>
+
+/*
+ *     Define a structure for our module configuration.
+ *
+ *     These variables do not need to be in a structure, but it's
+ *     a lot cleaner to do so, and a pointer to the structure can
+ *     be used as the instance handle.
+ */
+typedef struct rlm_cache_t {
+       char            *key;
+       int             ttl;
+       int             epoch;
+       CONF_SECTION    *cs;
+       rbtree_t        *cache;
+       fr_heap_t       *heap;
+} rlm_cache_t;
+
+typedef struct rlm_cache_entry_t {
+       const char      *key;
+       int             offset;
+       time_t          created;
+       time_t          expires;
+       VALUE_PAIR      *control;
+       VALUE_PAIR      *request;
+       VALUE_PAIR      *reply;
+} rlm_cache_entry_t;
+
+
+/*
+ *     Compare two entries by key.  There may only be one entry with
+ *     the same key.
+ */
+static int cache_entry_cmp(const void *one, const void *two)
+{
+       const rlm_cache_entry_t *a = one;
+       const rlm_cache_entry_t *b = two;
+
+       return strcmp(a->key, b->key);
+}
+
+static void cache_entry_free(void *data)
+{
+       rlm_cache_entry_t *c = data;
+
+       free(c->key);
+       pairfree(&c->control);
+       pairfree(&c->request);
+       pairfree(&c->reply);
+       free(c);
+}
+
+
+/*
+ *     Compare two entries by expiry time.  There may be multiple
+ *     entries with the same expiry time.
+ */
+static int cache_heap_cmp(const void *one, const void *two)
+{
+       const rlm_cache_entry_t *a = one;
+       const rlm_cache_entry_t *b = two;
+
+       if (a->expires < b->expires) return -1;
+       if (a->expires > b->expires) return +1;
+
+       return 0;
+}
+
+/*
+ *     Merge a cached entry into a REQUEST.
+ */
+static void cache_merge(REQUEST *request, rlm_cache_entry_t *c)
+{
+       VALUE_PAIR *vp;
+
+       rad_assert(request != NULL);
+       rad_assert(c != NULL);
+
+       if (c->control) {
+               vp = paircopy(c->control);
+               pairmove(&request->config_items, &vp);
+               pairfree(&vp);
+       }
+
+       if (c->request && request->packet) {
+               vp = paircopy(c->request);
+               pairmove(&request->packet->vps, &vp);
+               pairfree(&vp);
+       }
+
+       if (c->reply && request->reply) {
+               vp = paircopy(c->reply);
+               pairmove(&request->reply->vps, &vp);
+               pairfree(&vp);
+       }
+}
+
+
+/*
+ *     Find a cached entry.
+ */
+static rlm_cache_entry_t *cache_find(rlm_cache_t *inst, REQUEST *request,
+                                    const char *key)
+{
+       int ttl;
+       rlm_cache_entry_t *c, my_c;
+       VALUE_PAIR *vp;
+
+       /*
+        *      Look at the expiry heap.
+        */
+       c = fr_heap_peek(inst->heap);
+       if (!c) {
+               rad_assert(rbtree_num_elements(inst->cache) == 0);
+               return NULL;
+       }
+
+       /*
+        *      If it's time to expire an old entry, do so now.
+        */
+       if (c->expires < request->timestamp) {
+               fr_heap_extract(inst->heap, c);
+               rbtree_deletebydata(inst->cache, c);
+       }
+
+       /*
+        *      Is there an entry for this key?
+        */
+       my_c.key = key;
+       c = rbtree_finddata(inst->cache, &my_c);
+       if (!c) return NULL;
+
+       /*
+        *      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)) {
+       delete:
+               fr_heap_extract(inst->heap, c);
+               rbtree_deletebydata(inst->cache, c);
+               return NULL;
+       }
+
+       /*
+        *      Update the expiry time based on the TTL.
+        *      A TTL of 0 means "delete from the cache".
+        */
+       vp = pairfind(request->config_items, PW_CACHE_TTL);
+       if (vp) {
+               if (vp->vp_integer == 0) goto delete;
+
+               ttl = vp->vp_integer;
+       } else {
+               ttl = inst->ttl;
+       }
+
+       DEBUG("rlm_cache: Found entry for \"%s\".  Adding %d to the TTL",
+             key, ttl);
+       c->expires = request->timestamp + ttl;
+
+       return c;
+}
+
+
+/*
+ *     Add an entry to the cache.
+ */
+static rlm_cache_entry_t *cache_add(rlm_cache_t *inst, REQUEST *request,
+                                   const char *key)
+{
+       int ttl;
+       const char *attr, *p;
+       VALUE_PAIR *vp, **list;
+       CONF_ITEM *ci;
+       CONF_PAIR *cp;
+       rlm_cache_entry_t *c;
+       char buffer[1024];
+
+       /*
+        *      TTL of 0 means "don't cache this entry"
+        */
+       vp = pairfind(request->config_items, PW_CACHE_TTL);
+       if (vp && (vp->vp_integer == 0)) return NULL;
+
+       c = rad_malloc(sizeof(*c));
+       memset(c, 0, sizeof(*c));
+
+       c->key = strdup(key);
+       c->created = c->expires = request->timestamp;
+
+       /*
+        *      Use per-entry TTL, or globally defined one.
+        */
+       if (vp) {
+               ttl = vp->vp_integer;
+       } else {
+               ttl = inst->ttl;
+       }
+       c->expires += ttl;
+
+       /*
+        *      Walk over the attributes to cache, dynamically
+        *      expanding them, and adding them to the correct list.
+        */
+       for (ci = cf_item_find_next(inst->cs, NULL);
+            ci != NULL;
+            ci = cf_item_find_next(inst->cs, ci)) {
+               rad_assert(cf_item_is_pair(ci));
+
+               cp = cf_itemtopair(ci);
+               attr = cf_pair_attr(cp);
+
+               if (strncmp(attr, "control:", 8) == 0) {
+                       p = attr + 8;
+                       list = &c->control;
+
+               } else if (strncmp(attr, "request:", 8) == 0) {
+                       p = attr + 8;
+                       list = &c->request;
+
+               } else if (strncmp(attr, "reply:", 6) == 0) {
+                       p = attr + 6;
+                       list = &c->reply;
+
+               } else {
+                       p = attr;
+                       list = &c->request;
+               }
+
+               /*
+                *      Repeat much of cf_pairtovp here...
+                *      but we take list prefixes, and it doesn't.
+                *      I don't want to make that change for 2.0.
+                */
+               radius_xlat(buffer, sizeof(buffer), cf_pair_value(cp),
+                           request, NULL);
+
+               vp = pairmake(p, buffer, cf_pair_operator(cp));
+               pairadd(list, vp);
+       }
+
+       if (!rbtree_insert(inst->cache, c)) {
+               DEBUG("rlm_cache: FAILED adding entry for key %s", key);
+               cache_entry_free(c);
+               return NULL;
+       }
+
+       if (!fr_heap_insert(inst->heap, c)) {
+               DEBUG("rlm_cache: FAILED adding entry for key %s", key);
+               rbtree_deletebydata(inst->cache, c);
+               return NULL;
+       }
+
+       DEBUG("rlm_cache: Adding entry for \"%s\", with TTL of %d",
+             key, ttl);
+
+       return c;
+}
+
+
+/*
+ *     Verify that the "attributes" section makes sense.
+ */
+static int cache_verify(rlm_cache_t *inst)
+{
+       const char *attr, *p;
+       CONF_ITEM *ci;
+       CONF_PAIR *cp;
+
+       for (ci = cf_item_find_next(inst->cs, NULL);
+            ci != NULL;
+            ci = cf_item_find_next(inst->cs, ci)) {
+               if (!cf_item_is_pair(ci)) {
+                       cf_log_err(ci, "rlm_cache: Entry is not in \"attribute = value\" format");
+                       return 0;
+               }
+
+               cp = cf_itemtopair(ci);
+               attr = cf_pair_attr(cp);
+
+               if (strncmp(attr, "control:", 8) == 0) {
+                       p = attr + 8;
+
+               } else if (strncmp(attr, "request:", 8) == 0) {
+                       p = attr + 8;
+
+               } else if (strncmp(attr, "reply:", 6) == 0) {
+                       p = attr + 6;
+
+               } else {
+                       p = attr;
+               }
+
+               /*
+                *      FIXME: Can't do tags for now...
+                */
+               if (!dict_attrbyname(p)) {
+                       cf_log_err(ci, "rlm_cache: Unknown attribute \"%s\"", p);
+                       return 0;
+               }
+
+               if (!cf_pair_value(cp)) {
+                       cf_log_err(ci, "rlm_cache: Attribute has no value");
+                       return 0;
+               }
+       }
+
+       return 1;
+}
+
+
+/*
+ *     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[] = {
+       { "key",  PW_TYPE_STRING_PTR,
+         offsetof(rlm_cache_t, key), NULL,  NULL},
+       { "ttl", PW_TYPE_INTEGER,
+         offsetof(rlm_cache_t, ttl), NULL,   "500" },
+       { "epoch", PW_TYPE_INTEGER,
+         offsetof(rlm_cache_t, epoch), NULL,   NULL },
+
+       { NULL, -1, 0, NULL, NULL }             /* end the list */
+};
+
+
+/*
+ *     Only free memory we allocated.  The strings allocated via
+ *     cf_section_parse() do not need to be freed.
+ */
+static int cache_detach(void *instance)
+{
+       rlm_cache_t *inst = instance;
+
+       free(inst->key);
+
+       fr_heap_delete(inst->heap);
+       rbtree_free(inst->cache);
+       free(instance);
+       return 0;
+}
+
+
+/*
+ *     Instantiate the module.
+ */
+static int cache_instantiate(CONF_SECTION *conf, void **instance)
+{
+       rlm_cache_t *inst;
+
+       inst = rad_malloc(sizeof(*inst));
+       if (!inst) {
+               return -1;
+       }
+       memset(inst, 0, sizeof(*inst));
+
+       /*
+        *      If the configuration parameters can't be parsed, then
+        *      fail.
+        */
+       if (cf_section_parse(conf, inst, module_config) < 0) {
+               free(inst);
+               return -1;
+       }
+
+       if (!inst->key || !*inst->key) {
+               radlog(L_ERR, "rlm_cache: You must specify a key");
+               cache_detach(inst);
+               return -1;
+       }
+
+       if (inst->ttl == 0) {
+               radlog(L_ERR, "rlm_cache: TTL must be greater than zero");
+               cache_detach(inst);
+               return -1;
+       }
+
+       /*
+        *      The cache.
+        */
+       inst->cache = rbtree_create(cache_entry_cmp, cache_entry_free, 0);
+       if (!inst->cache) {
+               radlog(L_ERR, "rlm_cache: Failed to create cache");
+               cache_detach(inst);
+               return -1;
+       }
+
+       /*
+        *      The heap of entries to expire.
+        */
+       inst->heap = fr_heap_create(cache_heap_cmp,
+                                   offsetof(rlm_cache_entry_t, offset));
+       if (!inst->heap) {
+               radlog(L_ERR, "rlm_cache: Failed to create cache");
+               cache_detach(inst);
+               return -1;
+       }
+
+       inst->cs = cf_section_sub_find(conf, "attributes");
+       if (!inst->cs) {
+               radlog(L_ERR, "rlm_cache: Failed to find \"attributes\" subsection");
+               cache_detach(inst);
+               return -1;
+       }
+
+       /*
+        *      Make sure the users don't screw up too badly.
+        */
+       if (!cache_verify(inst)) {
+               cache_detach(inst);
+               return -1;
+       }
+
+       *instance = inst;
+
+       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 int cache_it(void *instance, REQUEST *request)
+{
+       rlm_cache_entry_t *c;
+       rlm_cache_t *inst = instance;
+       char buffer[1024];
+
+       radius_xlat(buffer, sizeof(buffer), inst->key, request, NULL);
+
+       c = cache_find(inst, request, buffer);
+       if (c) {
+               cache_merge(request, c);
+               return RLM_MODULE_UPDATED;
+       }
+
+       c = cache_add(inst, request, buffer);
+       if (!c) return RLM_MODULE_NOOP;
+
+       cache_merge(request, c);
+
+       return RLM_MODULE_OK;
+}
+
+
+/*
+ *     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.
+ */
+module_t rlm_cache = {
+       RLM_MODULE_INIT,
+       "cache",
+       0,                      /* type */
+       cache_instantiate,              /* instantiation */
+       cache_detach,                   /* detach */
+       {
+               NULL,                   /* authentication */
+               cache_it,               /* authorization */
+               NULL,                   /* preaccounting */
+               NULL,                   /* accounting */
+               NULL,                   /* checksimul */
+               cache_it,               /* pre-proxy */
+               cache_it,               /* post-proxy */
+               cache_it,               /* post-auth */
+       },
+};