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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 * Copyright 2001 The FreeRADIUS server project
21 * Copyright 2001 Alan DeKok <aland@ox.org>
22 * Copyright 2001 Kostas Kalevras <kkalev@noc.ntua.gr>
25 /* This module is based directly on the rlm_counter module */
30 #include "libradius.h"
44 static const char rcsid[] = "$Id$";
47 * Define a structure for our module configuration.
49 * These variables do not need to be in a structure, but it's
50 * a lot cleaner to do so, and a pointer to the structure can
51 * be used as the instance handle.
53 typedef struct rlm_sqlcounter_t {
54 char *counter_name; /* Daily-Session-Time */
55 char *check_name; /* Max-Daily-Session */
56 char *key_name; /* User-Name */
57 char *sqlmod_inst; /* instance of SQL module to use, usually just 'sql' */
58 char *query; /* SQL query to retrieve current session time */
59 char *reset; /* daily, weekly, monthly, never or user defined */
62 int key_attr; /* attribute number for key field */
63 int dict_attr; /* attribute number for the counter. */
67 * A mapping of configuration file names to internal variables.
69 * Note that the string is dynamically allocated, so it MUST
70 * be freed. When the configuration file parse re-reads the string,
71 * it free's the old one, and strdup's the new one, placing the pointer
72 * to the strdup'd string into 'config.string'. This gets around
75 static CONF_PARSER module_config[] = {
76 { "counter-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,counter_name), NULL, NULL },
77 { "check-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,check_name), NULL, NULL },
78 { "key", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,key_name), NULL, NULL },
79 { "sqlmod-inst", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,sqlmod_inst), NULL, NULL },
80 { "query", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,query), NULL, NULL },
81 { "reset", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,reset), NULL, NULL },
82 { NULL, -1, 0, NULL, NULL }
86 static int find_next_reset(rlm_sqlcounter_t *data, time_t timeval)
93 tm = localtime_r(&timeval, &s_tm);
94 tm->tm_sec = tm->tm_min = 0;
96 if (data->reset == NULL)
98 if (isdigit(data->reset[0])){
101 len = strlen(data->reset);
104 last = data->reset[len - 1];
107 num = atoi(data->reset);
108 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
110 if (strcmp(data->reset, "hourly") == 0 || last == 'h') {
112 * Round up to the next nearest hour.
115 data->reset_time = mktime(tm);
116 } else if (strcmp(data->reset, "daily") == 0 || last == 'd') {
118 * Round up to the next nearest day.
122 data->reset_time = mktime(tm);
123 } else if (strcmp(data->reset, "weekly") == 0 || last == 'w') {
125 * Round up to the next nearest week.
128 tm->tm_mday += (7 - tm->tm_wday) +(7*(num-1));
129 data->reset_time = mktime(tm);
130 } else if (strcmp(data->reset, "monthly") == 0 || last == 'm') {
134 data->reset_time = mktime(tm);
135 } else if (strcmp(data->reset, "never") == 0) {
136 data->reset_time = 0;
138 radlog(L_ERR, "rlm_sqlcounter: Unknown reset timer \"%s\"",
142 DEBUG2("rlm_sqlcounter: Current Time: %d, Next reset %d",
143 (int)timeval,(int)data->reset_time);
149 /* I don't believe that this routine handles Daylight Saving Time adjustments
150 properly. Any suggestions?
153 static int find_prev_reset(rlm_sqlcounter_t *data, time_t timeval)
160 tm = localtime_r(&timeval, &s_tm);
161 tm->tm_sec = tm->tm_min = 0;
163 if (data->reset == NULL)
165 if (isdigit(data->reset[0])){
168 len = strlen(data->reset);
171 last = data->reset[len - 1];
174 num = atoi(data->reset);
175 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
177 if (strcmp(data->reset, "hourly") == 0 || last == 'h') {
179 * Round down to the prev nearest hour.
181 tm->tm_hour -= num - 1;
182 data->last_reset = mktime(tm);
183 } else if (strcmp(data->reset, "daily") == 0 || last == 'd') {
185 * Round down to the prev nearest day.
188 tm->tm_mday -= num - 1;
189 data->last_reset = mktime(tm);
190 } else if (strcmp(data->reset, "weekly") == 0 || last == 'w') {
192 * Round down to the prev nearest week.
195 tm->tm_mday -= (7 - tm->tm_wday) +(7*(num-1));
196 data->last_reset = mktime(tm);
197 } else if (strcmp(data->reset, "monthly") == 0 || last == 'm') {
200 tm->tm_mon -= num - 1;
201 data->last_reset = mktime(tm);
202 } else if (strcmp(data->reset, "never") == 0) {
203 data->reset_time = 0;
205 radlog(L_ERR, "rlm_sqlcounter: Unknown reset timer \"%s\"",
209 DEBUG2("rlm_sqlcounter: Current Time: %d, Prev reset %d",
210 (int)timeval,(int)data->last_reset);
217 * Replace %<whatever> in a string.
226 static int sqlcounter_expand(char *out, int outlen, const char *fmt, void *instance)
228 rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
232 char tmpdt[40]; /* For temporary storing of dates */
236 for (p = fmt; *p ; p++) {
237 /* Calculate freespace in output */
238 freespace = outlen - (q - out);
242 if ((c != '%') && (c != '$') && (c != '\\')) {
244 * We check if we're inside an open brace. If we are
245 * then we assume this brace is NOT literal, but is
246 * a closing brace and apply it
248 if((c == '}') && openbraces) {
255 if (*++p == '\0') break;
256 if (c == '\\') switch(*p) {
271 } else if (c == '%') switch(*p) {
275 case 'b': /* last_reset */
276 sprintf(tmpdt, "%lu", data->last_reset);
277 strNcpy(q, tmpdt, freespace);
280 case 'e': /* reset_time */
281 sprintf(tmpdt, "%lu", data->reset_time);
282 strNcpy(q, tmpdt, freespace);
285 case 'k': /* Key Name */
286 strNcpy(q, data->key_name, freespace);
289 case 'S': /* SQL module instance */
290 strNcpy(q, data->sqlmod_inst, freespace);
301 DEBUG2("sqlcounter_expand: '%s'", out);
308 * See if the counter matches.
310 static int sqlcounter_cmp(void *instance, REQUEST *req, VALUE_PAIR *request, VALUE_PAIR *check,
311 VALUE_PAIR *check_pairs, VALUE_PAIR **reply_pairs)
313 rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
315 char querystr[MAX_STRING_LEN];
316 char responsestr[MAX_STRING_LEN];
318 check_pairs = check_pairs; /* shut the compiler up */
319 reply_pairs = reply_pairs;
321 /* first, expand %k, %b and %e in query */
322 sqlcounter_expand(querystr, MAX_STRING_LEN, data->query, instance);
324 /* second, xlat any request attribs in query */
325 radius_xlat(responsestr, MAX_STRING_LEN, querystr, req, NULL);
327 /* third, wrap query with sql module call & expand */
328 sprintf(querystr, "%%{%%S:%s}", responsestr);
329 sqlcounter_expand(responsestr, MAX_STRING_LEN, querystr, instance);
331 /* Finally, xlat resulting SQL query */
332 radius_xlat(querystr, MAX_STRING_LEN, responsestr, req, NULL);
334 counter = atoi(querystr);
336 return counter - check->lvalue;
341 * Do any per-module initialization that is separate to each
342 * configured instance of the module. e.g. set up connections
343 * to external databases, read configuration files, set up
344 * dictionary entries, etc.
346 * If configuration information is given in the config section
347 * that must be referenced in later calls, store a handle to it
348 * in *instance otherwise put a null pointer there.
350 static int sqlcounter_instantiate(CONF_SECTION *conf, void **instance)
352 rlm_sqlcounter_t *data;
358 * Set up a storage area for instance data
360 data = rad_malloc(sizeof(*data));
363 * If the configuration parameters can't be parsed, then
366 if (cf_section_parse(conf, data, module_config) < 0) {
372 * Discover the attribute number of the key.
374 if (data->key_name == NULL) {
375 radlog(L_ERR, "rlm_sqlcounter: 'key' must be set.");
378 dattr = dict_attrbyname(data->key_name);
380 radlog(L_ERR, "rlm_sqlcounter: No such attribute %s",
384 data->key_attr = dattr->attr;
388 * Create a new attribute for the counter.
390 if (data->counter_name == NULL) {
391 radlog(L_ERR, "rlm_sqlcounter: 'counter-name' must be set.");
395 memset(&flags, 0, sizeof(flags));
396 dict_addattr(data->counter_name, 0, PW_TYPE_INTEGER, -1, flags);
397 dattr = dict_attrbyname(data->counter_name);
399 radlog(L_ERR, "rlm_sqlcounter: Failed to create counter attribute %s",
403 data->dict_attr = dattr->attr;
404 DEBUG2("rlm_sqlcounter: Counter attribute %s is number %d",
405 data->counter_name, data->dict_attr);
408 * Create a new attribute for the check item.
410 if (data->check_name == NULL) {
411 radlog(L_ERR, "rlm_sqlcounter: 'check-name' must be set.");
414 dict_addattr(data->check_name, 0, PW_TYPE_INTEGER, -1, flags);
415 dattr = dict_attrbyname(data->check_name);
417 radlog(L_ERR, "rlm_sqlcounter: Failed to create check attribute %s",
421 DEBUG2("rlm_sqlcounter: Check attribute %s is number %d",
422 data->check_name, dattr->attr);
425 * Discover the end of the current time period.
427 if (data->reset == NULL) {
428 radlog(L_ERR, "rlm_sqlcounter: 'reset' must be set.");
432 data->reset_time = 0;
434 if (find_next_reset(data,now) == -1)
438 * Discover the beginning of the current time period.
440 data->last_reset = 0;
442 if (find_prev_reset(data,now) == -1)
447 * Register the counter comparison operation.
449 paircompare_register(data->dict_attr, 0, sqlcounter_cmp, data);
457 * Find the named user in this modules database. Create the set
458 * of attribute-value pairs to check and reply with for this user
459 * from the database. The authentication code only needs to check
460 * the password, the rest is done here.
462 static int sqlcounter_authorize(void *instance, REQUEST *request)
464 rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
465 int ret=RLM_MODULE_NOOP;
469 VALUE_PAIR *key_vp, *check_vp;
470 VALUE_PAIR *reply_item;
472 char querystr[MAX_STRING_LEN];
473 char responsestr[MAX_STRING_LEN];
475 /* quiet the compiler */
480 * Before doing anything else, see if we have to reset
483 if (data->reset_time && (data->reset_time <= request->timestamp)) {
486 * Re-set the next time and prev_time for this counters range
488 data->last_reset = data->reset_time;
489 find_next_reset(data,request->timestamp);
494 * Look for the key. User-Name is special. It means
495 * The REAL username, after stripping.
497 DEBUG2("rlm_sqlcounter: Entering module authorize code");
498 key_vp = (data->key_attr == PW_USER_NAME) ? request->username : pairfind(request->packet->vps, data->key_attr);
499 if (key_vp == NULL) {
500 DEBUG2("rlm_sqlcounter: Could not find Key value pair");
505 * Look for the check item
507 if ((dattr = dict_attrbyname(data->check_name)) == NULL) {
510 /* DEBUG2("rlm_sqlcounter: Found Check item attribute %d", dattr->attr); */
511 if ((check_vp= pairfind(request->config_items, dattr->attr)) == NULL) {
512 DEBUG2("rlm_sqlcounter: Could not find Check item value pair");
516 /* first, expand %k, %b and %e in query */
517 sqlcounter_expand(querystr, MAX_STRING_LEN, data->query, instance);
519 /* second, xlat any request attribs in query */
520 radius_xlat(responsestr, MAX_STRING_LEN, querystr, request, NULL);
522 /* third, wrap query with sql module & expand */
523 sprintf(querystr, "%%{%%S:%s}", responsestr);
524 sqlcounter_expand(responsestr, MAX_STRING_LEN, querystr, instance);
526 /* Finally, xlat resulting SQL query */
527 radius_xlat(querystr, MAX_STRING_LEN, responsestr, request, NULL);
529 counter = atoi(querystr);
533 * Check if check item > counter
535 res=check_vp->lvalue - counter;
537 DEBUG2("rlm_sqlcounter: (Check item - counter) is greater than zero");
539 * We are assuming that simultaneous-use=1. But
540 * even if that does not happen then our user
541 * could login at max for 2*max-usage-time Is
546 * User is allowed, but set Session-Timeout.
547 * Stolen from main/auth.c
551 * If we are near a reset then add the next
552 * limit, so that the user will not need to
555 if (data->reset_time && (
556 res >= (data->reset_time - request->timestamp))) {
557 res += check_vp->lvalue;
560 if ((reply_item = pairfind(request->reply->vps, PW_SESSION_TIMEOUT)) != NULL) {
561 if (reply_item->lvalue > res)
562 reply_item->lvalue = res;
564 if ((reply_item = paircreate(PW_SESSION_TIMEOUT, PW_TYPE_INTEGER)) == NULL) {
565 radlog(L_ERR|L_CONS, "no memory");
566 return RLM_MODULE_NOOP;
568 reply_item->lvalue = res;
569 pairadd(&request->reply->vps, reply_item);
574 DEBUG2("rlm_sqlcounter: Authorized user %s, check_item=%d, counter=%d",
575 key_vp->strvalue,check_vp->lvalue,counter);
576 DEBUG2("rlm_sqlcounter: Sent Reply-Item for user %s, Type=Session-Timeout, value=%d",
577 key_vp->strvalue,reply_item->lvalue);
580 char module_fmsg[MAX_STRING_LEN];
581 VALUE_PAIR *module_fmsg_vp;
583 DEBUG2("rlm_sqlcounter: (Check item - counter) is less than zero");
586 * User is denied access, send back a reply message
588 sprintf(msg, "Your maximum %s usage time has been reached", data->reset);
589 reply_item=pairmake("Reply-Message", msg, T_OP_EQ);
590 pairadd(&request->reply->vps, reply_item);
592 snprintf(module_fmsg, sizeof(module_fmsg), "rlm_sqlcounter: Maximum %s usage time reached", data->reset);
593 module_fmsg_vp = pairmake("Module-Failure-Message", module_fmsg, T_OP_EQ);
594 pairadd(&request->packet->vps, module_fmsg_vp);
596 ret=RLM_MODULE_REJECT;
598 DEBUG2("rlm_sqlcounter: Rejected user %s, check_item=%d, counter=%d",
599 key_vp->strvalue,check_vp->lvalue,counter);
605 static int sqlcounter_detach(void *instance)
607 rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
609 paircompare_unregister(data->dict_attr, sqlcounter_cmp);
612 free(data->check_name);
613 free(data->sqlmod_inst);
614 free(data->counter_name);
621 * The module name should be the only globally exported symbol.
622 * That is, everything else should be 'static'.
624 * If the module needs to temporarily modify it's instantiation
625 * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
626 * The server will then take care of ensuring that the module
627 * is single-threaded.
629 module_t rlm_sqlcounter = {
631 RLM_TYPE_THREAD_SAFE, /* type */
632 NULL, /* initialization */
633 sqlcounter_instantiate, /* instantiation */
635 NULL, /* authentication */
636 sqlcounter_authorize, /* authorization */
637 NULL, /* preaccounting */
638 NULL, /* accounting */
639 NULL /* checksimul */
641 sqlcounter_detach, /* detach */