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