/*
 * Copyright (c) 1994-2000 Begemot Computer Associates
 * Copyright (c) 2000-2002 by Hartmut Brandt
 *	All rights reserved.
 *
 * Redistribution of this software and documentation and use in source and
 * binary forms, with or without modification, are permitted provided that
 * the following conditions are met:
 *
 * 1. Redistributions of source code or documentation must retain the above
 *    copyright notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *
 * This product includes software and documentation developed by the
 * Hartmut Brandt.
 *
 * 4. The name of the author or its contributors may not be used to endorse
 *    or promote products derived from this software without specific prior
 *    written permission.
 *
 * THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE AUTHOR FOKUS
 * AND ITS CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * THE AUTHOR OR ITS CONTRIBUTORS  BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: tty_net.c 489 2002-09-03 15:11:03Z hbb $
 *
 * Author: Harti Brandt <brandt@fokus.gmd.de>
*
 * dev_nty - network tty
 *
 * listen on port given on command line for incoming
 * tcp connections.
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <netinet/in.h>
#include <signal.h>
#include <arpa/telnet.h>
#include <arpa/inet.h>
#include <begemot.h>

#include "cdefs.h"
#include "util.h"
#include "tty.h"

RCSID("$Id: tty_net.c 489 2002-09-03 15:11:03Z hbb $")

#define D(C)	/* C */

static pid_t	ppid;			/* parent */
static int	sock;			/* listening socket */
static int	conn;			/* connected socket */
static int	telnet;			/* do telnet option negotiation */
static int 	port;
static char	*logfname;
static pid_t	cpid;			/* child pid */
static pid_t	mypid;			/* server pid */
static struct sockaddr_in peer;

#define TO_SGA 	001	/* suppress go-ahead */
#define TO_ECHO	002	/* echo */
#define TO_BIN		004	/* binary */

static int	cwill;	/* telnet said WILL */
static int	cwont;	/* telnet said WON'T */
static int	cdo;	/* telnet said DO */
static int	cdont;	/* telnet said DON'T */
static int	cs7;

static char usgtxt[] =
"Begemot PDP11 emulator Version %s; Terminal-to-network adapter\n"
"Usage: tty_net [-l <file>] [-t] [-7] port\n"
"where:\n"
"	-l file	write log records to the file\n"
"	-t	use telnet option handling\n"
"	-7	mask to 7bits\n";

static void usage(int) DEAD_FUNC;

static void
usage(int e)
{
	fprintf(stderr, usgtxt, VERSION);
	exit(e);
}

/*
 * append line to logfile
 * try to lock it (is is reported that locking over NFS doesn't work so
 * ignore any errors)
 */
static void
logevent(char *fmt, ...)
{
	va_list ap;
	time_t	t;
	char	lbuf[400];
	char	obuf[400];
	int	fd;
	struct flock lock;

	if (!logfname)
		return;

	(void)time( &t );
	(void)strftime(lbuf, sizeof(lbuf), "%d/%m/%Y %T", localtime(&t) );
	(void)sprintf(lbuf+strlen(lbuf)," (%ld,%ld,%ld)", (long)ppid, (long)mypid, (long)cpid);
	(void)sprintf(obuf, "%-36s %5d ", lbuf, port);

	va_start(ap, fmt);
	(void)vsprintf(obuf+strlen(obuf), fmt, ap);
	(void)strcat(obuf, "\n");
	va_end(ap);

	if ((fd = open(logfname, O_WRONLY | O_APPEND | O_CREAT, 0666)) < 0)
		return;

	lock.l_whence = 0;
	lock.l_start = 0;
	lock.l_len = 0;
	lock.l_type = F_WRLCK;
	fcntl(fd, F_SETLKW, &lock);
	write(fd, obuf, strlen(obuf));
	close(fd);
}

void
Exit(int e)
{
	if (getpid() == mypid) {		/* parent server */
		logevent("offline %d", e);
		if (cpid) {
			logevent("%s,%d disconnect", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
			kill(cpid, SIGTERM);
		}
		close(sock);
		exit(e);
	} else {			/* child */
		close(conn);
		_exit(e);
	}
	exit(1994);	/* keep dumb gcc quiet */
}

/*
 * child died, wait
 */
static void
onsigchld(int s UNUSED)
{
	int	status;
	pid_t	p;

	while ((p = wait(&status)) != -1 || errno == EINTR)
		if (p == cpid) {
			logevent("%s,%d disconnect", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
			cpid = 0;
		}
}

static int
init_port(int sport)
{
	int	s;	/* socket we are listening on		*/
	int	on = 1;
	struct sockaddr_in sain;

	if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		error("socket(): %s", strerror(errno));
		Exit(5);
	}
	sain.sin_family = AF_INET;
	sain.sin_port = htons(sport);
	sain.sin_addr.s_addr = INADDR_ANY;
#ifdef HAVE_SIN_LEN
	sain.sin_len = sizeof(sain);
#endif
	if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on))) {
		error("setsockopt(): %s", strerror(errno));
		Exit(60);
	}
	if (bind(s, (struct sockaddr *)&sain, sizeof(sain))) {
		error("bind(): %s", strerror(errno));
		Exit(6);
	}
	if (listen(s, 5)) {
		error("listen(): %s", strerror(errno));
		Exit(7);
	}

	return (s);
}

