/* Distributed Checksum Clearinghouse
 *
 * dccif(), a sample C interface to the DCC interface daemon, dccifd
 *
 * Copyright (c) 2005 by Rhyolite Software
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND RHYOLITE SOFTWARE DISCLAIMS ALL
 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL RHYOLITE SOFTWARE
 * BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
 * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
 * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
 * SOFTWARE.
 *
 * Rhyolite Software DCC 1.2.74-1.19 $Revision$
 */


#include "dccif.h"
#include "dcc_clnt.h"
#include "sys/uio.h"
#include <sys/un.h>
#ifndef DCC_WIN32
#include <arpa/inet.h>
#endif



static void
dccif_closes(int *fd1, int *fd2, int *fd3)
{
	if (fd1 && *fd1 >= 0) {
		close(*fd1);
		*fd1 = -1;
	}
	if (fd2 && *fd2 >= 0) {
		close(*fd2);
		*fd2 = -1;
	}
	if (fd3 && *fd3 >= 0) {
		close(*fd3);
		*fd3 = -1;
	}
}



static void PATTRIB(6,7)
dccif_pemsg(int ex_code, DCC_EMSG emsg,
	    int *fd1, int *fd2, int *fd3,
	    const char *msg, ...)
{
	va_list args;

	if (emsg) {
		va_start(args, msg);
		dcc_vpemsg(ex_code, emsg, msg, args);
		va_end(args);
	}

	dccif_closes(fd1, fd2, fd3);
}



static u_char
dccif_sock(DCC_EMSG emsg,
	   const char *homedir,		/* home directory or host,port */
	   SOCKET *s, int* fd2, int* fd3)
{
	struct stat sb;
	char host[MAXHOSTNAMELEN];
	u_int16_t port;
	DCC_SOCKU su;
	struct sockaddr_un sunsock;
	const char *cp;
	int error;

	/* assume a non-existent home directory is a hostname,port */
	if (0 > stat(homedir, &sb)
	    || !S_ISDIR(sb.st_mode)) {
		cp = dcc_parse_nm_port(emsg, homedir, DCC_GET_PORT_INVALID,
				       host, sizeof(host),
				       &port, 0, 0,
				       0, 0);
		if (!cp) {
			dccif_closes(0, fd2, fd3);
			return 0;
		}
		if (*cp != '\0') {
			dccif_pemsg(EX_USAGE, emsg, 0, fd2, fd3,
				    "invalid IP address: %s",
				    homedir);
			return 0;
		}
		if (host[0] == '\0') {
			dccif_pemsg(EX_NOHOST, emsg, 0, fd2, fd3,
				    "missing host name in \"%s\"", homedir);
			return 0;

		} else {
			dcc_host_lock();
			if (!dcc_get_host(host, 2, &error)) {
				dcc_host_unlock();
				dccif_pemsg(EX_NOHOST, emsg, 0, fd2, fd3,
					    "%s: %s",
					    homedir, hstrerror(error));
				return 0;
			}
			su = dcc_hostaddrs[0];
			*DCC_SU_PORT(&su) = port;
			dcc_host_unlock();
		}

		*s = socket(su.sa.sa_family, SOCK_STREAM, 0);
		if (*s < 0) {
			dccif_pemsg(EX_IOERR, emsg, s, fd2, fd3,
				    "socket(AF_UNIX): %s", ERROR_STR());
			return 0;
		}

		if (0 > connect(*s, &su.sa, DCC_SU_LEN(&su))) {
			dccif_pemsg(EX_IOERR, emsg, s, fd2, fd3,
				    "connect(%s): %s", homedir, ERROR_STR());
			return 0;
		}
		return 1;
	}

	*s = socket(AF_UNIX, SOCK_STREAM, 0);
	if (*s < 0) {
		dccif_pemsg(EX_IOERR, emsg, s, fd2, fd3,
			    "socket(AF_UNIX): %s", ERROR_STR());
		return 0;
	}
	memset(&sunsock, 0, sizeof(sunsock));
	sunsock.sun_family = AF_UNIX;
	snprintf(sunsock.sun_path, sizeof(sunsock.sun_path),
		 "%s/"DCC_DCCIF_UDS, homedir);
#ifdef DCC_HAVE_SA_LEN
	sunsock.sun_len = SUN_LEN(&sunsock);
#endif
	if (0 > connect(*s, (struct sockaddr *)&sunsock, sizeof(sunsock))) {
		dccif_pemsg(EX_IOERR, emsg, s, fd2, fd3,
			    "connect(%s): %s",
			    sunsock.sun_path, ERROR_STR());
		return 0;
	}
	return 1;
}



