Coverage Report

Created: 2026-09-28 06:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/freeradius-server/src/lib/server/users_file.c
Line
Count
Source
1
/*
2
 * files.c  Read config files into memory.
3
 *
4
 * Version:     $Id: 8464bd60ea30cd61f2f103fe9fb0c1904817fde6 $
5
 *
6
 *   This program is free software; you can redistribute it and/or modify
7
 *   it under the terms of the GNU General Public License as published by
8
 *   the Free Software Foundation; either version 2 of the License, or
9
 *   (at your option) any later version.
10
 *
11
 *   This program is distributed in the hope that it will be useful,
12
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 *   GNU General Public License for more details.
15
 *
16
 *   You should have received a copy of the GNU General Public License
17
 *   along with this program; if not, write to the Free Software
18
 *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
19
 *
20
 * @copyright 2000,2006 The FreeRADIUS server project
21
 * @copyright 2000 Miquel van Smoorenburg (miquels@cistron.nl)
22
 * @copyright 2000 Alan DeKok (aland@freeradius.org)
23
 */
24
25
RCSID("$Id: 8464bd60ea30cd61f2f103fe9fb0c1904817fde6 $")
26
27
#include <freeradius-devel/server/log.h>
28
#include <freeradius-devel/util/debug.h>
29
#include <freeradius-devel/server/users_file.h>
30
31
#include <freeradius-devel/util/misc.h>
32
#include <freeradius-devel/util/pair_legacy.h>
33
#include <freeradius-devel/util/syserror.h>
34
35
#include <sys/stat.h>
36
37
#include <fcntl.h>
38
39
static int pairlist_read_internal(TALLOC_CTX *ctx, fr_dict_t const *dict, char const *file, PAIR_LIST_LIST *list,
40
          bool complain, bool v3_compat, int *order);
41
42
static inline void line_error_marker(char const *src_file, int src_line,
43
             char const *user_file, int user_line,
44
             fr_sbuff_t *sbuff, char const *error)
45
0
{
46
0
  char      *end;
47
0
  fr_sbuff_marker_t start;
48
49
0
  fr_sbuff_marker(&start, sbuff);
50
0
  end = fr_sbuff_adv_to_chr(sbuff, SIZE_MAX, '\n');
51
0
  if (!end) end = fr_sbuff_end(sbuff);
52
0
  fr_sbuff_set(sbuff, &start);
53
0
  fr_sbuff_marker_release(&start);
54
55
0
  fr_log_marker(LOG_DST, L_ERR, src_file, src_line,
56
0
          fr_sbuff_start(sbuff), end - fr_sbuff_start(sbuff),
57
0
          fr_sbuff_used(sbuff), error, "%s[%d]: ", user_file, user_line);
58
0
}
59
/** Print out a line oriented error marker at the current position of the sbuff
60
 *
61
 * @param[in] _sbuff  to print error for.
62
 * @param[in] _error  message.
63
 */
64
0
#define ERROR_MARKER(_sbuff, _error) line_error_marker(__FILE__, __LINE__, file, lineno, _sbuff, _error)
65
66
static inline void line_error_marker_adj(char const *src_file, int src_line,
67
           char const *user_file, int user_line,
68
           fr_sbuff_t *sbuff, ssize_t marker_idx, char const *error)
69
0
{
70
0
  char      *end;
71
0
  fr_sbuff_marker_t start;
72
73
0
  fr_sbuff_marker(&start, sbuff);
74
0
  end = fr_sbuff_adv_to_chr(sbuff, SIZE_MAX, '\n');
75
0
  if (!end) end = fr_sbuff_end(sbuff);
76
0
  fr_sbuff_set(sbuff, &start);
77
0
  fr_sbuff_marker_release(&start);
78
79
0
  fr_log_marker(LOG_DST, L_ERR, src_file, src_line,
80
0
          fr_sbuff_current(sbuff), end - fr_sbuff_current(sbuff),
81
0
          marker_idx, error, "%s[%d]: ", user_file, user_line);
82
0
}
83
/** Print out a line oriented error marker relative to the current position of the sbuff
84
 *
85
 * @param[in] _sbuff  to print error for.
86
 * @param[in] _idx  Where the error occurred.
87
 * @param[in] _error  message.
88
 */
