Pull from CVS head:
[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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
24  */
25
26 #include "autoconf.h"
27
28 #include <fcntl.h>
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <sys/stat.h>
32
33 #include "libradius.h"
34 #include "radiusd.h"
35 #include "modules.h"
36 #include "conffile.h"
37
38 static const char rcsid[] = "$Id$";
39
40 static int sql_log_instantiate(CONF_SECTION *conf, void **instance);
41 static int sql_log_detach(void *instance);
42 static int sql_log_accounting(void *instance, REQUEST *request);
43 static int sql_log_postauth(void *instance, REQUEST *request);
44
45 #define MAX_QUERY_LEN 4096
46
47 /*
48  *      Define a structure for our module configuration.
49  */
50 typedef struct rlm_sql_log_t {
51         char            *name;
52         char            *path;
53         char            *postauth_query;
54         char            *sql_user_name;
55         char            *allowed_chars;
56         CONF_SECTION    *conf_section;
57 } rlm_sql_log_t;
58
59 /*
60  *      A mapping of configuration file names to internal variables.
61  */
62 static const CONF_PARSER module_config[] = {
63         {"path", PW_TYPE_STRING_PTR,
64          offsetof(rlm_sql_log_t,path), NULL, "${radacctdir}/sql-relay"},
65         {"Post-Auth", PW_TYPE_STRING_PTR,
66          offsetof(rlm_sql_log_t,postauth_query), NULL, ""},
67         {"sql_user_name", PW_TYPE_STRING_PTR,
68          offsetof(rlm_sql_log_t,sql_user_name), NULL, ""},
69         {"safe-characters", PW_TYPE_STRING_PTR,
70          offsetof(rlm_sql_log_t,allowed_chars), NULL,
71         "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /"},
72
73         { NULL, -1, 0, NULL, NULL }     /* end the list */
74 };
75
76 static char *allowed_chars = NULL;
77
78 /*
79  *      Do any per-module initialization that is separate to each
80  *      configured instance of the module.  e.g. set up connections
81  *      to external databases, read configuration files, set up
82  *      dictionary entries, etc.
83  *
84  *      If configuration information is given in the config section
85  *      that must be referenced in later calls, store a handle to it
86  *      in *instance otherwise put a null pointer there.
87  */
88 static int sql_log_instantiate(CONF_SECTION *conf, void **instance)
89 {
90         const char      *name;
91         rlm_sql_log_t   *inst;
92
93         /*
94          *      Set up a storage area for instance data.
95          */
96         inst = calloc(1, sizeof(rlm_sql_log_t));
97         if (inst == NULL) {
98                 radlog(L_ERR, "rlm_sql_log: Not enough memory");
99                 return -1;
100         }
101
102         /*
103          *      Get the name of the current section in the conf file.
104          */
105         name = cf_section_name2(conf);
106         if (name == NULL)
107                 name = cf_section_name1(conf);
108         if (name == NULL)
109                 name = "sql_log";
110         inst->name = strdup(name);
111
112         /*
113          *      If the configuration parameters can't be parsed,
114          *      then fail.
115          */
116         if (cf_section_parse(conf, inst, module_config) < 0) {
117                 radlog(L_ERR, "rlm_sql_log (%s): Unable to parse parameters",
118                        inst->name);
119                 sql_log_detach(inst);
120                 return -1;
121         }
122
123         inst->conf_section = conf;
124         allowed_chars = inst->allowed_chars;
125         *instance = inst;
126         return 0;
127 }
128
129 /*
130  *      Say goodbye to the cruel world.
131  */
132 static int sql_log_detach(void *instance)
133 {
134         int i;
135         char **p;
136         rlm_sql_log_t *inst = (rlm_sql_log_t *)instance;
137
138         if (inst->name) {
139                 free(inst->name);
140                 inst->name = NULL;
141         }
142
143         /*
144          *      Free up dynamically allocated string pointers.
145          */
146         for (i = 0; module_config[i].name != NULL; i++) {
147                 if (module_config[i].type != PW_TYPE_STRING_PTR) {
148                         continue;
149                 }
150
151                 /*
152                  *      Treat 'config' as an opaque array of bytes,
153                  *      and take the offset into it.  There's a
154                  *      (char*) pointer at that offset, and we want
155                  *      to point to it.
156                  */
157                 p = (char **) (((char *)inst) + module_config[i].offset);
158                 if (!*p) { /* nothing allocated */
159                         continue;
160                 }
161                 free(*p);
162                 *p = NULL;
163         }
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         "sql_log",
431         RLM_TYPE_THREAD_SAFE,           /* type */
432         NULL,                           /* initialization */
433         sql_log_instantiate,            /* instantiation */
434         {
435                 NULL,                   /* authentication */
436                 NULL,                   /* authorization */
437                 NULL,                   /* preaccounting */
438                 sql_log_accounting,     /* accounting */
439                 NULL,                   /* checksimul */
440                 NULL,                   /* pre-proxy */
441                 NULL,                   /* post-proxy */
442                 sql_log_postauth        /* post-auth */
443         },
444         sql_log_detach,                 /* detach */
445         NULL,                           /* destroy */
446 };