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 DICT_ATTR const *key_attr; //!< Attribute number for key field.
73 DICT_ATTR const *dict_attr; //!< Attribute number for the counter.
74 DICT_ATTR const *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 { "sql-module-instance", PW_TYPE_STRING_PTR | PW_TYPE_DEPRECATED,
88 offsetof(rlm_sqlcounter_t,sqlmod_inst), NULL, NULL },
89 { "sql_module_instance", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED,
90 offsetof(rlm_sqlcounter_t,sqlmod_inst), NULL, NULL },
92 { "key", PW_TYPE_STRING_PTR | PW_TYPE_ATTRIBUTE,
93 offsetof(rlm_sqlcounter_t,key_name), NULL, NULL },
94 { "query", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED,
95 offsetof(rlm_sqlcounter_t,query), NULL, NULL },
96 { "reset", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED,
97 offsetof(rlm_sqlcounter_t,reset), NULL, NULL },
99 { "counter-name", PW_TYPE_STRING_PTR | PW_TYPE_DEPRECATED,
100 offsetof(rlm_sqlcounter_t,counter_name), NULL, NULL },
101 { "counter_name", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED,
102 offsetof(rlm_sqlcounter_t,counter_name), NULL, NULL },
104 { "check-name", PW_TYPE_STRING_PTR | PW_TYPE_DEPRECATED,
105 offsetof(rlm_sqlcounter_t,check_name), NULL, NULL },
106 { "check_name", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED,
107 offsetof(rlm_sqlcounter_t,check_name), NULL, NULL },
109 { "reply-name", PW_TYPE_STRING_PTR | PW_TYPE_DEPRECATED,
110 offsetof(rlm_sqlcounter_t,reply_name), NULL, NULL },
111 { "reply_name", PW_TYPE_STRING_PTR | PW_TYPE_ATTRIBUTE,
112 offsetof(rlm_sqlcounter_t,reply_name), NULL, "Session-Timeout" },
114 { NULL, -1, 0, NULL, NULL }
117 static int find_next_reset(rlm_sqlcounter_t *inst, time_t timeval)
121 unsigned int num = 1;
124 char sCurrentTime[40], sNextTime[40];
126 tm = localtime_r(&timeval, &s_tm);
127 len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
128 if (len == 0) *sCurrentTime = '\0';
129 tm->tm_sec = tm->tm_min = 0;
131 rad_assert(inst->reset != NULL);
133 if (isdigit((int) inst->reset[0])){
134 len = strlen(inst->reset);
137 last = inst->reset[len - 1];
138 if (!isalpha((int) last))
140 num = atoi(inst->reset);
141 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
143 if (strcmp(inst->reset, "hourly") == 0 || last == 'h') {
145 * Round up to the next nearest hour.
148 inst->reset_time = mktime(tm);
149 } else if (strcmp(inst->reset, "daily") == 0 || last == 'd') {
151 * Round up to the next nearest day.
155 inst->reset_time = mktime(tm);
156 } else if (strcmp(inst->reset, "weekly") == 0 || last == 'w') {
158 * Round up to the next nearest week.
161 tm->tm_mday += (7 - tm->tm_wday) +(7*(num-1));
162 inst->reset_time = mktime(tm);
163 } else if (strcmp(inst->reset, "monthly") == 0 || last == 'm') {
167 inst->reset_time = mktime(tm);
168 } else if (strcmp(inst->reset, "never") == 0) {
169 inst->reset_time = 0;
174 len = strftime(sNextTime, sizeof(sNextTime),"%Y-%m-%d %H:%M:%S",tm);
175 if (len == 0) *sNextTime = '\0';
176 DEBUG2("rlm_sqlcounter: Current Time: %" PRId64 " [%s], Next reset %" PRId64 " [%s]",
177 (int64_t) timeval, sCurrentTime, (int64_t) inst->reset_time, sNextTime);
183 /* I don't believe that this routine handles Daylight Saving Time adjustments
184 properly. Any suggestions?
187 static int find_prev_reset(rlm_sqlcounter_t *inst, time_t timeval)
191 unsigned int num = 1;
194 char sCurrentTime[40], sPrevTime[40];
196 tm = localtime_r(&timeval, &s_tm);
197 len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
198 if (len == 0) *sCurrentTime = '\0';
199 tm->tm_sec = tm->tm_min = 0;
201 rad_assert(inst->reset != NULL);
203 if (isdigit((int) inst->reset[0])){
204 len = strlen(inst->reset);
207 last = inst->reset[len - 1];
208 if (!isalpha((int) last))
210 num = atoi(inst->reset);
211 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
213 if (strcmp(inst->reset, "hourly") == 0 || last == 'h') {
215 * Round down to the prev nearest hour.
217 tm->tm_hour -= num - 1;
218 inst->last_reset = mktime(tm);
219 } else if (strcmp(inst->reset, "daily") == 0 || last == 'd') {
221 * Round down to the prev nearest day.
224 tm->tm_mday -= num - 1;
225 inst->last_reset = mktime(tm);
226 } else if (strcmp(inst->reset, "weekly") == 0 || last == 'w') {
228 * Round down to the prev nearest week.
231 tm->tm_mday -= (7 - tm->tm_wday) +(7*(num-1));
232 inst->last_reset = mktime(tm);
233 } else if (strcmp(inst->reset, "monthly") == 0 || last == 'm') {
236 tm->tm_mon -= num - 1;
237 inst->last_reset = mktime(tm);
238 } else if (strcmp(inst->reset, "never") == 0) {
239 inst->reset_time = 0;
243 len = strftime(sPrevTime, sizeof(sPrevTime), "%Y-%m-%d %H:%M:%S", tm);
244 if (len == 0) *sPrevTime = '\0';
245 DEBUG2("rlm_sqlcounter: Current Time: %" PRId64 " [%s], Prev reset %" PRId64 " [%s]",
246 (int64_t) timeval, sCurrentTime, (int64_t) inst->last_reset, sPrevTime);
253 * Replace %<whatever> in a string.
262 static size_t sqlcounter_expand(char *out, int outlen, char const *fmt, rlm_sqlcounter_t *inst)
267 char tmpdt[40]; /* For temporary storing of dates */
270 for (p = fmt; *p ; p++) {
271 /* Calculate freespace in output */
272 freespace = outlen - (q - out);
273 if (freespace <= 1) {
277 if ((c != '%') && (c != '\\')) {
281 if (*++p == '\0') break;
282 if (c == '\\') switch(*p) {
297 } else if (c == '%') switch(*p) {
302 case 'b': /* last_reset */
303 snprintf(tmpdt, sizeof(tmpdt), "%" PRId64, (int64_t) inst->last_reset);
304 strlcpy(q, tmpdt, freespace);
307 case 'e': /* reset_time */
308 snprintf(tmpdt, sizeof(tmpdt), "%" PRId64, (int64_t) inst->reset_time);
309 strlcpy(q, tmpdt, freespace);
312 case 'k': /* Key Name */
313 WDEBUG2("Please replace '%%k' with '${key}'");
314 strlcpy(q, inst->key_name, freespace);
325 DEBUG2("sqlcounter_expand: '%s'", out);
332 * See if the counter matches.
334 static int sqlcounter_cmp(void *instance, REQUEST *request, UNUSED VALUE_PAIR *req , VALUE_PAIR *check,
335 UNUSED VALUE_PAIR *check_pairs, UNUSED VALUE_PAIR **reply_pairs)
337 rlm_sqlcounter_t *inst = instance;
340 char query[MAX_QUERY_LEN];
342 char *expanded = NULL;
343 size_t len, freespace = sizeof(query);
345 /* Add xlat prefix */
346 len = snprintf(query, freespace, "%%{%s:", inst->sqlmod_inst);
347 if (len >= sizeof(query) - 1) {
348 REDEBUG("Insufficient query buffer space");
350 return RLM_MODULE_FAIL;
356 /* Copy query, performing any sqlcounter specific substitutions */
357 len = sqlcounter_expand(p, freespace, inst->query, inst);
359 REDEBUG("Insufficient query buffer space");
361 return RLM_MODULE_FAIL;
368 REDEBUG("Insufficient query buffer space");
370 return RLM_MODULE_FAIL;
373 /* Add xlat suffix */
377 /* Finally, xlat query */
378 if (radius_axlat(&expanded, request, query, NULL, NULL) < 0) {
379 return RLM_MODULE_FAIL;
382 counter = strtoull(expanded, NULL, 10);
383 talloc_free(expanded);
385 if (counter < check->vp_integer64) {
388 if (counter > check->vp_integer64) {
396 * Do any per-module initialization that is separate to each
397 * configured instance of the module. e.g. set up connections
398 * to external databases, read configuration files, set up
399 * dictionary entries, etc.
401 * If configuration information is given in the config section
402 * that must be referenced in later calls, store a handle to it
403 * in *instance otherwise put a null pointer there.
405 static int mod_instantiate(CONF_SECTION *conf, void *instance)
407 rlm_sqlcounter_t *inst = instance;
412 rad_assert(inst->query && *inst->query);
414 da = dict_attrbyname(inst->key_name);
416 cf_log_err_cs(conf, "Invalid attribute '%s'", inst->key_name);
421 da = dict_attrbyname(inst->reply_name);
423 cf_log_err_cs(conf, "Invalid attribute '%s'", inst->reply_name);
426 inst->reply_attr = da;
429 * Create a new attribute for the counter.
431 rad_assert(inst->counter_name && *inst->counter_name);
432 memset(&flags, 0, sizeof(flags));
433 dict_addattr(inst->counter_name, -1, 0, PW_TYPE_INTEGER, flags);
434 da = dict_attrbyname(inst->counter_name);
436 cf_log_err_cs(conf, "Failed to create counter attribute %s", inst->counter_name);
439 inst->dict_attr = da;
442 * Create a new attribute for the check item.
444 rad_assert(inst->check_name && *inst->check_name);
445 dict_addattr(inst->check_name, -1, 0, PW_TYPE_INTEGER, flags);
446 da = dict_attrbyname(inst->check_name);
448 cf_log_err_cs(conf, "Failed to create check attribute %s", inst->check_name);
453 inst->reset_time = 0;
455 if (find_next_reset(inst,now) == -1) {
456 cf_log_err_cs(conf, "Invalid reset '%s'", inst->reset);
461 * Discover the beginning of the current time period.
463 inst->last_reset = 0;
465 if (find_prev_reset(inst, now) < 0) {
466 cf_log_err_cs(conf, "Invalid reset '%s'", inst->reset);
471 * Register the counter comparison operation.
473 paircompare_register(inst->dict_attr, NULL, true, sqlcounter_cmp, inst);
479 * Find the named user in this modules database. Create the set
480 * of attribute-value pairs to check and reply with for this user
481 * from the database. The authentication code only needs to check
482 * the password, the rest is done here.
484 static rlm_rcode_t mod_authorize(void *instance, REQUEST *request)
486 rlm_sqlcounter_t *inst = instance;
487 int rcode = RLM_MODULE_NOOP;
488 uint64_t counter, res;
490 VALUE_PAIR *key_vp, *check_vp;
491 VALUE_PAIR *reply_item;
495 char query[MAX_QUERY_LEN];
496 char *expanded = NULL;
500 rad_assert(instance != NULL);
501 rad_assert(request != NULL);
504 * Before doing anything else, see if we have to reset
507 if (inst->reset_time && (inst->reset_time <= request->timestamp)) {
509 * Re-set the next time and prev_time for this counters range
511 inst->last_reset = inst->reset_time;
512 find_next_reset(inst,request->timestamp);
516 * Look for the key. User-Name is special. It means
517 * The REAL username, after stripping.
519 key_vp = ((inst->key_attr->vendor == 0) && (inst->key_attr->attr == PW_USER_NAME)) ?
521 pairfind(request->packet->vps, inst->key_attr->attr, inst->key_attr->vendor, TAG_ANY);
523 RDEBUG2("Could not find Key value pair");
528 * Look for the check item
530 if ((da = dict_attrbyname(inst->check_name)) == NULL) {
533 /* DEBUG2("rlm_sqlcounter: Found Check item attribute %d", da->attr); */
534 if ((check_vp = pairfind(request->config_items, da->attr, da->vendor, TAG_ANY)) == NULL) {
535 RDEBUG2("Could not find Check item value pair");
539 len = snprintf(query, sizeof(query), "%%{%s:%s}", inst->sqlmod_inst, inst->query);
540 if (len >= sizeof(query) - 1) {
541 REDEBUG("Insufficient query buffer space");
543 return RLM_MODULE_FAIL;
548 /* first, expand %k, %b and %e in query */
549 len = sqlcounter_expand(p, p - query, inst->query, inst);
551 REDEBUG("Insufficient query buffer space");
553 return RLM_MODULE_FAIL;
558 if ((p - query) < 2) {
559 REDEBUG("Insufficient query buffer space");
561 return RLM_MODULE_FAIL;
567 /* Finally, xlat resulting SQL query */
568 if (radius_axlat(&expanded, request, query, NULL, NULL) < 0) {
569 return RLM_MODULE_FAIL;
572 if (sscanf(expanded, "%" PRIu64, &counter) != 1) {
573 RDEBUG2("No integer found in string \"%s\"", expanded);
574 return RLM_MODULE_NOOP;
577 talloc_free(expanded);
580 * Check if check item > counter
582 if (check_vp->vp_integer64 <= counter) {
583 RDEBUG2("(Check item - counter) is less than zero");
586 * User is denied access, send back a reply message
588 snprintf(msg, sizeof(msg), "Your maximum %s usage time has been reached", inst->reset);
589 pairmake_reply("Reply-Message", msg, T_OP_EQ);
591 REDEBUG("Maximum %s usage time reached", inst->reset);
593 RDEBUG2("Rejected user %s, check_item=%" PRIu64 ", counter=%" PRIu64,
594 key_vp->vp_strvalue, check_vp->vp_integer64, counter);
596 return RLM_MODULE_REJECT;
600 res = check_vp->vp_integer64 - counter;
601 RDEBUG2("Check item is greater than query result");
603 * We are assuming that simultaneous-use=1. But
604 * even if that does not happen then our user
605 * could login at max for 2*max-usage-time Is
610 * If we are near a reset then add the next
611 * limit, so that the user will not need to login
612 * again. Do this only for Session-Timeout.
614 if (((inst->reply_attr->vendor == 0) && (inst->reply_attr->attr == PW_SESSION_TIMEOUT)) &&
615 inst->reset_time && ((int) res >= (inst->reset_time - request->timestamp))) {
616 res = inst->reset_time - request->timestamp;
617 res += check_vp->vp_integer;
621 * Limit the reply attribute to the minimum of
622 * the existing value, or this new one.
624 reply_item = pairfind(request->reply->vps, inst->reply_attr->attr, inst->reply_attr->vendor, TAG_ANY);
626 if (reply_item->vp_integer64 > res) {
627 reply_item->vp_integer64 = res;
629 RDEBUG2("Leaving existing limit of %" PRIu64, reply_item->vp_integer64);
632 reply_item = radius_paircreate(request, &request->reply->vps, inst->reply_attr->attr,
633 inst->reply_attr->vendor);
634 reply_item->vp_integer64 = res;
637 RDEBUG2("Authorized user %s, check_item=%" PRIu64 ", counter=%" PRIu64 ,
638 key_vp->vp_strvalue, check_vp->vp_integer64, counter);
639 RDEBUG2("Sent Reply-Item for user %s, Type=%s, value=%" PRIu64,
640 key_vp->vp_strvalue, inst->reply_name, reply_item->vp_integer64);
642 return RLM_MODULE_OK;
646 * The module name should be the only globally exported symbol.
647 * That is, everything else should be 'static'.
649 * If the module needs to temporarily modify it's instantiation
650 * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
651 * The server will then take care of ensuring that the module
652 * is single-threaded.
654 module_t rlm_sqlcounter = {
657 RLM_TYPE_THREAD_SAFE, /* type */
658 sizeof(rlm_sqlcounter_t),
660 mod_instantiate, /* instantiation */
663 NULL, /* authentication */
664 mod_authorize, /* authorization */
665 NULL, /* preaccounting */
666 NULL, /* accounting */
667 NULL, /* checksimul */
668 NULL, /* pre-proxy */
669 NULL, /* post-proxy */