Fixup rlm_sqlcounter warnings
[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 const      *counter_name;  //!< Daily-Session-Time.
61         char const      *limit_name;    //!< Max-Daily-Session.
62         char const      *reply_name;    //!< Session-Timeout.
63         char const      *key_name;      //!< User-Name.
64         char const      *sqlmod_inst;   //!< Instance of SQL module to use,
65                                         //!< usually just 'sql'.
66         char const      *query;         //!< SQL query to retrieve current
67                                         //!< session time.
68         char const      *reset;         //!< Daily, weekly, monthly,
69                                         //!< never or user defined.
70         time_t          reset_time;
71         time_t          last_reset;
72         DICT_ATTR const *key_attr;      //!< Attribute number for key field.
73         DICT_ATTR const *dict_attr;     //!< Attribute number for the counter.
74         DICT_ATTR const *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         { "sql-module-instance", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_DEPRECATED, rlm_sqlcounter_t, sqlmod_inst), NULL },
88         { "sql_module_instance", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_sqlcounter_t, sqlmod_inst), NULL },
89
90         { "key", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_ATTRIBUTE, rlm_sqlcounter_t, key_name), NULL },
91         { "query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_sqlcounter_t, query), NULL },
92         { "reset", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_sqlcounter_t, reset), NULL },
93
94         { "counter-name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_DEPRECATED, rlm_sqlcounter_t, counter_name), NULL },
95         { "counter_name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_sqlcounter_t, counter_name), NULL },
96
97         { "check-name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_DEPRECATED, rlm_sqlcounter_t, limit_name), NULL },
98         { "check_name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_sqlcounter_t, limit_name), NULL },
99
100         { "reply-name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_DEPRECATED, rlm_sqlcounter_t, reply_name), NULL },
101         { "reply_name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_ATTRIBUTE, rlm_sqlcounter_t, reply_name), "Session-Timeout" },
102
103         { NULL, -1, 0, NULL, NULL }
104 };
105
106 static int find_next_reset(rlm_sqlcounter_t *inst, time_t timeval)
107 {
108         int ret = 0;
109         size_t len;
110         unsigned int num = 1;
111         char last = '\0';
112         struct tm *tm, s_tm;
113         char sCurrentTime[40], sNextTime[40];
114
115         tm = localtime_r(&timeval, &s_tm);
116         len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
117         if (len == 0) *sCurrentTime = '\0';
118         tm->tm_sec = tm->tm_min = 0;
119
120         rad_assert(inst->reset != NULL);
121
122         if (isdigit((int) inst->reset[0])){
123                 len = strlen(inst->reset);
124                 if (len == 0)
125                         return -1;
126                 last = inst->reset[len - 1];
127                 if (!isalpha((int) last))
128                         last = 'd';
129                 num = atoi(inst->reset);
130                 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
131         }
132         if (strcmp(inst->reset, "hourly") == 0 || last == 'h') {
133                 /*
134                  *  Round up to the next nearest hour.
135                  */
136                 tm->tm_hour += num;
137                 inst->reset_time = mktime(tm);
138         } else if (strcmp(inst->reset, "daily") == 0 || last == 'd') {
139                 /*
140                  *  Round up to the next nearest day.
141                  */
142                 tm->tm_hour = 0;
143                 tm->tm_mday += num;
144                 inst->reset_time = mktime(tm);
145         } else if (strcmp(inst->reset, "weekly") == 0 || last == 'w') {
146                 /*
147                  *  Round up to the next nearest week.
148                  */
149                 tm->tm_hour = 0;
150                 tm->tm_mday += (7 - tm->tm_wday) +(7*(num-1));
151                 inst->reset_time = mktime(tm);
152         } else if (strcmp(inst->reset, "monthly") == 0 || last == 'm') {
153                 tm->tm_hour = 0;
154                 tm->tm_mday = 1;
155                 tm->tm_mon += num;
156                 inst->reset_time = mktime(tm);
157         } else if (strcmp(inst->reset, "never") == 0) {
158                 inst->reset_time = 0;
159         } else {
160                 return -1;
161         }
162
163         len = strftime(sNextTime, sizeof(sNextTime),"%Y-%m-%d %H:%M:%S",tm);
164         if (len == 0) *sNextTime = '\0';
165         DEBUG2("rlm_sqlcounter: Current Time: %" PRId64 " [%s], Next reset %" PRId64 " [%s]",
166                (int64_t) timeval, sCurrentTime, (int64_t) inst->reset_time, sNextTime);
167
168         return ret;
169 }
170
171
172 /*  I don't believe that this routine handles Daylight Saving Time adjustments
173     properly.  Any suggestions?
174 */
175
176 static int find_prev_reset(rlm_sqlcounter_t *inst, time_t timeval)
177 {
178         int ret = 0;
179         size_t len;
180         unsigned int num = 1;
181         char last = '\0';
182         struct tm *tm, s_tm;
183         char sCurrentTime[40], sPrevTime[40];
184
185         tm = localtime_r(&timeval, &s_tm);
186         len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
187         if (len == 0) *sCurrentTime = '\0';
188         tm->tm_sec = tm->tm_min = 0;
189
190         rad_assert(inst->reset != NULL);
191
192         if (isdigit((int) inst->reset[0])){
193                 len = strlen(inst->reset);
194                 if (len == 0)
195                         return -1;
196                 last = inst->reset[len - 1];
197                 if (!isalpha((int) last))
198                         last = 'd';
199                 num = atoi(inst->reset);
200                 DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
201         }
202         if (strcmp(inst->reset, "hourly") == 0 || last == 'h') {
203                 /*
204                  *  Round down to the prev nearest hour.
205                  */
206                 tm->tm_hour -= num - 1;
207                 inst->last_reset = mktime(tm);
208         } else if (strcmp(inst->reset, "daily") == 0 || last == 'd') {
209                 /*
210                  *  Round down to the prev nearest day.
211                  */
212                 tm->tm_hour = 0;
213                 tm->tm_mday -= num - 1;
214                 inst->last_reset = mktime(tm);
215         } else if (strcmp(inst->reset, "weekly") == 0 || last == 'w') {
216                 /*
217                  *  Round down to the prev nearest week.
218                  */
219                 tm->tm_hour = 0;
220                 tm->tm_mday -= (7 - tm->tm_wday) +(7*(num-1));
221                 inst->last_reset = mktime(tm);
222         } else if (strcmp(inst->reset, "monthly") == 0 || last == 'm') {
223                 tm->tm_hour = 0;
224                 tm->tm_mday = 1;
225                 tm->tm_mon -= num - 1;
226                 inst->last_reset = mktime(tm);
227         } else if (strcmp(inst->reset, "never") == 0) {
228                 inst->reset_time = 0;
229         } else {
230                 return -1;
231         }
232         len = strftime(sPrevTime, sizeof(sPrevTime), "%Y-%m-%d %H:%M:%S", tm);
233         if (len == 0) *sPrevTime = '\0';
234         DEBUG2("rlm_sqlcounter: Current Time: %" PRId64 " [%s], Prev reset %" PRId64 " [%s]",
235                (int64_t) timeval, sCurrentTime, (int64_t) inst->last_reset, sPrevTime);
236
237         return ret;
238 }
239
240
241 /*
242  *      Replace %<whatever> in a string.
243  *
244  *      %b      last_reset
245  *      %e      reset_time
246  *      %k      key_name
247  *      %S      sqlmod_inst
248  *
249  */
250
251 static size_t sqlcounter_expand(char *out, int outlen, char const *fmt, rlm_sqlcounter_t *inst)
252 {
253         int c, freespace;
254         char const *p;
255         char *q;
256         char tmpdt[40]; /* For temporary storing of dates */
257
258         q = out;
259         for (p = fmt; *p ; p++) {
260                 /* Calculate freespace in output */
261                 freespace = outlen - (q - out);
262                 if (freespace <= 1) {
263                         return -1;
264                 }
265                 c = *p;
266                 if (c != '%') {
267                         *q++ = *p;
268                         continue;
269                 }
270                 if (*++p == '\0') break;
271                 if (c == '%') switch(*p) {
272                         case 'b': /* last_reset */
273                                 snprintf(tmpdt, sizeof(tmpdt), "%" PRId64, (int64_t) inst->last_reset);
274                                 strlcpy(q, tmpdt, freespace);
275                                 q += strlen(q);
276                                 break;
277                         case 'e': /* reset_time */
278                                 snprintf(tmpdt, sizeof(tmpdt), "%" PRId64, (int64_t) inst->reset_time);
279                                 strlcpy(q, tmpdt, freespace);
280                                 q += strlen(q);
281                                 break;
282                         case 'k': /* Key Name */
283                                 WARN("Please replace '%%k' with '${key}'");
284                                 strlcpy(q, inst->key_name, freespace);
285                                 q += strlen(q);
286                                 break;
287                         default:
288                                 *q++ = '%';
289                                 *q++ = *p;
290                                 break;
291                 }
292         }
293         *q = '\0';
294
295         DEBUG2("sqlcounter_expand: '%s'", out);
296
297         return strlen(out);
298 }
299
300
301 /*
302  *      See if the counter matches.
303  */
304 static int sqlcounter_cmp(void *instance, REQUEST *request, UNUSED VALUE_PAIR *req , VALUE_PAIR *check,
305                           UNUSED VALUE_PAIR *check_pairs, UNUSED VALUE_PAIR **reply_pairs)
306 {
307         rlm_sqlcounter_t *inst = instance;
308         uint64_t counter;
309
310         char query[MAX_QUERY_LEN], subst[MAX_QUERY_LEN];
311         char *expanded = NULL;
312         size_t len;
313
314         /* First, expand %k, %b and %e in query */
315         if (sqlcounter_expand(subst, sizeof(subst), inst->query, inst) <= 0) {
316                 REDEBUG("Insufficient query buffer space");
317
318                 return RLM_MODULE_FAIL;
319         }
320
321         /* Then combine that with the name of the module were using to do the query */
322         len = snprintf(query, sizeof(query), "%%{%s:%s}", inst->sqlmod_inst, subst);
323         if (len >= sizeof(query) - 1) {
324                 REDEBUG("Insufficient query buffer space");
325
326                 return RLM_MODULE_FAIL;
327         }
328
329         /* Finally, xlat resulting SQL query */
330         if (radius_axlat(&expanded, request, query, NULL, NULL) < 0) {
331                 return RLM_MODULE_FAIL;
332         }
333
334         if (sscanf(expanded, "%" PRIu64, &counter) != 1) {
335                 RDEBUG2("No integer found in string \"%s\"", expanded);
336         }
337         talloc_free(expanded);
338
339         if (counter < check->vp_integer64) {
340                 return -1;
341         }
342         if (counter > check->vp_integer64) {
343                 return 1;
344         }
345         return 0;
346 }
347
348
349 /*
350  *      Do any per-module initialization that is separate to each
351  *      configured instance of the module.  e.g. set up connections
352  *      to external databases, read configuration files, set up
353  *      dictionary entries, etc.
354  *
355  *      If configuration information is given in the config section
356  *      that must be referenced in later calls, store a handle to it
357  *      in *instance otherwise put a null pointer there.
358  */
359 static int mod_instantiate(CONF_SECTION *conf, void *instance)
360 {
361         rlm_sqlcounter_t *inst = instance;
362         DICT_ATTR const *da;
363         ATTR_FLAGS flags;
364         time_t now;
365
366         rad_assert(inst->query && *inst->query);
367
368         da = dict_attrbyname(inst->key_name);
369         if (!da) {
370                 cf_log_err_cs(conf, "Invalid attribute '%s'", inst->key_name);
371                 return -1;
372         }
373         inst->key_attr = da;
374
375         da = dict_attrbyname(inst->reply_name);
376         if (!da) {
377                 cf_log_err_cs(conf, "Invalid attribute '%s'", inst->reply_name);
378                 return -1;
379         }
380         inst->reply_attr = da;
381
382         /*
383          *  Create a new attribute for the counter.
384          */
385         rad_assert(inst->counter_name && *inst->counter_name);
386         memset(&flags, 0, sizeof(flags));
387         dict_addattr(inst->counter_name, -1, 0, PW_TYPE_INTEGER64, flags);
388         da = dict_attrbyname(inst->counter_name);
389         if (!da) {
390                 cf_log_err_cs(conf, "Failed to create counter attribute %s", inst->counter_name);
391                 return -1;
392         }
393         inst->dict_attr = da;
394
395         /*
396          *  Create a new attribute for the check item.
397          */
398         rad_assert(inst->limit_name && *inst->limit_name);
399         dict_addattr(inst->limit_name, -1, 0, PW_TYPE_INTEGER64, flags);
400         da = dict_attrbyname(inst->limit_name);
401         if (!da) {
402                 cf_log_err_cs(conf, "Failed to create check attribute %s", inst->limit_name);
403                 return -1;
404         }
405
406         now = time(NULL);
407         inst->reset_time = 0;
408
409         if (find_next_reset(inst,now) == -1) {
410                 cf_log_err_cs(conf, "Invalid reset '%s'", inst->reset);
411                 return -1;
412         }
413
414         /*
415          *  Discover the beginning of the current time period.
416          */
417         inst->last_reset = 0;
418
419         if (find_prev_reset(inst, now) < 0) {
420                 cf_log_err_cs(conf, "Invalid reset '%s'", inst->reset);
421                 return -1;
422         }
423
424         /*
425          *  Register the counter comparison operation.
426          */
427         paircompare_register(inst->dict_attr, NULL, true, sqlcounter_cmp, inst);
428
429         return 0;
430 }
431
432 /*
433  *      Find the named user in this modules database.  Create the set
434  *      of attribute-value pairs to check and reply with for this user
435  *      from the database. The authentication code only needs to check
436  *      the password, the rest is done here.
437  */
438 static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, REQUEST *request)
439 {
440         rlm_sqlcounter_t *inst = instance;
441         int rcode = RLM_MODULE_NOOP;
442         uint64_t counter, res;
443         DICT_ATTR const *da;
444         VALUE_PAIR *key_vp, *limit;
445         VALUE_PAIR *reply_item;
446         char msg[128];
447
448         char query[MAX_QUERY_LEN], subst[MAX_QUERY_LEN];
449         char *expanded = NULL;
450
451         size_t len;
452
453         /*
454          *      Before doing anything else, see if we have to reset
455          *      the counters.
456          */
457         if (inst->reset_time && (inst->reset_time <= request->timestamp)) {
458                 /*
459                  *      Re-set the next time and prev_time for this counters range
460                  */
461                 inst->last_reset = inst->reset_time;
462                 find_next_reset(inst,request->timestamp);
463         }
464
465         /*
466          *      Look for the key.  User-Name is special.  It means
467          *      The REAL username, after stripping.
468          */
469         if ((inst->key_attr->vendor == 0) && (inst->key_attr->attr == PW_USER_NAME)) {
470                 key_vp = request->username;
471         } else {
472                 key_vp = pairfind(request->packet->vps, inst->key_attr->attr, inst->key_attr->vendor, TAG_ANY);
473         }
474         if (!key_vp) {
475                 RWDEBUG2("Couldn't find key attribute, request:%s, doing nothing...", inst->key_attr->name);
476                 return rcode;
477         }
478
479         /*
480          *      Look for the check item
481          */
482         if ((da = dict_attrbyname(inst->limit_name)) == NULL) {
483                 return rcode;
484         }
485
486         limit = pairfind(request->config_items, da->attr, da->vendor, TAG_ANY);
487         if (limit == NULL) {
488                 /* Yes this really is 'check' as distinct from control */
489                 RWDEBUG2("Couldn't find check attribute, control:%s, doing nothing...", inst->limit_name);
490                 return rcode;
491         }
492
493         /* First, expand %k, %b and %e in query */
494         if (sqlcounter_expand(subst, sizeof(subst), inst->query, inst) <= 0) {
495                 REDEBUG("Insufficient query buffer space");
496
497                 return RLM_MODULE_FAIL;
498         }
499
500         /* Then combine that with the name of the module were using to do the query */
501         len = snprintf(query, sizeof(query), "%%{%s:%s}", inst->sqlmod_inst, subst);
502         if (len >= (sizeof(query) - 1)) {
503                 REDEBUG("Insufficient query buffer space");
504
505                 return RLM_MODULE_FAIL;
506         }
507
508         /* Finally, xlat resulting SQL query */
509         if (radius_axlat(&expanded, request, query, NULL, NULL) < 0) {
510                 return RLM_MODULE_FAIL;
511         }
512         talloc_free(expanded);
513
514         if (sscanf(expanded, "%" PRIu64, &counter) != 1) {
515                 RDEBUG2("No integer found in result string \"%s\".  May be first session, setting counter to 0",
516                         expanded);
517                 counter = 0;
518         }
519
520         /*
521          *      Check if check item > counter
522          */
523         if (limit->vp_integer64 <= counter) {
524                 /* User is denied access, send back a reply message */
525                 snprintf(msg, sizeof(msg), "Your maximum %s usage time has been reached", inst->reset);
526                 pairmake_reply("Reply-Message", msg, T_OP_EQ);
527
528                 REDEBUG2("Maximum %s usage time reached", inst->reset);
529                 REDEBUG2("Rejecting user, control:%s value (%" PRIu64 ") is less than counter value (%" PRIu64 ")",
530                          inst->limit_name, limit->vp_integer64, counter);
531
532                 return RLM_MODULE_REJECT;
533         }
534
535         res = limit->vp_integer64 - counter;
536         RDEBUG2("Allowing user, control:%s value (%" PRIu64 ") is greater than counter value (%" PRIu64 ")",
537                 inst->limit_name, limit->vp_integer64, counter);
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          *      If we are near a reset then add the next
547          *      limit, so that the user will not need to login
548          *      again.  Do this only for Session-Timeout.
549          */
550         if (((inst->reply_attr->vendor == 0) && (inst->reply_attr->attr == PW_SESSION_TIMEOUT)) &&
551             inst->reset_time && ((int) res >= (inst->reset_time - request->timestamp))) {
552                 res = (inst->reset_time - request->timestamp);
553                 res += limit->vp_integer;
554         }
555
556         /*
557          *      Limit the reply attribute to the minimum of the existing value, or this new one.
558          */
559         reply_item = pairfind(request->reply->vps, inst->reply_attr->attr, inst->reply_attr->vendor, TAG_ANY);
560         if (reply_item) {
561                 if (reply_item->vp_integer64 <= res) {
562                         RDEBUG2("Leaving existing reply:%s value of %" PRIu64, inst->reply_attr->name,
563                                 reply_item->vp_integer64);
564
565                         return RLM_MODULE_OK;
566                 }
567         } else {
568                 reply_item = radius_paircreate(request->reply, &request->reply->vps, inst->reply_attr->attr,
569                                                inst->reply_attr->vendor);
570         }
571         reply_item->vp_integer64 = res;
572
573         RDEBUG2("Setting reply:%s value to %" PRIu64, inst->reply_name, reply_item->vp_integer64);
574
575         return RLM_MODULE_OK;
576 }
577
578 /*
579  *      The module name should be the only globally exported symbol.
580  *      That is, everything else should be 'static'.
581  *
582  *      If the module needs to temporarily modify it's instantiation
583  *      data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
584  *      The server will then take care of ensuring that the module
585  *      is single-threaded.
586  */
587 module_t rlm_sqlcounter = {
588         RLM_MODULE_INIT,
589         "rlm_sqlcounter",
590         RLM_TYPE_THREAD_SAFE,           /* type */
591         sizeof(rlm_sqlcounter_t),
592         module_config,
593         mod_instantiate,                /* instantiation */
594         NULL,                           /* detach */
595         {
596                 NULL,                   /* authentication */
597                 mod_authorize,          /* authorization */
598                 NULL,                   /* preaccounting */
599                 NULL,                   /* accounting */
600                 NULL,                   /* checksimul */
601                 NULL,                   /* pre-proxy */
602                 NULL,                   /* post-proxy */
603                 NULL                    /* post-auth */
604         },
605 };
606