#include <u.h>
#include <libc.h>
#include <bio.h>
#include <auth.h>
#include <libsec.h>

#include "../ip/telnet.h"

/*  console state (for consctl) */
typedef struct Consstate	Consstate;
struct Consstate{
	int raw;
	int hold;
};
Consstate *cons;

int notefd;		/* for sending notes to the child */
int noproto;		/* true if we shouldn't be using the telnet protocol */
int trusted;		/* true if we need not authenticate - current user
				is ok */
int nonone = 1;		/* don't allow none logins */
int noworldonly;	/* only noworld accounts */

enum
{
	Maxpath=	256,
	Maxuser=	64,
	Maxvar=		32,
};

/* input and output buffers for network connection */
Biobuf	netib;
Biobuf	childib;
char	remotesys[Maxpath];	/* name of remote system */

int	alnum(int);
int	conssim(void);
int	fromchild(char*, int);
int	fromnet(char*, int);
int	termchange(Biobuf*, int);
int	termsub(Biobuf*, uchar*, int);
int	xlocchange(Biobuf*, int);
int	xlocsub(Biobuf*, uchar*, int);
int	challuser(char*);
int	noworldlogin(char*);
void*	share(ulong);
int	doauth(char*);

#define TELNETLOG "telnet"

void
logit(char *fmt, ...)
{
	va_list arg;
	char buf[8192];

	va_start(arg, fmt);
	vseprint(buf, buf + sizeof(buf) / sizeof(*buf), fmt, arg);
	va_end(arg);
	syslog(0, TELNETLOG, "(%s) %s", remotesys, buf);
}

void
getremote(char *dir)
{
	int fd, n;
	char remfile[Maxpath];

	snprint(remfile, sizeof(remfile), "%s/remote", dir);
	fd = open(remfile, OREAD);
	if(fd < 0){
		strcpy(remotesys, remfile);
		return;
	}
	n = read(fd, remotesys, sizeof(remotesys)-1);
	if(n > 0)
		remotesys[n-1] = 0;
	else
		strcpy(remotesys, remfile);
	close(fd);
}

void
main(int argc, char *argv[])
{
	char buf[1024];
	int fd;
	char user[Maxuser];
	int tries = 0;
	int childpid;
	int n, eofs;

	memset(user, 0, sizeof(user));
	ARGBEGIN {
	case 'n':
		opt[Echo].local = 1;
		noproto = 1;
		break;
	case 'p':
		noproto = 1;
		break;
	case 'a':
		nonone = 0;
		break;
	case 't':
		trusted = 1;
		strncpy(user, getuser(), sizeof(user)-1);
		break;
	case 'u':
		strncpy(user, ARGF(), sizeof(user)-1);
		break;
	case 'd':
		debug = 1;
		break;
	case 'N':
		noworldonly = 1;
		break;
	} ARGEND

	if(argc)
		getremote(argv[argc-1]);
	else
		strcpy(remotesys, "unknown");

	/* options we need routines for */
	opt[Term].change = termchange;
	opt[Term].sub = termsub;
	opt[Xloc].sub = xlocsub;

	/* setup default telnet options */
	if(!noproto){
		send3(1, Iac, Will, opt[Echo].code);
		send3(1, Iac, Do, opt[Term].code);
		send3(1, Iac, Do, opt[Xloc].code);
	}

	/* shared data for console state */
	cons = share(sizeof(Consstate));
	if(cons == 0)
		fatal("shared memory", 0, 0);

	/* authenticate and create new name space */
	Binit(&netib, 0, OREAD);
	if (!trusted){
		while(doauth(user) < 0)
			if(++tries == 5){
				logit("failed as %s: %r", user);
				print("authentication failure:%r\r\n");
				exits("authentication");
			}
	}
	logit("logged in as %s", user);
	putenv("service", "con");

	/* simulate /dev/consctl and /dev/cons using pipes */
	fd = conssim();
	if(fd < 0)
		fatal("simulating", 0, 0);
	Binit(&childib, fd, OREAD);

	/* start a shell in a different process group */
	switch(childpid = rfork(RFPROC|RFNAMEG|RFFDG|RFNOTEG)){
	case -1:
		fatal("fork", 0, 0);
	case 0:
		close(fd);
		fd = open("/dev/cons", OREAD);
		dup(fd, 0);
		close(fd);
		fd = open("/dev/cons", OWRITE);
		dup(fd, 1);
		dup(fd, 2);
		close(fd);
		segdetach(cons);
		execl("/bin/rc", "rc", "-il", nil);
		fatal("/bin/rc", 0, 0);
	default:
		sprint(buf, "/proc/%d/notepg", childpid);
		notefd = open(buf, OWRITE);
		break;
	}

	/* two processes to shuttle bytes twixt children and network */
	switch(fork()){
	case -1:
		fatal("fork", 0, 0);
	case 0:
		eofs = 0;
		for(;;){
			n = fromchild(buf, sizeof(buf));
			if(n <= 0){
				if(eofs++ > 2)
					break;
				continue;
			}
			eofs = 0;
			if(write(1, buf, n) != n)
				break;
		}
		break;
	default:
		while((n = fromnet(buf, sizeof(buf))) >= 0)
			if(write(fd, buf, n) != n)
				break;
		break;
	}

	/* kill off all server processes */
	sprint(buf, "/proc/%d/notepg", getpid());
	fd = open(buf, OWRITE);
	write(fd, "die", 3);
	exits(0);
}

