]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-program-client: Add program-client from pigeonhole
authorAki Tuomi <aki.tuomi@dovecot.fi>
Mon, 12 Sep 2016 10:02:23 +0000 (13:02 +0300)
committerAki Tuomi <aki.tuomi@dovecot.fi>
Sun, 9 Oct 2016 19:48:19 +0000 (22:48 +0300)
configure.ac
dovecot-config.in.in
src/Makefile.am
src/lib-program-client/Makefile.am [new file with mode: 0644]
src/lib-program-client/program-client-local.c [new file with mode: 0644]
src/lib-program-client/program-client-private.h [new file with mode: 0644]
src/lib-program-client/program-client-remote.c [new file with mode: 0644]
src/lib-program-client/program-client.c [new file with mode: 0644]
src/lib-program-client/program-client.h [new file with mode: 0644]

index 5add63a192b4fd29013a7ea8bb56294952ba5f66..e9ea339d181d6c39451b8c772956f9b571b837f3 100644 (file)
@@ -2547,7 +2547,7 @@ dnl ** Shared libraries usage
 dnl **
 
 
-LIBDOVECOT_LA_LIBS='$(top_builddir)/src/lib-dict-extra/libdict_extra.la $(top_builddir)/src/lib-master/libmaster.la $(top_builddir)/src/lib-settings/libsettings.la $(top_builddir)/src/lib-stats/libstats.la $(top_builddir)/src/lib-http/libhttp.la $(top_builddir)/src/lib-fs/libfs.la $(top_builddir)/src/lib-dict/libdict.la $(top_builddir)/src/lib-dns/libdns.la $(top_builddir)/src/lib-imap/libimap.la $(top_builddir)/src/lib-mail/libmail.la $(top_builddir)/src/lib-sasl/libsasl.la $(top_builddir)/src/lib-auth/libauth.la $(top_builddir)/src/lib-charset/libcharset.la $(top_builddir)/src/lib-ssl-iostream/libssl_iostream.la $(top_builddir)/src/lib-dcrypt/libdcrypt.la $(top_builddir)/src/lib-test/libtest.la $(top_builddir)/src/lib/liblib.la'
+LIBDOVECOT_LA_LIBS='$(top_builddir)/src/lib-dict-extra/libdict_extra.la $(top_builddir)/src/lib-master/libmaster.la $(top_builddir)/src/lib-settings/libsettings.la $(top_builddir)/src/lib-stats/libstats.la $(top_builddir)/src/lib-http/libhttp.la $(top_builddir)/src/lib-fs/libfs.la $(top_builddir)/src/lib-dict/libdict.la $(top_builddir)/src/lib-dns/libdns.la $(top_builddir)/src/lib-imap/libimap.la $(top_builddir)/src/lib-mail/libmail.la $(top_builddir)/src/lib-sasl/libsasl.la $(top_builddir)/src/lib-auth/libauth.la $(top_builddir)/src/lib-charset/libcharset.la $(top_builddir)/src/lib-ssl-iostream/libssl_iostream.la $(top_builddir)/src/lib-dcrypt/libdcrypt.la $(top_builddir)/src/lib-program-client/libprogram_client.la $(top_builddir)/src/lib-test/libtest.la $(top_builddir)/src/lib/liblib.la'
 
 if test "$want_shared_libs" = "yes"; then
   LIBDOVECOT_DEPS='$(top_builddir)/src/lib-dovecot/libdovecot.la'
@@ -2909,6 +2909,7 @@ src/lib-ldap/Makefile
 src/lib-mail/Makefile
 src/lib-master/Makefile
 src/lib-ntlm/Makefile
+src/lib-program-client/Makefile
 src/lib-otp/Makefile
 src/lib-dovecot/Makefile
 src/lib-sasl/Makefile
index f776c387cf2f5d6adfca424f6d374800e5f445d0..539f281913048d6365ca0dfd373580215576d1ed 100644 (file)
@@ -22,7 +22,7 @@ LIBDOVECOT_STORAGE_DEPS="@LIBDOVECOT_STORAGE_DEPS@"
 LIBDOVECOT_DSYNC_DEPS="@LIBDOVECOT_DSYNC@"
 LIBDOVECOT_LIBFTS_DEPS="@LIBDOVECOT_LIBFTS@"
 
-LIBDOVECOT_INCLUDE="-I$(incdir) -I$(incdir)/src/lib -I$(incdir)/src/lib-dict -I$(incdir)/src/lib-dns -I$(incdir)/src/lib-http -I$(incdir)/src/lib-mail -I$(incdir)/src/lib-imap -I$(incdir)/src/lib-fs -I$(incdir)/src/lib-charset -I$(incdir)/src/lib-auth -I$(incdir)/src/lib-master -I$(incdir)/src/lib-ssl-iostream -I$(incdir)/src/lib-compression -I$(incdir)/src/lib-settings -I$(incdir)/src/lib-test -I$(incdir)/src/lib-sasl -I$(incdir)/src/lib-stats -I$(incdir)/src/lib-dcrypt"
+LIBDOVECOT_INCLUDE="-I$(incdir) -I$(incdir)/src/lib -I$(incdir)/src/lib-dict -I$(incdir)/src/lib-dns -I$(incdir)/src/lib-http -I$(incdir)/src/lib-mail -I$(incdir)/src/lib-imap -I$(incdir)/src/lib-fs -I$(incdir)/src/lib-charset -I$(incdir)/src/lib-auth -I$(incdir)/src/lib-master -I$(incdir)/src/lib-ssl-iostream -I$(incdir)/src/lib-compression -I$(incdir)/src/lib-settings -I$(incdir)/src/lib-test -I$(incdir)/src/lib-sasl -I$(incdir)/src/lib-stats -I$(incdir)/src/lib-dcrypt -I$(incdir)/src/lib-program-client"
 LIBDOVECOT_LDA_INCLUDE="-I$(incdir)/src/lib-lda -I$(incdir)/src/lda"
 LIBDOVECOT_AUTH_INCLUDE="-I$(incdir)/src/auth"
 LIBDOVECOT_DOVEADM_INCLUDE="-I$(incdir)/src/doveadm"
index dd76173d8a46afafb179b3e63151efc675c8d340..35a8d60b4444a4666cb6a3d2c8de0e2e19b2a011 100644 (file)
@@ -19,7 +19,8 @@ LIBDOVECOT_SUBDIRS = \
        lib-fs \
        lib-mail \
        lib-imap \
-       lib-imap-storage
+       lib-imap-storage \
+       lib-program-client
 
 SUBDIRS = \
        $(LIBDOVECOT_SUBDIRS) \
