From: Alan T. DeKok Date: Tue, 9 Jun 2026 07:20:13 +0000 (+0300) Subject: add support for $VALUE{...} and $FILE{...} X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=a501bde85ba2ca310df4be5c0e39b1ac16ef3c1e;p=thirdparty%2Ffreeradius-server.git add support for $VALUE{...} and $FILE{...} which loads a single value (one line only) from a file, or loads an entire file without change. Add tests and documentation --- diff --git a/doc/antora/modules/reference/pages/raddb/format.adoc b/doc/antora/modules/reference/pages/raddb/format.adoc index 59d542a5de5..cde4c81aab1 100644 --- a/doc/antora/modules/reference/pages/raddb/format.adoc +++ b/doc/antora/modules/reference/pages/raddb/format.adoc @@ -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. diff --git a/src/lib/server/cf_file.c b/src/lib/server/cf_file.c index 2e9ca983262..024dd68b013 100644 --- a/src/lib/server/cf_file.c +++ b/src/lib/server/cf_file.c @@ -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 index 00000000000..70abc626fe7 --- /dev/null +++ b/src/tests/keywords/file @@ -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 index 00000000000..788d1a3c98c --- /dev/null +++ b/src/tests/keywords/file-data.md @@ -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 index 00000000000..ce7eb6810f6 --- /dev/null +++ b/src/tests/keywords/file-empty-error @@ -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 index 00000000000..d646d40af98 --- /dev/null +++ b/src/tests/keywords/file-missing-error @@ -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 index 00000000000..06de22d0e6b --- /dev/null +++ b/src/tests/keywords/value @@ -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 index 00000000000..3c31421d28b --- /dev/null +++ b/src/tests/keywords/value-data.md @@ -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 index 00000000000..362a4c4a9dd --- /dev/null +++ b/src/tests/keywords/value-empty-error @@ -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 index 00000000000..e69de29bb2d diff --git a/src/tests/keywords/value-missing-error b/src/tests/keywords/value-missing-error new file mode 100644 index 00000000000..eda224104cd --- /dev/null +++ b/src/tests/keywords/value-missing-error @@ -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 index 00000000000..2ca65a0ac55 --- /dev/null +++ b/src/tests/keywords/value-multiline-error @@ -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 index 00000000000..e5c5c5583f4 --- /dev/null +++ b/src/tests/keywords/value-multiline.md @@ -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 index 00000000000..264ea309351 --- /dev/null +++ b/src/tests/keywords/value-unterminated-error @@ -0,0 +1,6 @@ +string test + +# +# $VALUE{...} requires a closing "}". Missing one is an error. +# +test = "$VALUE{value-data.md" # ERROR