diff options
Diffstat (limited to 'src/main/exec.c')
-rw-r--r-- | src/main/exec.c | 633 |
1 files changed, 633 insertions, 0 deletions
diff --git a/src/main/exec.c b/src/main/exec.c new file mode 100644 index 0000000..67243f7 --- /dev/null +++ b/src/main/exec.c @@ -0,0 +1,633 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* + * $Id$ + * + * @file exec.c + * @brief Execute external programs. + * + * @copyright 2000-2004,2006 The FreeRADIUS server project + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +#include <sys/file.h> + +#include <fcntl.h> +#include <ctype.h> + +#ifdef HAVE_SYS_WAIT_H +# include <sys/wait.h> +#endif +#ifndef WEXITSTATUS +# define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8) +#endif +#ifndef WIFEXITED +# define WIFEXITED(stat_val) (((stat_val) & 255) == 0) +#endif + +#define MAX_ARGV (256) + +/** Start a process + * + * @param cmd Command to execute. This is parsed into argv[] parts, + * then each individual argv part is xlat'ed. + * @param request Current reuqest + * @param exec_wait set to 1 if you want to read from or write to child + * @param[in,out] input_fd pointer to int, receives the stdin file. + * descriptor. Set to NULL and the child will have /dev/null on stdin + * @param[in,out] output_fd pinter to int, receives the stdout file + * descriptor. Set to NULL and child will have /dev/null on stdout. + * @param input_pairs list of value pairs - these will be put into + * the environment variables of the child. + * @param shell_escape values before passing them as arguments. + * @return PID of the child process, -1 on error. + */ +pid_t radius_start_program(char const *cmd, REQUEST *request, bool exec_wait, + int *input_fd, int *output_fd, + VALUE_PAIR *input_pairs, bool shell_escape) +{ +#ifndef __MINGW32__ + VALUE_PAIR *vp; + int n; + int to_child[2] = {-1, -1}; + int from_child[2] = {-1, -1}; + pid_t pid; +#endif + int argc; + int i; + char const **argv_p; + char *argv[MAX_ARGV], **argv_start = argv; + char argv_buf[4096]; +#define MAX_ENVP 1024 + char *envp[MAX_ENVP]; + int envlen = 0; + + /* + * Stupid array decomposition... + * + * If we do memcpy(&argv_p, &argv, sizeof(argv_p)) src ends up being a char ** + * pointing to the value of the first element. + */ + memcpy(&argv_p, &argv_start, sizeof(argv_p)); + argc = rad_expand_xlat(request, cmd, MAX_ARGV, argv_p, true, sizeof(argv_buf), argv_buf); + if (argc <= 0) { + DEBUG("invalid command line '%s'.", cmd); + return -1; + } + + +#ifndef NDEBUG + if (rad_debug_lvl > 2) { + DEBUG3("executing cmd %s", cmd); + for (i = 0; i < argc; i++) { + DEBUG3("\t[%d] %s", i, argv[i]); + } + } +#endif + +#ifndef __MINGW32__ + /* + * Open a pipe for child/parent communication, if necessary. + */ + if (exec_wait) { + if (input_fd) { + if (pipe(to_child) != 0) { + DEBUG("Couldn't open pipe to child: %s", fr_syserror(errno)); + return -1; + } + } + if (output_fd) { + if (pipe(from_child) != 0) { + DEBUG("Couldn't open pipe from child: %s", fr_syserror(errno)); + /* safe because these either need closing or are == -1 */ + close(to_child[0]); + close(to_child[1]); + return -1; + } + } + } + + envp[0] = NULL; + + if (input_pairs) { + vp_cursor_t cursor; + char buffer[1024]; + + /* + * Set up the environment variables in the + * parent, so we don't call libc functions that + * hold mutexes. They might be locked when we fork, + * and will remain locked in the child. + */ + for (vp = fr_cursor_init(&cursor, &input_pairs); + vp; + vp = fr_cursor_next(&cursor)) { + /* + * Hmm... maybe we shouldn't pass the + * user's password in an environment + * variable... + */ + snprintf(buffer, sizeof(buffer), "%s=", vp->da->name); + if (shell_escape) { + char *p; + + for (p = buffer; *p != '='; p++) { + if (*p == '-') { + *p = '_'; + } else if (isalpha((uint8_t) *p)) { + *p = toupper((uint8_t) *p); + } + } + } + + n = strlen(buffer); + vp_prints_value(buffer + n, sizeof(buffer) - n, vp, shell_escape ? '"' : 0); + + envp[envlen++] = strdup(buffer); + + /* + * Don't add too many attributes. + */ + if (envlen == (MAX_ENVP - 1)) break; + + /* + * NULL terminate for execve + */ + envp[envlen] = NULL; + } + } + + if (exec_wait) { + pid = rad_fork(); /* remember PID */ + } else { + pid = fork(); /* don't wait */ + } + + if (pid == 0) { + int devnull; + + /* + * Child process. + * + * We try to be fail-safe here. So if ANYTHING + * goes wrong, we exit with status 1. + */ + + /* + * Open STDIN to /dev/null + */ + devnull = open("/dev/null", O_RDWR); + if (devnull < 0) { + DEBUG("Failed opening /dev/null: %s\n", fr_syserror(errno)); + + /* + * Where the status code is interpreted as a module rcode + * one is subtracted from it, to allow 0 to equal success + * + * 2 is RLM_MODULE_FAIL + 1 + */ + exit(2); + } + + /* + * Only massage the pipe handles if the parent + * has created them. + */ + if (exec_wait) { + if (input_fd) { + close(to_child[1]); + dup2(to_child[0], STDIN_FILENO); + } else { + dup2(devnull, STDIN_FILENO); + } + + if (output_fd) { + close(from_child[0]); + dup2(from_child[1], STDOUT_FILENO); + } else { + dup2(devnull, STDOUT_FILENO); + } + + } else { /* no pipe, STDOUT should be /dev/null */ + dup2(devnull, STDIN_FILENO); + dup2(devnull, STDOUT_FILENO); + } + + /* + * If we're not debugging, then we can't do + * anything with the error messages, so we throw + * them away. + * + * If we are debugging, then we want the error + * messages to go to the STDERR of the server. + */ + if (rad_debug_lvl == 0) { + dup2(devnull, STDERR_FILENO); + } + close(devnull); + + /* + * The server may have MANY FD's open. We don't + * want to leave dangling FD's for the child process + * to play funky games with, so we close them. + */ + closefrom(3); + + /* + * I swear the signature for execve is wrong and should + * take 'char const * const argv[]'. + * + * Note: execve(), unlike system(), treats all the space + * delimited arguments as literals, so there's no need + * to perform additional escaping. + */ + execve(argv[0], argv, envp); + printf("Failed to execute \"%s\": %s", argv[0], fr_syserror(errno)); /* fork output will be captured */ + + /* + * Where the status code is interpreted as a module rcode + * one is subtracted from it, to allow 0 to equal success + * + * 2 is RLM_MODULE_FAIL + 1 + */ + exit(2); + } + + /* + * Free child environment variables + */ + for (i = 0; i < envlen; i++) { + free(envp[i]); + } + + /* + * Parent process. + */ + if (pid < 0) { + DEBUG("Couldn't fork %s: %s", argv[0], fr_syserror(errno)); + if (exec_wait) { + /* safe because these either need closing or are == -1 */ + close(to_child[0]); + close(to_child[1]); + close(from_child[0]); + close(from_child[1]); + } + return -1; + } + + /* + * We're not waiting, exit, and ignore any child's status. + */ + if (exec_wait) { + /* + * Close the ends of the pipe(s) the child is using + * return the ends of the pipe(s) our caller wants + * + */ + if (input_fd) { + *input_fd = to_child[1]; + close(to_child[0]); + } + if (output_fd) { + *output_fd = from_child[0]; + close(from_child[1]); + } + } + + return pid; +#else + if (exec_wait) { + DEBUG("Wait is not supported"); + return -1; + } + + { + /* + * The _spawn and _exec families of functions are + * found in Windows compiler libraries for + * portability from UNIX. There is a variety of + * functions, including the ability to pass + * either a list or array of parameters, to + * search in the PATH or otherwise, and whether + * or not to pass an environment (a set of + * environment variables). Using _spawn, you can + * also specify whether you want the new process + * to close your program (_P_OVERLAY), to wait + * until the new process is finished (_P_WAIT) or + * for the two to run concurrently (_P_NOWAIT). + + * _spawn and _exec are useful for instances in + * which you have simple requirements for running + * the program, don't want the overhead of the + * Windows header file, or are interested + * primarily in portability. + */ + + /* + * FIXME: check return code... what is it? + */ + _spawnve(_P_NOWAIT, argv[0], argv, envp); + } + + return 0; +#endif +} + +/** Read from the child process. + * + * @param fd file descriptor to read from. + * @param pid pid of child, will be reaped if it dies. + * @param timeout amount of time to wait, in seconds. + * @param answer buffer to write into. + * @param left length of buffer. + * @return -1 on error, or length of output. + */ +int radius_readfrom_program(int fd, pid_t pid, int timeout, + char *answer, int left) +{ + int done = 0; +#ifndef __MINGW32__ + int status; + struct timeval start; +#ifdef O_NONBLOCK + bool nonblock = true; +#endif + +#ifdef O_NONBLOCK + /* + * Try to set it non-blocking. + */ + do { + int flags; + + if ((flags = fcntl(fd, F_GETFL, NULL)) < 0) { + nonblock = false; + break; + } + + flags |= O_NONBLOCK; + if( fcntl(fd, F_SETFL, flags) < 0) { + nonblock = false; + break; + } + } while (0); +#endif + + + /* + * Read from the pipe until we doesn't get any more or + * until the message is full. + */ + gettimeofday(&start, NULL); + while (1) { + int rcode; + fd_set fds; + struct timeval when, elapsed, wake; + + FD_ZERO(&fds); + FD_SET(fd, &fds); + + gettimeofday(&when, NULL); + rad_tv_sub(&when, &start, &elapsed); + if (elapsed.tv_sec >= timeout) goto too_long; + + when.tv_sec = timeout; + when.tv_usec = 0; + rad_tv_sub(&when, &elapsed, &wake); + + rcode = select(fd + 1, &fds, NULL, NULL, &wake); + if (rcode == 0) { + too_long: + DEBUG("Child PID %u is taking too much time: forcing failure and killing child.", (unsigned int) pid); + kill(pid, SIGTERM); + close(fd); /* should give SIGPIPE to child, too */ + + /* + * Clean up the child entry. + */ + rad_waitpid(pid, &status); + return -1; + } + if (rcode < 0) { + if (errno == EINTR) continue; + break; + } + +#ifdef O_NONBLOCK + /* + * Read as many bytes as possible. The kernel + * will return the number of bytes available. + */ + if (nonblock) { + status = read(fd, answer + done, left); + } else +#endif + /* + * There's at least 1 byte ready: read it. + */ + status = read(fd, answer + done, 1); + + /* + * Nothing more to read: stop. + */ + if (status == 0) { + break; + } + + /* + * Error: See if we have to continue. + */ + if (status < 0) { + /* + * We were interrupted: continue reading. + */ + if (errno == EINTR) { + continue; + } + + /* + * There was another error. Most likely + * The child process has finished, and + * exited. + */ + break; + } + + done += status; + left -= status; + if (left <= 0) break; + } +#endif /* __MINGW32__ */ + + /* Strip trailing new lines */ + while ((done > 0) && (answer[done - 1] == '\n')) { + answer[--done] = '\0'; + } + + return done; +} + +/** Execute a program. + * + * @param[in,out] ctx to allocate new VALUE_PAIR (s) in. + * @param[out] out buffer to append plaintext (non valuepair) output. + * @param[in] outlen length of out buffer. + * @param[out] output_pairs list of value pairs - child stdout will be parsed and added into this list + * of value pairs. + * @param[in] request Current request (may be NULL). + * @param[in] cmd Command to execute. This is parsed into argv[] parts, then each individual argv part + * is xlat'ed. + * @param[in] input_pairs list of value pairs - these will be available in the environment of the child. + * @param[in] exec_wait set to 1 if you want to read from or write to child. + * @param[in] shell_escape values before passing them as arguments. + * @param[in] timeout amount of time to wait, in seconds. + + * @return 0 if exec_wait==0, exit code if exec_wait!=0, -1 on error. + */ +int radius_exec_program(TALLOC_CTX *ctx, char *out, size_t outlen, VALUE_PAIR **output_pairs, + REQUEST *request, char const *cmd, VALUE_PAIR *input_pairs, + bool exec_wait, bool shell_escape, int timeout) + +{ + pid_t pid; + int from_child; +#ifndef __MINGW32__ + char *p; + pid_t child_pid; + int comma = 0; + int status, ret = 0; + ssize_t len; + char answer[4096]; +#endif + + RDEBUG2("Executing: %s:", cmd); + + if (out) *out = '\0'; + + pid = radius_start_program(cmd, request, exec_wait, NULL, &from_child, input_pairs, shell_escape); + if (pid < 0) { + return -1; + } + + if (!exec_wait) { + return 0; + } + +#ifndef __MINGW32__ + len = radius_readfrom_program(from_child, pid, timeout, answer, sizeof(answer)); + if (len < 0) { + /* + * Failure - radius_readfrom_program will + * have called close(from_child) for us + */ + RERROR("Failed to read from child output"); + return -1; + + } + answer[len] = '\0'; + + /* + * Make sure that the writer can't block while writing to + * a pipe that no one is reading from anymore. + */ + close(from_child); + + if (len == 0) { + goto wait; + } + + /* + * Parse the output, if any. + */ + if (output_pairs) { + /* + * HACK: Replace '\n' with ',' so that + * fr_pair_list_afrom_str() can parse the buffer in + * one go (the proper way would be to + * fix fr_pair_list_afrom_str(), but oh well). + */ + for (p = answer; *p; p++) { + if (*p == '\n') { + *p = comma ? ' ' : ','; + p++; + comma = 0; + } + if (*p == ',') { + comma++; + } + } + + /* + * Replace any trailing comma by a NUL. + */ + if (answer[len - 1] == ',') { + answer[--len] = '\0'; + } + + if (fr_pair_list_afrom_str(ctx, answer, output_pairs) == T_INVALID) { + RERROR("Failed parsing output from: %s: %s", cmd, fr_strerror()); + if (out) strlcpy(out, answer, len); + ret = -1; + } + + VERIFY_REQUEST(request); + + + /* + * We've not been told to extract output pairs, + * just copy the programs output to the out + * buffer. + */ + + } else if (out) { + strlcpy(out, answer, outlen); + } + + /* + * Call rad_waitpid (should map to waitpid on non-threaded + * or single-server systems). + */ +wait: + child_pid = rad_waitpid(pid, &status); + if (child_pid == 0) { + RERROR("Timeout waiting for child"); + + return -2; + } + + if (child_pid == pid) { + if (WIFEXITED(status)) { + status = WEXITSTATUS(status); + if ((status != 0) || (ret < 0)) { + RERROR("Program returned code (%d) and output '%s'", status, answer); + } else { + RDEBUG2("Program returned code (%d) and output '%s'", status, answer); + } + + return ret < 0 ? ret : status; + } + } + + RERROR("Abnormal child exit: %s", fr_syserror(errno)); +#endif /* __MINGW32__ */ + + return -1; +} |