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/autoconf.h>
37 #include <freeradius-devel/radiusd.h>
38 #include <freeradius-devel/modules.h>
40 #define MAX_QUERY_LEN 1024
42 static int sqlcounter_detach(void *instance);
45 * Note: When your counter spans more than 1 period (ie 3 months
46 * or 2 weeks), this module probably does NOT do what you want! It
47 * calculates the range of dates to count across by first calculating
48 * the End of the Current period and then subtracting the number of
49 * periods you specify from that to determine the beginning of the
52 * For example, if you specify a 3 month counter and today is June 15th,
53 * the end of the current period is June 30. Subtracting 3 months from
54 * that gives April 1st. So, the counter will sum radacct entries from
55 * April 1st to June 30. Then, next month, it will sum entries from
56 * May 1st to July 31st.
58 * To fix this behavior, we need to add some way of storing the Next
63 * Define a structure for our module configuration.
65 * These variables do not need to be in a structure, but it's
66 * a lot cleaner to do so, and a pointer to the structure can
67 * be used as the instance handle.
69 typedef struct rlm_sqlcounter_t {
70 char *counter_name; /* Daily-Session-Time */
71 char *check_name; /* Max-Daily-Session */
72 char *reply_name; /* Session-Timeout */
73 char *key_name; /* User-Name */
74 char *sqlmod_inst; /* instance of SQL module to use, usually just 'sql' */
75 char *query; /* SQL query to retrieve current session time */
76 char *reset; /* daily, weekly, monthly, never or user defined */
77 char *allowed_chars; /* safe characters list for SQL queries */
80 int key_attr; /* attribute number for key field */
81 int dict_attr; /* attribute number for the counter. */
82 int reply_attr; /* attribute number for the reply */
86 * A mapping of configuration file names to internal variables.
88 * Note that the string is dynamically allocated, so it MUST
89 * be freed. When the configuration file parse re-reads the string,
90 * it free's the old one, and strdup's the new one, placing the pointer
91 * to the strdup'd string into 'config.string'. This gets around
94 static const CONF_PARSER module_config[] = {
95 { "counter-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,counter_name), NULL, NULL },
96 { "check-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,check_name), NULL, NULL },
97 { "reply-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,reply_name), NULL, NULL },
98 { "key", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,key_name), NULL, NULL },
99 { "sqlmod-inst", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,sqlmod_inst), NULL, NULL },
100 { "query", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,query), NULL, NULL },
101 { "reset", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,reset), NULL, NULL },
102 { "safe-characters", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,allowed_chars), NULL, "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /"},
103 { NULL, -1, 0, NULL, NULL }
106 static char *allowed_chars = NULL;
109 * Translate the SQL queries.
111 static int sql_escape_func(char *out, int outlen, const char *in)
117 * Non-printable characters get replaced with their
118 * mime-encoded equivalents.
121 strchr(allowed_chars, *in) == NULL) {
123 * Only 3 or less bytes available.
129 snprintf(out, outlen, "=%02X", (unsigned char) in[0]);
138 * Only one byte left.
157 static int find_next_reset(rlm_sqlcounter_t *data, time_t timeval)
161 unsigned int num = 1;
164 char sCurrentTime[40], sNextTime[40];
166 tm = localtime_r(&timeval, &s_tm);
167 len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
168 if (len == 0) *sCurrentTime = '\0';
169 tm->tm_sec = tm->tm_min = 0;
171 if (data->reset == NULL)
173 if (isdigit((int) data->reset[0])){
174 len = strlen(data->reset);
177 last = data->reset[len - 1];
178 if (!isalpha((int) last))
180 num = atoi(data->reset);
181 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
183 if (strcmp(data->reset, "hourly") == 0 || last == 'h') {
185 * Round up to the next nearest hour.
188 data->reset_time = mktime(tm);
189 } else if (strcmp(data->reset, "daily") == 0 || last == 'd') {
191 * Round up to the next nearest day.
195 data->reset_time = mktime(tm);
196 } else if (strcmp(data->reset, "weekly") == 0 || last == 'w') {
198 * Round up to the next nearest week.
201 tm->tm_mday += (7 - tm->tm_wday) +(7*(num-1));
202 data->reset_time = mktime(tm);
203 } else if (strcmp(data->reset, "monthly") == 0 || last == 'm') {
207 data->reset_time = mktime(tm);
208 } else if (strcmp(data->reset, "never") == 0) {
209 data->reset_time = 0;
211 radlog(L_ERR, "rlm_sqlcounter: Unknown reset timer \"%s\"",
216 len = strftime(sNextTime, sizeof(sNextTime),"%Y-%m-%d %H:%M:%S",tm);
217 if (len == 0) *sNextTime = '\0';
218 DEBUG2("rlm_sqlcounter: Current Time: %li [%s], Next reset %li [%s]",
219 timeval, sCurrentTime, data->reset_time, sNextTime);
225 /* I don't believe that this routine handles Daylight Saving Time adjustments
226 properly. Any suggestions?
229 static int find_prev_reset(rlm_sqlcounter_t *data, time_t timeval)
233 unsigned int num = 1;
236 char sCurrentTime[40], sPrevTime[40];
238 tm = localtime_r(&timeval, &s_tm);
239 len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
240 if (len == 0) *sCurrentTime = '\0';
241 tm->tm_sec = tm->tm_min = 0;
243 if (data->reset == NULL)
245 if (isdigit((int) data->reset[0])){
246 len = strlen(data->reset);
249 last = data->reset[len - 1];
250 if (!isalpha((int) last))
252 num = atoi(data->reset);
253 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
255 if (strcmp(data->reset, "hourly") == 0 || last == 'h') {
257 * Round down to the prev nearest hour.
259 tm->tm_hour -= num - 1;
260 data->last_reset = mktime(tm);
261 } else if (strcmp(data->reset, "daily") == 0 || last == 'd') {
263 * Round down to the prev nearest day.
266 tm->tm_mday -= num - 1;
267 data->last_reset = mktime(tm);
268 } else if (strcmp(data->reset, "weekly") == 0 || last == 'w') {
270 * Round down to the prev nearest week.
273 tm->tm_mday -= (7 - tm->tm_wday) +(7*(num-1));
274 data->last_reset = mktime(tm);
275 } else if (strcmp(data->reset, "monthly") == 0 || last == 'm') {
278 tm->tm_mon -= num - 1;
279 data->last_reset = mktime(tm);
280 } else if (strcmp(data->reset, "never") == 0) {
281 data->reset_time = 0;
283 radlog(L_ERR, "rlm_sqlcounter: Unknown reset timer \"%s\"",
287 len = strftime(sPrevTime, sizeof(sPrevTime), "%Y-%m-%d %H:%M:%S", tm);
288 if (len == 0) *sPrevTime = '\0';
289 DEBUG2("rlm_sqlcounter: Current Time: %li [%s], Prev reset %li [%s]",
290 timeval, sCurrentTime, data->last_reset, sPrevTime);
297 * Replace %<whatever> in a string.
306 static int sqlcounter_expand(char *out, int outlen, const char *fmt, void *instance)
308 rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
312 char tmpdt[40]; /* For temporary storing of dates */
316 for (p = fmt; *p ; p++) {
317 /* Calculate freespace in output */
318 freespace = outlen - (q - out);
322 if ((c != '%') && (c != '$') && (c != '\\')) {
324 * We check if we're inside an open brace. If we are
325 * then we assume this brace is NOT literal, but is
326 * a closing brace and apply it
328 if((c == '}') && openbraces) {
335 if (*++p == '\0') break;
336 if (c == '\\') switch(*p) {
351 } else if (c == '%') switch(*p) {
355 case 'b': /* last_reset */
356 snprintf(tmpdt, sizeof(tmpdt), "%lu", data->last_reset);
357 strlcpy(q, tmpdt, freespace);
360 case 'e': /* reset_time */
361 snprintf(tmpdt, sizeof(tmpdt), "%lu", data->reset_time);
362 strlcpy(q, tmpdt, freespace);
365 case 'k': /* Key Name */
366 strlcpy(q, data->key_name, freespace);
369 case 'S': /* SQL module instance */
370 strlcpy(q, data->sqlmod_inst, freespace);
381 DEBUG2("sqlcounter_expand: '%s'", out);
388 * See if the counter matches.
390 static int sqlcounter_cmp(void *instance, REQUEST *req,
391 UNUSED VALUE_PAIR *request, VALUE_PAIR *check,
392 VALUE_PAIR *check_pairs, VALUE_PAIR **reply_pairs)
394 rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
396 char querystr[MAX_QUERY_LEN];
397 char responsestr[MAX_QUERY_LEN];
399 check_pairs = check_pairs; /* shut the compiler up */
400 reply_pairs = reply_pairs;
402 /* first, expand %k, %b and %e in query */
403 sqlcounter_expand(querystr, MAX_QUERY_LEN, data->query, instance);
405 /* second, xlat any request attribs in query */
406 radius_xlat(responsestr, MAX_QUERY_LEN, querystr, req, sql_escape_func);
408 /* third, wrap query with sql module call & expand */
409 snprintf(querystr, sizeof(querystr), "%%{%%S:%s}", responsestr);
410 sqlcounter_expand(responsestr, MAX_QUERY_LEN, querystr, instance);
412 /* Finally, xlat resulting SQL query */
413 radius_xlat(querystr, MAX_QUERY_LEN, responsestr, req, sql_escape_func);
415 counter = atoi(querystr);
417 return counter - check->lvalue;
422 * Do any per-module initialization that is separate to each
423 * configured instance of the module. e.g. set up connections
424 * to external databases, read configuration files, set up
425 * dictionary entries, etc.
427 * If configuration information is given in the config section
428 * that must be referenced in later calls, store a handle to it
429 * in *instance otherwise put a null pointer there.
431 static int sqlcounter_instantiate(CONF_SECTION *conf, void **instance)
433 rlm_sqlcounter_t *data;
437 char buffer[MAX_STRING_LEN];
440 * Set up a storage area for instance data
442 data = rad_malloc(sizeof(*data));
444 radlog(L_ERR, "rlm_sqlcounter: Not enough memory.");
447 memset(data, 0, sizeof(*data));
450 * If the configuration parameters can't be parsed, then
453 if (cf_section_parse(conf, data, module_config) < 0) {
454 radlog(L_ERR, "rlm_sqlcounter: Unable to parse parameters.");
455 sqlcounter_detach(data);
462 if (data->query == NULL) {
463 radlog(L_ERR, "rlm_sqlcounter: 'query' must be set.");
464 sqlcounter_detach(data);
469 * Safe characters list for sql queries. Everything else is
470 * replaced with their mime-encoded equivalents.
472 allowed_chars = data->allowed_chars;
475 * Discover the attribute number of the key.
477 if (data->key_name == NULL) {
478 radlog(L_ERR, "rlm_sqlcounter: 'key' must be set.");
479 sqlcounter_detach(data);
482 sql_escape_func(buffer, sizeof(buffer), data->key_name);
483 if (strcmp(buffer, data->key_name) != 0) {
484 radlog(L_ERR, "rlm_sqlcounter: The value for option 'key' is too long or contains unsafe characters.");
485 sqlcounter_detach(data);
488 dattr = dict_attrbyname(data->key_name);
490 radlog(L_ERR, "rlm_sqlcounter: No such attribute %s",
492 sqlcounter_detach(data);
495 data->key_attr = dattr->attr;
498 * Discover the attribute number of the reply.
499 * If not set, set it to Session-Timeout
500 * for backward compatibility.
502 if (data->reply_name == NULL) {
503 DEBUG2("rlm_sqlcounter: Reply attribute set to Session-Timeout.");
504 data->reply_attr = PW_SESSION_TIMEOUT;
505 data->reply_name = strdup("Session-Timeout");
508 dattr = dict_attrbyname(data->reply_name);
510 radlog(L_ERR, "rlm_sqlcounter: No such attribute %s",
512 sqlcounter_detach(data);
515 data->reply_attr = dattr->attr;
516 DEBUG2("rlm_sqlcounter: Reply attribute %s is number %d",
517 data->reply_name, dattr->attr);
521 * Check the "sqlmod-inst" option.
523 if (data->sqlmod_inst == NULL) {
524 radlog(L_ERR, "rlm_sqlcounter: 'sqlmod-inst' must be set.");
525 sqlcounter_detach(data);
528 sql_escape_func(buffer, sizeof(buffer), data->sqlmod_inst);
529 if (strcmp(buffer, data->sqlmod_inst) != 0) {
530 radlog(L_ERR, "rlm_sqlcounter: The value for option 'sqlmod-inst' is too long or contains unsafe characters.");
531 sqlcounter_detach(data);
536 * Create a new attribute for the counter.
538 if (data->counter_name == NULL) {
539 radlog(L_ERR, "rlm_sqlcounter: 'counter-name' must be set.");
540 sqlcounter_detach(data);
544 memset(&flags, 0, sizeof(flags));
545 dict_addattr(data->counter_name, 0, PW_TYPE_INTEGER, -1, flags);
546 dattr = dict_attrbyname(data->counter_name);
548 radlog(L_ERR, "rlm_sqlcounter: Failed to create counter attribute %s",
550 sqlcounter_detach(data);
553 data->dict_attr = dattr->attr;
554 DEBUG2("rlm_sqlcounter: Counter attribute %s is number %d",
555 data->counter_name, data->dict_attr);
558 * Create a new attribute for the check item.
560 if (data->check_name == NULL) {
561 radlog(L_ERR, "rlm_sqlcounter: 'check-name' must be set.");
562 sqlcounter_detach(data);
565 dict_addattr(data->check_name, 0, PW_TYPE_INTEGER, -1, flags);
566 dattr = dict_attrbyname(data->check_name);
568 radlog(L_ERR, "rlm_sqlcounter: Failed to create check attribute %s",
570 sqlcounter_detach(data);
573 DEBUG2("rlm_sqlcounter: Check attribute %s is number %d",
574 data->check_name, dattr->attr);
577 * Discover the end of the current time period.
579 if (data->reset == NULL) {
580 radlog(L_ERR, "rlm_sqlcounter: 'reset' must be set.");
581 sqlcounter_detach(data);
585 data->reset_time = 0;
587 if (find_next_reset(data,now) == -1) {
588 radlog(L_ERR, "rlm_sqlcounter: Failed to find the next reset time.");
589 sqlcounter_detach(data);
594 * Discover the beginning of the current time period.
596 data->last_reset = 0;
598 if (find_prev_reset(data,now) == -1) {
599 radlog(L_ERR, "rlm_sqlcounter: Failed to find the previous reset time.");
600 sqlcounter_detach(data);
605 * Register the counter comparison operation.
607 paircompare_register(data->dict_attr, 0, sqlcounter_cmp, data);
615 * Find the named user in this modules database. Create the set
616 * of attribute-value pairs to check and reply with for this user
617 * from the database. The authentication code only needs to check
618 * the password, the rest is done here.
620 static int sqlcounter_authorize(void *instance, REQUEST *request)
622 rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
623 int ret=RLM_MODULE_NOOP;
627 VALUE_PAIR *key_vp, *check_vp;
628 VALUE_PAIR *reply_item;
630 char querystr[MAX_QUERY_LEN];
631 char responsestr[MAX_QUERY_LEN];
633 /* quiet the compiler */
638 * Before doing anything else, see if we have to reset
641 if (data->reset_time && (data->reset_time <= request->timestamp)) {
644 * Re-set the next time and prev_time for this counters range
646 data->last_reset = data->reset_time;
647 find_next_reset(data,request->timestamp);
652 * Look for the key. User-Name is special. It means
653 * The REAL username, after stripping.
655 DEBUG2("rlm_sqlcounter: Entering module authorize code");
656 key_vp = (data->key_attr == PW_USER_NAME) ? request->username : pairfind(request->packet->vps, data->key_attr);
657 if (key_vp == NULL) {
658 DEBUG2("rlm_sqlcounter: Could not find Key value pair");
663 * Look for the check item
665 if ((dattr = dict_attrbyname(data->check_name)) == NULL) {
668 /* DEBUG2("rlm_sqlcounter: Found Check item attribute %d", dattr->attr); */
669 if ((check_vp= pairfind(request->config_items, dattr->attr)) == NULL) {
670 DEBUG2("rlm_sqlcounter: Could not find Check item value pair");
674 /* first, expand %k, %b and %e in query */
675 sqlcounter_expand(querystr, MAX_QUERY_LEN, data->query, instance);
677 /* second, xlat any request attribs in query */
678 radius_xlat(responsestr, MAX_QUERY_LEN, querystr, request, sql_escape_func);
680 /* third, wrap query with sql module & expand */
681 snprintf(querystr, sizeof(querystr), "%%{%%S:%s}", responsestr);
682 sqlcounter_expand(responsestr, MAX_QUERY_LEN, querystr, instance);
684 /* Finally, xlat resulting SQL query */
685 radius_xlat(querystr, MAX_QUERY_LEN, responsestr, request, sql_escape_func);
687 counter = atoi(querystr);
691 * Check if check item > counter
693 res=check_vp->lvalue - counter;
695 DEBUG2("rlm_sqlcounter: (Check item - counter) is greater than zero");
697 * We are assuming that simultaneous-use=1. But
698 * even if that does not happen then our user
699 * could login at max for 2*max-usage-time Is
704 * User is allowed, but set Session-Timeout.
705 * Stolen from main/auth.c
709 * If we are near a reset then add the next
710 * limit, so that the user will not need to
713 if (data->reset_time && (
714 res >= (data->reset_time - request->timestamp))) {
715 res = data->reset_time - request->timestamp;
716 res += check_vp->lvalue;
719 if ((reply_item = pairfind(request->reply->vps, data->reply_attr)) != NULL) {
720 if (reply_item->lvalue > res)
721 reply_item->lvalue = res;
723 if ((reply_item = paircreate(data->reply_attr, PW_TYPE_INTEGER)) == NULL) {
724 radlog(L_ERR|L_CONS, "no memory");
725 return RLM_MODULE_NOOP;
727 reply_item->lvalue = res;
728 pairadd(&request->reply->vps, reply_item);
733 DEBUG2("rlm_sqlcounter: Authorized user %s, check_item=%d, counter=%d",
734 key_vp->vp_strvalue,check_vp->lvalue,counter);
735 DEBUG2("rlm_sqlcounter: Sent Reply-Item for user %s, Type=%s, value=%d",
736 key_vp->vp_strvalue,data->reply_name,reply_item->lvalue);
739 char module_fmsg[MAX_STRING_LEN];
740 VALUE_PAIR *module_fmsg_vp;
742 DEBUG2("rlm_sqlcounter: (Check item - counter) is less than zero");
745 * User is denied access, send back a reply message
747 snprintf(msg, sizeof(msg), "Your maximum %s usage time has been reached", data->reset);
748 reply_item=pairmake("Reply-Message", msg, T_OP_EQ);
749 pairadd(&request->reply->vps, reply_item);
751 snprintf(module_fmsg, sizeof(module_fmsg), "rlm_sqlcounter: Maximum %s usage time reached", data->reset);
752 module_fmsg_vp = pairmake("Module-Failure-Message", module_fmsg, T_OP_EQ);
753 pairadd(&request->packet->vps, module_fmsg_vp);
755 ret=RLM_MODULE_REJECT;
757 DEBUG2("rlm_sqlcounter: Rejected user %s, check_item=%d, counter=%d",
758 key_vp->vp_strvalue,check_vp->lvalue,counter);
764 static int sqlcounter_detach(void *instance)
768 rlm_sqlcounter_t *inst = (rlm_sqlcounter_t *)instance;
770 allowed_chars = NULL;
771 paircompare_unregister(inst->dict_attr, sqlcounter_cmp);
774 * Free up dynamically allocated string pointers.
776 for (i = 0; module_config[i].name != NULL; i++) {
777 if (module_config[i].type != PW_TYPE_STRING_PTR) {
782 * Treat 'config' as an opaque array of bytes,
783 * and take the offset into it. There's a
784 * (char*) pointer at that offset, and we want
787 p = (char **) (((char *)inst) + module_config[i].offset);
788 if (!*p) { /* nothing allocated */
799 * The module name should be the only globally exported symbol.
800 * That is, everything else should be 'static'.
802 * If the module needs to temporarily modify it's instantiation
803 * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
804 * The server will then take care of ensuring that the module
805 * is single-threaded.
807 module_t rlm_sqlcounter = {
810 RLM_TYPE_THREAD_SAFE, /* type */
811 sqlcounter_instantiate, /* instantiation */
812 sqlcounter_detach, /* detach */
814 NULL, /* authentication */
815 sqlcounter_authorize, /* authorization */
816 NULL, /* preaccounting */
817 NULL, /* accounting */
818 NULL, /* checksimul */
819 NULL, /* pre-proxy */
820 NULL, /* post-proxy */