/*
** Copyright 2004-2005 S. Varshavchik.
** See COPYING for distribution information.
*/

#if	HAVE_CONFIG_H
#include	"config.h"
#endif
#include	<stdio.h>
#include	<stdlib.h>
#include	<string.h>
#include	<errno.h>
#include	<ctype.h>
#include	<fcntl.h>
#include	<time.h>
#if	HAVE_UNISTD_H
#include	<unistd.h>
#endif
#if	HAVE_SYS_WAIT_H
#include	<sys/wait.h>
#endif
#include	<sys/socket.h>
#include	<netinet/in.h>
#include	<arpa/inet.h>
#include	<netdb.h>
#if HAVE_SYS_SELECT_H
#include	<sys/select.h>
#endif
#include	<sys/time.h>
#if HAVE_POLL
#include	<sys/poll.h>
#endif
#include	<courierauthdebug.h>
#include	"proxy.h"
#include	<string>
#include	<algorithm>

static int checkhostname(const char *host)
{
	char hostbuf[256];
	const char *proxyhostname;

	if ((proxyhostname=getenv("PROXY_HOSTNAME")) != NULL)
	{
		if (strcmp(proxyhostname, host) == 0)
		{
			courier_authdebug_printf("Proxy host %s is me, normal login.",
						 host);
			return -1;
		}
	}

	if (gethostname(hostbuf, sizeof(hostbuf)))
	{
		courier_authdebug_printf
			("gethostname failed: %s",
			 strerror(errno));
		return -1;
	}

	if (strcmp(hostbuf, host) == 0)
	{
		courier_authdebug_printf("Proxy host %s is me, normal login.",
					 host);
		return -1;
	}
	return 0;
}

static int connect_host(struct proxyinfo *pi, const std::string &host);

int connect_proxy(struct proxyinfo *pi)
{
	std::string h{pi->host};
	int fd;

	auto b=h.begin(), e=h.end();

	while (b != e)
	{
		if (*b == ',')
		{
			++b;
			continue;
		}

		auto p=b;
		b=std::find(b, e, ',');

		fd=connect_host(pi, {p, b});
		if (fd >= 0)
		{
			return fd;
		}
	}
	return -1;
}


static int proxyconnect(struct proxyinfo *pi,
			int aftype,
			void *addr,
			size_t);

#if HAVE_GETADDRINFO

static int connect_host(struct proxyinfo *pi, const std::string &host)
{
	int fd;
	char portbuf[40];
	int errcode;
	struct addrinfo hints, *res, *p;

	if (checkhostname(host.c_str()))
		return (0);

	sprintf(portbuf, "%d", pi->port);
	memset(&hints, 0, sizeof(hints));
	hints.ai_family=PF_UNSPEC;
	hints.ai_socktype=SOCK_STREAM;

	errcode=getaddrinfo(host.c_str(), portbuf, &hints, &res);

	if (errcode)
	{
		courier_authdebug_printf
			("getaddrinfo on proxyhost %s failed: %s",
			 pi->host, gai_strerror(errcode));
		return (-1);
	}

	for (p=res; p; p=p->ai_next)
	{
		if ((fd=proxyconnect(pi, p->ai_family,
				     p->ai_addr,
				     p->ai_addrlen))
		    >= 0)
		{
			if (pi->connected_func(fd, host))
			{
				close(fd);
				courier_authdebug_printf
					("Failed: %s.", strerror(errno));
				continue;
			}
			freeaddrinfo(res);
			return fd;
		}
	}
	freeaddrinfo(res);
	courier_authdebug_printf
		("Connection to proxyhost %s failed.", host.c_str());
	return (-1);
}

