/* telnet.c - Telnet client.
 *
 * Copyright 2012 Madhur Verma <mad.flexi@gmail.com>
 * Copyright 2013 Kyungwan Han <asura321@gmail.com>
 * Modified by Ashwini Kumar <ak.ashwini1981@gmail.com>
 *
 * Not in SUSv4.

USE_TELNET(NEWTOY(telnet, "<1>2", TOYFLAG_BIN))

config TELNET
  bool "telnet"
  default n
  help
    usage: telnet HOST [PORT]

    Connect to telnet server
*/

#define FOR_telnet
#include "toys.h"
#include <arpa/telnet.h>
#include <netinet/in.h>
#include  <sys/poll.h>

GLOBALS(
  int port;
  int sfd;
  char buff[128];
  int pbuff;
  char iac[256];
  int piac;
  char *ttype;
  struct termios def_term;
  struct termios raw_term;
  uint8_t term_ok;
  uint8_t term_mode;
  uint8_t flags;
  unsigned win_width;
  unsigned win_height;
)

#define DATABUFSIZE 128
#define IACBUFSIZE  256
#define CM_TRY      0
#define CM_ON       1
#define CM_OFF      2
#define UF_ECHO     0x01
#define UF_SGA      0x02

// sets terminal mode: LINE or CHARACTER based om internal stat.
static char const es[] = "\r\nEscape character is ";
static void set_mode(void)
{
  if (TT.flags & UF_ECHO) {
    if (TT.term_mode == CM_TRY) {
      TT.term_mode = CM_ON;
      printf("\r\nEntering character mode%s'^]'.\r\n", es);
      if (TT.term_ok) tcsetattr(0, TCSADRAIN, &TT.raw_term);
    }
  } else {
    if (TT.term_mode != CM_OFF) {
      TT.term_mode = CM_OFF;
      printf("\r\nEntering line mode%s'^C'.\r\n", es);
      if (TT.term_ok) tcsetattr(0, TCSADRAIN, &TT.def_term);
    }
  }
}

// flushes all data in IAC buff to server.
static void flush_iac(void)
{
  int wlen = write(TT.sfd, TT.iac, TT.piac);

  if(wlen <= 0) error_msg("IAC : send failed.");
  TT.piac = 0;
}

// puts DATA in iac buff of length LEN and updates iac buff pointer.
static void put_iac(int len, ...)
{
  va_list va; 

  if(TT.piac + len >= IACBUFSIZE) flush_iac();
  va_start(va, len);
  for(;len > 0; TT.iac[TT.piac++] = (uint8_t)va_arg(va, int), len--);
  va_end(va);
}

// puts string STR in iac buff and updates iac buff pointer.
static void str_iac(char *str)
{
  int len = strlen(str);

  if(TT.piac + len + 1 >= IACBUFSIZE) flush_iac();
  strcpy(&TT.iac[TT.piac], str);
  TT.piac += len+1;
}

