AmendHub

Download:

jcs

/

subtext

/

amendments

/

19

telnet: Add IAC processing, redo how socket slots are handled

Only setup one listener at a time and as soon as it gets a connection,
start up another one (up to the limit of slots)

jcs made amendment 19 over 2 years ago
--- telnet.c Tue Dec 7 15:41:49 2021 +++ telnet.c Thu Dec 9 20:58:41 2021 @@ -23,32 +23,101 @@ #include "tcp.h" #include "util.h" +#define SE 240 /* end of sub-negotiation options */ +#define SB 250 /* start of sub-negotiation options */ +#define WILL 251 /* confirm willingness to negotiate */ +#define WONT 252 /* confirm unwillingness to negotiate */ +#define DO 253 /* indicate willingness to negotiate */ +#define DONT 254 /* indicate unwillingness to negotiate */ +#define IAC 255 /* start of a negotiation sequence */ + +#define IS 0 /* sub-negotiation */ +#define SEND 1 /* sub-negotiation */ + +#define IAC_BINARY 0 /* Transmit Binary */ +#define IAC_ECHO 1 /* Echo Option */ +#define IAC_SGA 3 /* Suppress Go Ahead Option */ +#define IAC_STATUS 5 /* Status Option */ +#define IAC_TM 6 /* Timing Mark Option */ +#define IAC_NAOCRD 10 /* Output Carriage-Return Disposition Option */ +#define IAC_NAOHTS 11 /* Output Horizontal Tabstops Option */ +#define IAC_NAOHTD 12 /* Output Horizontal Tab Disposition Option */ +#define IAC_NAOFFD 13 /* Output Formfeed Disposition Option */ +#define IAC_NAOVTS 14 /* Output Vertical Tabstops Option */ +#define IAC_NAOVTD 15 /* Output Vertical Tab Disposition Option */ +#define IAC_NAOLFD 16 /* Output Linefeed Disposition */ +#define IAC_XASCII 17 /* Extended Ascii Option */ +#define IAC_LOGOUT 18 /* Logout Option */ +#define IAC_BM 19 /* Byte Macro Option */ +#define IAC_SUPDUP 22 /* SUPDUP-OUTPUT Option */ +#define IAC_SENDLOC 23 /* SEND-LOCATION Option */ +#define IAC_TTYPE 24 /* Terminal Type Option */ +#define IAC_EOR 25 /* End of Record Option */ +#define IAC_OUTMRK 27 /* Marking Telnet Option */ +#define IAC_TTYLOC 28 /* Terminal Location Number Option */ +#define IAC_DET 20 /* Data Entry Terminal Option DODIIS */ +#define IAC_X3PAD 30 /* X.3 PAD Option */ +#define IAC_NAWS 31 /* Window Size Option */ +#define IAC_TSPEED 32 /* Terminal Speed Option */ +#define IAC_FLOWCTRL 33 /* Remote Flow Control Option */ +#define IAC_LINEMODE 34 /* Linemode Option */ +#define IAC_XDISPLOC 35 /* X Display Location Option */ +#define IAC_ENVIRON 36 /* Environment Option */ +#define IAC_AUTH 37 /* Authentication */ +#define IAC_ENCRYPT 38 /* Encryption Option */ +#define IAC_NEWENV 39 /* Environment Option */ +#define IAC_CHARSET 42 /* Charset Option */ +#define IAC_COMPORT 44 /* Com Port Control Option */ + enum { TELNET_PB_STATE_UNUSED = 0, TELNET_PB_STATE_LISTENING, TELNET_PB_STATE_CONNECTED }; +enum { + TELNET_IAC_STATE_IDLE = 0, + TELNET_IAC_STATE_IAC, + TELNET_IAC_STATE_WILL, + TELNET_IAC_STATE_WONT, + TELNET_IAC_STATE_DO, + TELNET_IAC_STATE_DONT, + TELNET_IAC_STATE_SB +}; + struct telnet_node { short id; short state; TCPiopb pb, listen_pb; StreamPtr stream; - unsigned char *tcp_buf; - long tcp_buf_len; wdsEntry tcp_wds[2]; + unsigned short obuflen; + unsigned char obuf[512]; /* this must be session.obuf*2 */ + unsigned char ibuf[64]; + unsigned short ibuflen; + unsigned short sending_iac; + unsigned short escaped_obuflen; + short iac_state; + unsigned char iac_sb[64]; + short sb_len; struct session *session; + /* docs say 4*MTU+1024, but MTU will probably be <1500 */ + unsigned char tcp_buf[(4 * 1500) + 1024]; }; -#define TELNET_SLOTS 5 -struct telnet_node telnet_nodes[TELNET_SLOTS]; +#define MAX_TELNET_NODES 5 +struct telnet_node *telnet_nodes[MAX_TELNET_NODES] = { NULL }; +struct telnet_node *telnet_listener_node = NULL; short telnet_local_port; TCPiopb telnet_exit_pb; TCPStatusPB telnet_status_pb; void telnet_atexit(void); +void telnet_setup(struct session *session); +void telnet_output_iac(struct session *session, char *iacs, size_t len); struct node_funcs telnet_node_funcs = { + telnet_setup, telnet_input, telnet_output, telnet_close @@ -57,39 +126,28 @@ struct node_funcs telnet_node_funcs = { void telnet_init(void) { - struct telnet_node *node; + UDPiopb pb; short error, i; if (!db->config.telnet_port) return; - if (_TCPInit() != 0) - err(1, "Failed initializing MacTCP"); + if (_TCPInit() != noErr) + panic("Failed initializing MacTCP"); _atexit(telnet_atexit); - - for (i = 0; i < TELNET_SLOTS; i++) { - node = &telnet_nodes[i]; - memset(node, 0, sizeof(struct telnet_node)); - node->id = i; - - /* docs say buf should be 4*MTU+1024 */ - /* TODO: actually lookup MTU */ - node->tcp_buf_len = (4 * 1500) + 1024; - node->tcp_buf = xmalloc(node->tcp_buf_len); - - /* delay actual listening until telnet_idle */ - } } void telnet_atexit(void) { struct telnet_node *node; - short i, error; + short n, error; - for (i = 0; i < TELNET_SLOTS; i++) { - node = &telnet_nodes[i]; + for (n = 0; n < MAX_TELNET_NODES; n++) { + node = telnet_nodes[n]; + if (!node) + continue; if (node->state > TELNET_PB_STATE_UNUSED) { error = _TCPAbort(&telnet_exit_pb, node->stream, nil, nil, @@ -98,54 +156,68 @@ telnet_atexit(void) nil, false); } - if (node->tcp_buf) - free(node->tcp_buf); + free(node); + telnet_nodes[n] = NULL; } + + telnet_listener_node = NULL; } void telnet_idle(void) { struct telnet_node *node; - short i, error; - ip_port port = db->config.telnet_port; - - if (!port) + short n, error; + tcp_port port; + + if (!db->config.telnet_port) return; + + for (n = 0; n < MAX_TELNET_NODES; n++) { + node = telnet_nodes[n]; + if (!node) { + if (telnet_listener_node != NULL) + continue; + + node = xmalloczero(sizeof(struct telnet_node)); + node->id = n; - for (i = 0; i < TELNET_SLOTS; i++) { - node = &telnet_nodes[i]; - switch (node->state) { - case TELNET_PB_STATE_UNUSED: error = _TCPCreate(&node->pb, &node->stream, - (Ptr)node->tcp_buf, node->tcp_buf_len, nil, nil, nil, false); + (Ptr)&node->tcp_buf, sizeof(node->tcp_buf), nil, nil, nil, + false); if (error) - err(1, "TCPCreate[%d] failed: %d", i, error); + panic("TCPCreate[%d] failed: %d", node->id, error); + port = db->config.telnet_port; error = _TCPPassiveOpen(&node->listen_pb, node->stream, nil, nil, nil, &port, nil, nil, true); if (error) - err(1, "TCPPassiveOpen[%d] on port %d failed: %d", i, port, - error); + panic("TCPPassiveOpen[%d] on port %d failed: %d", + node->id, db->config.telnet_port, error); + node->state = TELNET_PB_STATE_LISTENING; - break; + telnet_nodes[node->id] = node; + telnet_listener_node = node; + } + + switch (node->state) { case TELNET_PB_STATE_LISTENING: - if (node->listen_pb.ioResult > 0) - continue; - error = _TCPStatus(&node->pb, node->stream, &telnet_status_pb, nil, nil, false); if (error == connectionDoesntExist) continue; if (error != noErr) - err(1, "TCPStatus[%d] %d", i, error); + panic("TCPStatus[%d] %d", n, error); if (telnet_status_pb.connectionState == ConnectionStateEstablished) { char tty[7]; - sprintf(tty, "ttyt%d", i); + sprintf(tty, "ttyt%d", n); node->state = TELNET_PB_STATE_CONNECTED; node->session = session_create(tty, &telnet_node_funcs); node->session->cookie = (void *)node; + + /* start up a new socket for listening */ + telnet_listener_node = NULL; } break; case TELNET_PB_STATE_CONNECTED: @@ -156,33 +228,236 @@ telnet_idle(void) break; } } + } void -telnet_close(struct session *session) +telnet_setup(struct session *session) { + char iacs[] = { + IAC, DO, IAC_TTYPE, /* send your TTYPE */ + IAC, DO, IAC_TSPEED, /* send your TSPEED */ + IAC, DO, IAC_NAWS, /* send your NAWS */ + IAC, WILL, IAC_SGA, /* we will supress go-ahead */ + IAC, WILL, IAC_ECHO, /* and we will echo for you */ + + /* IAC, DO, IAC_LINEMODE, */ + }; + + telnet_output_iac(session, iacs, sizeof(iacs)); +} + +short +telnet_input(struct session *session) +{ struct telnet_node *node = (struct telnet_node *)session->cookie; - short error; + unsigned short rlen; + short error, n; + unsigned char c; + char iac_out[8] = { IAC, 0 }; - error = _TCPClose(&telnet_exit_pb, node->stream, nil, nil, false); - /* TODO: allow some time to fully close? */ - error = _TCPRelease(&telnet_exit_pb, node->stream, nil, nil, false); - - if (error) { - warn("error shutting down telnet session: %d", error); - return; + if (session->ibuflen >= sizeof(session->ibuf)) + return 0; + + if (node->pb.ioResult > 0) + /* previous _TCPSend/_TCPRecv has not completed yet */ + return 0; + + error = _TCPStatus(&node->pb, node->stream, &telnet_status_pb, nil, + nil, false); + if (error || + telnet_status_pb.connectionState != ConnectionStateEstablished) { + telnet_close(session); + return 0; } - - session->cookie = NULL; - node->session = NULL; - node->state = TELNET_PB_STATE_UNUSED; + + if (telnet_status_pb.amtUnreadData == 0) + return 0; + + rlen = telnet_status_pb.amtUnreadData; + if (session->ibuflen + rlen >= sizeof(session->ibuf)) + rlen = sizeof(session->ibuf) - session->ibuflen; + + error = _TCPRcv(&node->pb, node->stream, (Ptr)(node->ibuf), &rlen, + nil, nil, false); + if (error) + /* TODO: make this not fatal */ + panic("TCPRecv[%d] failed: %d", node->id, error); + + for (n = 0; n < rlen; n++) { + c = node->ibuf[n]; + + switch (node->iac_state) { + case TELNET_IAC_STATE_IDLE: + if (c == IAC) + node->iac_state = TELNET_IAC_STATE_IAC; + else + session->ibuf[session->ibuflen++] = c; + break; + case TELNET_IAC_STATE_IAC: + switch (c) { + case IAC: + /* escaped iac */ + session->ibuf[session->ibuflen++] = c; + node->iac_state = TELNET_IAC_STATE_IDLE; + break; + case WILL: + /* client will do something */ + node->iac_state = TELNET_IAC_STATE_WILL; + break; + case WONT: + /* client will not do something */ + node->iac_state = TELNET_IAC_STATE_WONT; + break; + case DO: + /* client wants us to do something */ + node->iac_state = TELNET_IAC_STATE_DO; + break; + case DONT: + /* client wants us not to do something */ + node->iac_state = TELNET_IAC_STATE_DONT; + break; + case SB: + /* sub-negotiate */ + node->iac_state = TELNET_IAC_STATE_SB; + node->sb_len = 0; + break; + default: + node->iac_state = TELNET_IAC_STATE_IDLE; + /* XXX: what are we supposed to do here? */ + session->ibuf[session->ibuflen++] = IAC; + session->ibuf[session->ibuflen++] = c; + break; + } + break; + case TELNET_IAC_STATE_SB: { + unsigned short sboff; + + /* keep reading until we see [^IAC] IAC SE */ + if (c == SE && node->sb_len > 0 && + node->iac_sb[node->sb_len - 1] == IAC) { + switch (node->iac_sb[0]) { + case IAC_NAWS: + /* IAC SB NAWS 0 80 0 24 (80x24) */ + /* IAC SB NAWS 1 255 255 1 123 (256x378) */ + /* if either value is 255, it will be 255,255 */ + sboff = 1; + + if (node->iac_sb[sboff] == 255) + sboff++; + session->terminal_cols = (node->iac_sb[sboff++] << 8); + if (node->iac_sb[sboff] == 255) + sboff++; + session->terminal_cols |= node->iac_sb[sboff++]; + + if (node->iac_sb[sboff] == 255) + sboff++; + session->terminal_rows = (node->iac_sb[sboff++] << 8); + if (node->iac_sb[sboff] == 255) + sboff++; + session->terminal_rows |= node->iac_sb[sboff++]; + break; + case IAC_TTYPE: + /* IAC SB TTYPE IS XTERM IAC SE */ + node->iac_sb[node->sb_len - 1] = '\0'; + strlcpy(session->terminal_type, + (char *)&node->iac_sb + 2, + sizeof(session->terminal_type)); + break; + } + node->iac_state = TELNET_IAC_STATE_IDLE; + } else { + /* accumulate bytes into iac_sb buffer */ + if (node->sb_len < sizeof(node->iac_sb)) + node->iac_sb[node->sb_len++] = c; + else { + /* IAC overflow */ + node->iac_state = TELNET_IAC_STATE_IDLE; + node->sb_len = 0; + } + } + break; + } + case TELNET_IAC_STATE_WILL: + switch (c) { + case IAC_ECHO: + case IAC_SGA: + iac_out[1] = DO; + iac_out[2] = IAC_ECHO; + telnet_output_iac(session, iac_out, 3); + break; + case IAC_TTYPE: + iac_out[1] = SB; + iac_out[2] = IAC_TTYPE; + iac_out[3] = SEND; + iac_out[4] = IAC; + iac_out[5] = SE; + telnet_output_iac(session, iac_out, 6); + break; + case IAC_LINEMODE: +#if 0 + iac_out[1] = SB; + iac_out[2] = IAC_LINEMODE; + iac_out[3] = 1; + iac_out[4] = 0; + iac_out[5] = IAC; + iac_out[6] = SE; + telnet_output_iac(session, iac_out, 7); +#endif + break; + case IAC_ENCRYPT: + iac_out[1] = DONT; + iac_out[2] = IAC_ENCRYPT; + telnet_output_iac(session, iac_out, 3); + break; + } + node->iac_state = TELNET_IAC_STATE_IDLE; + break; + case TELNET_IAC_STATE_WONT: + /* we don't care about any of these yet */ + node->iac_state = TELNET_IAC_STATE_IDLE; + break; + case TELNET_IAC_STATE_DO: + switch (c) { + case IAC_BINARY: + iac_out[1] = WILL; + iac_out[2] = IAC_BINARY; + telnet_output_iac(session, iac_out, 3); + break; + case IAC_ECHO: + case IAC_NAWS: + case IAC_TSPEED: + case IAC_FLOWCTRL: + case IAC_SGA: + case IAC_LINEMODE: + /* we already sent WILLs for these at the beginning */ + break; + default: + iac_out[1] = WONT; + iac_out[2] = c; + telnet_output_iac(session, iac_out, 3); + break; + } + node->iac_state = TELNET_IAC_STATE_IDLE; + break; + case TELNET_IAC_STATE_DONT: + node->iac_state = TELNET_IAC_STATE_IDLE; + break; + default: + warn("telnet_input in bogus IAC state %d", node->iac_state); + break; + } + } + + return rlen; } short telnet_output(struct session *session) { struct telnet_node *node = (struct telnet_node *)session->cookie; - short error; + short n, error; + unsigned char c; if (session->obuflen == 0) return 0; @@ -193,18 +468,31 @@ telnet_output(struct session *session) if (node->tcp_wds[0].length) { /* previous _TCPSend completed, shift out those bytes */ - session->obuflen -= node->tcp_wds[0].length; + session->obuflen -= node->obuflen; if (session->obuflen < 0) warn("bogus obuflen %d", session->obuflen); if (session->obuflen > 0) - BlockMove(session->obuf + node->tcp_wds[0].length, - session->obuf, session->obuflen); + BlockMove(session->obuf + node->obuflen, session->obuf, + session->obuflen); node->tcp_wds[0].length = 0; if (session->obuflen == 0) - return node->tcp_wds[0].length; + return node->obuflen; } + + /* copy obuf to node buffer, escaping IACs */ + for (node->escaped_obuflen = 0, node->obuflen = 0; + node->obuflen < session->obuflen; + node->obuflen++) { + c = session->obuf[node->obuflen]; + + if (!node->sending_iac && c == IAC) { + node->obuf[node->escaped_obuflen++] = IAC; + node->obuf[node->escaped_obuflen++] = IAC; + } else + node->obuf[node->escaped_obuflen++] = c; + } /* * _TCPSend only knows how many wds pointers were passed in when it @@ -213,54 +501,57 @@ telnet_output(struct session *session) * has to be zeroed out. */ memset(&node->tcp_wds, 0, sizeof(node->tcp_wds)); - node->tcp_wds[0].ptr = session->obuf; - node->tcp_wds[0].length = session->obuflen; + node->tcp_wds[0].ptr = (Ptr)&node->obuf; + node->tcp_wds[0].length = node->escaped_obuflen; error = _TCPSend(&node->pb, node->stream, node->tcp_wds, nil, nil, true); if (error) /* TODO: make this not fatal */ - err(1, "TCPSend[%d] failed: %d", node->id, error); + panic("TCPSend[%d] failed: %d", node->id, error); return 0; } -short -telnet_input(struct session *session) +void +telnet_output_iac(struct session *session, char *iacs, size_t len) { struct telnet_node *node = (struct telnet_node *)session->cookie; - unsigned short rlen; - short error; - - if (session->ibuflen >= sizeof(session->ibuf)) - return 0; - - if (node->pb.ioResult > 0) - /* previous _TCPSend/_TCPRecv has not completed yet */ - return 0; - - error = _TCPStatus(&node->pb, node->stream, &telnet_status_pb, nil, - nil, false); - if (error || - telnet_status_pb.connectionState != ConnectionStateEstablished) { - telnet_close(session); - return 0; - } - if (telnet_status_pb.amtUnreadData == 0) - return 0; - - rlen = telnet_status_pb.amtUnreadData; - if (session->ibuflen + rlen >= sizeof(session->ibuf)) - rlen = sizeof(session->ibuf) - session->ibuflen; + /* flush output buffer */ + while (session->obuflen) + telnet_output(session); - error = _TCPRcv(&node->pb, node->stream, - (Ptr)(session->ibuf + session->ibuflen), &rlen, nil, nil, false); - if (error) - /* TODO: make this not fatal */ - err(1, "TCPRecv[%d] failed: %d", node->id, error); + node->sending_iac = 1; + + session_output_formatted(session, iacs, len); + + while (session->obuflen) + telnet_output(session); - session->ibuflen += rlen; + node->sending_iac = 0; +} - return rlen; +void +telnet_close(struct session *session) +{ + struct telnet_node *node = (struct telnet_node *)session->cookie; + short error; + + error = _TCPClose(&telnet_exit_pb, node->stream, nil, nil, false); + /* TODO: allow some time to fully close? */ + error = _TCPRelease(&telnet_exit_pb, node->stream, nil, nil, false); + + if (error) { + warn("error shutting down telnet session: %d", error); + return; + } + + session->cookie = NULL; + telnet_nodes[node->id] = NULL; + + if (node == telnet_listener_node) + telnet_listener_node = NULL; + + free(node); }