]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
v4: Convert %(exec: ) to new xlat API (#4106)
authorNick Porter <nick@portercomputing.co.uk>
Wed, 9 Jun 2021 01:31:32 +0000 (02:31 +0100)
committerGitHub <noreply@github.com>
Wed, 9 Jun 2021 01:31:32 +0000 (20:31 -0500)
* Define fr_exec_state_t for tracking the state of exec calls

Taken from unlang_frame_state_tmpl_t

* Define fr_exec_wait_start_io() for calling child processes

* Define callbacks for use with fr_exec_wait_start_io

* Convert %(exec: ) to new xlat API and make async

* Correct exec module tests and convert to new syntax

* Add test for exec with string argument containing special chars

* Convert unlang_tmpl_exec_wait_resume() to use fr_exec_wait_start_io()

* Remove unused unlang_tmpl_exec_timeout()

Functionality now in exec.c

* Remove unused unlang_tmpl_exec_stdout_read()

Functionality now in exec.c

* Remove unused unlang_tmpl_exec_waitpid()

Functionality now in exec.c

* Exec exit status codes are now parsed in exec.c

* Update documentation for new %(exec: ) syntax

* Always store status code back in the provided pointer

* Add a test which reads command line expanded args and env variables

* Add tests with a non-zero exit code from the child process

* Amend documentation for echo module so it makes more sense

25 files changed:
doc/antora/modules/howto/pages/modules/mschap/index.adoc
doc/antora/modules/raddb/pages/mods-available/echo.adoc
doc/antora/modules/raddb/pages/mods-available/exec.adoc
doc/antora/modules/raddb/pages/mods-available/mschap.adoc
doc/antora/modules/raddb/pages/sites-available/resource-check.adoc
doc/antora/modules/tutorials/pages/dynamic-translation.adoc
man/man5/unlang.5
raddb/mods-available/echo
raddb/mods-available/exec
raddb/mods-available/mschap
raddb/sites-available/resource-check
src/lib/server/exec.c
src/lib/server/exec.h
src/lib/unlang/tmpl.c
src/lib/unlang/tmpl_priv.h
src/modules/rlm_exec/rlm_exec.c
src/modules/rlm_mschap/rlm_mschap.c
src/tests/keywords/xlat-exec
src/tests/keywords/xlat-exec.attrs [new file with mode: 0644]
src/tests/modules/exec/async.unlang
src/tests/modules/exec/attrs.sh [new file with mode: 0755]
src/tests/modules/exec/fail.sh [new file with mode: 0644]
src/tests/modules/exec/module.conf
src/tests/modules/exec/sync.attrs
src/tests/modules/exec/sync.unlang

index 192d8ae34de11454d97f354c9072efdf247ba5d0..82ec9f10a7c5fc8b58d2763beb3e9d85e6521f12 100644 (file)
@@ -176,7 +176,7 @@ Or:
 . update via exec/script
 
 ```
-local_cpw = `%{exec:/my/script %{User-Name} %{MS-CHAP-New-Password.Cleartext}}`
+local_cpw = `%(exec:/my/script %{User-Name} %{MS-CHAP-New-Password.Cleartext})`
 ```
 
 WARNING: Wherever possible, you should use `MS-CHAP-New-NT-Password`. The
index 9b09f914ddb9e28076e39fa3372213bf4e2a009c..c08064c78166212b524779b00d823ee9f9ea49ed 100644 (file)
@@ -4,13 +4,19 @@
 
 = Echo Module
 
-The `echo` module is used for execute an external program in more than
-one section (e.g. `recv`, `pre_proxy`, etc), then it is probably best
-to define a different instance of the `exec` module for every section.
+The `echo` module is an example of the `exec` module, configured with
+its `program` configuration item set so it can be called as any other
+module i.e.
 
-NOTE: This is a more specific example of the `exec` module.
+    echo
 
-  Attribute-Name = `%{echo:/path/to/program args}`
+The sample `program` in the default configuration is a trivial example
+and should be replaced with something more meaningful.
+
+Arguments can be supplied to the program on the command line, with the
+value of `program` being dynamically expanded.  In addition, setting
+`input_pairs` causes the corresponding list of attributes to be placed
+in environment variables.
 
 The return value of the program run determines the result of the exec
 instance call as follows:
@@ -111,7 +117,7 @@ thereby saving resources.
 ```
 exec echo {
        wait = yes
-       program = "/bin/echo %{User-Name}"
+       program = "/bin/echo Tmp-String-0 := %{User-Name}"
        input_pairs = request
        output_pairs = reply
        shell_escape = yes
index af81ccc71ea27de162ff9f371dc1b70900078520..b33885429233a057a193de72b1aab2e9b9fb3a8b 100644 (file)
@@ -6,20 +6,29 @@
 
 The module `exec` is used for executing external programs.
 
-This module is useful only for `xlat`.  To use it, put `exec` into
+This module provides an `xlat`.  To use it, put `exec` into
 the `instantiate` section.  You can then do dynamic translation of
 attributes like:
 
-  Attribute-Name = "%{exec:/path/to/program args}"
+  Attribute-Name = "%(exec:/path/to/program args)"
 
 The value of the attribute will be replaced with the output of the
 program which is executed.  Due to RADIUS protocol limitations,
 any output over `253` bytes will be ignored.
 
-The RADIUS attributes from the user request will be placed
+The RADIUS attributes from the attribute list specified in the
+configuration item `input_list` will be placed
 into environment variables of the executed program, as
 described in `man unlang` and in `doc/unlang/xlat.adoc`
 
+Alternatively to using the `xlat`, by setting the `program` configuration,
+item, the module can be called as a module.
+
+In this scenario, the output of executing the program will be parsed for
+attribute pairs which will be added to the list referenced in the
+`output_pairs` configuration item, if the `wait` configuration item
+is set to "yes".
+
 The return value of the program run determines the result of the exec
 instance call as follows:
 
@@ -48,20 +57,18 @@ instance call as follows:
 wait:: Wait for the program to finish.
 
 If we do NOT wait, then the program is "fire and
-forget", and any output attributes from it are ignored.
+forget", and any output from it is ignored.
 
 
 
-program:: The name of the program to execute, and it's
+program:: The name of the program to execute, and its
 arguments.
 
 Dynamic translation is done on this field, so things like
-the following example will work.
 
-If 'program' is set to a value, then the module *cannot* be
-used in an `xlat`.  See the `echo` module for examples of
-how to use the module "in line".
+    program = "/bin/true %{User-Name}"
 
+will work.
 
 
 input_pairs:: The attributes which are placed into the
index af5ddc37bccbe0e74eb990f3de347215a7dda951..a1629b1e798b654e56bfd0e5c25ec5f58b5b007d 100644 (file)
@@ -342,7 +342,7 @@ mschap {
 #              ntlm_auth = "/usr/bin/ntlm_auth --helper-protocol=ntlm-change-password-1"
 #              ntlm_auth_username = "username: %(mschap:User-Name)"
 #              ntlm_auth_domain = "nt-domain: %(mschap:NT-Domain)"
-#              local_cpw = "%{exec:/path/to/script %(mschap:User-Name) %{MS-CHAP-New-Password.Cleartext}}"
+#              local_cpw = "%(exec:/path/to/script %(mschap:User-Name) %{MS-CHAP-New-Password.Cleartext})"
 #              local_cpw = "%{sql:UPDATE radcheck set value='%{MS-CHAP-New-NT-Password}' where username='%{User-Name}' and attribute='NT-Password'}"
        }
 #      use_open_directory = yes
index f06794e639859c31758aa7476fe83c7401811c4d..412116ba0af869f065989841713e6e23a5a154f9 100644 (file)
@@ -100,7 +100,7 @@ db_online control module.
 
 Other modules could be used here.
 
-You can even invoke synchronous checks using the %{exec:...} xlat in
+You can even invoke synchronous checks using the %(exec:...) xlat in
 which case timeout should be set to less than the check trigger
 interval to avoid buildup of checks when resources do not respond.
 See rlm_exec for details.
index 131249d16e916ed00864da5147e30617feabb3da..3e6f5e89706d8c4e7ff802387c447aead65c996e 100644 (file)
@@ -22,7 +22,7 @@ top:
 
 -------------------------------------------------------------------------------
 bob Password.Cleartext := "hello"
-    Callback-Id = "%{exec:/bin/echo Hello, there}"
+    Callback-Id = "%(exec:/bin/echo Hello, there)"
 -------------------------------------------------------------------------------
 
 The `echo` program may be in `/usr/bin/echo`, depending on your local system. On
@@ -43,7 +43,7 @@ the server should print messages similar to the following.
 (0)  files : users: Matched entry bob at line 1
 Executing: /bin/echo Hello, there:
 Program returned code (0) and output 'Hello, there'
-(0)  files : EXPAND %{exec:/bin/echo Hello, there}
+(0)  files : EXPAND %(exec:/bin/echo Hello, there)
 (0)  files :    --> Hello, there
 (0)   [files] = ok
 -------------------------------------------------------------------------------
@@ -81,11 +81,11 @@ You should use the `bob-login-one.sh` script to send a request to match the
 first entry and should send another request with a different NAS-Port.
 
 -------------------------------------------------------------------------------
-bob    Password.Cleartext := "hello", NAS-Port == "%{exec:/usr/bin/id -u}"
+bob    Password.Cleartext := "hello", NAS-Port == "%(exec:/usr/bin/id -u)"
        Reply-Message = "Your port is very nice.",
        Session-Timeout = "%{expr: 60 * 60}"
 
-bob    Password.Cleartext := "hello", NAS-Port != "%{exec:/usr/bin/id -u}"
+bob    Password.Cleartext := "hello", NAS-Port != "%(exec:/usr/bin/id -u)"
         Reply-Message = "Your port is less nice.",
         Session-Timeout = "%{expr: 60 * 2}"
 -------------------------------------------------------------------------------
@@ -95,7 +95,7 @@ demonstrates this nesting.
 
 -------------------------------------------------------------------------------
 bob    Password.Cleartext := "hello"
-       Session-Timeout = "%{expr: 60 * %{exec:/usr/bin/id -u}}"
+       Session-Timeout = "%{expr: 60 * %(exec:/usr/bin/id -u})"
 -------------------------------------------------------------------------------
 
 In this case, the user "bob" is given one minute of access time,
index 58d22cc9a1435ab2cf2bc4c713f908b4890def5e..f50f89153a9480bffcf958bcca5c36997b160b33 100644 (file)
@@ -346,7 +346,7 @@ from each "case" statement.  The first "case" statement which matches
 is executed.  All other "case" statements are ignored.  A default
 "case" statement can be defined, by omitting its argument.
 
-If the argument is a double quoted string (e.g. "%{exec:1 + 2}", it is
+If the argument is a double quoted string (e.g. "%(exec:1 + 2)", it is
 expanded as described in the DATA TYPES section, below.  The match is
 then performed on the string returned from the expansion.  If the
 argument is an attribute reference (e.g. &User-Name), then the match
@@ -370,7 +370,7 @@ Provides a place-holder which matches the argument of a parent
 
 A "case" statement cannot appear outside of a "switch" block.
 
-If the argument is a double quoted string (e.g. "%{exec:1 + 2}", it is
+If the argument is a double quoted string (e.g. "%(exec:1 + 2)", it is
 expanded as described in the DATA TYPES section, below.  The match is
 then performed on the string returned from the expansion.  If the
 argument is an attribute reference (e.g. &User-Name), then the match
index f24f31716cd13b7347fb5558aea2b6d2cfc12f88..6b7f144fe71d0000788758249ae19e0bac105a9c 100644 (file)
@@ -7,13 +7,14 @@
 #
 #  = Echo Module
 #
-#  The `echo` module is used for execute an external program in more than
-#  one section (e.g. `recv`, `pre_proxy`, etc), then it is probably best
-#  to define a different instance of the `exec` module for every section.
+#  The `echo` module an example of the `exec` module configured
+#  with a `program` configuration item set so it can be called
+#  as any other module, i.e.
 #
-#  NOTE: This is a more specific example of the `exec` module.
+#      echo
 #
-#    Attribute-Name = `%{echo:/path/to/program args}`
+#  The sample `program` below is a trivial example and should be replaced
+#  with something more meaningful.
 #
 #  The return value of the program run determines the result of the exec
 #  instance call as follows:
@@ -62,12 +63,15 @@ exec echo {
        #  Dynamic translation is done on this field, so things like
        #  the following example will work.
        #
-       program = "/bin/echo %{User-Name}"
+       program = "/bin/echo Tmp-String-0 := %{User-Name}"
 
        #
        #  input_pairs:: The attributes which are placed into the
        #  environment variables for the program.
        #
+       #  If your program does not require access to values from
+       #  environment variables, then do not set this.
+       #
        #  Allowed values are:
        #
        #  [options="header,autowidth"]
index 4e9e0b1d1c3608936800df7c7fde7d829a6f3bf4..bbbce9d8a4fc0e17a4db0410b733a8f5cd066c6c 100644 (file)
@@ -9,22 +9,31 @@
 #
 #  The module `exec` is used for executing external programs.
 #
-#  This module is useful only for `xlat`.  To use it, put `exec` into
+#  This module provides an `xlat`.  To use it, put `exec` into
 #  the `instantiate` section.  You can then do dynamic translation of
 #  attributes like:
 #
-#    Attribute-Name = "%{exec:/path/to/program args}"
+#    Attribute-Name = "%(exec:/path/to/program args)"
 #
 #  The value of the attribute will be replaced with the output of the
 #  program which is executed.  Due to RADIUS protocol limitations,
 #  any output over `253` bytes will be ignored.
 #
-#  The RADIUS attributes from the user request will be placed
-#  into environment variables of the executed program, as
-#  described in `man unlang` and in `doc/unlang/xlat.adoc`
+#  The RADIUS attributes from the list referenced in the `input_pairs`
+#  configuraton item will be placed into environment variables of the executed
+#  program, as described in `man unlang` and in `doc/unlang/xlat.adoc`
 #
-#  The return value of the program run determines the result of the exec
-#  instance call as follows:
+#  Alternatively, by setting the `program` item of the module configuration,
+#  the module can be called as a module rather than as an xlat function.
+#  See the `echo` module for an example of this.
+#
+#  In this scenario, if the configuration item `output_pairs` is set,
+#  and the `wait` configuration item is set to "yes", the output of
+#  executing the program will be parsed for attribute pairs which
+#  will be added to the list referenced in `output_pairs`.
+#
+#  When called as a module, the return value of the program run determines
+#  the result of the exec instance call as follows:
 #
 #  [options="header,autowidth"]
 #  |===
@@ -58,14 +67,12 @@ exec {
 
        #
        #  program:: The name of the program to execute, and it's
-       #  arguments.
+       #  arguments, when called as a module.
        #
        #  Dynamic translation is done on this field, so things like
        #  the following example will work.
        #
-       #  If 'program' is set to a value, then the module *cannot* be
-       #  used in an `xlat`.  See the `echo` module for examples of
-       #  how to use the module "in line".
+       #  See the `echo` module for examples of how to use the module "in line".
        #
 #      program = "/bin/true %{User-Name}"
 
index 5eb4501d6c2722aca526dfec66a8dcad4e478013..22db84fce43f610732b123ef9509cccfc5d450f7 100644 (file)
@@ -335,7 +335,7 @@ mschap {
                #
                #  TIP: We give both examples here, but *only one should be used*.
                #
-#              local_cpw = "%{exec:/path/to/script %(mschap:User-Name) %{MS-CHAP-New-Password.Cleartext}}"
+#              local_cpw = "%(exec:/path/to/script %(mschap:User-Name) %{MS-CHAP-New-Password.Cleartext})"
 #              local_cpw = "%{sql:UPDATE radcheck set value='%{MS-CHAP-New-NT-Password}' where username='%{User-Name}' and attribute='NT-Password'}"
        }
 
index 9f1d9dad719e18df6a716886ca46dfa6dc91483e..452b21031b1ab55ee636fcf71c93258c4767b6c9 100644 (file)
@@ -114,7 +114,7 @@ recv Status-Server {
        #
        #  Other modules could be used here.
        #
-       #  You can even invoke synchronous checks using the %{exec:...} xlat in
+       #  You can even invoke synchronous checks using the %(exec:...) xlat in
        #  which case timeout should be set to less than the check trigger
        #  interval to avoid buildup of checks when resources do not respond.
        #  See rlm_exec for details.
index 0ad10b7965dddff589a7d4ff8ef2874b371bd714..35c93573ad04a67e6208c557e892ba0cece7e232 100644 (file)
@@ -28,6 +28,7 @@ RCSID("$Id$")
 #include <freeradius-devel/util/debug.h>
 #include <freeradius-devel/server/request.h>
 #include <freeradius-devel/server/util.h>
+#include <freeradius-devel/unlang/interpret.h>
 
 #include <freeradius-devel/util/dlist.h>
 #include <freeradius-devel/util/misc.h>
@@ -889,3 +890,270 @@ int fr_exec_wait_start(pid_t *pid_p, int *stdin_fd, int *stdout_fd, int *stderr_
 
        return 0;
 }
+
+/*
+ *     Cleanup an exec after failures
+ */
+static void exec_cleanup(fr_exec_state_t *exec) {
+       request_t       *request = exec->request;
+
+       if (exec->pid) DEBUG3("Cleaning up exec state for pid %u", exec->pid);
+
+       if (exec->stdout_fd >= 0) {
+               if (fr_event_fd_delete(request->el, exec->stdout_fd, FR_EVENT_FILTER_IO) < 0){
+                       RPERROR("Failed removing stdout handler");
+               }
+               close(exec->stdout_fd);
+               exec->stdout_fd = -1;
+       }
+
+       if (exec->stderr_fd >= 0) {
+               if (fr_event_fd_delete(request->el, exec->stderr_fd, FR_EVENT_FILTER_IO) < 0) {
+                       RPERROR("Failed removing stderr handler");
+               }
+               close(exec->stderr_fd);
+               exec->stderr_fd = -1;
+       }
+
+       if (exec->pid) {
+               (void) fr_event_pid_wait(request->el, request->el, NULL, exec->pid, NULL, NULL);
+               exec->pid = 0;
+       }
+
+       if (exec->ev) fr_event_timer_delete(&exec->ev);
+}
+
+/*
+ *     Callback when exec has completed.  Record the status and tidy up.
+ */
+static void exec_waitpid(UNUSED fr_event_list_t *el, UNUSED pid_t pid, int status, void *uctx)
+{
+       fr_exec_state_t *exec = uctx;
+       request_t       *request = exec->request;
+
+       if (WIFEXITED(status)) {
+               RDEBUG("Program failed with status code %d", WEXITSTATUS(status));
+               exec->status = WEXITSTATUS(status);
+       } else if (WIFSIGNALED(status)) {
+               RDEBUG("Program exited due to signal with status code %d", WTERMSIG(status));
+               exec->status = -WTERMSIG(status);
+       } else {
+               RDEBUG("Program exited due to unknown status %d", exec->status);
+               exec->status = -status;
+       }
+       exec->pid = 0;
+
+       /*
+        *      We may receive the "child exited" signal before the
+        *      "pipe has been closed" signal
+        */
+       if (exec->stdout_fd >= 0) {
+               (void) fr_event_fd_delete(exec->request->el, exec->stdout_fd, FR_EVENT_FILTER_IO);
+               close(exec->stdout_fd);
+               exec->stdout_fd = -1;
+       }
+
+       if (exec->ev) fr_event_timer_delete(&exec->ev);
+
+       unlang_interpret_mark_runnable(exec->request);
+}
+
+/*
+ *     Callback when an exec times out.
+ */
+static void exec_timeout(
+#ifndef __linux__
+                        UNUSED
+#endif
+                        fr_event_list_t *el, UNUSED fr_time_t now, void *uctx)
+{
+       fr_exec_state_t *exec = talloc_get_type_abort(uctx, fr_exec_state_t);
+       request_t       *request = exec->request;
+
+       fr_assert(exec->pid > 0);
+
+#ifdef __linux__
+       int     status;
+
+       /*
+        *      libkqueue on Linux isn't quite there yet.  Maybe the
+        *      program has exited, and we haven't noticed.  In which
+        *      case, do a graceful cleanup.
+        */
+       if (waitpid(exec->pid, &status, WNOHANG) == exec->pid) {
+               exec_waitpid(el, exec->pid, status, exec);
+               return;
+       }
+#endif
+
+       if (exec->stdout_fd < 0) {
+               REDEBUG("Timeout waiting for program to exit - killing it and failing the request");
+       } else {
+               REDEBUG("Timeout running program - killing it and failing the request");
+       }
+       kill(exec->pid, SIGKILL);
+       exec->failed = true;
+
+       exec_cleanup(exec);
+       unlang_interpret_mark_runnable(exec->request);
+}
+
+/*
+ *     Callback to read stdout from an exec into the pre-prepared extensible sbuff
+ */
+static void exec_stdout_read(UNUSED fr_event_list_t *el, int fd, UNUSED int flags, void *uctx) {
+       fr_exec_state_t         *exec = uctx;
+       request_t               *request = exec->request;
+       ssize_t                 data_len, remaining;
+       fr_sbuff_marker_t       start_m;
+
+       fr_sbuff_marker(&start_m, &exec->stdout_buff);
+
+       do {
+               /*
+                *      Read in 128 byte chunks
+                */
+               remaining = fr_sbuff_extend_lowat(NULL, &exec->stdout_buff, 128);
+
+               /*
+                *      Ran out of buffer space.
+                */
+               if (unlikely(!remaining)) {
+                       REDEBUG("Too much output from program - killing it and failing the request");
+
+                       if (exec->pid > 0) kill(exec->pid, SIGKILL);
+
+               error:
+                       exec->failed = true;
+                       exec_cleanup(exec);
+                       break;
+               }
+
+               data_len = read(fd, fr_sbuff_current(&exec->stdout_buff), remaining);
+               if (data_len < 0) {
+                       if (errno == EINTR) continue;
+
+                       REDEBUG("Error reading from child program - %s", fr_syserror(errno));
+                       goto error;
+               }
+
+               /*
+                *      Even if we get 0 now the process may write more data later
+                *      before it completes, so we leave the fd handlers in place.
+                */
+               if (data_len == 0) break;
+
+               fr_sbuff_advance(&exec->stdout_buff, data_len);
+       } while (remaining == data_len);        /* If process returned maximum output, loop again */
+
+       /*
+        *      Only print if we got additional data
+        */
+       if (RDEBUG_ENABLED2 && fr_sbuff_behind(&start_m)) {
+               RDEBUG2("pid %u (stdout) - %pV", exec->pid,
+                       fr_box_strvalue_len(fr_sbuff_current(&start_m),
+                                           fr_sbuff_behind(&start_m)));
+       }
+
+       fr_sbuff_marker_release(&start_m);
+}
+
+/** Call an child program, optionally reading it's output
+ *
+ * @param[in] ctx      to allocate events in.
+ * @param[in,out] exec structure holding the state of the external call.
+ * @param[in] request  currently being processed.
+ * @param[in] cmd      to call as a fr_value_boc_list_t.  Program will be the first box
+ *                     and arguments in the subsequent boxes.
+ * @param[in] env_pairs        list of pairs to be presented as evironment variables to the child.
+ * @param[in] timeout  to wait for child to complete.
+ * @return
+ *     - 0 on success
+ *     - -1 on failure
+ */
+int fr_exec_wait_start_io(TALLOC_CTX *ctx, fr_exec_state_t *exec, request_t *request, fr_value_box_list_t *cmd, fr_pair_list_t *env_pairs, fr_time_delta_t timeout)
+{
+       int     *stdout_fd = (exec->stdout_used || RDEBUG_ENABLED2) ? &exec->stdout_fd : NULL;
+
+       exec->request = request;
+       exec->vps = env_pairs;
+       exec->stdout_fd = -1;
+       exec->stderr_fd = -1;
+       exec->stdin_fd = -1;
+
+       if (fr_exec_wait_start(&exec->pid, exec->stdin_used ? &exec->stdin_fd : NULL,
+                              stdout_fd, &exec->stderr_fd, request, cmd, exec->vps) < 0) {
+               RPEDEBUG("Failed executing program");
+       fail:
+               exec_cleanup(exec);
+               return -1;
+       }
+       exec->status = -1;      /* default to program didn't work */
+
+       /*
+        *      Tell the event loop that it needs to wait for this PID
+        */
+       if (fr_event_pid_wait(ctx, request->el, &exec->ev_pid, exec->pid, exec_waitpid, exec) < 0) {
+               exec->pid = 0;
+               RPEDEBUG("Failed adding watcher for child process");
+               goto fail;
+       }
+
+       /*
+        *      Setup event to kill the child process after a period of time.
+        */
+       if (fr_event_timer_in(ctx, request->el, &exec->ev, timeout, exec_timeout, exec) < 0) {
+               goto fail;
+       }
+
+       /*
+        *      If we need to parse stdout, insert a special IO handler that
+        *      aggregates all stdout data into an expandable buffer.
+        */
+       if (exec->stdout_used) {
+               /*
+                *      Accept a maximum of 32k of data from the process.
+                */
+               fr_sbuff_init_talloc(exec->outctx, &exec->stdout_buff, &exec->stdout_tctx, 128, 32 * 1024);
+               if (fr_event_fd_insert(ctx, request->el, exec->stdout_fd, exec_stdout_read, NULL, NULL, exec) < 0) {
+                       RPEDEBUG("Failed adding event listening to stdout");
+                       goto fail;
+               }
+
+       /*
+        *      If the caller doesn't want the output box, we still want to copy stdout
+        *      into the request log if we're logging at a high enough level of verbosity.
+        */
+       } else if (RDEBUG_ENABLED2) {
+               exec->stdout_uctx = (log_fd_event_ctx_t) {
+                       .type = L_DBG,
+                       .lvl = L_DBG_LVL_2,
+                       .request = request,
+                       .prefix = fr_asprintf(exec->outctx, "pid %u (stdout)", exec->pid)
+               };
+
+               if (fr_event_fd_insert(ctx, request->el, exec->stdout_fd, log_request_fd_event,
+                                      NULL, NULL, &exec->stdout_uctx) < 0){
+                       RPEDEBUG("Failed adding event listening to stdout");
+                       goto fail;
+               }
+       }
+
+       /*
+        *      Send stderr to the request log as error messages with a custom prefix
+        */
+       exec->stderr_uctx = (log_fd_event_ctx_t) {
+               .type = L_DBG_ERR,
+               .lvl = L_DBG_LVL_1,
+               .request = request,
+               .prefix = fr_asprintf(exec->outctx, "pid %u (stderr)", exec->pid)
+       };
+
+       if (fr_event_fd_insert(ctx, request->el, exec->stderr_fd, log_request_fd_event,
+                              NULL, NULL, &exec->stderr_uctx) < 0) {
+               RPEDEBUG("Failed adding event listening to stderr");
+               goto fail;
+       }
+
+       return 0;
+}
index 37bc08cc4ee746124ed16eefd36704b25a21f10e..9c913c82c1ba2110cdc4f6015a87993633ffaff6 100644 (file)
@@ -45,6 +45,35 @@ extern "C" {
 #ifdef __cplusplus
 extern "C" {
 #endif
+
+typedef struct {
+       fr_sbuff_t                      stdout_buff;    //!< Expandable buffer to store process output.
+       fr_sbuff_uctx_talloc_t          stdout_tctx;    //!< sbuff talloc ctx data.
+
+       log_fd_event_ctx_t              stdout_uctx;    //!< Config for the stdout logger.
+       log_fd_event_ctx_t              stderr_uctx;    //!< Config for the stderr logger.
+
+       pid_t                           pid;            //!< child PID
+       int                             stdin_fd;       //!< for writing to the child.
+       bool                            stdin_used;     //!< use stdin fd?
+       int                             stdout_fd;      //!< for reading from the child.
+       bool                            stdout_used;    //!< use stdout fd?
+       int                             stderr_fd;      //!< for producing error messages.
+
+       fr_event_timer_t const          *ev;            //!< for timing out the child
+       fr_event_pid_t const            *ev_pid;        //!< for cleaning up the process
+       bool                            failed;         //!< due to exec timeout or buffer overflow
+
+       int                             status;         //!< return code of the program
+       int                             *status_p;      //!< where we write the return status of the program
+
+       fr_pair_list_t                  *vps;           //!< input VPs
+
+       request_t                       *request;       //!< request this exec is related to
+       TALLOC_CTX                      *outctx;        //!< ctx to allocate output buffers
+} fr_exec_state_t;
+
+
 pid_t  radius_start_program(int *stdin_fd, int *stdout_fd, int *stderr_fd,
                             char const *cmd, request_t *request, bool exec_wait,
                             fr_pair_list_t *input_pairs, bool shell_escape);
@@ -61,6 +90,8 @@ int   fr_exec_nowait(request_t *request, fr_value_box_list_t *vb_list, fr_pair_lis
 int    fr_exec_wait_start(pid_t *pid_p, int *stdin_fd, int *stdout_fd, int *stderr_fd,
                           request_t *request, fr_value_box_list_t *vb_list, fr_pair_list_t *env_pairs);
 
+int    fr_exec_wait_start_io(TALLOC_CTX *ctx, fr_exec_state_t *exec, request_t *request,
+                             fr_value_box_list_t *vb_list, fr_pair_list_t *env_pairs, fr_time_delta_t timeout);
 #ifdef __cplusplus
 }
 #endif
index e0c61cdccd0327cd426108b31aea65748bb9aeaf..d61905a3e1c012d969d536e97562e8b2bb5b648b 100644 (file)
@@ -177,141 +177,6 @@ int unlang_tmpl_push(TALLOC_CTX *ctx, fr_value_box_list_t *out, request_t *reque
        return 0;
 }
 
-/*
- *     Run the callback which gets the PID and status
- */
-static void unlang_tmpl_exec_waitpid(UNUSED fr_event_list_t *el, UNUSED pid_t pid, int status, void *uctx)
-{
-       request_t                               *request = uctx;
-       unlang_stack_t                  *stack = request->stack;
-       unlang_stack_frame_t            *frame = &stack->frame[stack->depth];
-       unlang_frame_state_tmpl_t       *state = talloc_get_type_abort(frame->state,
-                                                                      unlang_frame_state_tmpl_t);
-
-       state->exec.status = status;
-       state->exec.pid = 0;
-
-       /*
-        *      We may receive the "child exited" signal before the
-        *      "pipe has been closed" signal.
-        */
-       if (state->exec.stdout_fd >= 0) {
-               (void) fr_event_fd_delete(request->el, state->exec.stdout_fd, FR_EVENT_FILTER_IO);
-               close(state->exec.stdout_fd);
-               state->exec.stdout_fd = -1;
-       }
-
-       if (state->exec.ev) fr_event_timer_delete(&state->exec.ev);
-
-       unlang_interpret_mark_runnable(request);
-}
-
-static void unlang_tmpl_exec_stdout_read(UNUSED fr_event_list_t *el, int fd, UNUSED int flags, void *uctx)
-{
-       request_t                       *request = uctx;
-       unlang_stack_t                  *stack = request->stack;
-       unlang_stack_frame_t            *frame = &stack->frame[stack->depth];
-       unlang_frame_state_tmpl_t       *state = talloc_get_type_abort(frame->state, unlang_frame_state_tmpl_t);
-       ssize_t                         data_len, remaining;
-       fr_sbuff_marker_t               start_m;
-
-       fr_sbuff_marker(&start_m, &state->exec.stdout_buff);
-
-       do {
-               /*
-                *      Read in 128 byte chunks
-                */
-               remaining = fr_sbuff_extend_lowat(NULL, &state->exec.stdout_buff, 128);
-
-               /*
-                *      Ran out of buffer space.
-                */
-               if (unlikely(!remaining)) {
-                       REDEBUG("Too much output from program - killing it and failing the request");
-
-                       if (state->exec.pid > 0) kill(state->exec.pid, SIGKILL);
-
-               error:
-                       state->exec.failed = true;
-                       unlang_tmpl_exec_cleanup(request);
-                       break;
-               }
-
-               data_len = read(fd,
-                               fr_sbuff_current(&state->exec.stdout_buff),
-                               remaining);
-               if (data_len < 0) {
-                       if (errno == EINTR) continue;
-
-                       REDEBUG("Error reading from child program - %s", fr_syserror(errno));
-                       goto error;
-               }
-
-               /*
-                *      Event if we get 0 now the process
-                *      may write more data later before
-                *      it completes, so we leave the fd
-                *      handlers in place.
-                */
-               if (data_len == 0) break;
-
-               fr_sbuff_advance(&state->exec.stdout_buff, data_len);
-       } while (remaining == data_len);        /* If process returned maximum output, loop again */
-
-       /*
-        *      Only print if we got additional data
-        */
-       if (RDEBUG_ENABLED2 && fr_sbuff_behind(&start_m)) {
-               RDEBUG2("pid %u (stdout) - %pV",
-                       state->exec.pid,
-                       fr_box_strvalue_len(fr_sbuff_current(&start_m),
-                                           fr_sbuff_behind(&start_m)));
-       }
-
-       fr_sbuff_marker_release(&start_m);
-}
-
-static void unlang_tmpl_exec_timeout(
-#ifndef __linux__
-                                    UNUSED
-#endif
-                                    fr_event_list_t *el, UNUSED fr_time_t now, void *uctx)
-{
-       request_t                       *request = uctx;
-       unlang_stack_t                  *stack = request->stack;
-       unlang_stack_frame_t            *frame = &stack->frame[stack->depth];
-       unlang_frame_state_tmpl_t       *state = talloc_get_type_abort(frame->state,
-                                                                      unlang_frame_state_tmpl_t);
-
-       fr_assert(state->exec.pid > 0);
-
-#ifdef __linux__
-       int status;
-
-       /*
-        *      libkqueue on Linux isn't quite there yet.  Maybe the
-        *      program has exited, and we haven't noticed.  In which
-        *      case, do a graceful cleanup.
-        */
-       if (waitpid(state->exec.pid, &status, WNOHANG) == state->exec.pid) {
-               unlang_tmpl_exec_waitpid(el, state->exec.pid, status, request);
-               return;
-       }
-#endif
-
-       if (state->exec.stdout_fd < 0) {
-               REDEBUG("Timeout waiting for program to exit - killing it and failing the request");
-       } else {
-               REDEBUG("Timeout running program - killing it and failing the request");
-       }
-       kill(state->exec.pid, SIGKILL);
-       state->exec.failed = true;
-
-       unlang_tmpl_exec_cleanup(request);
-       unlang_interpret_mark_runnable(request);
-}
-
-
 /** Wrapper to call a resumption function after a tmpl has been expanded
  *
  *  If the resumption function returns YIELD, then this function is
@@ -354,28 +219,10 @@ static unlang_action_t unlang_tmpl_exec_wait_final(rlm_rcode_t *p_result, reques
        fr_assert(state->exec.pid == 0);
 
        if (state->exec.status != 0) {
-               if (WIFEXITED(state->exec.status)) {
-                       RDEBUG("Program failed with status code %d", WEXITSTATUS(state->exec.status));
-                       state->exec.status = WEXITSTATUS(state->exec.status);
-
-               } else if (WIFSIGNALED(state->exec.status)) {
-                       RDEBUG("Program exited due to signal with status code %d", WTERMSIG(state->exec.status));
-                       state->exec.status = -WTERMSIG(state->exec.status);
-
-               } else {
-                       RDEBUG("Program exited due to unknown status %d", state->exec.status);
-                       state->exec.status = -state->exec.status;
-               }
-
                fr_assert(fr_dlist_empty(&state->box));
                goto resume;
        }
 
-       /*
-        *      Save the *mangled* exit status, not the raw one.
-        */
-       if (state->exec.status_p) *state->exec.status_p = state->exec.status;
-
        /*
         *      We might want to just get the status of the program,
         *      and not care about the output.
@@ -403,10 +250,15 @@ static unlang_action_t unlang_tmpl_exec_wait_final(rlm_rcode_t *p_result, reques
                fr_dlist_insert_head(&state->box, box);
        }
 
+resume:
+       /*
+        *      Save the *mangled* exit status, not the raw one.
+        */
+       if (state->exec.status_p) *state->exec.status_p = state->exec.status;
+
        /*
         *      Ensure that the callers resume function is called.
         */
-resume:
        frame->process = unlang_tmpl_resume;
        return unlang_tmpl_resume(p_result, request, frame);
 }
@@ -419,105 +271,20 @@ static unlang_action_t unlang_tmpl_exec_wait_resume(rlm_rcode_t *p_result, reque
                                                    unlang_stack_frame_t *frame)
 {
        unlang_frame_state_tmpl_t       *state = talloc_get_type_abort(frame->state, unlang_frame_state_tmpl_t);
-       pid_t                           pid;
-       int                             *stdout_fd = NULL;
-
-       state->exec.stdout_fd = -1;
-       state->exec.stderr_fd = -1;
-       if (state->out || RDEBUG_ENABLED2) stdout_fd = &state->exec.stdout_fd;
-
-       if (fr_exec_wait_start(&pid, NULL, stdout_fd, &state->exec.stderr_fd,
-                              request, &state->box, state->exec.vps) < 0) {
-               RPEDEBUG("Failed executing program");
-       fail:
-               unlang_tmpl_exec_cleanup(request);
-               *p_result = RLM_MODULE_FAIL;
-               return UNLANG_ACTION_CALCULATE_RESULT;
-       }
-
-       fr_dlist_talloc_free(&state->box); /* this is the xlat expansion, and not the output string we want */
-
-       state->exec.pid = pid;
-       state->exec.status = -1;        /* default to program didn't work */
 
-       /*
-        *      Tell the event loop that it needs to wait for this PID.
-        */
-       if (fr_event_pid_wait(state, request->el, &state->exec.ev_pid, pid,
-                             unlang_tmpl_exec_waitpid, request) < 0) {
-               state->exec.pid = 0;
-               RPEDEBUG("Failed adding watcher for child process");
-               unlang_tmpl_exec_cleanup(request);
-               goto fail;
-       }
+       state->exec.stdout_used = (state->out) ? true : false;
+       state->exec.outctx = state;
 
        /*
-        *      Kill the child process after a period of time.
-        *
         *      @todo - make the timeout configurable
         */
-       if (fr_event_timer_in(state->ctx, request->el, &state->exec.ev,
-                             fr_time_delta_from_sec(EXEC_TIMEOUT), unlang_tmpl_exec_timeout, request) < 0) {
-               unlang_tmpl_exec_cleanup(request);
-               goto fail;
-       }
-
-       /*
-        *      If we need to parse stdout, insert a
-        *      special IO handler that aggregates all
-        *      stdout data into an expandable buffer.
-        */
-       if (state->out) {
-               if (fr_event_fd_insert(state->ctx, request->el, state->exec.stdout_fd,
-                                      unlang_tmpl_exec_stdout_read, NULL, NULL, request) < 0) {
-                       RPEDEBUG("Failed adding event");
-                       goto fail;
-               }
-
-               /*
-                *      Accept a maximum of 32k of
-                *      data from the process.
-                */
-               fr_sbuff_init_talloc(state, &state->exec.stdout_buff, &state->exec.stdout_tctx, 128, 32 * 1024);
-               fr_value_box_list_init(&state->box);
-
-       /*
-        *      If the caller doesn't want the output box,
-        *      we still want to copy stdout into the
-        *      request log if we're logging at a high
-        *      enough level of verbosity.
-        */
-       } else if (RDEBUG_ENABLED2) {
-               state->exec.stdout_uctx = (log_fd_event_ctx_t){
-                       .type = L_DBG,
-                       .lvl = L_DBG_LVL_2,
-                       .request = request,
-                       .prefix = fr_asprintf(state, "pid %u (stdout)", state->exec.pid)
-               };
-
-               if (fr_event_fd_insert(state->ctx, request->el, state->exec.stdout_fd,
-                                      log_request_fd_event, NULL, NULL, &state->exec.stdout_uctx) < 0) {
-                       RPEDEBUG("Failed adding event");
-                       goto fail;
-               }
+       if (fr_exec_wait_start_io(state->ctx, &state->exec, request, &state->box, state->exec.vps,
+                                 fr_time_delta_from_sec(EXEC_TIMEOUT)) < 0) {
+               *p_result = RLM_MODULE_FAIL;
+               return UNLANG_ACTION_CALCULATE_RESULT;
        }
 
-       /*
-        *      Send stderr to the request log as
-        *      error messages with a custom prefix
-        */
-       state->exec.stderr_uctx = (log_fd_event_ctx_t){
-               .type = L_DBG_ERR,
-               .lvl = L_DBG_LVL_1,
-               .request = request,
-               .prefix = fr_asprintf(state, "pid %u (stderr)", state->exec.pid)
-       };
-
-       if (fr_event_fd_insert(state->ctx, request->el, state->exec.stderr_fd,
-                              log_request_fd_event, NULL, NULL, &state->exec.stderr_uctx) < 0) {
-               RPEDEBUG("Failed adding event");
-               goto fail;
-       }
+       fr_dlist_talloc_free(&state->box); /* this is the xlat expansion, and not the output string we want */
 
        frame->process = unlang_tmpl_exec_wait_final;
        repeatable_set(frame);
index 1bd899b8c035df285e36f8036181441b24737b0a..39410d28da92b0f3d6b93c5e0882fab0b392c8d3 100644 (file)
@@ -24,6 +24,7 @@
  * @copyright 2006-2019 The FreeRADIUS server project
  */
 #include "unlang_priv.h"
+#include <freeradius-devel/server/exec.h>
 
 #ifdef __cplusplus
 extern "C" {
@@ -43,26 +44,7 @@ typedef struct {
        fr_unlang_tmpl_signal_t         signal;         //!< signal handler
 
        union {
-               struct {
-                       fr_sbuff_t                      stdout_buff;    //!< Expandable buffer to store process output.
-                       fr_sbuff_uctx_talloc_t          stdout_tctx;    //!< sbuff talloc ctx data.
-
-                       log_fd_event_ctx_t              stdout_uctx;    //!< Config for the stdout logger.
-                       log_fd_event_ctx_t              stderr_uctx;    //!< Config for the stderr logger.
-
-                       pid_t                           pid;            //!< child PID
-                       int                             stdout_fd;      //!< for reading from the child.
-                       int                             stderr_fd;      //!< for producing error messages.
-
-                       fr_event_timer_t const          *ev;            //!< for timing out the child
-                       fr_event_pid_t const            *ev_pid;        //!< for cleaning up the process
-                       bool                            failed;         //!< due to exec timeout or buffer overflow
-
-                       int                             status;         //!< return code of the program
-                       int                             *status_p;      //!< where we write the return status of the program
-
-                       fr_pair_list_t                  *vps;           //!< input VPs
-               } exec;
+               fr_exec_state_t         exec;
        };
 } unlang_frame_state_tmpl_t;
 
index 904f9f3e77569cc252b822eb9e782e94fcaae76a..7a7081c21480b1b7f2eee5214e70fe5e2cb05e0d 100644 (file)
@@ -60,79 +60,99 @@ static const CONF_PARSER module_config[] = {
        CONF_PARSER_TERMINATOR
 };
 
-static char const special[] = "\\'\"`<>|; \t\r\n()[]?#$^&*=";
 
-/*
- *     Escape special characters
- */
-static size_t rlm_exec_shell_escape(UNUSED request_t *request, char *out, size_t outlen, char const *in,
-                                   UNUSED void *inst)
+static xlat_action_t exec_xlat_resume(TALLOC_CTX *ctx, fr_dcursor_t *out, request_t *request,
+                                     UNUSED void const *xlat_inst, UNUSED void *xlat_thread_inst,
+                                     UNUSED fr_value_box_list_t *in, void *rctx)
 {
-       char *q, *end;
-       char const *p;
+       fr_exec_state_t *exec = talloc_get_type_abort(rctx, fr_exec_state_t);
+       fr_value_box_t  *vb;
 
-       q = out;
-       end = out + outlen;
-       p = in;
+       /*
+        *      Allow a return code of 3 as success to match the behaviour of
+        *      inline module calls.
+        */
+       if ((exec->status != 0) && (exec->status != 3)) {
+               RPEDEBUG("Execution of external program returned %d", exec->status);
+               return XLAT_ACTION_FAIL;
+       }
 
-       while (*p) {
-               if ((q + 3) >= end) break;
+       MEM(vb = fr_value_box_alloc_null(ctx));
 
-               if (strchr(special, *p) != NULL) {
-                       *(q++) = '\\';
-               }
-               *(q++) = *(p++);
-       }
+       /*
+        *      Remove any trailing line endings and trim buffer
+        */
+       fr_sbuff_trim(&exec->stdout_buff, sbuff_char_line_endings);
+       fr_sbuff_trim_talloc(&exec->stdout_buff, SIZE_MAX);
 
-       *q = '\0';
-       return q - out;
+       /*
+        *      Use the buffer for the output vb
+        */
+       fr_value_box_strdup_shallow(vb, NULL, fr_sbuff_buff(&exec->stdout_buff), true);
+
+       fr_dcursor_append(out, vb);
+
+       return XLAT_ACTION_DONE;
 }
 
+static xlat_arg_parser_t const exec_xlat_args[] = {
+       { .required = true, .type = FR_TYPE_STRING },
+       { .variadic = true, .type = FR_TYPE_VOID},
+       XLAT_ARG_PARSER_TERMINATOR
+};
 
 /** Exec programs from an xlat
  *
  * Example:
 @verbatim
-"%{exec:/bin/echo hello}" == "hello"
+"%(exec:/bin/echo hello)" == "hello"
 @endverbatim
  *
  * @ingroup xlat_functions
  */
-static ssize_t exec_xlat(UNUSED TALLOC_CTX *ctx, char **out, size_t outlen,
-                        void const *mod_inst, UNUSED void const *xlat_inst,
-                        request_t *request, char const *fmt)
+static xlat_action_t exec_xlat(TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out, request_t *request,
+                              void const *xlat_inst, UNUSED void *xlat_thread_inst,
+                              fr_value_box_list_t *in)
 {
-       int                     result;
-       rlm_exec_t const        *inst = mod_inst;
+       rlm_exec_t const        *inst;
+       void                    *instance;
        fr_pair_list_t          *input_pairs = NULL;
-       char *p;
+       fr_exec_state_t         *exec;
 
-       if (!inst->wait) {
-               REDEBUG("'wait' must be enabled to use exec xlat");
-               return -1;
-       }
+       memcpy(&instance, xlat_inst, sizeof(instance));
+
+       inst = talloc_get_type_abort(instance, rlm_exec_t);
 
        if (inst->input_list) {
                input_pairs = tmpl_list_head(request, inst->input_list);
                if (!input_pairs) {
                        REDEBUG("Failed to find input pairs for xlat");
-                       return -1;
+                       return XLAT_ACTION_FAIL;
                }
        }
 
-       /*
-        *      This function does it's own xlat of the input program
-        *      to execute.
-        */
-       result = radius_exec_program(request, *out, outlen, NULL, request, fmt, input_pairs ? input_pairs : NULL,
-                                    inst->wait, inst->shell_escape, inst->timeout);
-       if (result != 0) return -1;
+       if (!inst->wait) {
+               /* Not waiting for the response */
+               fr_exec_nowait(request, in, input_pairs);
+               return XLAT_ACTION_DONE;
+       }
 
-       for (p = *out; *p != '\0'; p++) {
-               if (*p < ' ') *p = ' ';
+       exec = talloc_zero(request, fr_exec_state_t);
+       exec->stdout_used = inst->wait;
+       exec->outctx = ctx;
+
+       if(fr_exec_wait_start_io(exec, exec, request, in, input_pairs, inst->timeout) < 0) {
+               talloc_free(exec);
+               return XLAT_ACTION_FAIL;
        }
 
-       return strlen(*out);
+       return unlang_xlat_yield(request, exec_xlat_resume, NULL, exec);
+}
+
+static int mod_xlat_instantiate(void *xlat_inst, UNUSED xlat_exp_t const *exp, void *uctx)
+{
+       *((void **)xlat_inst) = talloc_get_type_abort(uctx, rlm_exec_t);
+       return 0;
 }
 
 /*
@@ -147,15 +167,18 @@ static ssize_t exec_xlat(UNUSED TALLOC_CTX *ctx, char **out, size_t outlen,
  */
 static int mod_bootstrap(void *instance, CONF_SECTION *conf)
 {
-       char const *p;
+       char const      *p;
        rlm_exec_t      *inst = instance;
+       xlat_t          *xlat;
 
        inst->name = cf_section_name2(conf);
        if (!inst->name) {
                inst->name = cf_section_name1(conf);
        }
 
-       xlat_register_legacy(inst, inst->name, exec_xlat, rlm_exec_shell_escape, NULL, 0, XLAT_DEFAULT_BUF_LEN);
+       xlat = xlat_register(NULL, inst->name, exec_xlat, true);
+       xlat_func_args(xlat, exec_xlat_args);
+       xlat_async_instantiate_set(xlat, mod_xlat_instantiate, rlm_exec_t *, NULL, inst);
 
        if (inst->input) {
                p = inst->input;
@@ -184,7 +207,7 @@ static int mod_bootstrap(void *instance, CONF_SECTION *conf)
                return -1;
        }
 
-       if (inst->timeout_is_set || !inst->timeout) {
+       if (!inst->timeout_is_set || !inst->timeout) {
                /*
                 *      Pick the shorter one
                 */
index 657c816296adc9f764cd62b44bb7a1dc79f54dd6..068015b8a5f967c600094d217b8cdd590d67612e 100644 (file)
@@ -963,7 +963,7 @@ ntlm_auth_err:
                 *
                 *  ...or...
                 *
-                *  %{exec:/path/to %(mschap:User-Name) %{MS-CHAP-New-Password}}"
+                *  %(exec:/path/to %(mschap:User-Name) %{MS-CHAP-New-Password})"
                 *
                 */
                fr_pair_t *new_pass, *new_hash;
index 8fc6e7be11b50398c595a70f3e67700f3dc1507d..055cd240242ed369b750e1e940220f8c1b375b1b 100644 (file)
@@ -18,4 +18,17 @@ if (&control.Tmp-String-1 != "hello foo:1234 world") {
        test_fail
 }
 
+# User-Password contains characters that need escaping
+update control {
+       &Tmp-String-2 := `/bin/echo %{User-Password}`
+}
+
+if (&control.Tmp-String-2 != &User-Password) {
+       test_fail
+} else {
+       update request {
+               &User-Password := "hello"
+       }
+}
+
 success
diff --git a/src/tests/keywords/xlat-exec.attrs b/src/tests/keywords/xlat-exec.attrs
new file mode 100644 (file)
index 0000000..487bf23
--- /dev/null
@@ -0,0 +1,12 @@
+#
+#  Input packet
+#
+Packet-Type = Access-Request
+User-Name = "bob"
+User-Password = "hel|o'"
+
+#
+#  Expected answer
+#
+Packet-Type == Access-Accept
+Result-Status == "success"
index 67775b9eb5c30c617cfb19621f2dcffe1e6c0904..9acaaf4b5f17c262020786225634e45fa1b2aaeb 100644 (file)
@@ -3,25 +3,22 @@
 #  because we don't wait for the response.
 #
 update request {
-       &Tmp-String-0 := "%{exec_async:/bin/sh -c 'echo hello'}"
+       &Tmp-String-0 := "%(exec_async:/bin/sh -c 'echo -n hello')"
 }
 if (&Tmp-String-0 != '') {
        test_fail
 }
 
+exec_async_mod
+
+if (ok) {
+       test_pass
+}
+
 #
 #  Async calls should not have their output added to the request
 #
-update request {
-       &Tmp-String-0 := "%{exec_async:/bin/sh -c \"echo 'reply.Reply-Message = \'hello\''\"}"
-}
 
 if (&reply.Reply-Message == 'hello') {
        test_fail
 }
-
-exec_async_inline
-
-if (ok) {
-       test_pass
-}
diff --git a/src/tests/modules/exec/attrs.sh b/src/tests/modules/exec/attrs.sh
new file mode 100755 (executable)
index 0000000..7633012
--- /dev/null
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+echo Tmp-String-1 := $1
+echo Tmp-String-2 := ${CALLED_STATION_ID}
+
+exit 0;
\ No newline at end of file
diff --git a/src/tests/modules/exec/fail.sh b/src/tests/modules/exec/fail.sh
new file mode 100644 (file)
index 0000000..6999952
--- /dev/null
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+echo Tmp-String-3 := Failure
+
+# exit status 7 maps to module exit code "notfound"
+exit 7
index 4dd8cb701d2b10e31bfda4a711c46256f3679c62..a66e64cec468332d67c999ece06ee659a3c44a93 100644 (file)
@@ -5,22 +5,22 @@ exec exec_async {
        timeout = 10
 }
 
-exec exec_async_inline {
+exec exec_async_mod {
        wait = no
        input_pairs = request
        shell_escape = yes
        timeout = 10
-       program = "/bin/sh -c 'echo goodbye'"
+       program = "/bin/sh -c 'echo \'Tmp-String-0 = welcome\''"
 }
 
 exec exec_sync {
        wait = yes
        input_pairs = control
        shell_escape = yes
-       timeout = 10
+       timeout = 1
 }
 
-exec exec_sync_inline {
+exec exec_sync_mod {
        wait = yes
        input_pairs = control
        output_pairs = control
@@ -28,3 +28,19 @@ exec exec_sync_inline {
        timeout = 10
        program = "/bin/sh -c 'echo \'Tmp-String-0 = welcome\''"
 }
+
+exec exec_sync_attrs {
+       wait = yes
+       input_pairs = request
+       output_pairs = control
+       shell_escape = yes
+       timeout = 10
+       program = "/bin/sh $ENV{MODULE_TEST_DIR}/attrs.sh %{User-Name}"
+}
+
+exec exec_sync_fail {
+       wait = yes
+       output_pairs = control
+       timeout = 10
+       program = "/bin/sh $ENV{MODULE_TEST_DIR}/fail.sh"
+}
index 4a263dbb3df02e740875ac78333223f6290f38e2..570fa3f0a06b2e5190568471bcbe8f08352cc385 100644 (file)
@@ -4,6 +4,7 @@
 Packet-Type = Access-Request
 User-Name = "tony"
 User-Password = "taponi"
+Called-Station-Id = "aabbccddeeff"
 
 #
 #  Expected answer
index 18cb317588907a12a521928327967df3400598fb..3fe37ec733966f7bc19beaf382fb4b1dc594f6fc 100644 (file)
@@ -1,28 +1,77 @@
 #
-#  Sync calls should always return a zero length string
-#  because we don't wait for the response.
+#  Sync calls return the response
 #
 update request {
-       &Tmp-String-0 := "%{exec_sync:/bin/sh -c 'echo hello'}"
+       &Tmp-String-0 := "%(exec_sync:/bin/sh -c 'echo hello')"
 }
 if (&Tmp-String-0 != 'hello') {
        test_fail
 }
 
 #
-#  Sync calls should have their output added to the request
+#  Call something which will take longer than the timeout
 #
 update request {
-       &Tmp-String-0 := "%{exec_sync:/bin/sh -c 'echo \"reply.Reply-Message = \'hello\'\"'}"
+       &Tmp-String-0 := "%(exec_sync:/bin/sleep 10)"
 }
 
-if (&reply.Reply-Message != 'hello') {
+if (&Module-Failure-Message[*] != "Timeout running program - killing it and failing the request") {
        test_fail
 }
 
-exec_sync_inline
+if (&Tmp-String-0 != "") {
+       test_fail
+}
+
+update request {
+       &Tmp-String-0 := "%(exec_sync:/bin/sh $ENV{MODULE_TEST_DIR}/fail.sh)"
+}
+
+if (&Tmp-String-0 != "") {
+       test_fail
+}
+
+if (&request.Module-Failure-Message != "Execution of external program returned 7") {
+       test_fail
+}
+
+#
+#  Sync calls as modules should have their output added to the request
+#
+exec_sync_mod
+
+if ((!&control.Tmp-String-0) || (&control.Tmp-String-0 != 'welcome')) {
+       test_fail
+}
+
+#
+#  The script called by this test reads input from both an expanded
+#  attribute on the command line and from the environment variables set.
+#  It then sets two attributes as two lines of output.
+#
+exec_sync_attrs
+
+if ((!&control.Tmp-String-1) || (&control.Tmp-String-1 != 'tony')) {
+       test_fail
+}
+
+if ((!&control.Tmp-String-2) || (&control.Tmp-String-2 != 'aabbccddeeff')) {
+       test_fail
+}
+
+#
+#  Call a module which returns status code 7 - mapped to module exit
+#  code "notfound".  Attributes in its output should be ignored.
+#
+exec_sync_fail
+
+if (notfound) {
+       ok
+} else {
+       test_fail
+}
 
-if (&control.Tmp-String-0 != 'welcome') {
+if (&control.Tmp-String-3 == "Failure") {
        test_fail
 }