89
0
#define ERROR_MARKER_ADJ(_sbuff, _idx, _error) line_error_marker_adj(__FILE__, __LINE__, file, lineno, _sbuff, _idx, _error)
90
91
static fr_table_num_sorted_t const check_cmp_op_table[] = {
92
  { L("!*"),  T_OP_CMP_FALSE    },
93
  { L("!="),  T_OP_NE     },
94
  { L("!~"),  T_OP_REG_NE   },
95
  { L("+="),  T_OP_ADD_EQ   },
96
  { L(":="),  T_OP_SET    },
97
  { L("<"), T_OP_LT     },
98
  { L("<="),  T_OP_LE     },
99
  { L("="), T_OP_EQ     },
100
  { L("=*"),  T_OP_CMP_TRUE   },
101
  { L("=="),  T_OP_CMP_EQ   },
102
  { L("=~"),  T_OP_REG_EQ   },
103
  { L(">"), T_OP_GT     },
104
  { L(">="),  T_OP_GE     }
105
};
106
static size_t check_cmp_op_table_len = NUM_ELEMENTS(check_cmp_op_table);
107
108
static const fr_sbuff_term_t name_terms = FR_SBUFF_TERMS(
109
    L("\t"),
110
    L("\n"),
111
    L(" "),
112
    L("#"),
113
);
114
115
static fr_sbuff_parse_rules_t const rhs_term = {
116
  .escapes = &(fr_sbuff_unescape_rules_t){
117
    .chr = '\\',
118
    /*
119
     *  Allow barewords to contain whitespace
120
     *  if they're escaped.
121
     */
122
    .subs = {
123
      ['\t'] = '\t',
124
      ['\n'] = '\n',
125
      [' '] = ' '
126
    },
127
    .do_hex = true,
128
    .do_oct = false
129
  },
130
  .terminals = &FR_SBUFF_TERMS(
131
    L(""),
132
    L("\t"),
133
    L("\n"),
134
    L("#"),
135
    L(","),
136
  )
137
};
138
139
/*
140
 *  Caller saw a $INCLUDE at the start of a line.
141
 */
142
static int users_include(TALLOC_CTX *ctx, fr_dict_t const *dict, fr_sbuff_t *sbuff, PAIR_LIST_LIST *list,
143
       char const *file, int lineno, bool v3_compat, int *order)
