2 * This program is is free software; you can redistribute it and/or modify
3 * it under the terms of the GNU General Public License, version 2 if the
4 * License as published by the Free Software Foundation.
6 * This program is distributed in the hope that it will be useful,
7 * but WITHOUT ANY WARRANTY; without even the implied warranty of
8 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 * GNU General Public License for more details.
11 * You should have received a copy of the GNU General Public License
12 * along with this program; if not, write to the Free Software
13 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
18 * @file rlm_sqlcounter.c
19 * @brief Tracks data usage and other counters using SQL.
21 * @copyright 2001,2006 The FreeRADIUS server project
22 * @copyright 2001 Alan DeKok <aland@ox.org>
26 #include <freeradius-devel/radiusd.h>
27 #include <freeradius-devel/modules.h>
28 #include <freeradius-devel/rad_assert.h>
32 #define MAX_QUERY_LEN 1024
35 * Note: When your counter spans more than 1 period (ie 3 months
36 * or 2 weeks), this module probably does NOT do what you want! It
37 * calculates the range of dates to count across by first calculating
38 * the End of the Current period and then subtracting the number of
39 * periods you specify from that to determine the beginning of the
42 * For example, if you specify a 3 month counter and today is June 15th,
43 * the end of the current period is June 30. Subtracting 3 months from
44 * that gives April 1st. So, the counter will sum radacct entries from
45 * April 1st to June 30. Then, next month, it will sum entries from
46 * May 1st to July 31st.
48 * To fix this behavior, we need to add some way of storing the Next
53 * Define a structure for our module configuration.
55 * These variables do not need to be in a structure, but it's
56 * a lot cleaner to do so, and a pointer to the structure can
57 * be used as the instance handle.
59 typedef struct rlm_sqlcounter_t {
60 char *counter_name; //!< Daily-Session-Time.
61 char *check_name; //!< Max-Daily-Session.
62 char *reply_name; //!< Session-Timeout.
63 char *key_name; //!< User-Name.
64 char *sqlmod_inst; //!< Instance of SQL module to use,
65 //!< usually just 'sql'.
66 char *query; //!< SQL query to retrieve current
68 char *reset; //!< Daily, weekly, monthly,
69 //!< never or user defined.
72 const DICT_ATTR *key_attr; //!< Attribute number for key field.
73 const DICT_ATTR *dict_attr; //!< Attribute number for the counter.
74 const DICT_ATTR *reply_attr; //!< Attribute number for the reply.
78 * A mapping of configuration file names to internal variables.
80 * Note that the string is dynamically allocated, so it MUST
81 * be freed. When the configuration file parse re-reads the string,
82 * it free's the old one, and strdup's the new one, placing the pointer
83 * to the strdup'd string into 'config.string'. This gets around
86 static const CONF_PARSER module_config[] = {
87 { "counter-name", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED, offsetof(rlm_sqlcounter_t,counter_name), NULL, NULL },
88 { "check-name", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED, offsetof(rlm_sqlcounter_t,check_name), NULL, NULL },
89 { "reply-name", PW_TYPE_STRING_PTR | PW_TYPE_ATTRIBUTE, offsetof(rlm_sqlcounter_t,reply_name), NULL, "Session-Timeout" },
90 { "key", PW_TYPE_STRING_PTR | PW_TYPE_ATTRIBUTE, offsetof(rlm_sqlcounter_t,key_name), NULL, NULL },
91 { "sql-module-instance", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED, offsetof(rlm_sqlcounter_t,sqlmod_inst), NULL, NULL },
92 { "query", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED , offsetof(rlm_sqlcounter_t,query), NULL, NULL },
93 { "reset", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED, offsetof(rlm_sqlcounter_t,reset), NULL, NULL },
94 { NULL, -1, 0, NULL, NULL }
97 static int find_next_reset(rlm_sqlcounter_t *inst, time_t timeval)
101 unsigned int num = 1;
104 char sCurrentTime[40], sNextTime[40];
106 tm = localtime_r(&timeval, &s_tm);
107 len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
108 if (len == 0) *sCurrentTime = '\0';
109 tm->tm_sec = tm->tm_min = 0;
111 rad_assert(inst->reset != NULL);
113 if (isdigit((int) inst->reset[0])){
114 len = strlen(inst->reset);
117 last = inst->reset[len - 1];
118 if (!isalpha((int) last))
120 num = atoi(inst->reset);
121 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
123 if (strcmp(inst->reset, "hourly") == 0 || last == 'h') {
125 * Round up to the next nearest hour.
128 inst->reset_time = mktime(tm);
129 } else if (strcmp(inst->reset, "daily") == 0 || last == 'd') {
131 * Round up to the next nearest day.
135 inst->reset_time = mktime(tm);
136 } else if (strcmp(inst->reset, "weekly") == 0 || last == 'w') {
138 * Round up to the next nearest week.
141 tm->tm_mday += (7 - tm->tm_wday) +(7*(num-1));
142 inst->reset_time = mktime(tm);
143 } else if (strcmp(inst->reset, "monthly") == 0 || last == 'm') {
147 inst->reset_time = mktime(tm);
148 } else if (strcmp(inst->reset, "never") == 0) {
149 inst->reset_time = 0;
154 len = strftime(sNextTime, sizeof(sNextTime),"%Y-%m-%d %H:%M:%S",tm);
155 if (len == 0) *sNextTime = '\0';
156 DEBUG2("rlm_sqlcounter: Current Time: %li [%s], Next reset %li [%s]",
157 timeval, sCurrentTime, inst->reset_time, sNextTime);
163 /* I don't believe that this routine handles Daylight Saving Time adjustments
164 properly. Any suggestions?
167 static int find_prev_reset(rlm_sqlcounter_t *inst, time_t timeval)
171 unsigned int num = 1;
174 char sCurrentTime[40], sPrevTime[40];
176 tm = localtime_r(&timeval, &s_tm);
177 len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
178 if (len == 0) *sCurrentTime = '\0';
179 tm->tm_sec = tm->tm_min = 0;
181 rad_assert(inst->reset != NULL);
183 if (isdigit((int) inst->reset[0])){
184 len = strlen(inst->reset);
187 last = inst->reset[len - 1];
188 if (!isalpha((int) last))
190 num = atoi(inst->reset);
191 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
193 if (strcmp(inst->reset, "hourly") == 0 || last == 'h') {
195 * Round down to the prev nearest hour.
197 tm->tm_hour -= num - 1;
198 inst->last_reset = mktime(tm);
199 } else if (strcmp(inst->reset, "daily") == 0 || last == 'd') {
201 * Round down to the prev nearest day.
204 tm->tm_mday -= num - 1;
205 inst->last_reset = mktime(tm);
206 } else if (strcmp(inst->reset, "weekly") == 0 || last == 'w') {
208 * Round down to the prev nearest week.
211 tm->tm_mday -= (7 - tm->tm_wday) +(7*(num-1));
212 inst->last_reset = mktime(tm);
213 } else if (strcmp(inst->reset, "monthly") == 0 || last == 'm') {
216 tm->tm_mon -= num - 1;
217 inst->last_reset = mktime(tm);
218 } else if (strcmp(inst->reset, "never") == 0) {
219 inst->reset_time = 0;
223 len = strftime(sPrevTime, sizeof(sPrevTime), "%Y-%m-%d %H:%M:%S", tm);
224 if (len == 0) *sPrevTime = '\0';
225 DEBUG2("rlm_sqlcounter: Current Time: %li [%s], Prev reset %li [%s]",
226 timeval, sCurrentTime, inst->last_reset, sPrevTime);
233 * Replace %<whatever> in a string.
242 static int sqlcounter_expand(char *out, int outlen, const char *fmt, rlm_sqlcounter_t *inst)
247 char tmpdt[40]; /* For temporary storing of dates */
250 for (p = fmt; *p ; p++) {
251 /* Calculate freespace in output */
252 freespace = outlen - (q - out);
256 if ((c != '%') && (c != '\\')) {
260 if (*++p == '\0') break;
261 if (c == '\\') switch(*p) {
276 } else if (c == '%') switch(*p) {
281 case 'b': /* last_reset */
282 snprintf(tmpdt, sizeof(tmpdt), "%lu", inst->last_reset);
283 strlcpy(q, tmpdt, freespace);
286 case 'e': /* reset_time */
287 snprintf(tmpdt, sizeof(tmpdt), "%lu", inst->reset_time);
288 strlcpy(q, tmpdt, freespace);
291 case 'k': /* Key Name */
292 DEBUG2W("Please replace '%%k' with '${key}'");
293 strlcpy(q, inst->key_name, freespace);
304 DEBUG2("sqlcounter_expand: '%s'", out);
311 * See if the counter matches.
313 static int sqlcounter_cmp(void *instance, REQUEST *req,
314 UNUSED VALUE_PAIR *request, VALUE_PAIR *check,
315 UNUSED VALUE_PAIR *check_pairs, UNUSED VALUE_PAIR **reply_pairs)
317 rlm_sqlcounter_t *inst = instance;
319 char querystr[MAX_QUERY_LEN];
320 char sqlxlat[MAX_QUERY_LEN];
322 /* first, expand %k, %b and %e in query */
323 sqlcounter_expand(querystr, MAX_QUERY_LEN, inst->query, inst);
325 /* third, wrap query with sql module call & expand */
326 snprintf(sqlxlat, sizeof(sqlxlat), "%%{%s:%s}", inst->sqlmod_inst, querystr);
328 /* Finally, xlat resulting SQL query */
329 radius_xlat(querystr, MAX_QUERY_LEN, sqlxlat, req, NULL, NULL);
331 counter = atoi(querystr);
333 return counter - check->vp_integer;
338 * Do any per-module initialization that is separate to each
339 * configured instance of the module. e.g. set up connections
340 * to external databases, read configuration files, set up
341 * dictionary entries, etc.
343 * If configuration information is given in the config section
344 * that must be referenced in later calls, store a handle to it
345 * in *instance otherwise put a null pointer there.
347 static int mod_instantiate(CONF_SECTION *conf, void *instance)
349 rlm_sqlcounter_t *inst = instance;
350 const DICT_ATTR *dattr;
354 rad_assert(inst->query && *inst->query);
356 dattr = dict_attrbyname(inst->key_name);
357 rad_assert(dattr != NULL);
358 if (dattr->vendor != 0) {
359 cf_log_err_cs(conf, "Configuration item 'key' cannot be a VSA");
362 inst->key_attr = dattr;
364 dattr = dict_attrbyname(inst->reply_name);
365 rad_assert(dattr != NULL);
366 if (dattr->vendor != 0) {
367 cf_log_err_cs(conf, "Configuration item 'reply-name' cannot be a VSA");
370 inst->reply_attr = dattr;
372 DEBUG2("rlm_sqlcounter: Reply attribute %s is number %d",
373 inst->reply_name, dattr->attr);
376 * Create a new attribute for the counter.
378 rad_assert(inst->counter_name && *inst->counter_name);
379 memset(&flags, 0, sizeof(flags));
380 dict_addattr(inst->counter_name, -1, 0, PW_TYPE_INTEGER, flags);
381 dattr = dict_attrbyname(inst->counter_name);
383 cf_log_err_cs(conf, "Failed to create counter attribute %s",
387 if (dattr->vendor != 0) {
388 cf_log_err_cs(conf, "Counter attribute must not be a VSA");
391 inst->dict_attr = dattr;
394 * Create a new attribute for the check item.
396 rad_assert(inst->check_name && *inst->check_name);
397 dict_addattr(inst->check_name, 0, PW_TYPE_INTEGER, -1, flags);
398 dattr = dict_attrbyname(inst->check_name);
400 cf_log_err_cs(conf, "Failed to create check attribute %s",
404 DEBUG2("rlm_sqlcounter: Check attribute %s is number %d",
405 inst->check_name, dattr->attr);
408 inst->reset_time = 0;
410 if (find_next_reset(inst,now) == -1) {
411 cf_log_err_cs(conf, "Invalid reset '%s'", inst->reset);
416 * Discover the beginning of the current time period.
418 inst->last_reset = 0;
420 if (find_prev_reset(inst, now) < 0) {
421 cf_log_err_cs(conf, "Invalid reset '%s'", inst->reset);
426 * Register the counter comparison operation.
428 paircompare_register(inst->dict_attr->attr, 0, sqlcounter_cmp, inst);
434 * Find the named user in this modules database. Create the set
435 * of attribute-value pairs to check and reply with for this user
436 * from the database. The authentication code only needs to check
437 * the password, the rest is done here.
439 static rlm_rcode_t mod_authorize(UNUSED void *instance, UNUSED REQUEST *request)
441 rlm_sqlcounter_t *inst = instance;
442 int rcode = RLM_MODULE_NOOP;
443 unsigned int counter;
444 const DICT_ATTR *dattr;
445 VALUE_PAIR *key_vp, *check_vp;
446 VALUE_PAIR *reply_item;
448 char querystr[MAX_QUERY_LEN];
449 char sqlxlat[MAX_QUERY_LEN];
452 * Before doing anything else, see if we have to reset
455 if (inst->reset_time && (inst->reset_time <= request->timestamp)) {
458 * Re-set the next time and prev_time for this counters range
460 inst->last_reset = inst->reset_time;
461 find_next_reset(inst,request->timestamp);
466 * Look for the key. User-Name is special. It means
467 * The REAL username, after stripping.
469 DEBUG2("rlm_sqlcounter: Entering module authorize code");
470 key_vp = ((inst->key_attr->vendor == 0) && (inst->key_attr->attr == PW_USER_NAME)) ? request->username : pairfind(request->packet->vps, inst->key_attr->attr, inst->key_attr->vendor, TAG_ANY);
472 DEBUG2("rlm_sqlcounter: Could not find Key value pair");
477 * Look for the check item
479 if ((dattr = dict_attrbyname(inst->check_name)) == NULL) {
482 /* DEBUG2("rlm_sqlcounter: Found Check item attribute %d", dattr->attr); */
483 if ((check_vp= pairfind(request->config_items, dattr->attr, dattr->vendor, TAG_ANY)) == NULL) {
484 DEBUG2("rlm_sqlcounter: Could not find Check item value pair");
488 /* first, expand %k, %b and %e in query */
489 sqlcounter_expand(querystr, MAX_QUERY_LEN, inst->query, inst);
491 /* next, wrap query with sql module & expand */
492 snprintf(sqlxlat, sizeof(sqlxlat), "%%{%s:%s}", inst->sqlmod_inst, querystr);
494 /* Finally, xlat resulting SQL query */
495 radius_xlat(querystr, MAX_QUERY_LEN, sqlxlat, request, NULL, NULL);
497 if (sscanf(querystr, "%u", &counter) != 1) {
498 DEBUG2("rlm_sqlcounter: No integer found in string \"%s\"",
500 return RLM_MODULE_NOOP;
504 * Check if check item > counter
506 if (check_vp->vp_integer > counter) {
507 unsigned int res = check_vp->vp_integer - counter;
509 DEBUG2("rlm_sqlcounter: Check item is greater than query result");
511 * We are assuming that simultaneous-use=1. But
512 * even if that does not happen then our user
513 * could login at max for 2*max-usage-time Is
518 * If we are near a reset then add the next
519 * limit, so that the user will not need to login
520 * again. Do this only for Session-Timeout.
522 if ((inst->reply_attr->attr == PW_SESSION_TIMEOUT) &&
524 (res >= (inst->reset_time - request->timestamp))) {
525 res = inst->reset_time - request->timestamp;
526 res += check_vp->vp_integer;
530 * Limit the reply attribute to the minimum of
531 * the existing value, or this new one.
533 reply_item = pairfind(request->reply->vps, inst->reply_attr->attr, inst->reply_attr->vendor, TAG_ANY);
535 if (reply_item->vp_integer > res)
536 reply_item->vp_integer = res;
539 reply_item = radius_paircreate(request,
540 &request->reply->vps,
541 inst->reply_attr->attr,
542 inst->reply_attr->vendor);
543 reply_item->vp_integer = res;
546 rcode = RLM_MODULE_OK;
548 DEBUG2("rlm_sqlcounter: Authorized user %s, check_item=%u, counter=%u",
549 key_vp->vp_strvalue,check_vp->vp_integer,counter);
550 DEBUG2("rlm_sqlcounter: Sent Reply-Item for user %s, Type=%s, value=%u",
551 key_vp->vp_strvalue,inst->reply_name,reply_item->vp_integer);
554 DEBUG2("rlm_sqlcounter: (Check item - counter) is less than zero");
557 * User is denied access, send back a reply message
559 snprintf(msg, sizeof(msg), "Your maximum %s usage time has been reached", inst->reset);
560 pairmake_reply("Reply-Message", msg, T_OP_EQ);
562 RDEBUGE("Maximum %s usage time reached",
564 rcode = RLM_MODULE_REJECT;
566 DEBUG2("rlm_sqlcounter: Rejected user %s, check_item=%u, counter=%u",
567 key_vp->vp_strvalue,check_vp->vp_integer,counter);
574 * The module name should be the only globally exported symbol.
575 * That is, everything else should be 'static'.
577 * If the module needs to temporarily modify it's instantiation
578 * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
579 * The server will then take care of ensuring that the module
580 * is single-threaded.
582 module_t rlm_sqlcounter = {
585 RLM_TYPE_THREAD_SAFE, /* type */
586 sizeof(rlm_sqlcounter_t),
588 mod_instantiate, /* instantiation */
591 NULL, /* authentication */
592 mod_authorize, /* authorization */
593 NULL, /* preaccounting */
594 NULL, /* accounting */
595 NULL, /* checksimul */
596 NULL, /* pre-proxy */
597 NULL, /* post-proxy */