/* vi: set sw=4 ts=4: */
/*
 * telnet implementation for busybox
 *
 * Author: Tomi Ollila <too@iki.fi>
 * Copyright (C) 1994-2000 by Tomi Ollila
 *
 * Created: Thu Apr  7 13:29:41 1994 too
 * Last modified: Fri Jun  9 14:34:24 2000 too
 *
 * Licensed under the GPL v2 or later, see the file LICENSE in this tarball.
 *
 * HISTORY
 * Revision 3.1  1994/04/17  11:31:54  too
 * initial revision
 * Modified 2000/06/13 for inclusion into BusyBox by Erik Andersen <andersen@codepoet.org>
 * Modified 2001/05/07 to add ability to pass TTYPE to remote host by Jim McQuillan
 * <jam@ltsp.org>
 * Modified 2004/02/11 to add ability to pass the USER variable to remote host
 * by Fernando Silveira <swrh@gmx.net>
 *
 */

#include <termios.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <arpa/telnet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "busybox.h"

#ifdef DOTRACE
#include <arpa/inet.h> /* for inet_ntoa()... */
#define TRACE(x, y) do { if (x) printf y; } while (0)
#else
#define TRACE(x, y)
#endif

#define DATABUFSIZE  128
#define IACBUFSIZE   128

enum {
	CHM_TRY = 0,
	CHM_ON = 1,
	CHM_OFF = 2,

	UF_ECHO = 0x01,
	UF_SGA = 0x02,

	TS_0 = 1,
	TS_IAC = 2,
	TS_OPT = 3,
	TS_SUB1 = 4,
	TS_SUB2 = 5,
};

#define WriteCS(fd, str) write(fd, str, sizeof str -1)

typedef unsigned char byte;

/* use globals to reduce size ??? */ /* test this hypothesis later */
static struct Globalvars {
	int		netfd; /* console fd:s are 0 and 1 (and 2) */
    /* same buffer used both for network and console read/write */
	char    buf[DATABUFSIZE]; /* allocating so static size is smaller */
	byte	telstate; /* telnet negotiation state from network input */
	byte	telwish;  /* DO, DONT, WILL, WONT */
	byte    charmode;
	byte    telflags;
	byte	gotsig;
	byte	do_termios;
	/* buffer to handle telnet negotiations */
	char    iacbuf[IACBUFSIZE];
	short	iaclen; /* could even use byte */
	struct termios termios_def;
	struct termios termios_raw;
} G;

#define xUSE_GLOBALVAR_PTR /* xUSE... -> don't use :D (makes smaller code) */

#ifdef USE_GLOBALVAR_PTR
struct Globalvars * Gptr;
#define G (*Gptr)
#endif

static inline void iacflush(void)
{
	write(G.netfd, G.iacbuf, G.iaclen);
	G.iaclen = 0;
}

/* Function prototypes */
static void rawmode(void);
static void cookmode(void);
static void do_linemode(void);
static void will_charmode(void);
static void telopt(byte c);
static int subneg(byte c);

/* Some globals */
static int one = 1;

#ifdef CONFIG_FEATURE_TELNET_TTYPE
static char *ttype;
#endif

#ifdef CONFIG_FEATURE_TELNET_AUTOLOGIN
static const char *autologin;
#endif

#ifdef CONFIG_FEATURE_AUTOWIDTH
static int win_width, win_height;
#endif

static void doexit(int ev)
{
	cookmode();
	exit(ev);
}