static u_char
dccif_writev(DCC_EMSG emsg,
	     int *out_fd, int *fd2, int *fd3,
	     const struct iovec *iovs,
	     int num_iovs,
	     int wtotal)
{
	int i;

	i = writev(*out_fd, iovs, num_iovs);
	if (i == wtotal)
		return 1;
	if (i < 0)
		dccif_pemsg(EX_IOERR, emsg, out_fd, fd2, fd3,
			    "dccif writev(%d): %s", wtotal, ERROR_STR());
	else
		dccif_pemsg(EX_IOERR, emsg, out_fd, fd2, fd3,
			    "dccif writev(%d)=%d", wtotal, i);
	return 0;
}



static u_char
dccif_write(DCC_EMSG emsg,
	    int *out_fd, int *fd2, int *fd3,
	    const void *buf, int buf_len)
{
	int i;

	i = write(*out_fd, buf, buf_len);
	if (i == buf_len)
		return 1;
	if (i < 0)
		dccif_pemsg(EX_IOERR, emsg, out_fd, fd2, fd3,
			    "dccif writev(%d): %s", buf_len, ERROR_STR());
	else
		dccif_pemsg(EX_IOERR, emsg, out_fd, fd2, fd3,
			    "dccif writev(%d)=%d",
			    buf_len, i);
	return 0;
}



static int
dccif_read(DCC_EMSG emsg,
	   int *in_fd, int *fd2, int *fd3,
	   char *buf, int buf_len, int min_read)
{
	int total, i;

	total = 0;
	for (;;) {
		i = read(*in_fd, &buf[total], buf_len-total);
		if (i < 0) {
			dccif_pemsg(EX_IOERR, emsg, in_fd, fd2, fd3,
				    "dccif read(): %s", ERROR_STR());
			return -1;
		}
		total += i;
		if (total >= min_read)
			return total;
		if (i == 0) {
			dccif_pemsg(EX_IOERR, emsg, in_fd, fd2, fd3,
				    "dccif read(): premature EOF");
			return -1;
		}
	}
}



