/* * rlm_sqlcounter.c * * Version: $Id: rlm_sqlcounter.c,v 1.7 2002/09/08 14:47:09 kkalev Exp $ * * 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 */ /* This module is based directly on the rlm_counter module */ #include "config.h" #include "autoconf.h" #include "libradius.h" #include #include #include #include #include "radiusd.h" #include "modules.h" #include "conffile.h" #define MAX_QUERY_LEN 1024 #include /* Note: When your counter spans more than 1 period (ie 3 months or 2 weeks), this module * probably does NOT do what you want! It calculates the range of dates to count across * by first calculating the End of the Current period and then subtracting the number of * periods you specify from that to determine the beginning of the range. * * For example, if you specify a 3 month counter and today is June 15th, the end of the current * period is June 30. Subtracting 3 months from that gives April 1st. So, the counter will * sum radacct entries from April 1st to June 30. Then, next month, it will sum entries * from May 1st to July 31st. * * To fix this behavior, we need to add some way of storing the Next Reset Time */ static const char rcsid[] = "$Id: rlm_sqlcounter.c,v 1.7 2002/09/08 14:47:09 kkalev Exp $"; /* * 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_sqlcounter_t { char *counter_name; /* Daily-Session-Time */ char *check_name; /* Max-Daily-Session */ char *key_name; /* User-Name */ char *sqlmod_inst; /* instance of SQL module to use, usually just 'sql' */ char *query; /* SQL query to retrieve current session time */ char *reset; /* daily, weekly, monthly, never or user defined */ time_t reset_time; time_t last_reset; int key_attr; /* attribute number for key field */ int dict_attr; /* attribute number for the counter. */ } rlm_sqlcounter_t; /* * 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[] = { { "counter-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,counter_name), NULL, NULL }, { "check-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,check_name), NULL, NULL }, { "key", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,key_name), NULL, NULL }, { "sqlmod-inst", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,sqlmod_inst), NULL, NULL }, { "query", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,query), NULL, NULL }, { "reset", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,reset), NULL, NULL }, { NULL, -1, 0, NULL, NULL } }; static int find_next_reset(rlm_sqlcounter_t *data, time_t timeval) { int ret=0; unsigned int num=1; char last = 0; struct tm *tm, s_tm; char sCurrentTime[40], sNextTime[40]; tm = localtime_r(&timeval, &s_tm); strftime(sCurrentTime, sizeof(sCurrentTime),"%Y-%m-%d %H:%M:%S",tm); tm->tm_sec = tm->tm_min = 0; if (data->reset == NULL) return -1; if (isdigit((int) data->reset[0])){ unsigned int len=0; len = strlen(data->reset); if (len == 0) return -1; last = data->reset[len - 1]; if (!isalpha((int) last)) last = 'd'; /* num = atoi(data->reset); */ DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last); } if (strcmp(data->reset, "hourly") == 0 || last == 'h') { /* * Round up to the next nearest hour. */ tm->tm_hour += num; data->reset_time = mktime(tm); } else if (strcmp(data->reset, "daily") == 0 || last == 'd') { /* * Round up to the next nearest day. */ tm->tm_hour = 0; tm->tm_mday += num; data->reset_time = mktime(tm); } else if (strcmp(data->reset, "weekly") == 0 || last == 'w') { /* * Round up to the next nearest week. */ tm->tm_hour = 0; tm->tm_mday += (7 - tm->tm_wday) +(7*(num-1)); data->reset_time = mktime(tm); } else if (strcmp(data->reset, "monthly") == 0 || last == 'm') { tm->tm_hour = 0; tm->tm_mday = 1; tm->tm_mon += num; data->reset_time = mktime(tm); } else if (strcmp(data->reset, "never") == 0) { data->reset_time = 0; } else { radlog(L_ERR, "rlm_sqlcounter: Unknown reset timer \"%s\"", data->reset); return -1; } strftime(sNextTime, sizeof(sNextTime),"%Y-%m-%d %H:%M:%S",tm); DEBUG2("rlm_sqlcounter: Current Time: %d [%s], Next reset %d [%s]", (int)timeval,sCurrentTime,(int)data->reset_time, sNextTime); return ret; } /* I don't believe that this routine handles Daylight Saving Time adjustments properly. Any suggestions? */ static int find_prev_reset(rlm_sqlcounter_t *data, time_t timeval) { int ret=0; unsigned int num=1; char last = 0; struct tm *tm, s_tm; char sCurrentTime[40], sPrevTime[40]; tm = localtime_r(&timeval, &s_tm); strftime(sCurrentTime, sizeof(sCurrentTime),"%Y-%m-%d %H:%M:%S",tm); tm->tm_sec = tm->tm_min = 0; if (data->reset == NULL) return -1; if (isdigit((int) data->reset[0])){ unsigned int len=0; len = strlen(data->reset); if (len == 0) return -1; last = data->reset[len - 1]; if (!isalpha((int) last)) last = 'd'; num = atoi(data->reset); DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last); } if (strcmp(data->reset, "hourly") == 0 || last == 'h') { /* * Round down to the prev nearest hour. */ tm->tm_hour -= num - 1; data->last_reset = mktime(tm); } else if (strcmp(data->reset, "daily") == 0 || last == 'd') { /* * Round down to the prev nearest day. */ tm->tm_hour = 0; tm->tm_mday -= num - 1; data->last_reset = mktime(tm); } else if (strcmp(data->reset, "weekly") == 0 || last == 'w') { /* * Round down to the prev nearest week. */ tm->tm_hour = 0; tm->tm_mday -= (7 - tm->tm_wday) +(7*(num-1)); data->last_reset = mktime(tm); } else if (strcmp(data->reset, "monthly") == 0 || last == 'm') { tm->tm_hour = 0; tm->tm_mday = 1; tm->tm_mon -= num - 1; data->last_reset = mktime(tm); } else if (strcmp(data->reset, "never") == 0) { data->reset_time = 0; } else { radlog(L_ERR, "rlm_sqlcounter: Unknown reset timer \"%s\"", data->reset); return -1; } strftime(sPrevTime, sizeof(sPrevTime),"%Y-%m-%d %H:%M:%S",tm); DEBUG2("rlm_sqlcounter: Current Time: %d [%s], Prev reset %d [%s]", (int)timeval,sCurrentTime,(int)data->last_reset, sPrevTime); return ret; } /* * Replace % in a string. * * %b last_reset * %e reset_time * %k key_name * %S sqlmod_inst * */ static int sqlcounter_expand(char *out, int outlen, const char *fmt, void *instance) { rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance; int c,freespace; const char *p; char *q; char tmpdt[40]; /* For temporary storing of dates */ int openbraces=0; q = out; for (p = fmt; *p ; p++) { /* Calculate freespace in output */ freespace = outlen - (q - out); if (freespace <= 1) break; c = *p; if ((c != '%') && (c != '$') && (c != '\\')) { /* * We check if we're inside an open brace. If we are * then we assume this brace is NOT literal, but is * a closing brace and apply it */ if((c == '}') && openbraces) { openbraces--; continue; } *q++ = *p; continue; } if (*++p == '\0') break; if (c == '\\') switch(*p) { case '\\': *q++ = *p; break; case 't': *q++ = '\t'; break; case 'n': *q++ = '\n'; break; default: *q++ = c; *q++ = *p; break; } else if (c == '%') switch(*p) { case '%': *q++ = *p; case 'b': /* last_reset */ sprintf(tmpdt, "%lu", data->last_reset); strNcpy(q, tmpdt, freespace); q += strlen(q); break; case 'e': /* reset_time */ sprintf(tmpdt, "%lu", data->reset_time); strNcpy(q, tmpdt, freespace); q += strlen(q); break; case 'k': /* Key Name */ strNcpy(q, data->key_name, freespace); q += strlen(q); break; case 'S': /* SQL module instance */ strNcpy(q, data->sqlmod_inst, freespace); q += strlen(q); break; default: *q++ = '%'; *q++ = *p; break; } } *q = '\0'; DEBUG2("sqlcounter_expand: '%s'", out); return strlen(out); } /* * See if the counter matches. */ static int sqlcounter_cmp(void *instance, REQUEST *req, VALUE_PAIR *request, VALUE_PAIR *check, VALUE_PAIR *check_pairs, VALUE_PAIR **reply_pairs) { rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance; int counter; char querystr[MAX_QUERY_LEN]; char responsestr[MAX_QUERY_LEN]; check_pairs = check_pairs; /* shut the compiler up */ reply_pairs = reply_pairs; /* first, expand %k, %b and %e in query */ sqlcounter_expand(querystr, MAX_QUERY_LEN, data->query, instance); /* second, xlat any request attribs in query */ radius_xlat(responsestr, MAX_QUERY_LEN, querystr, req, NULL); /* third, wrap query with sql module call & expand */ sprintf(querystr, "%%{%%S:%s}", responsestr); sqlcounter_expand(responsestr, MAX_QUERY_LEN, querystr, instance); /* Finally, xlat resulting SQL query */ radius_xlat(querystr, MAX_QUERY_LEN, responsestr, req, NULL); counter = atoi(querystr); return counter - check->lvalue; } /* * 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 sqlcounter_instantiate(CONF_SECTION *conf, void **instance) { rlm_sqlcounter_t *data; DICT_ATTR *dattr; ATTR_FLAGS flags; time_t now; /* * Set up a storage area for instance data */ data = rad_malloc(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; } /* * Discover the attribute number of the key. */ if (data->key_name == NULL) { radlog(L_ERR, "rlm_sqlcounter: 'key' must be set."); exit(0); } dattr = dict_attrbyname(data->key_name); if (dattr == NULL) { radlog(L_ERR, "rlm_sqlcounter: No such attribute %s", data->key_name); return -1; } data->key_attr = dattr->attr; /* * Create a new attribute for the counter. */ if (data->counter_name == NULL) { radlog(L_ERR, "rlm_sqlcounter: 'counter-name' must be set."); exit(0); } memset(&flags, 0, sizeof(flags)); dict_addattr(data->counter_name, 0, PW_TYPE_INTEGER, -1, flags); dattr = dict_attrbyname(data->counter_name); if (dattr == NULL) { radlog(L_ERR, "rlm_sqlcounter: Failed to create counter attribute %s", data->counter_name); return -1; } data->dict_attr = dattr->attr; DEBUG2("rlm_sqlcounter: Counter attribute %s is number %d", data->counter_name, data->dict_attr); /* * Create a new attribute for the check item. */ if (data->check_name == NULL) { radlog(L_ERR, "rlm_sqlcounter: 'check-name' must be set."); exit(0); } dict_addattr(data->check_name, 0, PW_TYPE_INTEGER, -1, flags); dattr = dict_attrbyname(data->check_name); if (dattr == NULL) { radlog(L_ERR, "rlm_sqlcounter: Failed to create check attribute %s", data->counter_name); return -1; } DEBUG2("rlm_sqlcounter: Check attribute %s is number %d", data->check_name, dattr->attr); /* * Discover the end of the current time period. */ if (data->reset == NULL) { radlog(L_ERR, "rlm_sqlcounter: 'reset' must be set."); exit(0); } now = time(NULL); data->reset_time = 0; if (find_next_reset(data,now) == -1) return -1; /* * Discover the beginning of the current time period. */ data->last_reset = 0; if (find_prev_reset(data,now) == -1) return -1; /* * Register the counter comparison operation. */ paircompare_register(data->dict_attr, 0, sqlcounter_cmp, data); *instance = data; return 0; } /* * 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 sqlcounter_authorize(void *instance, REQUEST *request) { rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance; int ret=RLM_MODULE_NOOP; int counter=0; int res=0; DICT_ATTR *dattr; VALUE_PAIR *key_vp, *check_vp; VALUE_PAIR *reply_item; char msg[128]; char querystr[MAX_QUERY_LEN]; char responsestr[MAX_QUERY_LEN]; /* quiet the compiler */ instance = instance; request = request; /* * Before doing anything else, see if we have to reset * the counters. */ if (data->reset_time && (data->reset_time <= request->timestamp)) { /* * Re-set the next time and prev_time for this counters range */ data->last_reset = data->reset_time; find_next_reset(data,request->timestamp); } /* * Look for the key. User-Name is special. It means * The REAL username, after stripping. */ DEBUG2("rlm_sqlcounter: Entering module authorize code"); key_vp = (data->key_attr == PW_USER_NAME) ? request->username : pairfind(request->packet->vps, data->key_attr); if (key_vp == NULL) { DEBUG2("rlm_sqlcounter: Could not find Key value pair"); return ret; } /* * Look for the check item */ if ((dattr = dict_attrbyname(data->check_name)) == NULL) { return ret; } /* DEBUG2("rlm_sqlcounter: Found Check item attribute %d", dattr->attr); */ if ((check_vp= pairfind(request->config_items, dattr->attr)) == NULL) { DEBUG2("rlm_sqlcounter: Could not find Check item value pair"); return ret; } /* first, expand %k, %b and %e in query */ sqlcounter_expand(querystr, MAX_QUERY_LEN, data->query, instance); /* second, xlat any request attribs in query */ radius_xlat(responsestr, MAX_QUERY_LEN, querystr, request, NULL); /* third, wrap query with sql module & expand */ sprintf(querystr, "%%{%%S:%s}", responsestr); sqlcounter_expand(responsestr, MAX_QUERY_LEN, querystr, instance); /* Finally, xlat resulting SQL query */ radius_xlat(querystr, MAX_QUERY_LEN, responsestr, request, NULL); counter = atoi(querystr); /* * Check if check item > counter */ res=check_vp->lvalue - counter; if (res > 0) { DEBUG2("rlm_sqlcounter: (Check item - counter) is greater than zero"); /* * We are assuming that simultaneous-use=1. But * even if that does not happen then our user * could login at max for 2*max-usage-time Is * that acceptable? */ /* * User is allowed, but set Session-Timeout. * Stolen from main/auth.c */ /* * If we are near a reset then add the next * limit, so that the user will not need to * login again */ if (data->reset_time && ( res >= (data->reset_time - request->timestamp))) { res += check_vp->lvalue; } if ((reply_item = pairfind(request->reply->vps, PW_SESSION_TIMEOUT)) != NULL) { if (reply_item->lvalue > res) reply_item->lvalue = res; } else { if ((reply_item = paircreate(PW_SESSION_TIMEOUT, PW_TYPE_INTEGER)) == NULL) { radlog(L_ERR|L_CONS, "no memory"); return RLM_MODULE_NOOP; } reply_item->lvalue = res; pairadd(&request->reply->vps, reply_item); } ret=RLM_MODULE_OK; DEBUG2("rlm_sqlcounter: Authorized user %s, check_item=%d, counter=%d", key_vp->strvalue,check_vp->lvalue,counter); DEBUG2("rlm_sqlcounter: Sent Reply-Item for user %s, Type=Session-Timeout, value=%d", key_vp->strvalue,reply_item->lvalue); } else{ char module_fmsg[MAX_STRING_LEN]; VALUE_PAIR *module_fmsg_vp; DEBUG2("rlm_sqlcounter: (Check item - counter) is less than zero"); /* * User is denied access, send back a reply message */ sprintf(msg, "Your maximum %s usage time has been reached", data->reset); reply_item=pairmake("Reply-Message", msg, T_OP_EQ); pairadd(&request->reply->vps, reply_item); snprintf(module_fmsg, sizeof(module_fmsg), "rlm_sqlcounter: Maximum %s usage time reached", data->reset); module_fmsg_vp = pairmake("Module-Failure-Message", module_fmsg, T_OP_EQ); pairadd(&request->packet->vps, module_fmsg_vp); ret=RLM_MODULE_REJECT; DEBUG2("rlm_sqlcounter: Rejected user %s, check_item=%d, counter=%d", key_vp->strvalue,check_vp->lvalue,counter); } return ret; } static int sqlcounter_detach(void *instance) { rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance; paircompare_unregister(data->dict_attr, sqlcounter_cmp); free(data->reset); free(data->query); free(data->check_name); free(data->sqlmod_inst); free(data->counter_name); 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_sqlcounter = { "SQL Counter", RLM_TYPE_THREAD_SAFE, /* type */ NULL, /* initialization */ sqlcounter_instantiate, /* instantiation */ { NULL, /* authentication */ sqlcounter_authorize, /* authorization */ NULL, /* preaccounting */ NULL, /* accounting */ NULL, /* checksimul */ NULL, /* pre-proxy */ NULL, /* post-proxy */ NULL /* post-auth */ }, sqlcounter_detach, /* detach */ NULL, /* destroy */ };