static void conescape(void)
{
	char b;

	if (G.gotsig)	/* came from line  mode... go raw */
		rawmode();

	WriteCS(1, "\r\nConsole escape. Commands are:\r\n\n"
			" l	go to line mode\r\n"
			" c	go to character mode\r\n"
			" z	suspend telnet\r\n"
			" e	exit telnet\r\n");

	if (read(0, &b, 1) <= 0)
		doexit(1);

	switch (b)
	{
	case 'l':
		if (!G.gotsig)
		{
			do_linemode();
			goto rrturn;
		}
		break;
	case 'c':
		if (G.gotsig)
		{
			will_charmode();
			goto rrturn;
		}
		break;
	case 'z':
		cookmode();
		kill(0, SIGTSTP);
		rawmode();
		break;
	case 'e':
		doexit(0);
	}

	WriteCS(1, "continuing...\r\n");

	if (G.gotsig)
		cookmode();

 rrturn:
	G.gotsig = 0;

}
static void handlenetoutput(int len)
{
	/*	here we could do smart tricks how to handle 0xFF:s in output
	 *	stream  like writing twice every sequence of FF:s (thus doing
	 *	many write()s. But I think interactive telnet application does
	 *	not need to be 100% 8-bit clean, so changing every 0xff:s to
	 *	0x7f:s
	 *
	 *	2002-mar-21, Przemyslaw Czerpak (druzus@polbox.com)
	 *	I don't agree.
	 *	first - I cannot use programs like sz/rz
	 *	second - the 0x0D is sent as one character and if the next
	 *	         char is 0x0A then it's eaten by a server side.
	 *	third - whay doy you have to make 'many write()s'?
	 *	        I don't understand.
	 *	So I implemented it. It's realy useful for me. I hope that
	 *	others people will find it interesting to.
	 */

	int i, j;
	byte * p = (byte*)G.buf;
	byte outbuf[4*DATABUFSIZE];

	for (i = len, j = 0; i > 0; i--, p++)
	{
		if (*p == 0x1d)
		{
			conescape();
			return;
		}
		outbuf[j++] = *p;
		if (*p == 0xff)
		    outbuf[j++] = 0xff;
		else if (*p == 0x0d)
		    outbuf[j++] = 0x00;
	}
	if (j > 0 )
	    write(G.netfd, outbuf, j);
}


static void handlenetinput(int len)
{
	int i;
	int cstart = 0;

	for (i = 0; i < len; i++)
	{
		byte c = G.buf[i];

		if (G.telstate == 0) /* most of the time state == 0 */
		{
			if (c == IAC)
			{
				cstart = i;
				G.telstate = TS_IAC;
			}
		}
		else
			switch (G.telstate)
			 {
			 case TS_0:
				 if (c == IAC)
					 G.telstate = TS_IAC;
				 else
					 G.buf[cstart++] = c;
				 break;

			 case TS_IAC:
				 if (c == IAC) /* IAC IAC -> 0xFF */
				 {
					 G.buf[cstart++] = c;
					 G.telstate = TS_0;
					 break;
				 }
				 /* else */
				 switch (c)
				 {
				 case SB:
					 G.telstate = TS_SUB1;
					 break;
				 case DO:
				 case DONT:
				 case WILL:
				 case WONT:
					 G.telwish =  c;
					 G.telstate = TS_OPT;
					 break;
				 default:
					 G.telstate = TS_0;	/* DATA MARK must be added later */
				 }
				 break;
			 case TS_OPT: /* WILL, WONT, DO, DONT */
				 telopt(c);
				 G.telstate = TS_0;
				 break;
			 case TS_SUB1: /* Subnegotiation */
			 case TS_SUB2: /* Subnegotiation */
				 if (subneg(c))
					 G.telstate = TS_0;
				 break;
			 }
	}
	if (G.telstate)
	{
		if (G.iaclen)			iacflush();
		if (G.telstate == TS_0)	G.telstate = 0;

		len = cstart;
	}

	if (len)
		write(1, G.buf, len);
}


/* ******************************* */

static inline void putiac(int c)
{
	G.iacbuf[G.iaclen++] = c;
}


static void putiac2(byte wwdd, byte c)
{
	if (G.iaclen + 3 > IACBUFSIZE)
		iacflush();

	putiac(IAC);
	putiac(wwdd);
	putiac(c);
}

#ifdef CONFIG_FEATURE_TELNET_TTYPE
static void putiac_subopt(byte c, char *str)
{
	int	len = strlen(str) + 6;   // ( 2 + 1 + 1 + strlen + 2 )

	if (G.iaclen + len > IACBUFSIZE)
		iacflush();

	putiac(IAC);
	putiac(SB);
	putiac(c);
	putiac(0);

	while(*str)
		putiac(*str++);

	putiac(IAC);
	putiac(SE);
}
#endif

#ifdef CONFIG_FEATURE_TELNET_AUTOLOGIN
static void putiac_subopt_autologin(void)
{
	int len = strlen(autologin) + 6;	// (2 + 1 + 1 + strlen + 2)
	char *user = "USER";

	if (G.iaclen + len > IACBUFSIZE)
		iacflush();

	putiac(IAC);
	putiac(SB);
	putiac(TELOPT_NEW_ENVIRON);
	putiac(TELQUAL_IS);
	putiac(NEW_ENV_VAR);

	while(*user)
		putiac(*user++);

	putiac(NEW_ENV_VALUE);

	while(*autologin)
		putiac(*autologin++);

	putiac(IAC);
	putiac(SE);
}
#endif

