Add the first version of a request caching module. More information in
authorkkalev <kkalev>
Tue, 21 Dec 2004 15:08:05 +0000 (15:08 +0000)
committerkkalev <kkalev>
Tue, 21 Dec 2004 15:08:05 +0000 (15:08 +0000)
experimental.conf
When creating the datadir, create it with mode 755, not 700. Otherwise, only
root may run radclient.

Makefile
raddb/experimental.conf
share/dictionary
src/include/radius.h
src/modules/rlm_caching/rlm_caching.c [new file with mode: 0644]

index 119a3a0..03ecfb7 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -43,7 +43,7 @@ install:
        $(INSTALL) -d -m 755    $(R)$(RUNDIR)
        $(INSTALL) -d -m 700    $(R)$(logdir)
        $(INSTALL) -d -m 700    $(R)$(radacctdir)
-       $(INSTALL) -d -m 700    $(R)$(datadir)
+       $(INSTALL) -d -m 755    $(R)$(datadir)
        $(INSTALL) -d -m 755    $(R)$(dictdir)
        for i in 1 5 8; do \
                $(INSTALL) -d -m 755    $(R)$(mandir)/man$$i; \
index c3462b0..0dd1310 100644 (file)
                #
                key = %{Realm:-DEFAULT}
        }
+       # Caching module
+       #
+       # Should be added in the post-auth section (after all other modules)
+       # and in the authorize section (before any other modules)
+       #
+       # authorize {
+       #       caching {
+       #               ok = return
+       #       }
+       #       [... other modules ...]
+       # }
+       # post-auth {
+       #       [... other modules ...]
+       #       caching
+       # }
+       #
+       # The caching module will cache the Auth-Type and reply items and send them back
+       # on any subsequent requests for the same key
+       #
+       # Configuration:
+       #
+       # filename: The gdbm file to use for the cache database (can be memory mapped for
+       #               more speed)
+       # key: A string to xlat and use as a key. For instace, "%{Acct-Unique-Session-Id}"
+       # post-auth: If we find a cached entry, set the post-auth to that value
+       # cache-ttl: The time to cache the entry. The values from the counter module apply here
+       # cache-size: The gdbm cache size to request (default 1000)
+       # hit-ratio: If set to non-zero we print out statistical information after so many cache requests
+       # cache-rejects: Do we also cache rejects, or not? (default 'yes')
+       #
+       caching {
+               filename = ${raddbdir}/db.cache
+               cache-ttl = 1d
+               hit-ratio = 1000
+               key = "%{Acct-Unique-Session-Id}"
+               #post-auth = ""
+               # cache-size = 2000
+               # cache-rejects = yes
+       }
index 3327499..81e7181 100644 (file)
@@ -309,6 +309,8 @@ ATTRIBUTE   Packet-Dst-Port         1087    integer
 ATTRIBUTE      Packet-Authentication-Vector 1088 octets
 ATTRIBUTE      Time-Of-Day             1089    string
 ATTRIBUTE      Request-Processing-Stage 1090   string
+ATTRIBUTE      Cache-No-Caching        1091    string
+ATTRIBUTE      Cache-Delete-Cache      1092    string
 
 #
 #      Range:  1084-1199
index 0dc8f63..8919ea9 100644 (file)
 #define PW_PACKET_AUTHENTICATION_VECTOR        1088
 #define PW_TIME_OF_DAY                 1089
 #define PW_REQUEST_PROCESSING_STAGE    1090