144
0
{
145
0
  size_t    len;
146
0
  char    *newfile, c;
147
0
  char const  *p;
148
0
  fr_sbuff_marker_t name;
149
150
0
  fr_sbuff_advance(sbuff, 8);
151
152
  /*
153
   *  Skip spaces after the $INCLUDE.
154
   */
155
0
  if (fr_sbuff_adv_past_blank(sbuff, SIZE_MAX, NULL) == 0) {
156
0
        ERROR_MARKER(sbuff, "Unexpected text after $INCLUDE");
157
0
    return -1;
158
0
  }
159
160
  /*
161
   *  Remember when the name started, and skip over the name
162
   *  until spaces, comments, or LF
163
   */
164
0
  fr_sbuff_marker(&name, sbuff);
165
0
  len = fr_sbuff_adv_until(sbuff, SIZE_MAX, &name_terms, 0);
166
0
  if (len == 0) {
167
0
        ERROR_MARKER(sbuff, "No filename after $INCLUDE");
168
0
    fr_sbuff_marker_release(&name);
169
0
    return -1;
170
0
  }
171
172
  /*
173
   *  If the input file is a relative path, try to copy the
174
   *  leading directory from it.  If there's no leading
175
   *  directory, just use the $INCLUDE name as-is.
176
   *
177
   *  If there is a leading directory in the input name,
178
   *  paste that as the directory to the $INCLUDE name.
179
   *
180
   *  Otherwise the $INCLUDE name is an absolute path, use
181
   *  it as -is.
182
   */
183
0
  c = fr_sbuff_char(&name, '\0');
184
0
  if (c != '/') {
185
0
    p = strrchr(file, '/');
186
187
0
    if (!p) goto copy_name;
188
189
0
    newfile = talloc_asprintf(NULL, "%.*s/%.*s",
190
0
            (int) (p - file), file,
191
0
            (int) len, fr_sbuff_current(&name));
192
0
  } else {
193
0
  copy_name:
194
0
    newfile = talloc_asprintf(NULL, "%.*s", (int) len, fr_sbuff_current(&name));
195
0
  }
196
0
  fr_sbuff_marker_release(&name);
197
198
  /*
199
   *  Skip spaces and comments after the name.
200
   */
201
0
  fr_sbuff_adv_past_blank(sbuff, SIZE_MAX, NULL);
202
0
  if (fr_sbuff_next_if_char(sbuff, '#')) {
203
0
    (void) fr_sbuff_adv_to_chr(sbuff, SIZE_MAX, '\n');
204
0
  }
205
206
  /*
207
   *  There's no LF, but if we skip non-spaces and
208
   *  non-comments to find the LF, then there must be extra
209
   *  text after the filename.  That's an error.
210
   *
211
   *  Unless the line has EOF after the filename.  in which
212
   *  case this error will get hit, too.
213
   */
214
0
  if (!fr_sbuff_is_char(sbuff, '\n') &&
215
0
      (fr_sbuff_adv_to_chr(sbuff, SIZE_MAX, '\n') != NULL)) {
216
0
        ERROR_MARKER(sbuff, "Unexpected text after filename");
217
0
    talloc_free(newfile);
218
0
    return -1;
219
0
  }
220
221
  /*
222
   *  Read the $INCLUDEd file recursively.
223
   */
224
0
  if (pairlist_read_internal(ctx, dict, newfile, list, false, v3_compat, order) != 0) {
225
0
    ERROR("%s[%d]: Could not read included file %s: %s",
226
0
          file, lineno, newfile, fr_syserror(errno));
227
0
    talloc_free(newfile);
228
0
    return -1;
229
0
  }
230
0
  talloc_free(newfile);
231
232
0
  return 0;
233
0
}
234
235
int pairlist_read(TALLOC_CTX *ctx, fr_dict_t const *dict, char const *file, PAIR_LIST_LIST *list, bool v3_compat)
236
0
{
237
0
  int order = 0;
238
239
0
  return pairlist_read_internal(ctx, dict, file, list, true, v3_compat, &order);
240
0
}
241
242
/*
243
 *  Read the users file. Return a PAIR_LIST.
244
 */
