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