u_char					/* 0=failed or DCCIF_RESULT_* */
dccif(DCC_EMSG emsg,			/* put error message here */
      int out_body_fd,			/* -1 or write body to here */
      char **out_body,			/* 0 or pointer for resulting body */
      const char *opts,			/* blank separated string DCCIF_OPT_* */
      const DCC_SOCKU *clnt_addr,	/* SMTP client IPv4 or IPv6 address */
      const char *clnt_name,		/* optional SMTP client name */
      const char *helo,
      const char *env_from,		/* envelope sender */
      DCCIF_RCPT *rcpts,		/* pruned envelope recipients */
      int in_body_fd,			/* -1 or read body from here */
      const char *in_body,		/* 0 or start of incoming body */
      const char *homedir)		/* home directory or host,port */
{
	static const char nl = '\n';
	static const char cr = '\r';
	int s;
	char clnt_str[INET6_ADDRSTRLEN+1+1];
	struct iovec iovs[50], *iovp;
	int wtotal;
#	define ADD_IOV(b,l) {int _l = (l); iovp->iov_base = (char *)(b); \
		iovp->iov_len = _l; ++iovp; wtotal += _l;}
	DCCIF_RCPT *rcpt;
	char buf[4*1024];
	char result;
	int read_len, in_body_len, max_body_len, nxt;
	int i, j;

	if (!homedir || *homedir == '\0')
		homedir = DCC_HOMEDIR;

	if (emsg)
		*emsg = '\0';

	if (!clnt_addr) {
		clnt_str[0] = '\0';
	} else if (clnt_addr->sa.sa_family == AF_INET) {
		if (!DCC_INET_NTOP(AF_INET,
				   &clnt_addr->ipv4.sin_addr,
				   clnt_str, sizeof(clnt_str))) {
			dccif_pemsg(EX_SOFTWARE, emsg,
				    0, &in_body_fd, &out_body_fd,
				    "inet_ntop(): %s", ERROR_STR());
			return 0;
		}
	} else {
		if (!DCC_INET_NTOP(clnt_addr->sa.sa_family,
				   &clnt_addr->ipv6.sin6_addr,
				   clnt_str, sizeof(clnt_str))) {
			dccif_pemsg(EX_SOFTWARE, emsg, 0,
				    &in_body_fd, &out_body_fd,
				    "inet_ntop(): %s", ERROR_STR());
			return 0;
		}
	}

	if (!dccif_sock(emsg, homedir, &s, &in_body_fd, &out_body_fd))
		return 0;

	/* first line of request */
	iovp = iovs;
	wtotal = 0;
	if (opts)
		ADD_IOV(opts, strlen(opts));
	ADD_IOV(&nl, 1);

	i = strlen(clnt_str);
	clnt_str[i++] = '\r';
	ADD_IOV(clnt_str, i);
	if (i > 1 && clnt_name)
		ADD_IOV(clnt_name, strlen(clnt_name));
	ADD_IOV(&nl, 1);

	if (helo)
		ADD_IOV(helo, strlen(helo));
	ADD_IOV(&nl, 1);

	if (env_from)
		ADD_IOV(env_from, strlen(env_from));
	ADD_IOV(&nl, 1);

	for (rcpt = rcpts; rcpt; rcpt = rcpt->next) {
		ADD_IOV(rcpt->addr, strlen(rcpt->addr));
		ADD_IOV(&cr, 1);
		if (rcpt->user)
			ADD_IOV(rcpt->user, strlen(rcpt->user));
		ADD_IOV(&nl, 1);
		if (iovp >= &iovs[DIM(iovs)-4-1]) {
			if (!dccif_writev(emsg, &s, &in_body_fd, &out_body_fd,
					  iovs, iovp - iovs, wtotal))
				return 0;
			iovp = iovs;
			wtotal = 0;
		}
	}
	ADD_IOV(&nl, 1);

	/* copy (some of) the body from the buffer to the daemon */
	if (in_body) {
		in_body_len = strlen(in_body);
		ADD_IOV(in_body, in_body_len);
	} else {
		in_body_len = 0;
	}

	if (!dccif_writev(emsg, &s, &in_body_fd, &out_body_fd,
			  iovs, iovp - iovs, wtotal))
		return 0;

	/* copy (the rest of) the body from input file to the daemon */
	if (in_body_fd >= 0) {
		for (;;) {
			i = read(in_body_fd, buf, sizeof(buf));
			if (i <= 0) {
				if (i == 0)
					break;
				dccif_pemsg(EX_IOERR, emsg,
					    &s, &in_body_fd, &out_body_fd,
					    "read(body): %s", ERROR_STR());
				return 0;
			}
			if (!dccif_write(emsg, &s, &in_body_fd, &out_body_fd,
					 buf, i))
				return 0;
			in_body_len += i;
		}
		if (0 > close(in_body_fd)) {
			dccif_pemsg(EX_IOERR, emsg,
				    &s, &in_body_fd, &out_body_fd,
				    "close(in_body_fd): %s", ERROR_STR());
			return 0;
		}
		in_body_fd = -1;
	}

	/* tell the daemon it has all of the body */
	if (0 > shutdown(s, 1)) {
		dccif_pemsg(EX_IOERR, emsg, &s, &in_body_fd, &out_body_fd,
			    "shutdown(): %s", ERROR_STR());
		return 0;
	}

	/* get the overall result */
	read_len = dccif_read(emsg, &s, &in_body_fd, &out_body_fd,
			      buf, sizeof(buf), 2);
	if (read_len < 0)
		return 0;
	result = buf[0];
	if (result != DCCIF_RESULT_OK
	    && result != DCCIF_RESULT_GREY
	    && result != DCCIF_RESULT_REJECT
	    && result != DCCIF_RESULT_SOME
	    && result != DCCIF_RESULT_TEMP) {
		dccif_pemsg(EX_SOFTWARE, emsg, &s, &in_body_fd, &out_body_fd,
			  "unrecognized dccifd result \"%.*s\"", 1, buf);
		return 0;
	}

	/* read the vector of results from the daemon */
	nxt = 1;			/* skip '\n' after result */
	for (rcpt = rcpts; rcpt != 0; rcpt = rcpt->next) {
		if (++nxt >= read_len) {
			read_len = dccif_read(emsg,
					      &s, &in_body_fd, &out_body_fd,
					      buf, sizeof(buf), 2);
			if (read_len < 0)
				return 0;
			nxt = 0;
		}
		switch (buf[nxt]) {
		case DCCIF_RCPT_ACCEPT:
		case DCCIF_RCPT_REJECT:
		case DCCIF_RCPT_GREY:
			rcpt->ok = buf[nxt];
			break;
		default:
			dccif_pemsg(EX_SOFTWARE, emsg,
				    &s, &in_body_fd, &out_body_fd,
				    "unrecognized dccifd recipient result"
				    " \"%c\" for %s",
				    buf[nxt], rcpt->addr);
			return 0;
		}
	}
	if (++nxt >= read_len) {
		read_len = dccif_read(emsg, &s, &in_body_fd, &out_body_fd,
				      buf, 1, 1);
		if (read_len < 0)
			return 0;
		nxt = 0;
	}
	if (buf[nxt++] != '\n') {
		dccif_pemsg(EX_SOFTWARE, emsg, &s, &in_body_fd, &out_body_fd,
			  "unrecognized dccifd text after results: \"%c\"",
			  buf[nxt]);
		return 0;
	}

	/* copy the body from the daemon to the output buffer for file */
	if (out_body_fd >= 0) {
		for (;;) {
			j = read_len - nxt;
			if (j <= 0) {
				j = dccif_read(emsg, &s,
					       &in_body_fd, &out_body_fd,
					       buf, sizeof(buf), 0);
				if (j < 0)
					return 0;
				if (j == 0)
					break;
				read_len = j;
				nxt = 0;
			}
			if (!dccif_write(emsg, &out_body_fd,
					 &s, &in_body_fd,
					 &buf[nxt], j))
				return 0;
			read_len = 0;
		}
		if (0 > close(out_body_fd)) {
			dccif_pemsg(EX_IOERR, emsg,
				    &s, &in_body_fd, &out_body_fd,
				    "close(out_body_fd): %s",
				    ERROR_STR());
			return 0;
		}
		out_body_fd = -1;

	} else if (out_body) {
		char *body;

		max_body_len = in_body_len + DCC_MAX_XHDR_LEN+1;
		body = malloc(max_body_len);
		*out_body = body;
		j = read_len - nxt;
		if (j > 0) {
			if (j > max_body_len)
				j = max_body_len;
			memcpy(body, &buf[nxt], j);
			body += j;
			max_body_len -= j;
		}
		for (;;) {
			if (max_body_len <= 0) {
				dccif_pemsg(EX_SOFTWARE, emsg,
					    &s, &in_body_fd, &out_body_fd,
					    "too much body from dccifd");
				free(*out_body);
				return 0;
			}

			j = dccif_read(emsg, &s, &in_body_fd, &out_body_fd,
				       body, max_body_len, 0);
			if (j < 0) {
				free(*out_body);
				return 0;
			}
			if (j == 0)
				break;
			body += j;
			max_body_len -= j;
		}
		*body = '\0';
	}

	if (0 > close(s)) {
		dccif_pemsg(EX_IOERR, emsg, &s, &in_body_fd, &out_body_fd,
			    "close(socket): %s", ERROR_STR());
		return 0;
	}
	return result;
#undef ADD_IOV
}
