Remove paircompare_unregister() from modules
[freeradius.git] / src / modules / rlm_sqlcounter / rlm_sqlcounter.c
1 /*
2  *   This program is is free software; you can redistribute it and/or modify
3  *   it under the terms of the GNU General Public License, version 2 if the
4  *   License as published by the Free Software Foundation.
5  *
6  *   This program is distributed in the hope that it will be useful,
7  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
8  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9  *   GNU General Public License for more details.
10  *
11  *   You should have received a copy of the GNU General Public License
12  *   along with this program; if not, write to the Free Software
13  *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
14  */
15
16 /**
17  * $Id$
18  * @file rlm_sqlcounter.c
19  * @brief Tracks data usage and other counters using SQL.
20  *
21  * @copyright 2001,2006  The FreeRADIUS server project
22  * @copyright 2001  Alan DeKok <aland@ox.org>
23  */
24 RCSID("$Id$")
25
26 #include <freeradius-devel/radiusd.h>
27 #include <freeradius-devel/modules.h>
28 #include <freeradius-devel/rad_assert.h>
29
30 #include <ctype.h>
31
32 #define MAX_QUERY_LEN 1024
33
34 /*
35  *      Note: When your counter spans more than 1 period (ie 3 months
36  *      or 2 weeks), this module probably does NOT do what you want! It
37  *      calculates the range of dates to count across by first calculating
38  *      the End of the Current period and then subtracting the number of
39  *      periods you specify from that to determine the beginning of the
40  *      range.
41  *
42  *      For example, if you specify a 3 month counter and today is June 15th,
43  *      the end of the current period is June 30. Subtracting 3 months from
44  *      that gives April 1st. So, the counter will sum radacct entries from
45  *      April 1st to June 30. Then, next month, it will sum entries from
46  *      May 1st to July 31st.
47  *
48  *      To fix this behavior, we need to add some way of storing the Next
49  *      Reset Time.
50  */
51
52 /*
53  *      Define a structure for our module configuration.
54  *
55  *      These variables do not need to be in a structure, but it's
56  *      a lot cleaner to do so, and a pointer to the structure can
57  *      be used as the instance handle.
58  */
59 typedef struct rlm_sqlcounter_t {
60         char            *counter_name;  //!< Daily-Session-Time.
61         char            *check_name;    //!< Max-Daily-Session.
62         char            *reply_name;    //!< Session-Timeout.
63         char            *key_name;      //!< User-Name.
64         char            *sqlmod_inst;   //!< Instance of SQL module to use,
65                                         //!< usually just 'sql'.
66         char            *query;         //!< SQL query to retrieve current
67                                         //!< session time.
68         char            *reset;         //!< Daily, weekly, monthly,
69                                         //!< never or user defined.
70         time_t          reset_time;
71         time_t          last_reset;
72         const DICT_ATTR *key_attr;      //!< Attribute number for key field.
73         const DICT_ATTR *dict_attr;     //!< Attribute number for the counter.
74         const DICT_ATTR *reply_attr;    //!< Attribute number for the reply.
75 } rlm_sqlcounter_t;
76
77 /*
78  *      A mapping of configuration file names to internal variables.
79  *
80  *      Note that the string is dynamically allocated, so it MUST
81  *      be freed.  When the configuration file parse re-reads the string,
82  *      it free's the old one, and strdup's the new one, placing the pointer
83  *      to the strdup'd string into 'config.string'.  This gets around
84  *      buffer over-flows.
85  */
86 static const CONF_PARSER module_config[] = {
87   { "counter-name", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED, offsetof(rlm_sqlcounter_t,counter_name), NULL,  NULL },
88   { "check-name", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED, offsetof(rlm_sqlcounter_t,check_name), NULL, NULL },
89   { "reply-name", PW_TYPE_STRING_PTR | PW_TYPE_ATTRIBUTE, offsetof(rlm_sqlcounter_t,reply_name), NULL, "Session-Timeout" },
90   { "key", PW_TYPE_STRING_PTR | PW_TYPE_ATTRIBUTE, offsetof(rlm_sqlcounter_t,key_name), NULL, NULL },
91   { "sql-module-instance", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED, offsetof(rlm_sqlcounter_t,sqlmod_inst), NULL, NULL },
92   { "query", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED , offsetof(rlm_sqlcounter_t,query), NULL, NULL },
93   { "reset", PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED, offsetof(rlm_sqlcounter_t,reset), NULL,  NULL },
94   { NULL, -1, 0, NULL, NULL }
95 };
96
97 static int find_next_reset(rlm_sqlcounter_t *inst, time_t timeval)
98 {
99         int ret = 0;
100         size_t len;
101         unsigned int num = 1;
102         char last = '\0';
103         struct tm *tm, s_tm;
104         char sCurrentTime[40], sNextTime[40];
105
106         tm = localtime_r(&timeval, &s_tm);
107         len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
108         if (len == 0) *sCurrentTime = '\0';
109         tm->tm_sec = tm->tm_min = 0;
110
111         rad_assert(inst->reset != NULL);
112
113         if (isdigit((int) inst->reset[0])){
114                 len = strlen(inst->reset);
115                 if (len == 0)
116                         return -1;
117                 last = inst->reset[len - 1];
118                 if (!isalpha((int) last))
119                         last = 'd';
120                 num = atoi(inst->reset);
121                 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
122         }
123         if (strcmp(inst->reset, "hourly") == 0 || last == 'h') {
124                 /*
125                  *  Round up to the next nearest hour.
126                  */
127                 tm->tm_hour += num;
128                 inst->reset_time = mktime(tm);
129         } else if (strcmp(inst->reset, "daily") == 0 || last == 'd') {
130                 /*
131                  *  Round up to the next nearest day.
132                  */
133                 tm->tm_hour = 0;
134                 tm->tm_mday += num;
135                 inst->reset_time = mktime(tm);
136         } else if (strcmp(inst->reset, "weekly") == 0 || last == 'w') {
137                 /*
138                  *  Round up to the next nearest week.
139                  */
140                 tm->tm_hour = 0;
141                 tm->tm_mday += (7 - tm->tm_wday) +(7*(num-1));
142                 inst->reset_time = mktime(tm);
143         } else if (strcmp(inst->reset, "monthly") == 0 || last == 'm') {
144                 tm->tm_hour = 0;
145                 tm->tm_mday = 1;
146                 tm->tm_mon += num;
147                 inst->reset_time = mktime(tm);
148         } else if (strcmp(inst->reset, "never") == 0) {
149                 inst->reset_time = 0;
150         } else {
151                 return -1;
152         }
153
154         len = strftime(sNextTime, sizeof(sNextTime),"%Y-%m-%d %H:%M:%S",tm);
155         if (len == 0) *sNextTime = '\0';
156         DEBUG2("rlm_sqlcounter: Current Time: %li [%s], Next reset %li [%s]",
157                 timeval, sCurrentTime, inst->reset_time, sNextTime);
158
159         return ret;
160 }
161
162
163 /*  I don't believe that this routine handles Daylight Saving Time adjustments
164     properly.  Any suggestions?
165 */
166
167 static int find_prev_reset(rlm_sqlcounter_t *inst, time_t timeval)
168 {
169         int ret = 0;
170         size_t len;
171         unsigned int num = 1;
172         char last = '\0';
173         struct tm *tm, s_tm;
174         char sCurrentTime[40], sPrevTime[40];
175
176         tm = localtime_r(&timeval, &s_tm);
177         len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
178         if (len == 0) *sCurrentTime = '\0';
179         tm->tm_sec = tm->tm_min = 0;
180
181         rad_assert(inst->reset != NULL);
182
183         if (isdigit((int) inst->reset[0])){
184                 len = strlen(inst->reset);
185                 if (len == 0)
186                         return -1;
187                 last = inst->reset[len - 1];
188                 if (!isalpha((int) last))
189                         last = 'd';
190                 num = atoi(inst->reset);
191                 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
192         }
193         if (strcmp(inst->reset, "hourly") == 0 || last == 'h') {
194                 /*
195                  *  Round down to the prev nearest hour.
196                  */
197                 tm->tm_hour -= num - 1;
198                 inst->last_reset = mktime(tm);
199         } else if (strcmp(inst->reset, "daily") == 0 || last == 'd') {
200                 /*
201                  *  Round down to the prev nearest day.
202                  */
203                 tm->tm_hour = 0;
204                 tm->tm_mday -= num - 1;
205                 inst->last_reset = mktime(tm);
206         } else if (strcmp(inst->reset, "weekly") == 0 || last == 'w') {
207                 /*
208                  *  Round down to the prev nearest week.
209                  */
210                 tm->tm_hour = 0;
211                 tm->tm_mday -= (7 - tm->tm_wday) +(7*(num-1));
212                 inst->last_reset = mktime(tm);
213         } else if (strcmp(inst->reset, "monthly") == 0 || last == 'm') {
214                 tm->tm_hour = 0;
215                 tm->tm_mday = 1;
216                 tm->tm_mon -= num - 1;
217                 inst->last_reset = mktime(tm);
218         } else if (strcmp(inst->reset, "never") == 0) {
219                 inst->reset_time = 0;
220         } else {
221                 return -1;
222         }
223         len = strftime(sPrevTime, sizeof(sPrevTime), "%Y-%m-%d %H:%M:%S", tm);
224         if (len == 0) *sPrevTime = '\0';
225         DEBUG2("rlm_sqlcounter: Current Time: %li [%s], Prev reset %li [%s]",
226                timeval, sCurrentTime, inst->last_reset, sPrevTime);
227
228         return ret;
229 }
230
231
232 /*
233  *      Replace %<whatever> in a string.
234  *
235  *      %b      last_reset
236  *      %e      reset_time
237  *      %k      key_name
238  *      %S      sqlmod_inst
239  *
240  */
241
242 static int sqlcounter_expand(char *out, int outlen, const char *fmt, rlm_sqlcounter_t *inst)
243 {
244         int c,freespace;
245         const char *p;
246         char *q;
247         char tmpdt[40]; /* For temporary storing of dates */
248
249         q = out;
250         for (p = fmt; *p ; p++) {
251         /* Calculate freespace in output */
252         freespace = outlen - (q - out);
253                 if (freespace <= 1)
254                         break;
255                 c = *p;
256                 if ((c != '%') && (c != '\\')) {
257                         *q++ = *p;
258                         continue;
259                 }
260                 if (*++p == '\0') break;
261                 if (c == '\\') switch(*p) {
262                         case '\\':
263                                 *q++ = *p;
264                                 break;
265                         case 't':
266                                 *q++ = '\t';
267                                 break;
268                         case 'n':
269                                 *q++ = '\n';
270                                 break;
271                         default:
272                                 *q++ = c;
273                                 *q++ = *p;
274                                 break;
275
276                 } else if (c == '%') switch(*p) {
277
278                         case '%':
279                                 *q++ = *p;
280                                 break;
281                         case 'b': /* last_reset */
282                                 snprintf(tmpdt, sizeof(tmpdt), "%lu", inst->last_reset);
283                                 strlcpy(q, tmpdt, freespace);
284                                 q += strlen(q);
285                                 break;
286                         case 'e': /* reset_time */
287                                 snprintf(tmpdt, sizeof(tmpdt), "%lu", inst->reset_time);
288                                 strlcpy(q, tmpdt, freespace);
289                                 q += strlen(q);
290                                 break;
291                         case 'k': /* Key Name */
292                                 DEBUG2W("Please replace '%%k' with '${key}'");
293                                 strlcpy(q, inst->key_name, freespace);
294                                 q += strlen(q);
295                                 break;
296                         default:
297                                 *q++ = '%';
298                                 *q++ = *p;
299                                 break;
300                 }
301         }
302         *q = '\0';
303
304         DEBUG2("sqlcounter_expand:  '%s'", out);
305
306         return strlen(out);
307 }
308
309
310 /*
311  *      See if the counter matches.
312  */
313 static int sqlcounter_cmp(void *instance, REQUEST *req,
314                           UNUSED VALUE_PAIR *request, VALUE_PAIR *check,
315                           UNUSED VALUE_PAIR *check_pairs, UNUSED VALUE_PAIR **reply_pairs)
316 {
317         rlm_sqlcounter_t *inst = instance;
318         int counter;
319         char querystr[MAX_QUERY_LEN];
320         char sqlxlat[MAX_QUERY_LEN];
321
322         /* first, expand %k, %b and %e in query */
323         sqlcounter_expand(querystr, MAX_QUERY_LEN, inst->query, inst);
324
325         /* third, wrap query with sql module call & expand */
326         snprintf(sqlxlat, sizeof(sqlxlat), "%%{%s:%s}", inst->sqlmod_inst, querystr);
327
328         /* Finally, xlat resulting SQL query */
329         radius_xlat(querystr, MAX_QUERY_LEN, sqlxlat, req, NULL, NULL);
330
331         counter = atoi(querystr);
332
333         return counter - check->vp_integer;
334 }
335
336
337 /*
338  *      Do any per-module initialization that is separate to each
339  *      configured instance of the module.  e.g. set up connections
340  *      to external databases, read configuration files, set up
341  *      dictionary entries, etc.
342  *
343  *      If configuration information is given in the config section
344  *      that must be referenced in later calls, store a handle to it
345  *      in *instance otherwise put a null pointer there.
346  */
347 static int mod_instantiate(CONF_SECTION *conf, void *instance)
348 {
349         rlm_sqlcounter_t *inst = instance;
350         const DICT_ATTR *dattr;
351         ATTR_FLAGS flags;
352         time_t now;
353
354         rad_assert(inst->query && *inst->query);
355
356         dattr = dict_attrbyname(inst->key_name);
357         rad_assert(dattr != NULL);
358         if (dattr->vendor != 0) {
359                 cf_log_err_cs(conf, "Configuration item 'key' cannot be a VSA");
360                 return -1;
361         }
362         inst->key_attr = dattr;
363
364         dattr = dict_attrbyname(inst->reply_name);
365         rad_assert(dattr != NULL);
366         if (dattr->vendor != 0) {
367                 cf_log_err_cs(conf, "Configuration item 'reply-name' cannot be a VSA");
368                 return -1;
369         }
370         inst->reply_attr = dattr;
371
372         DEBUG2("rlm_sqlcounter: Reply attribute %s is number %d",
373                        inst->reply_name, dattr->attr);
374
375         /*
376          *  Create a new attribute for the counter.
377          */
378         rad_assert(inst->counter_name && *inst->counter_name);
379         memset(&flags, 0, sizeof(flags));
380         dict_addattr(inst->counter_name, -1, 0, PW_TYPE_INTEGER, flags);
381         dattr = dict_attrbyname(inst->counter_name);
382         if (!dattr) {
383                 cf_log_err_cs(conf, "Failed to create counter attribute %s",
384                                 inst->counter_name);
385                 return -1;
386         }
387         if (dattr->vendor != 0) {
388                 cf_log_err_cs(conf, "Counter attribute must not be a VSA");
389                 return -1;
390         }
391         inst->dict_attr = dattr;
392
393         /*
394          * Create a new attribute for the check item.
395          */
396         rad_assert(inst->check_name && *inst->check_name);
397         dict_addattr(inst->check_name, 0, PW_TYPE_INTEGER, -1, flags);
398         dattr = dict_attrbyname(inst->check_name);
399         if (!dattr) {
400                 cf_log_err_cs(conf, "Failed to create check attribute %s",
401                                 inst->check_name);
402                 return -1;
403         }
404         DEBUG2("rlm_sqlcounter: Check attribute %s is number %d",
405                         inst->check_name, dattr->attr);
406
407         now = time(NULL);
408         inst->reset_time = 0;
409
410         if (find_next_reset(inst,now) == -1) {
411                 cf_log_err_cs(conf, "Invalid reset '%s'", inst->reset);
412                 return -1;
413         }
414
415         /*
416          *  Discover the beginning of the current time period.
417          */
418         inst->last_reset = 0;
419
420         if (find_prev_reset(inst, now) < 0) {
421                 cf_log_err_cs(conf, "Invalid reset '%s'", inst->reset);
422                 return -1;
423         }
424
425         /*
426          *      Register the counter comparison operation.
427          */
428         paircompare_register(inst->dict_attr->attr, 0, sqlcounter_cmp, inst);
429
430         return 0;
431 }
432
433 /*
434  *      Find the named user in this modules database.  Create the set
435  *      of attribute-value pairs to check and reply with for this user
436  *      from the database. The authentication code only needs to check
437  *      the password, the rest is done here.
438  */
439 static rlm_rcode_t mod_authorize(UNUSED void *instance, UNUSED REQUEST *request)
440 {
441         rlm_sqlcounter_t *inst = instance;
442         int rcode = RLM_MODULE_NOOP;
443         unsigned int counter;
444         const DICT_ATTR *dattr;
445         VALUE_PAIR *key_vp, *check_vp;
446         VALUE_PAIR *reply_item;
447         char msg[128];
448         char querystr[MAX_QUERY_LEN];
449         char sqlxlat[MAX_QUERY_LEN];
450
451         /*
452          *      Before doing anything else, see if we have to reset
453          *      the counters.
454          */
455         if (inst->reset_time && (inst->reset_time <= request->timestamp)) {
456
457                 /*
458                  *      Re-set the next time and prev_time for this counters range
459                  */
460                 inst->last_reset = inst->reset_time;
461                 find_next_reset(inst,request->timestamp);
462         }
463
464
465         /*
466          *      Look for the key.  User-Name is special.  It means
467          *      The REAL username, after stripping.
468          */
469         DEBUG2("rlm_sqlcounter: Entering module authorize code");
470         key_vp = ((inst->key_attr->vendor == 0) && (inst->key_attr->attr == PW_USER_NAME)) ? request->username : pairfind(request->packet->vps, inst->key_attr->attr, inst->key_attr->vendor, TAG_ANY);
471         if (!key_vp) {
472                 DEBUG2("rlm_sqlcounter: Could not find Key value pair");
473                 return rcode;
474         }
475
476         /*
477          *      Look for the check item
478          */
479         if ((dattr = dict_attrbyname(inst->check_name)) == NULL) {
480                 return rcode;
481         }
482         /* DEBUG2("rlm_sqlcounter: Found Check item attribute %d", dattr->attr); */
483         if ((check_vp= pairfind(request->config_items, dattr->attr, dattr->vendor, TAG_ANY)) == NULL) {
484                 DEBUG2("rlm_sqlcounter: Could not find Check item value pair");
485                 return rcode;
486         }
487
488         /* first, expand %k, %b and %e in query */
489         sqlcounter_expand(querystr, MAX_QUERY_LEN, inst->query, inst);
490
491         /* next, wrap query with sql module & expand */
492         snprintf(sqlxlat, sizeof(sqlxlat), "%%{%s:%s}", inst->sqlmod_inst, querystr);
493
494         /* Finally, xlat resulting SQL query */
495         radius_xlat(querystr, MAX_QUERY_LEN, sqlxlat, request, NULL, NULL);
496
497         if (sscanf(querystr, "%u", &counter) != 1) {
498                 DEBUG2("rlm_sqlcounter: No integer found in string \"%s\"",
499                        querystr);
500                 return RLM_MODULE_NOOP;
501         }
502
503         /*
504          * Check if check item > counter
505          */
506         if (check_vp->vp_integer > counter) {
507                 unsigned int res = check_vp->vp_integer - counter;
508
509                 DEBUG2("rlm_sqlcounter: Check item is greater than query result");
510                 /*
511                  *      We are assuming that simultaneous-use=1. But
512                  *      even if that does not happen then our user
513                  *      could login at max for 2*max-usage-time Is
514                  *      that acceptable?
515                  */
516
517                 /*
518                  *      If we are near a reset then add the next
519                  *      limit, so that the user will not need to login
520                  *      again.  Do this only for Session-Timeout.
521                  */
522                 if ((inst->reply_attr->attr == PW_SESSION_TIMEOUT) &&
523                     inst->reset_time &&
524                     (res >= (inst->reset_time - request->timestamp))) {
525                         res = inst->reset_time - request->timestamp;
526                         res += check_vp->vp_integer;
527                 }
528
529                 /*
530                  *      Limit the reply attribute to the minimum of
531                  *      the existing value, or this new one.
532                  */
533                 reply_item = pairfind(request->reply->vps, inst->reply_attr->attr, inst->reply_attr->vendor, TAG_ANY);
534                 if (reply_item) {
535                         if (reply_item->vp_integer > res)
536                                 reply_item->vp_integer = res;
537
538                 } else {
539                         reply_item = radius_paircreate(request,
540                                                        &request->reply->vps,
541                                                        inst->reply_attr->attr,
542                                                        inst->reply_attr->vendor);
543                         reply_item->vp_integer = res;
544                 }
545
546                 rcode = RLM_MODULE_OK;
547
548                 DEBUG2("rlm_sqlcounter: Authorized user %s, check_item=%u, counter=%u",
549                                 key_vp->vp_strvalue,check_vp->vp_integer,counter);
550                 DEBUG2("rlm_sqlcounter: Sent Reply-Item for user %s, Type=%s, value=%u",
551                                 key_vp->vp_strvalue,inst->reply_name,reply_item->vp_integer);
552         }
553         else{
554                 DEBUG2("rlm_sqlcounter: (Check item - counter) is less than zero");
555
556                 /*
557                  * User is denied access, send back a reply message
558                  */
559                 snprintf(msg, sizeof(msg), "Your maximum %s usage time has been reached", inst->reset);
560                 pairmake_reply("Reply-Message", msg, T_OP_EQ);
561
562                 RDEBUGE("Maximum %s usage time reached",
563                                    inst->reset);
564                 rcode = RLM_MODULE_REJECT;
565
566                 DEBUG2("rlm_sqlcounter: Rejected user %s, check_item=%u, counter=%u",
567                                 key_vp->vp_strvalue,check_vp->vp_integer,counter);
568         }
569
570         return rcode;
571 }
572
573 /*
574  *      The module name should be the only globally exported symbol.
575  *      That is, everything else should be 'static'.
576  *
577  *      If the module needs to temporarily modify it's instantiation
578  *      data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
579  *      The server will then take care of ensuring that the module
580  *      is single-threaded.
581  */
582 module_t rlm_sqlcounter = {
583         RLM_MODULE_INIT,
584         "SQL Counter",
585         RLM_TYPE_THREAD_SAFE,           /* type */
586         sizeof(rlm_sqlcounter_t),
587         module_config,
588         mod_instantiate,                /* instantiation */
589         NULL,                           /* detach */
590         {
591                 NULL,                   /* authentication */
592                 mod_authorize,  /* authorization */
593                 NULL,                   /* preaccounting */
594                 NULL,                   /* accounting */
595                 NULL,                   /* checksimul */
596                 NULL,                   /* pre-proxy */
597                 NULL,                   /* post-proxy */
598                 NULL                    /* post-auth */
599         },
600 };
601