diff options
| -rw-r--r-- | toys/pending/telnet.c | 416 | 
1 files changed, 187 insertions, 229 deletions
diff --git a/toys/pending/telnet.c b/toys/pending/telnet.c index b4d5c72f..a7ea91e2 100644 --- a/toys/pending/telnet.c +++ b/toys/pending/telnet.c @@ -14,329 +14,287 @@ config TELNET    help      usage: telnet HOST [PORT] -    Connect to telnet server +    Connect to telnet server.  */  #define FOR_telnet  #include "toys.h"  #include <arpa/telnet.h> -#include <netinet/in.h>  GLOBALS( -  int port; -  int sfd; -  char buff[128]; -  int pbuff; -  char iac[256]; -  int piac; -  char *ttype; -  struct termios def_term; +  int sock; +  char buf[2048]; // Half sizeof(toybuf) allows a buffer full of IACs. +  char iac[128]; +  int iac_len; +  struct termios old_term;    struct termios raw_term; -  uint8_t term_ok; -  uint8_t term_mode; -  uint8_t flags; -  unsigned win_width; -  unsigned win_height; +  uint8_t mode; +  int echo, sga; +  int state, request;  ) -#define DATABUFSIZE 128 -#define IACBUFSIZE  256 +#define NORMAL 0 +#define SAW_IAC 1 +#define SAW_WWDD 2 +#define SAW_SB 3 +#define SAW_SB_TTYPE 4 +#define WANT_IAC 5 +#define WANT_SE 6 +#define SAW_CR 10 +  #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) +static void raw(int raw)  { -  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); -    } -  } +  tcsetattr(0, TCSADRAIN, raw ? &TT.raw_term : &TT.old_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; +  xwrite(TT.sock, TT.iac, TT.iac_len); +  TT.iac_len = 0;  } -// puts DATA in iac buff of length LEN and updates iac buff pointer. -static void put_iac(int len, ...) +static void iac(int n, ...)  {    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--); +  if (TT.iac_len + n >= sizeof(TT.iac)) flush_iac(); +  va_start(va, n); +  while (n--) TT.iac[TT.iac_len++] = va_arg(va, int);    va_end(va);  } -// puts string STR in iac buff and updates iac buff pointer. -static void str_iac(char *str) +static void iacstr(char *str)  { -  int len = strlen(str); +  if (TT.iac_len) flush_iac(); +  xwrite(TT.sock, str, strlen(str)); +  TT.iac_len = 0; +} -  if(TT.piac + len + 1 >= IACBUFSIZE) flush_iac(); -  strcpy(&TT.iac[TT.piac], str); -  TT.piac += len+1; +static void slc(int line) +{ +  TT.mode = line ? CM_OFF : CM_ON; +  xprintf("Entering %s mode\r\nEscape character is '^%c'.\r\n", +      line ? "line" : "character", line ? 'C' : ']'); +  raw(!line); +} + +static void set_mode(void) +{ +  if (TT.echo) { +    if (TT.mode == CM_TRY) slc(0); +  } else if (TT.mode != CM_OFF) slc(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" +  if (toys.signal) raw(1); + +  // This matches busybox telnet, not BSD telnet. +  xputsn("\r\n" +      "Console escape. Commands are:\r\n" +      "\r\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(0, &input, 1) <= 0) { -    if(TT.term_ok) tcsetattr(0, TCSADRAIN, &TT.def_term); -    exit(0); +      " e  exit telnet\r\n" +      "\r\n" +      "telnet> "); +  // In particular, the boxes only read a single character, not a line. +  if (read(0, &input, 1) <= 0 || input == 4 || input == 'e') { +    xprintf("Connection closed.\r\n"); +    xexit();    } -  switch (input) { -  case 'l': +  if (input == 'l') {      if (!toys.signal) { -      TT.term_mode = CM_TRY; -      TT.flags &= ~(UF_ECHO | UF_SGA); +      TT.mode = CM_TRY; +      TT.echo = TT.sga = 0;        set_mode(); -      put_iac(6, IAC,DONT,TELOPT_ECHO,IAC,DONT, TELOPT_SGA); +      iac(6, IAC, DONT, TELOPT_ECHO, IAC, DONT, TELOPT_SGA);        flush_iac();        goto ret;      } -    break; -  case 'c': +  } else if (input == 'c') {      if (toys.signal) { -      TT.term_mode = CM_TRY; -      TT.flags |= (UF_ECHO | UF_SGA); +      TT.mode = CM_TRY; +      TT.echo = TT.sga = 1;        set_mode(); -      put_iac(6, IAC,DO,TELOPT_ECHO,IAC,DO,TELOPT_SGA); +      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); +  } else if (input == 'z') { +    raw(0);      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; +    raw(1);    } -  xwrite(1, "continuing...\r\n", 15); -  if (toys.signal && TT.term_ok) tcsetattr(0, TCSADRAIN, &TT.def_term); +  xprintf("telnet %s %s\r\n", toys.optargs[0], toys.optargs[1] ?: "23"); +  if (toys.signal) raw(0);  ret:    toys.signal = 0;  } -/* - * handles telnet SUB NEGOTIATIONS - * only terminal type is supported. - */ -static void handle_negotiations(void) +// Handle WILL WONT DO DONT requests from the server. +static void handle_wwdd(char opt)  { -  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); +  if (opt == TELOPT_ECHO) { +    if (TT.request == DO) iac(3, IAC, WONT, TELOPT_ECHO); +    if (TT.request == DONT) return; +    if (TT.echo) { +        if (TT.request == WILL) return; +    } else if (TT.request == WONT) return; +    if (TT.mode != CM_OFF) TT.echo ^= 1; +    iac(3, IAC, TT.echo ? DO : 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; +  } else if (opt == TELOPT_SGA) { // Suppress Go Ahead +    if (TT.sga) { +      if (TT.request == WILL) return; +    } else if (TT.request == WONT) return; +    TT.sga ^= 1; +    iac(3, IAC, TT.sga ? DO : DONT, TELOPT_SGA); +  } else if (opt == TELOPT_TTYPE) { // Terminal TYPE +    iac(3, IAC, WILL, TELOPT_TTYPE); +  } else if (opt == TELOPT_NAWS) { // Negotiate About Window Size +    unsigned cols = 80, rows = 24; + +    terminal_size(&cols, &rows); +    iac(3, IAC, WILL, TELOPT_NAWS); +    iac(7, IAC, SB, TELOPT_NAWS, cols>>8, cols, rows>>8, rows); +    iac(2, IAC, SE); +  } else { +    // Say "no" to anything we don't understand. +    iac(3, IAC, (TT.request == WILL) ? DONT : WONT, opt);    }  } -/* - * parses data which is read from server of length LEN. - * and passes it to console. - */ -static int read_server(int len) +static void handle_server_output(int n)  { +  char *p = TT.buf, *end = TT.buf + n, ch;    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; + +  // Possibilities: +  // +  // 1. Regular character +  // 2. IAC [WILL|WONT|DO|DONT] option +  // 3. IAC SB option arg... IAC SE +  // +  // The only subnegotiation we support is IAC SB TTYPE SEND IAC SE, so we just +  // hard-code that into our state machine rather than having a more general +  // "collect the subnegotation into a buffer and handle it after we've seen +  // the IAC SE at the end". It's 2021, so we're unlikely to need more. + +  while (p < end) { +    ch = *p++; +    if (TT.state == SAW_IAC) { +      if (ch >= WILL && ch <= DONT) { +        TT.state = SAW_WWDD; +        TT.request = ch; +      } else if (ch == SB) { +        TT.state = SAW_SB; +      } else { +        TT.state = NORMAL;        } -    } else { -      toybuf[i++] = curr; -      if (curr == '\r') { curr = TT.buff[TT.pbuff++]; -        if (curr != '\0') TT.pbuff--; +    } else if (TT.state == SAW_WWDD) { +      handle_wwdd(ch); +      TT.state = NORMAL; +    } else if (TT.state == SAW_SB) { +      if (ch == TELOPT_TTYPE) TT.state = SAW_SB_TTYPE; +      else TT.state = WANT_IAC; +    } else if (TT.state == SAW_SB_TTYPE) { +      if (ch == TELQUAL_SEND) { +        iac(4, IAC, SB, TELOPT_TTYPE, TELQUAL_IS); +        iacstr(getenv("TERM") ?: "NVT"); +        iac(2, IAC, SE);        } +      TT.state = WANT_IAC; +    } else if (TT.state == WANT_IAC) { +      if (ch == IAC) TT.state = WANT_SE; +    } else if (TT.state == WANT_SE) { +      if (ch == SE) TT.state = NORMAL; +    } else if (ch == IAC) { +      TT.state = SAW_IAC; +    } else { +      if (TT.state == SAW_CR && ch == '\0') { +        // CR NUL -> CR +      } else toybuf[i++] = ch; +      if (ch == '\r') TT.state = SAW_CR; +      TT.state = NORMAL;      } -  } while (TT.pbuff < len); - +  }    if (i) xwrite(0, 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) +static void handle_user_input(int n)  { -  char *c = (char*)TT.buff; +  char *p = TT.buf, ch;    int i = 0; -  for (; len > 0; len--, c++) { -    if (*c == 0x1d) { +  while (n--) { +    ch = *p++; +    if (ch == 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 */ +    toybuf[i++] = ch; +    if (ch == IAC) toybuf[i++] = IAC; // IAC -> IAC IAC +    else if (ch == '\r') toybuf[i++] = '\n'; // CR -> CR LF +    else if (ch == '\n') { // LF -> CR LF +      toybuf[i-1] = '\r'; +      toybuf[i++] = '\n'; +    }    } -  if(i) xwrite(TT.sfd, toybuf, i); +  if (i) xwrite(TT.sock, toybuf, i); +} + +static void reset_terminal(void) +{ +  raw(0);  }  void telnet_main(void)  { -  char *port = "23"; -  int set = 1, len;    struct pollfd pfds[2]; +  int n = 1; -  TT.win_width = 80; //columns -  TT.win_height = 24; //rows +  tcgetattr(0, &TT.old_term); +  TT.raw_term = TT.old_term; +  cfmakeraw(&TT.raw_term); -  if (toys.optc == 2) port = toys.optargs[1]; +  TT.sock = xconnectany(xgetaddrinfo(*toys.optargs, toys.optargs[1] ?: "23", 0, +      SOCK_STREAM, IPPROTO_TCP, 0)); +  xsetsockopt(TT.sock, SOL_SOCKET, SO_KEEPALIVE, &n, sizeof(n)); -  TT.ttype = getenv("TERM"); -  if(!TT.ttype) TT.ttype = ""; -  if(strlen(TT.ttype) > IACBUFSIZE-1) TT.ttype[IACBUFSIZE - 1] = '\0'; +  xprintf("Connected to %s.\r\n", *toys.optargs); -  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 = xconnectany(xgetaddrinfo(*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)); +  sigatexit(reset_terminal); +  signal(SIGINT, generic_signal);    pfds[0].fd = 0;    pfds[0].events = POLLIN; -  pfds[1].fd = TT.sfd; +  pfds[1].fd = TT.sock;    pfds[1].events = POLLIN; - -  signal(SIGINT, generic_signal); -  while(1) { -    if(TT.piac) flush_iac(); -    if(poll(pfds, 2, -1) < 0) { +  for (;;) { +    if (poll(pfds, 2, -1) < 0) {        if (toys.signal) handle_esc(); -      else sleep(1); - -      continue; +      else perror_exit("poll");      } -    if(pfds[0].revents) { -      len = read(0, TT.buff, DATABUFSIZE); -      if(len > 0) write_server(len); -      else return; +    if (pfds[0].revents) { +      if ((n = read(0, TT.buf, sizeof(TT.buf))) <= 0) xexit(); +      handle_user_input(n);      } -    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); -      } +    if (pfds[1].revents) { +      if ((n = read(TT.sock, TT.buf, sizeof(TT.buf))) <= 0) +        error_exit("Connection closed by foreign host\r"); +      handle_server_output(n);      } +    if (TT.iac_len) flush_iac();    }  }  | 