+#define PW_CACHE_NO_CACHING            1091
+#define PW_CACHE_DELETE_CACHE          1092
 
 /*
  *     Integer Translations
diff --git a/src/modules/rlm_caching/rlm_caching.c b/src/modules/rlm_caching/rlm_caching.c
new file mode 100644 (file)
index 0000000..7670f12
--- /dev/null
@@ -0,0 +1,520 @@
+/*
+ * rlm_caching.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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * Copyright 2001  The FreeRADIUS server project
+ * Copyright 2001  Alan DeKok <aland@ox.org>
+ * Copyright 2001-3  Kostas Kalevras <kkalev@noc.ntua.gr>
+ */
+
+#include "config.h"
+#include "autoconf.h"
+#include "libradius.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "radiusd.h"
+#include "modules.h"
+#include "conffile.h"
+
+#include <gdbm.h>
+#include <time.h>
+
+#ifdef NEEDS_GDBM_SYNC
+#      define GDBM_SYNCOPT GDBM_SYNC
+#else
+#      define GDBM_SYNCOPT 0
+#endif
+
+#ifdef GDBM_NOLOCK
+#define GDBM_COUNTER_OPTS (GDBM_SYNCOPT | GDBM_NOLOCK)
+#else
+#define GDBM_COUNTER_OPTS (GDBM_SYNCOPT)
+#endif
+
+#ifndef HAVE_GDBM_FDESC
+#define gdbm_fdesc(foo) (-1)
+#endif
+
+#define UNIQUEID_MAX_LEN 32
+
+static const char rcsid[] = "$Id$";
+
+/*
+ *     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_caching_t {
+       char *filename;         /* name of the database file */
+       char *key;              /* An xlated string to use as key for the records */
+       char *post_auth;        /* If set and we find a cached entry, set Post-Auth to this value */    
+       char *cache_ttl_str;    /* The string represantation of the TTL */
+       int cache_ttl;          /* The cache TTL */
+       int hit_ratio;          /* Show cache hit ratio every so many queries */
+       int cache_rejects;      /* Do we also cache rejects? */
+       int cache_size;         /* The cache size to pass to GDBM */
+       uint32_t cache_queries; /* The number of cache requests */
+       uint32_t cache_hits;    /* The number of cache hits */
+       GDBM_FILE gdbm;         /* The gdbm file handle */
+#ifdef HAVE_PTHREAD_H
+       pthread_mutex_t mutex;  /* A mutex to lock the gdbm file for only one reader/writer */
+#endif
+} rlm_caching_t;
+
+#define MAX_RECORD_LEN 750
+#define MAX_AUTH_TYPE 32
+
+#define show_hit_ratio \
+       if (data->hit_ratio && (data->cache_queries % data->hit_ratio) == 0) \
+               radlog(L_INFO, "rlm_caching: Cache Queries: %7d, Cache Hits: %7d, Hit Ratio: %.2f%%", \
+                       data->cache_queries,data->cache_hits,hit_ratio)
+
+typedef struct rlm_caching_data {
+       time_t creation;
+       char data[MAX_RECORD_LEN];
+       char auth_type[MAX_AUTH_TYPE];
+       int len;
+} rlm_caching_data;
+
+#ifndef HAVE_PTHREAD_H
+/*
+ *     This is a lot simpler than putting ifdef's around
+ *     every use of the pthread functions.
+ */
+#define pthread_mutex_lock(a)
+#define pthread_mutex_unlock(a)
+#define pthread_mutex_init(a,b)
+#define pthread_mutex_destroy(a)
+#endif
+
+/*
+ *     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 CONF_PARSER module_config[] = {
+  { "filename", PW_TYPE_STRING_PTR, offsetof(rlm_caching_t,filename), NULL, NULL },
+  { "key", PW_TYPE_STRING_PTR, offsetof(rlm_caching_t,key), NULL, "%{Acct-Unique-Session-Id}" },
+  { "post-auth", PW_TYPE_STRING_PTR, offsetof(rlm_caching_t,post_auth), NULL,  NULL },
+  { "cache-ttl", PW_TYPE_STRING_PTR, offsetof(rlm_caching_t,cache_ttl_str), NULL, "1d" },
+  { "cache-size", PW_TYPE_INTEGER, offsetof(rlm_caching_t,cache_size), NULL, "1000" },
+  { "hit-ratio", PW_TYPE_INTEGER, offsetof(rlm_caching_t,hit_ratio), NULL, "0" },
+  { "cache-rejects", PW_TYPE_BOOLEAN, offsetof(rlm_caching_t,cache_rejects), NULL, "yes" },
+  { NULL, -1, 0, NULL, NULL }
+};
+
+static int caching_detach(void *instance);
+
+static int find_ttl(char *ttl)
+{
+       unsigned len = 0;
+       char last = 's';
+
+       if (isdigit((int) ttl[0])){
+               len = strlen(ttl);
+               if (len == 0)
+                       return -1;
+               last = ttl[len - 1];
+               if (!isalpha((int) last))
+                       last = 's';
+               len = atoi(ttl);
+               DEBUG("rlm_caching::find_ttl: num=%d, last=%c",len,last);
+       }
+       switch (last){
+               case 's':
+               default:
+                       break;
+               case 'm':
+                       len *= 60;
+                       break;
+               case 'h':
+                       len *= 3600;
+                       break;
+               case 'd':
+                       len *= 86400;
+                       break;
+               case 'w':
+                       len *= 604800;
+                       break;
+       }
+
+       DEBUG("rlm_caching::find_ttl: Returning '%d'",len);
+
+       return len;
+}
+
+/*
+ *     Do any per-module initialization that is separate to each
+ *     configured instance of the module.  e.g. set up connections
+ *     to external databases, read configuration files, set up
+ *     dictionary entries, etc.
+ *
+ *     If configuration information is given in the config section
+ *     that must be referenced in later calls, store a handle to it
+ *     in *instance otherwise put a null pointer there.
+ */
+static int caching_instantiate(CONF_SECTION *conf, void **instance)
+{
+       rlm_caching_t *data;
+       int cache_size;
+
+       /*
+        *      Set up a storage area for instance data
+        */
+       data = rad_malloc(sizeof(*data));
+       if (!data) {
+               radlog(L_ERR, "rlm_caching: rad_malloc() failed.");
+               return -1;
+       }
+       memset(data, 0, sizeof(*data));
+
+       /*
+        *      If the configuration parameters can't be parsed, then
+        *      fail.
+        */
+       if (cf_section_parse(conf, data, module_config) < 0) {
+               free(data);
+               return -1;
+       }
+       cache_size = data->cache_size;
+
+       /*
+        *      Discover the attribute number of the key.
+        */
+       if (data->key == NULL) {
+               radlog(L_ERR, "rlm_caching: 'key' must be set.");
+               caching_detach(data);
+               return -1;
+       }
+       if (data->cache_ttl_str == NULL) {
+               radlog(L_ERR, "rlm_caching: 'cache-ttl' must be set.");
+               caching_detach(data);
+               return -1;
+       }
+       else {
+               data->cache_ttl = find_ttl(data->cache_ttl_str);
+               if (data->cache_ttl == 0) {
+                       radlog(L_ERR, "rlm_caching: 'cache-ttl' is invalid.");
+                       caching_detach(data);
+                       return -1;
+               }
+       }
+
+       if (data->filename == NULL) {
+               radlog(L_ERR, "rlm_caching: 'filename' must be set.");
+               caching_detach(data);
+               return -1;
+       }
+       data->gdbm = gdbm_open(data->filename, sizeof(int),
+                       GDBM_WRCREAT | GDBM_COUNTER_OPTS, 0600, NULL);
+       if (data->gdbm == NULL) {
+               radlog(L_ERR, "rlm_caching: Failed to open file %s: %s",
+                               data->filename, strerror(errno));
+               caching_detach(data);
+               return -1;
+       }
+       if (gdbm_setopt(data->gdbm, GDBM_CACHESIZE, &cache_size, sizeof(int)) == -1)
+               radlog(L_ERR, "rlm_caching: Failed to set cache size");
+
+       /*
+        * Init the mutex
+        */
+       pthread_mutex_init(&data->mutex, NULL);
+
+       *instance = data;
+
+       return 0;
+}
+
+/*
+ *     Cache the reply items and the Auth-Type
+ */
+static int caching_postauth(void *instance, REQUEST *request)
+{
+       rlm_caching_t *data = (rlm_caching_t *)instance;
+       char key[MAX_STRING_LEN];
+       datum key_datum;
+       datum data_datum;
+       VALUE_PAIR *reply_vp;
+       VALUE_PAIR *auth_type;
+       rlm_caching_data cache_data;
+       int count = 0;
+       int ret = 0;    
+       int size = 0;
+       int rcode = 0;
+
+       if (pairfind(request->packet->vps, PW_CACHE_NO_CACHING) != NULL){
+               DEBUG("rlm_caching: Cache-No-Caching is set. Returning NOOP");
+               return RLM_MODULE_NOOP;
+       }
+       if ((auth_type = pairfind(request->config_items, PW_AUTH_TYPE)) != NULL){
+               DEBUG("rlm_caching: Found Auth-Type, value: '%s'",auth_type->strvalue);
+               if (strcmp(auth_type->strvalue,"Reject") == 0 && data->cache_rejects == 0){
+                       DEBUG("rlm_caching: No caching of Rejects. Returning NOOP");
+                       return RLM_MODULE_NOOP;
+               }
+               if (strlen(auth_type->strvalue) > MAX_AUTH_TYPE - 1){
+                       DEBUG("rlm_caching: Auth-Type value too large");
+                       return RLM_MODULE_NOOP;
+               }
+       }
+       else{
+               DEBUG("rlm_caching: No Auth-Type found. Returning NOOP");
+               return RLM_MODULE_NOOP;
+       }
+
+       reply_vp = request->reply->vps;
+
+       if (reply_vp == NULL) {
+               DEBUG("rlm_caching: The Request does not contain any reply attributes");
+               return RLM_MODULE_NOOP;
+       }
+       if (!radius_xlat(key,sizeof(key), data->key, request, NULL)){
+               radlog(L_ERR, "rlm_caching: xlat on key '%s' failed.",data->key);
+               return RLM_MODULE_FAIL;
+       }
+
+       memset(&cache_data,0,sizeof(rlm_caching_data));
+
+       cache_data.creation = time(NULL);
+       strcpy(cache_data.auth_type,auth_type->strvalue);
+
+       size = MAX_RECORD_LEN;
+
+       while(reply_vp) {
+               if (size <= 1){
+                       DEBUG("rlm_caching: Not enough space.");
+                       return RLM_MODULE_NOOP;
+               }
+               ret = vp_prints(cache_data.data + count,size,reply_vp);
+               if (ret == 0) {
+                       DEBUG("rlm_caching: Record is too large, will not store it.");
+                       return RLM_MODULE_NOOP;
+               }
+               count += (ret + 1);
+               size -= (ret + 1);
+               DEBUG("rlm_caching: VP=%s,VALUE=%s,length=%d,cache record length=%d, space left=%d",
+                       reply_vp->name,reply_vp->strvalue,ret,count,size);
+               reply_vp = reply_vp->next;
+       }
+       cache_data.len = count;
+
+       DEBUG("rlm_caching: Storing cache for Key='%s'",key);
+       data_datum.dptr = (rlm_caching_data *) &cache_data;
+       data_datum.dsize = sizeof(rlm_caching_data);
+
+       key_datum.dptr = (char *) key;
+       key_datum.dsize = strlen(key);
+
+       pthread_mutex_lock(&data->mutex);
+       rcode = gdbm_store(data->gdbm, key_datum, data_datum, GDBM_REPLACE);
+       pthread_mutex_unlock(&data->mutex);
+       if (rcode < 0) {
+               radlog(L_ERR, "rlm_caching: Failed storing data to %s: %s",
+                               data->filename, gdbm_strerror(gdbm_errno));
+               return RLM_MODULE_FAIL;
+       }
+       DEBUG("rlm_caching: New value stored successfully.");
+
+       return RLM_MODULE_OK;
+}
+
+/*
+ *     Find the named user in this modules database.  Create the set
+ *     of attribute-value pairs to check and reply with for this user
+ *     from the database. The authentication code only needs to check
+ *     the password, the rest is done here.
+ */
+static int caching_authorize(void *instance, REQUEST *request)
+{
+       rlm_caching_t *data = (rlm_caching_t *) instance;
+       char key[MAX_STRING_LEN];
+       datum key_datum;
+       datum data_datum;
+       rlm_caching_data cache_data;
+       VALUE_PAIR *reply_item;
+       VALUE_PAIR *item;
+       char *tmp;
+       int len = 0;
+       int delete_cache = 0;
+       float hit_ratio = 0.0;
+
+       /* quiet the compiler */
+       instance = instance;
+       request = request;
+
+       if (pairfind(request->packet->vps, PW_CACHE_NO_CACHING) != NULL){
+               DEBUG("rlm_caching: Cache-No-Caching is set. Returning NOOP");
+               return RLM_MODULE_NOOP;
+       }
+       if (pairfind(request->packet->vps, PW_CACHE_DELETE_CACHE) != NULL){
+               DEBUG("rlm_caching: Found Cache-Delete-Cache. Will delete record if found");
+               delete_cache = 1;
+       }
+
+       if (!radius_xlat(key,sizeof(key), data->key, request, NULL)){
+               radlog(L_ERR, "rlm_caching: xlat on key '%s' failed.",data->key);
+               return RLM_MODULE_FAIL;
+       }
+
+       key_datum.dptr = key;
+       key_datum.dsize = strlen(key);
+
+
+       DEBUG("rlm_caching: Searching the database for key '%s'",key);
+       pthread_mutex_lock(&data->mutex);
+       data_datum = gdbm_fetch(data->gdbm, key_datum);
+       pthread_mutex_unlock(&data->mutex);
+       data->cache_queries++;
+       if (data_datum.dptr != NULL){
+               DEBUG("rlm_caching: Key Found.");
+               data->cache_hits++;
+               hit_ratio = (float)data->cache_hits / data->cache_queries;
+               hit_ratio *= 100.0;
+               memcpy(&cache_data, data_datum.dptr, sizeof(rlm_caching_data));
+               free(data_datum.dptr);
+
+               if (delete_cache == 0 && cache_data.creation + data->cache_ttl <= time(NULL)){
+                       DEBUG("rlm_cacing: Cache entry has expired");
+                       DEBUG("rlm_caching: Cache Queries: %7d, Cache Hits: %7d, Hit Ratio: %.2f%%",
+                       data->cache_queries,data->cache_hits,hit_ratio);
+                       show_hit_ratio;
+                       delete_cache = 1;
+               }
+               if (delete_cache){
+                       DEBUG("rlm_caching: Deleting record");
+                       
+                       pthread_mutex_lock(&data->mutex);
+                       gdbm_delete(data->gdbm, key_datum);
+                       pthread_mutex_unlock(&data->mutex);
+
+                       return RLM_MODULE_NOOP;
+               }
+               tmp = cache_data.data;
+               if (tmp){
+                       pairfree(&request->reply->vps);
+                       while(tmp && len < cache_data.len){
+                               reply_item = NULL;
+                               if (userparse(tmp, &reply_item) > 0 && reply_item != NULL)
+                                       pairadd(&request->reply->vps, reply_item);
+                               len += (strlen(tmp) + 1);
+                               DEBUG("rlm_caching: VP='%s',VALUE='%s',lenth='%d',cache record length='%d'",
+                               reply_item->name,reply_item->strvalue,reply_item->length,len);
+                               tmp = cache_data.data + len;
+                       }
+               }
+               else{
+                       DEBUG("rlm_caching: No reply items found. Returning NOOP");
+                       return RLM_MODULE_NOOP;
+               }               
+               if (cache_data.auth_type){
+                       DEBUG("rlm_caching: Adding Auth-Type '%s'",cache_data.auth_type);
+
+                       if ((item = pairfind(request->config_items, PW_AUTH_TYPE)) == NULL){
+                               item = pairmake("Auth-Type", cache_data.auth_type, T_OP_SET);
+                               pairadd(&request->config_items, item);
+                       }
+                       else{
+                               strcmp(item->strvalue, cache_data.auth_type);
+                               item->length = strlen(cache_data.auth_type);
+                       }
+               }
+               if (data->post_auth){
+                       DEBUG("rlm_caching: Adding Post-Auth-Type '%s'",data->post_auth);
+
+                       if ((item = pairfind(request->config_items, PW_POST_AUTH_TYPE)) == NULL){
+                               item = pairmake("Post-Auth-Type", data->post_auth, T_OP_SET);
+                               pairadd(&request->config_items, item);
+                       }
+                       else{
+                               strcmp(item->strvalue, data->post_auth);
+                               item->length = strlen(data->post_auth);
+                       }
+               }
+               item = pairmake("Cache-No-Caching", "YES", T_OP_EQ);
+               pairadd(&request->packet->vps, item);
+
+               DEBUG("rlm_caching: Cache Queries: %7d, Cache Hits: %7d, Hit Ratio: %.2f%%",
+                       data->cache_queries,data->cache_hits,hit_ratio);
+               show_hit_ratio;
+
+               return RLM_MODULE_OK;
+       }
+       else{
+               DEBUG("rlm_caching: Could not find the requested key in the database.");
+               DEBUG("rlm_caching: Cache Queries: %7d, Cache Hits: %7d, Hit Ratio: %.2f%%",
+                       data->cache_queries,data->cache_hits,hit_ratio);
+               show_hit_ratio;
+       }
+
+       return RLM_MODULE_NOOP;
+}
+
+static int caching_detach(void *instance)
+{
+       rlm_caching_t *data = (rlm_caching_t *) instance;
+
+       if (data->gdbm)
+               gdbm_close(data->gdbm);
+       free(data->filename);
+       free(data->key);
+       free(data->post_auth);
+       free(data->cache_ttl_str);
+       pthread_mutex_destroy(&data->mutex);
+
+       free(instance);
+       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.
+ */
+module_t rlm_caching = {
+       "Caching",
+       RLM_TYPE_THREAD_SAFE,           /* type */
+       NULL,                           /* initialization */
+       caching_instantiate,            /* instantiation */
+       {
+               NULL,                   /* authentication */
+               caching_authorize,      /* authorization */
+               NULL,                   /* preaccounting */
+               NULL,                   /* accounting */
+               NULL,                   /* checksimul */
+               NULL,                   /* pre-proxy */
+               NULL,                   /* post-proxy */
+               caching_postauth        /* post-auth */
+       },
+       caching_detach,                 /* detach */
+       NULL,                           /* destroy */
+};