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>
27 #include "libradius.h"
41 #ifdef NEEDS_GDBM_SYNC
42 # define GDBM_SYNCOPT GDBM_SYNC
44 # define GDBM_SYNCOPT 0
48 static const char rcsid[] = "$Id$";
51 * Define a structure for our module configuration.
53 * These variables do not need to be in a structure, but it's
54 * a lot cleaner to do so, and a pointer to the structure can
55 * be used as the instance handle.
57 typedef struct rlm_counter_t {
58 char *filename; /* name of the database file */
59 char *reset; /* daily, weekly, monthly, never or user defined */
60 char *key_name; /* User-Name */
61 char *count_attribute; /* Acct-Session-Time */
62 char *counter_name; /* Daily-Session-Time */
63 char *check_name; /* Daily-Max-Session */
64 char *service_type; /* Service-Type to search for */
71 int dict_attr; /* attribute number for the counter. */
77 * A mapping of configuration file names to internal variables.
79 * Note that the string is dynamically allocated, so it MUST
80 * be freed. When the configuration file parse re-reads the string,
81 * it free's the old one, and strdup's the new one, placing the pointer
82 * to the strdup'd string into 'config.string'. This gets around
85 static CONF_PARSER module_config[] = {
86 { "filename", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,filename), NULL, NULL },
87 { "key", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,key_name), NULL, NULL },
88 { "reset", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,reset), NULL, NULL },
89 { "count-attribute", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,count_attribute), NULL, NULL },
90 { "counter-name", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,counter_name), NULL, NULL },
91 { "check-name", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,check_name), NULL, NULL },
92 { "allowed-servicetype", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,service_type),NULL, NULL },
93 { "cache-size", PW_TYPE_INTEGER, offsetof(rlm_counter_t,cache_size), NULL, "1000" },
94 { NULL, -1, 0, NULL, NULL }
99 * See if the counter matches.
101 static int counter_cmp(void *instance, REQUEST *req, VALUE_PAIR *request, VALUE_PAIR *check,
102 VALUE_PAIR *check_pairs, VALUE_PAIR **reply_pairs)
104 rlm_counter_t *data = (rlm_counter_t *) instance;
110 check_pairs = check_pairs; /* shut the compiler up */
111 reply_pairs = reply_pairs;
114 * Find the key attribute.
116 key_vp = pairfind(request, data->key_attr);
117 if (key_vp == NULL) {
118 return RLM_MODULE_NOOP;
121 key_datum.dptr = key_vp->strvalue;
122 key_datum.dsize = key_vp->length;
124 rad_lockfd(data->fd, sizeof(int));
125 count_datum = gdbm_fetch(data->gdbm, key_datum);
126 rad_unlockfd(data->fd, sizeof(int));
128 if (count_datum.dptr == NULL) {
131 memcpy(&counter, count_datum.dptr, sizeof(int));
132 free(count_datum.dptr);
134 return counter - check->lvalue;
138 static int find_next_reset(rlm_counter_t *data, time_t timeval)
145 tm = localtime_r(&timeval, &s_tm);
146 tm->tm_sec = tm->tm_min = 0;
148 if (data->reset == NULL)
150 if (isdigit(data->reset[0])){
153 len = strlen(data->reset);
156 last = data->reset[len - 1];
159 num = atoi(data->reset);
160 DEBUG("rlm_counter: num=%d, last=%c",num,last);
162 if (strcmp(data->reset, "hourly") == 0 || last == 'h') {
164 * Round up to the next nearest hour.
167 data->reset_time = mktime(tm);
168 } else if (strcmp(data->reset, "daily") == 0 || last == 'd') {
170 * Round up to the next nearest day.
174 data->reset_time = mktime(tm);
175 } else if (strcmp(data->reset, "weekly") == 0 || last == 'w') {
177 * Round up to the next nearest week.
180 tm->tm_mday += (7 - tm->tm_wday) +(7*(num-1));
181 data->reset_time = mktime(tm);
182 } else if (strcmp(data->reset, "monthly") == 0 || last == 'm') {
186 data->reset_time = mktime(tm);
187 } else if (strcmp(data->reset, "never") == 0) {
188 data->reset_time = 0;
190 radlog(L_ERR, "rlm_counter: Unknown reset timer \"%s\"",
194 DEBUG2("rlm_counter: Current Time: %d, Next reset %d",
195 (int)timeval,(int)data->reset_time);
202 * Do any per-module initialization that is separate to each
203 * configured instance of the module. e.g. set up connections
204 * to external databases, read configuration files, set up
205 * dictionary entries, etc.
207 * If configuration information is given in the config section
208 * that must be referenced in later calls, store a handle to it
209 * in *instance otherwise put a null pointer there.
211 static int counter_instantiate(CONF_SECTION *conf, void **instance)
221 * Set up a storage area for instance data
223 data = rad_malloc(sizeof(*data));
226 * If the configuration parameters can't be parsed, then
229 if (cf_section_parse(conf, data, module_config) < 0) {
233 cache_size = data->cache_size;
236 * Discover the attribute number of the key.
238 if (data->key_name == NULL) {
239 radlog(L_ERR, "rlm_counter: 'key' must be set.");
242 dattr = dict_attrbyname(data->key_name);
244 radlog(L_ERR, "rlm_counter: No such attribute %s",
248 data->key_attr = dattr->attr;
251 * Discover the attribute number of the counter.
253 if (data->count_attribute == NULL) {
254 radlog(L_ERR, "rlm_counter: 'count-attribute' must be set.");
257 dattr = dict_attrbyname(data->count_attribute);
259 radlog(L_ERR, "rlm_counter: No such attribute %s",
260 data->count_attribute);
263 data->count_attr = dattr->attr;
266 * Create a new attribute for the counter.
268 if (data->counter_name == NULL) {
269 radlog(L_ERR, "rlm_counter: 'counter-name' must be set.");
273 memset(&flags, 0, sizeof(flags));
274 dict_addattr(data->counter_name, 0, PW_TYPE_INTEGER, -1, flags);
275 dattr = dict_attrbyname(data->counter_name);
277 radlog(L_ERR, "rlm_counter: Failed to create counter attribute %s",
281 data->dict_attr = dattr->attr;
282 DEBUG2("rlm_counter: Counter attribute %s is number %d",
283 data->counter_name, data->dict_attr);
286 * Create a new attribute for the check item.
288 if (data->check_name == NULL) {
289 radlog(L_ERR, "rlm_counter: 'check-name' must be set.");
292 dict_addattr(data->check_name, 0, PW_TYPE_INTEGER, -1, flags);
293 dattr = dict_attrbyname(data->check_name);
295 radlog(L_ERR, "rlm_counter: Failed to create check attribute %s",
301 * Find the attribute for the allowed protocol
303 if (data->service_type != NULL) {
304 if ((dval = dict_valbyname(PW_SERVICE_TYPE, data->service_type)) == NULL) {
305 radlog(L_ERR, "rlm_counter: Failed to find attribute number for %s",
309 data->service_val = dval->value;
313 * Discover when next to reset the database.
315 if (data->reset == NULL) {
316 radlog(L_ERR, "rlm_counter: 'reset' must be set.");
320 data->reset_time = 0;
322 if (find_next_reset(data,now) == -1)
325 if (data->filename == NULL) {
326 radlog(L_ERR, "rlm_counter: 'filename' must be set.");
329 data->gdbm = gdbm_open(data->filename, sizeof(int),
330 GDBM_WRCREAT | GDBM_SYNCOPT | GDBM_NOLOCK, 0600, NULL);
331 if (data->gdbm == NULL) {
332 radlog(L_ERR, "rlm_counter: Failed to open file %s: %s",
333 data->filename, strerror(errno));
336 data->fd = gdbm_fdesc(data->gdbm);
338 if (gdbm_setopt(data->gdbm, GDBM_CACHESIZE, &cache_size, sizeof(int)) == -1)
339 radlog(L_ERR, "rlm_counter: Failed to set cache size");
343 * Register the counter comparison operation.
345 paircompare_register(data->dict_attr, 0, counter_cmp, data);
353 * Write accounting information to this modules database.
355 static int counter_accounting(void *instance, REQUEST *request)
357 rlm_counter_t *data = (rlm_counter_t *)instance;
360 VALUE_PAIR *key_vp, *count_vp, *proto_vp;
366 * Before doing anything else, see if we have to reset
369 if (data->reset_time && (data->reset_time <= request->timestamp)) {
370 int cache_size = data->cache_size;
372 gdbm_close(data->gdbm);
375 * Re-set the next time to clean the database.
377 data->last_reset = data->reset_time;
378 find_next_reset(data,request->timestamp);
381 * Open a completely new database.
383 data->gdbm = gdbm_open(data->filename, sizeof(int),
384 GDBM_NEWDB | GDBM_SYNCOPT | GDBM_NOLOCK, 0600, NULL);
385 if (data->gdbm == NULL) {
386 radlog(L_ERR, "rlm_counter: Failed to open file %s: %s",
387 data->filename, strerror(errno));
388 return RLM_MODULE_FAIL;
390 data->fd = gdbm_fdesc(data->gdbm);
391 if (gdbm_setopt(data->gdbm, GDBM_CACHESIZE, &cache_size, sizeof(int)) == -1)
392 radlog(L_ERR, "rlm_counter: Failed to set cache size");
395 * Check if we need to watch out for a specific service-type. If yes then check it
397 if (data->service_type != NULL) {
398 if ((proto_vp = pairfind(request->packet->vps, PW_SERVICE_TYPE)) == NULL)
399 return RLM_MODULE_NOOP;
400 if (proto_vp->lvalue != data->service_val)
401 return RLM_MODULE_NOOP;
407 * Look for the key. User-Name is special. It means
408 * The REAL username, after stripping.
410 key_vp = (data->key_attr == PW_USER_NAME) ? request->username : pairfind(request->packet->vps, data->key_attr);
412 return RLM_MODULE_NOOP;
415 * Look for the attribute to use as a counter.
417 count_vp = pairfind(request->packet->vps, data->count_attr);
418 if (count_vp == NULL)
419 return RLM_MODULE_NOOP;
421 key_datum.dptr = key_vp->strvalue;
422 key_datum.dsize = key_vp->length;
424 rad_lockfd(data->fd, sizeof(int));
425 count_datum = gdbm_fetch(data->gdbm, key_datum);
426 if (count_datum.dptr == NULL)
429 memcpy(&counter, count_datum.dptr, sizeof(int));
430 free(count_datum.dptr);
433 if (count_vp->type == PW_TYPE_DATE) {
435 * If session time < diff then the user got in after the
436 * last reset. So add his session time, otherwise add the
439 * That way if he logged in at 23:00 and we reset the
440 * daily counter at 24:00 and he logged out at 01:00
441 * then we will only count one hour (the one in the new
442 * day). That is the right thing
444 diff = request->timestamp - data->last_reset;
445 counter += (count_vp->lvalue < diff) ? count_vp->lvalue : diff;
447 } else if (count_vp->type == PW_TYPE_INTEGER) {
449 * Integers get counted, without worrying about
452 counter += count_vp->lvalue;
456 * The attribute is NOT an integer, just count once
457 * more that we've seen it.
461 count_datum.dptr = (char *) &counter;
462 count_datum.dsize = sizeof(int);
464 rcode = gdbm_store(data->gdbm, key_datum, count_datum, GDBM_REPLACE);
465 rad_unlockfd(data->fd, sizeof(int));
467 radlog(L_ERR, "rlm_counter: Failed storing data to %s: %s",
468 data->filename, gdbm_strerror(gdbm_errno));
469 return RLM_MODULE_FAIL;
472 return RLM_MODULE_OK;
476 * Find the named user in this modules database. Create the set
477 * of attribute-value pairs to check and reply with for this user
478 * from the database. The authentication code only needs to check
479 * the password, the rest is done here.
481 static int counter_authorize(void *instance, REQUEST *request)
483 rlm_counter_t *data = (rlm_counter_t *) instance;
484 int ret=RLM_MODULE_NOOP;
490 VALUE_PAIR *key_vp, *check_vp;
491 VALUE_PAIR *reply_item;
494 /* quiet the compiler */
499 * Before doing anything else, see if we have to reset
502 if (data->reset_time && (data->reset_time <= request->timestamp)) {
503 int cache_size = data->cache_size;
505 gdbm_close(data->gdbm);
508 * Re-set the next time to clean the database.
510 data->last_reset = data->reset_time;
511 find_next_reset(data,request->timestamp);
514 * Open a completely new database.
516 data->gdbm = gdbm_open(data->filename, sizeof(int),
517 GDBM_NEWDB | GDBM_SYNCOPT | GDBM_NOLOCK, 0600, NULL);
518 if (data->gdbm == NULL) {
519 radlog(L_ERR, "rlm_counter: Failed to open file %s: %s",
520 data->filename, strerror(errno));
521 return RLM_MODULE_FAIL;
523 data->fd = gdbm_fdesc(data->gdbm);
524 if (gdbm_setopt(data->gdbm, GDBM_CACHESIZE, &cache_size, sizeof(int)) == -1)
525 radlog(L_ERR, "rlm_counter: Failed to set cache size");
530 * Look for the key. User-Name is special. It means
531 * The REAL username, after stripping.
533 DEBUG2("rlm_counter: Entering module authorize code");
534 key_vp = (data->key_attr == PW_USER_NAME) ? request->username : pairfind(request->packet->vps, data->key_attr);
535 if (key_vp == NULL) {
536 DEBUG2("rlm_counter: Could not find Key value pair");
541 * Look for the check item
543 if ((dattr = dict_attrbyname(data->check_name)) == NULL) {
546 if ((check_vp= pairfind(request->config_items, dattr->attr)) == NULL) {
547 DEBUG2("rlm_counter: Could not find Check item value pair");
551 key_datum.dptr = key_vp->strvalue;
552 key_datum.dsize = key_vp->length;
554 rad_lockfd(data->fd, sizeof(int));
555 count_datum = gdbm_fetch(data->gdbm, key_datum);
556 rad_unlockfd(data->fd, sizeof(int));
557 if (count_datum.dptr != NULL){
558 memcpy(&counter, count_datum.dptr, sizeof(int));
559 free(count_datum.dptr);
563 * Check if check item > counter
565 res=check_vp->lvalue - counter;
568 * We are assuming that simultaneous-use=1. But
569 * even if that does not happen then our user
570 * could login at max for 2*max-usage-time Is
575 * User is allowed, but set Session-Timeout.
576 * Stolen from main/auth.c
580 * If we are near a reset then add the next
581 * limit, so that the user will not need to
584 if (data->reset_time && (
585 res >= (data->reset_time - request->timestamp))) {
586 res += check_vp->lvalue;
589 DEBUG2("rlm_counter: (Check item - counter) is greater than zero");
590 if ((reply_item = pairfind(request->reply->vps, PW_SESSION_TIMEOUT)) != NULL) {
591 if (reply_item->lvalue > res)
592 reply_item->lvalue = res;
594 if ((reply_item = paircreate(PW_SESSION_TIMEOUT, PW_TYPE_INTEGER)) == NULL) {
595 radlog(L_ERR|L_CONS, "no memory");
596 return RLM_MODULE_NOOP;
598 reply_item->lvalue = res;
599 pairadd(&request->reply->vps, reply_item);
604 DEBUG2("rlm_counter: Authorized user %s, check_item=%d, counter=%d",
605 key_vp->strvalue,check_vp->lvalue,counter);
606 DEBUG2("rlm_counter: Sent Reply-Item for user %s, Type=Session-Timeout, value=%d",
607 key_vp->strvalue,res);
610 char module_msg[MAX_STRING_LEN];
611 VALUE_PAIR *module_msg_vp;
614 * User is denied access, send back a reply message
616 sprintf(msg, "Your maximum %s usage time has been reached", data->reset);
617 reply_item=pairmake("Reply-Message", msg, T_OP_EQ);
618 pairadd(&request->reply->vps, reply_item);
620 snprintf(module_msg, sizeof(module_msg), "rlm_counter: Maximum %s usage time reached", data->reset);
621 module_msg_vp = pairmake("Module-Message", module_msg, T_OP_EQ);
622 pairadd(&request->packet->vps, module_msg_vp);
624 ret=RLM_MODULE_REJECT;
626 DEBUG2("rlm_counter: Rejected user %s, check_item=%d, counter=%d",
627 key_vp->strvalue,check_vp->lvalue,counter);
633 static int counter_detach(void *instance)
635 rlm_counter_t *data = (rlm_counter_t *) instance;
637 paircompare_unregister(data->dict_attr, counter_cmp);
638 gdbm_close(data->gdbm);
639 free(data->filename);
641 free(data->key_name);
642 free(data->count_attribute);
643 free(data->counter_name);
650 * The module name should be the only globally exported symbol.
651 * That is, everything else should be 'static'.
653 * If the module needs to temporarily modify it's instantiation
654 * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
655 * The server will then take care of ensuring that the module
656 * is single-threaded.
658 module_t rlm_counter = {
660 RLM_TYPE_THREAD_UNSAFE, /* type */
661 NULL, /* initialization */
662 counter_instantiate, /* instantiation */
664 NULL, /* authentication */
665 counter_authorize, /* authorization */
666 NULL, /* preaccounting */
667 counter_accounting, /* accounting */
668 NULL /* checksimul */
670 counter_detach, /* detach */