Merge branch 'sam'
[freeradius.git] / src / modules / rlm_sql_log / rlm_sql_log.c
1 /*
2  *  rlm_sql_log.c       Append the SQL queries in a log file which
3  *                      is read later by the radsqlrelay program
4  *
5  *  Version:    $Id$
6  *
7  *  Author:     Nicolas Baradakis <nicolas.baradakis@cegetel.net>
8  *
9  *  Copyright (C) 2005 Cegetel
10  *  Copyright 2006 The FreeRADIUS server project
11  *
12  *  This program is free software; you can redistribute it and/or
13  *  modify it under the terms of the GNU General Public License
14  *  as published by the Free Software Foundation; either version 2
15  *  of the License, or (at your option) any later version.
16  *
17  *  This program is distributed in the hope that it will be useful,
18  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
19  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20  *  GNU General Public License for more details.
21  *
22  *  You should have received a copy of the GNU General Public License
23  *  along with this program; if not, write to the Free Software
24  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
25  */
26
27 #include <freeradius-devel/ident.h>
28 RCSID("$Id$")
29
30 #include <freeradius-devel/radiusd.h>
31 #include <freeradius-devel/modules.h>
32 #include <freeradius-devel/rad_assert.h>
33
34 #include <fcntl.h>
35 #include <sys/stat.h>
36
37 static int sql_log_instantiate(CONF_SECTION *conf, void **instance);
38 static int sql_log_detach(void *instance);
39 static int sql_log_accounting(void *instance, REQUEST *request);
40 static int sql_log_postauth(void *instance, REQUEST *request);
41
42 #define MAX_QUERY_LEN 4096
43
44 /*
45  *      Define a structure for our module configuration.
46  */
47 typedef struct rlm_sql_log_t {
48         char            *path;
49         char            *postauth_query;
50         char            *sql_user_name;
51         int             utf8;   
52         char            *allowed_chars;
53         CONF_SECTION    *conf_section;
54 } rlm_sql_log_t;
55
56 /*
57  *      A mapping of configuration file names to internal variables.
58  */
59 static const CONF_PARSER module_config[] = {
60         {"path", PW_TYPE_STRING_PTR,
61          offsetof(rlm_sql_log_t,path), NULL, "${radacctdir}/sql-relay"},
62         {"Post-Auth", PW_TYPE_STRING_PTR,
63          offsetof(rlm_sql_log_t,postauth_query), NULL, ""},
64         {"sql_user_name", PW_TYPE_STRING_PTR,
65          offsetof(rlm_sql_log_t,sql_user_name), NULL, ""},
66         {"utf8", PW_TYPE_BOOLEAN,
67          offsetof(rlm_sql_log_t,utf8), NULL, "no"},
68         {"safe-characters", PW_TYPE_STRING_PTR,
69          offsetof(rlm_sql_log_t,allowed_chars), NULL,
70         "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /"},
71
72         { NULL, -1, 0, NULL, NULL }     /* end the list */
73 };
74
75 static char *allowed_chars = NULL;
76
77 /*
78  *      Do any per-module initialization that is separate to each
79  *      configured instance of the module.  e.g. set up connections
80  *      to external databases, read configuration files, set up
81  *      dictionary entries, etc.
82  *
83  *      If configuration information is given in the config section
84  *      that must be referenced in later calls, store a handle to it
85  *      in *instance otherwise put a null pointer there.
86  */
87 static int sql_log_instantiate(CONF_SECTION *conf, void **instance)
88 {
89         rlm_sql_log_t   *inst;
90
91         /*
92          *      Set up a storage area for instance data.
93          */
94         inst = calloc(1, sizeof(rlm_sql_log_t));
95         if (inst == NULL) {
96                 radlog(L_ERR, "rlm_sql_log: Not enough memory");
97                 return -1;
98         }
99
100         /*
101          *      If the configuration parameters can't be parsed,
102          *      then fail.
103          */
104         if (cf_section_parse(conf, inst, module_config) < 0) {
105                 radlog(L_ERR, "rlm_sql_log: Unable to parse parameters");
106                 sql_log_detach(inst);
107                 return -1;
108         }
109
110         inst->conf_section = conf;
111         allowed_chars = inst->allowed_chars;
112         *instance = inst;
113         return 0;
114 }
115
116 /*
117  *      Say goodbye to the cruel world.
118  */
119 static int sql_log_detach(void *instance)
120 {
121         int i;
122         char **p;
123         rlm_sql_log_t *inst = (rlm_sql_log_t *)instance;
124
125         /*
126          *      Free up dynamically allocated string pointers.
127          */
128         for (i = 0; module_config[i].name != NULL; i++) {
129                 if (module_config[i].type != PW_TYPE_STRING_PTR) {
130                         continue;
131                 }
132
133                 /*
134                  *      Treat 'config' as an opaque array of bytes,
135                  *      and take the offset into it.  There's a
136                  *      (char*) pointer at that offset, and we want
137                  *      to point to it.
138                  */
139                 p = (char **) (((char *)inst) + module_config[i].offset);
140                 if (!*p) { /* nothing allocated */
141                         continue;
142                 }
143                 free(*p);
144                 *p = NULL;
145         }
146         allowed_chars = NULL;
147         free(inst);
148         return 0;
149 }
150
151 /*
152  *      Translate the SQL queries.
153  */
154 static size_t sql_escape_func(char *out, size_t outlen, const char *in)
155 {
156         int len = 0;
157
158         while (in[0]) {
159                 /*
160                  *      Non-printable characters get replaced with their
161                  *      mime-encoded equivalents.
162                  */
163                 if ((in[0] < 32) ||
164                     strchr(allowed_chars, *in) == NULL) {
165                         /*
166                          *      Only 3 or less bytes available.
167                          */
168                         if (outlen <= 3) {
169                                 break;
170                         }
171
172                         snprintf(out, outlen, "=%02X", (unsigned char) in[0]);
173                         in++;
174                         out += 3;
175                         outlen -= 3;
176                         len += 3;
177                         continue;
178                 }
179
180                 /*
181                  *      Only one byte left.
182                  */
183                 if (outlen <= 1) {
184                         break;
185                 }
186
187                 /*
188                  *      Allowed character.
189                  */
190                 *out = *in;
191                 out++;
192                 in++;
193                 outlen--;
194                 len++;
195         }
196         *out = '\0';
197         return len;
198 }
199
200 static size_t sql_utf8_escape_func(char *out, size_t outlen, const char *in)
201 {
202         size_t len = 0;
203         size_t utf8 = 0;
204
205         while (in[0]) {
206                 /* 
207                  * Skip over UTF8 characters
208                  */
209                 utf8 = fr_utf8_char((const uint8_t *)in);
210                 if (utf8) {
211                         if (outlen <= utf8) {
212                                 break;
213                         }
214                         while (utf8-- > 0) {
215                                 *out = *in;
216                                 out++;
217                                 in++;
218                                 outlen--;
219                                 len++;
220                         }
221                         continue;
222                 }
223
224                 /*
225                  *      Non-printable characters get replaced with their
226                  *      mime-encoded equivalents.
227                  */
228                 if ((in[0] < 32) ||
229                     strchr(allowed_chars, *in) == NULL) {
230                         /*
231                          *      Only 3 or less bytes available.
232                          */
233                         if (outlen <= 3) {
234                                 break;
235                         }
236
237                         snprintf(out, outlen, "=%02X", (unsigned char) in[0]);
238                         in++;
239                         out += 3;
240                         outlen -= 3;
241                         len += 3;
242                         continue;
243                 }
244
245                 /*
246                  *      Only one byte left.
247                  */
248                 if (outlen <= 1) {
249                         break;
250                 }
251
252                 /*
253                  *      Allowed character.
254                  */
255                 *out = *in;
256                 out++;
257                 in++;
258                 outlen--;
259                 len++;
260         }
261         *out = '\0';
262         return len;
263 }
264
265 /*
266  *      Add the 'SQL-User-Name' attribute to the packet.
267  */
268 static int sql_set_user(rlm_sql_log_t *inst, REQUEST *request, char *sqlusername, const char *username)
269 {
270         VALUE_PAIR *vp=NULL;
271         char tmpuser[MAX_STRING_LEN];
272
273         tmpuser[0] = '\0';
274         sqlusername[0] = '\0';
275
276         rad_assert(request != NULL);
277         rad_assert(request->packet != NULL);
278
279         /* Remove any user attr we added previously */
280         pairdelete(&request->packet->vps, PW_SQL_USER_NAME, 0);
281
282         if (username != NULL) {
283                 strlcpy(tmpuser, username, MAX_STRING_LEN);
284         } else if (inst->sql_user_name[0] != '\0') {
285                 radius_xlat(tmpuser, sizeof(tmpuser), inst->sql_user_name,
286                             request, NULL);
287         } else {
288                 return 0;
289         }
290
291         if (tmpuser[0] != '\0') {
292                 strlcpy(sqlusername, tmpuser, sizeof(tmpuser));
293                 RDEBUG2("sql_set_user escaped user --> '%s'", sqlusername);
294                 vp = pairmake("SQL-User-Name", sqlusername, 0);
295                 if (vp == NULL) {
296                         radlog(L_ERR, "%s", fr_strerror());
297                         return -1;
298                 }
299
300                 pairadd(&request->packet->vps, vp);
301                 return 0;
302         }
303         return -1;
304 }
305
306 /*
307  *      Replace %<whatever> in the query.
308  */
309 static int sql_xlat_query(rlm_sql_log_t *inst, REQUEST *request, const char *query, char *xlat_query, size_t len)
310 {
311         char    sqlusername[MAX_STRING_LEN];
312
313         /* If query is not defined, we stop here */
314         if (query[0] == '\0')
315                 return RLM_MODULE_NOOP;
316
317         /* Add attribute 'SQL-User-Name' */
318         if (sql_set_user(inst, request, sqlusername, NULL) <0) {
319                 radlog_request(L_ERR, 0, request, 
320                                "Couldn't add SQL-User-Name attribute");
321                 return RLM_MODULE_FAIL;
322         }
323
324         /* Expand variables in the query */
325         xlat_query[0] = '\0';
326         radius_xlat(xlat_query, len, query, request,
327                     inst->utf8 ? sql_utf8_escape_func : sql_escape_func);
328         if (xlat_query[0] == '\0') {
329                 radlog_request(L_ERR, 0, request, "Couldn't xlat the query %s",
330                        query);
331                 return RLM_MODULE_FAIL;
332         }
333
334         return RLM_MODULE_OK;
335 }
336
337 /*
338  *      The Perl version of radsqlrelay uses fcntl locks.
339  */
340 static int setlock(int fd)
341 {
342 #ifdef F_WRLCK
343         struct flock fl;
344         memset(&fl, 0, sizeof(fl));
345         fl.l_start = 0;
346         fl.l_len = 0;
347         fl.l_type = F_WRLCK;
348         fl.l_whence = SEEK_SET;
349         return fcntl(fd, F_SETLKW, &fl);
350 #else
351         return -1;
352 #endif
353 }
354
355 /*
356  *      Write the line into file (with lock)
357  */
358 static int sql_log_write(rlm_sql_log_t *inst, REQUEST *request, const char *line)
359 {
360         int fd;
361         FILE *fp;
362         int locked = 0;
363         struct stat st;
364         char path[MAX_STRING_LEN];
365
366         path[0] = '\0';
367         radius_xlat(path, sizeof(path), inst->path, request, NULL);
368         if (path[0] == '\0') {
369                 return RLM_MODULE_FAIL;
370         }
371
372         while (!locked) {
373                 if ((fd = open(path, O_WRONLY | O_APPEND | O_CREAT, 0666)) < 0) {
374                         radlog_request(L_ERR, 0, request, "Couldn't open file %s: %s",
375                                        path, strerror(errno));
376                         return RLM_MODULE_FAIL;
377                 }
378                 if (setlock(fd) != 0) {
379                         radlog_request(L_ERR, 0, request, "Couldn't lock file %s: %s",
380                                        path, strerror(errno));
381                         close(fd);
382                         return RLM_MODULE_FAIL;
383                 }
384                 if (fstat(fd, &st) != 0) {
385                         radlog_request(L_ERR, 0, request, "Couldn't stat file %s: %s",
386                                        path, strerror(errno));
387                         close(fd);
388                         return RLM_MODULE_FAIL;
389                 }
390                 if (st.st_nlink == 0) {
391                         RDEBUG("File %s removed by another program, retrying",
392                               path);
393                         close(fd);
394                         continue;
395                 }
396                 locked = 1;
397         }
398
399         if ((fp = fdopen(fd, "a")) == NULL) {
400                 radlog_request(L_ERR, 0, request, "Couldn't associate a stream with file %s: %s",
401                                path, strerror(errno));
402                 close(fd);
403                 return RLM_MODULE_FAIL;
404         }
405         fputs(line, fp);
406         putc('\n', fp);
407         fclose(fp);     /* and unlock */
408         return RLM_MODULE_OK;
409 }
410
411 /*
412  *      Write accounting information to this module's database.
413  */
414 static int sql_log_accounting(void *instance, REQUEST *request)
415 {
416         int             ret;
417         char            querystr[MAX_QUERY_LEN];
418         const char      *cfquery;
419         rlm_sql_log_t   *inst = (rlm_sql_log_t *)instance;
420         VALUE_PAIR      *pair;
421         DICT_VALUE      *dval;
422         CONF_PAIR       *cp;
423
424         rad_assert(request != NULL);
425         rad_assert(request->packet != NULL);
426
427         RDEBUG("Processing sql_log_accounting");
428
429         /* Find the Acct Status Type. */
430         if ((pair = pairfind(request->packet->vps, PW_ACCT_STATUS_TYPE, 0)) == NULL) {
431                 radlog_request(L_ERR, 0, request, "Packet has no account status type");
432                 return RLM_MODULE_INVALID;
433         }
434
435         /* Search the query in conf section of the module */
436         if ((dval = dict_valbyattr(PW_ACCT_STATUS_TYPE, 0, pair->vp_integer)) == NULL) {
437                 radlog_request(L_ERR, 0, request, "Unsupported Acct-Status-Type = %d",
438                                pair->vp_integer);
439                 return RLM_MODULE_NOOP;
440         }
441         if ((cp = cf_pair_find(inst->conf_section, dval->name)) == NULL) {
442                 RDEBUG("Couldn't find an entry %s in the config section",
443                        dval->name);
444                 return RLM_MODULE_NOOP;
445         }
446         cfquery = cf_pair_value(cp);
447
448         /* Xlat the query */
449         ret = sql_xlat_query(inst, request, cfquery, querystr, sizeof(querystr));
450         if (ret != RLM_MODULE_OK)
451                 return ret;
452
453         /* Write query into sql-relay file */
454         return sql_log_write(inst, request, querystr);
455 }
456
457 /*
458  *      Write post-auth information to this module's database.
459  */
460 static int sql_log_postauth(void *instance, REQUEST *request)
461 {
462         int             ret;
463         char            querystr[MAX_QUERY_LEN];
464         rlm_sql_log_t   *inst = (rlm_sql_log_t *)instance;
465
466         rad_assert(request != NULL);
467
468         RDEBUG("Processing sql_log_postauth");
469
470         /* Xlat the query */
471         ret = sql_xlat_query(inst, request, inst->postauth_query,
472                              querystr, sizeof(querystr));
473         if (ret != RLM_MODULE_OK)
474                 return ret;
475
476         /* Write query into sql-relay file */
477         return sql_log_write(inst, request, querystr);
478 }
479
480 /*
481  *      The module name should be the only globally exported symbol.
482  *      That is, everything else should be 'static'.
483  *
484  *      If the module needs to temporarily modify it's instantiation
485  *      data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
486  *      The server will then take care of ensuring that the module
487  *      is single-threaded.
488  */
489 module_t rlm_sql_log = {
490         RLM_MODULE_INIT,
491         "sql_log",
492         RLM_TYPE_THREAD_UNSAFE | RLM_TYPE_CHECK_CONFIG_SAFE | RLM_TYPE_HUP_SAFE,                /* type */
493         sql_log_instantiate,            /* instantiation */
494         sql_log_detach,                 /* detach */
495         {
496                 NULL,                   /* authentication */
497                 NULL,                   /* authorization */
498                 NULL,                   /* preaccounting */
499                 sql_log_accounting,     /* accounting */
500                 NULL,                   /* checksimul */
501                 NULL,                   /* pre-proxy */
502                 NULL,                   /* post-proxy */
503                 sql_log_postauth        /* post-auth */
504         },
505 };