| @@ -11,13 +11,11 @@ |
| * Licensed under GPLv2 or later, see file LICENSE in this source tree. |
| * |
| * HISTORY |
| - | * Revision 3.1 1994/04/17 11:31:54 too |
| - | * initial revision |
| - | * Modified 2000/06/13 for inclusion into BusyBox by Erik Andersen <[email protected]> |
| - | * Modified 2001/05/07 to add ability to pass TTYPE to remote host by Jim McQuillan |
| - | * <[email protected]> |
| - | * Modified 2004/02/11 to add ability to pass the USER variable to remote host |
| - | * by Fernando Silveira <[email protected]> |
| + | * 1994/04/17 Initial revision |
| + | * 2000/06/13 for inclusion into BusyBox by Erik Andersen <[email protected]> |
| + | * 2001/05/07 add ability to pass TTYPE to remote host by Jim McQuillan <[email protected]> |
| + | * 2004/02/11 add ability to pass the USER variable to remote host by Fernando Silveira <[email protected]> |
| + | * 2026/02/20 Almost total rewrite to use ioloop() |
| */ |
| //config:config TELNET |
| //config: bool "telnet (8.8 kb)" |
| @@ -80,7 +78,7 @@ |
| # define DONT 254 /* you are not to use option */ |
| # define DO 253 /* please, you use option */ |
| # define WONT 252 /* I won't use option */ |
| - | # define WILL 251 /* I will use option */ |
| + | # define WILL 251 /* I will use option (if I see your "IAC DO <opt>" confirmation */ |
| # define SB 250 /* interpret as subnegotiation */ |
| # define SE 240 /* end sub negotiation */ |
| # define TELOPT_ECHO 1 /* echo */ |
| @@ -89,276 +87,143 @@ |
| # define TELOPT_NAWS 31 /* window size */ |
| #endif |
| |
| - | enum { |
| - | DATABUFSIZE = 128, |
| - | IACBUFSIZE = 128, |
| + | #define DEBUG 0 |
| |
| - | CHM_TRY = 0, |
| - | CHM_ON = 1, |
| - | CHM_OFF = 2, |
| + | #if DEBUG |
| + | # define dbg(...) bb_error_msg(__VA_ARGS__) |
| + | static char *bin_to_hex(const void *hash_value, unsigned hash_length) |
| + | { |
| + | /* xzalloc zero-terminates */ |
| + | char *hex_value = xzalloc((hash_length * 2) + 1); |
| + | bin2hex(hex_value, (char*)hash_value, hash_length); |
| + | return auto_string(hex_value); |
| + | } |
| + | #else |
| + | # define dbg(...) ((void)0) |
| + | #endif |
| |
| - | UF_ECHO = 0x01, |
| - | UF_SGA = 0x02, |
| + | enum { |
| + | BUFSIZE = 1024, |
| + | BUFMASK = BUFSIZE - 1, |
| |
| TS_NORMAL = 0, |
| - | TS_COPY = 1, |
| - | TS_IAC = 2, |
| - | TS_OPT = 3, |
| - | TS_SUB1 = 4, |
| - | TS_SUB2 = 5, |
| - | TS_CR = 6, |
| + | TS_IAC = 1, |
| + | TS_OPT = 2, |
| + | TS_SUB1 = 3, |
| + | TS_SUB2 = 4, |
| + | TS_CR = 5, |
| + | |
| + | MAX_NAWS_SIZE = 13, /* pathological 65535x65535 case needs full escaping */ |
| }; |
| |
| typedef unsigned char byte; |
| |
| enum { netfd = 3 }; |
| |
| - | struct globals { |
| - | int iaclen; /* could even use byte, but it's a loss on x86 */ |
| - | byte telstate; /* telnet negotiation state from network input */ |
| - | byte telwish; /* DO, DONT, WILL, WONT */ |
| - | byte charmode; |
| - | byte telflags; |
| - | byte do_termios; |
| - | #if ENABLE_FEATURE_TELNET_TTYPE |
| - | char *ttype; |
| - | #endif |
| - | #if ENABLE_FEATURE_TELNET_AUTOLOGIN |
| - | const char *autologin; |
| - | #endif |
| - | #if ENABLE_FEATURE_TELNET_WIDTH |
| - | unsigned win_width, win_height; |
| + | typedef struct stdin_to_net { |
| + | STRUCT_CONNECTION |
| + | int rdidx, wridx, size; |
| + | byte buf[BUFSIZE]; |
| + | } stdin_to_net_t; |
| + | |
| + | typedef struct net_to_stdout { |
| + | STRUCT_CONNECTION |
| + | int rdidx, wridx, size; |
| + | byte input_state; |
| + | byte negotiation_verb; |
| + | byte buf[BUFSIZE]; |
| + | } net_to_stdout_t; |
| + | |
| + | static int remaining_free_bytes(int n) |
| + | { |
| + | return BUFSIZE - n; |
| + | } |
| + | |
| + | #if DEBUG |
| + | static void set_input_state(net_to_stdout_t *conn, int new_state, int c) |
| + | { |
| + | if (conn->input_state != new_state) { |
| + | conn->input_state = new_state; |
| + | dbg("input_state=%d at char:0x%02x '%c'", new_state, c, |
| + | c >= 32 && c < 127 ? c : '.'); |
| + | } |
| + | } |
| + | # define SET_INPUT_STATE(conn, state, c) set_input_state(conn, state, c) |
| + | #else |
| + | # define SET_INPUT_STATE(conn, state, c) ((conn)->input_state = (state)) |
| #endif |
| - | /* same buffer used both for network and console read/write */ |
| - | char buf[DATABUFSIZE]; |
| - | /* buffer to handle telnet negotiations */ |
| - | char iacbuf[IACBUFSIZE]; |
| + | |
| + | struct globals { |
| + | unsigned flags; |
| + | #define FLAGS_NAWS_ON (1 << 0) |
| + | #define FLAGS_SGA_SEEN (1 << 1) |
| + | /* Seen telnet protocol from server and sent our wishes: */ |
| + | #define INITIAL_SENT (1 << 2) |
| + | #define DO_TERMIOS (1 << 3) |
| + | |
| + | byte word_aligned_bytes[2]; |
| + | #define changed word_aligned_bytes[0] |
| + | #define CHANGED_ECHO (1 << 0) |
| + | #define CHANGED_NAWS (1 << 1) |
| + | /* These happen only once: */ |
| + | #define CHANGED_SGA (1 << 2) |
| + | #define CHANGED_TTYPE (1 << 3) |
| + | #define CHANGED_NEW_ENVIRON (1 << 4) |
| + | /* The second byte is changed async, by signal handler: */ |
| + | #define got_SIGWINCH word_aligned_bytes[1] |
| + | #define G_changed_word (*(uint16_t*)G.word_aligned_bytes) |
| + | |
| + | byte optstate_ECHO; |
| + | byte echo_sga_response_size; |
| + | |
| + | IF_FEATURE_TELNET_TTYPE( const char *ttype;) |
| + | IF_FEATURE_TELNET_AUTOLOGIN(const char *autologin;) |
| + | ioloop_state_t io; |
| + | stdin_to_net_t conn_stdin2net; |
| + | net_to_stdout_t conn_net2stdout; |
| struct termios termios_def; |
| struct termios termios_raw; |
| } FIX_ALIASING; |
| - | #define G (*(struct globals*)bb_common_bufsiz1) |
| + | #define G (*ptr_to_globals) |
| #define INIT_G() do { \ |
| - | setup_common_bufsiz(); \ |
| - | BUILD_BUG_ON(sizeof(G) > COMMON_BUFSIZE); \ |
| + | SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \ |
| } while (0) |
| |
| |
| - | static void rawmode(void); |
| - | static void cookmode(void); |
| - | static void do_linemode(void); |
| - | static void will_charmode(void); |
| - | static void telopt(byte c); |
| - | static void subneg(byte c); |
| - | |
| - | static void iac_flush(void) |
| - | { |
| - | if (G.iaclen != 0) { |
| - | full_write(netfd, G.iacbuf, G.iaclen); |
| - | G.iaclen = 0; |
| - | } |
| - | } |
| - | |
| - | static void doexit(int ev) NORETURN; |
| - | static void doexit(int ev) |
| + | #if ENABLE_FEATURE_TELNET_WIDTH |
| + | static void handle_SIGWINCH(int sig UNUSED_PARAM) |
| { |
| - | cookmode(); |
| - | exit(ev); |
| + | if (G.flags & FLAGS_NAWS_ON) /* Only if server okayed NAWS */ |
| + | G.got_SIGWINCH = 1; |
| } |
| + | #endif |
| |
| - | static void con_escape(void) |
| + | static void rawmode(void) |
| { |
| - | char b; |
| - | |
| - | if (bb_got_signal) /* came from line mode... go raw */ |
| - | rawmode(); |
| - | |
| - | full_write1_str("\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(STDIN_FILENO, &b, 1) <= 0) |
| - | doexit(EXIT_FAILURE); |
| - | |
| - | switch (b) { |
| - | case 'l': |
| - | if (!bb_got_signal) { |
| - | do_linemode(); |
| - | goto ret; |
| - | } |
| - | break; |
| - | case 'c': |
| - | if (bb_got_signal) { |
| - | will_charmode(); |
| - | goto ret; |
| - | } |
| - | break; |
| - | case 'z': |
| - | cookmode(); |
| - | kill(0, SIGTSTP); |
| - | rawmode(); |
| - | break; |
| - | case 'e': |
| - | doexit(EXIT_SUCCESS); |
| - | } |
| - | |
| - | full_write1_str("continuing...\r\n"); |
| - | |
| - | if (bb_got_signal) |
| - | cookmode(); |
| - | ret: |
| - | bb_got_signal = 0; |
| + | if (G.flags & DO_TERMIOS) |
| + | tcsetattr(0, TCSADRAIN, &G.termios_raw); |
| } |
| |
| - | static void handle_net_output(int len) |
| + | static void cookmode(void) |
| { |
| - | byte outbuf[2 * DATABUFSIZE]; |
| - | byte *dst = outbuf; |
| - | byte *src = (byte*)G.buf; |
| - | byte *end = src + len; |
| - | |
| - | while (src < end) { |
| - | byte c = *src++; |
| - | if (c == 0x1d) { |
| - | con_escape(); |
| - | return; |
| - | } |
| - | *dst = c; |
| - | if (c == IAC) |
| - | *++dst = c; /* IAC -> IAC IAC */ |
| - | else |
| - | if (c == '\r' || c == '\n') { |
| - | /* Enter key sends '\r' in raw mode and '\n' in cooked one. |
| - | * |
| - | * See RFC 1123 3.3.1 Telnet End-of-Line Convention. |
| - | * Using CR LF instead of other allowed possibilities |
| - | * like CR NUL - easier to talk to HTTP/SMTP servers. |
| - | */ |
| - | *dst = '\r'; /* Enter -> CR LF */ |
| - | *++dst = '\n'; |
| - | } |
| - | #if 0 |
| - | /* putty's "special commands" mode does this: */ |
| - | /* Korenix 3005 switch needs at least the backspace tweak */ |
| - | if (c == 0x08 || c == 0x7f) { /* ctrl+h || backspace */ |
| - | *dst = IAC; |
| - | *++dst = EC; |
| - | } |
| - | if (c == 0x03) { /* ctrl+c */ |
| - | *dst = IAC; |
| - | *++dst = IP; |
| - | } |
| - | #endif |
| - | dst++; |
| - | } |
| - | if (dst - outbuf != 0) |
| - | full_write(netfd, outbuf, dst - outbuf); |
| + | if (G.flags & DO_TERMIOS) |
| + | tcsetattr(0, TCSADRAIN, &G.termios_def); |
| } |
| |
| - | static void handle_net_input(int len) |
| + | static void doexit(int ev) NORETURN; |
| + | static void doexit(int ev) |
| { |
| - | byte c; |
| - | int i; |
| - | int cstart = 0; |
| - | |
| - | i = 0; |
| - | //bb_error_msg("[%u,'%.*s']", G.telstate, len, G.buf); |
| - | if (G.telstate == TS_NORMAL) { /* most typical state */ |
| - | while (i < len) { |
| - | c = G.buf[i]; |
| - | i++; |
| - | if (c == IAC) /* unlikely */ |
| - | goto got_IAC; |
| - | if (c != '\r') /* likely */ |
| - | continue; |
| - | G.telstate = TS_CR; |
| - | cstart = i; |
| - | goto got_special; |
| - | } |
| - | full_write(STDOUT_FILENO, G.buf, len); |
| - | return; |
| - | got_IAC: |
| - | G.telstate = TS_IAC; |
| - | cstart = i - 1; |
| - | got_special: ; |
| - | } |
| - | |
| - | for (; i < len; i++) { |
| - | c = G.buf[i]; |
| - | |
| - | switch (G.telstate) { |
| - | case TS_CR: |
| - | /* Prev char was CR. If cur one is NUL, ignore it. |
| - | * See RFC 1123 section 3.3.1 for discussion of telnet EOL handling. |
| - | */ |
| - | G.telstate = TS_COPY; |
| - | if (c == '\0') |
| - | break; |
| - | /* else: fall through - need to handle CR IAC ... properly */ |
| - | |
| - | case TS_COPY: /* Prev char was ordinary */ |
| - | /* Similar to NORMAL, but in TS_COPY we need to copy bytes */ |
| - | if (c == IAC) |
| - | G.telstate = TS_IAC; |
| - | else { |
| - | G.buf[cstart++] = c; |
| - | if (c == '\r') |
| - | G.telstate = TS_CR; |
| - | } |
| - | break; |
| - | |
| - | case TS_IAC: /* Prev char was IAC */ |
| - | switch (c) { |
| - | case IAC: /* IAC IAC -> one IAC */ |
| - | G.buf[cstart++] = c; |
| - | G.telstate = TS_COPY; |
| - | break; |
| - | case SB: |
| - | G.telstate = TS_SUB1; |
| - | break; |
| - | case DO: |
| - | case DONT: |
| - | case WILL: |
| - | case WONT: |
| - | G.telwish = c; |
| - | G.telstate = TS_OPT; |
| - | break; |
| - | /* DATA MARK must be added later */ |
| - | default: |
| - | G.telstate = TS_COPY; |
| - | } |
| - | break; |
| - | |
| - | case TS_OPT: /* Prev chars were IAC WILL/WONT/DO/DONT */ |
| - | telopt(c); |
| - | G.telstate = TS_COPY; |
| - | break; |
| - | |
| - | case TS_SUB1: /* Subnegotiation */ |
| - | case TS_SUB2: /* Subnegotiation */ |
| - | subneg(c); /* can change G.telstate */ |
| - | break; |
| - | } |
| - | } |
| - | |
| - | /* We had some IACs, or CR */ |
| - | iac_flush(); |
| - | if (G.telstate == TS_COPY) /* we aren't in the middle of IAC */ |
| - | G.telstate = TS_NORMAL; |
| - | if (cstart != 0) |
| - | full_write(STDOUT_FILENO, G.buf, cstart); |
| + | cookmode(); |
| + | exit(ev); |
| } |
| |
| static void put_iac(int c) |
| { |
| - | int iaclen = G.iaclen; |
| - | if (iaclen >= IACBUFSIZE) { |
| - | iac_flush(); |
| - | iaclen = 0; |
| - | } |
| - | G.iacbuf[iaclen] = c; /* "... & 0xff" is implicit */ |
| - | G.iaclen = iaclen + 1; |
| + | stdin_to_net_t *conn = &G.conn_stdin2net; |
| + | /* Write directly to stdin2net buffer */ |
| + | conn->buf[conn->rdidx] = c; /* "... & 0xff" is implicit */ |
| + | conn->rdidx = (conn->rdidx + 1) & BUFMASK; |
| + | conn->size++; |
| } |
| |
| static void put_iac2_msb_lsb(unsigned x_y) |
| @@ -376,7 +241,17 @@ static void put_iac4_msb_lsb(unsigned x_y_z_t) |
| put_iac2_msb_lsb(x_y_z_t >> 16); |
| put_iac2_msb_lsb(x_y_z_t); /* "... & 0xffff" is implicit */ |
| } |
| - | #define put_iac4_x_y_z_t(x,y,z,t) put_iac4_msb_lsb(((x)<<24) + ((y)<<16) + ((z)<<8) + (t)) |
| + | #define put_iac4_x_y_z_t(x,y,z,t) \ |
| + | put_iac4_msb_lsb(((x)<<24) + ((y)<<16) + ((z)<<8) + (t)) |
| + | |
| + | /* Send a byte in subnegotiation, escaping IAC (0xFF) as IAC IAC */ |
| + | static void put_iac_byte_escaped(int c) |
| + | { |
| + | c = (byte)c; |
| + | put_iac(c); |
| + | if (c == IAC) |
| + | put_iac(IAC); |
| + | } |
| #endif |
| |
| static void put_iac3_IAC_x_y_merged(unsigned wwdd_and_c) |
| @@ -384,237 +259,595 @@ static void put_iac3_IAC_x_y_merged(unsigned wwdd_and_c) |
| put_iac(IAC); |
| put_iac2_msb_lsb(wwdd_and_c); |
| } |
| - | #define put_iac3_IAC_x_y(wwdd,c) put_iac3_IAC_x_y_merged(((wwdd)<<8) + (c)) |
| + | #define put_iac3_IAC_x_y(wwdd,c) \ |
| + | put_iac3_IAC_x_y_merged(((wwdd)<<8) + (c)) |
| |
| #if ENABLE_FEATURE_TELNET_TTYPE |
| - | static void put_iac_subopt(byte c, char *str) |
| + | static void put_iac_subopt(byte c, const char *str) |
| { |
| put_iac4_x_y_z_t(IAC, SB, c, 0); |
| |
| - | while (*str) |
| - | put_iac(*str++); |
| + | while (*str) { |
| + | put_iac_byte_escaped(*str); |
| + | str++; |
| + | } |
| |
| put_iac2_x_y(IAC, SE); |
| } |
| #endif |
| |
| #if ENABLE_FEATURE_TELNET_AUTOLOGIN |
| - | static void put_iac_subopt_autologin(void) |
| + | static void put_iac_subopt_autologin(const char *p) |
| { |
| - | const char *p; |
| - | |
| put_iac4_x_y_z_t(IAC, SB, TELOPT_NEW_ENVIRON, TELQUAL_IS); |
| put_iac4_x_y_z_t(NEW_ENV_VAR, 'U', 'S', 'E'); /* "USER" */ |
| put_iac2_x_y('R', NEW_ENV_VALUE); |
| |
| - | p = G.autologin; |
| while (*p) |
| - | put_iac(*p++); |
| + | put_iac_byte_escaped(*p++); |
| |
| put_iac2_x_y(IAC, SE); |
| } |
| #endif |
| |
| #if ENABLE_FEATURE_TELNET_WIDTH |
| - | static void put_iac_naws(byte c, int x, int y) |
| + | static void put_iac_naws(void) |
| { |
| - | put_iac3_IAC_x_y(SB, c); |
| + | unsigned width, height; |
| + | |
| + | put_iac3_IAC_x_y(SB, TELOPT_NAWS); |
| |
| - | put_iac4_msb_lsb((x << 16) + y); |
| + | /* Send width and height as 16-bit big-endian, escaping IAC bytes */ |
| + | get_terminal_width_height(0, &width, &height); |
| + | put_iac_byte_escaped(width >> 8); |
| + | put_iac_byte_escaped(width); |
| + | put_iac_byte_escaped(height >> 8); |
| + | put_iac_byte_escaped(height); |
| |
| put_iac2_x_y(IAC, SE); |
| } |
| #endif |
| |
| - | static void setConMode(void) |
| + | // Telnet option handling strategy: |
| + | // |
| + | // We send nothing on startup by our own (think "telnet www.kernel.org 80"). |
| + | // As soon as we see ECHO or SGA message, we respond to them and then |
| + | // send "IAC WILL NAWS", "IAC WILL TTYPE", "IAC WILL NEW_ENVIRON" |
| + | // without waiting for server to advertise those options. |
| + | // (Why? servers may decide to not advertise all 123 options they know). |
| + | // |
| + | // TELOPT_ECHO (1) - remote: server echoes our keystrokes back to us |
| + | // - Server says: "IAC WILL ECHO" or "IAC WONT ECHO" |
| + | // - If we don't see it, or see WONT: line mode (local terminal echoes), aka cooked mode |
| + | // - When active: We're in character mode (server echoes), aka raw mode |
| + | // - Response logic: |
| + | // - Server says WILL ECHO: we send "IAC DO ECHO", enter raw mode |
| + | // (this is the most usual case for non-ancient servers) |
| + | // - Server says WONT ECHO: we send "IAC DONT ECHO", enter cooked mode |
| + | // - We mirror server's changes, we do allow it to change during session |
| + | // unlike one-shot options which are negotiated once during startup. |
| + | // - If user changes mode from ^] menu, we send corresponding "IAC DO/DONT ECHO". |
| + | // |
| + | // TELOPT_SGA (3) - remote: "Suppress Go Ahead", full-duplex |
| + | // - Server says: "IAC WILL SGA" or "IAC WONT SGA" |
| + | // - Response logic: one-shot. |
| + | // - We expect all sane servers to say "IAC WILL SGA" at startup. |
| + | // - If that happens, we send "IAC DO SGA". After that, we never react to any |
| + | // further SGA messages (we expect them to not happen in practice) |
| + | // |
| + | // TELOPT_NAWS (31) - Window Size - local: we send our window size |
| + | // - If server says: "IAC DO NAWS", it understands NAWS. We check this. |
| + | // - We send window size during initial handshake and on window resize |
| + | // |
| + | // TELOPT_TTYPE (24) - Terminal Type - local: we send our terminal type |
| + | // - Response logic: one-shot. |
| + | // - If server says DO TTYPE: send "IAC SB TTYPE 0 <$TERM> IAC SE" |
| + | // |
| + | // TELOPT_NEW_ENVIRON (39) - Environment (Autologin) - local: we send our username |
| + | // - Response logic: one-shot. |
| + | // - If server says DO NEW_ENVIRON: send "IAC SB NEW_ENVIRON IS VAR USER VALUE <name> IAC SE" |
| + | // |
| + | // Unknown options: |
| + | // Response: NONE - just ignore them |
| + | // By protocol definition, an option is not in effect until BOTH sides agree. |
| + | // We don't respond to unknown options - they simply won't be in effect. |
| + | static void handle_changes_in_options(stdin_to_net_t *conn) |
| { |
| - | if (G.telflags & UF_ECHO) { |
| - | if (G.charmode == CHM_TRY) { |
| - | G.charmode = CHM_ON; |
| - | printf("\r\nEntering %s mode" |
| - | "\r\nEscape character is '^%c'.\r\n", "character", ']'); |
| - | rawmode(); |
| + | int count; |
| + | |
| + | //bb_error_msg("changed:%x flags:%x", G.changed, G.flags); |
| + | |
| + | count = remaining_free_bytes(conn->size); |
| + | /* As soon as we see either ECHO or SGA from server, |
| + | * we assume it *is* a telnet server |
| + | * (not in "telnet www.kernel.org 80" scenario), |
| + | * and we respond to them, also expressing our own |
| + | * wishes: "WILL NAWS" etc. |
| + | */ |
| + | if ((G.changed & (CHANGED_ECHO|CHANGED_SGA)) |
| + | && (count >= G.echo_sga_response_size) |
| + | ) { |
| + | if (G.changed & CHANGED_ECHO) { |
| + | /* Server said WILL/WONT - confirm */ |
| + | put_iac3_IAC_x_y(G.optstate_ECHO ? DO : DONT, TELOPT_ECHO); |
| + | } |
| + | if (G.changed & CHANGED_SGA) { |
| + | /* Server said WILL - send DO */ |
| + | put_iac3_IAC_x_y(DO, TELOPT_SGA); |
| + | G.flags |= FLAGS_SGA_SEEN; /* remember we did it */ |
| + | G.changed -= CHANGED_SGA; |
| + | } |
| + | G.changed &= ~(CHANGED_ECHO|CHANGED_SGA); |
| + | |
| + | if (!(G.flags & INITIAL_SENT)) { |
| + | G.flags |= INITIAL_SENT; |
| + | G.echo_sga_response_size = 6; /* from now on, we'll need only 6 */ |
| + | |
| + | /* Send initial IAC sequences for local options we want to advertise */ |
| + | #if ENABLE_FEATURE_TELNET_WIDTH |
| + | put_iac3_IAC_x_y(WILL, TELOPT_NAWS); |
| + | #endif |
| + | #if ENABLE_FEATURE_TELNET_TTYPE |
| + | if (G.ttype) |
| + | put_iac3_IAC_x_y(WILL, TELOPT_TTYPE); |
| + | #endif |
| + | #if ENABLE_FEATURE_TELNET_AUTOLOGIN |
| + | if (G.autologin) |
| + | put_iac3_IAC_x_y(WILL, TELOPT_NEW_ENVIRON); |
| + | #endif |
| } |
| - | } else { |
| - | if (G.charmode != CHM_OFF) { |
| - | G.charmode = CHM_OFF; |
| - | printf("\r\nEntering %s mode" |
| - | "\r\nEscape character is '^%c'.\r\n", "line", 'C'); |
| - | cookmode(); |
| + | } |
| + | |
| + | #if ENABLE_FEATURE_TELNET_WIDTH |
| + | if (remaining_free_bytes(conn->size) > MAX_NAWS_SIZE) { |
| + | if (G.changed & CHANGED_NAWS) { |
| + | G.flags |= FLAGS_NAWS_ON; /* remember we did it */ |
| + | G.changed -= CHANGED_NAWS; |
| + | goto generate_naws; |
| } |
| + | /* Handle window resize: send updated NAWS if we have room */ |
| + | if (G.got_SIGWINCH) { |
| + | generate_naws: |
| + | G.got_SIGWINCH = 0; |
| + | /* Clear the flag before put_iac_naws() to avoid race! */ |
| + | put_iac_naws(); |
| + | } |
| + | } |
| + | #endif |
| + | #if ENABLE_FEATURE_TELNET_TTYPE |
| + | if ((G.changed & CHANGED_TTYPE) |
| + | && remaining_free_bytes(conn->size) > 2 * strlen(G.ttype) |
| + | ) { |
| + | put_iac_subopt(TELOPT_TTYPE, G.ttype); |
| + | G.ttype = NULL; /* remember we did it */ |
| + | G.changed -= CHANGED_TTYPE; |
| + | } |
| + | #endif |
| + | #if ENABLE_FEATURE_TELNET_AUTOLOGIN |
| + | if ((G.changed & CHANGED_NEW_ENVIRON) |
| + | && remaining_free_bytes(conn->size) > 2 * strlen(G.autologin) |
| + | ) { |
| + | put_iac_subopt_autologin(G.autologin); |
| + | G.autologin = NULL; /* remember we did it */ |
| + | G.changed -= CHANGED_NEW_ENVIRON; |
| } |
| + | #endif |
| } |
| |
| - | static void will_charmode(void) |
| + | static void announce_rawmode(void) |
| { |
| - | G.charmode = CHM_TRY; |
| - | G.telflags |= (UF_ECHO | UF_SGA); |
| - | setConMode(); |
| - | |
| - | put_iac3_IAC_x_y(DO, TELOPT_ECHO); |
| - | put_iac3_IAC_x_y(DO, TELOPT_SGA); |
| - | iac_flush(); |
| + | printf("\r\nEntering %s mode" |
| + | "\r\nEscape character is '^%c'.\r\n", "character", ']' |
| + | ); |
| + | rawmode(); |
| } |
| - | |
| - | static void do_linemode(void) |
| + | static void announce_and_switch_to_rawmode(void) |
| { |
| - | G.charmode = CHM_TRY; |
| - | G.telflags &= ~(UF_ECHO | UF_SGA); |
| - | setConMode(); |
| - | |
| - | put_iac3_IAC_x_y(DONT, TELOPT_ECHO); |
| - | put_iac3_IAC_x_y(DONT, TELOPT_SGA); |
| - | iac_flush(); |
| + | announce_rawmode(); |
| + | rawmode(); |
| } |
| - | |
| - | static void to_notsup(char c) |
| + | static void announce_and_switch_to_cookmode(void) |
| { |
| - | if (G.telwish == WILL) |
| - | put_iac3_IAC_x_y(DONT, c); |
| - | else if (G.telwish == DO) |
| - | put_iac3_IAC_x_y(WONT, c); |
| + | printf("\r\nEntering %s mode" |
| + | "\r\nEscape character is '^%c'.\r\n", "line", 'C' |
| + | ); |
| + | cookmode(); |
| } |
| |
| - | static void to_echo(void) |
| + | static void show_menu(void) |
| { |
| - | /* if server requests ECHO, don't agree */ |
| - | if (G.telwish == DO) { |
| - | put_iac3_IAC_x_y(WONT, TELOPT_ECHO); |
| - | return; |
| + | char b; |
| + | |
| + | rawmode(); |
| + | |
| + | full_write1_str("\r\nConsole escape. Commands are:\r\n" |
| + | "l go to line mode\r\n" |
| + | "c go to character mode\r\n" |
| + | "z suspend telnet\r\n" |
| + | "e exit\r\n"); |
| + | if (read(STDIN_FILENO, &b, 1) <= 0 |
| + | || b == 'e' |
| + | ) { |
| + | doexit(EXIT_FAILURE); |
| } |
| - | if (G.telwish == DONT) |
| - | return; |
| |
| - | if (G.telflags & UF_ECHO) { |
| - | if (G.telwish == WILL) |
| + | switch (b) { |
| + | case 'l': |
| + | if (G.optstate_ECHO) { |
| + | G.optstate_ECHO = 0; |
| + | if (G.flags & INITIAL_SENT) |
| + | G.changed |= CHANGED_ECHO; /* inform the server at next send */ |
| + | announce_and_switch_to_cookmode(); |
| + | return; |
| + | } |
| + | break; |
| + | case 'c': |
| + | if (!G.optstate_ECHO) { |
| + | G.optstate_ECHO = 1; |
| + | if (G.flags & INITIAL_SENT) |
| + | G.changed |= CHANGED_ECHO; /* inform the server at next send */ |
| + | announce_rawmode(); /* no "_and_switch_": we are already in rawmode */ |
| return; |
| - | } else if (G.telwish == WONT) |
| - | return; |
| + | } |
| + | break; |
| + | case 'z': |
| + | cookmode(); |
| + | kill(0, SIGTSTP); |
| + | rawmode(); |
| + | break; |
| + | } |
| |
| - | if (G.charmode != CHM_OFF) |
| - | G.telflags ^= UF_ECHO; |
| + | full_write1_str("continuing...\r\n"); |
| |
| - | if (G.telflags & UF_ECHO) |
| - | put_iac3_IAC_x_y(DO, TELOPT_ECHO); |
| - | else |
| - | put_iac3_IAC_x_y(DONT, TELOPT_ECHO); |
| + | if (!G.optstate_ECHO) |
| + | cookmode(); |
| + | } |
| |
| - | setConMode(); |
| - | full_write1_str("\r\n"); /* sudden modec */ |
| + | static int have_buffer_to_read_from_stdin(void *this) |
| + | { |
| + | stdin_to_net_t *conn = this; |
| + | if (conn->read_fd < 0) |
| + | return 0; |
| + | return conn->size < BUFSIZE; |
| } |
| |
| - | static void to_sga(void) |
| + | static int read_from_stdin(void *this) |
| { |
| - | /* daemon always sends will/wont, client do/dont */ |
| + | stdin_to_net_t *conn = this; |
| + | int count, rem, expand_count; |
| + | byte *start, *src, *end; |
| + | byte c; |
| |
| - | if (G.telflags & UF_SGA) { |
| - | if (G.telwish == WILL) |
| - | return; |
| - | } else if (G.telwish == WONT) |
| - | return; |
| - | |
| - | G.telflags ^= UF_SGA; /* toggle */ |
| - | if (G.telflags & UF_SGA) |
| - | put_iac3_IAC_x_y(DO, TELOPT_SGA); |
| - | else |
| - | put_iac3_IAC_x_y(DONT, TELOPT_SGA); |
| + | //if (conn->read_fd < 0) |
| + | // return 0; /* Already stopped reading stdin */ |
| + | //ioloop_run() guarantees it won't call us with fd < 0 |
| + | |
| + | count = BUFSIZE - conn->size; |
| + | count = MIN(BUFSIZE - conn->rdidx, count); |
| + | count /= 2; /* Reserve room for worst-case expansion */ |
| + | if (count == 0) |
| + | return 0; |
| + | |
| + | start = conn->buf + conn->rdidx; |
| + | count = safe_read(conn->read_fd, start, count); |
| + | if (count <= 0) { |
| + | conn->read_fd = -1; |
| + | return 0; /* Error or EOF - didn't read anything */ |
| + | } |
| + | |
| + | /* First pass: scan forward counting characters that need expansion */ |
| + | src = start; |
| + | expand_count = 0; |
| + | rem = count; /* Remaining bytes to scan */ |
| + | do { |
| + | c = *src++; |
| + | if (c == 0x1d) { |
| + | /* Escape character - process bytes before it, then handle escape */ |
| + | count -= rem; /* Drop the remaining tail */ |
| + | #define found_escape (rem != 0) |
| + | break; |
| + | } |
| + | if (c == IAC) { |
| + | expand_count++; /* IAC -> IAC IAC (one extra byte) */ |
| + | } else if (c == '\r' || c == '\n') { |
| + | expand_count++; /* \r or \n -> \r\n (one extra byte) */ |
| + | } |
| + | } while (--rem != 0); |
| + | |
| + | if (expand_count != 0) { |
| + | /* Slow path: expand in place working backwards */ |
| + | src = start + count - 1; /* Last read byte */ |
| + | end = src + expand_count; /* Last byte after expansion */ |
| + | |
| + | /* As soon as src == end, the remaining bytes do not need processing (think about it!) */ |
| + | while (src < end) { |
| + | c = *src--; |
| + | if (c == IAC) { |
| + | *end-- = IAC; |
| + | } else if (c == '\r' || c == '\n') { |
| + | *end-- = '\n'; |
| + | c = '\r'; |
| + | } |
| + | *end-- = c; |
| + | } |
| + | count += expand_count; |
| + | } |
| + | |
| + | conn->size += count; |
| + | conn->rdidx = (conn->rdidx + count) & BUFMASK; |
| + | |
| + | if (found_escape) |
| + | show_menu(); |
| + | #undef found_escape |
| + | |
| + | return count; |
| } |
| |
| - | #if ENABLE_FEATURE_TELNET_TTYPE |
| - | static void to_ttype(void) |
| + | static int have_data_to_write_to_net(void *this) |
| { |
| - | /* Tell server we will (or won't) do TTYPE */ |
| - | if (G.ttype) |
| - | put_iac3_IAC_x_y(WILL, TELOPT_TTYPE); |
| - | else |
| - | put_iac3_IAC_x_y(WONT, TELOPT_TTYPE); |
| + | stdin_to_net_t *conn = this; |
| + | if (conn->size == 0 && conn->read_fd < 0) { |
| + | /* buffer drained and stdin EOF - signal EOF to server */ |
| + | shutdown(conn->write_fd, SHUT_WR); |
| + | /* Remove this pipe */ |
| + | ioloop_remove_conn(conn->io, (connection_t*)conn); |
| + | return -1; |
| + | } |
| + | return conn->size > 0 || G_changed_word != 0; |
| } |
| - | #endif |
| |
| - | #if ENABLE_FEATURE_TELNET_AUTOLOGIN |
| - | static void to_new_environ(void) |
| + | static int write_to_net(void *this) |
| { |
| - | /* Tell server we will (or will not) do AUTOLOGIN */ |
| - | if (G.autologin) |
| - | put_iac3_IAC_x_y(WILL, TELOPT_NEW_ENVIRON); |
| - | else |
| - | put_iac3_IAC_x_y(WONT, TELOPT_NEW_ENVIRON); |
| + | stdin_to_net_t *conn = this; |
| + | int count; |
| + | |
| + | /* Do we have option or NAWS changes to handle? */ |
| + | if (G_changed_word) |
| + | handle_changes_in_options(conn); /* yes */ |
| + | |
| + | count = MIN(BUFSIZE - conn->wridx, conn->size); |
| + | count = safe_write(conn->write_fd, conn->buf + conn->wridx, count); |
| + | if (count <= 0) { |
| + | if (count < 0 && errno == EAGAIN) |
| + | return 0; |
| + | full_write1_str("Error writing to foreign host\r\n"); |
| + | ioloop_remove_conn(conn->io, (connection_t*)conn); |
| + | return -1; /* "I'm gone" */ |
| + | } |
| + | |
| + | conn->wridx = (conn->wridx + count) & BUFMASK; |
| + | conn->size -= count; |
| + | if (conn->size == 0) { |
| + | conn->rdidx = 0; |
| + | conn->wridx = 0; |
| + | } |
| + | return count; |
| } |
| - | #endif |
| |
| - | #if ENABLE_FEATURE_TELNET_WIDTH |
| - | static void to_naws(void) |
| + | static int have_buffer_to_read_from_net(void *this) |
| { |
| - | /* Tell server we will do NAWS */ |
| - | put_iac3_IAC_x_y(WILL, TELOPT_NAWS); |
| + | net_to_stdout_t *conn = this; |
| + | if (conn->read_fd < 0) |
| + | return 0; |
| + | return conn->size < BUFSIZE; |
| } |
| - | #endif |
| |
| - | static void telopt(byte c) |
| + | static int read_from_net(void *this) |
| { |
| - | switch (c) { |
| - | case TELOPT_ECHO: |
| - | to_echo(); break; |
| - | case TELOPT_SGA: |
| - | to_sga(); break; |
| + | net_to_stdout_t *conn = this; |
| + | int count; |
| + | byte *src, *dst; |
| + | byte c; |
| + | byte oldstate_ECHO; |
| + | |
| + | count = BUFSIZE - conn->size; |
| + | //if (count == 0) |
| + | // return 0; /* buffer full */ |
| + | //ioloop_run() ensures this does not happen |
| + | count = MIN(BUFSIZE - conn->rdidx, count); /* can't be zero */ |
| + | |
| + | /* Read directly into circular buffer's linear fragment */ |
| + | dst = conn->buf + conn->rdidx; |
| + | count = safe_read(conn->read_fd, dst, count); |
| + | dbg("read_from_net bytes:%d input_state:%d %s", |
| + | count, conn->input_state, bin_to_hex(dst, count > 0 ? count : 0)); |
| + | if (count <= 0) { |
| + | if (count < 0 && errno == EAGAIN) |
| + | return 0; |
| + | full_write1_str("EOF on read from foreign host\r\n"); |
| + | conn->read_fd = -1; |
| + | /* Imagine scenario: "cat" is running in the server. |
| + | * We connect and press ^D. Byte 0x04 is transmitted to "cat", |
| + | * it sees that as EOF and exits. |
| + | * We see EOF here (netfd read side is closed). |
| + | * Our stdin is still open, but we have no buffered data to write to net. |
| + | * ioloop_run() will not _poll_ the netfd for writing. |
| + | * We will not realize that netfd write side is also closed (!!!) |
| + | * until something is typed in our stdin and we poll netfd to write that data. |
| + | */ |
| + | //FIXME: send a few IAC NOPs to see whether the peer can still receive? |
| + | /* This is a workaround: pretend that our stdin is also closed: */ |
| + | G.conn_stdin2net.read_fd = -1; |
| + | /* Unlike just exiting, this will try to send any buffered data */ |
| + | |
| + | return 0; |
| + | } |
| + | |
| + | /* Copy option states - will be updated during processing, then compared after */ |
| + | oldstate_ECHO = G.optstate_ECHO; |
| + | |
| + | /* Optimization: do not load/store-in-place if unnecessary */ |
| + | if (conn->input_state == TS_NORMAL) { |
| + | while (count != 0 && *dst != IAC && *dst != '\r') |
| + | count--, dst++; |
| + | } |
| + | /* Process IAC sequences in place: |
| + | * - Update option states (G.oldstate_XYZ) |
| + | * - Decode/remove IAC sequences, compacting the data |
| + | * - src = read pointer, dst = write pointer (for compaction) |
| + | */ |
| + | src = dst; |
| + | while (--count >= 0) { |
| + | c = *src++; |
| + | |
| + | switch (conn->input_state) { |
| + | int will; |
| + | case TS_NORMAL: |
| + | normal: |
| + | if (c == IAC) { |
| + | SET_INPUT_STATE(conn, TS_IAC, c); |
| + | continue; |
| + | } |
| + | if (c == '\r') { |
| + | SET_INPUT_STATE(conn, TS_CR, c); |
| + | } |
| + | *dst++ = c; |
| + | continue; |
| + | |
| + | case TS_CR: |
| + | SET_INPUT_STATE(conn, TS_NORMAL, c); |
| + | if (c == '\0') /* Skip NUL after CR (telnet EOL: CR NUL) */ |
| + | continue; |
| + | goto normal; |
| + | |
| + | case TS_IAC: |
| + | if (c == IAC) { |
| + | /* IAC IAC -> single IAC */ |
| + | *dst++ = c; |
| + | SET_INPUT_STATE(conn, TS_NORMAL, c); |
| + | } else if (c == SB) { |
| + | conn->negotiation_verb = 0xff; /* reuse as counter */ |
| + | SET_INPUT_STATE(conn, TS_SUB1, c); |
| + | } else if (c == DO || c == DONT || c == WILL || c == WONT) { |
| + | conn->negotiation_verb = c; |
| + | SET_INPUT_STATE(conn, TS_OPT, c); |
| + | } else { |
| + | /* Unknown IAC command, ignore */ |
| + | SET_INPUT_STATE(conn, TS_NORMAL, c); |
| + | } |
| + | break; |
| + | |
| + | case TS_OPT: |
| + | /* Process option negotiation */ |
| + | will = (conn->negotiation_verb == WILL); |
| + | if (will || conn->negotiation_verb == WONT) { |
| + | switch (c) { |
| + | case TELOPT_ECHO: /* Remote option: server echoes our typing */ |
| + | G.optstate_ECHO = will; |
| + | break; |
| + | case TELOPT_SGA: /* Remote option: "suppress go ahead" */ |
| + | if (will && !(G.flags & FLAGS_SGA_SEEN)) |
| + | G.changed |= CHANGED_SGA; |
| + | break; |
| + | } |
| + | } else if (conn->negotiation_verb == DO) { |
| + | switch (c) { |
| #if ENABLE_FEATURE_TELNET_TTYPE |
| - | case TELOPT_TTYPE: |
| - | to_ttype(); break; |
| + | case TELOPT_TTYPE: /* Local option: we send terminal type */ |
| + | if (G.ttype) |
| + | G.changed |= CHANGED_TTYPE; |
| + | break; |
| #endif |
| #if ENABLE_FEATURE_TELNET_AUTOLOGIN |
| - | case TELOPT_NEW_ENVIRON: |
| - | to_new_environ(); break; |
| + | case TELOPT_NEW_ENVIRON: /* Local option: we send username */ |
| + | if (G.autologin) |
| + | G.changed |= CHANGED_NEW_ENVIRON; |
| + | break; |
| #endif |
| #if ENABLE_FEATURE_TELNET_WIDTH |
| - | case TELOPT_NAWS: |
| - | to_naws(); |
| - | put_iac_naws(c, G.win_width, G.win_height); |
| - | break; |
| + | case TELOPT_NAWS: /* Local option: we send window size */ |
| + | if (!(G.flags & FLAGS_NAWS_ON)) |
| + | G.changed |= CHANGED_NAWS; |
| + | break; |
| #endif |
| - | default: |
| - | to_notsup(c); |
| - | break; |
| + | } |
| + | } |
| + | /* else: "DONT <something>": ignore */ |
| + | SET_INPUT_STATE(conn, TS_NORMAL, c); |
| + | break; |
| + | |
| + | case TS_SUB1: |
| + | /* Avoid being stuck in TS_SUB1 forever (with detours into TS_SUB2) |
| + | * if IAC SE is never seen (buggy server response?). |
| + | */ |
| + | if (--conn->negotiation_verb == 0) { |
| + | dbg("unterminated SB seen"); |
| + | SET_INPUT_STATE(conn, TS_NORMAL, c); |
| + | } else |
| + | /* Skip over subnegotiation bytes until we see IAC */ |
| + | if (c == IAC) { |
| + | SET_INPUT_STATE(conn, TS_SUB2, c); |
| + | } |
| + | break; |
| + | |
| + | case TS_SUB2: |
| + | /* After IAC in subnegotiation, check for SE */ |
| + | if (c == SE) { |
| + | /* End of subnegotiation */ |
| + | SET_INPUT_STATE(conn, TS_NORMAL, c); |
| + | } else { |
| + | /* IAC followed by something other than SE, back to SUB1 */ |
| + | SET_INPUT_STATE(conn, TS_SUB1, c); |
| + | } |
| + | break; |
| + | } |
| } |
| - | } |
| |
| - | /* subnegotiation -- ignore all (except TTYPE,NAWS) */ |
| - | static void subneg(byte c) |
| - | { |
| - | switch (G.telstate) { |
| - | case TS_SUB1: |
| - | if (c == IAC) |
| - | G.telstate = TS_SUB2; |
| - | #if ENABLE_FEATURE_TELNET_TTYPE |
| - | else |
| - | if (c == TELOPT_TTYPE && G.ttype) |
| - | put_iac_subopt(TELOPT_TTYPE, G.ttype); |
| - | #endif |
| - | #if ENABLE_FEATURE_TELNET_AUTOLOGIN |
| + | if (oldstate_ECHO != G.optstate_ECHO) { |
| + | /* Tell net writer to generate a confirmation */ |
| + | G.changed |= CHANGED_ECHO; |
| + | /* Print the banner and set termios */ |
| + | if (G.optstate_ECHO) |
| + | announce_and_switch_to_rawmode(); |
| else |
| - | if (c == TELOPT_NEW_ENVIRON && G.autologin) |
| - | put_iac_subopt_autologin(); |
| - | #endif |
| - | break; |
| - | case TS_SUB2: |
| - | if (c == SE) { |
| - | G.telstate = TS_COPY; |
| - | return; |
| - | } |
| - | G.telstate = TS_SUB1; |
| - | break; |
| + | announce_and_switch_to_cookmode(); |
| } |
| + | |
| + | /* Update circular buffer: only the compacted data */ |
| + | count = dst - (conn->buf + conn->rdidx); |
| + | conn->size += count; |
| + | conn->rdidx = (conn->rdidx + count) & BUFMASK; |
| + | |
| + | dbg("read_from_net: user bytes:%d input_state:%d", count, conn->input_state); |
| + | |
| + | return count; |
| } |
| |
| - | static void rawmode(void) |
| + | static int have_data_to_write_to_stdout(void *this) |
| { |
| - | if (G.do_termios) |
| - | tcsetattr(0, TCSADRAIN, &G.termios_raw); |
| + | net_to_stdout_t *conn = this; |
| + | if (conn->size == 0 && conn->read_fd < 0) { |
| + | /* buffer drained and network read EOF */ |
| + | /* Remove this pipe */ |
| + | ioloop_remove_conn(conn->io, (connection_t*)conn); |
| + | return -1; |
| + | } |
| + | return conn->size > 0; |
| } |
| |
| - | static void cookmode(void) |
| + | static int write_to_stdout(void *this) |
| { |
| - | if (G.do_termios) |
| - | tcsetattr(0, TCSADRAIN, &G.termios_def); |
| + | net_to_stdout_t *conn = this; |
| + | int wr = MIN(BUFSIZE - conn->wridx, conn->size); |
| + | ssize_t count; |
| + | |
| + | dbg("write_to_stdout: wr:%d %s", wr, bin_to_hex(conn->buf + conn->wridx, wr)); |
| + | count = safe_write(conn->write_fd, conn->buf + conn->wridx, wr); |
| + | if (count <= 0) { |
| + | if (count < 0 && errno == EAGAIN) |
| + | return 0; |
| + | //full_write1_str("Error writing to stdout\r\n"); |
| + | ioloop_remove_conn(conn->io, (connection_t*)conn); |
| + | return -1; /* "I'm gone" */ |
| + | } |
| + | |
| + | conn->wridx = (conn->wridx + count) & BUFMASK; |
| + | conn->size -= count; |
| + | if (conn->size == 0) { |
| + | conn->rdidx = 0; |
| + | conn->wridx = 0; |
| + | } |
| + | return count; |
| } |
| |
| int telnet_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; |
| @@ -622,21 +855,12 @@ int telnet_main(int argc UNUSED_PARAM, char **argv) |
| { |
| char *host; |
| int port; |
| - | int len; |
| - | struct pollfd ufds[2]; |
| |
| INIT_G(); |
| |
| #if ENABLE_FEATURE_TELNET_TTYPE |
| G.ttype = getenv("TERM"); |
| #endif |
| - | |
| - | if (tcgetattr(0, &G.termios_def) >= 0) { |
| - | G.do_termios = 1; |
| - | G.termios_raw = G.termios_def; |
| - | cfmakeraw(&G.termios_raw); |
| - | } |
| - | |
| #if ENABLE_FEATURE_TELNET_AUTOLOGIN |
| if (1 == getopt32(argv, "al:", &G.autologin)) { |
| /* Only -a without -l USER picks $USER from envvar */ |
| @@ -654,49 +878,72 @@ int telnet_main(int argc UNUSED_PARAM, char **argv) |
| if (*argv) /* extra params?? */ |
| bb_show_usage(); |
| |
| + | /* Save our termios */ |
| + | if (tcgetattr(0, &G.termios_def) >= 0) { |
| + | G.flags |= DO_TERMIOS; |
| + | G.termios_raw = G.termios_def; |
| + | cfmakeraw(&G.termios_raw); |
| + | } |
| + | |
| xmove_fd(create_and_connect_stream_or_die(host, port), netfd); |
| printf("Connected to %s\n", host); |
| |
| setsockopt_keepalive(netfd); |
| + | ndelay_on(netfd); |
| |
| #if ENABLE_FEATURE_TELNET_WIDTH |
| - | get_terminal_width_height(0, &G.win_width, &G.win_height); |
| - | //TODO: support dynamic resize? |
| + | signal(SIGWINCH, handle_SIGWINCH); |
| #endif |
| - | |
| signal(SIGINT, record_signo); |
| - | |
| - | ufds[0].fd = STDIN_FILENO; |
| - | ufds[0].events = POLLIN; |
| - | ufds[1].fd = netfd; |
| - | ufds[1].events = POLLIN; |
| - | |
| - | while (1) { |
| - | if (poll(ufds, 2, -1) < 0) { |
| - | /* error, ignore and/or log something, bay go to loop */ |
| - | if (bb_got_signal) |
| - | con_escape(); |
| - | else |
| - | sleep1(); |
| - | continue; |
| + | /* Without this, SIGPIPE was seen on loopback connections: */ |
| + | signal(SIGPIPE, SIG_IGN); |
| + | |
| + | /* Initialize connections */ |
| + | G.conn_stdin2net.have_buffer_to_read_into = have_buffer_to_read_from_stdin; |
| + | G.conn_stdin2net.have_data_to_write = have_data_to_write_to_net; |
| + | G.conn_stdin2net.read = read_from_stdin; |
| + | G.conn_stdin2net.write = write_to_net; |
| + | if (STDIN_FILENO != 0) |
| + | G.conn_stdin2net.read_fd = STDIN_FILENO; |
| + | G.conn_stdin2net.write_fd = netfd; |
| + | |
| + | G.conn_net2stdout.have_buffer_to_read_into = have_buffer_to_read_from_net; |
| + | G.conn_net2stdout.have_data_to_write = have_data_to_write_to_stdout; |
| + | G.conn_net2stdout.read = read_from_net; |
| + | G.conn_net2stdout.write = write_to_stdout; |
| + | G.conn_net2stdout.read_fd = netfd; |
| + | G.conn_net2stdout.write_fd = STDOUT_FILENO; |
| + | if (TS_NORMAL != 0) |
| + | G.conn_net2stdout.input_state = TS_NORMAL; |
| + | |
| + | ioloop_insert_conn(&G.io, (connection_t*)&G.conn_stdin2net); |
| + | ioloop_insert_conn(&G.io, (connection_t*)&G.conn_net2stdout); |
| + | |
| + | G.echo_sga_response_size = 3 * (2 |
| + | + ENABLE_FEATURE_TELNET_WIDTH |
| + | IF_FEATURE_TELNET_TTYPE(+ !!G.ttype) |
| + | IF_FEATURE_TELNET_AUTOLOGIN(+ !!G.autologin) |
| + | ); |
| + | #if DEBUG |
| + | /* Terminal can change to raw mode, fix line printing */ |
| + | msg_eol = "\r\n"; |
| + | #endif |
| + | /* EINTR flag and looping is only needed to handle ^C |
| + | * in line mode, otherwise just a call to ioloop_run() would do. |
| + | * TODO: replace primitive line mode with read_line_input()!!! |
| + | */ |
| + | G.io.flags |= IOLOOP_FLAG_EXIT_IF_EINTR; |
| + | for (;;) { |
| + | int rc = ioloop_run(&G.io); |
| + | if (rc == IOLOOP_NO_CONNS) { |
| + | dbg("connection is closed"); |
| + | break; |
| } |
| - | |
| - | // FIXME: reads can block. Need full bidirectional buffering. |
| - | |
| - | if (ufds[0].revents) { |
| - | len = safe_read(STDIN_FILENO, G.buf, DATABUFSIZE); |
| - | if (len <= 0) |
| - | doexit(EXIT_SUCCESS); |
| - | handle_net_output(len); |
| + | if (bb_got_signal /*&& rc == IOLOOP_EINTR*/) { |
| + | bb_got_signal = 0; |
| + | show_menu(); |
| } |
| + | } |
| |
| - | if (ufds[1].revents) { |
| - | len = safe_read(netfd, G.buf, DATABUFSIZE); |
| - | if (len <= 0) { |
| - | full_write1_str("Connection closed by foreign host\r\n"); |
| - | doexit(EXIT_FAILURE); |
| - | } |
| - | handle_net_input(len); |
| - | } |
| - | } /* while (1) */ |
| + | doexit(EXIT_SUCCESS); |
| } |