view src/lib-program-client/program-client-remote.c @ 22784:99acd6d25ad2

lib-program-client: remote: Don't change exit_code in program_client_remote_disconnect() when program_input is already NULL. When the program_input is NULL, the stream is finished, meaning that the exit_code is set based on the return code. There can be a program_input for remote streams, even when the program produces no output.
author Stephan Bosch <stephan.bosch@dovecot.fi>
date Wed, 24 Jan 2018 01:09:16 +0100
parents 751eb2d43bc7
children
line wrap: on
line source

/* 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 "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;
};

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 *prclient =
		(struct program_client_remote *) pclient;
	const char **args = pclient->args;
	string_t *str;

	io_remove(&pclient->io);
	program_client_init_streams(pclient);

	if (!prclient->noreply) {
		struct istream *is = pclient->program_input;
		pclient->program_input =
			program_client_istream_create(pclient, pclient->program_input);
		i_stream_unref(&is);
	}

	str = t_str_new(1024);
	str_append(str, PROGRAM_CLIENT_VERSION_STRING);
	if (array_is_created(&pclient->envs)) {
		const char *const *env;
		array_foreach(&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->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 *prclient =
		(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 = (prclient->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
void program_client_remote_disconnect(struct program_client *pclient, bool force)
{
	struct program_client_remote *prclient =
		(struct program_client_remote *)pclient;

	if (pclient->program_input == NULL) {
		/* nothing */
	} else if (pclient->error == PROGRAM_CLIENT_ERROR_NONE &&
		   !prclient->noreply && !force) {
		const unsigned char *data;
		size_t size;

		/* Skip any remaining program output and parse the exit code */
		while (i_stream_read_more
			(pclient->program_input, &data, &size) > 0) {
			i_stream_skip(pclient->program_input, size);
		}

		/* Check for error and EOF. Since we're disconnected, always
		   mark an internal error when not all input is read. This is
		   generally unlikely to occur. */
		if (pclient->program_input->stream_errno != 0 ||
		    i_stream_have_bytes_left(pclient->program_input))
			pclient->exit_code = -1;
	} else {
		pclient->exit_code = 1;
	}

	program_client_disconnected(pclient);
}

static
void program_client_remote_switch_ioloop(struct program_client *pclient ATTR_UNUSED)
{
}

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->client.switch_ioloop = program_client_remote_switch_ioloop;
	pclient->noreply = noreply;

	return &pclient->client;
}