6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
20 * Copyright 2001,2006 The FreeRADIUS server project
21 * Copyright 2001 Alan DeKok <aland@ox.org>
24 /* This module is based directly on the rlm_counter module */
27 #include <freeradius-devel/ident.h>
30 #include <freeradius-devel/radiusd.h>
31 #include <freeradius-devel/modules.h>
35 #define MAX_QUERY_LEN 1024
37 static int sqlcounter_detach(void *instance);
40 * Note: When your counter spans more than 1 period (ie 3 months
41 * or 2 weeks), this module probably does NOT do what you want! It
42 * calculates the range of dates to count across by first calculating
43 * the End of the Current period and then subtracting the number of
44 * periods you specify from that to determine the beginning of the
47 * For example, if you specify a 3 month counter and today is June 15th,
48 * the end of the current period is June 30. Subtracting 3 months from
49 * that gives April 1st. So, the counter will sum radacct entries from
50 * April 1st to June 30. Then, next month, it will sum entries from
51 * May 1st to July 31st.
53 * To fix this behavior, we need to add some way of storing the Next
58 * Define a structure for our module configuration.
60 * These variables do not need to be in a structure, but it's
61 * a lot cleaner to do so, and a pointer to the structure can
62 * be used as the instance handle.
64 typedef struct rlm_sqlcounter_t {
65 char *counter_name; /* Daily-Session-Time */
66 char *check_name; /* Max-Daily-Session */
67 char *reply_name; /* Session-Timeout */
68 char *key_name; /* User-Name */
69 char *sqlmod_inst; /* instance of SQL module to use, usually just 'sql' */
70 char *query; /* SQL query to retrieve current session time */
71 char *reset; /* daily, weekly, monthly, never or user defined */
74 DICT_ATTR *key_attr; /* attribute number for key field */
75 DICT_ATTR *dict_attr; /* attribute number for the counter. */
76 DICT_ATTR *reply_attr; /* attribute number for the reply */
80 * A mapping of configuration file names to internal variables.
82 * Note that the string is dynamically allocated, so it MUST
83 * be freed. When the configuration file parse re-reads the string,
84 * it free's the old one, and strdup's the new one, placing the pointer
85 * to the strdup'd string into 'config.string'. This gets around
88 static const CONF_PARSER module_config[] = {
89 { "counter-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,counter_name), NULL, NULL },
90 { "check-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,check_name), NULL, NULL },
91 { "reply-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,reply_name), NULL, "Session-Timeout" },
92 { "key", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,key_name), NULL, NULL },
93 { "sql-module-instance", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,sqlmod_inst), NULL, NULL },
94 { "query", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,query), NULL, NULL },
95 { "reset", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,reset), NULL, NULL },
96 { NULL, -1, 0, NULL, NULL }
99 static int find_next_reset(rlm_sqlcounter_t *data, time_t timeval)
103 unsigned int num = 1;
106 char sCurrentTime[40], sNextTime[40];
108 tm = localtime_r(&timeval, &s_tm);
109 len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
110 if (len == 0) *sCurrentTime = '\0';
111 tm->tm_sec = tm->tm_min = 0;
113 if (data->reset == NULL)
115 if (isdigit((int) data->reset[0])){
116 len = strlen(data->reset);
119 last = data->reset[len - 1];
120 if (!isalpha((int) last))
122 num = atoi(data->reset);
123 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
125 if (strcmp(data->reset, "hourly") == 0 || last == 'h') {
127 * Round up to the next nearest hour.
130 data->reset_time = mktime(tm);
131 } else if (strcmp(data->reset, "daily") == 0 || last == 'd') {
133 * Round up to the next nearest day.
137 data->reset_time = mktime(tm);
138 } else if (strcmp(data->reset, "weekly") == 0 || last == 'w') {
140 * Round up to the next nearest week.
143 tm->tm_mday += (7 - tm->tm_wday) +(7*(num-1));
144 data->reset_time = mktime(tm);
145 } else if (strcmp(data->reset, "monthly") == 0 || last == 'm') {
149 data->reset_time = mktime(tm);
150 } else if (strcmp(data->reset, "never") == 0) {
151 data->reset_time = 0;
153 radlog(L_ERR, "rlm_sqlcounter: Unknown reset timer \"%s\"",
158 len = strftime(sNextTime, sizeof(sNextTime),"%Y-%m-%d %H:%M:%S",tm);
159 if (len == 0) *sNextTime = '\0';
160 DEBUG2("rlm_sqlcounter: Current Time: %li [%s], Next reset %li [%s]",
161 timeval, sCurrentTime, data->reset_time, sNextTime);
167 /* I don't believe that this routine handles Daylight Saving Time adjustments
168 properly. Any suggestions?
171 static int find_prev_reset(rlm_sqlcounter_t *data, time_t timeval)
175 unsigned int num = 1;
178 char sCurrentTime[40], sPrevTime[40];
180 tm = localtime_r(&timeval, &s_tm);
181 len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
182 if (len == 0) *sCurrentTime = '\0';
183 tm->tm_sec = tm->tm_min = 0;
185 if (data->reset == NULL)
187 if (isdigit((int) data->reset[0])){
188 len = strlen(data->reset);
191 last = data->reset[len - 1];
192 if (!isalpha((int) last))
194 num = atoi(data->reset);
195 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
197 if (strcmp(data->reset, "hourly") == 0 || last == 'h') {
199 * Round down to the prev nearest hour.
201 tm->tm_hour -= num - 1;
202 data->last_reset = mktime(tm);
203 } else if (strcmp(data->reset, "daily") == 0 || last == 'd') {
205 * Round down to the prev nearest day.
208 tm->tm_mday -= num - 1;
209 data->last_reset = mktime(tm);
210 } else if (strcmp(data->reset, "weekly") == 0 || last == 'w') {
212 * Round down to the prev nearest week.
215 tm->tm_mday -= (7 - tm->tm_wday) +(7*(num-1));
216 data->last_reset = mktime(tm);
217 } else if (strcmp(data->reset, "monthly") == 0 || last == 'm') {
220 tm->tm_mon -= num - 1;
221 data->last_reset = mktime(tm);
222 } else if (strcmp(data->reset, "never") == 0) {
223 data->reset_time = 0;
225 radlog(L_ERR, "rlm_sqlcounter: Unknown reset timer \"%s\"",
229 len = strftime(sPrevTime, sizeof(sPrevTime), "%Y-%m-%d %H:%M:%S", tm);
230 if (len == 0) *sPrevTime = '\0';
231 DEBUG2("rlm_sqlcounter: Current Time: %li [%s], Prev reset %li [%s]",
232 timeval, sCurrentTime, data->last_reset, sPrevTime);
239 * Replace %<whatever> in a string.
248 static int sqlcounter_expand(char *out, int outlen, const char *fmt, void *instance)
250 rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
254 char tmpdt[40]; /* For temporary storing of dates */
257 for (p = fmt; *p ; p++) {
258 /* Calculate freespace in output */
259 freespace = outlen - (q - out);
263 if ((c != '%') && (c != '\\')) {
267 if (*++p == '\0') break;
268 if (c == '\\') switch(*p) {
283 } else if (c == '%') switch(*p) {
288 case 'b': /* last_reset */
289 snprintf(tmpdt, sizeof(tmpdt), "%lu", data->last_reset);
290 strlcpy(q, tmpdt, freespace);
293 case 'e': /* reset_time */
294 snprintf(tmpdt, sizeof(tmpdt), "%lu", data->reset_time);
295 strlcpy(q, tmpdt, freespace);
298 case 'k': /* Key Name */
299 DEBUG2("WARNING: Please replace '%%k' with '${key}'");
300 strlcpy(q, data->key_name, freespace);
311 DEBUG2("sqlcounter_expand: '%s'", out);
318 * See if the counter matches.
320 static int sqlcounter_cmp(void *instance, REQUEST *req,
321 UNUSED VALUE_PAIR *request, VALUE_PAIR *check,
322 VALUE_PAIR *check_pairs, VALUE_PAIR **reply_pairs)
324 rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
326 char querystr[MAX_QUERY_LEN];
327 char sqlxlat[MAX_QUERY_LEN];
329 check_pairs = check_pairs; /* shut the compiler up */
330 reply_pairs = reply_pairs;
332 /* first, expand %k, %b and %e in query */
333 sqlcounter_expand(querystr, MAX_QUERY_LEN, data->query, instance);
335 /* third, wrap query with sql module call & expand */
336 snprintf(sqlxlat, sizeof(sqlxlat), "%%{%s:%s}", data->sqlmod_inst, querystr);
338 /* Finally, xlat resulting SQL query */
339 radius_xlat(querystr, MAX_QUERY_LEN, sqlxlat, req, NULL, NULL);
341 counter = atoi(querystr);
343 return counter - check->vp_integer;
348 * Do any per-module initialization that is separate to each
349 * configured instance of the module. e.g. set up connections
350 * to external databases, read configuration files, set up
351 * dictionary entries, etc.
353 * If configuration information is given in the config section
354 * that must be referenced in later calls, store a handle to it
355 * in *instance otherwise put a null pointer there.
357 static int sqlcounter_instantiate(CONF_SECTION *conf, void **instance)
359 rlm_sqlcounter_t *data;
365 * Set up a storage area for instance data
367 data = rad_malloc(sizeof(*data));
369 radlog(L_ERR, "rlm_sqlcounter: Not enough memory.");
372 memset(data, 0, sizeof(*data));
375 * If the configuration parameters can't be parsed, then
378 if (cf_section_parse(conf, data, module_config) < 0) {
379 radlog(L_ERR, "rlm_sqlcounter: Unable to parse parameters.");
380 sqlcounter_detach(data);
387 if (data->query == NULL) {
388 radlog(L_ERR, "rlm_sqlcounter: 'query' must be set.");
389 sqlcounter_detach(data);
394 * Discover the attribute number of the key.
396 if (data->key_name == NULL) {
397 radlog(L_ERR, "rlm_sqlcounter: 'key' must be set.");
398 sqlcounter_detach(data);
401 dattr = dict_attrbyname(data->key_name);
403 radlog(L_ERR, "rlm_sqlcounter: No such attribute %s",
405 sqlcounter_detach(data);
408 data->key_attr = dattr;
410 dattr = dict_attrbyname(data->reply_name);
412 radlog(L_ERR, "rlm_sqlcounter: No such attribute %s",
414 sqlcounter_detach(data);
417 data->reply_attr = dattr;
418 DEBUG2("rlm_sqlcounter: Reply attribute %s is number %d",
419 data->reply_name, dattr->attr);
422 * Check the "sqlmod-inst" option.
424 if (data->sqlmod_inst == NULL) {
425 radlog(L_ERR, "rlm_sqlcounter: 'sqlmod-inst' must be set.");
426 sqlcounter_detach(data);
431 * Create a new attribute for the counter.
433 if (data->counter_name == NULL) {
434 radlog(L_ERR, "rlm_sqlcounter: 'counter-name' must be set.");
435 sqlcounter_detach(data);
439 memset(&flags, 0, sizeof(flags));
440 dict_addattr(data->counter_name, -1, 0, PW_TYPE_INTEGER, flags);
441 dattr = dict_attrbyname(data->counter_name);
443 radlog(L_ERR, "rlm_sqlcounter: Failed to create counter attribute %s",
445 sqlcounter_detach(data);
448 if (dattr->vendor != 0) {
449 radlog(L_ERR, "Counter attribute must not be a VSA");
450 sqlcounter_detach(data);
453 data->dict_attr = dattr;
456 * Create a new attribute for the check item.
458 if (data->check_name == NULL) {
459 radlog(L_ERR, "rlm_sqlcounter: 'check-name' must be set.");
460 sqlcounter_detach(data);
463 dict_addattr(data->check_name, 0, PW_TYPE_INTEGER, -1, flags);
464 dattr = dict_attrbyname(data->check_name);
466 radlog(L_ERR, "rlm_sqlcounter: Failed to create check attribute %s",
468 sqlcounter_detach(data);
471 DEBUG2("rlm_sqlcounter: Check attribute %s is number %d",
472 data->check_name, dattr->attr);
475 * Discover the end of the current time period.
477 if (data->reset == NULL) {
478 radlog(L_ERR, "rlm_sqlcounter: 'reset' must be set.");
479 sqlcounter_detach(data);
483 data->reset_time = 0;
485 if (find_next_reset(data,now) == -1) {
486 radlog(L_ERR, "rlm_sqlcounter: Failed to find the next reset time.");
487 sqlcounter_detach(data);
492 * Discover the beginning of the current time period.
494 data->last_reset = 0;
496 if (find_prev_reset(data,now) == -1) {
497 radlog(L_ERR, "rlm_sqlcounter: Failed to find the previous reset time.");
498 sqlcounter_detach(data);
503 * Register the counter comparison operation.
505 paircompare_register(data->dict_attr->attr, 0, sqlcounter_cmp, data);
513 * Find the named user in this modules database. Create the set
514 * of attribute-value pairs to check and reply with for this user
515 * from the database. The authentication code only needs to check
516 * the password, the rest is done here.
518 static int sqlcounter_authorize(void *instance, REQUEST *request)
520 rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
521 int ret=RLM_MODULE_NOOP;
522 unsigned int counter;
524 VALUE_PAIR *key_vp, *check_vp;
525 VALUE_PAIR *reply_item;
527 char querystr[MAX_QUERY_LEN];
528 char sqlxlat[MAX_QUERY_LEN];
530 /* quiet the compiler */
535 * Before doing anything else, see if we have to reset
538 if (data->reset_time && (data->reset_time <= request->timestamp)) {
541 * Re-set the next time and prev_time for this counters range
543 data->last_reset = data->reset_time;
544 find_next_reset(data,request->timestamp);
549 * Look for the key. User-Name is special. It means
550 * The REAL username, after stripping.
552 DEBUG2("rlm_sqlcounter: Entering module authorize code");
553 key_vp = ((data->key_attr->vendor == 0) && (data->key_attr->attr == PW_USER_NAME)) ? request->username : pairfind(request->packet->vps, data->key_attr->attr, data->key_attr->vendor);
554 if (key_vp == NULL) {
555 DEBUG2("rlm_sqlcounter: Could not find Key value pair");
560 * Look for the check item
562 if ((dattr = dict_attrbyname(data->check_name)) == NULL) {
565 /* DEBUG2("rlm_sqlcounter: Found Check item attribute %d", dattr->attr); */
566 if ((check_vp= pairfind(request->config_items, dattr->attr, dattr->vendor)) == NULL) {
567 DEBUG2("rlm_sqlcounter: Could not find Check item value pair");
571 /* first, expand %k, %b and %e in query */
572 sqlcounter_expand(querystr, MAX_QUERY_LEN, data->query, instance);
574 /* next, wrap query with sql module & expand */
575 snprintf(sqlxlat, sizeof(sqlxlat), "%%{%s:%s}", data->sqlmod_inst, querystr);
577 /* Finally, xlat resulting SQL query */
578 radius_xlat(querystr, MAX_QUERY_LEN, sqlxlat, request, NULL, NULL);
580 if (sscanf(querystr, "%u", &counter) != 1) {
581 DEBUG2("rlm_sqlcounter: No integer found in string \"%s\"",
583 return RLM_MODULE_NOOP;
587 * Check if check item > counter
589 if (check_vp->vp_integer > counter) {
590 unsigned int res = check_vp->vp_integer - counter;
592 DEBUG2("rlm_sqlcounter: Check item is greater than query result");
594 * We are assuming that simultaneous-use=1. But
595 * even if that does not happen then our user
596 * could login at max for 2*max-usage-time Is
601 * If we are near a reset then add the next
602 * limit, so that the user will not need to login
603 * again. Do this only for Session-Timeout.
605 if ((data->reply_attr->attr == PW_SESSION_TIMEOUT) &&
607 (res >= (data->reset_time - request->timestamp))) {
608 res = data->reset_time - request->timestamp;
609 res += check_vp->vp_integer;
613 * Limit the reply attribute to the minimum of
614 * the existing value, or this new one.
616 reply_item = pairfind(request->reply->vps, data->reply_attr->attr, data->reply_attr->vendor);
618 if (reply_item->vp_integer > res)
619 reply_item->vp_integer = res;
622 reply_item = radius_paircreate(request,
623 &request->reply->vps,
624 data->reply_attr->attr,
625 data->reply_attr->vendor,
627 reply_item->vp_integer = res;
632 DEBUG2("rlm_sqlcounter: Authorized user %s, check_item=%u, counter=%u",
633 key_vp->vp_strvalue,check_vp->vp_integer,counter);
634 DEBUG2("rlm_sqlcounter: Sent Reply-Item for user %s, Type=%s, value=%u",
635 key_vp->vp_strvalue,data->reply_name,reply_item->vp_integer);
638 char module_fmsg[MAX_STRING_LEN];
639 VALUE_PAIR *module_fmsg_vp;
641 DEBUG2("rlm_sqlcounter: (Check item - counter) is less than zero");
644 * User is denied access, send back a reply message
646 snprintf(msg, sizeof(msg), "Your maximum %s usage time has been reached", data->reset);
647 reply_item=pairmake("Reply-Message", msg, T_OP_EQ);
648 pairadd(&request->reply->vps, reply_item);
650 snprintf(module_fmsg, sizeof(module_fmsg), "rlm_sqlcounter: Maximum %s usage time reached", data->reset);
651 module_fmsg_vp = pairmake("Module-Failure-Message", module_fmsg, T_OP_EQ);
652 pairadd(&request->packet->vps, module_fmsg_vp);
654 ret=RLM_MODULE_REJECT;
656 DEBUG2("rlm_sqlcounter: Rejected user %s, check_item=%u, counter=%u",
657 key_vp->vp_strvalue,check_vp->vp_integer,counter);
663 static int sqlcounter_detach(void *instance)
667 rlm_sqlcounter_t *inst = (rlm_sqlcounter_t *)instance;
669 paircompare_unregister(inst->dict_attr->attr, sqlcounter_cmp);
672 * Free up dynamically allocated string pointers.
674 for (i = 0; module_config[i].name != NULL; i++) {
675 if (module_config[i].type != PW_TYPE_STRING_PTR) {
680 * Treat 'config' as an opaque array of bytes,
681 * and take the offset into it. There's a
682 * (char*) pointer at that offset, and we want
685 p = (char **) (((char *)inst) + module_config[i].offset);
686 if (!*p) { /* nothing allocated */
697 * The module name should be the only globally exported symbol.
698 * That is, everything else should be 'static'.
700 * If the module needs to temporarily modify it's instantiation
701 * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
702 * The server will then take care of ensuring that the module
703 * is single-threaded.
705 module_t rlm_sqlcounter = {
708 RLM_TYPE_THREAD_SAFE, /* type */
709 sqlcounter_instantiate, /* instantiation */
710 sqlcounter_detach, /* detach */
712 NULL, /* authentication */
713 sqlcounter_authorize, /* authorization */
714 NULL, /* preaccounting */
715 NULL, /* accounting */
716 NULL, /* checksimul */
717 NULL, /* pre-proxy */
718 NULL, /* post-proxy */