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