Jumbo fix to quiet compiler warnings by adding (int) cast to
[freeradius.git] / src / modules / rlm_counter / rlm_counter.c
1 /*
2  * rlm_counter.c
3  *
4  * Version:  $Id$
5  *
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.
10  *
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.
15  *
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
19  *
20  * Copyright 2001  The FreeRADIUS server project
21  * Copyright 2001  Alan DeKok <aland@ox.org>
22  * Copyright 2001  Kostas Kalevras <kkalev@noc.ntua.gr>
23  */
24
25 #include "config.h"
26 #include "autoconf.h"
27 #include "libradius.h"
28
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <ctype.h>
33
34 #include "radiusd.h"
35 #include "modules.h"
36 #include "conffile.h"
37
38 #include <gdbm.h>
39 #include <time.h>
40
41 #ifdef NEEDS_GDBM_SYNC
42 #       define GDBM_SYNCOPT GDBM_SYNC
43 #else
44 #       define GDBM_SYNCOPT 0
45 #endif
46
47 #ifdef GDBM_NOLOCK
48 #define GDBM_COUNTER_OPTS (GDBM_SYNCOPT | GDBM_NOLOCK)
49 #else
50 #define GDBM_COUNTER_OPTS (GDBM_SYNCOPT)
51 #endif
52
53 #ifndef HAVE_GDBM_FDESC
54 #define gdbm_fdesc(foo) (-1)
55 #endif
56
57 static const char rcsid[] = "$Id$";
58
59 /*
60  *      Define a structure for our module configuration.
61  *
62  *      These variables do not need to be in a structure, but it's
63  *      a lot cleaner to do so, and a pointer to the structure can
64  *      be used as the instance handle.
65  */
66 typedef struct rlm_counter_t {
67         char *filename;  /* name of the database file */
68         char *reset;  /* daily, weekly, monthly, never or user defined */
69         char *key_name;  /* User-Name */
70         char *count_attribute;  /* Acct-Session-Time */
71         char *counter_name;  /* Daily-Session-Time */
72         char *check_name;  /* Daily-Max-Session */
73         char *service_type;  /* Service-Type to search for */
74         int cache_size;
75         int service_val;
76         int key_attr;
77         int count_attr;
78         time_t reset_time;
79         time_t last_reset;
80         int dict_attr;  /* attribute number for the counter. */
81         GDBM_FILE gdbm;
82         int fd;
83 } rlm_counter_t;
84
85 /*
86  *      A mapping of configuration file names to internal variables.
87  *
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
92  *      buffer over-flows.
93  */
94 static CONF_PARSER module_config[] = {
95   { "filename", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,filename), NULL, NULL },
96   { "key", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,key_name), NULL, NULL },
97   { "reset", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,reset), NULL,  NULL },
98   { "count-attribute", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,count_attribute), NULL, NULL },
99   { "counter-name", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,counter_name), NULL,  NULL },
100   { "check-name", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,check_name), NULL, NULL },
101   { "allowed-servicetype", PW_TYPE_STRING_PTR, offsetof(rlm_counter_t,service_type),NULL, NULL },
102   { "cache-size", PW_TYPE_INTEGER, offsetof(rlm_counter_t,cache_size), NULL, "1000" },
103   { NULL, -1, 0, NULL, NULL }
104 };
105
106
107 /*
108  *      See if the counter matches.
109  */
110 static int counter_cmp(void *instance, REQUEST *req, VALUE_PAIR *request, VALUE_PAIR *check,
111                 VALUE_PAIR *check_pairs, VALUE_PAIR **reply_pairs)
112 {
113         rlm_counter_t *data = (rlm_counter_t *) instance;
114         datum key_datum;
115         datum count_datum;
116         VALUE_PAIR *key_vp;
117         int counter;
118
119         check_pairs = check_pairs; /* shut the compiler up */
120         reply_pairs = reply_pairs;
121
122         /*
123          *      Find the key attribute.
124          */
125         key_vp = pairfind(request, data->key_attr);
126         if (key_vp == NULL) {
127                 return RLM_MODULE_NOOP;
128         }
129
130         key_datum.dptr = key_vp->strvalue;
131         key_datum.dsize = key_vp->length;
132
133         count_datum = gdbm_fetch(data->gdbm, key_datum);
134
135         if (count_datum.dptr == NULL) {
136                 return -1;
137         }
138         memcpy(&counter, count_datum.dptr, sizeof(int));
139         free(count_datum.dptr);
140
141         return counter - check->lvalue;
142 }
143
144
145 static int find_next_reset(rlm_counter_t *data, time_t timeval)
146 {
147         int ret=0;
148         unsigned int num=1;
149         char last = 0;
150         struct tm *tm, s_tm;
151
152         tm = localtime_r(&timeval, &s_tm);
153         tm->tm_sec = tm->tm_min = 0;
154
155         if (data->reset == NULL)
156                 return -1;
157         if (isdigit((int) data->reset[0])){
158                 unsigned int len=0;
159
160                 len = strlen(data->reset);
161                 if (len == 0)
162                         return -1;
163                 last = data->reset[len - 1];
164                 if (!isalpha((int) last))
165                         last = 'd';
166                 num = atoi(data->reset);
167                 DEBUG("rlm_counter: num=%d, last=%c",num,last);
168         }
169         if (strcmp(data->reset, "hourly") == 0 || last == 'h') {
170                 /*
171                  *  Round up to the next nearest hour.
172                  */
173                 tm->tm_hour += num;
174                 data->reset_time = mktime(tm);
175         } else if (strcmp(data->reset, "daily") == 0 || last == 'd') {
176                 /*
177                  *  Round up to the next nearest day.
178                  */
179                 tm->tm_hour = 0;
180                 tm->tm_mday += num;
181                 data->reset_time = mktime(tm);
182         } else if (strcmp(data->reset, "weekly") == 0 || last == 'w') {
183                 /*
184                  *  Round up to the next nearest week.
185                  */
186                 tm->tm_hour = 0;
187                 tm->tm_mday += (7 - tm->tm_wday) +(7*(num-1));
188                 data->reset_time = mktime(tm);
189         } else if (strcmp(data->reset, "monthly") == 0 || last == 'm') {
190                 tm->tm_hour = 0;
191                 tm->tm_mday = 1;
192                 tm->tm_mon += num;
193                 data->reset_time = mktime(tm);
194         } else if (strcmp(data->reset, "never") == 0) {
195                 data->reset_time = 0;
196         } else {
197                 radlog(L_ERR, "rlm_counter: Unknown reset timer \"%s\"",
198                         data->reset);
199                 return -1;
200         }
201         DEBUG2("rlm_counter: Current Time: %d, Next reset %d", 
202                 (int)timeval,(int)data->reset_time);
203
204         return ret;
205 }
206
207
208 /*
209  *      Do any per-module initialization that is separate to each
210  *      configured instance of the module.  e.g. set up connections
211  *      to external databases, read configuration files, set up
212  *      dictionary entries, etc.
213  *
214  *      If configuration information is given in the config section
215  *      that must be referenced in later calls, store a handle to it
216  *      in *instance otherwise put a null pointer there.
217  */
218 static int counter_instantiate(CONF_SECTION *conf, void **instance)
219 {
220         rlm_counter_t *data;
221         DICT_ATTR *dattr;
222         DICT_VALUE *dval;
223         ATTR_FLAGS flags;
224         time_t now;
225         int cache_size;
226         
227         /*
228          *      Set up a storage area for instance data
229          */
230         data = rad_malloc(sizeof(*data));
231
232         /*
233          *      If the configuration parameters can't be parsed, then
234          *      fail.
235          */
236         if (cf_section_parse(conf, data, module_config) < 0) {
237                 free(data);
238                 return -1;
239         }
240         cache_size = data->cache_size;
241
242         /*
243          *      Discover the attribute number of the key. 
244          */
245         if (data->key_name == NULL) {
246                 radlog(L_ERR, "rlm_counter: 'key' must be set.");
247                 exit(0);
248         }
249         dattr = dict_attrbyname(data->key_name);
250         if (dattr == NULL) {
251                 radlog(L_ERR, "rlm_counter: No such attribute %s",
252                                 data->key_name);
253                 return -1;
254         }
255         data->key_attr = dattr->attr;
256         
257         /*
258          *      Discover the attribute number of the counter. 
259          */
260         if (data->count_attribute == NULL) {
261                 radlog(L_ERR, "rlm_counter: 'count-attribute' must be set.");
262                 exit(0);
263         }
264         dattr = dict_attrbyname(data->count_attribute);
265         if (dattr == NULL) {
266                 radlog(L_ERR, "rlm_counter: No such attribute %s",
267                                 data->count_attribute);
268                 return -1;
269         }
270         data->count_attr = dattr->attr;
271
272         /*
273          *  Create a new attribute for the counter.
274          */
275         if (data->counter_name == NULL) {
276                 radlog(L_ERR, "rlm_counter: 'counter-name' must be set.");
277                 exit(0);
278         }
279
280         memset(&flags, 0, sizeof(flags));
281         dict_addattr(data->counter_name, 0, PW_TYPE_INTEGER, -1, flags);
282         dattr = dict_attrbyname(data->counter_name);
283         if (dattr == NULL) {
284                 radlog(L_ERR, "rlm_counter: Failed to create counter attribute %s",
285                                 data->counter_name);
286                 return -1;
287         }
288         data->dict_attr = dattr->attr;
289         DEBUG2("rlm_counter: Counter attribute %s is number %d",
290                         data->counter_name, data->dict_attr);
291
292         /*
293          * Create a new attribute for the check item.
294          */
295         if (data->check_name == NULL) {
296                 radlog(L_ERR, "rlm_counter: 'check-name' must be set.");
297                 exit(0);
298         }
299         dict_addattr(data->check_name, 0, PW_TYPE_INTEGER, -1, flags);
300         dattr = dict_attrbyname(data->check_name);
301         if (dattr == NULL) {
302                 radlog(L_ERR, "rlm_counter: Failed to create check attribute %s",
303                                 data->counter_name);
304                 return -1;
305         }
306
307         /*
308          * Find the attribute for the allowed protocol
309          */
310         if (data->service_type != NULL) {
311                 if ((dval = dict_valbyname(PW_SERVICE_TYPE, data->service_type)) == NULL) {
312                         radlog(L_ERR, "rlm_counter: Failed to find attribute number for %s",
313                                         data->service_type);
314                         return -1;
315                 }
316                 data->service_val = dval->value;
317         }       
318
319         /*
320          *  Discover when next to reset the database.
321          */
322         if (data->reset == NULL) {
323                 radlog(L_ERR, "rlm_counter: 'reset' must be set.");
324                 exit(0);
325         }
326         now = time(NULL);
327         data->reset_time = 0;
328
329         if (find_next_reset(data,now) == -1)
330                 return -1;
331
332         if (data->filename == NULL) {
333                 radlog(L_ERR, "rlm_counter: 'filename' must be set.");
334                 exit(0);
335         }
336         data->gdbm = gdbm_open(data->filename, sizeof(int),
337                         GDBM_WRCREAT | GDBM_COUNTER_OPTS, 0600, NULL);
338         if (data->gdbm == NULL) {
339                 radlog(L_ERR, "rlm_counter: Failed to open file %s: %s",
340                                 data->filename, strerror(errno));
341                 return -1;
342         }
343         if (data->fd >= 0) data->fd = gdbm_fdesc(data->gdbm);
344
345         if (gdbm_setopt(data->gdbm, GDBM_CACHESIZE, &cache_size, sizeof(int)) == -1)
346                 radlog(L_ERR, "rlm_counter: Failed to set cache size");
347
348
349         /*
350          *      Register the counter comparison operation.
351          */
352         paircompare_register(data->dict_attr, 0, counter_cmp, data);
353
354         *instance = data;
355         
356         return 0;
357 }
358
359 /*
360  *      Write accounting information to this modules database.
361  */
362 static int counter_accounting(void *instance, REQUEST *request)
363 {
364         rlm_counter_t *data = (rlm_counter_t *)instance;
365         datum key_datum;
366         datum count_datum;
367         VALUE_PAIR *key_vp, *count_vp, *proto_vp;
368         int counter;
369         int rcode;
370         time_t diff;
371
372         /*
373          *      Before doing anything else, see if we have to reset
374          *      the counters.
375          */
376         if (data->reset_time && (data->reset_time <= request->timestamp)) {
377                 int cache_size = data->cache_size;
378
379                 gdbm_close(data->gdbm);
380
381                 /*
382                  *      Re-set the next time to clean the database.
383                  */
384                 data->last_reset = data->reset_time;
385                 find_next_reset(data,request->timestamp);
386
387                 /*
388                  *      Open a completely new database.
389                  */
390                 data->gdbm = gdbm_open(data->filename, sizeof(int),
391                                 GDBM_NEWDB | GDBM_COUNTER_OPTS, 0600, NULL);
392                 if (data->gdbm == NULL) {
393                         radlog(L_ERR, "rlm_counter: Failed to open file %s: %s",
394                                         data->filename, strerror(errno));
395                         return RLM_MODULE_FAIL;
396                 }
397                 if (data->fd >= 0) data->fd = gdbm_fdesc(data->gdbm);
398                 if (gdbm_setopt(data->gdbm, GDBM_CACHESIZE, &cache_size, sizeof(int)) == -1)
399                         radlog(L_ERR, "rlm_counter: Failed to set cache size");
400         }
401         /*
402          * Check if we need to watch out for a specific service-type. If yes then check it
403          */
404         if (data->service_type != NULL) {
405                 if ((proto_vp = pairfind(request->packet->vps, PW_SERVICE_TYPE)) == NULL)
406                         return RLM_MODULE_NOOP;
407                 if (proto_vp->lvalue != data->service_val)
408                         return RLM_MODULE_NOOP;
409
410         }       
411         
412
413         /*
414          *      Look for the key.  User-Name is special.  It means
415          *      The REAL username, after stripping.
416          */
417         key_vp = (data->key_attr == PW_USER_NAME) ? request->username : pairfind(request->packet->vps, data->key_attr);
418         if (key_vp == NULL)
419                 return RLM_MODULE_NOOP;
420
421         /*
422          *      Look for the attribute to use as a counter.
423          */
424         count_vp = pairfind(request->packet->vps, data->count_attr);
425         if (count_vp == NULL)
426                 return RLM_MODULE_NOOP;
427
428         key_datum.dptr = key_vp->strvalue;
429         key_datum.dsize = key_vp->length;
430
431         count_datum = gdbm_fetch(data->gdbm, key_datum);
432         if (count_datum.dptr == NULL)
433                 counter = 0;
434         else{
435                 memcpy(&counter, count_datum.dptr, sizeof(int));
436                 free(count_datum.dptr);
437         }
438
439         if (count_vp->type == PW_TYPE_DATE) {
440                 /*
441                  *      If session time < diff then the user got in after the
442                  *      last reset. So add his session time, otherwise add the
443                  *      diff.
444                  *
445                  *      That way if he logged in at 23:00 and we reset the
446                  *      daily counter at 24:00 and he logged out at 01:00
447                  *      then we will only count one hour (the one in the new
448                  *      day). That is the right thing
449                  */
450                 diff = request->timestamp - data->last_reset;
451                 counter += (count_vp->lvalue < diff) ? count_vp->lvalue : diff;
452
453         } else if (count_vp->type == PW_TYPE_INTEGER) {
454                 /*
455                  *      Integers get counted, without worrying about
456                  *      reset dates.
457                  */
458                 counter += count_vp->lvalue;
459
460         } else {
461                 /*
462                  *      The attribute is NOT an integer, just count once
463                  *      more that we've seen it.
464                  */
465                 counter++;
466         }
467         count_datum.dptr = (char *) &counter;
468         count_datum.dsize = sizeof(int);
469
470         rcode = gdbm_store(data->gdbm, key_datum, count_datum, GDBM_REPLACE);
471         if (rcode < 0) {
472                 radlog(L_ERR, "rlm_counter: Failed storing data to %s: %s",
473                                 data->filename, gdbm_strerror(gdbm_errno));
474                 return RLM_MODULE_FAIL;
475         }
476
477         return RLM_MODULE_OK;
478 }
479
480 /*
481  *      Find the named user in this modules database.  Create the set
482  *      of attribute-value pairs to check and reply with for this user
483  *      from the database. The authentication code only needs to check
484  *      the password, the rest is done here.
485  */
486 static int counter_authorize(void *instance, REQUEST *request)
487 {
488         rlm_counter_t *data = (rlm_counter_t *) instance;
489         int ret=RLM_MODULE_NOOP;
490         datum key_datum;
491         datum count_datum;
492         int counter=0;
493         int res=0;
494         DICT_ATTR *dattr;
495         VALUE_PAIR *key_vp, *check_vp;
496         VALUE_PAIR *reply_item;
497         char msg[128];
498
499         /* quiet the compiler */
500         instance = instance;
501         request = request;
502
503         /*
504          *      Before doing anything else, see if we have to reset
505          *      the counters.
506          */
507         if (data->reset_time && (data->reset_time <= request->timestamp)) {
508                 int cache_size = data->cache_size;
509
510                 gdbm_close(data->gdbm);
511
512                 /*
513                  *      Re-set the next time to clean the database.
514                  */
515                 data->last_reset = data->reset_time;
516                 find_next_reset(data,request->timestamp);
517
518                 /*
519                  *      Open a completely new database.
520                  */
521                 data->gdbm = gdbm_open(data->filename, sizeof(int),
522                                 GDBM_NEWDB | GDBM_COUNTER_OPTS, 0600, NULL);
523                 if (data->gdbm == NULL) {
524                         radlog(L_ERR, "rlm_counter: Failed to open file %s: %s",
525                                         data->filename, strerror(errno));
526                         return RLM_MODULE_FAIL;
527                 }
528                 data->fd = gdbm_fdesc(data->gdbm);
529                 if (gdbm_setopt(data->gdbm, GDBM_CACHESIZE, &cache_size, sizeof(int)) == -1)
530                         radlog(L_ERR, "rlm_counter: Failed to set cache size");
531         }
532
533
534         /*
535          *      Look for the key.  User-Name is special.  It means
536          *      The REAL username, after stripping.
537          */
538         DEBUG2("rlm_counter: Entering module authorize code");
539         key_vp = (data->key_attr == PW_USER_NAME) ? request->username : pairfind(request->packet->vps, data->key_attr);
540         if (key_vp == NULL) {
541                 DEBUG2("rlm_counter: Could not find Key value pair");
542                 return ret;
543         }
544
545         /*
546          *      Look for the check item
547          */
548         if ((dattr = dict_attrbyname(data->check_name)) == NULL) {
549                 return ret;
550         }
551         if ((check_vp= pairfind(request->config_items, dattr->attr)) == NULL) {
552                 DEBUG2("rlm_counter: Could not find Check item value pair");
553                 return ret;
554         }
555
556         key_datum.dptr = key_vp->strvalue;
557         key_datum.dsize = key_vp->length;
558         
559         count_datum = gdbm_fetch(data->gdbm, key_datum);
560         if (count_datum.dptr != NULL){
561                 memcpy(&counter, count_datum.dptr, sizeof(int));
562                 free(count_datum.dptr);
563         }
564
565         /*
566          * Check if check item > counter
567          */
568         res=check_vp->lvalue - counter;
569         if (res > 0) {
570                 /*
571                  *      We are assuming that simultaneous-use=1. But
572                  *      even if that does not happen then our user
573                  *      could login at max for 2*max-usage-time Is
574                  *      that acceptable?
575                  */
576
577                 /*
578                  *      User is allowed, but set Session-Timeout.
579                  *      Stolen from main/auth.c
580                  */
581
582                 /*
583                  *      If we are near a reset then add the next
584                  *      limit, so that the user will not need to
585                  *      login again
586                  */
587                 if (data->reset_time && (
588                         res >= (data->reset_time - request->timestamp))) {
589                         res += check_vp->lvalue;
590                 }
591
592                 DEBUG2("rlm_counter: (Check item - counter) is greater than zero");
593                 if ((reply_item = pairfind(request->reply->vps, PW_SESSION_TIMEOUT)) != NULL) {
594                         if (reply_item->lvalue > res)
595                                 reply_item->lvalue = res;
596                 } else {
597                         if ((reply_item = paircreate(PW_SESSION_TIMEOUT, PW_TYPE_INTEGER)) == NULL) {
598                                 radlog(L_ERR|L_CONS, "no memory");
599                                 return RLM_MODULE_NOOP;
600                         }
601                         reply_item->lvalue = res;
602                         pairadd(&request->reply->vps, reply_item);
603                 }
604
605                 ret=RLM_MODULE_OK;
606
607                 DEBUG2("rlm_counter: Authorized user %s, check_item=%d, counter=%d",
608                                 key_vp->strvalue,check_vp->lvalue,counter);
609                 DEBUG2("rlm_counter: Sent Reply-Item for user %s, Type=Session-Timeout, value=%d",
610                                 key_vp->strvalue,res);
611         }
612         else{
613                 char module_fmsg[MAX_STRING_LEN];
614                 VALUE_PAIR *module_fmsg_vp;
615
616                 /*
617                  * User is denied access, send back a reply message
618                 */
619                 sprintf(msg, "Your maximum %s usage time has been reached", data->reset);
620                 reply_item=pairmake("Reply-Message", msg, T_OP_EQ);
621                 pairadd(&request->reply->vps, reply_item);
622
623                 snprintf(module_fmsg,sizeof(module_fmsg), "rlm_counter: Maximum %s usage time reached", data->reset);
624                 module_fmsg_vp = pairmake("Module-Failure-Message", module_fmsg, T_OP_EQ);
625                 pairadd(&request->packet->vps, module_fmsg_vp); 
626
627                 ret=RLM_MODULE_REJECT;
628
629                 DEBUG2("rlm_counter: Rejected user %s, check_item=%d, counter=%d",
630                                 key_vp->strvalue,check_vp->lvalue,counter);
631         }
632
633         return ret;
634 }
635
636 static int counter_detach(void *instance)
637 {
638         rlm_counter_t *data = (rlm_counter_t *) instance;
639
640         paircompare_unregister(data->dict_attr, counter_cmp);
641         gdbm_close(data->gdbm);
642         free(data->filename);
643         free(data->reset);
644         free(data->key_name);
645         free(data->count_attribute);
646         free(data->counter_name);
647
648         free(instance);
649         return 0;
650 }
651
652 /*
653  *      The module name should be the only globally exported symbol.
654  *      That is, everything else should be 'static'.
655  *
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.
660  */
661 module_t rlm_counter = {
662         "Counter",      
663         RLM_TYPE_THREAD_UNSAFE,         /* type */
664         NULL,                           /* initialization */
665         counter_instantiate,            /* instantiation */
666         {
667                 NULL,                   /* authentication */
668                 counter_authorize,      /* authorization */
669                 NULL,                   /* preaccounting */
670                 counter_accounting,     /* accounting */
671                 NULL                    /* checksimul */
672         },
673         counter_detach,                 /* detach */
674         NULL,                           /* destroy */
675 };