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