*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*
- * Copyright 2000 The FreeRADIUS server project
- * Copyright 2000 Michael J. Hartwick <hartwick@hartwick.com>
+ * Copyright 2000-2004,2006 The FreeRADIUS server project
*/
-static const char rcsid[] = "$Id$";
-#include "autoconf.h"
-#include "libradius.h"
+#include <freeradius-devel/ident.h>
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/rad_assert.h>
#include <sys/file.h>
-#include <stdlib.h>
-#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <signal.h>
-#if HAVE_SYS_WAIT_H
+#ifdef HAVE_SYS_WAIT_H
# include <sys/wait.h>
#endif
#ifndef WEXITSTATUS
# define WIFEXITED(stat_val) (((stat_val) & 255) == 0)
#endif
-#include "radiusd.h"
-
+#define MAX_ARGV (256)
/*
* Execute a program on successful authentication.
* Return 0 if exec_wait == 0.
* Return the exit code of the called program if exec_wait != 0.
- *
+ * Return -1 on fork/other errors in the parent process.
*/
int radius_exec_program(const char *cmd, REQUEST *request,
- int exec_wait, const char **user_msg)
+ int exec_wait,
+ char *user_msg, int msg_len,
+ VALUE_PAIR *input_pairs,
+ VALUE_PAIR **output_pairs,
+ int shell_escape)
{
VALUE_PAIR *vp;
- static char message[256];
+ char mycmd[1024];
char answer[4096];
- char *argv[32];
- char *buf, *p;
+ char argv_buf[4096];
+ char *argv[MAX_ARGV];
+ const char *from;
+ char *p, *to;
int pd[2];
- pid_t pid;
+ pid_t pid, child_pid;
int argc = -1;
int comma = 0;
int status;
+ int i;
int n, left, done;
- void (*oldsig)(int) = NULL;
+
+ if (user_msg) *user_msg = '\0';
+ if (output_pairs) *output_pairs = NULL;
+
+ if (strlen(cmd) > (sizeof(mycmd) - 1)) {
+ radlog(L_ERR|L_CONS, "Command line is too long");
+ return -1;
+ }
+
+ /*
+ * Check for bad escapes.
+ */
+ if (cmd[strlen(cmd) - 1] == '\\') {
+ radlog(L_ERR|L_CONS, "Command line has final backslash, without a following character");
+ return -1;
+ }
+
+ strlcpy(mycmd, cmd, sizeof(mycmd));
/*
- * (hs) - Open a pipe for child/parent communication.
- * - Reset the signal handler for SIGCHLD, so
- * we have a chance to notice the dead child here and
- * not in some signal handler.
- * This has to be done for the exec_wait case only, since
- * if we don't wait we aren't interested in any
- * gone children ...
- */
+ * Split the string into argv's BEFORE doing radius_xlat...
+ */
+ from = cmd;
+ to = mycmd;
+ argc = 0;
+ while (*from) {
+ int length;
+
+ /*
+ * Skip spaces.
+ */
+ if ((*from == ' ') || (*from == '\t')) {
+ from++;
+ continue;
+ }
+
+ argv[argc] = to;
+ argc++;
+
+ if (argc >= (MAX_ARGV - 1)) break;
+
+ /*
+ * Copy the argv over to our buffer.
+ */
+ while (*from && (*from != ' ') && (*from != '\t')) {
+ if (to >= mycmd + sizeof(mycmd) - 1) {
+ return -1; /* ran out of space */
+ }
+
+ switch (*from) {
+ case '"':
+ case '\'':
+ length = rad_copy_string(to, from);
+ if (length < 0) {
+ radlog(L_ERR|L_CONS, "Invalid string passed as argument for external program");
+ return -1;
+ }
+ from += length;
+ to += length;
+ break;
+
+ case '%':
+ if (from[1] == '{') {
+ *(to++) = *(from++);
+
+ length = rad_copy_variable(to, from);
+ if (length < 0) {
+ radlog(L_ERR|L_CONS, "Invalid variable expansion passed as argument for external program");
+ return -1;
+ }
+ from += length;
+ to += length;
+ } else { /* FIXME: catch %%{ ? */
+ *(to++) = *(from++);
+ }
+ break;
+
+ default:
+ *(to++) = *(from++);
+ }
+ } /* end of string, or found a space */
+
+ *(to++) = '\0'; /* terminate the string */
+ }
+
+ /*
+ * We have to have SOMETHING, at least.
+ */
+ if (argc <= 0) {
+ radlog(L_ERR, "Exec-Program: empty command line.");
+ return -1;
+ }
+
+ /*
+ * Expand each string, as appropriate.
+ */
+ to = argv_buf;
+ left = sizeof(argv_buf);
+ for (i = 0; i < argc; i++) {
+ int sublen;
+
+ /*
+ * Don't touch argv's which won't be translated.
+ */
+ if (strchr(argv[i], '%') == NULL) continue;
+
+ sublen = radius_xlat(to, left - 1, argv[i], request, NULL);
+ if (sublen <= 0) {
+ /*
+ * Fail to be backwards compatible.
+ *
+ * It's yucky, but it won't break anything,
+ * and it won't cause security problems.
+ */
+ sublen = 0;
+ }
+
+ argv[i] = to;
+ to += sublen;
+ *(to++) = '\0';
+ left -= sublen;
+ left--;
+
+ if (left <= 0) {
+ radlog(L_ERR, "Exec-Program: Ran out of space while expanding arguments.");
+ return -1;
+ }
+ }
+ argv[argc] = NULL;
+
+ /*
+ * Open a pipe for child/parent communication, if necessary.
+ */
if (exec_wait) {
if (pipe(pd) != 0) {
- radlog(L_ERR|L_CONS, "Couldn't open pipe: %m");
- pd[0] = pd[1] = 0;
- }
- if ((oldsig = signal(SIGCHLD, SIG_DFL)) == SIG_ERR) {
- radlog(L_ERR|L_CONS, "Can't reset SIGCHLD: %m");
- oldsig = NULL;
+ radlog(L_ERR|L_CONS, "Couldn't open pipe: %s",
+ strerror(errno));
+ return -1;
}
+ } else {
+ /*
+ * We're not waiting, so we don't look for a
+ * message, or VP's.
+ */
+ user_msg = NULL;
+ output_pairs = NULL;
}
- if ((pid = fork()) == 0) {
+ if (exec_wait) {
+ pid = rad_fork(); /* remember PID */
+ } else {
+ pid = fork(); /* don't wait */
+ }
+
+ if (pid == 0) {
#define MAX_ENVP 1024
+ int devnull;
char *envp[MAX_ENVP];
int envlen;
char buffer[1024];
- /*
- * Child
- */
- radius_xlat2(answer, sizeof(answer), cmd, request);
- buf = answer;
-
/*
- * Log the command if we are debugging something
+ * Child process.
+ *
+ * We try to be fail-safe here. So if ANYTHING
+ * goes wrong, we exit with status 1.
*/
- DEBUG("Exec-Program: %s", buf);
/*
- * Build vector list and execute.
+ * Open STDIN to /dev/null
*/
- p = strtok(buf, " \t");
- if (p) do {
- argv[++argc] = p;
- p = strtok(NULL, " \t");
- } while(p != NULL);
- argv[++argc] = p;
- if (argc == 0) {
- radlog(L_ERR, "Exec-Program: empty command line.");
+ devnull = open("/dev/null", O_RDWR);
+ if (devnull < 0) {
+ radlog(L_ERR|L_CONS, "Failed opening /dev/null: %s\n",
+ strerror(errno));
exit(1);
}
+ dup2(devnull, STDIN_FILENO);
+ /*
+ * Only massage the pipe handles if the parent
+ * has created them.
+ */
if (exec_wait) {
- if (close(pd[0]) != 0)
- radlog(L_ERR|L_CONS, "Can't close pipe: %m");
- if (dup2(pd[1], 1) != 1)
- radlog(L_ERR|L_CONS, "Can't dup stdout: %m");
+ /*
+ * pd[0] is the FD the child will read from,
+ * which we don't want.
+ */
+ if (close(pd[0]) != 0) {
+ radlog(L_ERR|L_CONS, "Can't close pipe: %s",
+ strerror(errno));
+ exit(1);
+ }
+
+ /*
+ * pd[1] is the FD that the child will write to,
+ * so we make it STDOUT.
+ */
+ if (dup2(pd[1], STDOUT_FILENO) != 1) {
+ radlog(L_ERR|L_CONS, "Can't dup stdout: %s",
+ strerror(errno));
+ exit(1);
+ }
+
+ } else { /* no pipe, STDOUT should be /dev/null */
+ 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 (debug_flag == 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);
+
+ /*
* Set up the environment variables.
* We're in the child, and it will exit in 4 lines
* anyhow, so memory allocation isn't an issue.
*/
envlen = 0;
- for (vp = request->packet->vps; vp->next; vp = vp->next) {
+ for (vp = input_pairs; vp != NULL; vp = vp->next) {
/*
* Hmm... maybe we shouldn't pass the
* user's password in an environment
* variable...
*/
snprintf(buffer, sizeof(buffer), "%s=", vp->name);
- for (p = buffer; *p != '='; p++) {
- if (*p == '-') {
- *p = '_';
- } else if (isalpha(*p)) {
- *p = toupper(*p);
+ if (shell_escape) {
+ for (p = buffer; *p != '='; p++) {
+ if (*p == '-') {
+ *p = '_';
+ } else if (isalpha((int) *p)) {
+ *p = toupper(*p);
+ }
}
}
n = strlen(buffer);
- vp_prints_value(buffer+n, sizeof(buffer) - n, vp, 1);
+ vp_prints_value(buffer+n, sizeof(buffer) - n, vp, shell_escape);
envp[envlen++] = strdup(buffer);
- }
+ /*
+ * Don't add too many attributes.
+ */
+ if (envlen == (MAX_ENVP - 1)) break;
+ }
envp[envlen] = NULL;
-
-
-
-
- for(n = 32; n >= 3; n--)
- close(n);
execve(argv[0], argv, envp);
-
- radlog(L_ERR, "Exec-Program: %s: %m", argv[0]);
+ radlog(L_ERR, "Exec-Program: FAILED to execute %s: %s",
+ argv[0], strerror(errno));
exit(1);
}
/*
- * Parent
+ * Parent process.
*/
if (pid < 0) {
- radlog(L_ERR|L_CONS, "Couldn't fork: %m");
+ radlog(L_ERR|L_CONS, "Couldn't fork %s: %s",
+ argv[0], strerror(errno));
return -1;
}
- if (!exec_wait)
+
+ /*
+ * We're not waiting, exit, and ignore any child's status.
+ */
+ if (!exec_wait) {
return 0;
+ }
/*
- * (hs) Do we have a pipe?
- * --> Close the write side of the pipe
- * --> Read from it.
+ * Close the FD to which the child writes it's data.
+ *
+ * If we can't close it, then we close pd[0], and return an
+ * error.
*/
- done = 0;
- if (pd[0] || pd[1]) {
- if (close(pd[1]) != 0)
- radlog(L_ERR|L_CONS, "Can't close pipe: %m");
+ if (close(pd[1]) != 0) {
+ radlog(L_ERR|L_CONS, "Can't close pipe: %s", strerror(errno));
+ close(pd[0]);
+ return -1;
+ }
+ /*
+ * Read from the pipe until we doesn't get any more or
+ * until the message is full.
+ */
+ done = 0;
+ left = sizeof(answer) - 1;
+ while (1) {
+ status = read(pd[0], answer + done, left);
/*
- * (hs) Read until we doesn't get any more
- * or until the message is full.
+ * Nothing more to read: stop.
*/
- done = 0;
- left = sizeof(answer) - 1;
- while ((n = read(pd[0], answer + done, left)) > 0) {
- done += n;
- left -= n;
- if (left <= 0) break;
+ if (status == 0) {
+ break;
}
- answer[done] = 0;
/*
- * (hs) Make sure that the writer can't block
- * while writing in a pipe that isn't read anymore.
+ * Error: See if we have to continue.
*/
- close(pd[0]);
+ 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;
}
+ answer[done] = 0;
+
+ /*
+ * Make sure that the writer can't block while writing to
+ * a pipe that no one is reading from anymore.
+ */
+ close(pd[0]);
+
+ DEBUG2("Exec-Program output: %s", answer);
/*
* Parse the output, if any.
*/
if (done) {
- /*
- * For backwards compatibility, first check
- * for plain text (user_msg).
- */
- vp = NULL;
- n = userparse(answer, &vp);
- if (vp)
- pairfree(&vp);
+ n = T_OP_INVALID;
+ if (output_pairs) {
+ /*
+ * For backwards compatibility, first check
+ * for plain text (user_msg).
+ */
+ vp = NULL;
+ n = userparse(answer, &vp);
+ if (vp) {
+ pairfree(&vp);
+ }
+ }
- if (n != 0) {
- radlog(L_DBG, "Exec-Program-Wait: plaintext: %s", answer);
+ if (n == T_OP_INVALID) {
+ DEBUG("Exec-Program-Wait: plaintext: %s", answer);
if (user_msg) {
- strncpy(message, answer, sizeof(message));
- message[sizeof(message) - 1] = 0;
- *user_msg = message;
+ strlcpy(user_msg, answer, msg_len);
}
} else {
/*
}
/*
- * Replace any trailing comma by a NUL.
+ * Replace any trailing comma by a NUL.
*/
- if (answer[strlen(answer) - 1] == ',')
+ if (answer[strlen(answer) - 1] == ',') {
answer[strlen(answer) - 1] = '\0';
+ }
radlog(L_DBG,"Exec-Program-Wait: value-pairs: %s", answer);
- if (userparse(answer, &vp) != 0)
- radlog(L_ERR,
- "Exec-Program-Wait: %s: unparsable reply", cmd);
- else {
- pairmove(&request->reply->vps, &vp);
- pairfree(&vp);
+ if (userparse(answer, &vp) == T_OP_INVALID) {
+ radlog(L_ERR, "Exec-Program-Wait: %s: unparsable reply", cmd);
+
+ } else {
+ /*
+ * Tell the caller about the value
+ * pairs.
+ */
+ *output_pairs = vp;
}
- }
- }
-
- while(waitpid(pid, &status, 0) != pid)
- ;
+ } /* else the answer was a set of VP's, not a text message */
+ } /* else we didn't read anything from the child */
/*
- * (hs) Now we let our cleanup_sig handler take care for
- * all signals that will arise.
+ * Call rad_waitpid (should map to waitpid on non-threaded
+ * or single-server systems).
*/
- if (oldsig && (signal(SIGCHLD, oldsig) == SIG_ERR))
- radlog(L_ERR|L_CONS,
- "Can't set SIGCHLD to the cleanup handler: %m");
- sig_cleanup(SIGCHLD);
-
- if (WIFEXITED(status)) {
- status = WEXITSTATUS(status);
- radlog(L_INFO, "Exec-Program: returned: %d", status);
- return status;
+ child_pid = rad_waitpid(pid, &status);
+ if (child_pid == 0) {
+ radlog(L_DBG, "Exec-Program: Timeout waiting for child");
+ return 2;
}
- radlog(L_ERR|L_CONS, "Exec-Program: Abnormal child exit (killed or coredump)");
+ if (child_pid == pid) {
+ if (WIFEXITED(status)) {
+ status = WEXITSTATUS(status);
+ radlog(L_DBG, "Exec-Program: returned: %d", status);
+ return status;
+ }
+ }
+
+ radlog(L_ERR|L_CONS, "Exec-Program: Abnormal child exit: %s",
+ strerror(errno));
return 1;
}
-