void
prompt(char *p, char *b, int n, int raw)
{
	char *e;
	int i;
	int echo;

	echo = opt[Echo].local;
	if(raw)
		opt[Echo].local = 0;
	print("%s: ", p);
	for(e = b+n; b < e;){
		i = fromnet(b, e-b);
		if(i <= 0)
			exits("fromnet: hungup");
		b += i;
		if(*(b-1) == '\n' || *(b-1) == '\r'){
			*(b-1) = 0;
			break;
		}
	}
	if(raw)
		opt[Echo].local = echo;
}

/*
 *  challenge user
 */
int
challuser(char *user)
{
	char nchall[64];
	char response[64];
	Chalstate *ch;
	AuthInfo *ai;
	Dir nd;

	if(strcmp(user, "none") == 0){
		if(nonone)
			return -1;
		newns("none", nil);
		return 0;
	}
	if((ch = auth_challenge("proto=p9cr role=server user=%q", user)) == nil)
		return -1;
	snprint(nchall, sizeof nchall, "challenge: %s\r\nresponse", ch->chal);
	prompt(nchall, response, sizeof response, 0);
	ch->resp = response;
	ch->nresp = strlen(response);
	ai = auth_response(ch);
	auth_freechal(ch);
	if(ai == nil || auth_chuid(ai, nil) < 0){
		rerrstr(response, sizeof response);
		print("!%s\n", response);

		auth_freeAI(ai);
		return -1;
	}
	/* chown network connection */
	nulldir(&nd);
	nd.mode = 0660;
	nd.uid = ai->cuid;
	dirfwstat(0, &nd);

	auth_freeAI(ai);
	return 0;
}
/*
 *  use the in the clear apop password to change user id
 */
int
noworldlogin(char *user)
{
	char password[256];

	prompt("password", password, sizeof(password), 1);
	if(login(user, password, "/lib/namespace.noworld") < 0)
		return -1;
	rfork(RFNOMNT);	/* sandbox */
	return 0;
}

int
doauth(char *user)
{
	if(*user == 0)
		prompt("user", user, Maxuser, 0);
	if(noworld(user))
		return noworldlogin(user);
	if(noworldonly)
		return -1;
	return challuser(user);
		
}

/*
 *  Process some input from the child, add protocol if needed.  If
 *  the input buffer goes empty, return.
 */