#ifdef CONFIG_FEATURE_AUTOWIDTH
static void putiac_naws(byte c, int x, int y)
{
	if (G.iaclen + 9 > IACBUFSIZE)
		iacflush();

	putiac(IAC);
	putiac(SB);
	putiac(c);

	putiac((x >> 8) & 0xff);
	putiac(x & 0xff);
	putiac((y >> 8) & 0xff);
	putiac(y & 0xff);

	putiac(IAC);
	putiac(SE);
}
#endif

/* void putiacstring (subneg strings) */

/* ******************************* */

static char const escapecharis[] = "\r\nEscape character is ";

static void setConMode(void)
{
	if (G.telflags & UF_ECHO)
	{
		if (G.charmode == CHM_TRY) {
			G.charmode = CHM_ON;
			printf("\r\nEntering character mode%s'^]'.\r\n", escapecharis);
			rawmode();
		}
	}
	else
	{
		if (G.charmode != CHM_OFF) {
			G.charmode = CHM_OFF;
			printf("\r\nEntering line mode%s'^C'.\r\n", escapecharis);
			cookmode();
		}
	}
}

/* ******************************* */

static void will_charmode(void)
{
	G.charmode = CHM_TRY;
	G.telflags |= (UF_ECHO | UF_SGA);
	setConMode();

	putiac2(DO, TELOPT_ECHO);
	putiac2(DO, TELOPT_SGA);
	iacflush();
}

static void do_linemode(void)
{
	G.charmode = CHM_TRY;
	G.telflags &= ~(UF_ECHO | UF_SGA);
	setConMode();

	putiac2(DONT, TELOPT_ECHO);
	putiac2(DONT, TELOPT_SGA);
	iacflush();
}

/* ******************************* */

static inline void to_notsup(char c)
{
	if      (G.telwish == WILL)	putiac2(DONT, c);
	else if (G.telwish == DO)	putiac2(WONT, c);
}

static inline void to_echo(void)
{
	/* if server requests ECHO, don't agree */
	if      (G.telwish == DO) {	putiac2(WONT, TELOPT_ECHO);	return; }
	else if (G.telwish == DONT)	return;

	if (G.telflags & UF_ECHO)
	{
		if (G.telwish == WILL)
			return;
	}
	else
		if (G.telwish == WONT)
			return;

	if (G.charmode != CHM_OFF)
		G.telflags ^= UF_ECHO;

	if (G.telflags & UF_ECHO)
		putiac2(DO, TELOPT_ECHO);
	else
		putiac2(DONT, TELOPT_ECHO);

	setConMode();
	WriteCS(1, "\r\n");  /* sudden modec */
}

static inline void to_sga(void)
{
	/* daemon always sends will/wont, client do/dont */

	if (G.telflags & UF_SGA)
	{
		if (G.telwish == WILL)
			return;
	}
	else
		if (G.telwish == WONT)
			return;

	if ((G.telflags ^= UF_SGA) & UF_SGA) /* toggle */
		putiac2(DO, TELOPT_SGA);
	else
		putiac2(DONT, TELOPT_SGA);

	return;
}

#ifdef CONFIG_FEATURE_TELNET_TTYPE
static inline void to_ttype(void)
{
	/* Tell server we will (or won't) do TTYPE */

	if(ttype)
		putiac2(WILL, TELOPT_TTYPE);
	else
		putiac2(WONT, TELOPT_TTYPE);

	return;
}
#endif

#ifdef CONFIG_FEATURE_TELNET_AUTOLOGIN
static inline void to_new_environ(void)
{
	/* Tell server we will (or will not) do AUTOLOGIN */

	if (autologin)
		putiac2(WILL, TELOPT_NEW_ENVIRON);
	else
		putiac2(WONT, TELOPT_NEW_ENVIRON);

	return;
}
#endif

#ifdef CONFIG_FEATURE_AUTOWIDTH
static inline void to_naws(void)
{
	/* Tell server we will do NAWS */
	putiac2(WILL, TELOPT_NAWS);
	return;
}
#endif

static void telopt(byte c)
{
	switch (c)
	{
		case TELOPT_ECHO:		to_echo();	break;
		case TELOPT_SGA:		to_sga();	break;
#ifdef CONFIG_FEATURE_TELNET_TTYPE
		case TELOPT_TTYPE:		to_ttype();break;
#endif
#ifdef CONFIG_FEATURE_TELNET_AUTOLOGIN
		case TELOPT_NEW_ENVIRON:	to_new_environ();	break;
#endif
#ifdef CONFIG_FEATURE_AUTOWIDTH
		case TELOPT_NAWS:		to_naws();
								putiac_naws(c, win_width, win_height);
								break;
#endif
		default:				to_notsup(c);
								break;
	}
}