#else
static int connect_host(struct proxyinfo *pi, const char *host)
{
	struct hostent *he;
	int i;
	int fd;

	if (checkhostname(host))
		return (0);

	he=gethostbyname(host);

	if (he == NULL)
	{
		courier_authdebug_printf
			("gethostbyname on proxyhost %s failed.",
			 host);
		return (-1);
	}

	for (i=0; he->h_addr_list[i]; i++)
	{
		switch (he->h_addrtype) {
		case AF_INET:
			{
				struct sockaddr_in sin;

				memset(&sin, 0, sizeof(sin));

				sin.sin_family=AF_INET;

				memcpy(&sin.sin_addr, he->h_addr_list[i],
				       sizeof(sin.sin_addr));
				sin.sin_port=htons(pi->port);

				fd=proxyconnect(pi, PF_INET, &sin,
						sizeof(sin));
			}
			break;
#ifdef AF_INET6
		case AF_INET6:
			{
				struct sockaddr_in6 sin6;

				memset(&sin6, 0, sizeof(sin6));

				sin6.sin6_family=AF_INET6;

				memcpy(&sin6.sin6_addr, he->h_addr_list[i],
				       sizeof(sin6.sin6_addr));

				sin6.sin6_port=htons(pi->port);
				fd=proxyconnect(pi, PF_INET6, &sin6,
						sizeof(sin6));
			}
			break;
#endif
		default:
			courier_authdebug_printf
				("Unknown address family type %d",
				 he->h_addrtype);
			continue;
		}

		if (fd >= 0)
		{
			if ((*pi->connected_func)(fd, host,
						  pi->void_arg))
			{
				close(fd);
				courier_authdebug_printf
					("Failed: %s.", strerror(errno));
				continue;
			}
			return fd;
		}
	}
	courier_authdebug_printf
		("Connection to proxyhost %s failed.", host);
	return (-1);
}
#endif

#if HAVE_POLL

static int proxy_waitfd(int fd, int waitWrite, const std::string &hostnamebuf)
{
	struct pollfd pfd;

	memset(&pfd, 0, sizeof(pfd));

	pfd.fd=fd;
	pfd.events= waitWrite ? POLLOUT:POLLIN;

	if (poll(&pfd, 1, 30 * 1000) < 0)
	{
		courier_authdebug_printf
			("Poll failed while waiting to connect to %s: %s",
			 hostnamebuf.c_str(), strerror(errno));
		return -1;
	}

	if (pfd.revents & (POLLOUT|POLLIN))
		return 0;

	courier_authdebug_printf
		("Timeout/error connecting to %s", hostnamebuf.c_str());
	return -1;
}

#else
static int proxy_waitfd(int fd, int waitWrite, const char *hostnamebuf)
{
	fd_set fds;
	struct timeval tv;

	FD_ZERO(&fds);
	FD_SET(fd, &fds);
	tv.tv_sec=30;
	tv.tv_usec=0;

	if (select(fd+1, waitWrite ? NULL:&fds,
		   waitWrite ? &fds:NULL, NULL, &tv) < 0)
	{
		courier_authdebug_printf
			("Select failed while waiting to connect to %s: %s",
			 hostnamebuf, strerror(errno));

		return -1;
	}

	if (!FD_ISSET(fd, &fds))
	{
		courier_authdebug_printf
			("Timeout connecting to %s", hostnamebuf);
		return -1;
	}

	return 0;
}
#endif

