]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
add support for $VALUE{...} and $FILE{...}
authorAlan T. DeKok <aland@freeradius.org>
Tue, 9 Jun 2026 07:20:13 +0000 (10:20 +0300)
committerAlan T. DeKok <aland@freeradius.org>
Tue, 9 Jun 2026 09:12:12 +0000 (12:12 +0300)
which loads a single value (one line only) from a file, or loads
an entire file without change.

Add tests and documentation

14 files changed:
doc/antora/modules/reference/pages/raddb/format.adoc
src/lib/server/cf_file.c
src/tests/keywords/file [new file with mode: 0644]
src/tests/keywords/file-data.md [new file with mode: 0644]
src/tests/keywords/file-empty-error [new file with mode: 0644]
src/tests/keywords/file-missing-error [new file with mode: 0644]
src/tests/keywords/value [new file with mode: 0644]
src/tests/keywords/value-data.md [new file with mode: 0644]
src/tests/keywords/value-empty-error [new file with mode: 0644]
src/tests/keywords/value-empty.md [new file with mode: 0644]
src/tests/keywords/value-missing-error [new file with mode: 0644]
src/tests/keywords/value-multiline-error [new file with mode: 0644]
src/tests/keywords/value-multiline.md [new file with mode: 0644]
src/tests/keywords/value-unterminated-error [new file with mode: 0644]

index 59d542a5de53672dde24a485c43f6055327cdc6f..cde4c81aab127016bdd5258e4dc55d447d4c9e4c 100644 (file)
@@ -401,5 +401,67 @@ blogs = "this ${foo} is ${baz}"
 
 Will set variable `blogs` to the string `this bar is bug`.
 
+=== Environment Variables
+
+In addition to variable expansions as described above, it is also
+possible to refer to environment variables via the following syntax:
+
+.Loading value from an environment Variable
+====
+----
+blogs = "this $ENV{FOO}"
+----
+====
+
+If an environment variable does not exist, the server will exit with
+an error.  The name of the environment variable is a fixed string, and
+cannot be another reference.
+
+The expansion will copy the value of the environment variable exactly.
+
+=== Value from a file
+
+In addition to variable expansions as described above, it is also
+possible to refer to read a value from a file via the following syntax:
+
+.Loading value from a file
+====
+----
+blogs = "this $VALUE{foo.txt}"
+----
+====
+
+If the file does not exist, the server will exit with an error.  The
+filename is a fixed string, and cannot be another reference.  Both
+absolute and relative filenames are supported.  If the filename does
+not begin with '/', then it is taken as relative to the current file.
+
+The file must contain one line.  A file with multiple lines will return an error.
+
+The expansion will copy the contents of the file, and will remove any
+trailing CR / LF.
+
+=== Data from a file
+
+In addition to variable expansions as described above, it is also
+possible to refer to read an entire file via the following syntax:
+
+.Loading value from a file
+====
+----
+blogs = "this $FILE{foo.txt}"
+----
+====
+
+If the file does not exist, the server will exit with an error.  The
+filename is a fixed string, and cannot be another reference.  Both
+absolute and relative filenames are supported.  If the filename does
+not begin with '/', then it is taken as relative to the current file.
+
+The file can be binary, multiple lines, etc.
+
+The expansion will copy all of the contents of the file exactly,
+without changes.
+
 // Copyright (C) 2026 Network RADIUS SAS.  Licenced under CC-by-NC 4.0.
 // This documentation was developed by Network RADIUS SAS.
index 2e9ca9832629d30e85cf5027ed30764f1832db64..024dd68b013e511162487ccf31ed9ca14a93e569 100644 (file)
@@ -154,6 +154,140 @@ typedef struct {
        cf_stack_frame_t frame[MAX_STACK];      //!< stack frames
 } cf_stack_t;
 
