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