2cabe90f7d230876a692af1a7399078c4b8e18a6
[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         char            *allowed_chars;
52         CONF_SECTION    *conf_section;
53 } rlm_sql_log_t;
54
55 /*
56  *      A mapping of configuration file names to internal variables.
57  */
58 static const CONF_PARSER module_config[] = {
59         {"path", PW_TYPE_STRING_PTR,
60          offsetof(rlm_sql_log_t,path), NULL, "${radacctdir}/sql-relay"},
61         {"Post-Auth", PW_TYPE_STRING_PTR,
62          offsetof(rlm_sql_log_t,postauth_query), NULL, ""},
63         {"sql_user_name", PW_TYPE_STRING_PTR,
64          offsetof(rlm_sql_log_t,sql_user_name), NULL, ""},
65         {"safe-characters", PW_TYPE_STRING_PTR,
66          offsetof(rlm_sql_log_t,allowed_chars), NULL,
67         "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /"},
68
69         { NULL, -1, 0, NULL, NULL }     /* end the list */
70 };
71
72 static char *allowed_chars = NULL;
73
74 /*
75  *      Do any per-module initialization that is separate to each
76  *      configured instance of the module.  e.g. set up connections
77  *      to external databases, read configuration files, set up
78  *      dictionary entries, etc.
79  *
80  *      If configuration information is given in the config section
81  *      that must be referenced in later calls, store a handle to it
82  *      in *instance otherwise put a null pointer there.
83  */
84 static int sql_log_instantiate(CONF_SECTION *conf, void **instance)
85 {
86         rlm_sql_log_t   *inst;
87
88         /*
89          *      Set up a storage area for instance data.
90          */
91         inst = calloc(1, sizeof(rlm_sql_log_t));
92         if (inst == NULL) {
93                 radlog(L_ERR, "rlm_sql_log: Not enough memory");
94                 return -1;
95         }
96
97         /*
98          *      If the configuration parameters can't be parsed,
99          *      then fail.
100          */
101         if (cf_section_parse(conf, inst, module_config) < 0) {
102                 radlog(L_ERR, "rlm_sql_log: Unable to parse parameters");
103                 sql_log_detach(inst);
104                 return -1;
105         }
106
107         inst->conf_section = conf;
108         allowed_chars = inst->allowed_chars;
109         *instance = inst;
110         return 0;
111 }
112
113 /*
114  *      Say goodbye to the cruel world.
115  */
116 static int sql_log_detach(void *instance)
117 {
118         int i;
119         char **p;
120         rlm_sql_log_t *inst = (rlm_sql_log_t *)instance;
121
122         /*
123          *      Free up dynamically allocated string pointers.
124          */
125         for (i = 0; module_config[i].name != NULL; i++) {
126                 if (module_config[i].type != PW_TYPE_STRING_PTR) {
127                         continue;
128                 }
129
130                 /*
131                  *      Treat 'config' as an opaque array of bytes,
132                  *      and take the offset into it.  There's a
133                  *      (char*) pointer at that offset, and we want
134                  *      to point to it.
135                  */
136                 p = (char **) (((char *)inst) + module_config[i].offset);
137                 if (!*p) { /* nothing allocated */
138                         continue;
139                 }
140                 free(*p);
141                 *p = NULL;
142         }
143         allowed_chars = NULL;
144         free(inst);
145         return 0;
146 }
147
148 /*
149  *      Translate the SQL queries.
150  */
151 static size_t sql_escape_func(char *out, size_t outlen, const char *in)
152 {
153         int len = 0;
154
155         while (in[0]) {
156                 /*
157                  *      Non-printable characters get replaced with their
158                  *      mime-encoded equivalents.
159                  */
160                 if ((in[0] < 32) ||
161                     strchr(allowed_chars, *in) == NULL) {
162                         /*
163                          *      Only 3 or less bytes available.
164                          */
165                         if (outlen <= 3) {
166                                 break;
167                         }
168
169                         snprintf(out, outlen, "=%02X", (unsigned char) in[0]);
170                         in++;
171                         out += 3;
172                         outlen -= 3;
173                         len += 3;
174                         continue;
175                 }
176
177                 /*
178                  *      Only one byte left.
179                  */
180                 if (outlen <= 1) {
181                         break;
182                 }
183
184                 /*
185                  *      Allowed character.
186                  */
187                 *out = *in;
188                 out++;
189                 in++;
190                 outlen--;
191                 len++;
192         }
193         *out = '\0';
194         return len;
195 }
196
197 /*
198  *      Add the 'SQL-User-Name' attribute to the packet.
199  */
200 static int sql_set_user(rlm_sql_log_t *inst, REQUEST *request, char *sqlusername, const char *username)
201 {
202         VALUE_PAIR *vp=NULL;
203         char tmpuser[MAX_STRING_LEN];
204
205         tmpuser[0] = '\0';
206         sqlusername[0] = '\0';
207
208         rad_assert(request != NULL);
209         rad_assert(request->packet != NULL);
210
211         /* Remove any user attr we added previously */
212         pairdelete(&request->packet->vps, PW_SQL_USER_NAME);
213
214         if (username != NULL) {
215                 strlcpy(tmpuser, username, MAX_STRING_LEN);
216         } else if (inst->sql_user_name[0] != '\0') {
217                 radius_xlat(tmpuser, sizeof(tmpuser), inst->sql_user_name,
218                             request, NULL);
219         } else {
220                 return 0;
221         }
222
223         if (tmpuser[0] != '\0') {
224                 strlcpy(sqlusername, tmpuser, sizeof(tmpuser));
225                 RDEBUG2("sql_set_user escaped user --> '%s'", sqlusername);
226                 vp = pairmake("SQL-User-Name", sqlusername, 0);
227                 if (vp == NULL) {
228                         radlog(L_ERR, "%s", fr_strerror());
229                         return -1;
230                 }
231
232                 pairadd(&request->packet->vps, vp);
233                 return 0;
234         }
235         return -1;
236 }
237
238 /*
239  *      Replace %<whatever> in the query.
240  */
241 static int sql_xlat_query(rlm_sql_log_t *inst, REQUEST *request, const char *query, char *xlat_query, size_t len)
242 {
243         char    sqlusername[MAX_STRING_LEN];
244
245         /* If query is not defined, we stop here */
246         if (query[0] == '\0')
247                 return RLM_MODULE_NOOP;
248
249         /* Add attribute 'SQL-User-Name' */
250         if (sql_set_user(inst, request, sqlusername, NULL) <0) {
251                 radlog_request(L_ERR, 0, request, 
252                                "Couldn't add SQL-User-Name attribute");
253                 return RLM_MODULE_FAIL;
254         }
255
256         /* Expand variables in the query */
257         xlat_query[0] = '\0';
258         radius_xlat(xlat_query, len, query, request, sql_escape_func);
259         if (xlat_query[0] == '\0') {
260                 radlog_request(L_ERR, 0, request, "Couldn't xlat the query %s",
261                        query);
262                 return RLM_MODULE_FAIL;
263         }
264
265         return RLM_MODULE_OK;
266 }
267
268 /*
269  *      The Perl version of radsqlrelay uses fcntl locks.
270  */
271 static int setlock(int fd)
272 {
273         struct flock fl;
274         memset(&fl, 0, sizeof(fl));
275         fl.l_start = 0;
276         fl.l_len = 0;
277         fl.l_type = F_WRLCK;
278         fl.l_whence = SEEK_SET;
279         return fcntl(fd, F_SETLKW, &fl);
280 }
281
282 /*
283  *      Write the line into file (with lock)
284  */
285 static int sql_log_write(rlm_sql_log_t *inst, REQUEST *request, const char *line)
286 {
287         int fd;
288         FILE *fp;
289         int locked = 0;
290         struct stat st;
291         char path[MAX_STRING_LEN];
292
293         path[0] = '\0';
294         radius_xlat(path, sizeof(path), inst->path, request, NULL);
295         if (path[0] == '\0') {
296                 return RLM_MODULE_FAIL;
297         }
298
299         while (!locked) {
300                 if ((fd = open(path, O_WRONLY | O_APPEND | O_CREAT, 0666)) < 0) {
301                         radlog_request(L_ERR, 0, request, "Couldn't open file %s: %s",
302                                        path, strerror(errno));
303                         return RLM_MODULE_FAIL;
304                 }
305                 if (setlock(fd) != 0) {
306                         radlog_request(L_ERR, 0, request, "Couldn't lock file %s: %s",
307                                        path, strerror(errno));
308                         close(fd);
309                         return RLM_MODULE_FAIL;
310                 }
311                 if (fstat(fd, &st) != 0) {
312                         radlog_request(L_ERR, 0, request, "Couldn't stat file %s: %s",
313                                        path, strerror(errno));
314                         close(fd);
315                         return RLM_MODULE_FAIL;
316                 }
317                 if (st.st_nlink == 0) {
318                         RDEBUG("File %s removed by another program, retrying",
319                               path);
320                         close(fd);
321                         continue;
322                 }
323                 locked = 1;
324         }
325
326         if ((fp = fdopen(fd, "a")) == NULL) {
327                 radlog_request(L_ERR, 0, request, "Couldn't associate a stream with file %s: %s",
328                                path, strerror(errno));
329                 close(fd);
330                 return RLM_MODULE_FAIL;
331         }
332         fputs(line, fp);
333         putc('\n', fp);
334         fclose(fp);     /* and unlock */
335         return RLM_MODULE_OK;
336 }
337
338 /*
339  *      Write accounting information to this module's database.
340  */
341 static int sql_log_accounting(void *instance, REQUEST *request)
342 {
343         int             ret;
344         char            querystr[MAX_QUERY_LEN];
345         const char      *cfquery;
346         rlm_sql_log_t   *inst = (rlm_sql_log_t *)instance;
347         VALUE_PAIR      *pair;
348         DICT_VALUE      *dval;
349         CONF_PAIR       *cp;
350
351         rad_assert(request != NULL);
352         rad_assert(request->packet != NULL);
353
354         RDEBUG("Processing sql_log_accounting");
355
356         /* Find the Acct Status Type. */
357         if ((pair = pairfind(request->packet->vps, PW_ACCT_STATUS_TYPE)) == NULL) {
358                 radlog_request(L_ERR, 0, request, "Packet has no account status type");
359                 return RLM_MODULE_INVALID;
360         }
361
362         /* Search the query in conf section of the module */
363         if ((dval = dict_valbyattr(PW_ACCT_STATUS_TYPE, pair->vp_integer)) == NULL) {
364                 radlog_request(L_ERR, 0, request, "Unsupported Acct-Status-Type = %d",
365                                pair->vp_integer);
366                 return RLM_MODULE_NOOP;
367         }
368         if ((cp = cf_pair_find(inst->conf_section, dval->name)) == NULL) {
369                 RDEBUG("Couldn't find an entry %s in the config section",
370                        dval->name);
371                 return RLM_MODULE_NOOP;
372         }
373         cfquery = cf_pair_value(cp);
374
375         /* Xlat the query */
376         ret = sql_xlat_query(inst, request, cfquery, querystr, sizeof(querystr));
377         if (ret != RLM_MODULE_OK)
378                 return ret;
379
380         /* Write query into sql-relay file */
381         return sql_log_write(inst, request, querystr);
382 }
383
384 /*
385  *      Write post-auth information to this module's database.
386  */
387 static int sql_log_postauth(void *instance, REQUEST *request)
388 {
389         int             ret;
390         char            querystr[MAX_QUERY_LEN];
391         rlm_sql_log_t   *inst = (rlm_sql_log_t *)instance;
392
393         rad_assert(request != NULL);
394
395         RDEBUG("Processing sql_log_postauth");
396
397         /* Xlat the query */
398         ret = sql_xlat_query(inst, request, inst->postauth_query,
399                              querystr, sizeof(querystr));
400         if (ret != RLM_MODULE_OK)
401                 return ret;
402
403         /* Write query into sql-relay file */
404         return sql_log_write(inst, request, querystr);
405 }
406
407 /*
408  *      The module name should be the only globally exported symbol.
409  *      That is, everything else should be 'static'.
410  *
411  *      If the module needs to temporarily modify it's instantiation
412  *      data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
413  *      The server will then take care of ensuring that the module
414  *      is single-threaded.
415  */
416 module_t rlm_sql_log = {
417         RLM_MODULE_INIT,
418         "sql_log",
419         RLM_TYPE_THREAD_UNSAFE | RLM_TYPE_CHECK_CONFIG_SAFE | RLM_TYPE_HUP_SAFE,                /* type */
420         sql_log_instantiate,            /* instantiation */
421         sql_log_detach,                 /* detach */
422         {
423                 NULL,                   /* authentication */
424                 NULL,                   /* authorization */
425                 NULL,                   /* preaccounting */
426                 sql_log_accounting,     /* accounting */
427                 NULL,                   /* checksimul */
428                 NULL,                   /* pre-proxy */
429                 NULL,                   /* post-proxy */
430                 sql_log_postauth        /* post-auth */
431         },
432 };