/*
 * try to switch client into binary char mode
 * give up after a reasonable time
 *
 * this is an only partial implemented telnet state machine
 * the main machine is almost complete, but the do/dont/will/wont
 * mechanism is striped down. we assume, that the telnet client doesn't
 * do any option negotiation on his own (because he doesn't know the port
 * number) so any received will/do is the answer for one of our will/do's.
 * we ignore all other commands and time out if we can't get the desired state.
 */
enum {
	TS_DATA,
	TS_IAC,
	TS_WILL,
	TS_DO,
	TS_WONT,
	TS_DONT,
	TS_SB,
	TS_SE,
};

static char	subbuf[512];
static char	*subptr;

#define SUBOPT_START()	subptr = subbuf
#define SUBOPT_CHAR(C)	(void)((subptr < subbuf + sizeof(subbuf)) ? (*subptr++ = (C)) : 0)

static void
bwrite(int fd, int n, ...)
{
	va_list	ap;
	u_char	c;

	va_start(ap, n);
	while (n--) {
		c = (u_char)va_arg(ap, int);
		write(fd, &c, 1);
	}
	va_end(ap);
}

static void
swrite(int fd, char *s)
{
	write(fd, "p11: ", 5);
	write(fd, s, strlen(s));
	write(fd, "\r\n", 2);
}

static void
send_will(u_char c)
{
	bwrite(conn, 3, IAC, WILL, c);
}

static void
send_wont(u_char c)
{
	bwrite(conn, 3, IAC, WONT, c);
}

static void
send_dont(u_char c)
{
	bwrite(conn, 3, IAC, DONT, c);
}

static void
send_do(u_char c)
{
	bwrite(conn, 3, IAC, DO, c);
}

/*
 * client requests us to switch option on
 */
static void
doo(u_char c)
{
	switch (c) {

	  case TELOPT_BINARY:
		cdo |= TO_BIN;
		break;

	  case TELOPT_ECHO:
		cdo |= TO_ECHO;
		break;

	  case TELOPT_SGA:
		cdo |= TO_SGA;
		break;

	  default:
		send_wont(c);
		break;
	}
}

static void
dont(u_char c)
{
	switch (c) {

	  case TELOPT_BINARY:
		cdont |= TO_BIN;
		break;

	  case TELOPT_ECHO:
		cdont |= TO_ECHO;
		break;

	  case TELOPT_SGA:
		cdont |= TO_SGA;
		break;

	  default:
		send_wont(c);
		break;
	}
}

static void
will(u_char c)
{
	switch (c) {

	  case TELOPT_BINARY:
		cwill |= TO_BIN;
		break;

	  default:
		send_dont(c);
		break;
	}
}

static void
wont(u_char c)
{
	switch (c) {

	  case TELOPT_BINARY:
		cwont |= TO_BIN;
		break;

	  default:
		send_dont(c);
		break;
	}
}

static void	
subopt(void)
{
}

static int
telnet_options(void)
{
	int	state;
	int	tgot;
	u_char	c;
	u_long	iticks = ticks;
	fd_set	iset, oset;

	cwill = cwont = cdo = cdont = 0;

	FD_ZERO(&iset);
	FD_SET(conn, &iset);

	send_will(TELOPT_SGA);
	send_will(TELOPT_ECHO);
	send_will(TELOPT_BINARY);
	send_do(TELOPT_BINARY);

	state = TS_DATA;
	while ((cwill | cwont) != (TO_BIN) ||
	    (cdo | cdont) != (TO_SGA|TO_ECHO|TO_BIN)) {
		oset = iset;
		if ((tgot = select(conn + 1, &oset, 0, 0, 0)) < 0) {
			if (errno != EINTR)
				return (0);
			if (ticks > iticks + 3)
				break;
			continue;
		} else if (tgot == 0) {
			error("select(): %s", strerror(errno));
			Exit(77);		/* should not happen */
		}

		if ((tgot = read(conn, &c, 1)) <= 0)
			return (0);

		switch (state) {

		  case TS_DATA:
			if (c == IAC)
				state = TS_IAC;
			/* drop any data */
			break;

		  case TS_IAC:
			switch (c) {

			  case TS_IAC:
				/* drop any data */
				break;

			  case SB:
				SUBOPT_START();
				state = TS_SB;
				break;

			  case WILL:
				state = TS_WILL;
				break;

			  case WONT:
				state = TS_WONT;
				break;

			  case DONT:
				state = TS_DONT;
				break;

			  case DO:
				state = TS_DO;
				break;

			  default:
				state = TS_DATA;
				break;
			}
			break;

		  case TS_WILL:
			will(c);
			state = TS_DATA;
			break;

		  case TS_DO:
			doo(c);
			state = TS_DATA;
			break;

		  case TS_WONT:
			wont(c);
			state = TS_DATA;
			break;

		  case TS_DONT:
			dont(c);
			state = TS_DATA;
			break;

		  case TS_SB:
			if (c == IAC)
				state = TS_SE;
			else
				SUBOPT_CHAR(c);
			break;

		  case TS_SE:
			if (c == SE) {
				subopt();
				state = TS_DATA;
			} else if (c == IAC) {
				SUBOPT_CHAR(c);
				state = TS_SB;
			} else {			/* error */
				subopt();
				state = TS_IAC;
			}
			break;

		  default:
			abort();
		}
	}

	if (cdont & TO_SGA)
		swrite(conn, "warning: can't generate go-aheads");
	if (cdont & TO_ECHO)
		swrite(conn, "warning: WILL do echo anyway");
	if (cdont & TO_BIN)
		swrite(conn, "warning: switching to NVT ASCII output");
	if (cwont & TO_BIN)
		swrite(conn, "warning: switching to NVT ASCII input");

	return (1);
}