static int proxyconnect(struct proxyinfo *pi,
			int pf,
			void *addr,
			size_t addrLen)
{
	int fd;
	char hostnamebuf[256];
	int errcode;
	socklen_t errcode_l;

	struct sockaddr *sa=(struct sockaddr *)addr;

	switch (sa->sa_family) {
	case AF_INET:
		{
			struct sockaddr_in *sin=(struct sockaddr_in *)sa;

			strcpy(hostnamebuf, inet_ntoa(sin->sin_addr));
		}
		break;

#ifdef AF_INET6
	case AF_INET6:
		{
			struct sockaddr_in6 *sin6=
				(struct sockaddr_in6 *)sa;

			if (inet_ntop(AF_INET6, &sin6->sin6_addr,
				      hostnamebuf,
				      sizeof(hostnamebuf)) == NULL)
				strcpy(hostnamebuf, "inet_ntop() failed");
		}
		break;
#endif
	}


	fd=socket(pf, SOCK_STREAM, 0);

	if (fd < 0)
	{
		courier_authdebug_printf("socket: %s", strerror(errno));
		return (-1);
	}

	if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0 ||
	    fcntl(fd, F_SETFD, FD_CLOEXEC) < 0)
	{
		close(fd);
		courier_authdebug_printf("fcntl(socket): %s", strerror(errno));
	}

	if (connect(fd, reinterpret_cast<sockaddr *>(addr), addrLen) == 0)
		return fd;

	if (errno != EINPROGRESS)
	{
		courier_authdebug_printf
			("Proxy connection to %s failed: %s",
			 hostnamebuf, strerror(errno));
		close(fd);
		return -1;
	}

	if (proxy_waitfd(fd, 1, hostnamebuf))
	{
		close(fd);
		return -1;
	}

	errcode_l=sizeof(errcode);

	if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &errcode, &errcode_l) < 0)
	{
		courier_authdebug_printf
			("getsockopt failed: %s", strerror(errno));
		close(fd);
		return -1;
	}

	if (errcode)
	{
		courier_authdebug_printf
			("Proxy connection to %s failed: %s", hostnamebuf,
			 strerror(errcode));
		close(fd);
		return -1;
	}

	return fd;
}

static int proxy_getch(int fd, struct proxybuf *pb)
{
	if (pb->bufleft == 0)
	{
		int n;

		if (proxy_waitfd(fd, 0, "server"))
			return -1;

		pb->bufptr=pb->buffer;

		n=read(fd, pb->buffer, sizeof(pb->buffer));

		if (n < 0)
		{
			courier_authdebug_printf
				("Connection error: %s",
				 strerror(errno));
			return -1;
		}

		if (n == 0)
		{
			courier_authdebug_printf
				("Connection closed by remote host");
			return -1;
		}

		pb->bufleft=(size_t)n;
	}
	--pb->bufleft;
	return ((int)(unsigned char)*pb->bufptr++);
}

int proxy_readline(int fd, struct proxybuf *pb,
		   char *linebuf,
		   size_t linebuflen,
		   int imapmode)
{
	size_t i;
	int ch;
	int prevch;

	i=0;

	ch=0;

	do
	{
		prevch=ch;

		ch=proxy_getch(fd, pb);

		if (ch < 0)
			return -1;

		if (i < linebuflen)
			linebuf[i++]=(char)ch;
	} while (ch != '\n' || (imapmode && prevch != '\r'));

	if (i)
		linebuf[--i]=0;

	if (i && linebuf[i-1] == '\r')
		linebuf[--i]=0;

	DPRINTF("Received: %s", linebuf);

	return 0;
}

int proxy_write(int fd, const std::string &hostname,
		const char *buf, size_t buf_len)
{
	DPRINTF("Sending: %s", buf);

	while (buf_len)
	{
		int n;

		if (proxy_waitfd(fd, 1, hostname))
			return -1;

		n=write(fd, buf, buf_len);

		if (n < 0)
		{
			courier_authdebug_printf
				("Error sending to %s: %s",
				 hostname.c_str(), strerror(errno));
			return -1;
		}

		if (n == 0)
		{
			courier_authdebug_printf
				("Connection close by %s", hostname.c_str());
			return -1;
		}

		buf_len -= n;
		buf += n;
	}
	return 0;
}

