/*
 * piaip's simplified implementation of TELNET protocol
 */
#ifdef DEBUG
#define TELOPTS
#define TELCMDS
#endif

#include "bbs.h"

#ifdef DETECT_CLIENT
void UpdateClientCode(unsigned char c); // see mbbsd.c
#endif

unsigned int telnet_handler(unsigned char c) ;
void	telnet_init(void);
ssize_t tty_read(unsigned char *buf, size_t max);

enum TELNET_IAC_STATES {
	IAC_NONE,
	IAC_COMMAND,
	IAC_WAIT_OPT,
	IAC_WAIT_SE,
	IAC_PROCESS_OPT,
	IAC_ERROR
};

static unsigned char iac_state = 0; /* as byte to reduce memory */

#define TELNET_IAC_MAXLEN (16)
/* We don't reply to most commands, so this maxlen can be minimal.
 * Warning: if you want to support ENV passing or other long commands,
 * remember to increase this value. Howver, some poorly implemented
 * terminals like xxMan may not follow the protocols and user will hang
 * on those terminals when IACs were sent.
 */

void
telnet_init(void)
{
    /* We are the boss. We don't respect to client.
     * It's client's responsibility to follow us.
     * Please write these codes in i-dont-care opt handlers.
     */
    const char telnet_init_cmds[] = {
	/* retrieve terminal type and throw away.
	 * why? because without this, clients enter line mode.
	 */
	IAC, DO, TELOPT_TTYPE,
	IAC, SB, TELOPT_TTYPE, TELQUAL_SEND, IAC, SE,

	/* i'm a smart term with resize ability. */
	IAC, DO, TELOPT_NAWS,

	/* i will echo. */
	IAC, WILL, TELOPT_ECHO,
	/* supress ga. */
	IAC, WILL, TELOPT_SGA,
	/* 8 bit binary. */
	IAC, WILL, TELOPT_BINARY,
	IAC, DO,   TELOPT_BINARY,
    };

    raw_connection = 1;
    write(0, telnet_init_cmds, sizeof(telnet_init_cmds));
}

/* tty_read
 * read from tty, process telnet commands if raw connection.
 * return: >0 = length, <=0 means read more, abort/eof is automatically processed.
 */
ssize_t
tty_read(unsigned char *buf, size_t max)
{
    ssize_t l = read(0, buf, max);

    if(l == 0 || (l < 0 && !(errno == EINTR || errno == EAGAIN)))
	abort_bbs(0);

    if(!raw_connection)
	return l;

    /* process buffer */
    if (l > 0) {
	unsigned char *buf2 = buf;
	size_t i = 0, i2 = 0;

	/* prescan. because IAC is rare, 
	 * this cost is worthy. */
	if (iac_state == IAC_NONE && memchr(buf, IAC, l) == NULL)
	    return l;

	/* we have to look into the buffer. */
	for (i = 0; i < l; i++, buf++)
	    if(telnet_handler(*buf) == 0)
		*(buf2++) = *buf;
	    else
		i2 ++;
	l = (i2 == l) ? -1L : l - i2;
    }
    return l;
}

#ifdef DBG_OUTRPT
extern unsigned char fakeEscape;
#endif // DBG_OUTRPT

/* input:  raw character
 * output: telnet command if c was handled, otherwise zero.
 */
