diff options
Diffstat (limited to 'src/lib-program-client/program-client-remote.c')
-rw-r--r-- | src/lib-program-client/program-client-remote.c | 702 |
1 files changed, 702 insertions, 0 deletions
diff --git a/src/lib-program-client/program-client-remote.c b/src/lib-program-client/program-client-remote.c new file mode 100644 index 0000000..858abe6 --- /dev/null +++ b/src/lib-program-client/program-client-remote.c @@ -0,0 +1,702 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "ioloop.h" +#include "str.h" +#include "strescape.h" +#include "array.h" +#include "net.h" +#include "write-full.h" +#include "eacces-error.h" +#include "istream-private.h" +#include "ostream.h" +#include "dns-lookup.h" +#include "program-client-private.h" + +#include <unistd.h> +#include <sys/wait.h> +#include <sysexits.h> + +#define PROGRAM_CLIENT_VERSION_MAJOR "4" +#define PROGRAM_CLIENT_VERSION_MINOR "0" + +#define PROGRAM_CLIENT_VERSION_STRING \ + "VERSION\tscript\t" \ + PROGRAM_CLIENT_VERSION_MAJOR "\t" \ + PROGRAM_CLIENT_VERSION_MINOR "\n" + +/* + * Script client input stream + */ + +struct program_client_istream { + struct istream_private istream; + + struct stat statbuf; + + struct program_client *client; + + bool parsed_result:1; +}; + +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 void +program_client_istream_parse_result(struct program_client_istream *scstream, + size_t pos) +{ + struct istream_private *stream = &scstream->istream; + + if (scstream->parsed_result) + return; + scstream->parsed_result = TRUE; + + if (stream->buffer == NULL || pos < 2 || + stream->buffer[pos - 1] != '\n') { + if (pos == 0) { + e_error(scstream->client->event, + "No result code received from remote"); + } else if (pos < 2) { + e_error(scstream->client->event, + "Received too short result code from remote"); + } else { + e_error(scstream->client->event, + "Missing LF in result code"); + } + scstream->client->exit_status = + PROGRAM_CLIENT_EXIT_STATUS_INTERNAL_FAILURE; + return; + } + + unsigned char rcode = stream->buffer[pos - 2]; + switch (rcode) { + case '+': + e_debug(scstream->client->event, + "Received '+' result code from remote"); + scstream->client->exit_status = + PROGRAM_CLIENT_EXIT_STATUS_SUCCESS; + break; + case '-': + e_debug(scstream->client->event, + "Received '-' result code from remote"); + scstream->client->exit_status = + PROGRAM_CLIENT_EXIT_STATUS_FAILURE; + break; + default: + if (rcode >= 0x20 && rcode < 0x7f) { + e_error(scstream->client->event, + "Unexpected result code '%c'", rcode); + } else { + e_error(scstream->client->event, + "Unexpected result code 0x%02x", rcode); + } + scstream->client->exit_status = + PROGRAM_CLIENT_EXIT_STATUS_INTERNAL_FAILURE; + } +} + +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); + + if (stream->parent->eof) { + /* Check return code at EOF */ + program_client_istream_parse_result(scstream, 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) { + i_assert(scstream->parsed_result); + if (pos == 0) + i_stream_skip(stream->parent, reserved); + stream->istream.eof = TRUE; + ret = -1; + } else { + do { + ret = i_stream_read_memarea(stream->parent); + stream->istream.stream_errno = + stream->parent->stream_errno; + stream->buffer = + i_stream_get_data(stream->parent, &pos); + if (ret == -2) { + /* Input buffer full */ + return -2; + } + if (ret < 0 && stream->istream.stream_errno != 0) + break; + + if (stream->parent->eof) { + /* Check return code at EOF */ + program_client_istream_parse_result( + scstream, pos); + } + + ssize_t reserve_mod = 0; + if (stream->buffer != NULL && pos >= 1) { + /* Retain/hide potential return code at end of + buffer */ + size_t old_reserved = reserved; + + reserved = (stream->buffer[pos - 1] == '\n' && + pos > 1 ? 2 : 1); + reserve_mod = (ssize_t)reserved - (ssize_t)old_reserved; + pos -= reserved; + } + if (ret == 0) { + /* Parent already blocked, but we had to update + pos first, to make sure reserved bytes are + not visible to application. */ + break; + } + if (ret > 0 && ret >= reserve_mod) { + /* Subtract additional reserved bytes */ + ret -= reserve_mod; + } + + if (ret <= 0 && stream->parent->eof) { + /* Parent EOF and not more data to return; + EOF here as well */ + i_assert(scstream->parsed_result); + 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, + i_stream_get_fd(input), 0); +} + +/* + * Program client + */ + +struct program_client_remote { + struct program_client client; + + const char *address; + struct dns_lookup_settings dns_set; + struct dns_lookup *lookup; + unsigned int ips_count; + unsigned int ips_left; + struct ip_addr *ips; + in_port_t port; + + struct timeout *to_retry; + + bool noreply:1; + bool resolved:1; + bool have_hostname:1; +}; + +static void +program_client_net_connect_again(struct program_client_remote *prclient); + +static void +program_client_remote_connected(struct program_client_remote *prclient) +{ + struct program_client *pclient = &prclient->client; + const char **args = pclient->args; + string_t *str; + + timeout_remove(&pclient->to); + io_remove(&pclient->io); + program_client_init_streams(pclient); + + if (!prclient->noreply) { + struct istream *is = pclient->raw_program_input; + + pclient->raw_program_input = + program_client_istream_create(pclient, is); + i_stream_unref(&is); + } + + str = t_str_new(1024); + str_append(str, PROGRAM_CLIENT_VERSION_STRING); + if (array_is_created(&pclient->envs)) { + const char *env; + array_foreach_elem(&pclient->envs, env) { + str_append(str, "env_"); + str_append_tabescaped(str, env); + str_append_c(str, '\n'); + } + } + if (prclient->noreply) + str_append(str, "noreply\n"); + else + str_append(str, "-\n"); + if (args != NULL) { + for(; *args != NULL; args++) { + str_append_tabescaped(str, *args); + str_append_c(str, '\n'); + } + } + str_append_c(str, '\n'); + + if (o_stream_send(pclient->raw_program_output, + str_data(str), str_len(str)) < 0) { + e_error(pclient->event, + "write(%s) failed: %s", + o_stream_get_name(pclient->raw_program_output), + o_stream_get_error(pclient->raw_program_output)); + program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO); + return; + } + + program_client_connected(pclient); +} + +static int program_client_unix_connect(struct program_client *pclient); + +static void +program_client_unix_reconnect(struct program_client_remote *prclient) +{ + (void)program_client_unix_connect(&prclient->client); +} + +static int program_client_unix_connect(struct program_client *pclient) +{ + struct program_client_remote *prclient = + (struct program_client_remote *)pclient; + int fd; + + e_debug(pclient->event, "Trying to connect"); + + timeout_remove(&prclient->to_retry); + + if ((fd = net_connect_unix(prclient->address)) < 0) { + switch (errno) { + case EACCES: + e_error(pclient->event, "%s", + eacces_error_get("net_connect_unix", + prclient->address)); + return -1; + case EAGAIN: + prclient->to_retry = timeout_add_short( + 100, program_client_unix_reconnect, prclient); + return 0; + default: + e_error(pclient->event, + "net_connect_unix(%s) failed: %m", + prclient->address); + return -1; + } + } + + pclient->fd_in = (prclient->noreply && pclient->output == NULL ? + -1 : fd); + pclient->fd_out = fd; + pclient->io = io_add(fd, IO_WRITE, + program_client_remote_connected, prclient); + return 0; +} + +static void +program_client_net_connect_timeout(struct program_client_remote *prclient) +{ + struct program_client *pclient = &prclient->client; + + io_remove(&pclient->io); + timeout_remove(&pclient->to); + + e_error(pclient->event, "connect(%s) failed: " + "Timeout in %u milliseconds", prclient->address, + pclient->set.client_connect_timeout_msecs); + + /* Set error to timeout here */ + pclient->error = PROGRAM_CLIENT_ERROR_CONNECT_TIMEOUT; + i_close_fd(&pclient->fd_out); + pclient->fd_in = pclient->fd_out = -1; + program_client_net_connect_again(prclient); +} + +/* See if connect succeeded or not, if it did, then proceed normally, otherwise + try reconnect to next address. + */ +static void program_client_net_connected(struct program_client_remote *prclient) +{ + struct program_client *pclient = &prclient->client; + + io_remove(&pclient->io); + + errno = net_geterror(pclient->fd_out); + if (errno != 0) { + e_error(pclient->event, "connect(%s) failed: %m", + prclient->address); + + /* Disconnect and try again */ + i_close_fd(&pclient->fd_out); + pclient->fd_in = pclient->fd_out = -1; + program_client_net_connect_again(prclient); + } else { + pclient->io = io_add(pclient->fd_out, IO_WRITE, + program_client_remote_connected, prclient); + } +} + +static void +program_client_net_connect_real(struct program_client_remote *prclient) +{ + struct program_client *pclient = &prclient->client; + const char *address, *label; + + timeout_remove(&pclient->to); + timeout_remove(&prclient->to_retry); + + i_assert(prclient->ips_count > 0); + + if (net_ipport2str(prclient->ips, prclient->port, &address) < 0) + i_unreached(); + label = t_strconcat("tcp:", address, NULL); + program_client_set_label(pclient, label); + + e_debug(pclient->event, "Trying to connect (timeout %u msecs)", + pclient->set.client_connect_timeout_msecs); + + /* Try to connect */ + int fd; + if ((fd = net_connect_ip(prclient->ips, prclient->port, + (prclient->ips->family == AF_INET ? + &net_ip4_any : &net_ip6_any))) < 0) { + e_error(pclient->event, "connect(%s) failed: %m", address); + prclient->to_retry = timeout_add_short( + 0, program_client_net_connect_again, prclient); + return; + } + + pclient->fd_in = (prclient->noreply && pclient->output == NULL ? + -1 : fd); + pclient->fd_out = fd; + pclient->io = io_add(fd, IO_WRITE, + program_client_net_connected, prclient); + + if (pclient->set.client_connect_timeout_msecs != 0) { + pclient->to = timeout_add( + pclient->set.client_connect_timeout_msecs, + program_client_net_connect_timeout, prclient); + } +} + +static void +program_client_net_connect_again(struct program_client_remote *prclient) +{ + struct program_client *pclient = &prclient->client; + enum program_client_error error = pclient->error; + + pclient->error = PROGRAM_CLIENT_ERROR_NONE; + + if (--prclient->ips_left == 0) { + if (prclient->ips_count > 1) { + e_error(pclient->event, + "No IP addresses left to try"); + } + program_client_fail(pclient, + (error != PROGRAM_CLIENT_ERROR_NONE ? + error : PROGRAM_CLIENT_ERROR_OTHER)); + return; + }; + + prclient->ips++; + program_client_net_connect_real(prclient); +} + +static void +program_client_net_connect_resolved(const struct dns_lookup_result *result, + struct program_client_remote *prclient) +{ + struct program_client *pclient = &prclient->client; + + if (result->ret != 0) { + e_error(pclient->event, "Cannot resolve `%s': %s", + prclient->address, result->error); + program_client_fail(pclient, PROGRAM_CLIENT_ERROR_OTHER); + return; + } + + e_debug(pclient->event, "DNS lookup successful; got %d IPs", + result->ips_count); + + /* Reduce timeout */ + if (pclient->set.client_connect_timeout_msecs > 0) { + if (pclient->set.client_connect_timeout_msecs <= + result->msecs) { + /* We ran out of time */ + program_client_fail( + pclient, PROGRAM_CLIENT_ERROR_CONNECT_TIMEOUT); + return; + } + pclient->set.client_connect_timeout_msecs -= result->msecs; + } + + /* Then connect */ + prclient->ips_count = result->ips_count; + prclient->ips_left = prclient->ips_count; + prclient->ips = p_memdup(pclient->pool, result->ips, + sizeof(struct ip_addr)*result->ips_count); + program_client_net_connect_real(prclient); +} + +static int program_client_net_connect_init(struct program_client *pclient) +{ + struct program_client_remote *prclient = + (struct program_client_remote *)pclient; + struct ip_addr ip; + + if (prclient->ips != NULL) { + /* Nothing to do */ + } else if (net_addr2ip(prclient->address, &ip) == 0) { + prclient->resolved = TRUE; + prclient->ips = p_new(pclient->pool, struct ip_addr, 1); + *prclient->ips = ip; + prclient->ips_count = 1; + } else { + prclient->resolved = FALSE; + if (pclient->set.dns_client_socket_path != NULL) { + e_debug(pclient->event, + "Performing asynchronous DNS lookup"); + prclient->dns_set.dns_client_socket_path = + pclient->set.dns_client_socket_path; + prclient->dns_set.timeout_msecs = + pclient->set.client_connect_timeout_msecs; + prclient->dns_set.event_parent = pclient->event; + dns_lookup(prclient->address, &prclient->dns_set, + program_client_net_connect_resolved, + prclient, &prclient->lookup); + return 0; + } else { + struct ip_addr *ips; + unsigned int ips_count; + int err; + + /* Guess we do it here then.. */ + err = net_gethostbyname(prclient->address, + &ips, &ips_count); + if (err != 0) { + e_error(pclient->event, + "Cannot resolve `%s': %s", + prclient->address, + net_gethosterror(err)); + return -1; + } + prclient->ips_count = ips_count; + prclient->ips = p_memdup(pclient->pool, + ips, sizeof(*ips)*ips_count); + + e_debug(pclient->event, + "DNS lookup successful; got %d IPs", + ips_count); + } + } + + prclient->ips_left = prclient->ips_count; + prclient->to_retry = timeout_add_short( + 0, program_client_net_connect_real, prclient); + 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) { + e_error(pclient->event, + "shutdown(fd_out, SHUT_WR) failed: %m"); + return -1; + } + } else { + i_close_fd(&fd_out); + } + } + + return 1; +} + +static void +program_client_remote_disconnect(struct program_client *pclient, + bool force ATTR_UNUSED) +{ + struct program_client_remote *prclient = + (struct program_client_remote *)pclient; + + timeout_remove(&prclient->to_retry); + + program_client_disconnected(pclient); +} + +static void +program_client_remote_switch_ioloop(struct program_client *pclient) +{ + struct program_client_remote *prclient = + (struct program_client_remote *)pclient; + + if (prclient->to_retry != NULL) + prclient->to_retry = io_loop_move_timeout(&prclient->to_retry); + if (prclient->lookup != NULL) + dns_lookup_switch_ioloop(prclient->lookup); +} + +struct program_client * +program_client_unix_create(const char *socket_path, const char *const *args, + const struct program_client_settings *set, + bool noreply) +{ + struct program_client_remote *prclient; + const char *label; + pool_t pool; + + label = t_strconcat("unix:", socket_path, NULL); + + pool = pool_alloconly_create("program client unix", 1024); + prclient = p_new(pool, struct program_client_remote, 1); + program_client_init(&prclient->client, pool, label, args, set); + prclient->client.connect = program_client_unix_connect; + prclient->client.close_output = program_client_remote_close_output; + prclient->client.disconnect = program_client_remote_disconnect; + prclient->client.switch_ioloop = program_client_remote_switch_ioloop; + prclient->address = p_strdup(pool, socket_path); + prclient->noreply = noreply; + + return &prclient->client; +} + +struct program_client * +program_client_net_create(const char *host, in_port_t port, + const char *const *args, + const struct program_client_settings *set, + bool noreply) +{ + struct program_client_remote *prclient; + const char *label; + pool_t pool; + + label = t_strdup_printf("tcp:%s:%u", host, port); + + pool = pool_alloconly_create("program client net", 1024); + prclient = p_new(pool, struct program_client_remote, 1); + program_client_init(&prclient->client, pool, label, args, set); + prclient->client.connect = program_client_net_connect_init; + prclient->client.close_output = program_client_remote_close_output; + prclient->client.disconnect = program_client_remote_disconnect; + prclient->client.set.use_dotstream = TRUE; + prclient->address = p_strdup(pool, host); + prclient->port = port; + prclient->have_hostname = TRUE; + prclient->noreply = noreply; + return &prclient->client; +} + +struct program_client * +program_client_net_create_ips(const struct ip_addr *ips, size_t ips_count, + in_port_t port, + const char *const *args, + const struct program_client_settings *set, + bool noreply) +{ + struct program_client_remote *prclient; + const char *label; + pool_t pool; + + i_assert(ips != NULL && ips_count > 0); + + if (net_ipport2str(ips, port, &label) < 0) + i_unreached(); + label = t_strconcat("tcp:", label, NULL); + + pool = pool_alloconly_create("program client net", 1024); + prclient = p_new(pool, struct program_client_remote, 1); + program_client_init(&prclient->client, pool, label, args, set); + prclient->client.connect = program_client_net_connect_init; + prclient->client.close_output = program_client_remote_close_output; + prclient->client.disconnect = program_client_remote_disconnect; + prclient->client.switch_ioloop = program_client_remote_switch_ioloop; + prclient->client.set.use_dotstream = TRUE; + prclient->address = p_strdup(pool, net_ip2addr(ips)); + prclient->ips = p_memdup(pool, ips, + sizeof(struct ip_addr)*ips_count); + prclient->ips_count = ips_count; + prclient->port = port; + prclient->noreply = noreply; + return &prclient->client; +} |