static void handle_esc(void)
{
  char input;

  if(toys.signal && TT.term_ok) tcsetattr(0, TCSADRAIN, &TT.raw_term);
  xwrite(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", 114);

  if (read(STDIN_FILENO, &input, 1) <= 0) {
    if(TT.term_ok) tcsetattr(0, TCSADRAIN, &TT.def_term);
    exit(0);
  }

  switch (input) {
  case 'l':
    if (!toys.signal) {
      TT.term_mode = CM_TRY;
      TT.flags &= ~(UF_ECHO | UF_SGA);
      set_mode();
      put_iac(6, IAC,DONT,TELOPT_ECHO,IAC,DONT, TELOPT_SGA);
      flush_iac();
      goto ret;
    }
    break;
  case 'c':
    if (toys.signal) {
      TT.term_mode = CM_TRY;
      TT.flags |= (UF_ECHO | UF_SGA);
      set_mode();
      put_iac(6, IAC,DO,TELOPT_ECHO,IAC,DO,TELOPT_SGA);
      flush_iac();
      goto ret;
    }
    break;
  case 'z':
    if(TT.term_ok) tcsetattr(0, TCSADRAIN, &TT.def_term);
    kill(0, SIGTSTP);
    if(TT.term_ok) tcsetattr(0, TCSADRAIN, &TT.raw_term);
    break;
  case 'e':
    if(TT.term_ok) tcsetattr(0, TCSADRAIN, &TT.def_term);
    exit(0);
  default: break;
  }

  xwrite(1, "continuing...\r\n", 15);
  if (toys.signal && TT.term_ok) tcsetattr(0, TCSADRAIN, &TT.def_term);

ret:
  toys.signal = 0;
}

/*
 * handles telnet SUB NEGOTIATIONS
 * only terminal type is supported.
 */
static void handle_negotiations(void)
{
  char opt = TT.buff[TT.pbuff++];

  switch(opt) {
  case TELOPT_TTYPE:
    opt =  TT.buff[TT.pbuff++];
    if(opt == TELQUAL_SEND) {
      put_iac(4, IAC,SB,TELOPT_TTYPE,TELQUAL_IS);
      str_iac(TT.ttype);
      put_iac(2, IAC,SE);
    }
    break;
  default: break;
  }
}

/*
 * handles server's DO DONT WILL WONT requests.
 * supports ECHO, SGA, TTYPE, NAWS
 */
static void handle_ddww(char ddww)
{
  char opt = TT.buff[TT.pbuff++];

  switch (opt) {
  case TELOPT_ECHO: /* ECHO */
    if (ddww == DO) put_iac(3, IAC,WONT,TELOPT_ECHO);
    if(ddww == DONT) break;
    if (TT.flags & UF_ECHO) {
        if (ddww == WILL) return;
      } else if (ddww == WONT) return;
    if (TT.term_mode != CM_OFF) TT.flags ^= UF_ECHO;
    (TT.flags & UF_ECHO)? put_iac(3, IAC,DO,TELOPT_ECHO) : 
      put_iac(3, IAC,DONT,TELOPT_ECHO);
    set_mode();
    printf("\r\n");
    break;

  case TELOPT_SGA: /* Supress GO Ahead */
    if (TT.flags & UF_SGA){ if (ddww == WILL) return;
    } else if (ddww == WONT) return;

    TT.flags ^= UF_SGA;
    (TT.flags & UF_SGA)? put_iac(3, IAC,DO,TELOPT_SGA) :
      put_iac(3, IAC,DONT,TELOPT_SGA);
    break;

  case TELOPT_TTYPE: /* Terminal Type */
    (TT.ttype)? put_iac(3, IAC,WILL,TELOPT_TTYPE):
      put_iac(3, IAC,WONT,TELOPT_TTYPE);
    break;

  case TELOPT_NAWS: /* Window Size */
    put_iac(3, IAC,WILL,TELOPT_NAWS);
    put_iac(9, IAC,SB,TELOPT_NAWS,(TT.win_width >> 8) & 0xff,
        TT.win_width & 0xff,(TT.win_height >> 8) & 0xff,
        TT.win_height & 0xff,IAC,SE);
    break;

  default: /* Default behaviour is to say NO */
    if(ddww == WILL) put_iac(3, IAC,DONT,opt);
    if(ddww == DO) put_iac(3, IAC,WONT,opt);
    break;
  }
}

/*
 * parses data which is read from server of length LEN.
 * and passes it to console.
 */
static int read_server(int len)
{
  int i = 0;
  char curr;
  TT.pbuff = 0;

  do {
    curr = TT.buff[TT.pbuff++];
    if (curr == IAC) {
      curr = TT.buff[TT.pbuff++];
      switch (curr) {
      case DO:    /* FALLTHROUGH */
      case DONT:    /* FALLTHROUGH */
      case WILL:    /* FALLTHROUGH */
      case WONT:
        handle_ddww(curr);
        break;
      case SB:
        handle_negotiations();
        break;
      case SE:
        break;
      default: break;
      }
    } else {
      toybuf[i++] = curr;
      if (curr == '\r') { curr = TT.buff[TT.pbuff++];
        if (curr != '\0') TT.pbuff--;
      }
    }
  } while (TT.pbuff < len);

  if (i) xwrite(STDIN_FILENO, toybuf, i);
  return 0;
}

/*
 * parses data which is read from console of length LEN
 * and passes it to server.
 */
static void write_server(int len)
{
  char *c = (char*)TT.buff;
  int i = 0;

  for (; len > 0; len--, c++) {
    if (*c == 0x1d) {
      handle_esc();
      return;
    }
    toybuf[i++] = *c;
    if (*c == IAC) toybuf[i++] = *c; /* IAC -> IAC IAC */
    else if (*c == '\r') toybuf[i++] = '\0'; /* CR -> CR NUL */
  }
  if(i) xwrite(TT.sfd, toybuf, i);
}

void telnet_main(void)
{
  char *port = "23";
  int set = 1, len;
  struct pollfd pfds[2];

  TT.win_width = 80; //columns
  TT.win_height = 24; //rows

  if (toys.optc == 2) port = toys.optargs[1];

  TT.ttype = getenv("TERM");
  if(!TT.ttype) TT.ttype = "";
  if(strlen(TT.ttype) > IACBUFSIZE-1) TT.ttype[IACBUFSIZE - 1] = '\0';

  if (!tcgetattr(0, &TT.def_term)) {
    TT.term_ok = 1;
    TT.raw_term = TT.def_term;
    cfmakeraw(&TT.raw_term);
  }
  terminal_size(&TT.win_width, &TT.win_height);

  TT.sfd = xconnect(*toys.optargs, port, 0, SOCK_STREAM, IPPROTO_TCP, 0);
  setsockopt(TT.sfd, SOL_SOCKET, SO_REUSEADDR, &set, sizeof(set));
  setsockopt(TT.sfd, SOL_SOCKET, SO_KEEPALIVE, &set, sizeof(set));

  pfds[0].fd = STDIN_FILENO;
  pfds[0].events = POLLIN;
  pfds[1].fd = TT.sfd;
  pfds[1].events = POLLIN;

  signal(SIGINT, generic_signal);
  while(1) {
    if(TT.piac) flush_iac();
    if(poll(pfds, 2, -1) < 0) {
      if (toys.signal) handle_esc();
      else sleep(1);

      continue;
    }
    if(pfds[0].revents) {
      len = read(STDIN_FILENO, TT.buff, DATABUFSIZE);
      if(len > 0) write_server(len);
      else return;
    }
    if(pfds[1].revents) {
      len = read(TT.sfd, TT.buff, DATABUFSIZE);
      if(len > 0) read_server(len);
      else {
        printf("Connection closed by foreign host\r\n");
        if(TT.term_ok) tcsetattr(0, TCSADRAIN, &TT.def_term);
        exit(1);
      }
    }
  }
}
