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);
}