#if HAVE_POLL
void proxyloop(int fd)
{
	char stdin_buf[BUFSIZ];
	char stdout_buf[BUFSIZ];

	char *stdin_ptr=NULL;
	char *stdout_ptr=NULL;
	size_t stdin_left=0;
	size_t stdout_left=0;
	int n;

	struct pollfd pfd[2];

	if (fcntl(0, F_SETFL, O_NONBLOCK) ||
	    fcntl(1, F_SETFL, O_NONBLOCK))
	{
		courier_authdebug_printf("fcntl: %s",
					 strerror(errno));
		return;
	}

	do
	{
		memset(&pfd, 0, sizeof(pfd));

		if (stdin_left == 0)
		{
			pfd[0].fd=0;
			pfd[0].events=POLLIN;
		}
		else
		{
			pfd[0].fd=fd;
			pfd[0].events=POLLOUT;
		}

		if (stdout_left == 0)
		{
			pfd[1].fd=fd;
			pfd[1].events=POLLIN;
		}
		else
		{
			pfd[1].fd=1;
			pfd[1].events=POLLOUT;
		}

		n=1;

		if (poll(pfd, 2, -1) < 0)
		{
			courier_authdebug_printf("poll: %s",
						 strerror(errno));
			continue;
		}

		if (stdin_left == 0)
		{
			if (pfd[0].revents)
			{
				n=read(0, stdin_buf, sizeof(stdin_buf));

				if (n > 0)
				{
					stdin_ptr=stdin_buf;
					stdin_left=(size_t)n;
				}
			}
		}
		else if (pfd[0].revents)
		{
			n=write(fd, stdin_ptr, stdin_left);

			if (n > 0)
			{
				stdin_ptr += n;
				stdin_left -= n;
			}
		}

		if (n > 0)
		{
			if (stdout_left == 0)
			{
				if (pfd[1].revents)
				{
					n=read(fd, stdout_buf,
					       sizeof(stdout_buf));

					if (n > 0)
					{
						stdout_ptr=stdout_buf;
						stdout_left=(size_t)n;
					}
				}
			} else if (pfd[1].revents)
			{
				n=write(1, stdout_ptr, stdout_left);

				if (n > 0)
				{
					stdout_ptr += n;
					stdout_left -= n;
				}
			}
		}
	} while (n > 0);

	if (n < 0)
		courier_authdebug_printf("%s", strerror(errno));
}

#else
void proxyloop(int fd)
{
	char stdin_buf[BUFSIZ];
	char stdout_buf[BUFSIZ];

	char *stdin_ptr=NULL;
	char *stdout_ptr=NULL;
	size_t stdin_left=0;
	size_t stdout_left=0;
	int n;

	fd_set fdr, fdw;

	if (fcntl(0, F_SETFL, O_NONBLOCK) ||
	    fcntl(1, F_SETFL, O_NONBLOCK))
	{
		courier_authdebug_printf("fcntl: %s",
					 strerror(errno));
		return;
	}

	do
	{
		FD_ZERO(&fdr);
		FD_ZERO(&fdw);

		if (stdin_left == 0)
			FD_SET(0, &fdr);
		else
			FD_SET(fd, &fdw);

		if (stdout_left == 0)
			FD_SET(fd, &fdr);
		else
			FD_SET(1, &fdw);

		n=1;

		if (select(fd+1, &fdr, &fdw, NULL, NULL) < 0)
		{
			courier_authdebug_printf("select: %s",
						 strerror(errno));
			continue;
		}

		if (stdin_left == 0)
		{
			if (FD_ISSET(0, &fdr))
			{
				n=read(0, stdin_buf, sizeof(stdin_buf));

				if (n > 0)
				{
					stdin_ptr=stdin_buf;
					stdin_left=(size_t)n;
				}
			}
		}
		else if (FD_ISSET(fd, &fdw))
		{
			n=write(fd, stdin_ptr, stdin_left);

			if (n > 0)
			{
				stdin_ptr += n;
				stdin_left -= n;
			}
		}

		if (n > 0)
		{
			if (stdout_left == 0)
			{
				if (FD_ISSET(fd, &fdr))
				{
					n=read(fd, stdout_buf,
					       sizeof(stdout_buf));

					if (n > 0)
					{
						stdout_ptr=stdout_buf;
						stdout_left=(size_t)n;
					}
				}
			} else if (FD_ISSET(1, &fdw))
			{
				n=write(1, stdout_ptr, stdout_left);

				if (n > 0)
				{
					stdout_ptr += n;
					stdout_left -= n;
				}
			}
		}
	} while (n > 0);

	if (n < 0)
		courier_authdebug_printf("%s", strerror(errno));
}
#endif