diff --git a/src/lib-program-client/Makefile.am b/src/lib-program-client/Makefile.am
new file mode 100644 (file)
index 0000000..a31e101
--- /dev/null
@@ -0,0 +1,18 @@
+noinst_LTLIBRARIES = libprogram_client.la
+
+AM_CPPFLAGS = \
+       -I$(top_srcdir)/src/lib
+
+libprogram_client_la_SOURCES = \
+       program-client.c \
+       program-client-local.c \
+       program-client-remote.c
+
+headers = \
+       program-client.h
+
+noinst_HEADERS = \
+       program-client-private.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-program-client/program-client-local.c b/src/lib-program-client/program-client-local.c
new file mode 100644 (file)
index 0000000..d6c7992
--- /dev/null
@@ -0,0 +1,445 @@
+/* Copyright (c) 2002-2016 Dovecot authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "env-util.h"
+#include "execv-const.h"
+#include "array.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+
+#include "program-client-private.h"
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <grp.h>
+
+struct program_client_local {
+       struct program_client client;
+
+       pid_t pid;
+};
+
+static
+void exec_child(const char *bin_path, const char *const *args, const char *const *envs,
+               int in_fd, int out_fd, int *extra_fds, bool drop_stderr)
+{
+       ARRAY_TYPE(const_string) exec_args;
+
+       /* Setup stdin/stdout */
+
+       if (in_fd < 0) {
+               in_fd = open("/dev/null", O_RDONLY);
+
+               if (in_fd == -1)
+                       i_fatal("open(/dev/null) failed: %m");
+       }
+       if (out_fd < 0) {
+               out_fd = open("/dev/null", O_WRONLY);
+
+               if (out_fd == -1)
+                       i_fatal("open(/dev/null) failed: %m");
+       }
+
+       if (in_fd != STDIN_FILENO && dup2(in_fd, STDIN_FILENO) < 0)
+               i_fatal("dup2(stdin) failed: %m");
+       if (out_fd != STDOUT_FILENO && dup2(out_fd, STDOUT_FILENO) < 0)
+               i_fatal("dup2(stdout) failed: %m");
+
+       if (in_fd != STDIN_FILENO && close(in_fd) < 0)
+               i_error("close(in_fd) failed: %m");
+       if (out_fd != STDOUT_FILENO && (out_fd != in_fd) && close(out_fd) < 0)
+               i_error("close(out_fd) failed: %m");
+
+       /* Drop stderr if requested */
+       if (drop_stderr) {
+               int err_fd = open("/dev/null", O_WRONLY);
+               if (err_fd == -1)
+                       i_fatal("open(/dev/null) failed: %m");
+               if (err_fd != STDERR_FILENO) {
+                       if (dup2(err_fd, STDERR_FILENO) < 0)
+                               i_fatal("dup2(stderr) failed: %m");
+                       if (close(err_fd) < 0)
+                               i_error("close(err_fd) failed: %m");
+               }
+       }
+
+       /* Setup extra fds */
+       if (extra_fds != NULL) {
+               int *efd;
+               for(efd = extra_fds; *efd != -1; efd += 2) {
+                       i_assert(efd[1] != STDIN_FILENO);
+                       i_assert(efd[1] != STDOUT_FILENO);
+                       i_assert(efd[1] != STDERR_FILENO);
+                       if (efd[0] != efd[1]) {
+                               if (dup2(efd[0], efd[1]) < 0)
+                                       i_fatal("dup2(extra_fd=%d) failed: %m",
+                                               efd[1]);
+                       }
+               }
+               for(efd = extra_fds; *efd != -1; efd += 2) {
+                       if (efd[0] != efd[1] && efd[0] != STDIN_FILENO &&
+                           efd[0] != STDOUT_FILENO &&
+                           efd[0] != STDERR_FILENO) {
+                               if (close(efd[0]) < 0)
+                                       i_error("close(extra_fd=%d) failed: %m",
+                                               efd[1]);
+                       }
+               }
+       }
+
+       /* Compose argv */
+
+       t_array_init(&exec_args, 16);
+       array_append(&exec_args, &bin_path, 1);
+       if (args != NULL) {
+               for(; *args != NULL; args++)
+                       array_append(&exec_args, args, 1);
+       }
+       (void) array_append_space(&exec_args);
+
+       /* Setup environment */
+
+       env_clean();
+       if (envs != NULL) {
+               for(; *envs != NULL; envs++)
+                       env_put(*envs);
+       }
+
+       /* Execute */
+
+       args = array_idx(&exec_args, 0);
+       execvp_const(args[0], args);
+}
+
+static
+int program_client_local_connect(struct program_client *pclient)
+{
+       struct program_client_local *slclient = (struct program_client_local *) pclient;
+       int fd_in[2] = { -1, -1 }, fd_out[2] = {-1, -1};
+       struct program_client_extra_fd *efds = NULL;
+       int *parent_extra_fds = NULL, *child_extra_fds = NULL;
+       unsigned int xfd_count = 0, i;
+
+       /* create normal I/O fds */
+       if (pclient->input != NULL) {
+               if (pipe(fd_in) < 0) {
+                       i_error("pipe(in) failed: %m");
+                       return -1;
+               }
+       }
+       if (pclient->output != NULL || pclient->output_seekable) {
+               if (pipe(fd_out) < 0) {
+                       i_error("pipe(out) failed: %m");
+                       return -1;
+               }
+       }
+
+       /* create pipes for additional output through side-channel fds */
+       if (array_is_created(&pclient->extra_fds)) {
+               int extra_fd[2];
+
+               efds = array_get_modifiable(&pclient->extra_fds, &xfd_count);
+               if (xfd_count > 0) {
+                       parent_extra_fds = t_malloc0(sizeof(int) * xfd_count);
+                       child_extra_fds =
+                               t_malloc0(sizeof(int) * xfd_count * 2 + 1);
+                       for(i = 0; i < xfd_count; i++) {
+                               if (pipe(extra_fd) < 0) {
+                                       i_error("pipe(extra=%d) failed: %m",
+                                               extra_fd[1]);
+                                       return -1;
+                               }
+                               parent_extra_fds[i] = extra_fd[0];
+                               child_extra_fds[i * 2 + 0] = extra_fd[1];
+                               child_extra_fds[i * 2 + 1] = efds[i].child_fd;
+                       }
+                       child_extra_fds[xfd_count * 2] = -1;
+               }
+       }
+
+       /* fork child */
+       if ((slclient->pid = fork()) == (pid_t) - 1) {
+               i_error("fork() failed: %m");
+
+               /* clean up */
+               if (fd_in[0] >= 0 && close(fd_in[0]) < 0) {
+                       i_error("close(pipe:in:rd) failed: %m");
+               }
+               if (fd_in[1] >= 0 && close(fd_in[1]) < 0) {
+                       i_error("close(pipe:in:wr) failed: %m");
+               }
+               if (fd_out[0] >= 0 && close(fd_out[0]) < 0) {
+                       i_error("close(pipe:out:rd) failed: %m");
+               }
+               if (fd_out[1] >= 0 && close(fd_out[1]) < 0) {
+                       i_error("close(pipe:out:wr) failed: %m");
+               }
+               for(i = 0; i < xfd_count; i++) {
+                       if (close(child_extra_fds[i * 2]) < 0) {
+                               i_error("close(pipe:extra=%d:wr) failed: %m",
+                                       child_extra_fds[i * 2 + 1]);
+                       }
+                       if (close(parent_extra_fds[i]) < 0) {
+                               i_error("close(pipe:extra=%d:rd) failed: %m",
+                                       child_extra_fds[i * 2 + 1]);
+                       }
+               }
+               return -1;
+       }
+
+       if (slclient->pid == 0) {
+               unsigned int count;
+               const char *const *envs = NULL;
+
+               /* child */
+               if (fd_in[1] >= 0 && close(fd_in[1]) < 0)
+                       i_error("close(pipe:in:wr) failed: %m");
+               if (fd_out[0] >= 0 && close(fd_out[0]) < 0)
+                       i_error("close(pipe:out:rd) failed: %m");
+               for(i = 0; i < xfd_count; i++) {
+                       if (close(parent_extra_fds[i]) < 0) {
+                               i_error("close(pipe:extra=%d:rd) failed: %m",
+                                       child_extra_fds[i * 2 + 1]);
+                       }
+               }
+
+               /* drop privileges if we have any */
+               if (getuid() == 0) {
+                       uid_t uid;
+                       gid_t gid;
+
+                       /* switch back to root */
+                       if (seteuid(0) < 0)
+                               i_fatal("seteuid(0) failed: %m");
+
+                       /* drop gids first */
+                       gid = getgid();
+                       if (gid == 0 || gid != pclient->set.gid) {
+                               if (pclient->set.gid != 0)
+                                       gid = pclient->set.gid;
+                               else
+                                       gid = getegid();
+                       }
+                       if (setgroups(1, &gid) < 0)
+                               i_fatal("setgroups(%d) failed: %m", gid);
+                       if (gid != 0 && setgid(gid) < 0)
+                               i_fatal("setgid(%d) failed: %m", gid);
+
+                       /* drop uid */
+                       if (pclient->set.uid != 0)
+                               uid = pclient->set.uid;
+                       else
+                               uid = geteuid();
+                       if (uid != 0 && setuid(uid) < 0)
+                               i_fatal("setuid(%d) failed: %m", uid);
+               }
+
+               i_assert(pclient->set.uid == 0 || getuid() != 0);
+               i_assert(pclient->set.gid == 0 || getgid() != 0);
+
+               if (array_is_created(&pclient->envs))
+                       envs = array_get(&pclient->envs, &count);
+
+               exec_child(pclient->path, pclient->args, envs,
+                          fd_in[0], fd_out[1], child_extra_fds,
+                          pclient->set.drop_stderr);
+               i_unreached();
+       }
+
+       /* parent */
+       if (fd_in[0] >= 0 && close(fd_in[0]) < 0)
+               i_error("close(pipe:in:rd) failed: %m");
+       if (fd_out[1] >= 0 && close(fd_out[1]) < 0)
+               i_error("close(pipe:out:wr) failed: %m");
+       if (fd_in[1] >= 0) {
+               net_set_nonblock(fd_in[1], TRUE);
+               pclient->fd_out = fd_in[1];
+       }
+       if (fd_out[0] >= 0) {
+               net_set_nonblock(fd_out[0], TRUE);
+               pclient->fd_in = fd_out[0];
+       }
+       for(i = 0; i < xfd_count; i++) {
+               if (close(child_extra_fds[i * 2]) < 0) {
+                       i_error("close(pipe:extra=%d:wr) failed: %m",
+                               child_extra_fds[i * 2 + 1]);
+               }
+               net_set_nonblock(parent_extra_fds[i], TRUE);
+               efds[i].parent_fd = parent_extra_fds[i];
+       }
+
+       program_client_init_streams(pclient);
+       return program_client_connected(pclient);
+}
+
+static
+int program_client_local_close_output(struct program_client *pclient)
+{
+       int fd_out = pclient->fd_out;
+
+       pclient->fd_out = -1;
+
+       /* Shutdown output; program stdin will get EOF */
+       if (fd_out >= 0 && close(fd_out) < 0) {
+               i_error("close(%s) failed: %m", pclient->path);
+               return -1;
+       }
+       return 1;
+}
+
+static
+int program_client_local_disconnect(struct program_client *pclient, bool force)
+{
+       struct program_client_local *slclient = (struct program_client_local *) pclient;
+       pid_t pid = slclient->pid, ret;
+       time_t runtime, timeout = 0;
+       int status;
+
+       if (pid < 0) {
+               /* program never started */
+               pclient->exit_code = 0;
+               return 0;
+       }
+
+       slclient->pid = -1;
+
+       /* Calculate timeout */
+       runtime = ioloop_time - pclient->start_time;
+       if (!force && pclient->set.input_idle_timeout_secs > 0 &&
+           runtime < (time_t) pclient->set.input_idle_timeout_secs)
+               timeout = pclient->set.input_idle_timeout_secs - runtime;
+
+       if (pclient->debug) {
+               i_debug("waiting for program `%s' to finish after %llu seconds",
+                       pclient->path, (unsigned long long int) runtime);
+       }
+
+       /* Wait for child to exit */
+       force = force ||
+               (timeout == 0 && pclient->set.input_idle_timeout_secs > 0);
+       if (!force) {
+               alarm(timeout);
+               ret = waitpid(pid, &status, 0);
+               alarm(0);
+       }
+       if (force || ret < 0) {
+               if (!force && errno != EINTR) {
+                       i_error("waitpid(%s) failed: %m", pclient->path);
+                       (void) kill(pid, SIGKILL);
+                       return -1;
+               }
+
+               /* Timed out */
+               force = TRUE;
+               if (pclient->error == PROGRAM_CLIENT_ERROR_NONE)
+                       pclient->error = PROGRAM_CLIENT_ERROR_RUN_TIMEOUT;
+               if (pclient->debug) {
+                       i_debug("program `%s' execution timed out after %llu seconds: "
+                               "sending TERM signal", pclient->path,
+                               (unsigned long long int)pclient->set.input_idle_timeout_secs);
+               }
+
+               /* Kill child gently first */
+               if (kill(pid, SIGTERM) < 0) {
+                       i_error("failed to send SIGTERM signal to program `%s'",
+                               pclient->path);
+                       (void) kill(pid, SIGKILL);
+                       return -1;
+               }
+
+               /* Wait for it to die (give it some more time) */
+               alarm(5);
+               ret = waitpid(pid, &status, 0);
+               alarm(0);
+               if (ret < 0) {
+                       if (errno != EINTR) {
+                               i_error("waitpid(%s) failed: %m",
+                                       pclient->path);
+                               (void) kill(pid, SIGKILL);
+                               return -1;
+                       }
+
+                       /* Timed out again */
+                       if (pclient->debug) {
+                               i_debug("program `%s' execution timed out: sending KILL signal", pclient->path);
+                       }
+
+                       /* Kill it brutally now */
+                       if (kill(pid, SIGKILL) < 0) {
+                               i_error("failed to send SIGKILL signal to program `%s'", pclient->path);
+                               return -1;
+                       }
+
+                       /* Now it will die immediately */
+                       if (waitpid(pid, &status, 0) < 0) {
+                               i_error("waitpid(%s) failed: %m",
+                                       pclient->path);
+                               return -1;
+                       }
+               }
+       }
+
+       /* Evaluate child exit status */
+       pclient->exit_code = -1;
+       if (WIFEXITED(status)) {
+               /* Exited */
+               int exit_code = WEXITSTATUS(status);
+
+               if (exit_code != 0) {
+                       i_info("program `%s' terminated with non-zero exit code %d", pclient->path, exit_code);
+                       pclient->exit_code = 0;
+                       return 0;
+               }
+
+               pclient->exit_code = 1;
+               return 1;
+
+       } else if (WIFSIGNALED(status)) {
+               /* Killed with a signal */
+
+               if (force) {
+                       i_error("program `%s' was forcibly terminated with signal %d", pclient->path, WTERMSIG(status));
+               } else {
+                       i_error("program `%s' terminated abnormally, signal %d",
+                               pclient->path, WTERMSIG(status));
+               }
+               return -1;
+
+       } else if (WIFSTOPPED(status)) {
+               /* Stopped */
+               i_error("program `%s' stopped, signal %d",
+                       pclient->path, WSTOPSIG(status));
+               return -1;
+       }
+
+       /* Something else */
+       i_error("program `%s' terminated abnormally, return status %d",
+               pclient->path, status);
+       return -1;
+}
+
+struct program_client *
+program_client_local_create(const char *bin_path,
+                           const char *const *args,
+                           const struct program_client_settings *set)
+{
+       struct program_client_local *pclient;
+       pool_t pool;
+
+       pool = pool_alloconly_create("program client local", 1024);
+       pclient = p_new(pool, struct program_client_local, 1);
+       program_client_init(&pclient->client, pool, bin_path, args, set);
+       pclient->client.connect = program_client_local_connect;
+       pclient->client.close_output = program_client_local_close_output;
+       pclient->client.disconnect = program_client_local_disconnect;
+       pclient->pid = -1;
+
+       return &pclient->client;
+}
diff --git a/src/lib-program-client/program-client-private.h b/src/lib-program-client/program-client-private.h
new file mode 100644 (file)
index 0000000..90b49ef
--- /dev/null
@@ -0,0 +1,69 @@
+/* Copyright (c) 2002-2016 Dovecot authors, see the included COPYING file
+ */
+
+#ifndef PROGRAM_CLIENT_PRIVATE_H
+#define PROGRAM_CLIENT_PRIVATE_H
+
+#include "program-client.h"
+
+enum program_client_error {
+       PROGRAM_CLIENT_ERROR_NONE,
+       PROGRAM_CLIENT_ERROR_CONNECT_TIMEOUT,
+       PROGRAM_CLIENT_ERROR_RUN_TIMEOUT,
+       PROGRAM_CLIENT_ERROR_IO,
+       PROGRAM_CLIENT_ERROR_OTHER
+};
+
+struct program_client_extra_fd {
+       struct program_client *pclient;
+
+       int child_fd, parent_fd;
+       struct istream *input;
+       struct io *io;
+
+       program_client_fd_callback_t *callback;
+       void *context;
+};
+
+struct program_client {
+       pool_t pool;
+       struct program_client_settings set;
+
+       char *path;
+       const char **args;
+         ARRAY_TYPE(const_string) envs;
+
+       int fd_in, fd_out;
+       struct io *io;
+       struct ioloop *ioloop;
+       struct timeout *to;
+       time_t start_time;
+
+       struct istream *input, *program_input, *seekable_output;
+       struct ostream *output, *program_output;
+       char *temp_prefix;
+
+         ARRAY(struct program_client_extra_fd) extra_fds;
+
+       enum program_client_error error;
+       int exit_code;
+
+       int (*connect) (struct program_client * pclient);
+       int (*close_output) (struct program_client * pclient);
+       int (*disconnect) (struct program_client * pclient, bool force);
+
+       bool debug:1;
+       bool disconnected:1;
+       bool output_seekable:1;
+};
+
+void program_client_init(struct program_client *pclient, pool_t pool, const char *path,
+                        const char *const *args, const struct program_client_settings *set);
+
+void program_client_init_streams(struct program_client *pclient);
+
+int program_client_connected(struct program_client *pclient);
+
+void program_client_fail(struct program_client *pclient, enum program_client_error error);
+
+#endif
diff --git a/src/lib-program-client/program-client-remote.c b/src/lib-program-client/program-client-remote.c
new file mode 100644 (file)
index 0000000..dba5cfd
--- /dev/null
@@ -0,0 +1,334 @@
+/* Copyright (c) 2002-2016 Dovecot authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "net.h"
+#include "write-full.h"
+#include "eacces-error.h"
+#include "istream-private.h"
+#include "ostream.h"
+
+#include "program-client-private.h"
+
+#include <unistd.h>
+#include <sys/wait.h>
+#include <sysexits.h>
+
+/*
+ * Script client input stream
+ */
+
+struct program_client_istream {
+       struct istream_private istream;
+
+       struct stat statbuf;
+
+       struct program_client *client;
+};
+
+static
+void program_client_istream_destroy(struct iostream_private *stream)
+{
+       struct program_client_istream *scstream =
+               (struct program_client_istream *) stream;
+
+       i_stream_unref(&scstream->istream.parent);
+}
+
+static ssize_t
+program_client_istream_read(struct istream_private *stream)
+{
+       struct program_client_istream *scstream =
+               (struct program_client_istream *) stream;
+       size_t pos, reserved;
+       ssize_t ret = 0;
+
+       i_stream_skip(stream->parent, stream->skip);
+       stream->skip = 0;
+
+       stream->buffer = i_stream_get_data(stream->parent, &pos);
+
+       reserved = 0;
+       if (stream->buffer != NULL && pos >= 1) {
+               /* retain/hide potential return code at end of buffer */
+               reserved = (stream->buffer[pos - 1] == '\n' && pos > 1 ? 2 : 1);
+               pos -= reserved;
+       }
+
+       if (stream->parent->eof) {
+               if (pos == 0)
+                       i_stream_skip(stream->parent, reserved);
+               stream->istream.eof = TRUE;
+               ret = -1;
+       } else
+               do {
+                       if ((ret = i_stream_read(stream->parent)) == -2) {
+                               return -2;      /* input buffer full */
+                       }
+
+                       if (ret == 0 || (ret < 0 && !stream->parent->eof))
+                               break;
+
+                       stream->istream.stream_errno =
+                               stream->parent->stream_errno;
+                       stream->buffer =
+                               i_stream_get_data(stream->parent, &pos);
+
+                       if (stream->parent->eof) {
+                               /* Check return code at EOF */
+                               if (stream->buffer != NULL && pos >= 2 &&
+                                   stream->buffer[pos - 1] == '\n') {
+                                       switch (stream->buffer[pos - 2]) {
+                                       case '+':
+                                               scstream->client->exit_code = 1;
+                                               break;
+                                       case '-':
+                                               scstream->client->exit_code = 0;
+                                               break;
+                                       default:
+                                               scstream->client->exit_code =
+                                                       -1;
+                                       }
+                               } else {
+                                       scstream->client->exit_code = -1;
+                               }
+                       }
+
+                       if (stream->buffer != NULL && pos >= 1) {
+                               /* retain/hide potential return code at end of buffer */
+                               size_t old_reserved = reserved;
+                               ssize_t reserve_mod;
+
+                               reserved = (stream->buffer[pos - 1] == '\n' &&
+                                           pos > 1 ? 2 : 1);
+                               reserve_mod = reserved - old_reserved;
+                               pos -= reserved;
+
+                               if (ret >= reserve_mod) {
+                                       ret -= reserve_mod;
+                               }
+                       }
+
+                       if (ret <= 0 && stream->parent->eof) {
+                               /* Parent EOF and not more data to return; EOF here as well */
+                               if (pos == 0)
+                                       i_stream_skip(stream->parent, reserved);
+                               stream->istream.eof = TRUE;
+                               ret = -1;
+                       }
+               } while (ret == 0);
+
+       stream->pos = pos;
+
+       i_assert(ret != -1 || stream->istream.eof ||
+                stream->istream.stream_errno != 0);
+       return ret;
+}
+
+static
+void ATTR_NORETURN program_client_istream_sync(struct istream_private *stream ATTR_UNUSED)
+{
+       i_panic("program_client_istream sync() not implemented");
+}
+
+static
+int program_client_istream_stat(struct istream_private *stream, bool exact)
+{
+       struct program_client_istream *scstream =
+               (struct program_client_istream *) stream;
+       const struct stat *st;
+       int ret;
+
+       /* Stat the original stream */
+       ret = i_stream_stat(stream->parent, exact, &st);
+       if (ret < 0 || st->st_size == -1 || !exact)
+               return ret;
+
+       scstream->statbuf = *st;
+       scstream->statbuf.st_size = -1;
+
+       return ret;
+}
+
+static
+struct istream *program_client_istream_create(struct program_client *program_client,
+                                             struct istream *input)
+{
+       struct program_client_istream *scstream;
+
+       scstream = i_new(struct program_client_istream, 1);
+       scstream->client = program_client;
+
+       scstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+       scstream->istream.iostream.destroy = program_client_istream_destroy;
+       scstream->istream.read = program_client_istream_read;
+       scstream->istream.sync = program_client_istream_sync;
+       scstream->istream.stat = program_client_istream_stat;
+
+       scstream->istream.istream.readable_fd = FALSE;
+       scstream->istream.istream.blocking = input->blocking;
+       scstream->istream.istream.seekable = FALSE;
+
+       i_stream_seek(input, 0);
+
+       return i_stream_create(&scstream->istream, input, -1);
+}
+
+/*
+ * Program client
+ */
+
+struct program_client_remote {
+       struct program_client client;
+
+       bool noreply:1;
+};
+
+static
+void program_client_remote_connected(struct program_client *pclient)
+{
+       struct program_client_remote *slclient =
+               (struct program_client_remote *) pclient;
+       const char **args = pclient->args;
+       string_t *str;
+
+       io_remove(&pclient->io);
+       program_client_init_streams(pclient);
+
+       if (!slclient->noreply) {
+               pclient->program_input =
+                       program_client_istream_create(pclient, pclient->program_input);
+       }
+
+       str = t_str_new(1024);
+       str_append(str, "VERSION\tscript\t3\t0\n");
+       if (slclient->noreply)
+               str_append(str, "noreply\n");
+       else
+               str_append(str, "-\n");
+       if (args != NULL) {
+               for(; *args != NULL; args++) {
+                       str_append(str, *args);
+                       str_append_c(str, '\n');
+               }
+       }
+       str_append_c(str, '\n');
+
+       if (o_stream_send(pclient->program_output,
+                         str_data(str), str_len(str)) < 0) {
+               i_error("write(%s) failed: %s",
+                       o_stream_get_name(pclient->program_output),
+                       o_stream_get_error(pclient->program_output));
+               program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+               return;
+       }
+
+       (void)program_client_connected(pclient);
+}
+
+static
+int program_client_remote_connect(struct program_client *pclient)
+{
+       struct program_client_remote *slclient =
+               (struct program_client_remote *) pclient;
+       int fd;
+
+       if ((fd = net_connect_unix_with_retries(pclient->path, 1000)) < 0) {
+               switch (errno) {
+               case EACCES:
+                       i_error("%s",
+                               eacces_error_get("net_connect_unix",
+                                                pclient->path));
+                       return -1;
+               default:
+                       i_error("net_connect_unix(%s) failed: %m",
+                               pclient->path);
+                       return -1;
+               }
+       }
+
+       net_set_nonblock(fd, TRUE);
+
+       pclient->fd_in = (slclient->noreply && pclient->output == NULL &&
+                         !pclient->output_seekable ? -1 : fd);
+       pclient->fd_out = fd;
+       pclient->io =
+               io_add(fd, IO_WRITE, program_client_remote_connected, pclient);
+       return 0;
+}
+
+static
+int program_client_remote_close_output(struct program_client *pclient)
+{
+       int fd_out = pclient->fd_out, fd_in = pclient->fd_in;
+
+       pclient->fd_out = -1;
+
+       /* Shutdown output; program stdin will get EOF */
+       if (fd_out >= 0) {
+               if (fd_in >= 0) {
+                       if (shutdown(fd_out, SHUT_WR) < 0 && errno != ENOTCONN) {
+                               i_error("shutdown(%s, SHUT_WR) failed: %m",
+                                       pclient->path);
+                               return -1;
+                       }
+               } else if (close(fd_out) < 0) {
+                       i_error("close(%s) failed: %m", pclient->path);
+                       return -1;
+               }
+       }
+
+       return 1;
+}
+
+static
+int program_client_remote_disconnect(struct program_client *pclient, bool force)
+{
+       struct program_client_remote *slclient =
+               (struct program_client_remote *)pclient;
+       int ret = 0;
+
+       if (pclient->error == PROGRAM_CLIENT_ERROR_NONE && !slclient->noreply &&
+           pclient->program_input != NULL && !force) {
+               const unsigned char *data;
+               size_t size;
+
+               /* Skip any remaining program output and parse the exit code */
+               while ((ret = i_stream_read_more
+                       (pclient->program_input, &data, &size)) > 0) {
+                       i_stream_skip(pclient->program_input, size);
+               }
+
+               /* Get exit code */
+               if (!pclient->program_input->eof)
+                       ret = -1;
+               else
+                       ret = pclient->exit_code;
+       } else {
+               ret = 1;
+       }
+
+       return ret;
+}
+
+struct program_client *
+program_client_remote_create(const char *socket_path, const char *const *args,
+                            const struct program_client_settings *set,
+                            bool noreply)
+{
+       struct program_client_remote *pclient;
+       pool_t pool;
+
+       pool = pool_alloconly_create("program client remote", 1024);
+       pclient = p_new(pool, struct program_client_remote, 1);
+       program_client_init(&pclient->client, pool, socket_path, args, set);
+       pclient->client.connect = program_client_remote_connect;
+       pclient->client.close_output = program_client_remote_close_output;
+       pclient->client.disconnect = program_client_remote_disconnect;
+       pclient->noreply = noreply;
+
+       return &pclient->client;
+}
diff --git a/src/lib-program-client/program-client.c b/src/lib-program-client/program-client.c
new file mode 100644 (file)
index 0000000..cfff787
--- /dev/null
@@ -0,0 +1,554 @@
+/* Copyright (c) 2002-2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "safe-mkstemp.h"
+#include "istream-private.h"
+#include "istream-seekable.h"
+#include "ostream.h"
+
+#include "program-client-private.h"
+
+#include <unistd.h>
+
+#define MAX_OUTPUT_BUFFER_SIZE 16384
+#define MAX_OUTPUT_MEMORY_BUFFER (1024*128)
+
+static int program_client_seekable_fd_callback
+(const char **path_r, void *context)
+{
+       struct program_client *pclient = (struct program_client *)context;
+       string_t *path;
+       int fd;
+
+       path = t_str_new(128);
+       str_append(path, pclient->temp_prefix);
+       fd = safe_mkstemp(path, 0600, (uid_t)-1, (gid_t)-1);
+       if (fd == -1) {
+               i_error("safe_mkstemp(%s) failed: %m", str_c(path));
+               return -1;
+       }
+
+       /* we just want the fd, unlink it */
+       if (i_unlink(str_c(path)) < 0) {
+               /* shouldn't happen.. */
+               i_close_fd(&fd);
+               return -1;
+       }
+
+       *path_r = str_c(path);
+       return fd;
+}
+
+static void program_client_timeout(struct program_client *pclient)
+{
+       i_error("program `%s' execution timed out (> %d secs)",
+               pclient->path, pclient->set.input_idle_timeout_secs);
+       program_client_fail(pclient, PROGRAM_CLIENT_ERROR_RUN_TIMEOUT);
+}
+
+static void program_client_connect_timeout(struct program_client *pclient)
+{
+       i_error("program `%s' socket connection timed out (> %d msecs)",
+               pclient->path, pclient->set.client_connect_timeout_msecs);
+       program_client_fail(pclient, PROGRAM_CLIENT_ERROR_CONNECT_TIMEOUT);
+}
+
+static int program_client_connect(struct program_client *pclient)
+{
+       int ret;
+
+       if (pclient->set.client_connect_timeout_msecs != 0) {
+               pclient->to = timeout_add
+                       (pclient->set.client_connect_timeout_msecs,
+                               program_client_connect_timeout, pclient);
+       }
+
+       if ((ret=pclient->connect(pclient)) < 0) {
+               program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+               return -1;
+       }
+       return ret;
+}
+
+static int program_client_close_output(struct program_client *pclient)
+{
+       int ret;
+
+       if (pclient->program_output != NULL)
+               o_stream_destroy(&pclient->program_output);
+       if ((ret=pclient->close_output(pclient)) < 0)
+               return -1;
+       pclient->program_output = NULL;
+
+       return ret;
+}
+
+static void program_client_disconnect_extra_fds
+(struct program_client *pclient)
+{
+       struct program_client_extra_fd *efds;
+       unsigned int i, count;
+       
+       if (!array_is_created(&pclient->extra_fds))
+               return;
+
+       efds = array_get_modifiable(&pclient->extra_fds, &count);
+       for (i = 0; i < count; i++) {
+               if (efds[i].input != NULL)
+                       i_stream_unref(&efds[i].input);
+               if (efds[i].io != NULL)
+                       io_remove(&efds[i].io);
+               if (efds[i].parent_fd != -1 && close(efds[i].parent_fd) < 0)
+                       i_error("close(fd=%d) failed: %m", efds[i].parent_fd);
+       }
+}
+
+static void program_client_disconnect
+(struct program_client *pclient, bool force)
+{
+       int ret, error = FALSE;
+
+       if (pclient->ioloop != NULL)
+               io_loop_stop(pclient->ioloop);
+
+       if (pclient->disconnected)
+               return;
+
+       if ((ret=program_client_close_output(pclient)) < 0)
+               error = TRUE;
+
+       program_client_disconnect_extra_fds(pclient);
+       if ((ret=pclient->disconnect(pclient, force)) < 0)
+               error = TRUE;
+
+       if (pclient->program_input != NULL) {
+               if (pclient->output_seekable)
+                       i_stream_unref(&pclient->program_input);
+               else
+                       i_stream_destroy(&pclient->program_input);
+       } 
+       if (pclient->program_output != NULL)
+               o_stream_destroy(&pclient->program_output);
+
+       if (pclient->to != NULL)
+               timeout_remove(&pclient->to);
+       if (pclient->io != NULL)
+               io_remove(&pclient->io);
+
+       if (pclient->fd_in != -1 && close(pclient->fd_in) < 0)
+               i_error("close(%s) failed: %m", pclient->path);
+       if (pclient->fd_out != -1 && pclient->fd_out != pclient->fd_in
+               && close(pclient->fd_out) < 0)
+               i_error("close(%s/out) failed: %m", pclient->path);
+       pclient->fd_in = pclient->fd_out = -1;
+       
+       pclient->disconnected = TRUE;
+       if (error && pclient->error == PROGRAM_CLIENT_ERROR_NONE) {
+               pclient->error = PROGRAM_CLIENT_ERROR_OTHER;
+       }
+}
+
+void program_client_fail
+(struct program_client *pclient, enum program_client_error error)
+{
+       if (pclient->error != PROGRAM_CLIENT_ERROR_NONE)
+               return;
+
+       pclient->error = error;
+       program_client_disconnect(pclient, TRUE);
+}
+
+static bool program_client_input_pending(struct program_client *pclient)
+{
+       struct program_client_extra_fd *efds = NULL;
+       unsigned int count, i;
+
+       if (pclient->program_input != NULL &&
+               !pclient->program_input->closed &&
+               !i_stream_is_eof(pclient->program_input)) {
+               return TRUE;
+       }
+
+       if (array_is_created(&pclient->extra_fds)) {
+               efds = array_get_modifiable(&pclient->extra_fds, &count);
+               for (i = 0; i < count; i++) {
+                       if (efds[i].input != NULL &&
+                               !efds[i].input->closed &&
+                               !i_stream_is_eof(efds[i].input)) {
+                               return TRUE;
+                       }
+               }
+       }
+
+       return FALSE;
+}
+
+static int program_client_program_output(struct program_client *pclient)
+{
+       struct istream *input = pclient->input;
+       struct ostream *output = pclient->program_output;
+       const unsigned char *data;
+       size_t size;
+       int ret = 0;
+
+       if ((ret = o_stream_flush(output)) <= 0) {
+               if (ret < 0) {
+                       i_error("write(%s) failed: %s",
+                               o_stream_get_name(output),
+                               o_stream_get_error(output));
+                       program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+               }
+               return ret;
+       }
+
+       if (input != NULL && output != NULL) {
+               do {
+                       while ((data=i_stream_get_data(input, &size)) != NULL) {
+                               ssize_t sent;
+       
+                               if ((sent=o_stream_send(output, data, size)) < 0) {
+                                       i_error("write(%s) failed: %s",
+                                               o_stream_get_name(output),
+                                               o_stream_get_error(output));
+                                       program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+                                       return -1;
+                               }
+       
+                               if (sent == 0)
+                                       return 0;
+                               i_stream_skip(input, sent);
+                       }
+               } while ((ret=i_stream_read(input)) > 0);
+
+               if (ret == 0)
+                       return 1;
+
+               if (ret < 0) {
+                       if (input->stream_errno != 0) {
+                               i_error("read(%s) failed: %s",
+                                       i_stream_get_name(input),
+                                       i_stream_get_error(input));
+                               program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+                               return -1;
+                       } else if (!i_stream_have_bytes_left(input)) {
+                               i_stream_unref(&pclient->input);
+                               input = NULL;
+
+                               if ((ret = o_stream_flush(output)) <= 0) {
+                                       if (ret < 0) {
+                                               i_error("write(%s) failed: %s",
+                                                       o_stream_get_name(output),
+                                                       o_stream_get_error(output));
+                                               program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+                                       }
+                                       return ret;
+                               }
+                       } 
+               }
+       }
+
+       if (input == NULL) {
+               if (!program_client_input_pending(pclient)) {
+                       program_client_disconnect(pclient, FALSE);
+               } else if (program_client_close_output(pclient) < 0) {
+                       program_client_fail(pclient, PROGRAM_CLIENT_ERROR_OTHER);
+               }
+       }
+       return 1;
+}
+
+static void program_client_program_input(struct program_client *pclient)
+{
+       struct istream *input = pclient->program_input;
+       struct ostream *output = pclient->output;
+       const unsigned char *data;
+       size_t size;
+       int ret = 0;
+
+       if (pclient->output_seekable && pclient->seekable_output == NULL) {
+               struct istream *input_list[2] = { input, NULL };
+
+               input = i_stream_create_seekable(input_list, MAX_OUTPUT_MEMORY_BUFFER,
+                                        program_client_seekable_fd_callback, pclient);
+               i_stream_unref(&pclient->program_input);
+               pclient->program_input = input;
+
+               pclient->seekable_output = input;
+               i_stream_ref(pclient->seekable_output);
+       }
+
+       if (input != NULL) {
+               while ((ret=i_stream_read_data(input, &data, &size, 0)) > 0) {
+                       if (output != NULL) {
+                               ssize_t sent;
+
+                               if ((sent=o_stream_send(output, data, size)) < 0) {
+                                       i_error("write(%s) failed: %s",
+                                               o_stream_get_name(output),
+                                               o_stream_get_error(output));
+                                       program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+                                       return;
+                               }
+                               size = (size_t)sent;
+                       }
+
+                       i_stream_skip(input, size);
+               }
+
+               if (ret < 0) {
+                       if (input->stream_errno != 0) {
+                               i_error("read(%s) failed: %s",
+                                       i_stream_get_name(input),
+                                       i_stream_get_error(input));
+                               program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+                       } else {
+                               if (!program_client_input_pending(pclient))
+                                       program_client_disconnect(pclient, FALSE);
+                       }
+               }
+       }
+}
+
+static void program_client_extra_fd_input
+(struct program_client_extra_fd *efd)
+{
+       struct program_client *pclient = efd->pclient;
+
+       i_assert(efd->callback != NULL);
+       efd->callback(efd->context, efd->input);
+
+       if (efd->input->closed || i_stream_is_eof(efd->input)) {
+               if (!program_client_input_pending(pclient))
+                       program_client_disconnect(pclient, FALSE);
+       }
+}
+
+int program_client_connected
+(struct program_client *pclient)
+{
+       int ret = 1;
+
+       pclient->start_time = ioloop_time;
+       if (pclient->to != NULL)
+               timeout_remove(&pclient->to);
+       if (pclient->set.input_idle_timeout_secs != 0) {
+               pclient->to = timeout_add(pclient->set.input_idle_timeout_secs*1000,
+      program_client_timeout, pclient);
+       }
+
+       /* run output */
+       if (pclient->program_output != NULL &&
+               (ret=program_client_program_output(pclient)) == 0) {
+               if (pclient->program_output != NULL) {
+                       o_stream_set_flush_callback
+                               (pclient->program_output, program_client_program_output, pclient);
+               }
+       }
+
+       return ret;
+}
+
+void program_client_init
+(struct program_client *pclient, pool_t pool, const char *path,
+       const char *const *args, const struct program_client_settings *set)
+{
+       pclient->pool = pool;
+       pclient->path = p_strdup(pool, path);
+       if (args != NULL)
+               pclient->args = p_strarray_dup(pool, args);
+       pclient->set = *set;
+       pclient->debug = set->debug;
+       pclient->fd_in = -1;
+       pclient->fd_out = -1;
+}
+
+void program_client_set_input
+(struct program_client *pclient, struct istream *input)
+{
+       if (pclient->input != NULL)
+               i_stream_unref(&pclient->input);
+       if (input != NULL)
+               i_stream_ref(input);
+       pclient->input = input;
+}
+
+void program_client_set_output
+(struct program_client *pclient, struct ostream *output)
+{
+       if (pclient->output != NULL)
+               o_stream_unref(&pclient->output);
+       if (output != NULL)
+               o_stream_ref(output);
+       pclient->output = output;
+       pclient->output_seekable = FALSE;
+       i_free(pclient->temp_prefix);
+}
+
+void program_client_set_output_seekable
+(struct program_client *pclient, const char *temp_prefix)
+{
+       if (pclient->output != NULL)
+               o_stream_unref(&pclient->output);
+       pclient->temp_prefix = i_strdup(temp_prefix);
+       pclient->output_seekable = TRUE;
+}
+
+struct istream *program_client_get_output_seekable
+(struct program_client *pclient)
+{
+       struct istream *input = pclient->seekable_output;
+       
+       pclient->seekable_output = NULL;
+
+       i_stream_seek(input, 0);
+       return input;
+}
+
+#undef program_client_set_extra_fd
+void program_client_set_extra_fd
+(struct program_client *pclient, int fd,
+       program_client_fd_callback_t *callback, void *context)
+{
+       struct program_client_extra_fd *efds;
+       struct program_client_extra_fd *efd = NULL;
+       unsigned int i, count;
+       i_assert(fd > 1);
+       
+       if (!array_is_created(&pclient->extra_fds))
+               p_array_init(&pclient->extra_fds, pclient->pool, 2);
+
+       efds = array_get_modifiable(&pclient->extra_fds, &count);
+       for (i = 0; i < count; i++) {
+               if (efds[i].child_fd == fd) {
+                       efd = &efds[i];
+                       break;
+               }
+       }
+
+       if (efd == NULL) {
+               efd = array_append_space(&pclient->extra_fds);
+               efd->pclient = pclient;
+               efd->child_fd = fd;
+               efd->parent_fd = -1;
+       }
+       efd->callback = callback;
+       efd->context = context;
+}
+
+void program_client_set_env
+(struct program_client *pclient, const char *name, const char *value)
+{
+       const char *env;
+
+       if (!array_is_created(&pclient->envs))
+               p_array_init(&pclient->envs, pclient->pool, 16);
+
+       env = p_strdup_printf(pclient->pool, "%s=%s", name, value);
+       array_append(&pclient->envs, &env, 1);
+}
+
+void program_client_init_streams(struct program_client *pclient)
+{
+       /* Create streams for normal program I/O */
+       if (pclient->fd_out >= 0) {
+               pclient->program_output =
+                       o_stream_create_fd(pclient->fd_out, MAX_OUTPUT_BUFFER_SIZE, FALSE);
+               o_stream_set_name(pclient->program_output, "program stdin");
+       }
+       if (pclient->fd_in >= 0) {
+               struct istream *input;
+               
+               input = i_stream_create_fd(pclient->fd_in, (size_t)-1, FALSE);
+
+               pclient->program_input = input;
+               i_stream_set_name(pclient->program_input, "program stdout");
+
+               pclient->io = io_add
+                       (pclient->fd_in, IO_READ, program_client_program_input, pclient);
+       }
+
+       /* Create streams for additional output through side-channel fds */
+       if (array_is_created(&pclient->extra_fds)) {
+               struct program_client_extra_fd *efds = NULL;
+               unsigned int count, i;
+               
+               efds = array_get_modifiable(&pclient->extra_fds, &count);
+               for (i = 0; i < count; i++) {
+                       i_assert(efds[i].parent_fd >= 0);
+                       efds[i].input = i_stream_create_fd
+                               (efds[i].parent_fd, (size_t)-1, FALSE);
+                       i_stream_set_name(efds[i].input,
+                               t_strdup_printf("program output fd=%d", efds[i].child_fd));
+                       efds[i].io = io_add
+                               (efds[i].parent_fd, IO_READ, program_client_extra_fd_input, &efds[i]);
+               }
+       }
+}
+
+void program_client_destroy(struct program_client **_pclient)
+{
+       struct program_client *pclient = *_pclient;
+
+       program_client_disconnect(pclient, TRUE);
+
+       if (pclient->input != NULL)
+               i_stream_unref(&pclient->input);
+       if (pclient->output != NULL)
+               o_stream_unref(&pclient->output);
+       if (pclient->seekable_output != NULL)
+               i_stream_unref(&pclient->seekable_output);
+       if (pclient->io != NULL)
+               io_remove(&pclient->io);
+       if (pclient->ioloop != NULL)
+               io_loop_destroy(&pclient->ioloop);
+       i_free(pclient->temp_prefix);
+       pool_unref(&pclient->pool);
+       *_pclient = NULL;
+}
+
+int program_client_run(struct program_client *pclient)
+{
+       int ret;
+
+       /* reset */
+       pclient->disconnected = FALSE;
+       pclient->exit_code = 1;
+       pclient->error = PROGRAM_CLIENT_ERROR_NONE;
+
+       pclient->ioloop = io_loop_create();
+
+       if ((ret=program_client_connect(pclient)) >= 0) {
+               /* run output */
+               if (ret > 0 && pclient->program_output != NULL &&
+                       (ret=o_stream_flush(pclient->program_output)) == 0) {
+                       o_stream_set_flush_callback
+                               (pclient->program_output, program_client_program_output, pclient);
+               }
+
+               /* run i/o event loop */
+               if (ret < 0) {
+                       i_error("write(%s) failed: %s",
+                               o_stream_get_name(pclient->program_output),
+                               o_stream_get_error(pclient->program_output));
+                       pclient->error = PROGRAM_CLIENT_ERROR_IO;
+               } else if (!pclient->disconnected &&
+                       (ret == 0 || program_client_input_pending(pclient))) {
+                       io_loop_run(pclient->ioloop);
+               }
+
+               /* finished */
+               program_client_disconnect(pclient, FALSE);
+       }
+
+       io_loop_destroy(&pclient->ioloop);
+
+       if (pclient->error != PROGRAM_CLIENT_ERROR_NONE)
+               return -1;
+
+       return pclient->exit_code;
+}
+
diff --git a/src/lib-program-client/program-client.h b/src/lib-program-client/program-client.h
new file mode 100644 (file)
index 0000000..4060695
--- /dev/null
@@ -0,0 +1,54 @@
+/* Copyright (c) 2002-2016 Dovecot authors, see the included COPYING file
+ */
+
+#ifndef PROGRAM_CLIENT_H
+#define PROGRAM_CLIENT_H
+
+struct program_client;
+
+struct program_client_settings {
+       unsigned int client_connect_timeout_msecs;
+       unsigned int input_idle_timeout_secs;
+
+       uid_t uid;
+       gid_t gid;
+
+       bool debug:1;
+       bool drop_stderr:1;
+};
+
+typedef void program_client_fd_callback_t(void *context, struct istream *input);
+
+struct program_client *program_client_local_create(const char *bin_path,
+       const char *const *args,
+       const struct program_client_settings *set);
+struct program_client *program_client_remote_create(const char *socket_path,
+       const char *const *args,
+       const struct program_client_settings *set, bool noreply);
+
+void program_client_destroy(struct program_client **_pclient);
+
+void program_client_set_input(struct program_client *pclient,
+       struct istream *input);
+void program_client_set_output(struct program_client *pclient,
+       struct ostream *output);
+
+void program_client_set_output_seekable(struct program_client *pclient,
+       const char *temp_prefix);
+struct istream *program_client_get_output_seekable(struct program_client *pclient);
+
+/* Program provides side-channel output through an extra fd */
+void program_client_set_extra_fd(struct program_client *pclient, int fd,
+        program_client_fd_callback_t * callback, void *context);
+#define program_client_set_extra_fd(pclient, fd, callback, context) \
+       program_client_set_extra_fd(pclient, fd + \
+               CALLBACK_TYPECHECK(callback, \
+                       void (*)(typeof(context), struct istream *input)), \
+               (program_client_fd_callback_t *)callback, context)
+
+void program_client_set_env(struct program_client *pclient,
+       const char *name, const char *value);
+
+int program_client_run(struct program_client *pclient);
+
+#endif