Jeff Carneal <jeff@apex.net>
[freeradius.git] / src / modules / rlm_files / rlm_files.c
1 /*
2  * rlm_files.c  authorization: Find a user in the "users" file.
3  *              accounting:    Write the "detail" files.
4  *
5  * Version:     $Id$
6  *
7  */
8
9 static const char rcsid[] = "$Id$";
10
11 #include        "autoconf.h"
12
13 #include        <sys/socket.h>
14 #include        <sys/stat.h>
15 #include        <netinet/in.h>
16
17 #include        "radiusd.h"
18
19 #include        <stdlib.h>
20 #include        <string.h>
21 #include        <netdb.h>
22 #include        <ctype.h>
23 #include        <fcntl.h>
24 #include        <limits.h>
25
26 #if HAVE_MALLOC_H
27 #  include      <malloc.h>
28 #endif
29
30 #include        "modules.h"
31
32 #ifdef WITH_DBM
33 #  include      <dbm.h>
34 #endif
35 #ifdef WITH_NDBM
36 #  include      <ndbm.h>
37 #endif
38
39 struct file_instance {
40         char *compat_mode;
41
42         /* autz */
43         char *usersfile;
44         PAIR_LIST *users;
45
46         /* preacct */
47         char *acctusersfile;
48         PAIR_LIST *acctusers;
49 };
50
51 #if defined(WITH_DBM) || defined(WITH_NDBM)
52 /*
53  *      See if a potential DBM file is present.
54  */
55 static int checkdbm(char *users, char *ext)
56 {
57         char buffer[256];
58         struct stat st;
59
60         strcpy(buffer, users);
61         strcat(buffer, ext);
62
63         return stat(buffer, &st);
64 }
65
66 /*
67  *      Find the named user in the DBM user database.
68  *      Returns: -1 not found
69  *                0 found but doesn't match.
70  *                1 found and matches.
71  */
72 static int dbm_find(DBM *dbmfile, char *name, VALUE_PAIR *request_pairs,
73                 VALUE_PAIR **check_pairs, VALUE_PAIR **reply_pairs)
74 {
75         datum           named;
76         datum           contentd;
77         char            *ptr;
78         VALUE_PAIR      *check_tmp;
79         VALUE_PAIR      *reply_tmp;
80         int             ret = 0;
81
82         named.dptr = name;
83         named.dsize = strlen(name);
84 #ifdef WITH_DBM
85         contentd = fetch(named);
86 #endif
87 #ifdef WITH_NDBM
88         contentd = dbm_fetch(dbmfile, named);
89 #endif
90         if(contentd.dptr == NULL)
91                 return -1;
92
93         check_tmp = NULL;
94         reply_tmp = NULL;
95
96         /*
97          *      Parse the check values
98          */
99         ptr = contentd.dptr;
100         contentd.dptr[contentd.dsize] = '\0';
101
102         if (*ptr != '\n' && userparse(ptr, &check_tmp) != 0) {
103                 radlog(L_ERR|L_CONS, "Parse error (check) for user %s", name);
104                 pairfree(check_tmp);
105                 return -1;
106         }
107         while(*ptr != '\n' && *ptr != '\0') {
108                 ptr++;
109         }
110         if(*ptr != '\n') {
111                 radlog(L_ERR|L_CONS, "Parse error (no reply pairs) for user %s",
112                         name);
113                 pairfree(check_tmp);
114                 return -1;
115         }
116         ptr++;
117
118         /*
119          *      Parse the reply values
120          */
121         if (userparse(ptr, &reply_tmp) != 0) {
122                 radlog(L_ERR|L_CONS, "Parse error (reply) for user %s", name);
123                 pairfree(check_tmp);
124                 pairfree(reply_tmp);
125                 return -1;
126         }
127
128         /*
129          *      See if the check_pairs match.
130          */
131         if (paircmp(request_pairs, check_tmp, reply_pairs) == 0) {
132                 ret = 1;
133                 pairmove(reply_pairs, &reply_tmp);
134                 pairmove2(reply_pairs, &reply_tmp, PW_FALL_THROUGH);
135                 pairmove(check_pairs, &check_tmp);
136         }
137         pairfree(reply_tmp);
138         pairfree(check_tmp);
139
140         return ret;
141 }
142 #endif /* DBM */
143
144 /*
145  *     See if a VALUE_PAIR list contains Fall-Through = Yes
146  */
147 static int fallthrough(VALUE_PAIR *vp)
148 {
149         VALUE_PAIR *tmp;
150
151         tmp = pairfind(vp, PW_FALL_THROUGH);
152
153         return tmp ? tmp->lvalue : 0;
154 }
155
156
157
158 static int file_init(void)
159 {
160         return 0;
161 }
162
163 /*
164  *      A temporary holding area for config values to be extracted
165  *      into, before they are copied into the instance data
166  */
167 static struct file_instance config;
168
169 static CONF_PARSER module_config[] = {
170         { "usersfile",     PW_TYPE_STRING_PTR, &config.usersfile, RADIUS_USERS },
171         { "acctusersfile", PW_TYPE_STRING_PTR, &config.acctusersfile, RADIUS_ACCT_USERS },
172         { "compat",        PW_TYPE_STRING_PTR, &config.compat_mode, "cistron" },
173         { NULL, -1, NULL, NULL }
174 };
175
176 static int getusersfile(const char *filename, PAIR_LIST **pair_list)
177 {
178         int rcode;
179         PAIR_LIST *users = NULL;
180 #if defined(WITH_DBM) || defined(WITH_NDBM)
181         if (!use_dbm &&
182             (checkdbm(filename, ".dir") == 0 ||
183              checkdbm(filename, ".db") == 0)) {
184                 radlog(L_INFO|L_CONS, "DBM files found but no -b flag " "given - NOT using DBM");
185         }
186 #endif
187
188         if (!use_dbm) {
189                 rcode = pairlist_read(filename, &users, 1);
190                 if (rcode < 0) {
191                         return -1;
192                 }
193         }
194
195         /*
196          *      Walk through the 'users' file list, if we're debugging,
197          *      or if we're in compat_mode.
198          */
199         if ((debug_flag) ||
200             (strcmp(config.compat_mode, "cistron") == 0)) {
201                 PAIR_LIST *entry;
202                 VALUE_PAIR *vp;
203                 int compat_mode = FALSE;
204
205                 if (strcmp(config.compat_mode, "cistron") == 0) {
206                         compat_mode = TRUE;
207                 }
208         
209                 entry = users;
210                 while (entry) {
211                         if (compat_mode) {
212                                 DEBUG("[%s]:%d Cistron compatibility checks for entry %s ...",
213                                       filename, entry->lineno,
214                                       entry->name);
215                         }
216
217                         /*
218                          *      Look for improper use of '=' in the
219                          *      check items.  They should be using
220                          *      '==' for on-the-wire RADIUS attributes,
221                          *      and probably ':=' for server
222                          *      configuration items.
223                          */
224                         for (vp = entry->check; vp != NULL; vp = vp->next) {
225                                 /*
226                                  *      Ignore attributes which are set
227                                  *      properly.
228                                  */
229                                 if (vp->operator != T_OP_EQ) {
230                                         continue;
231                                 }
232
233                                 /*
234                                  *      If it's a vendor attribute,
235                                  *      or it's a wire protocol, 
236                                  *      ensure it has '=='.
237                                  */
238                                 if (((vp->attribute & ~0xffff) != 0) ||
239                                     (vp->attribute < 0x100)) {
240                                         if (!compat_mode) {
241                                                 DEBUG("[%s]:%d WARNING! Changing '%s =' to '%s =='\n\tfor comparing RADIUS attribute in check item list for user %s",
242                                                       filename, entry->lineno,
243                                                       vp->name, vp->name,
244                                                       entry->name);
245                                         } else {
246                                                 DEBUG("\tChanging '%s =' to '%s =='",
247                                                       vp->name, vp->name);
248                                         }
249                                         vp->operator = T_OP_CMP_EQ;
250                                         continue;
251                                 }
252                                 
253                                 /*
254                                  *      Cistron Compatibility mode.
255                                  *
256                                  *      Re-write selected attributes
257                                  *      to be '+=', instead of '='.
258                                  *
259                                  *      All others get set to '=='
260                                  */
261                                 if (compat_mode) {
262                                         /*
263                                          *      Non-wire attributes become +=
264                                          *
265                                          *      On the write attributes
266                                          *      become ==
267                                          */
268                                         if ((vp->attribute >= 0x100) &&
269                                             (vp->attribute <= 0xffff) &&
270                                             (vp->attribute != PW_HINT) &&
271                                             (vp->attribute != PW_HUNTGROUP_NAME)) {
272                                                 DEBUG("\tChanging '%s =' to '%s +='",
273                                                       vp->name, vp->name);
274                                                 vp->operator = T_OP_ADD;
275                                         } else {
276                                                 DEBUG("\tChanging '%s =' to '%s =='",
277                                                       vp->name, vp->name);
278                                                 vp->operator = T_OP_CMP_EQ;
279                                         }
280                                 }
281                                 
282                         } /* end of loop over check items */
283                 
284                 
285                         /*
286                          *      Look for server configuration items
287                          *      in the reply list.
288                          *
289                          *      It's a common enough mistake, that it's
290                          *      worth doing.
291                          */
292                         for (vp = entry->reply; vp != NULL; vp = vp->next) {
293                                 /*
294                                  *      If it's NOT a vendor attribute,
295                                  *      and it's NOT a wire protocol
296                                  *      and we ignore Fall-Through,
297                                  *      then bitch about it, giving a
298                                  *      good warning message.
299                                  */
300                                 if (!(vp->attribute & ~0xffff) &&
301                                     (vp->attribute > 0xff) &&
302                                     (vp->attribute > 1000)) {
303                                         log_debug("[%s]:%d WARNING! Check item \"%s\"\n"
304                                                   "\tfound in reply item list for user \"%s\".\n"
305                                                   "\tThis attribute MUST go on the first line"
306                                                   " with the other check items", 
307                                                   filename, entry->lineno, vp->name,
308                                                   entry->name);
309                                 }
310                         }
311                 
312                         entry = entry->next;
313                 }
314         
315         }
316
317         *pair_list = users;
318         return 0;
319 }
320
321 /*
322  *      (Re-)read the "users" file into memory.
323  */
324 static int file_instantiate(CONF_SECTION *conf, void **instance)
325 {
326         struct file_instance *inst;
327         int rcode;
328
329         inst = malloc(sizeof *inst);
330         if (!inst) {
331                 radlog(L_ERR|L_CONS, "Out of memory\n");
332                 return -1;
333         }
334
335         if (cf_section_parse(conf, module_config) < 0) {
336                 free(inst);
337                 return -1;
338         }
339
340         inst->usersfile = config.usersfile;
341         inst->acctusersfile = config.acctusersfile;
342         config.usersfile = NULL;
343         config.acctusersfile = NULL;
344
345         rcode = getusersfile(inst->usersfile, &inst->users);
346         if (rcode != 0) {
347                 radlog(L_ERR|L_CONS, "Errors reading %s", inst->usersfile);
348                 free(inst->usersfile);
349                 free(inst->acctusersfile);
350                 free(inst);
351                 return -1;
352         }
353
354         rcode = getusersfile(inst->acctusersfile, &inst->acctusers);
355         if (rcode != 0) {
356                 radlog(L_ERR|L_CONS, "Errors reading %s", inst->acctusersfile);
357                 pairlist_free(&inst->users);
358                 free(inst->usersfile);
359                 free(inst->acctusersfile);
360                 free(inst);
361                 return -1;
362         }
363
364         *instance = inst;
365         return 0;
366 }
367
368 /*
369  *      Find the named user in the database.  Create the
370  *      set of attribute-value pairs to check and reply with
371  *      for this user from the database. The main code only
372  *      needs to check the password, the rest is done here.
373  */
374 static int file_authorize(void *instance, REQUEST *request)
375 {
376         VALUE_PAIR      *namepair;
377         VALUE_PAIR      *request_pairs;
378         VALUE_PAIR      *check_tmp;
379         VALUE_PAIR      *reply_tmp;
380         PAIR_LIST       *pl;
381         int             found = 0;
382 #if defined(WITH_DBM) || defined(WITH_NDBM)
383         int             i, r;
384         char            buffer[256];
385 #endif
386         const char      *name;
387         struct file_instance *inst = instance;
388         VALUE_PAIR **check_pairs, **reply_pairs;
389         VALUE_PAIR *check_save;
390
391
392         request_pairs = request->packet->vps;
393         check_pairs = &request->config_items;
394         reply_pairs = &request->reply->vps;
395
396         /*
397          *      Grab the canonical user name.
398          */
399         namepair = request->username;
400         name = namepair ? (char *) namepair->strvalue : "NONE";
401
402         /*
403          *      Find the entry for the user.
404          */
405 #if defined(WITH_DBM) || defined(WITH_NDBM)
406         /*
407          *      FIXME: move to rlm_dbm.c
408          */
409         if (use_dbm) {
410                 /*
411                  *      FIXME: No Prefix / Suffix support for DBM.
412                  */
413 #ifdef WITH_DBM
414                 if (dbminit(inst->usersfile) != 0)
415 #endif
416 #ifdef WITH_NDBM
417                 if ((dbmfile = dbm_open(inst->usersfile, O_RDONLY, 0)) == NULL)
418 #endif
419                 {
420                         radlog(L_ERR|L_CONS, "cannot open dbm file %s",
421                                 buffer);
422                         return RLM_MODULE_FAIL;
423                 }
424
425                 r = dbm_find(dbmfile, name, request_pairs, check_pairs,
426                              reply_pairs);
427                 if (r > 0) found = 1;
428                 if (r <= 0 || fallthrough(*reply_pairs)) {
429
430                         pairdelete(reply_pairs, PW_FALL_THROUGH);
431
432                         sprintf(buffer, "DEFAULT");
433                         i = 0;
434                         while ((r = dbm_find(dbmfile, buffer, request_pairs,
435                                check_pairs, reply_pairs)) >= 0 || i < 2) {
436                                 if (r > 0) {
437                                         found = 1;
438                                         if (!fallthrough(*reply_pairs))
439                                                 break;
440                                         pairdelete(reply_pairs,PW_FALL_THROUGH);
441                                 }
442                                 sprintf(buffer, "DEFAULT%d", i++);
443                         }
444                 }
445 #ifdef WITH_DBM
446                 dbmclose();
447 #endif
448 #ifdef WITH_NDBM
449                 dbm_close(dbmfile);
450 #endif
451         } else
452         /*
453          *      Note the fallthrough through the #endif.
454          */
455 #endif
456
457         for(pl = inst->users; pl; pl = pl->next) {
458                 /*
459                  *      If the current entry is NOT a default,
460                  *      AND the name does NOT match the current entry,
461                  *      then skip to the next entry.
462                  */
463                 if ((strcmp(pl->name, "DEFAULT") != 0) &&
464                     (strcmp(name, pl->name) != 0))  {
465                         continue;
466                 }
467
468                 /*
469                  *      If the current request matches against the
470                  *      check pairs, then add the reply pairs from the
471                  *      entry to the current list of reply pairs.
472                  */
473                 if ((paircmp(request_pairs, pl->check, reply_pairs) == 0)) {
474
475                         if((mainconfig.do_usercollide) && (strcmp(pl->name, "DEFAULT"))) {
476
477                                 /* 
478                                  * We have to make sure the password
479                                  * matches as well
480                                  */
481         
482                                 /* Save the orginal config items */
483                                 check_save = paircopy(request->config_items);
484         
485                                 /* Copy this users check pairs to the request */
486                                 check_tmp = paircopy(pl->check);
487                                 pairmove(check_pairs, &check_tmp);
488                                 pairfree(check_tmp);
489         
490                                 DEBUG2("  users: Checking %s at %d", pl->name, pl->lineno);
491                                 /* Check the req to see if we matched */
492                                 if(rad_check_password(request)==0) {
493                                         DEBUG2("  users: Matched %s at %d", pl->name, pl->lineno);
494
495                                         found = 1;
496
497                                         /* Free our saved config items */
498                                         pairfree(check_save);
499
500                                         /* 
501                                          * Already copied check items, so 
502                                          * just copy reply here
503                                          */
504                                         reply_tmp = paircopy(pl->reply);
505                                         pairmove(reply_pairs, &reply_tmp);
506                                         pairfree(reply_tmp);
507
508                                 /* We didn't match here */
509                                 } else {
510                                         /* Restore check items */
511                                         pairfree(request->config_items);
512                                         request->config_items = paircopy(check_save);
513                                         check_pairs = &request->config_items;
514                                         continue;
515                                 }
516         
517                         /* No usercollide */
518                         } else {
519                 
520                                 DEBUG2("  users: Matched %s at %d", pl->name, pl->lineno);
521                                 found = 1;
522                                 check_tmp = paircopy(pl->check);
523                                 reply_tmp = paircopy(pl->reply);
524                                 pairmove(reply_pairs, &reply_tmp);
525                                 pairmove(check_pairs, &check_tmp);
526                                 pairfree(reply_tmp);
527                                 pairfree(check_tmp); /* should be NULL */
528                         }
529                         /*
530                          *      Fallthrough?
531                          */
532                         if (!fallthrough(pl->reply))
533                                 break;
534                 }
535         }
536         
537         /*
538          *      See if we succeeded.  If we didn't find the user,
539          *      then exit from the module.
540          */
541         if (!found)
542                 return RLM_MODULE_NOTFOUND;
543
544         /*
545          *      Remove server internal parameters.
546          */
547         pairdelete(reply_pairs, PW_FALL_THROUGH);
548
549         return RLM_MODULE_OK;
550 }
551
552 /*
553  *      Authentication - unused.
554  */
555 static int file_authenticate(void *instance, REQUEST *request)
556 {
557         instance = instance;
558         request = request;
559         return RLM_MODULE_OK;
560 }
561
562 /*
563  *      Pre-Accounting - read the acct_users file for check_items and
564  *      config_items. Reply items are Not Recommended(TM) in acct_users,
565  *      except for Fallthrough, which should work
566  *
567  *      This function is mostly a copy of file_authorize
568  */
569 static int file_preacct(void *instance, REQUEST *request)
570 {
571         VALUE_PAIR      *namepair;
572         const char      *name;
573         VALUE_PAIR      *request_pairs;
574         VALUE_PAIR      **config_pairs;
575         VALUE_PAIR      *reply_pairs = NULL;
576         VALUE_PAIR      *check_tmp;
577         VALUE_PAIR      *reply_tmp;
578         PAIR_LIST       *pl;
579         int             found = 0;
580 #if defined(WITH_DBM) || defined(WITH_NDBM)
581         int             i, r;
582         char            buffer[256];
583 #endif
584         struct file_instance *inst = instance;
585
586         namepair = request->username;
587         name = namepair ? (char *) namepair->strvalue : "NONE";
588         request_pairs = request->packet->vps;
589         config_pairs = &request->config_items;
590         
591         /*
592          *      Find the entry for the user.
593          */
594 #if defined(WITH_DBM) || defined(WITH_NDBM)
595         /*
596          *      FIXME: move to rlm_dbm.c
597          */
598         if (use_dbm) {
599                 /*
600                  *      FIXME: No Prefix / Suffix support for DBM.
601                  */
602 #ifdef WITH_DBM
603                 if (dbminit(inst->acctusersfile) != 0)
604 #endif
605 #ifdef WITH_NDBM
606                 if ((dbmfile = dbm_open(inst->acctusersfile, O_RDONLY, 0)) == NULL)
607 #endif
608                 {
609                         radlog(L_ERR|L_CONS, "cannot open dbm file %s",
610                                 buffer);
611                         return RLM_MODULE_FAIL;
612                 }
613
614                 r = dbm_find(dbmfile, name, request_pairs, config_pairs,
615                              &reply_pairs);
616                 if (r > 0) found = 1;
617                 if (r <= 0 || fallthrough(*reply_pairs)) {
618
619                   pairdelete(reply_pairs, PW_FALL_THROUGH);
620
621                         sprintf(buffer, "DEFAULT");
622                         i = 0;
623                         while ((r = dbm_find(dbmfile, buffer, request_pairs,
624                                config_pairs, &reply_pairs)) >= 0 || i < 2) {
625                                 if (r > 0) {
626                                         found = 1;
627                                         if (!fallthrough(*reply_pairs))
628                                                 break;
629                                         pairdelete(reply_pairs,PW_FALL_THROUGH);
630                                 }
631                                 sprintf(buffer, "DEFAULT%d", i++);
632                         }
633                 }
634 #ifdef WITH_DBM
635                 dbmclose();
636 #endif
637 #ifdef WITH_NDBM
638                 dbm_close(dbmfile);
639 #endif
640         } else
641         /*
642          *      Note the fallthrough through the #endif.
643          */
644 #endif
645
646         for(pl = inst->acctusers; pl; pl = pl->next) {
647
648                 if (strcmp(name, pl->name) && strcmp(pl->name, "DEFAULT"))
649                         continue;
650
651                 if (paircmp(request_pairs, pl->check, &reply_pairs) == 0) {
652                         DEBUG2("  acct_users: Matched %s at %d",
653                                pl->name, pl->lineno);
654                         found = 1;
655                         check_tmp = paircopy(pl->check);
656                         reply_tmp = paircopy(pl->reply);
657                         pairmove(&reply_pairs, &reply_tmp);
658                         pairmove(config_pairs, &check_tmp);
659                         pairfree(reply_tmp);
660                         pairfree(check_tmp); /* should be NULL */
661                         /*
662                          *      Fallthrough?
663                          */
664                         if (!fallthrough(pl->reply))
665                                 break;
666                 }
667         }
668
669         /*
670          *      See if we succeeded.
671          */
672         if (!found)
673                 return RLM_MODULE_NOOP; /* on to the next module */
674
675         /*
676          *      FIXME: log a warning if there are any reply items other than
677          *      Fallthrough
678          */
679         pairfree(reply_pairs); /* Don't need these */
680
681         return RLM_MODULE_OK;
682 }
683
684 /*
685  *      Clean up.
686  */
687 static int file_detach(void *instance)
688 {
689         struct file_instance *inst = instance;
690         pairlist_free(&inst->users);
691         pairlist_free(&inst->acctusers);
692         free(inst->usersfile);
693         free(inst->acctusersfile);
694         free(inst);
695         return 0;
696 }
697
698
699 /* globally exported name */
700 module_t rlm_files = {
701         "files",
702         0,                              /* type: reserved */
703         file_init,                      /* initialization */
704         file_instantiate,               /* instantiation */
705         file_authorize,                 /* authorization */
706         file_authenticate,              /* authentication */
707         file_preacct,                   /* preaccounting */
708         NULL,                           /* accounting */
709         NULL,                           /* checksimul */
710         file_detach,                    /* detach */
711         NULL                            /* destroy */
712 };
713