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