From: Nick Porter Date: Wed, 9 Jun 2021 01:31:32 +0000 (+0100) Subject: v4: Convert %(exec: ) to new xlat API (#4106) X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=43f90e708b720971df5cfce7125e278ee189e97a;p=thirdparty%2Ffreeradius-server.git v4: Convert %(exec: ) to new xlat API (#4106) * 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 --- diff --git a/doc/antora/modules/howto/pages/modules/mschap/index.adoc b/doc/antora/modules/howto/pages/modules/mschap/index.adoc index 192d8ae34de..82ec9f10a7c 100644 --- a/doc/antora/modules/howto/pages/modules/mschap/index.adoc +++ b/doc/antora/modules/howto/pages/modules/mschap/index.adoc @@ -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 diff --git a/doc/antora/modules/raddb/pages/mods-available/echo.adoc b/doc/antora/modules/raddb/pages/mods-available/echo.adoc index 9b09f914ddb..c08064c7816 100644 --- a/doc/antora/modules/raddb/pages/mods-available/echo.adoc +++ b/doc/antora/modules/raddb/pages/mods-available/echo.adoc @@ -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 diff --git a/doc/antora/modules/raddb/pages/mods-available/exec.adoc b/doc/antora/modules/raddb/pages/mods-available/exec.adoc index af81ccc71ea..b3388542923 100644 --- a/doc/antora/modules/raddb/pages/mods-available/exec.adoc +++ b/doc/antora/modules/raddb/pages/mods-available/exec.adoc @@ -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 diff --git a/doc/antora/modules/raddb/pages/mods-available/mschap.adoc b/doc/antora/modules/raddb/pages/mods-available/mschap.adoc index af5ddc37bcc..a1629b1e798 100644 --- a/doc/antora/modules/raddb/pages/mods-available/mschap.adoc +++ b/doc/antora/modules/raddb/pages/mods-available/mschap.adoc @@ -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 diff --git a/doc/antora/modules/raddb/pages/sites-available/resource-check.adoc b/doc/antora/modules/raddb/pages/sites-available/resource-check.adoc index f06794e6398..412116ba0af 100644 --- a/doc/antora/modules/raddb/pages/sites-available/resource-check.adoc +++ b/doc/antora/modules/raddb/pages/sites-available/resource-check.adoc @@ -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. diff --git a/doc/antora/modules/tutorials/pages/dynamic-translation.adoc b/doc/antora/modules/tutorials/pages/dynamic-translation.adoc index 131249d16e9..3e6f5e89706 100644 --- a/doc/antora/modules/tutorials/pages/dynamic-translation.adoc +++ b/doc/antora/modules/tutorials/pages/dynamic-translation.adoc @@ -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, diff --git a/man/man5/unlang.5 b/man/man5/unlang.5 index 58d22cc9a14..f50f89153a9 100644 --- a/man/man5/unlang.5 +++ b/man/man5/unlang.5 @@ -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 diff --git a/raddb/mods-available/echo b/raddb/mods-available/echo index f24f31716cd..6b7f144fe71 100644 --- a/raddb/mods-available/echo +++ b/raddb/mods-available/echo @@ -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"] diff --git a/raddb/mods-available/exec b/raddb/mods-available/exec index 4e9e0b1d1c3..bbbce9d8a4f 100644 --- a/raddb/mods-available/exec +++ b/raddb/mods-available/exec @@ -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}" diff --git a/raddb/mods-available/mschap b/raddb/mods-available/mschap index 5eb4501d6c2..22db84fce43 100644 --- a/raddb/mods-available/mschap +++ b/raddb/mods-available/mschap @@ -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'}" } diff --git a/raddb/sites-available/resource-check b/raddb/sites-available/resource-check index 9f1d9dad719..452b21031b1 100644 --- a/raddb/sites-available/resource-check +++ b/raddb/sites-available/resource-check @@ -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. diff --git a/src/lib/server/exec.c b/src/lib/server/exec.c index 0ad10b7965d..35c93573ad0 100644 --- a/src/lib/server/exec.c +++ b/src/lib/server/exec.c @@ -28,6 +28,7 @@ RCSID("$Id$") #include #include #include +#include #include #include @@ -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; +} diff --git a/src/lib/server/exec.h b/src/lib/server/exec.h index 37bc08cc4ee..9c913c82c1b 100644 --- a/src/lib/server/exec.h +++ b/src/lib/server/exec.h @@ -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 diff --git a/src/lib/unlang/tmpl.c b/src/lib/unlang/tmpl.c index e0c61cdccd0..d61905a3e1c 100644 --- a/src/lib/unlang/tmpl.c +++ b/src/lib/unlang/tmpl.c @@ -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); diff --git a/src/lib/unlang/tmpl_priv.h b/src/lib/unlang/tmpl_priv.h index 1bd899b8c03..39410d28da9 100644 --- a/src/lib/unlang/tmpl_priv.h +++ b/src/lib/unlang/tmpl_priv.h @@ -24,6 +24,7 @@ * @copyright 2006-2019 The FreeRADIUS server project */ #include "unlang_priv.h" +#include #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; diff --git a/src/modules/rlm_exec/rlm_exec.c b/src/modules/rlm_exec/rlm_exec.c index 904f9f3e775..7a7081c2148 100644 --- a/src/modules/rlm_exec/rlm_exec.c +++ b/src/modules/rlm_exec/rlm_exec.c @@ -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 */ diff --git a/src/modules/rlm_mschap/rlm_mschap.c b/src/modules/rlm_mschap/rlm_mschap.c index 657c816296a..068015b8a5f 100644 --- a/src/modules/rlm_mschap/rlm_mschap.c +++ b/src/modules/rlm_mschap/rlm_mschap.c @@ -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; diff --git a/src/tests/keywords/xlat-exec b/src/tests/keywords/xlat-exec index 8fc6e7be11b..055cd240242 100644 --- a/src/tests/keywords/xlat-exec +++ b/src/tests/keywords/xlat-exec @@ -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 index 00000000000..487bf23fb8c --- /dev/null +++ b/src/tests/keywords/xlat-exec.attrs @@ -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" diff --git a/src/tests/modules/exec/async.unlang b/src/tests/modules/exec/async.unlang index 67775b9eb5c..9acaaf4b5f1 100644 --- a/src/tests/modules/exec/async.unlang +++ b/src/tests/modules/exec/async.unlang @@ -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 index 00000000000..7633012a26f --- /dev/null +++ b/src/tests/modules/exec/attrs.sh @@ -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 index 00000000000..6999952d6a8 --- /dev/null +++ b/src/tests/modules/exec/fail.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +echo Tmp-String-3 := Failure + +# exit status 7 maps to module exit code "notfound" +exit 7 diff --git a/src/tests/modules/exec/module.conf b/src/tests/modules/exec/module.conf index 4dd8cb701d2..a66e64cec46 100644 --- a/src/tests/modules/exec/module.conf +++ b/src/tests/modules/exec/module.conf @@ -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" +} diff --git a/src/tests/modules/exec/sync.attrs b/src/tests/modules/exec/sync.attrs index 4a263dbb3df..570fa3f0a06 100644 --- a/src/tests/modules/exec/sync.attrs +++ b/src/tests/modules/exec/sync.attrs @@ -4,6 +4,7 @@ Packet-Type = Access-Request User-Name = "tony" User-Password = "taponi" +Called-Station-Id = "aabbccddeeff" # # Expected answer diff --git a/src/tests/modules/exec/sync.unlang b/src/tests/modules/exec/sync.unlang index 18cb3175889..3fe37ec7339 100644 --- a/src/tests/modules/exec/sync.unlang +++ b/src/tests/modules/exec/sync.unlang @@ -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 }