int
fromchild(char *bp, int len)
{
	int c;
	char *start;

	for(start = bp; bp-start < len-1; ){
		c = Bgetc(&childib);
		if(c < 0){
			if(bp == start)
				return -1;
			else
				break;
		}
		if(cons->raw == 0 && c == '\n')
			*bp++ = '\r';
		*bp++ = c;
		if(Bbuffered(&childib) == 0)
			break;
	}
	return bp-start;
}

/*
 *  Read from the network up to a '\n' or some other break.
 *
 *  If in binary mode, buffer characters but don't 
 *
 *  The following characters are special:
 *	'\r\n's and '\r's get turned into '\n's.
 *	^H erases the last character buffered.
 *	^U kills the whole line buffered.
 *	^W erases the last word
 *	^D causes a 0-length line to be returned.
 *	Intr causes an "interrupt" note to be sent to the children.
 */
#define ECHO(c) { *ebp++ = (c); }
int
fromnet(char *bp, int len)
{
	int c;
	char echobuf[1024];
	char *ebp;
	char *start;
	static int crnl;
	static int doeof;


	/* simulate an EOF as a 0 length input */
	if(doeof){
		doeof = 0;
		return 0;
	}

	for(ebp = echobuf,start = bp; bp-start < len && ebp-echobuf < sizeof(echobuf); ){
		c = Bgetc(&netib);
		if(c < 0){
			if(bp == start)
				return -1;
			else
				break;
		}

		/* telnet protocol only */
		if(!noproto){
			/* protocol messages */
			switch(c){
			case Iac:
				crnl = 0;
				c = Bgetc(&netib);
				if(c == Iac)
					break;
				control(&netib, c);
				continue;
			}

		}

		/* \r\n or \n\r become \n  */
		if(c == '\r' || c == '\n'){
			if(crnl && crnl != c){
				crnl = 0;
				continue;
			}
			if(cons->raw == 0 && opt[Echo].local){
				ECHO('\r');
				ECHO('\n');
			}
			crnl = c;
			if(cons->raw == 0)
				*bp++ = '\n';
			else
				*bp++ = c;
			break;
		} else
			crnl = 0;

		/* raw processing (each character terminates */
		if(cons->raw){
			*bp++ = c;
			break;
		}

		/* in binary mode, there are no control characters */
		if(opt[Binary].local){
			if(opt[Echo].local)
				ECHO(c);
			*bp++ = c;
			continue;
		}

		/* cooked processing */
		switch(c){
		case 0x00:
			if(noproto)		/* telnet ignores nulls */
				*bp++ = c;
			continue;
		case 0x04:
			if(bp != start)
				doeof = 1;
			goto out;

		case 0x08:	/* ^H */
			if(start < bp)
				bp--;
			if(opt[Echo].local)
				ECHO(c);
			break;

		case 0x15:	/* ^U */
			bp = start;
			if(opt[Echo].local){
				ECHO('^');
				ECHO('U');
				ECHO('\r');
				ECHO('\n');
			}
			break;

		case 0x17:	/* ^W */
			if (opt[Echo].local) {
				while (--bp >= start && !alnum(*bp))
					ECHO('\b');
				while (bp >= start && alnum(*bp)) {
					ECHO('\b');
					bp--;
				}
				bp++;
			}
			break;

		case 0x7f:	/* Del */
			write(notefd, "interrupt", 9);
			bp = start;
			break;

		default:
			if(opt[Echo].local)
				ECHO(c);
			*bp++ = c;
		}
		if(ebp != echobuf)
			write(1, echobuf, ebp-echobuf);
		ebp = echobuf;
	}
out:
	if(ebp != echobuf)
		write(1, echobuf, ebp-echobuf);
	return bp - start;
}

int
termchange(Biobuf *bp, int cmd)
{
	char buf[8];
	char *p = buf;

	if(cmd != Will)
		return 0;

	/* ask other side to send term type info */
	*p++ = Iac;
	*p++ = Sb;
	*p++ = opt[Term].code;
	*p++ = 1;
	*p++ = Iac;
	*p++ = Se;
	return iwrite(Bfildes(bp), buf, p-buf);
}

