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