/* ******************************* */

/* subnegotiation -- ignore all (except TTYPE,NAWS) */

static int subneg(byte c)
{
	switch (G.telstate)
	{
	case TS_SUB1:
		if (c == IAC)
			G.telstate = TS_SUB2;
#ifdef CONFIG_FEATURE_TELNET_TTYPE
		else
		if (c == TELOPT_TTYPE)
			putiac_subopt(TELOPT_TTYPE,ttype);
#endif
#ifdef CONFIG_FEATURE_TELNET_AUTOLOGIN
		else
		if (c == TELOPT_NEW_ENVIRON)
			putiac_subopt_autologin();
#endif
		break;
	case TS_SUB2:
		if (c == SE)
			return TRUE;
		G.telstate = TS_SUB1;
		/* break; */
	}
	return FALSE;
}

/* ******************************* */

static void fgotsig(int sig)
{
	G.gotsig = sig;
}


static void rawmode(void)
{
	if (G.do_termios) tcsetattr(0, TCSADRAIN, &G.termios_raw);
}

static void cookmode(void)
{
	if (G.do_termios) tcsetattr(0, TCSADRAIN, &G.termios_def);
}

int telnet_main(int argc, char** argv)
{
	int len;
	struct sockaddr_in s_in;
#ifdef USE_POLL
	struct pollfd ufds[2];
#else
	fd_set readfds;
	int maxfd;
#endif

#ifdef CONFIG_FEATURE_AUTOWIDTH
	get_terminal_width_height(0, &win_width, &win_height);
#endif

#ifdef CONFIG_FEATURE_TELNET_TTYPE
    ttype = getenv("TERM");
#endif

	memset(&G, 0, sizeof G);

	if (tcgetattr(0, &G.termios_def) >= 0) {
		G.do_termios = 1;

		G.termios_raw = G.termios_def;
		cfmakeraw(&G.termios_raw);
	}

	if (argc < 2)
		bb_show_usage();

#ifdef CONFIG_FEATURE_TELNET_AUTOLOGIN
	if (1 & bb_getopt_ulflags(argc, argv, "al:", &autologin))
		autologin = getenv("USER");

	if (optind < argc) {
		bb_lookup_host(&s_in, argv[optind++]);
		s_in.sin_port = bb_lookup_port((optind < argc) ? argv[optind++] :
				"telnet", "tcp", 23);
		if (optind < argc)
			bb_show_usage();
	} else
		bb_show_usage();
#else
	bb_lookup_host(&s_in, argv[1]);
	s_in.sin_port = bb_lookup_port((argc == 3) ? argv[2] : "telnet", "tcp", 23);
#endif

	G.netfd = xconnect(&s_in);

	setsockopt(G.netfd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof one);

	signal(SIGINT, fgotsig);

#ifdef USE_POLL
	ufds[0].fd = 0; ufds[1].fd = G.netfd;
	ufds[0].events = ufds[1].events = POLLIN;
#else
	FD_ZERO(&readfds);
	FD_SET(0, &readfds);
	FD_SET(G.netfd, &readfds);
	maxfd = G.netfd + 1;
#endif

	while (1)
	{
#ifndef USE_POLL
		fd_set rfds = readfds;

		switch (select(maxfd, &rfds, NULL, NULL, NULL))
#else
		switch (poll(ufds, 2, -1))
#endif
		{
		case 0:
			/* timeout */
		case -1:
			/* error, ignore and/or log something, bay go to loop */
			if (G.gotsig)
				conescape();
			else
				sleep(1);
			break;
		default:

#ifdef USE_POLL
			if (ufds[0].revents) /* well, should check POLLIN, but ... */
#else
			if (FD_ISSET(0, &rfds))
#endif
			{
				len = read(0, G.buf, DATABUFSIZE);

				if (len <= 0)
					doexit(0);

				TRACE(0, ("Read con: %d\n", len));

				handlenetoutput(len);
			}

#ifdef USE_POLL
			if (ufds[1].revents) /* well, should check POLLIN, but ... */
#else
			if (FD_ISSET(G.netfd, &rfds))
#endif
			{
				len = read(G.netfd, G.buf, DATABUFSIZE);

				if (len <= 0)
				{
					WriteCS(1, "Connection closed by foreign host.\r\n");
					doexit(1);
				}
				TRACE(0, ("Read netfd (%d): %d\n", G.netfd, len));

				handlenetinput(len);
			}
		}
	}
}