diff options
Diffstat (limited to '')
-rw-r--r-- | common/exectool.c | 650 |
1 files changed, 650 insertions, 0 deletions
diff --git a/common/exectool.c b/common/exectool.c new file mode 100644 index 0000000..3458de4 --- /dev/null +++ b/common/exectool.c @@ -0,0 +1,650 @@ +/* exectool.c - Utility functions to execute a helper tool + * Copyright (C) 2015 Werner Koch + * Copyright (C) 2016 g10 Code GmbH + * + * This file is part of GnuPG. + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of either + * + * - the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at + * your option) any later version. + * + * or + * + * - 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. + * + * or both in parallel, as here. + * + * This file 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, see <https://www.gnu.org/licenses/>. + */ + +#include <config.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> +#include <errno.h> +#include <assert.h> +#include <gpg-error.h> + +#include <assuan.h> +#include "i18n.h" +#include "logging.h" +#include "membuf.h" +#include "mischelp.h" +#include "exechelp.h" +#include "sysutils.h" +#include "util.h" +#include "exectool.h" + +typedef struct +{ + const char *pgmname; + exec_tool_status_cb_t status_cb; + void *status_cb_value; + int cont; + size_t used; + size_t buffer_size; + char *buffer; +} read_and_log_buffer_t; + + +static inline gpg_error_t +my_error_from_syserror (void) +{ + return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); +} + + +static void +read_and_log_stderr (read_and_log_buffer_t *state, es_poll_t *fderr) +{ + gpg_error_t err; + int c; + + if (!fderr) + { + /* Flush internal buffer. */ + if (state->used) + { + const char *pname; + int len; + + state->buffer[state->used] = 0; + state->used = 0; + + pname = strrchr (state->pgmname, '/'); + if (pname && pname != state->pgmname && pname[1]) + pname++; + else + pname = state->pgmname; + len = strlen (pname); + + if (state->status_cb + && !strncmp (state->buffer, "[GNUPG:] ", 9) + && state->buffer[9] >= 'A' && state->buffer[9] <= 'Z') + { + char *rest; + + rest = strchr (state->buffer + 9, ' '); + if (!rest) + { + /* Set REST to an empty string. */ + rest = state->buffer + strlen (state->buffer); + } + else + { + *rest++ = 0; + trim_spaces (rest); + } + state->status_cb (state->status_cb_value, + state->buffer + 9, rest); + } + else if (!state->cont + && !strncmp (state->buffer, pname, len) + && strlen (state->buffer) > strlen (pname) + && state->buffer[len] == ':' ) + { + /* PGMNAME plus colon is identical to the start of + the output: print only the output. */ + log_info ("%s\n", state->buffer); + } + else + log_info ("%s%c %s\n", + pname, state->cont? '+':':', state->buffer); + } + state->cont = 0; + return; + } + for (;;) + { + c = es_fgetc (fderr->stream); + if (c == EOF) + { + if (es_feof (fderr->stream)) + { + fderr->ignore = 1; /* Not anymore needed. */ + } + else if (es_ferror (fderr->stream)) + { + err = my_error_from_syserror (); + log_error ("error reading stderr of '%s': %s\n", + state->pgmname, gpg_strerror (err)); + fderr->ignore = 1; /* Disable. */ + } + + break; + } + else if (c == '\n') + { + read_and_log_stderr (state, NULL); + } + else + { + if (state->used >= state->buffer_size - 1) + { + if (state->status_cb) + { + /* A status callback requires that we have a full + * line. Thus we need to enlarget the buffer in + * this case. */ + char *newbuffer; + size_t newsize = state->buffer_size + 256; + + newbuffer = xtrymalloc (newsize); + if (!newbuffer) + { + log_error ("error allocating memory for status cb: %s\n", + gpg_strerror (my_error_from_syserror ())); + /* We better disable the status CB in this case. */ + state->status_cb = NULL; + read_and_log_stderr (state, NULL); + state->cont = 1; + } + else + { + memcpy (newbuffer, state->buffer, state->used); + xfree (state->buffer); + state->buffer = newbuffer; + state->buffer_size = newsize; + } + } + else + { + read_and_log_stderr (state, NULL); + state->cont = 1; + } + } + state->buffer[state->used++] = c; + } + } +} + + + +/* A buffer to copy from one stream to another. */ +struct copy_buffer +{ + char buffer[4096]; + char *writep; + size_t nread; +}; + + +/* Initialize a copy buffer. */ +static void +copy_buffer_init (struct copy_buffer *c) +{ + c->writep = c->buffer; + c->nread = 0; +} + + +/* Securely wipe a copy buffer. */ +static void +copy_buffer_shred (struct copy_buffer *c) +{ + if (c == NULL) + return; + wipememory (c->buffer, sizeof c->buffer); + c->writep = NULL; + c->nread = ~0U; +} + + +/* Copy data from SOURCE to SINK using copy buffer C. */ +static gpg_error_t +copy_buffer_do_copy (struct copy_buffer *c, estream_t source, estream_t sink) +{ + gpg_error_t err; + size_t nwritten = 0; + + if (c->nread == 0) + { + c->writep = c->buffer; + if (es_read (source, c->buffer, sizeof c->buffer, &c->nread)) + { + err = my_error_from_syserror (); + if (gpg_err_code (err) == GPG_ERR_EAGAIN) + return 0; /* We will just retry next time. */ + + return err; + } + + log_assert (c->nread <= sizeof c->buffer); + } + + if (c->nread == 0) + return 0; /* Done copying. */ + + nwritten = 0; + if (sink && es_write (sink, c->writep, c->nread, &nwritten)) + err = my_error_from_syserror (); + else + err = 0; + + log_assert (nwritten <= c->nread); + c->writep += nwritten; + c->nread -= nwritten; + log_assert (c->writep - c->buffer <= sizeof c->buffer); + + if (err) + { + if (gpg_err_code (err) == GPG_ERR_EAGAIN) + return 0; /* We will just retry next time. */ + + return err; + } + + if (sink && es_fflush (sink) && errno != EAGAIN) + err = my_error_from_syserror (); + + return err; +} + + +/* Flush the remaining data to SINK. */ +static gpg_error_t +copy_buffer_flush (struct copy_buffer *c, estream_t sink) +{ + gpg_error_t err = 0; + size_t nwritten = 0; + + if (es_write (sink, c->writep, c->nread, &nwritten)) + err = my_error_from_syserror (); + + log_assert (nwritten <= c->nread); + c->writep += nwritten; + c->nread -= nwritten; + log_assert (c->writep - c->buffer <= sizeof c->buffer); + + if (err) + return err; + + if (es_fflush (sink)) + err = my_error_from_syserror (); + + return err; +} + + + +/* Run the program PGMNAME with the command line arguments given in + * the NULL terminates array ARGV. If INPUT is not NULL it will be + * fed to stdin of the process. stderr is logged using log_info and + * the process' stdout is written to OUTPUT. If OUTPUT is NULL the + * output is discarded. If INEXTRA is given, an additional input + * stream will be passed to the child; to tell the child about this + * ARGV is scanned and the first occurrence of an argument + * "-&@INEXTRA@" is replaced by the concatenation of "-&" and the + * child's file descriptor of the pipe created for the INEXTRA stream. + * + * On error a diagnostic is printed and an error code returned. */ +gpg_error_t +gnupg_exec_tool_stream (const char *pgmname, const char *argv[], + estream_t input, estream_t inextra, + estream_t output, + exec_tool_status_cb_t status_cb, + void *status_cb_value) +{ + gpg_error_t err; + pid_t pid = (pid_t) -1; + estream_t infp = NULL; + estream_t extrafp = NULL; + estream_t outfp = NULL, errfp = NULL; + es_poll_t fds[4]; + int exceptclose[2]; + int extrapipe[2] = {-1, -1}; + char extrafdbuf[20]; + const char *argsave = NULL; + int argsaveidx; + int count; + read_and_log_buffer_t fderrstate; + struct copy_buffer *cpbuf_in = NULL, *cpbuf_out = NULL, *cpbuf_extra = NULL; + + memset (fds, 0, sizeof fds); + memset (&fderrstate, 0, sizeof fderrstate); + + cpbuf_in = xtrymalloc (sizeof *cpbuf_in); + if (cpbuf_in == NULL) + { + err = my_error_from_syserror (); + goto leave; + } + copy_buffer_init (cpbuf_in); + + cpbuf_out = xtrymalloc (sizeof *cpbuf_out); + if (cpbuf_out == NULL) + { + err = my_error_from_syserror (); + goto leave; + } + copy_buffer_init (cpbuf_out); + + cpbuf_extra = xtrymalloc (sizeof *cpbuf_extra); + if (cpbuf_extra == NULL) + { + err = my_error_from_syserror (); + goto leave; + } + copy_buffer_init (cpbuf_extra); + + fderrstate.pgmname = pgmname; + fderrstate.status_cb = status_cb; + fderrstate.status_cb_value = status_cb_value; + fderrstate.buffer_size = 256; + fderrstate.buffer = xtrymalloc (fderrstate.buffer_size); + if (!fderrstate.buffer) + { + err = my_error_from_syserror (); + goto leave; + } + + if (inextra) + { + err = gnupg_create_outbound_pipe (extrapipe, &extrafp, 1); + if (err) + { + log_error ("error running outbound pipe for extra fp: %s\n", + gpg_strerror (err)); + goto leave; + } + exceptclose[0] = extrapipe[0]; /* Do not close in child. */ + exceptclose[1] = -1; + /* Now find the argument marker and replace by the pipe's fd. + Yeah, that is an ugly non-thread safe hack but it safes us to + create a copy of the array. */ +#ifdef HAVE_W32_SYSTEM + snprintf (extrafdbuf, sizeof extrafdbuf, "-&%lu", + (unsigned long)(void*)_get_osfhandle (extrapipe[0])); +#else + snprintf (extrafdbuf, sizeof extrafdbuf, "-&%d", extrapipe[0]); +#endif + for (argsaveidx=0; argv[argsaveidx]; argsaveidx++) + if (!strcmp (argv[argsaveidx], "-&@INEXTRA@")) + { + argsave = argv[argsaveidx]; + argv[argsaveidx] = extrafdbuf; + break; + } + } + else + exceptclose[0] = -1; + + err = gnupg_spawn_process (pgmname, argv, + exceptclose, NULL, GNUPG_SPAWN_NONBLOCK, + input? &infp : NULL, + &outfp, &errfp, &pid); + if (extrapipe[0] != -1) + close (extrapipe[0]); + if (argsave) + argv[argsaveidx] = argsave; + if (err) + { + log_error ("error running '%s': %s\n", pgmname, gpg_strerror (err)); + goto leave; + } + + fds[0].stream = infp; + fds[0].want_write = 1; + if (!input) + fds[0].ignore = 1; + fds[1].stream = outfp; + fds[1].want_read = 1; + fds[2].stream = errfp; + fds[2].want_read = 1; + fds[3].stream = extrafp; + fds[3].want_write = 1; + if (!inextra) + fds[3].ignore = 1; + + /* Now read as long as we have something to poll. We continue + reading even after EOF or error on stdout so that we get the + other error messages or remaining output. */ + while (! (fds[1].ignore && fds[2].ignore)) + { + count = es_poll (fds, DIM(fds), -1); + if (count == -1) + { + err = my_error_from_syserror (); + log_error ("error polling '%s': %s\n", pgmname, gpg_strerror (err)); + goto leave; + } + if (!count) + { + log_debug ("unexpected timeout while polling '%s'\n", pgmname); + break; + } + + if (fds[0].got_write) + { + err = copy_buffer_do_copy (cpbuf_in, input, fds[0].stream); + if (err) + { + log_error ("error feeding data to '%s': %s\n", + pgmname, gpg_strerror (err)); + goto leave; + } + + if (es_feof (input)) + { + err = copy_buffer_flush (cpbuf_in, fds[0].stream); + if (gpg_err_code (err) == GPG_ERR_EAGAIN) + continue; /* Retry next time. */ + if (err) + { + log_error ("error feeding data to '%s': %s\n", + pgmname, gpg_strerror (err)); + goto leave; + } + + fds[0].ignore = 1; /* ready. */ + es_fclose (infp); infp = NULL; + } + } + + if (fds[3].got_write) + { + log_assert (inextra); + err = copy_buffer_do_copy (cpbuf_extra, inextra, fds[3].stream); + if (err) + { + log_error ("error feeding data to '%s': %s\n", + pgmname, gpg_strerror (err)); + goto leave; + } + + if (es_feof (inextra)) + { + err = copy_buffer_flush (cpbuf_extra, fds[3].stream); + if (gpg_err_code (err) == GPG_ERR_EAGAIN) + continue; /* Retry next time. */ + if (err) + { + log_error ("error feeding data to '%s': %s\n", + pgmname, gpg_strerror (err)); + goto leave; + } + + fds[3].ignore = 1; /* ready. */ + es_fclose (extrafp); extrafp = NULL; + } + } + + if (fds[1].got_read) + { + err = copy_buffer_do_copy (cpbuf_out, fds[1].stream, output); + if (err) + { + log_error ("error reading data from '%s': %s\n", + pgmname, gpg_strerror (err)); + goto leave; + } + + if (es_feof (fds[1].stream)) + { + err = copy_buffer_flush (cpbuf_out, output); + if (err) + { + log_error ("error reading data from '%s': %s\n", + pgmname, gpg_strerror (err)); + goto leave; + } + + fds[1].ignore = 1; /* ready. */ + } + } + + if (fds[2].got_read) + read_and_log_stderr (&fderrstate, fds + 2); + } + + read_and_log_stderr (&fderrstate, NULL); /* Flush. */ + es_fclose (infp); infp = NULL; + es_fclose (extrafp); extrafp = NULL; + es_fclose (outfp); outfp = NULL; + es_fclose (errfp); errfp = NULL; + + err = gnupg_wait_process (pgmname, pid, 1, NULL); + pid = (pid_t)(-1); + + leave: + if (err && pid != (pid_t) -1) + gnupg_kill_process (pid); + + es_fclose (infp); + es_fclose (extrafp); + es_fclose (outfp); + es_fclose (errfp); + if (pid != (pid_t)(-1)) + gnupg_wait_process (pgmname, pid, 1, NULL); + gnupg_release_process (pid); + + copy_buffer_shred (cpbuf_in); + xfree (cpbuf_in); + copy_buffer_shred (cpbuf_out); + xfree (cpbuf_out); + copy_buffer_shred (cpbuf_extra); + xfree (cpbuf_extra); + xfree (fderrstate.buffer); + return err; +} + + +/* A dummy free function to pass to 'es_mopen'. */ +static void +nop_free (void *ptr) +{ + (void) ptr; +} + +/* Run the program PGMNAME with the command line arguments given in + the NULL terminates array ARGV. If INPUT_STRING is not NULL it + will be fed to stdin of the process. stderr is logged using + log_info and the process' stdout is returned in a newly malloced + buffer RESULT with the length stored at RESULTLEN if not given as + NULL. A hidden Nul is appended to the output. On error NULL is + stored at RESULT, a diagnostic is printed, and an error code + returned. */ +gpg_error_t +gnupg_exec_tool (const char *pgmname, const char *argv[], + const char *input_string, + char **result, size_t *resultlen) +{ + gpg_error_t err; + estream_t input = NULL; + estream_t output; + size_t len; + size_t nread; + + *result = NULL; + if (resultlen) + *resultlen = 0; + + if (input_string) + { + len = strlen (input_string); + input = es_mopen ((char *) input_string, len, len, + 0 /* don't grow */, NULL, nop_free, "rb"); + if (! input) + return my_error_from_syserror (); + } + + output = es_fopenmem (0, "wb"); + if (! output) + { + err = my_error_from_syserror (); + goto leave; + } + + err = gnupg_exec_tool_stream (pgmname, argv, input, NULL, output, NULL, NULL); + if (err) + goto leave; + + len = es_ftello (output); + err = es_fseek (output, 0, SEEK_SET); + if (err) + goto leave; + + *result = xtrymalloc (len + 1); + if (!*result) + { + err = my_error_from_syserror (); + goto leave; + } + + if (len) + { + if (es_read (output, *result, len, &nread)) + { + err = my_error_from_syserror (); + goto leave; + } + if (nread != len) + log_fatal ("%s: short read from memstream\n", __func__); + } + (*result)[len] = 0; + + if (resultlen) + *resultlen = len; + + leave: + es_fclose (input); + es_fclose (output); + if (err) + { + xfree (*result); + *result = NULL; + } + return err; +} |