int
termsub(Biobuf *bp, uchar *sub, int n)
{
	char term[Maxvar];

	USED(bp);
	if(n-- < 1 || sub[0] != 0)
		return 0;
	if(n >= sizeof term)
		n = sizeof term;
	strncpy(term, (char*)sub, n);
	putenv("TERM", term);
	return 0;
}

int
xlocchange(Biobuf *bp, int cmd)
{
	char buf[8];
	char *p = buf;

	if(cmd != Will)
		return 0;

	/* ask other side to send x display info */
	*p++ = Iac;
	*p++ = Sb;
	*p++ = opt[Xloc].code;
	*p++ = 1;
	*p++ = Iac;
	*p++ = Se;
	return iwrite(Bfildes(bp), buf, p-buf);
}

int
xlocsub(Biobuf *bp, uchar *sub, int n)
{
	char xloc[Maxvar];

	USED(bp);
	if(n-- < 1 || sub[0] != 0)
		return 0;
	if(n >= sizeof xloc)
		n = sizeof xloc;
	strncpy(xloc, (char*)sub, n);
	putenv("DISPLAY", xloc);
	return 0;
}

/*
 *  create a shared segment.
 */
void*
share(ulong len)
{
	void *v;

	v = segattach(0, "shared", 0, len);
	if(v == (void*)-1)
		return nil;
	return v;
}

/*
 *  bind a pipe onto consctl and keep reading it to
 *  get changes to console state.
 */
int
conssim(void)
{
	int i, n;
	int fd;
	int tries;
	char buf[128];
	char *field[10];

	/* a pipe to simulate the /dev/cons */
	if(bind("#|", "/mnt/cons", MREPL) == -1)
		fatal("/dev/cons1", 0, 0);
	if(bind("/mnt/cons/data1", "/dev/cons", MREPL) == -1)
		fatal("/dev/cons2", 0, 0);

	/* a pipe to simulate consctl */
	if(bind("#|", "/mnt/consctl", MBEFORE) == -1
	|| bind("/mnt/consctl/data1", "/dev/consctl", MREPL) == -1)
		fatal("/dev/consctl", 0, 0);

	/* a process to read /dev/consctl and set the state in cons */
	switch(fork()){
	case -1:
		fatal("forking", 0, 0);
	case 0:
		break;
	default:
		return open("/mnt/cons/data", ORDWR);
	}

	for(tries = 0; tries < 100; tries++){
		cons->raw = 0;
		cons->hold = 0;
		fd = open("/mnt/consctl/data", OREAD);
		if(fd < 0)
			continue;
		tries = 0;
		for(;;){
			n = read(fd, buf, sizeof(buf)-1);
			if(n <= 0)
				break;
			buf[n] = 0;
			n = getfields(buf, field, 10, 1, " ");
			for(i = 0; i < n; i++){
				if(strcmp(field[i], "rawon") == 0) {
					if(debug) fprint(2, "raw = 1\n");
					cons->raw = 1;
				} else if(strcmp(field[i], "rawoff") == 0) {
					if(debug) fprint(2, "raw = 0\n");
					cons->raw = 0;
				} else if(strcmp(field[i], "holdon") == 0) {
					cons->hold = 1;
					if(debug) fprint(2, "raw = 1\n");
				} else if(strcmp(field[i], "holdoff") == 0) {
					cons->hold = 0;
					if(debug) fprint(2, "raw = 0\n");
				}
			}
		}
		close(fd);
	}
	exits(0);
}

int
alnum(int c)
{
	/*
	 * Hard to get absolutely right.  Use what we know about ASCII
	 * and assume anything above the Latin control characters is
	 * potentially an alphanumeric.
	 */
	if(c <= ' ')
		return 0;
	if(0x7F<=c && c<=0xA0)
		return 0;
	if(strchr("!\"#$%&'()*+,-./:;<=>?@`[\\]^{|}~", c))
		return 0;
	return 1;
}
