Open the DB unlocked, and do file locking ourselves using the
[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
48 static const char rcsid[] = "$Id$";
49
50 /*
51  *      Define a structure for our module configuration.
52  *
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.
56  */
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 */
65         int cache_size;
66         int service_val;
67         int key_attr;
68         int count_attr;
69         time_t reset_time;
70         time_t last_reset;
71         int dict_attr;  /* attribute number for the counter. */
72         GDBM_FILE gdbm;
73         int fd;
74 } rlm_counter_t;
75
76 /*
77  *      A mapping of configuration file names to internal variables.
78  *
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
83  *      buffer over-flows.
84  */
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 }
95 };
96
97
98 /*
99  *      See if the counter matches.
100  */
101 static int counter_cmp(void *instance, REQUEST *req, VALUE_PAIR *request, VALUE_PAIR *check,
102                 VALUE_PAIR *check_pairs, VALUE_PAIR **reply_pairs)
103 {
104         rlm_counter_t *data = (rlm_counter_t *) instance;
105         datum key_datum;
106         datum count_datum;
107         VALUE_PAIR *key_vp;
108         int counter;
109
110         check_pairs = check_pairs; /* shut the compiler up */
111         reply_pairs = reply_pairs;
112
113         /*
114          *      Find the key attribute.
115          */
116         key_vp = pairfind(request, data->key_attr);
117         if (key_vp == NULL) {
118                 return RLM_MODULE_NOOP;
119         }
120
121         key_datum.dptr = key_vp->strvalue;
122         key_datum.dsize = key_vp->length;
123
124         rad_lockfd(data->fd, sizeof(int));
125         count_datum = gdbm_fetch(data->gdbm, key_datum);
126         rad_unlockfd(data->fd, sizeof(int));
127
128         if (count_datum.dptr == NULL) {
129                 return -1;
130         }
131         memcpy(&counter, count_datum.dptr, sizeof(int));
132         free(count_datum.dptr);
133
134         return counter - check->lvalue;
135 }
136
137
138 static int find_next_reset(rlm_counter_t *data, time_t timeval)
139 {
140         int ret=0;
141         unsigned int num=1;
142         char last = 0;
143         struct tm *tm, s_tm;
144
145         tm = localtime_r(&timeval, &s_tm);
146         tm->tm_sec = tm->tm_min = 0;
147
148         if (data->reset == NULL)
149                 return -1;
150         if (isdigit(data->reset[0])){
151                 unsigned int len=0;
152
153                 len = strlen(data->reset);
154                 if (len == 0)
155                         return -1;
156                 last = data->reset[len - 1];
157                 if (!isalpha(last))
158                         last = 'd';
159                 num = atoi(data->reset);
160                 DEBUG("rlm_counter: num=%d, last=%c",num,last);
161         }
162         if (strcmp(data->reset, "hourly") == 0 || last == 'h') {
163                 /*
164                  *  Round up to the next nearest hour.
165                  */
166                 tm->tm_hour += num;
167                 data->reset_time = mktime(tm);
168         } else if (strcmp(data->reset, "daily") == 0 || last == 'd') {
169                 /*
170                  *  Round up to the next nearest day.
171                  */
172                 tm->tm_hour = 0;
173                 tm->tm_mday += num;
174                 data->reset_time = mktime(tm);
175         } else if (strcmp(data->reset, "weekly") == 0 || last == 'w') {
176                 /*
177                  *  Round up to the next nearest week.
178                  */
179                 tm->tm_hour = 0;
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') {
183                 tm->tm_hour = 0;
184                 tm->tm_mday = 1;
185                 tm->tm_mon += num;
186                 data->reset_time = mktime(tm);
187         } else if (strcmp(data->reset, "never") == 0) {
188                 data->reset_time = 0;
189         } else {
190                 radlog(L_ERR, "rlm_counter: Unknown reset timer \"%s\"",
191                         data->reset);
192                 return -1;
193         }
194         DEBUG2("rlm_counter: Current Time: %d, Next reset %d", 
195                 (int)timeval,(int)data->reset_time);
196
197         return ret;
198 }
199
200
201 /*
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.
206  *
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.
210  */
211 static int counter_instantiate(CONF_SECTION *conf, void **instance)
212 {
213         rlm_counter_t *data;
214         DICT_ATTR *dattr;
215         DICT_VALUE *dval;
216         ATTR_FLAGS flags;
217         time_t now;
218         int cache_size;
219         
220         /*
221          *      Set up a storage area for instance data
222          */
223         data = rad_malloc(sizeof(*data));
224
225         /*
226          *      If the configuration parameters can't be parsed, then
227          *      fail.
228          */
229         if (cf_section_parse(conf, data, module_config) < 0) {
230                 free(data);
231                 return -1;
232         }
233         cache_size = data->cache_size;
234
235         /*
236          *      Discover the attribute number of the key. 
237          */
238         if (data->key_name == NULL) {
239                 radlog(L_ERR, "rlm_counter: 'key' must be set.");
240                 exit(0);
241         }
242         dattr = dict_attrbyname(data->key_name);
243         if (dattr == NULL) {
244                 radlog(L_ERR, "rlm_counter: No such attribute %s",
245                                 data->key_name);
246                 return -1;
247         }
248         data->key_attr = dattr->attr;
249         
250         /*
251          *      Discover the attribute number of the counter. 
252          */
253         if (data->count_attribute == NULL) {
254                 radlog(L_ERR, "rlm_counter: 'count-attribute' must be set.");
255                 exit(0);
256         }
257         dattr = dict_attrbyname(data->count_attribute);
258         if (dattr == NULL) {
259                 radlog(L_ERR, "rlm_counter: No such attribute %s",
260                                 data->count_attribute);
261                 return -1;
262         }
263         data->count_attr = dattr->attr;
264
265         /*
266          *  Create a new attribute for the counter.
267          */
268         if (data->counter_name == NULL) {
269                 radlog(L_ERR, "rlm_counter: 'counter-name' must be set.");
270                 exit(0);
271         }
272
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);
276         if (dattr == NULL) {
277                 radlog(L_ERR, "rlm_counter: Failed to create counter attribute %s",
278                                 data->counter_name);
279                 return -1;
280         }
281         data->dict_attr = dattr->attr;
282         DEBUG2("rlm_counter: Counter attribute %s is number %d",
283                         data->counter_name, data->dict_attr);
284
285         /*
286          * Create a new attribute for the check item.
287          */
288         if (data->check_name == NULL) {
289                 radlog(L_ERR, "rlm_counter: 'check-name' must be set.");
290                 exit(0);
291         }
292         dict_addattr(data->check_name, 0, PW_TYPE_INTEGER, -1, flags);
293         dattr = dict_attrbyname(data->check_name);
294         if (dattr == NULL) {
295                 radlog(L_ERR, "rlm_counter: Failed to create check attribute %s",
296                                 data->counter_name);
297                 return -1;
298         }
299
300         /*
301          * Find the attribute for the allowed protocol
302          */
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",
306                                         data->service_type);
307                         return -1;
308                 }
309                 data->service_val = dval->value;
310         }       
311
312         /*
313          *  Discover when next to reset the database.
314          */
315         if (data->reset == NULL) {
316                 radlog(L_ERR, "rlm_counter: 'reset' must be set.");
317                 exit(0);
318         }
319         now = time(NULL);
320         data->reset_time = 0;
321
322         if (find_next_reset(data,now) == -1)
323                 return -1;
324
325         if (data->filename == NULL) {
326                 radlog(L_ERR, "rlm_counter: 'filename' must be set.");
327                 exit(0);
328         }
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));
334                 return -1;
335         }
336         data->fd = gdbm_fdesc(data->gdbm);
337
338         if (gdbm_setopt(data->gdbm, GDBM_CACHESIZE, &cache_size, sizeof(int)) == -1)
339                 radlog(L_ERR, "rlm_counter: Failed to set cache size");
340
341
342         /*
343          *      Register the counter comparison operation.
344          */
345         paircompare_register(data->dict_attr, 0, counter_cmp, data);
346
347         *instance = data;
348         
349         return 0;
350 }
351
352 /*
353  *      Write accounting information to this modules database.
354  */
355 static int counter_accounting(void *instance, REQUEST *request)
356 {
357         rlm_counter_t *data = (rlm_counter_t *)instance;
358         datum key_datum;
359         datum count_datum;
360         VALUE_PAIR *key_vp, *count_vp, *proto_vp;
361         int counter;
362         int rcode;
363         time_t diff;
364
365         /*
366          *      Before doing anything else, see if we have to reset
367          *      the counters.
368          */
369         if (data->reset_time && (data->reset_time <= request->timestamp)) {
370                 int cache_size = data->cache_size;
371
372                 gdbm_close(data->gdbm);
373
374                 /*
375                  *      Re-set the next time to clean the database.
376                  */
377                 data->last_reset = data->reset_time;
378                 find_next_reset(data,request->timestamp);
379
380                 /*
381                  *      Open a completely new database.
382                  */
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;
389                 }
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");
393         }
394         /*
395          * Check if we need to watch out for a specific service-type. If yes then check it
396          */
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;
402
403         }       
404         
405
406         /*
407          *      Look for the key.  User-Name is special.  It means
408          *      The REAL username, after stripping.
409          */
410         key_vp = (data->key_attr == PW_USER_NAME) ? request->username : pairfind(request->packet->vps, data->key_attr);
411         if (key_vp == NULL)
412                 return RLM_MODULE_NOOP;
413
414         /*
415          *      Look for the attribute to use as a counter.
416          */
417         count_vp = pairfind(request->packet->vps, data->count_attr);
418         if (count_vp == NULL)
419                 return RLM_MODULE_NOOP;
420
421         key_datum.dptr = key_vp->strvalue;
422         key_datum.dsize = key_vp->length;
423
424         rad_lockfd(data->fd, sizeof(int));
425         count_datum = gdbm_fetch(data->gdbm, key_datum);
426         if (count_datum.dptr == NULL)
427                 counter = 0;
428         else{
429                 memcpy(&counter, count_datum.dptr, sizeof(int));
430                 free(count_datum.dptr);
431         }
432
433         if (count_vp->type == PW_TYPE_DATE) {
434                 /*
435                  *      If session time < diff then the user got in after the
436                  *      last reset. So add his session time, otherwise add the
437                  *      diff.
438                  *
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
443                  */
444                 diff = request->timestamp - data->last_reset;
445                 counter += (count_vp->lvalue < diff) ? count_vp->lvalue : diff;
446
447         } else if (count_vp->type == PW_TYPE_INTEGER) {
448                 /*
449                  *      Integers get counted, without worrying about
450                  *      reset dates.
451                  */
452                 counter += count_vp->lvalue;
453
454         } else {
455                 /*
456                  *      The attribute is NOT an integer, just count once
457                  *      more that we've seen it.
458                  */
459                 counter++;
460         }
461         count_datum.dptr = (char *) &counter;
462         count_datum.dsize = sizeof(int);
463
464         rcode = gdbm_store(data->gdbm, key_datum, count_datum, GDBM_REPLACE);
465         rad_unlockfd(data->fd, sizeof(int));
466         if (rcode < 0) {
467                 radlog(L_ERR, "rlm_counter: Failed storing data to %s: %s",
468                                 data->filename, gdbm_strerror(gdbm_errno));
469                 return RLM_MODULE_FAIL;
470         }
471
472         return RLM_MODULE_OK;
473 }
474
475 /*
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.
480  */
481 static int counter_authorize(void *instance, REQUEST *request)
482 {
483         rlm_counter_t *data = (rlm_counter_t *) instance;
484         int ret=RLM_MODULE_NOOP;
485         datum key_datum;
486         datum count_datum;
487         int counter=0;
488         int res=0;
489         DICT_ATTR *dattr;
490         VALUE_PAIR *key_vp, *check_vp;
491         VALUE_PAIR *reply_item;
492         char msg[128];
493
494         /* quiet the compiler */
495         instance = instance;
496         request = request;
497
498         /*
499          *      Before doing anything else, see if we have to reset
500          *      the counters.
501          */
502         if (data->reset_time && (data->reset_time <= request->timestamp)) {
503                 int cache_size = data->cache_size;
504
505                 gdbm_close(data->gdbm);
506
507                 /*
508                  *      Re-set the next time to clean the database.
509                  */
510                 data->last_reset = data->reset_time;
511                 find_next_reset(data,request->timestamp);
512
513                 /*
514                  *      Open a completely new database.
515                  */
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;
522                 }
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");
526         }
527
528
529         /*
530          *      Look for the key.  User-Name is special.  It means
531          *      The REAL username, after stripping.
532          */
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");
537                 return ret;
538         }
539
540         /*
541          *      Look for the check item
542          */
543         if ((dattr = dict_attrbyname(data->check_name)) == NULL) {
544                 return ret;
545         }
546         if ((check_vp= pairfind(request->config_items, dattr->attr)) == NULL) {
547                 DEBUG2("rlm_counter: Could not find Check item value pair");
548                 return ret;
549         }
550
551         key_datum.dptr = key_vp->strvalue;
552         key_datum.dsize = key_vp->length;
553         
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);
560         }
561
562         /*
563          * Check if check item > counter
564          */
565         res=check_vp->lvalue - counter;
566         if (res > 0) {
567                 /*
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
571                  *      that acceptable?
572                  */
573
574                 /*
575                  *      User is allowed, but set Session-Timeout.
576                  *      Stolen from main/auth.c
577                  */
578
579                 /*
580                  *      If we are near a reset then add the next
581                  *      limit, so that the user will not need to
582                  *      login again
583                  */
584                 if (data->reset_time && (
585                         res >= (data->reset_time - request->timestamp))) {
586                         res += check_vp->lvalue;
587                 }
588
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;
593                 } else {
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;
597                         }
598                         reply_item->lvalue = res;
599                         pairadd(&request->reply->vps, reply_item);
600                 }
601
602                 ret=RLM_MODULE_OK;
603
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);
608         }
609         else{
610                 char module_msg[MAX_STRING_LEN];
611                 VALUE_PAIR *module_msg_vp;
612
613                 /*
614                  * User is denied access, send back a reply message
615                 */
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);
619
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);  
623
624                 ret=RLM_MODULE_REJECT;
625
626                 DEBUG2("rlm_counter: Rejected user %s, check_item=%d, counter=%d",
627                                 key_vp->strvalue,check_vp->lvalue,counter);
628         }
629
630         return ret;
631 }
632
633 static int counter_detach(void *instance)
634 {
635         rlm_counter_t *data = (rlm_counter_t *) instance;
636
637         paircompare_unregister(data->dict_attr, counter_cmp);
638         gdbm_close(data->gdbm);
639         free(data->filename);
640         free(data->reset);
641         free(data->key_name);
642         free(data->count_attribute);
643         free(data->counter_name);
644
645         free(instance);
646         return 0;
647 }
648
649 /*
650  *      The module name should be the only globally exported symbol.
651  *      That is, everything else should be 'static'.
652  *
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.
657  */
658 module_t rlm_counter = {
659         "Counter",      
660         RLM_TYPE_THREAD_UNSAFE,         /* type */
661         NULL,                           /* initialization */
662         counter_instantiate,            /* instantiation */
663         {
664                 NULL,                   /* authentication */
665                 counter_authorize,      /* authorization */
666                 NULL,                   /* preaccounting */
667                 counter_accounting,     /* accounting */
668                 NULL                    /* checksimul */
669         },
670         counter_detach,                 /* detach */
671         NULL,                           /* destroy */
672 };