/* telnet.c -- basic telnet protocol handling for ttywatch
 *
 * Copyright © 2001 Michael K. Johnson <a1237+ttywatch@danlj.org>
 *
 * 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
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include "telnet.h"
#include "errors.h"

#define IAC "\xff"
#define DONT "\xfe"
#define WONT "\xfc"
#define WILL "\xfb"
#define DO "\xfd"
#define SB "\xfa"
#define SE "\xf0"
/* we do not care about termios ECHO... */
#ifdef ECHO
#undef ECHO
#endif
#define ECHO "\x01"
#define SUPPRESS_GO_AHEAD "\x03"
#define STATUS "\x05"
#define TERMINAL_TYPE "\x18"
#define TERMINAL_SPEED "\x20"
#define NEGOTIATE_WINDOW_SIZE "\x1f"
#define REMOTE_FLOW_CONTROL "\x21"
#define X_DISPLAY_LOCATION "\x23"
#define LINEMODE "\x22"
#define NEWENVIRON "\x27"
#define MODE "\x01"
#define BREAK '\xf3'

/* Make a request.  Not intended to be RFC-compatible, just enough
 * to convince telnet clients to do what we want...  To do this
 * right, we would have to honestly negotiate, not speak blind.
 *
 * For now, assume all responses will be favorable and stripped
 * out in telnet_process_input()...  Sending it all in a single
 * write makes it more efficient because it will all go out in a
 * single packet, and the responses are more likely to all come
 * back in a single packet (and thus, practically, a single read)
 * too.
 */
void
telnet_negotiate_server(int socket) {
    static char request[]=
      IAC DONT ECHO
      IAC WILL ECHO
      IAC WILL SUPPRESS_GO_AHEAD
      IAC DO SUPPRESS_GO_AHEAD
      IAC DONT TERMINAL_TYPE
      IAC DONT TERMINAL_SPEED
      IAC DONT X_DISPLAY_LOCATION
      IAC DONT NEWENVIRON
#if 0
      IAC DO LINEMODE
      IAC SB LINEMODE MODE "0" IAC SE
#endif
      ;
    write(socket, request, sizeof(request)-1);
}

void
telnet_negotiate_client(int socket) {
    static char request[]=
      IAC WONT ECHO
      IAC DO SUPPRESS_GO_AHEAD
      IAC WONT NEWENVIRON
      IAC WONT NEGOTIATE_WINDOW_SIZE
      IAC WONT TERMINAL_TYPE
      IAC WONT TERMINAL_SPEED
      IAC WONT X_DISPLAY_LOCATION
      IAC WILL REMOTE_FLOW_CONTROL
      ;
    write(socket, request, sizeof(request)-1);
}

int
telnet_process_input(net_conn *nc, char *data, int len) {
    char *s, *d; /* source, destination */

#   define DEBUG_TELNET 0
#   if DEBUG_TELNET
    printf("\nprinting packet:");
    for (s=data; s<data+len; s++) {
	if (!((s-data)%10))
	    printf("\n %03d: ", s-data);
	printf("%02x ", *s & 0x000000FF);
    }
    printf("\n");
#   endif /* DEBUG_TELNET */

    for (s=data, d=data; s<data+len; s++) {
	switch (nc->ts) {
	case TS_DATA:
	    if (*s == '\xff') { /* IAC */
		nc->ts = TS_IAC;
		continue;
	    }
	    if (*s == 0) { /* Sometimes NUL shows up, be liberal and ignore */
		s++;
		continue;
	    }
#if	    DEBUG_TELNET
	    printf("copying data element '%c'\n", *s);
#endif	    /* DEBUG_TELNET */
	    if (s>d) {
		*(d++) = *s;
	    } else {
		d++;
	    }
	    break;

	case TS_IAC:
	    if (*s == BREAK) {
		/* this BREAK handling is far from spec, but it happens
		 * to work for real use cases, and to do it completely
		 * right would complicate ttywatch
		 */
		nc->flags |= TS_FLAG_BREAK;
		nc->ts = TS_DATA;
		continue;
	    }
	    if (*s == '\xfa') { /* SB */
		nc->ts = TS_SB;
		continue;
	    }
	    /* if not SB or BREAK, skip IAC verb object */
#	    if DEBUG_TELNET
	    printf("skipping verb/object (offset %d)...\n", s-data-1);
#	    endif /* DEBUG_TELNET */
	    s += 1;
	    nc->ts = TS_DATA;
	    break;

	case TS_SB:
#	    if DEBUG_TELNET
	    printf("skipping SB (offset %d)...\n", s-data-1);
#	    endif /* DEBUG_TELNET */
	    while (s < (data+(len-1))) {
		if (*s == '\xff') {
		    break; /* fall through to TS_SB_IAC setting below */
		} else {
		    s++;
		}
	    }
	    if (*s == '\xff') {
		nc->ts = TS_SB_IAC;
	    }
	    break;

	case TS_SB_IAC:
	    if (*s == '\xf0') { /* SE */
#		if DEBUG_TELNET
		printf("SE ends SB (offset %d)...\n", s-data-1);
#		endif /* DEBUG_TELNET */
		nc->ts = TS_DATA;
	    } else {
#		if DEBUG_TELNET
		printf("IAC without SE in SB\n");
#		endif /* DEBUG_TELNET */
		nc->ts = TS_SB;
	    }
	    break;

	default:
	    warn(nc->m, "unknown telnet state %d for data element %c", nc->ts, *s);
	    nc->ts = TS_DATA;
	    break;
	}
    }

    /* calculate new length after copying data around */
    len = d - data;
#if DEBUG_TELNET
    printf("returning len: %d of packet:", len);
    for (s=data; s<data+len; s++) {
	if (!((s-data)%10))
	    printf("\n %03d: ", s-data);
	printf("%02x ", *s & 0x000000FF);
    }
    printf("\n");
#endif /* DEBUG_TELNET */
    return len;
}

/* Test input data for a BREAK character */
/* should be return strnchr(data, BREAK, len); but strnstr not std */
int
telnet_data_has_break(net_conn *nc, char *data, int len) {
    char *s;

    for (s=data; s < data+len; s++) {
	if (*s == BREAK) return 1;
    }
    return 0;
}

/* The telnet protocol requires CR/NL instead of just NL
 * We normally deal with Unix, which just uses NL, so we need to translate.
 *
 * It would be easy to go through line-by-line and write each line, but
 * that would create more packet overhead by sending out one packet
 * per line, and over things like slow PPP connections, that is painful.
 * Therefore, instead, we create a modified copy of the data and write
 * the whole modified copy at once.
 */
void
telnet_send_output(net_conn *nc, char *data, int len) {
    char *s, *d; /* source, destination */
    char *buf;

    buf = alloca((len*2)+1);  /* max necessary size */

    /* just may need to add CR before NL (but do not double existing CRs) */
    for (s=data, d=buf; d-buf<len; s++, d++) {
	if ((*s == '\n') && (s == data || (*(s-1) != '\r'))) {
	    /* NL without preceding CR */
	    *(d++) = '\r';
	    len++;
	}
	*d = *s;
    }

    /* now send it... */
    write(nc->fd, buf, len);
}
