/* 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
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 * 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@lineo.com> 
 *
 */

#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 <netdb.h>
#include "busybox.h"

#if 0
static const int DOTRACE = 1;
#endif

#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

#if 0
#define USE_POLL
#include <sys/poll.h>
#else
#include <sys/time.h>
#endif

static const int DATABUFSIZE = 128;
static const int IACBUFSIZE = 128;

static const int CHM_TRY = 0;
static const int CHM_ON = 1;
static const int CHM_OFF = 2;

static const int UF_ECHO = 0x01;
static const int UF_SGA = 0x02;

enum {
	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; /* allocating so static size is smaller */
	short	len;
	byte	telstate; /* telnet negotiation state from network input */
	byte	telwish;  /* DO, DONT, WILL, WONT */
	byte    charmode;
	byte    telflags;
	byte	gotsig;
	/* buffer to handle telnet negotiations */
	char *	iacbuf;
	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)
#else
static struct Globalvars G;
#endif

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

/* Function prototypes */
static int getport(char * p);
static struct in_addr getserver(char * p);
static int create_socket();
static void setup_sockaddr_in(struct sockaddr_in * addr, int port);
static int remote_connect(struct in_addr addr, int port);
static void rawmode();
static void cookmode();
static void do_linemode();
static void will_charmode();
static void telopt(byte c);
static int subneg(byte c);
#if 0
static int local_bind(int port);
#endif

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

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

static void conescape()
{
	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()
{
	/*	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 */

	int i;
	byte * p = G.buf;

	for (i = G.len; i > 0; i--, p++)
	{
		if (*p == 0x1d)
		{
			conescape();
			return;
		}
		if (*p == 0xff)
			*p = 0x7f;
	}
	write(G.netfd, G.buf, G.len);
}


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

	for (i = 0; i < G.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) == TRUE)
					 G.telstate = TS_0;
				 break;
			 }
	}
	if (G.telstate)
	{
		if (G.iaclen)			iacflush();
		if (G.telstate == TS_0)	G.telstate = 0;

		G.len = cstart;
	}

	if (G.len)
		write(1, G.buf, G.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);
}

#if 0
static void putiac1(byte c)
{
	if (G.iaclen + 2 > IACBUFSIZE)
		iacflush();

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

/* void putiacstring (subneg strings) */

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

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

static void setConMode()
{
	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()
{
	G.charmode = CHM_TRY;
	G.telflags |= (UF_ECHO | UF_SGA);
	setConMode();
  
	putiac2(DO, TELOPT_ECHO);
	putiac2(DO, TELOPT_SGA);
	iacflush();
}

static void do_linemode()
{
	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()
{
	/* 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()
{
	/* 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;
}

static void telopt(byte c)
{
	switch (c)
	{
	case TELOPT_ECHO:		to_echo(c);		break;
	case TELOPT_SGA:		to_sga(c);		break;
	default:				to_notsup(c);	break;
	}
}


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

/* subnegotiation -- ignore all */

static int subneg(byte c)
{
	switch (G.telstate)
	{
	case TS_SUB1:
		if (c == IAC)
			G.telstate = TS_SUB2;
		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()
{
	tcsetattr(0, TCSADRAIN, &G.termios_raw);
}	

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

extern int telnet_main(int argc, char** argv)
{
	struct in_addr host;
	int port;
#ifdef USE_POLL
	struct pollfd ufds[2];
#else	
	fd_set readfds;
	int maxfd;
#endif	


	memset(&G, 0, sizeof G);

	if (tcgetattr(0, &G.termios_def) < 0)
		exit(1);
	
	G.termios_raw = G.termios_def;

	cfmakeraw(&G.termios_raw);
	
	if (argc < 2)	show_usage();
	port = (argc > 2)? getport(argv[2]): 23;
	
	G.buf = xmalloc(DATABUFSIZE);
	G.iacbuf = xmalloc(IACBUFSIZE);
	
	host = getserver(argv[1]);

	G.netfd = remote_connect(host, port);

	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				
			{
				G.len = read(0, G.buf, DATABUFSIZE);

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

				TRACE(0, ("Read con: %d\n", G.len));
				
				handlenetoutput();
			}

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

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

				handlenetinput();
			}
		}
	}
}

static int getport(char * p)
{
	unsigned int port = atoi(p);

	if ((unsigned)(port - 1 ) > 65534)
	{
		error_msg_and_die("%s: bad port number", p);
	}
	return port;
}

static struct in_addr getserver(char * host)
{
	struct in_addr addr;
	
	struct hostent * he;
	if ((he = gethostbyname(host)) == NULL)
	{
		error_msg_and_die("%s: Unknown host", host);
	}
	memcpy(&addr, he->h_addr, sizeof addr);

	TRACE(1, ("addr: %s\n", inet_ntoa(addr)));
	
	return addr;
}	

static int create_socket()
{
	return socket(AF_INET, SOCK_STREAM, 0);
}

static void setup_sockaddr_in(struct sockaddr_in * addr, int port)
{
	memset(addr, 0, sizeof(struct sockaddr_in));
	addr->sin_family = AF_INET;
	addr->sin_port = htons(port);
}
  
#if 0
static int local_bind(int port)
{
	struct sockaddr_in s_addr;
	int s = create_socket();
  
	setup_sockaddr_in(&s_addr, port);
  
	setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
  
	if (bind(s, &s_addr, sizeof s_addr) < 0)
	{
		char * e = sys_errlist[errno];
		syserrorexit("bind");
		exit(1);
	}
	listen(s, 1);
	
	return s;
}
#endif

static int remote_connect(struct in_addr addr, int port)
{
	struct sockaddr_in s_addr;
	int s = create_socket();

	setup_sockaddr_in(&s_addr, port);
	s_addr.sin_addr = addr;

	setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof one);

	if (connect(s, (struct sockaddr *)&s_addr, sizeof s_addr) < 0)
	{
		perror_msg_and_die("Unable to connect to remote host");
	}
	return s;
}

/*
Local Variables:
c-file-style: "linux"
c-basic-offset: 4
tab-width: 4
End:
*/