. 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
= 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:
```
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
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:
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
# 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
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.
-------------------------------------------------------------------------------
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
(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
-------------------------------------------------------------------------------
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}"
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
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,
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
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
#
# = 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:
# 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"]
#
# 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"]
# |===
#
# 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}"
#
# 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'}"
}
#
# 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.
#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>
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;
+}
#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);
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
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
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.
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);
}
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);
* @copyright 2006-2019 The FreeRADIUS server project
*/
#include "unlang_priv.h"
+#include <freeradius-devel/server/exec.h>
#ifdef __cplusplus
extern "C" {
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;
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;
}
/*
*/
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;
return -1;
}
- if (inst->timeout_is_set || !inst->timeout) {
+ if (!inst->timeout_is_set || !inst->timeout) {
/*
* Pick the shorter one
*/
*
* ...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;
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
--- /dev/null
+#
+# Input packet
+#
+Packet-Type = Access-Request
+User-Name = "bob"
+User-Password = "hel|o'"
+
+#
+# Expected answer
+#
+Packet-Type == Access-Accept
+Result-Status == "success"
# 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
-}
--- /dev/null
+#!/bin/sh
+
+echo Tmp-String-1 := $1
+echo Tmp-String-2 := ${CALLED_STATION_ID}
+
+exit 0;
\ No newline at end of file
--- /dev/null
+#!/bin/sh
+
+echo Tmp-String-3 := Failure
+
+# exit status 7 maps to module exit code "notfound"
+exit 7
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
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"
+}
Packet-Type = Access-Request
User-Name = "tony"
User-Password = "taponi"
+Called-Station-Id = "aabbccddeeff"
#
# Expected answer
#
-# 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
}