/*
tcpd.c
*/

#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <minix/config.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <net/gen/in.h>
#include <net/gen/inet.h>
#include <net/gen/netdb.h>
#include <net/gen/tcp.h>
#include <net/gen/tcp_io.h>
#include <net/hton.h>

/* This program can be compiled to be paranoid, i.e. check incoming connection
 * according to an access file, or to trust anyone.  The much smaller "trust
 * 'em" binary will call the paranoid version if the access file exists.
 */

static char *arg0, *service;
static unsigned nchildren;

static void report(const char *label)
{
	int err= errno;

	fprintf(stderr, "%s %s: %s: %s\n", arg0, service, label, strerror(err));
	errno= err;
}

static void sigchld(int sig)
{
	while (waitpid(0, NULL, WNOHANG) > 0) {
		if (nchildren > 0) nchildren--;
	}
}

static void release(int *fd)
{
	if (*fd != -1) {
		close(*fd);
		*fd= -1;
	}
}

static void usage(void)
{
	fprintf(stderr,
		"Usage: %s [-d] [-m maxclients] service program [arg ...]\n",
		arg0);
	exit(1);
}

int main(int argc, char **argv)
{
	tcpport_t port;
	struct nwio_tcpcl tcplistenopt;
	struct nwio_tcpconf tcpconf;
	struct nwio_tcpopt tcpopt;
	struct servent *servent;
	int tcp_fd, client_fd, count, r;
	int pfd[2];
	unsigned stall= 0;
	struct sigaction sa;
	sigset_t chldmask, chldunmask;
	char **progv;

#if !PARANOID
#	define debug 0
#	define max_children ((unsigned) -1)
	arg0= argv[0];

	/* Switch to the paranoid version of me if there are flags, or if
	 * there is an access file.
	 */
	if (argv[1][0] == '-' || access(_PATH_SERVACCES, F_OK) == 0) {
		execv("/usr/bin/tcpdp", argv);
		report("tcpdp");
		exit(1);
	}
	if (argc < 3) usage();
	service= argv[1];
	progv= argv+2;

#else /* PARANOID */
	int debug, i;
	unsigned max_children;

	arg0= argv[0];
	debug= 0;
	max_children= -1;
	i= 1;
	while (i < argc && argv[i][0] == '-') {
		char *opt= argv[i++] + 1;
		unsigned long m;
		char *end;

		if (*opt == '-' && opt[1] == 0) break;	/* -- */

		while (*opt != 0) switch (*opt++) {
		case 'd':
			debug= 1;
			break;
		case 'm':
			if (*opt == 0) {
				if (i == argc) usage();
				opt= argv[i++];
			}
			m= strtoul(opt, &end, 10);
			if (m <= 0 || m > UINT_MAX || *end != 0) usage();
			max_children= m;
			opt= "";
			break;
		default:
			usage();
		}
	}
	service= argv[i++];
	progv= argv+i;
	if (i >= argc) usage();
#endif

	/* Let SIGCHLD interrupt whatever I'm doing. */
	sigemptyset(&chldmask);
	sigaddset(&chldmask, SIGCHLD);
	sigprocmask(SIG_BLOCK, &chldmask, &chldunmask);
	sigdelset(&chldunmask, SIGCHLD);
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = 0;
	sa.sa_handler = sigchld;
	sigaction(SIGCHLD, &sa, NULL);

	/* Open a socket to the service I'm to serve. */
	if ((servent= getservbyname(service, "tcp")) == NULL) {
		unsigned long p;
		char *end;

		p= strtoul(service, &end, 0);
		if (p <= 0 || p > 0xFFFF || *end != 0) {
			fprintf(stderr, "%s: %s: Unknown service\n",
				arg0, service);
			exit(1);
		}
		port= htons((tcpport_t) p);
	} else {
		port= servent->s_port;

		if (debug)
		{
			fprintf(stderr, "%s %s: listening to port %u\n",
				arg0, service, ntohs(port));
		}
	}

	/* No client yet. */
	client_fd= -1;

	while (1) {
		if ((tcp_fd= open("/dev/tcp", O_RDWR)) < 0) {
			report("Can't open /dev/tcp");
			if (errno == ENOENT || errno == ENODEV
							|| errno == ENXIO) {
				exit(1);
			}
			goto bad;
		}

		tcpconf.nwtc_flags= NWTC_LP_SET | NWTC_UNSET_RA | NWTC_UNSET_RP;
		tcpconf.nwtc_locport= port;

		if (ioctl(tcp_fd, NWIOSTCPCONF, &tcpconf) < 0) {
			report("Can't configure TCP channel");
			exit(1);
		}

		tcpopt.nwto_flags= NWTO_DEL_RST;

		if (ioctl(tcp_fd, NWIOSTCPOPT, &tcpopt) < 0) {
			report("Can't set TCP options");
			exit(1);
		}

		if (client_fd != -1) {
			/* We have a client, so start a server for it. */

			tcpopt.nwto_flags= 0;
			(void) ioctl(client_fd, NWIOSTCPOPT, &tcpopt);

			fflush(NULL);

			/* Create a pipe to serve as an error indicator. */
			if (pipe(pfd) < 0) {
				report("pipe");
				goto bad;
			}
			(void) fcntl(pfd[1], F_SETFD,
					fcntl(pfd[1], F_GETFD) | FD_CLOEXEC);

			/* Fork and exec. */
			switch (fork()) {
			case -1:
				report("fork");
				close(pfd[0]);
				close(pfd[1]);
				goto bad;
			case 0:
				close(tcp_fd);
				close(pfd[0]);
#if PARANOID
				/* Check if access to this service allowed. */
				if (!servxcheck(client_fd, argv[1], NULL))
					exit(1);
#endif
				dup2(client_fd, 0);
				dup2(client_fd, 1);
				close(client_fd);
				execvp(progv[0], progv);
				report(progv[0]);
				write(pfd[1], &errno, sizeof(errno));
				exit(1);
			default:
				nchildren++;
				release(&client_fd);
				close(pfd[1]);
				r= read(pfd[0], &errno, sizeof(errno));
				close(pfd[0]);
				if (r != 0) goto bad;
				break;
			}
		}

		while (nchildren >= max_children) {
			/* Too many clients, wait for one to die off. */
			sigsuspend(&chldunmask);
		}

		/* Wait for a new connection. */
		sigprocmask(SIG_UNBLOCK, &chldmask, NULL);

		tcplistenopt.nwtcl_flags= 0;
		while (ioctl(tcp_fd, NWIOTCPLISTEN, &tcplistenopt) < 0) {
			if (errno != EINTR) {
				if (errno != EAGAIN || debug) {
					report("Unable to listen");
				}
				goto bad;
			}
		}
		sigprocmask(SIG_BLOCK, &chldmask, NULL);

		/* We got a connection. */
		client_fd= tcp_fd;
		tcp_fd= -1;

		if (debug && ioctl(client_fd, NWIOGTCPCONF, &tcpconf) == 0) {
			fprintf(stderr, "%s %s: Connection from %s:%u\n",
				arg0, service,
				inet_ntoa(tcpconf.nwtc_remaddr),
				ntohs(tcpconf.nwtc_remport));
		}
		/* All is well, no need to stall. */
		stall= 0;
		continue;

	bad:
		/* All is not well, release resources. */
		release(&tcp_fd);
		release(&client_fd);

		/* Wait a bit if this happens more than once. */
		if (stall != 0) {
			if (debug) {
				fprintf(stderr, "%s %s: stalling %u second%s\n",
					arg0, service,
					stall, stall == 1 ? "" : "s");
			}
			sleep(stall);
			stall <<= 1;
		} else {
			stall= 1;
		}
	}
}