+/*
+ *     Open and read a file.
+ */
+static int cf_expand_file(char const *cf, int lineno, char name[static PATH_MAX],
+                         char **p_p, char const **ptr_p, char *output, size_t outsize,
+                         bool raw)
+{
+       int fd;
+       size_t room;
+       ssize_t len;
+       char *p, *next;
+       char const *ptr;
+
+       /*
+        *      Note that we do NOT recursively expand the value.  It has to be a hard-coded string.
+        *
+        *      We do NOT do any sanity checks on the value.  i.e. filenames beginning with '/' are allowed,
+        *      as are files with "../../".  The value here comes from the configuration files, and only the
+        *      administrator has write access to them.
+        */
+       strlcpy(name, cf, PATH_MAX);
+       p = strrchr(name, '/');
+       if (p) {
+               p++;
+       } else {
+               p = name;
+       }
+
+       ptr = *ptr_p;
+
+       /*
+        *      Look for trailing '}', and log a
+        *      warning for anything that doesn't match,
+        *      and exit with a fatal error.
+        */
+       next = strchr(ptr, '}');
+       if (next == NULL) {
+               *p = '\0';
+               ERROR("%s[%d]: File expansion missing }",
+                     cf, lineno);
+               return -1;
+       }
+
+       /*
+        *      Can't really happen because input lines are
+        *      capped at 8k, which is sizeof(name)
+        */
+       if ((next - ptr) >= (name + PATH_MAX - p)) {
+               ERROR("%s[%d]: File name is too large",
+                     cf, lineno);
+               return -1;
+       }
+
+       memcpy(p, ptr, next - ptr);
+       p[next - ptr] = '\0';
+
+       fd = open(name, O_RDONLY);
+       if (fd < 0) {
+               ERROR("%s[%d]: Failed opening %s: %s",
+                     cf, lineno, name, strerror(errno));
+               return -1;
+       }
+
+       p = *p_p;
+       room = (output + outsize) - p;
+       fr_assert(room > 0);
+
+       /*
+        *      Read the raw data.
+        */
+       len = read(fd, p, room);
+       if (len < 0) {
+               ERROR("%s[%d]: Failed reading %s: %s",
+                     cf, lineno, name, strerror(errno));
+               close(fd);
+               return -1;
+       }
+       close(fd);
+
+       if (!len) {
+               ERROR("%s[%d]: Failed reading %s: the file is empty",
+                     cf, lineno, name);
+               return -1;
+       }
+
+       /*
+        *      We don't know whether or not it was
+        *      truncated, so we just error out.
+        */
+       if ((size_t) len >= room) {
+               ERROR("%s[%d]: Too much data in %s: did not read the entire file",
+                     cf, lineno, name);
+               return -1;
+       }
+
+       /*
+        *      If we're not reading the raw file, return only the first line.
+        */
+       if (!raw) {
+               char *q, *end = p + len;
+
+               while (p < end) {
+                       if (*p >= ' ') {
+                               p++;
+                               continue;
+                       }
+
+                       break;
+               }
+
+               /*
+                *      Strip trailing CR/LF.
+                */
+               for (q = p; q < end; q++) {
+                       if (*q >= ' ') break;
+
+                       *q = '\0';
+               }
+
+               if (q != end) {
+                       ERROR("%s[%d]: Too much data in %s: expected one line of text, found multiple lines in the file",
+                             cf, lineno, name);
+                       return -1;
+               }
+       } else {
+               p += len;
+       }
+
+       *ptr_p = next + 1;
+       *p_p = p;
+
+       return 0;
+}
+
 /*
  *     Expand the variables in an input string.
  *
@@ -168,7 +302,7 @@ char const *cf_expand_variables(char const *cf, int lineno,
        char *p;
        char const *end, *next, *ptr;
        CONF_SECTION const *parent_cs;
-       char name[8192];
+       char name[PATH_MAX];
 
        if (soft_fail) *soft_fail = false;
 
@@ -457,6 +591,16 @@ char const *cf_expand_variables(char const *cf, int lineno,
                        p += strlen(p);
                        ptr = next + 1;
 
+               } else if (strncmp(ptr, "$VALUE{", 7) == 0) {
+                       ptr += 7;
+
+                       if (cf_expand_file(cf, lineno, name, &p, &ptr, output, outsize, false) < 0) return NULL;
+
+               } else if (strncmp(ptr, "$FILE{", 6) == 0) {
+                       ptr += 6;
+
+                       if (cf_expand_file(cf, lineno, name, &p, &ptr, output, outsize, true) < 0) return NULL;
+
                } else {
                        /*
                         *      Copy it over verbatim.
diff --git a/src/tests/keywords/file b/src/tests/keywords/file
new file mode 100644 (file)
index 0000000..70abc62
--- /dev/null
@@ -0,0 +1,15 @@
+string test
+
+#
+#  $FILE{path} reads the entire file at config parse time and
+#  substitutes its raw contents into the input stream.  The data
+#  file used here has no trailing newline so the result is the
+#  same as $VALUE{} would produce.
+#
+test = "$FILE{file-data.md}"
+
+if (test != "hello\nvalue") {
+       test_fail
+}
+
+success
diff --git a/src/tests/keywords/file-data.md b/src/tests/keywords/file-data.md
new file mode 100644 (file)
index 0000000..788d1a3
--- /dev/null
@@ -0,0 +1,2 @@
+hello
+value
\ No newline at end of file
diff --git a/src/tests/keywords/file-empty-error b/src/tests/keywords/file-empty-error
new file mode 100644 (file)
index 0000000..ce7eb68
--- /dev/null
@@ -0,0 +1,6 @@
+string test
+
+#
+#  $FILE{path} fails when the file is empty.
+#
+test = "$FILE{value-empty.md}"                                    # ERROR
diff --git a/src/tests/keywords/file-missing-error b/src/tests/keywords/file-missing-error
new file mode 100644 (file)
index 0000000..d646d40
--- /dev/null
@@ -0,0 +1,6 @@
+string test
+
+#
+#  $FILE{path} fails when the file does not exist.
+#
+test = "$FILE{this-file-does-not-exist.md}"                       # ERROR
diff --git a/src/tests/keywords/value b/src/tests/keywords/value
new file mode 100644 (file)
index 0000000..06de22d
--- /dev/null
@@ -0,0 +1,13 @@
+string test
+
+#
+#  $VALUE{path} reads a single line from the named file at config
+#  parse time and substitutes its contents into the input stream.
+#
+test = "$VALUE{value-data.md}"
+
+if (test != "hello-value") {
+       test_fail
+}
+
+success
diff --git a/src/tests/keywords/value-data.md b/src/tests/keywords/value-data.md
new file mode 100644 (file)
index 0000000..3c31421
--- /dev/null
@@ -0,0 +1 @@
+hello-value
\ No newline at end of file
diff --git a/src/tests/keywords/value-empty-error b/src/tests/keywords/value-empty-error
new file mode 100644 (file)
index 0000000..362a4c4
--- /dev/null
@@ -0,0 +1,6 @@
+string test
+
+#
+#  $VALUE{path} fails when the file is empty.
+#
+test = "$VALUE{value-empty.md}"                                   # ERROR
diff --git a/src/tests/keywords/value-empty.md b/src/tests/keywords/value-empty.md
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/tests/keywords/value-missing-error b/src/tests/keywords/value-missing-error
new file mode 100644 (file)
index 0000000..eda2241
--- /dev/null
@@ -0,0 +1,6 @@
+string test
+
+#
+#  $VALUE{path} fails when the file does not exist.
+#
+test = "$VALUE{this-file-does-not-exist.md}"                      # ERROR
diff --git a/src/tests/keywords/value-multiline-error b/src/tests/keywords/value-multiline-error
new file mode 100644 (file)
index 0000000..2ca65a0
--- /dev/null
@@ -0,0 +1,7 @@
+string test
+
+#
+#  $VALUE{path} accepts only a single line of text.  A file that
+#  contains multiple non-blank lines is an error.
+#
+test = "$VALUE{value-multiline.md}"                               # ERROR
diff --git a/src/tests/keywords/value-multiline.md b/src/tests/keywords/value-multiline.md
new file mode 100644 (file)
index 0000000..e5c5c55
--- /dev/null
@@ -0,0 +1,2 @@
+line one
+line two
diff --git a/src/tests/keywords/value-unterminated-error b/src/tests/keywords/value-unterminated-error
new file mode 100644 (file)
index 0000000..264ea30
--- /dev/null
@@ -0,0 +1,6 @@
+string test
+
+#
+#  $VALUE{...} requires a closing "}".  Missing one is an error.
+#
+test = "$VALUE{value-data.md"                                     # ERROR