/* Copyright (c) Dovecot authors, see top-level 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),
			       ISTREAM_HIDDEN_INPUTS_NONE, 0);
}

/*
 * Program client
 */

struct program_client_remote {
	struct program_client client;

	const char *address;
	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;
	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->params.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);

	address = net_ipport2str(prclient->ips, prclient->port);
	label = t_strconcat("tcp:", address, NULL);
	program_client_set_label(pclient, label);

	e_debug(pclient->event, "Trying to connect (timeout %u msecs)",
		pclient->params.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->params.client_connect_timeout_msecs != 0) {
		pclient->to = timeout_add(
			pclient->params.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->params.client_connect_timeout_msecs > 0) {
		if (pclient->params.client_connect_timeout_msecs <=
		    result->msecs) {
			/* We ran out of time */
			program_client_fail(
				pclient, PROGRAM_CLIENT_ERROR_CONNECT_TIMEOUT);
			return;
		}
		pclient->params.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->params.dns_client_socket_path != NULL) {
			e_debug(pclient->event,
				"Performing asynchronous DNS lookup");
			(void)dns_lookup(prclient->address, NULL,
					 pclient->event,
					 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(struct event *event,
			   const char *socket_path, const char *const *args,
			   const struct program_client_parameters *params)
{
	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, event, label, args, params);
	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 = params->no_reply;

	return &prclient->client;
}

struct program_client *
program_client_net_create(struct event *event, const char *host, in_port_t port,
			  const char *const *args,
			  const struct program_client_parameters *params)
{
	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, event, label, args, params);
	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.params.use_dotstream = TRUE;
	prclient->address = p_strdup(pool, host);
	prclient->port = port;
	prclient->have_hostname = TRUE;
	prclient->noreply = params->no_reply;
	return &prclient->client;
}

struct program_client *
program_client_net_create_ips(struct event *event,
			      const struct ip_addr *ips, size_t ips_count,
			      in_port_t port,
			      const char *const *args,
			      const struct program_client_parameters *params)
{
	struct program_client_remote *prclient;
	const char *label;
	pool_t pool;

	i_assert(ips != NULL && ips_count > 0);

	label = t_strconcat("tcp:", net_ipport2str(ips, port), NULL);

	pool = pool_alloconly_create("program client net", 1024);
	prclient = p_new(pool, struct program_client_remote, 1);
	program_client_init(&prclient->client, pool, event, label, args, params);
	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.params.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 = params->no_reply;
	return &prclient->client;
}