unsigned int 
telnet_handler(unsigned char c) 
{
    static unsigned char iac_quote = 0; /* as byte to reduce memory */
    static unsigned char iac_opt_req = 0;

    static unsigned char iac_buf[TELNET_IAC_MAXLEN];
    static unsigned int  iac_buflen = 0;

    /* we have to quote all IACs. */
    if(c == IAC && !iac_quote) {
	iac_quote = 1;
	return NOP;
    }

#ifdef DETECT_CLIENT
    /* hash client telnet sequences */
    if(cuser.userid[0]==0) {
	if(iac_state == IAC_WAIT_SE) {
	    // skip suboption
	} else {
	    if(iac_quote)
		UpdateClientCode(IAC);
	    UpdateClientCode(c);
	}
    }
#endif

    /* a special case is the top level iac. otherwise, iac is just a quote. */
    if (iac_quote) {
	if(iac_state == IAC_NONE)
	    iac_state = IAC_COMMAND;
	if(iac_state == IAC_WAIT_SE && c == SE)
	    iac_state = IAC_PROCESS_OPT;
	iac_quote = 0;
    }

    /* now, let's process commands by state */
    switch(iac_state) {

	case IAC_NONE:
	    return 0;

	case IAC_COMMAND:
#if 0 // def DEBUG
	    {
		int cx = c; /* to make compiler happy */
		write(0, "-", 1);
		if(TELCMD_OK(cx))
		    write(0, TELCMD(c), strlen(TELCMD(c)));
		write(0, " ", 1);
	    }
#endif
	    iac_state = IAC_NONE; /* by default we restore state. */
	    switch(c) {
		case IAC:
		    // return 0;
		    // we don't want to allow IACs as input.
		    return 1;

		/* we don't want to process these. or maybe in future. */
		case BREAK:           /* break */
#ifdef DBG_OUTRPT
		    fakeEscape = !fakeEscape;
		    return NOP;
#endif

		case ABORT:           /* Abort process */
		case SUSP:            /* Suspend process */
		case AO:              /* abort output--but let prog finish */
		case IP:              /* interrupt process--permanently */
		case EOR:             /* end of record (transparent mode) */
		case DM:              /* data mark--for connect. cleaning */
		case xEOF:            /* End of file: EOF is already used... */
		    return NOP;

		case NOP:             /* nop */
		    return NOP;

		/* we should process these, but maybe in future. */
		case GA:              /* you may reverse the line */
		case EL:              /* erase the current line */
		case EC:              /* erase the current character */
		    return NOP;

		/* good */
		case AYT:             /* are you there */
		    {
			    const char *alive = "I'm still alive, loading: ";
			    char buf[STRLEN];

			    /* respond as fast as we can */
			    write(0, alive, strlen(alive));
			    cpuload(buf);
			    write(0, buf, strlen(buf));
			    write(0, "\r\n", 2);
		    }
		    return NOP;

		case DONT:            /* you are not to use option */
		case DO:              /* please, you use option */
		case WONT:            /* I won't use option */
		case WILL:            /* I will use option */
		    iac_opt_req = c;
		    iac_state = IAC_WAIT_OPT;
		    return NOP;

		case SB:              /* interpret as subnegotiation */
		    iac_state = IAC_WAIT_SE;
		    iac_buflen = 0;
		    return NOP;

		case SE:              /* end sub negotiation */
		default:
		    return NOP;
	    }
	    return 1;

	case IAC_WAIT_OPT:
#if 0 // def DEBUG
	    write(0, "-", 1);
	    if(TELOPT_OK(c))
		write(0, TELOPT(c), strlen(TELOPT(c)));
	    write(0, " ", 1);
#endif
	    iac_state = IAC_NONE;
	    /*
	     * According to RFC, there're some tricky steps to prevent loop.
	     * However because we have a poor term which does not allow 
	     * most abilities, let's be a strong boss here.
	     *
	     * Although my old imeplementation worked, it's even better to follow this:
	     * http://www.tcpipguide.com/free/t_TelnetOptionsandOptionNegotiation-3.htm
	     */
	    switch(c) {
		/* i-dont-care: i don't care about what client is. 
		 * these should be clamed in init and
		 * client must follow me. */
		case TELOPT_TTYPE:	/* termtype or line. */
		case TELOPT_NAWS:       /* resize terminal */
		case TELOPT_SGA:	/* supress GA */
		case TELOPT_ECHO:       /* echo */
		case TELOPT_BINARY:	/* we are CJK. */
		    break;

		/* i-dont-agree: i don't understand/agree these.
		 * according to RFC, saying NO stopped further
		 * requests so there'll not be endless loop. */
		case TELOPT_RCP:         /* prepare to reconnect */
		default:
		    if (iac_opt_req == WILL || iac_opt_req == DO)
		    {
			/* unknown option, reply with won't */
			unsigned char cmd[3] = { IAC, DONT, 0 };
			if(iac_opt_req == DO) cmd[1] = WONT;
			cmd[2] = c;
			write(0, cmd, sizeof(cmd));
		    }
		    break;
	    }
	    return 1;

	case IAC_WAIT_SE:
	    iac_buf[iac_buflen++] = c;
	    /* no need to convert state because previous quoting will do. */
		    
	    if(iac_buflen == TELNET_IAC_MAXLEN) {
		/* may be broken protocol?
		 * whether finished or not, break for safety 
		 * or user may be frozen.
		 */
		iac_state = IAC_NONE;
		return 0;
	    }
	    return 1;

	case IAC_PROCESS_OPT:
	    iac_state = IAC_NONE;
#if 0 // def DEBUG
	    write(0, "-", 1);
	    if(TELOPT_OK(iac_buf[0]))
		write(0, TELOPT(iac_buf[0]), strlen(TELOPT(iac_buf[0])));
	    write(0, " ", 1);
#endif
	    switch(iac_buf[0]) {

		/* resize terminal */
		case TELOPT_NAWS:
		    {
			int w = (iac_buf[1] << 8) + (iac_buf[2]);
			int h = (iac_buf[3] << 8) + (iac_buf[4]);
			term_resize(w, h);
#ifdef DETECT_CLIENT
			if(cuser.userid[0]==0) {
			    UpdateClientCode(iac_buf[0]);
			    if(w==80 && h==24)
				UpdateClientCode(1);
			    else if(w==80)
				UpdateClientCode(2);
			    else if(h==24)
				UpdateClientCode(3);
			    else
				UpdateClientCode(4);
			    UpdateClientCode(IAC);
			    UpdateClientCode(SE);
			}
#endif
		    }
		    break;

		default:
#ifdef DETECT_CLIENT
		    if(cuser.userid[0]==0) {
			int i;
			for(i=0;i<iac_buflen;i++)
			    UpdateClientCode(iac_buf[i]);
			UpdateClientCode(IAC);
			UpdateClientCode(SE);
		    }
#endif
		    break;
	    }
	    return 1;
    }
    return 1; /* never reached */
}

// vim: sw=4