245
static int pairlist_read_internal(TALLOC_CTX *ctx, fr_dict_t const *dict, char const *file, PAIR_LIST_LIST *list, bool complain, bool v3_compat, int *order)
246
0
{
247
0
  char      *q;
248
0
  int     lineno    = 1;
249
0
  map_t     *new_map, *relative_map;
250
0
  FILE      *fp;
251
0
  fr_sbuff_t    sbuff;
252
0
  fr_sbuff_uctx_file_t  fctx;
253
0
  tmpl_rules_t    lhs_rules, rhs_rules;
254
0
  char      *filename = talloc_strdup(ctx, file);
255
0
  char      buffer[8192];
256
257
0
  DEBUG2("Reading file %s", file);
258
259
  /*
260
   *  Open the file.  The error message should be a little
261
   *  more useful...
262
   */
263
0
  if ((fp = fopen(file, "r")) == NULL) {
264
0
    if (complain) ERROR("Couldn't open %s for reading: %s", file, fr_syserror(errno));
265
0
    return -1;
266
0
  }
267
268
0
  fr_sbuff_init_file(&sbuff, &fctx, buffer, sizeof(buffer), fp, SIZE_MAX);
269
270
0
  lhs_rules = (tmpl_rules_t) {
271
0
    .attr = {
272
0
      .dict_def = dict,
273
0
      .list_def = request_attr_request,
274
0
      .list_presence = TMPL_ATTR_LIST_ALLOW,
275
0
    },
276
0
    .literals_safe_for = FR_VALUE_BOX_SAFE_FOR_ANY,
277
278
0
  };
279
0
  rhs_rules = (tmpl_rules_t) {
280
0
    .attr = {
281
0
      .dict_def = dict,
282
0
      .list_def = request_attr_request,
283
0
      .list_presence = TMPL_ATTR_LIST_ALLOW,
284
0
      .bare_word_enum = v3_compat,
285
0
    },
286
0
    .literals_safe_for = FR_VALUE_BOX_SAFE_FOR_ANY,
287
0
  };
288
289
0
  while (true) {
290
0
    size_t    len;
291
0
    ssize_t   slen;
292
0
    bool    comma;
293
0
    bool    leading_spaces;
294
0
    PAIR_LIST *t;
295
296
    /*
297
     *  If the line is empty or has only comments,
298
     *  then we don't care about leading spaces.
299
     */
300
0
    leading_spaces = (fr_sbuff_adv_past_blank(&sbuff, SIZE_MAX, NULL) > 0);
301
0
    if (fr_sbuff_next_if_char(&sbuff, '#')) {
302
0
      (void) fr_sbuff_adv_to_chr(&sbuff, SIZE_MAX, '\n');
303
0
    }
304
0
    if (fr_sbuff_next_if_char(&sbuff, '\n')) {
305
0
      lineno++;
306
0
      continue;
307
0
    }
308
309
    /*
310
     *  We're trying to read a key value.  It MUST have
311
     *  been at the start of the line.  So whatever
312
     *  this is, it's wrong.
313
     */
314
0
    if (leading_spaces) {
315
0
          ERROR_MARKER(&sbuff, "Entry does not begin with a key value");
316
0
    fail:
317
0
      fclose(fp);
318
0
      return -1;
319
0
    }
320
321
    /*
322
     *  $INCLUDE filename.  This will do some sanity checks, and then add the new entries to
323
     *  the tail of the current list.
324
     */
325
0
    if (fr_sbuff_is_str(&sbuff, "$INCLUDE", 8)) {
326
0
      if (users_include(ctx, dict, &sbuff, list, file, lineno, v3_compat, order) < 0) goto fail;
327
328
0
      if (fr_sbuff_next_if_char(&sbuff, '\n')) {
329
0
        lineno++;
330
0
        continue;
331
0
      }
332
333
      /*
334
       *  The next character is not LF, but the
335
       *  function skipped to LF.  So, by
336
       *  process of elimination, we must be at
337
       *  EOF.
338
       */
339
0
      break;
340
0
    } /* else it wasn't $INCLUDE */
341
342
    /*
343
     *  We MUST be either at a valid entry, OR at EOF.
344
     */
345
0
    MEM(t = talloc_zero(ctx, PAIR_LIST));
346
0
    map_list_init(&t->check);
347
0
    map_list_init(&t->reply);
348
0
    t->filename = filename;
349
0
    t->lineno = lineno;
350
0
    t->order = (*order)++;
351
352
    /*
353
     *  Copy the name from the entry.
354
     */
355
0
    if (fr_sbuff_out_abstrncpy_until(t, &q, &len, &sbuff, SIZE_MAX, &name_terms, NULL) < 0) {
356
0
      ERROR_MARKER(&sbuff, "Failed reading entry name");
357
0
      talloc_free(t);
358
0
      goto fail;
359
0
    }
360
0
    if (len == 0) {
361
0
      talloc_free(t);
362
0
      break;
363
0
    }
364
0
    t->name = q;
365
366
0
    lhs_rules.attr.list_def = request_attr_control;
367
0
    comma = false;
368
369
0
check_item:
370
    /*
371
     *  Skip spaces before the item, and allow the
372
     *  check list to end on comment or LF.
373
     *
374
     *  Note that we _don't_ call map_afrom_substr() to
375
     *  skip spaces, as it will skip LF, too!
376
     */
377
0
    (void) fr_sbuff_adv_past_blank(&sbuff, SIZE_MAX, NULL);
378
0
    if (fr_sbuff_is_char(&sbuff, '#')) goto check_item_comment;
379
0
    if (fr_sbuff_is_char(&sbuff, '\n')) goto check_item_end;
380
381
    /*
382
     *  Try to parse the check item.
383
     */
384
0
    slen = map_afrom_substr(t, &new_map, NULL, &sbuff, check_cmp_op_table, check_cmp_op_table_len,
385
0
               &lhs_rules, &rhs_rules, &rhs_term);
386
0
    if (!new_map) {
387
0
          ERROR_MARKER_ADJ(&sbuff, slen, fr_strerror());
388
0
    fail_entry:
389
0
      talloc_free(t);
390
0
      goto fail;
391
0
    }
392
393
0
    fr_assert(new_map->lhs != NULL);
394
0
    fr_assert(new_map->rhs != NULL);
395
396
    /*
397
     *  The default rule says that the check
398
     *  items look at the control list, but
399
     *  protocol attributes should be compared in the request.
400
     */
401
0
    if (!tmpl_attr_tail_da(new_map->lhs)->flags.internal) {
402
0
      tmpl_attr_set_list(new_map->lhs, request_attr_request);
403
0
    }
404
405
0
    if (tmpl_contains_regex(new_map->rhs)) {
406
0
      if (tmpl_is_regex_uncompiled(new_map->rhs) &&
407
0
          (tmpl_regex_compile(new_map->rhs, false) < 0)) {
408
0
        ERROR("%s[%d]: Failed compiling regular expression /%s/ - %s",
409
0
              file, lineno, new_map->rhs->name, fr_strerror());
410
0
        goto fail_entry;
411
0
      }
412
413
0
      goto do_insert;
414
0
    }
415
416
0
  do_insert:
417
0
    fr_assert(!new_map->parent);
418
0
    map_list_insert_tail(&t->check, new_map);
419
420
    /*
421
     *  There can be spaces before any comma.
422
     */
423
0
    (void) fr_sbuff_adv_past_blank(&sbuff, SIZE_MAX, NULL);
424
425
    /*
426
     *  Allow a comma after this item.  But remember
427
     *  if we had a comma.
428
     */
429
0
    if (fr_sbuff_next_if_char(&sbuff, ',')) {
430
0
      comma = true;
431
0
      goto check_item;
432
0
    }
433
0
    comma = false;
434
435
0
  check_item_comment:
436
    /*
437
     *  There wasn't a comma after the item, so the
438
     *  next thing MUST be a comment, LF, EOF.
439
     *
440
     *  If there IS stuff before the LF, then it's
441
     *  unknown text.
442
     */
443
0
    if (fr_sbuff_next_if_char(&sbuff, '#')) {
444
0
      (void) fr_sbuff_adv_to_chr(&sbuff, SIZE_MAX, '\n');
445
0
    }
446
0
  check_item_end:
447
0
    if (fr_sbuff_next_if_char(&sbuff, '\n')) {
448
      /*
449
       *  The check item list ended with a comma.
450
       *  That's bad.
451
       */
452
0
      if (comma) {
453
0
        ERROR_MARKER(&sbuff, "Invalid comma ending the check item list");
454
0
        goto fail_entry;
455
0
      }
456
457
0
      lineno++;
458
0
      goto setup_reply;
459
0
    }
460
461
    /*
462
     *  We didn't see SPACE LF or SPACE COMMENT LF.
463
     *  There's something else going on.
464
     */
465
0
    if (fr_sbuff_adv_to_chr(&sbuff, SIZE_MAX, '\n') != NULL) {
466
0
      ERROR_MARKER(&sbuff, "Unexpected text after check item");
467
0
      goto fail_entry;
468
0
    }
469
470
    /*
471
     *  The next character is not LF, but we
472
     *  skipped to LF above.  So, by process
473
     *  of elimination, we must be at EOF.
474
     */
475
0
    if (!fr_sbuff_is_char(&sbuff, '\n')) {
476
0
    add_entry:
477
0
      fr_dlist_insert_tail(&list->head, t);
478
0
      break;
479
0
    }
480
481
0
setup_reply:
482
    /*
483
     *  Setup the reply items.
484
     */
485
0
    lhs_rules.attr.list_def = request_attr_reply;
486
487
0
    comma = false;
488
489
0
    rhs_rules.attr.list_def = request_attr_request;
490
491
0
    relative_map = NULL;
492
493
0
reply_item:
494
    /*
495
     *  Reply items start with spaces.  If there's no
496
     *  spaces, then the current entry is done.  Add
497
     *  it to the list, and go back to reading the
498
     *  user name or $INCLUDE.
499
     */
500
0
    if (fr_sbuff_adv_past_blank(&sbuff, SIZE_MAX, NULL) == 0) {
501
0
      if (comma) {
502
0
        ERROR("%s[%d]: Unexpected trailing comma in previous line", file, lineno);
503
0
        goto fail_entry;
504
0
      }
505
506
      /*
507
       *  The line doesn't begin with spaces.
508
       *  The list of reply items MUST be
509
       *  finished.  Go look for an entry name.
510
       *
511
       *  Note that we don't allow comments in
512
       *  the middle of the reply item list.  Oh
513
       *  well.
514
       */
515
0
      fr_dlist_insert_tail(&list->head, t);
516
0
      continue;
517
518
0
    } else if (lineno == (t->lineno + 1)) {
519
0
      fr_assert(comma == false);
520
521
0
    } else if (!comma) {
522
0
      ERROR("%s[%d]: Missing comma in previous line", file, lineno);
523
0
      goto fail_entry;
524
0
    }
525
526
    /*
527
     *  SPACES COMMENT or SPACES LF means "end of
528
     *  reply item list"
529
     */
530
0
    if (fr_sbuff_is_char(&sbuff, '#')) {
531
0
      (void) fr_sbuff_adv_to_chr(&sbuff, SIZE_MAX, '\n');
532
0
    }
533
0
    if (fr_sbuff_next_if_char(&sbuff, '\n')) {
534
0
      lineno++;
535
0
      goto add_entry;
536
0
    }
537
538
0
next_reply_item:
539
    /*
540
     *  Unlike check items, we don't skip spaces or
541
     *  comments here.  All of the code paths which
542
     *  lead to here have already checked for those
543
     *  cases.
544
     */
545
0
    slen = map_afrom_substr(t, &new_map, &relative_map, &sbuff, map_assignment_op_table, map_assignment_op_table_len,
546
0
               &lhs_rules, &rhs_rules, &rhs_term);
547
0
    if (!new_map) {
548
0
      ERROR_MARKER_ADJ(&sbuff, slen, fr_strerror());
549
0
      goto fail_entry;
550
0
    }
551
552
0
    fr_assert(new_map->lhs != NULL);
553
554
0
    if (!tmpl_is_attr(new_map->lhs)) {
555
0
      ERROR("%s[%d]: Unknown attribute '%s'",
556
0
            file, lineno, new_map->lhs->name);
557
0
      goto fail_entry;
558
0
    }
559
560
    /*
561
     *  We no longer really care what the RHS is, or what the list is on the LHS.  The caller
562
     *  takes care of checking that for us.
563
     */
564
565
0
    if (!new_map->parent) map_list_insert_tail(&t->reply, new_map);
566
567
0
    (void) fr_sbuff_adv_past_blank(&sbuff, SIZE_MAX, NULL);
568
569
    /*
570
     *  Commas separate entries on the same line.  And
571
     *  we allow spaces after commas, too.
572
     */
573
0
    if (fr_sbuff_next_if_char(&sbuff, ',')) {
574
0
      comma = true;
575
0
      (void) fr_sbuff_adv_past_blank(&sbuff, SIZE_MAX, NULL);
576
0
    } else {
577
0
      comma = false;
578
0
    }
579
580
    /*
581
     *  Comments or LF will end this particular line.
582
     *
583
     *  Reading the next line will cause a complaint
584
     *  if this line ended with a comma.
585
     */
586
0
    if (fr_sbuff_next_if_char(&sbuff, '#')) {
587
0
      (void) fr_sbuff_adv_to_chr(&sbuff, SIZE_MAX, '\n');
588
0
    }
589
590
0
    if (fr_sbuff_next_if_char(&sbuff, '\n')) {
591
0
      lineno++;
592
0
      goto reply_item;
593
0
    }
594
595
    /*
596
     *  Not comment or LF, the content MUST be another
597
     *  pair.
598
     */
599
0
    if (comma) goto next_reply_item;
600
601
0
    ERROR_MARKER(&sbuff, "Unexpected text after reply");
602
0
    goto fail_entry;
603
0
  }
604
605
  /*
606
   *  Else we were looking for an entry.  We didn't get one
607
   *  because we were at EOF, so that's OK.
608
   */
609
610
0
  fclose(fp);
611
612
0
  return 0;
613
0
}