static void
acceptconn(void)
{
	int rcvlowat = 1;

	if ((cpid = fork()) < 0) {
		error("fork(): %s", strerror(errno));
		Exit(21);
	}
	if (cpid != 0) 	/* parent */
		return;

	if (setsockopt(conn, SOL_SOCKET, SO_RCVLOWAT,
	    (const char *)&rcvlowat, sizeof(rcvlowat))) {
		/*
		 * Not changeable on Lunix
		 */
		if (errno != ENOPROTOOPT) {
			error("setsockopt(): %s", strerror(errno));
			Exit(10);
		}
	}

	if (!telnet || telnet_options())
		tty_loop(conn);
	_exit(0);
}

static void
dontaccept(void)
{
	swrite(conn, "Sorry. This port is in use - try another one.\n");
}

int
main(int argc, char *argv[])
{
	char 	*end;
	char	tbuf[1024];
	int	opt;
	D(FILE *dbg);

	D((sprintf(tbuf, "nty.%d", (int)getpid())));
	D((dbg = fopen(tbuf, "w")));
	D((setvbuf(dbg, 0, _IONBF, 0)));

	while ((opt = getopt(argc, argv, "hl:t7")) != -1)
		switch (opt) {

		  case 'h':
			usage(0);

		  case 't':
			telnet++;
			break;

		  case '7':
			cs7 = 1;
			break;

		  case 'l':
			logfname = optarg;
			break;
		}

	argc -= optind;
	argv += optind;

	if (argc != 1) {
		error("bad number of args");
		usage(1);
	}
	port = strtol(argv[0], &end, 0);
	if (*end != 0) {
		error("bad port number '%s'", argv[0]);
		usage(2);
	}

	mypid = getpid();
	tty_init();

	if (setsid() < 0) {
		error("setsid(): %s", strerror(errno));
		exit(3);
	}

	catch_signal(SIGPIPE, SIG_IGN);
	catch_signal(SIGCHLD, onsigchld);

	sock = init_port(port);

	if (fcntl(0, F_SETFL, O_NONBLOCK)) {
		error("fcntl(O_NONBLOCK): %s", strerror(errno));
		exit(4);
	}

	D((fprintf(dbg, "starting main loop\n")));
	logevent("online");
	for (;;) {
		fd_set	irset, rset;
		int 	err;
		int	plen;
		struct sockaddr_in npeer;

		/*
	 	 * wait for incomming connection, discard output from emulgator
		 * if no child
	 	 */	
		FD_ZERO(&irset);
		FD_SET(sock, &irset);
		if (cpid == 0)
			FD_SET(0, &irset);

		D((fprintf(dbg, "listening ...\n")));
		while (rset = irset, (err = select(FD_SETSIZE, &rset, 0, 0, 0)) >= 0 || errno == EINTR) 
			if (err > 0) {
				if (FD_ISSET(0, &rset)) {
					while (read(0, tbuf, sizeof(tbuf)) > 0)
						;
				}
				if (FD_ISSET(sock, &rset))
					break;
			}


		/*
		 * got connection request - accept or not
		 */
		plen = sizeof(npeer);
		if ((conn = accept(sock, (struct sockaddr *)&npeer, &plen)) < 0)
			continue;

		if (cpid == 0) {
			peer = npeer;
			logevent("%s,%d connect", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
			acceptconn();
		} else {
			logevent("%s,%d connection rejected", inet_ntoa(npeer.sin_addr), ntohs(npeer.sin_port));
			dontaccept();
		}
		close(conn);
	}
}

/*
 * out/input processing
 */
void
process_input()
{
	if (cwont & TO_BIN)
		nvt2bin();
	if (cwill || cwont || cdo || cdont)
		deliacs();
}

void
process_output()
{
	if (cs7)
		makecs7();
	if (cdont & TO_BIN)
		bin2nvt();
	if (cwill || cwont || cdo || cdont)
		insiacs();
}

void
process_end()
{
}
