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 as published by
4 * the Free Software Foundation; either version 2 of the License, or (at
5 * your option) any later version.
7 * This program is distributed in the hope that it will be useful,
8 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 * GNU General Public License for more details.
12 * You should have received a copy of the GNU General Public License
13 * along with this program; if not, write to the Free Software
14 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
19 * @file rlm_sqlcounter.c
20 * @brief Tracks data usage and other counters using SQL.
22 * @copyright 2001,2006 The FreeRADIUS server project
23 * @copyright 2001 Alan DeKok <aland@ox.org>
27 #include <freeradius-devel/radiusd.h>
28 #include <freeradius-devel/modules.h>
29 #include <freeradius-devel/rad_assert.h>
33 #define MAX_QUERY_LEN 2048
36 * Note: When your counter spans more than 1 period (ie 3 months
37 * or 2 weeks), this module probably does NOT do what you want! It
38 * calculates the range of dates to count across by first calculating
39 * the End of the Current period and then subtracting the number of
40 * periods you specify from that to determine the beginning of the
43 * For example, if you specify a 3 month counter and today is June 15th,
44 * the end of the current period is June 30. Subtracting 3 months from
45 * that gives April 1st. So, the counter will sum radacct entries from
46 * April 1st to June 30. Then, next month, it will sum entries from
47 * May 1st to July 31st.
49 * To fix this behavior, we need to add some way of storing the Next
54 * Define a structure for our module configuration.
56 * These variables do not need to be in a structure, but it's
57 * a lot cleaner to do so, and a pointer to the structure can
58 * be used as the instance handle.
60 typedef struct rlm_sqlcounter_t {
61 char const *counter_name; //!< Daily-Session-Time.
62 char const *limit_name; //!< Max-Daily-Session.
63 char const *reply_name; //!< Session-Timeout.
64 char const *key_name; //!< User-Name.
65 char const *sqlmod_inst; //!< Instance of SQL module to use,
66 //!< usually just 'sql'.
67 char const *query; //!< SQL query to retrieve current
69 char const *reset; //!< Daily, weekly, monthly,
70 //!< never or user defined.
73 DICT_ATTR const *key_attr; //!< Attribute number for key field.
74 DICT_ATTR const *dict_attr; //!< Attribute number for the counter.
75 DICT_ATTR const *reply_attr; //!< Attribute number for the reply.
79 * A mapping of configuration file names to internal variables.
81 * Note that the string is dynamically allocated, so it MUST
82 * be freed. When the configuration file parse re-reads the string,
83 * it free's the old one, and strdup's the new one, placing the pointer
84 * to the strdup'd string into 'config.string'. This gets around
87 static const CONF_PARSER module_config[] = {
88 { "sql-module-instance", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_DEPRECATED, rlm_sqlcounter_t, sqlmod_inst), NULL },
89 { "sql_module_instance", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_sqlcounter_t, sqlmod_inst), NULL },
91 { "key", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_ATTRIBUTE, rlm_sqlcounter_t, key_name), NULL },
92 { "query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT | PW_TYPE_REQUIRED, rlm_sqlcounter_t, query), NULL },
93 { "reset", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_sqlcounter_t, reset), NULL },
95 { "counter-name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_DEPRECATED, rlm_sqlcounter_t, counter_name), NULL },
96 { "counter_name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_sqlcounter_t, counter_name), NULL },
98 { "check-name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_DEPRECATED, rlm_sqlcounter_t, limit_name), NULL },
99 { "check_name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_sqlcounter_t, limit_name), NULL },
101 { "reply-name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_DEPRECATED, rlm_sqlcounter_t, reply_name), NULL },
102 { "reply_name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_ATTRIBUTE, rlm_sqlcounter_t, reply_name), "Session-Timeout" },
103 CONF_PARSER_TERMINATOR
106 static int find_next_reset(rlm_sqlcounter_t *inst, REQUEST *request, time_t timeval)
110 unsigned int num = 1;
113 char sCurrentTime[40], sNextTime[40];
115 tm = localtime_r(&timeval, &s_tm);
116 tm->tm_sec = tm->tm_min = 0;
118 rad_assert(inst->reset != NULL);
121 * Reset every N hours, days, weeks, months.
123 if (isdigit((int) inst->reset[0])){
124 len = strlen(inst->reset);
125 if (len == 0) return -1;
127 last = inst->reset[len - 1];
128 if (!isalpha((int) last)) {
132 num = atoi(inst->reset);
133 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
136 if (strcmp(inst->reset, "hourly") == 0 || last == 'h') {
138 * Round up to the next nearest hour.
141 inst->reset_time = mktime(tm);
143 } else if (strcmp(inst->reset, "daily") == 0 || last == 'd') {
145 * Round up to the next nearest day.
149 inst->reset_time = mktime(tm);
151 } else if (strcmp(inst->reset, "weekly") == 0 || last == 'w') {
153 * Round up to the next nearest week.
156 tm->tm_mday += (7 - tm->tm_wday) +(7*(num-1));
157 inst->reset_time = mktime(tm);
159 } else if (strcmp(inst->reset, "monthly") == 0 || last == 'm') {
163 inst->reset_time = mktime(tm);
165 } else if (strcmp(inst->reset, "never") == 0) {
166 inst->reset_time = 0;
172 if (!request || (rad_debug_lvl < 2)) return ret;
174 len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
175 if (len == 0) *sCurrentTime = '\0';
177 len = strftime(sNextTime, sizeof(sNextTime),"%Y-%m-%d %H:%M:%S",tm);
178 if (len == 0) *sNextTime = '\0';
179 RDEBUG2("rlm_sqlcounter: Current Time: %" PRId64 " [%s], Next reset %" PRId64 " [%s]",
180 (int64_t) timeval, sCurrentTime, (int64_t) inst->reset_time, sNextTime);
186 /* I don't believe that this routine handles Daylight Saving Time adjustments
187 properly. Any suggestions?
190 static int find_prev_reset(rlm_sqlcounter_t *inst, time_t timeval)
194 unsigned int num = 1;
197 char sCurrentTime[40], sPrevTime[40];
199 tm = localtime_r(&timeval, &s_tm);
200 len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
201 if (len == 0) *sCurrentTime = '\0';
202 tm->tm_sec = tm->tm_min = 0;
204 rad_assert(inst->reset != NULL);
206 if (isdigit((int) inst->reset[0])){
207 len = strlen(inst->reset);
210 last = inst->reset[len - 1];
211 if (!isalpha((int) last))
213 num = atoi(inst->reset);
214 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
216 if (strcmp(inst->reset, "hourly") == 0 || last == 'h') {
218 * Round down to the prev nearest hour.
220 tm->tm_hour -= num - 1;
221 inst->last_reset = mktime(tm);
223 } else if (strcmp(inst->reset, "daily") == 0 || last == 'd') {
225 * Round down to the prev nearest day.
228 tm->tm_mday -= num - 1;
229 inst->last_reset = mktime(tm);
231 } else if (strcmp(inst->reset, "weekly") == 0 || last == 'w') {
233 * Round down to the prev nearest week.
236 tm->tm_mday -= tm->tm_wday +(7*(num-1));
237 inst->last_reset = mktime(tm);
239 } else if (strcmp(inst->reset, "monthly") == 0 || last == 'm') {
242 tm->tm_mon -= num - 1;
243 inst->last_reset = mktime(tm);
245 } else if (strcmp(inst->reset, "never") == 0) {
246 inst->reset_time = 0;
251 len = strftime(sPrevTime, sizeof(sPrevTime), "%Y-%m-%d %H:%M:%S", tm);
252 if (len == 0) *sPrevTime = '\0';
253 DEBUG2("rlm_sqlcounter: Current Time: %" PRId64 " [%s], Prev reset %" PRId64 " [%s]",
254 (int64_t) timeval, sCurrentTime, (int64_t) inst->last_reset, sPrevTime);
261 * Replace %<whatever> in a string.
270 static size_t sqlcounter_expand(char *out, int outlen, char const *fmt, rlm_sqlcounter_t *inst)
275 char tmpdt[40]; /* For temporary storing of dates */
280 /* Calculate freespace in output */
281 freespace = outlen - (q - out);
282 if (freespace <= 1) {
287 * Non-% get copied as-is.
294 if (!*p) { /* % and then EOS --> % */
299 if (freespace <= 2) return -1;
302 * We need TWO %% in a row before we do our expansions.
303 * If we only get one, just copy the %s as-is.
317 if (freespace <= 3) return -1;
320 case 'b': /* last_reset */
321 snprintf(tmpdt, sizeof(tmpdt), "%" PRId64, (int64_t) inst->last_reset);
322 strlcpy(q, tmpdt, freespace);
326 case 'e': /* reset_time */
327 snprintf(tmpdt, sizeof(tmpdt), "%" PRId64, (int64_t) inst->reset_time);
328 strlcpy(q, tmpdt, freespace);
332 case 'k': /* Key Name */
333 WARN("Please replace '%%k' with '${key}'");
334 strlcpy(q, inst->key_name, freespace);
340 * %%s gets copied over as-is.
351 DEBUG2("sqlcounter_expand: '%s'", out);
358 * See if the counter matches.
360 static int sqlcounter_cmp(void *instance, REQUEST *request, UNUSED VALUE_PAIR *req , VALUE_PAIR *check,
361 UNUSED VALUE_PAIR *check_pairs, UNUSED VALUE_PAIR **reply_pairs)
363 rlm_sqlcounter_t *inst = instance;
366 char query[MAX_QUERY_LEN], subst[MAX_QUERY_LEN];
367 char *expanded = NULL;
370 /* First, expand %k, %b and %e in query */
371 if (sqlcounter_expand(subst, sizeof(subst), inst->query, inst) <= 0) {
372 REDEBUG("Insufficient query buffer space");
374 return RLM_MODULE_FAIL;
377 /* Then combine that with the name of the module were using to do the query */
378 len = snprintf(query, sizeof(query), "%%{%s:%s}", inst->sqlmod_inst, subst);
379 if (len >= sizeof(query) - 1) {
380 REDEBUG("Insufficient query buffer space");
382 return RLM_MODULE_FAIL;
385 /* Finally, xlat resulting SQL query */
386 if (radius_axlat(&expanded, request, query, NULL, NULL) < 0) {
387 return RLM_MODULE_FAIL;
390 if (sscanf(expanded, "%" PRIu64, &counter) != 1) {
391 RDEBUG2("No integer found in string \"%s\"", expanded);
393 talloc_free(expanded);
395 if (counter < check->vp_integer64) {
398 if (counter > check->vp_integer64) {
404 static int mod_bootstrap(CONF_SECTION *conf, void *instance)
406 rlm_sqlcounter_t *inst = instance;
410 memset(&flags, 0, sizeof(flags));
411 flags.compare = 1; /* ugly hack */
412 da = dict_attrbyname(inst->counter_name);
413 if (da && (da->type != PW_TYPE_INTEGER64)) {
414 cf_log_err_cs(conf, "Counter attribute %s MUST be integer64", inst->counter_name);
418 if (!da && (dict_addattr(inst->counter_name, -1, 0, PW_TYPE_INTEGER64, flags) < 0)) {
419 cf_log_err_cs(conf, "Failed to create counter attribute %s: %s", inst->counter_name, fr_strerror());
424 * Register the counter comparison operation.
426 if (paircompare_register_byname(inst->counter_name, NULL, true, sqlcounter_cmp, inst) < 0) {
427 cf_log_err_cs(conf, "Failed registering counter attribute %s: %s", inst->counter_name, fr_strerror());
431 inst->dict_attr = dict_attrbyname(inst->counter_name);
432 if (!inst->dict_attr) {
433 cf_log_err_cs(conf, "Failed to find counter attribute %s", inst->counter_name);
438 * Create a new attribute for the check item.
441 if ((dict_addattr(inst->limit_name, -1, 0, PW_TYPE_INTEGER64, flags) < 0) ||
442 !dict_attrbyname(inst->limit_name)) {
443 cf_log_err_cs(conf, "Failed to create check attribute %s: %s", inst->limit_name, fr_strerror());
451 * Do any per-module initialization that is separate to each
452 * configured instance of the module. e.g. set up connections
453 * to external databases, read configuration files, set up
454 * dictionary entries, etc.
456 * If configuration information is given in the config section
457 * that must be referenced in later calls, store a handle to it
458 * in *instance otherwise put a null pointer there.
460 static int mod_instantiate(CONF_SECTION *conf, void *instance)
462 rlm_sqlcounter_t *inst = instance;
466 rad_assert(inst->query && *inst->query);
468 da = dict_attrbyname(inst->key_name);
470 cf_log_err_cs(conf, "Invalid attribute '%s'", inst->key_name);
475 da = dict_attrbyname(inst->reply_name);
477 cf_log_err_cs(conf, "Invalid attribute '%s'", inst->reply_name);
480 inst->reply_attr = da;
483 inst->reset_time = 0;
485 if (find_next_reset(inst, NULL, now) < 0) {
486 cf_log_err_cs(conf, "Invalid reset '%s'", inst->reset);
491 * Discover the beginning of the current time period.
493 inst->last_reset = 0;
495 if (find_prev_reset(inst, now) < 0) {
496 cf_log_err_cs(conf, "Invalid reset '%s'", inst->reset);
504 * Find the named user in this modules database. Create the set
505 * of attribute-value pairs to check and reply with for this user
506 * from the database. The authentication code only needs to check
507 * the password, the rest is done here.
509 static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, REQUEST *request)
511 rlm_sqlcounter_t *inst = instance;
512 int rcode = RLM_MODULE_NOOP;
513 uint64_t counter, res;
515 VALUE_PAIR *key_vp, *limit;
516 VALUE_PAIR *reply_item;
519 char query[MAX_QUERY_LEN], subst[MAX_QUERY_LEN];
520 char *expanded = NULL;
525 * Before doing anything else, see if we have to reset
528 if (inst->reset_time && (inst->reset_time <= request->timestamp)) {
530 * Re-set the next time and prev_time for this counters range
532 inst->last_reset = inst->reset_time;
533 find_next_reset(inst, request, request->timestamp);
537 * Look for the key. User-Name is special. It means
538 * The REAL username, after stripping.
540 if ((inst->key_attr->vendor == 0) && (inst->key_attr->attr == PW_USER_NAME)) {
541 key_vp = request->username;
543 key_vp = fr_pair_find_by_da(request->packet->vps, inst->key_attr, TAG_ANY);
546 RWDEBUG2("Couldn't find key attribute, request:%s, doing nothing...", inst->key_attr->name);
551 * Look for the check item
553 if ((da = dict_attrbyname(inst->limit_name)) == NULL) {
557 limit = fr_pair_find_by_da(request->config, da, TAG_ANY);
559 /* Yes this really is 'check' as distinct from control */
560 RWDEBUG2("Couldn't find check attribute, control:%s, doing nothing...", inst->limit_name);
564 /* First, expand %k, %b and %e in query */
565 if (sqlcounter_expand(subst, sizeof(subst), inst->query, inst) <= 0) {
566 REDEBUG("Insufficient query buffer space");
568 return RLM_MODULE_FAIL;
571 /* Then combine that with the name of the module were using to do the query */
572 len = snprintf(query, sizeof(query), "%%{%s:%s}", inst->sqlmod_inst, subst);
573 if (len >= (sizeof(query) - 1)) {
574 REDEBUG("Insufficient query buffer space");
576 return RLM_MODULE_FAIL;
579 /* Finally, xlat resulting SQL query */
580 if (radius_axlat(&expanded, request, query, NULL, NULL) < 0) {
581 return RLM_MODULE_FAIL;
583 talloc_free(expanded);
585 if (sscanf(expanded, "%" PRIu64, &counter) != 1) {
586 RDEBUG2("No integer found in result string \"%s\". May be first session, setting counter to 0",
592 * Check if check item > counter
594 if (limit->vp_integer64 <= counter) {
595 /* User is denied access, send back a reply message */
596 snprintf(msg, sizeof(msg), "Your maximum %s usage time has been reached", inst->reset);
597 pair_make_reply("Reply-Message", msg, T_OP_EQ);
599 REDEBUG2("Maximum %s usage time reached", inst->reset);
600 REDEBUG2("Rejecting user, &control:%s value (%" PRIu64 ") is less than counter value (%" PRIu64 ")",
601 inst->limit_name, limit->vp_integer64, counter);
603 return RLM_MODULE_REJECT;
606 res = limit->vp_integer64 - counter;
607 RDEBUG2("Allowing user, &control:%s value (%" PRIu64 ") is greater than counter value (%" PRIu64 ")",
608 inst->limit_name, limit->vp_integer64, counter);
610 * We are assuming that simultaneous-use=1. But
611 * even if that does not happen then our user
612 * could login at max for 2*max-usage-time Is
617 * If we are near a reset then add the next
618 * limit, so that the user will not need to login
619 * again. Do this only for Session-Timeout.
621 if (((inst->reply_attr->vendor == 0) && (inst->reply_attr->attr == PW_SESSION_TIMEOUT)) &&
622 inst->reset_time && (res >= (uint64_t)(inst->reset_time - request->timestamp))) {
623 uint64_t to_reset = inst->reset_time - request->timestamp;
625 RDEBUG2("Time remaining (%" PRIu64 "s) is greater than time to reset (%" PRIu64 "s). "
626 "Adding %" PRIu64 "s to reply value", to_reset, res, to_reset);
627 res = to_reset + limit->vp_integer;
631 * Limit the reply attribute to the minimum of the existing value, or this new one.
633 reply_item = fr_pair_find_by_da(request->reply->vps, inst->reply_attr, TAG_ANY);
635 if (reply_item->vp_integer64 <= res) {
636 RDEBUG2("Leaving existing &reply:%s value of %" PRIu64, inst->reply_attr->name,
637 reply_item->vp_integer64);
639 return RLM_MODULE_OK;
642 reply_item = radius_pair_create(request->reply, &request->reply->vps, inst->reply_attr->attr,
643 inst->reply_attr->vendor);
645 reply_item->vp_integer64 = res;
647 RDEBUG2("Setting &reply:%s value to %" PRIu64, inst->reply_name, reply_item->vp_integer64);
649 return RLM_MODULE_OK;
653 * The module name should be the only globally exported symbol.
654 * That is, everything else should be 'static'.
656 * If the module needs to temporarily modify it's instantiation
657 * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
658 * The server will then take care of ensuring that the module
659 * is single-threaded.
661 extern module_t rlm_sqlcounter;
662 module_t rlm_sqlcounter = {
663 .magic = RLM_MODULE_INIT,
664 .name = "sqlcounter",
665 .type = RLM_TYPE_THREAD_SAFE,
666 .inst_size = sizeof(rlm_sqlcounter_t),
667 .config = module_config,
668 .bootstrap = mod_bootstrap,
669 .instantiate = mod_instantiate,
671 [MOD_AUTHORIZE] = mod_authorize