#include <kernel/kernel.h> #include <kernel/user.h> #include <phantasmal/lpc_names.h> #include <phantasmal/telnet.h> #include <phantasmal/mudclient.h> #include <phantasmal/log.h> inherit COMMON_AUTO; inherit conn LIB_CONN; inherit user LIB_USER; /* This object is both a user object and a connection object. When the MudClientD returns it as a user, a fairly complicated structure springs up for handling this connection. Remember that the Kernel Library separates the connection and user object from each other anyway. [bin conn] <-> [LIB_USER] [LIB_CONN] <-> [Phant User] A A \ / --- Inherits -- | | mudclient_connection The Mudclient Connection object gets its input from a standard Kernel binary connection in raw mode. It processes that input and returns appropriate lines to its underlying (inherited) connection, which believes itself to be a Kernel telnet connection. The first line of input on the connection causes it to query UserD (and thus, indirectly, the telnet handler) with a username to get a new user object. The Mudclient connection thus acts as a filter, and its inherited LIB_CONN structure gets only the filtered input. Because this hat-trick isn't perfect, it'll always return a user object as though the Mudclient connection were on port offset 0, the first telnet port. Thanks to Felix's SSH code for showing me how this is done, and for a lot of the code in this file. Have I mentioned that his warped brilliance continues to intimidate me? */ private string buffer, outbuf; private mixed* input_lines; private int active_protocols; /* telnet options */ private int suppress_ga; private int suppress_echo; private string tel_goahead; private int new_telnet_input(string str); private string debug_escape_str(string line); private int process_input(string str); static void create(int clone) { if (clone) { conn::create("telnet"); /* Treat it like a telnet object */ buffer = ""; outbuf = nil; input_lines = ({ }); active_protocols = 0; tel_goahead = " "; tel_goahead[0] = TP_GA; suppress_ga = suppress_echo = 0; } } /* * NAME: datagram_challenge() * DESCRIPTION: there is no datagram channel to be opened */ void datagram_challenge(string str) { } /* * NAME: datagram() * DESCRIPTION: don't send a datagram to the client */ int datagram(string str) { return 0; } /* * NAME: login() * DESCRIPTION: accept connection */ int login(string str) { if (previous_program() == LIB_CONN) { user::connection(previous_object()); process_input(str); /* Don't call ::login() or we'll see a separate connection for the MUDClient connection, not just the 'real' user conn. */ } return MODE_RAW; } /* * NAME: logout() * DESCRIPTION: disconnect */ void logout(int quit) { if (previous_program() == LIB_CONN) { conn::close(nil, quit); if (quit) { destruct_object(this_object()); } } } /* * NAME: set_mode() * DESCRIPTION: pass mode changes to the binary connection object */ void set_mode(int mode) { if (KERNEL() || SYSTEM()) { if(mode != MODE_ECHO && mode != MODE_NOECHO) { query_conn()->set_mode(mode); /* Don't destruct the object on disconnect, because the connection library will have already done that. */ return; } if(suppress_echo && mode == MODE_NOECHO) { LOGD->write_syslog("Already suppressing echo!", LOG_VERBOSE); return; } if(!suppress_echo && mode == MODE_ECHO) { LOGD->write_syslog("Already allowing echo!", LOG_VERBOSE); return; } if(suppress_echo && mode == MODE_ECHO) { suppress_echo = 0; this_object()->send_telnet_option(TP_WONT, TELOPT_ECHO); LOGD->write_syslog("Doing echo in set_mode!", LOG_VERBOSE); } else if(!suppress_echo && mode == MODE_NOECHO) { suppress_echo = 1; this_object()->send_telnet_option(TP_WILL, TELOPT_ECHO); LOGD->write_syslog("Suppressing echo in set_mode!", LOG_VERBOSE); } } else error("Illegal caller '" + previous_program() + "' of MCC:set_mode!"); } /* * NAME: user_input() * DESCRIPTION: send filtered input to inherited telnet connection */ static int user_input(string str) { LOGD->write_syslog("MCC user_input: " + str, LOG_VERBOSE); return conn::receive_message(nil, str); } /* * NAME: disconnect() * DESCRIPTION: forward a disconnect to the binary connection */ void disconnect() { if (previous_program() == LIB_USER) { user::disconnect(); } } /* * NAME: message_done() * DESCRIPTION: forward message_done to user */ int message_done() { object user; int mode; if(previous_program() == LIB_CONN) { user = query_user(); if (user) { mode = user->message_done(); if (mode == MODE_DISCONNECT || mode >= MODE_UNBLOCK) { return mode; } } return MODE_NOCHANGE; } else error("Illegal call to message_done()!"); } /* * NAME: binary_message * DESCRIPTION: does a buffered send on the underlying binary connection */ int binary_message(string str) { if(user::query_conn()) { if(outbuf) { user::message(outbuf); outbuf = nil; } return user::message(str); } else { if(!outbuf) { outbuf = str; return strlen(str); } outbuf += str; return strlen(str); } } /* * NAME: message() * DESCRIPTION: send a message to the other side */ int message(string str) { if(previous_program() == LIB_USER || previous_program() == PHANTASMAL_USER) { /* Do appropriate send-filtering first */ LOGD->write_syslog("MCC message: " + str, LOG_VERBOSE); /* Do newline expansion */ str = implode(explode(str, "\r"), ""); str = implode(explode("\n" + str + "\n", "\n"), "\r\n"); return binary_message(str); } else { error("Unprivileged code calling MCC::message()!"); } } private string get_input_line(void) { string tmp; if(sizeof(input_lines)) { tmp = input_lines[0]; input_lines = input_lines[1..]; return tmp; } return nil; } private int process_input(string str) { string line; int mode; new_telnet_input(str); line = get_input_line(); while(line) { mode = user_input(line); if(mode == MODE_DISCONNECT || mode >= MODE_UNBLOCK) return mode; line = get_input_line(); } if(!suppress_ga && !strlen(buffer)) { /* Have read all pending lines, nothing uncompleted in buffer */ return binary_message(tel_goahead); } return MODE_NOCHANGE; } /* * NAME: receive_message() * DESCRIPTION: receive a message */ int receive_message(string str) { if (previous_program() == LIB_CONN || previous_program() == MUDCLIENTD) { return process_input(str); } error("Illegal call!"); } nomask int send_telnet_option(int command, int option) { string opts; if(!SYSTEM() && previous_object() != MUDCLIENTD->get_telopt_handler(option)) error("Only SYSTEM code and telopt handlers can send telnet options!"); if(command != TP_DO && command != TP_DONT && command != TP_WILL && command != TP_WONT) error("Invalid command in send_telnet_option!"); LOGD->write_syslog("Sending telnet option IAC " + command + " " + option, LOG_VERBOSE); opts = " "; opts[0] = TP_IAC; opts[1] = command; opts[2] = option; return binary_message(opts); } nomask int send_telnet_subnegotiation(int option, string arguments) { string tmp, options; if(!SYSTEM() && previous_object() != MUDCLIENTD->get_telopt_handler(option)) error("Only SYSTEM code and telopt handlers can send telnet options!"); tmp = " "; tmp[0] = TP_IAC; tmp[1] = TP_SB; options = tmp; tmp[1] = option; options = options + tmp[1..1] + arguments; tmp[1] = TP_SE; options = options + tmp; return binary_message(options); } void should_suppress_ga(int do_suppress) { if(SYSTEM() || previous_object() == MUDCLIENTD->get_telopt_handler(TELOPT_SGA)) { suppress_ga = do_suppress; } else error("Only privileged code may call should_suppress_ga!"); } /************************************************************************/ /************************************************************************/ /************************************************************************/ /* Specifics of telnet protocol */ /************************************************************************/ /************************************************************************/ /************************************************************************/ private string debug_escape_str(string line) { string ret; int ctr; ret = ""; for(ctr = 0; ctr < strlen(line); ctr++) { /*if(line[ctr] >= 32 && line[ctr] <= 127) ret += line[ctr..ctr]; else */ ret += "\\" + line[ctr]; } return ret; } private string double_iac_filter(string input_line) { string pre, post, outstr, iac_str; outstr = ""; post = input_line; iac_str = " "; iac_str[0] = TP_IAC; while(sscanf(post, "%s" + iac_str + iac_str + "%s", pre, post) == 2) { outstr += pre + iac_str; } outstr += post; return outstr; } private void negotiate_option(int command, int option) { object handler; handler = MUDCLIENTD->get_telopt_handler(option); if(handler) { switch(command) { case TP_WILL: handler->telnet_will(option); break; case TP_WONT: handler->telnet_wont(option); break; case TP_DO: handler->telnet_do(option); break; case TP_DONT: handler->telnet_dont(option); break; } } else { /* If no handler, ignore */ LOGD->write_syslog("Ignoring telnet option " + option, LOG_WARN); } } /* This function is called on any subnegotiation string sent. The string passed in was originally between an IAC SB and an IAC SE. */ private void subnegotiation_string(string str) { object handler; /* First, remove doubling of TP_IAC characters in string */ str = double_iac_filter(str); handler = MUDCLIENTD->get_telopt_handler(str[0]); if(handler) { handler->telnet_sb(str[0], str[1..]); } else { /* Now, ignore it. We don't yet accept subnegotiation on that option. */ LOGD->write_syslog("Ignoring subnegotiation, option " + str[0] + ": '" + debug_escape_str(str) + "'", LOG_WARN); } } /* Scan off a series starting with TP_IAC and return the remainder. The function will return nil if the series isn't a complete IAC sequence. The caller is assumed to have stripped off the leading TP_IAC character of 'series'. The caller has also already filtered for double-TP_IAC series, so we don't have to worry about those. */ static string scan_iac_series(string series) { string pre, post, iac_str, se_str; /* If there's nothing, we're not done yet */ if(!series || !strlen(series)) return nil; switch(series[0]) { case TP_WILL: case TP_WONT: case TP_DO: case TP_DONT: if(strlen(series) < 2) return nil; LOGD->write_syslog("Processing option: " + series[0] + " / " + series[1], LOG_VERBOSE); negotiate_option(series[0], series[1]); return series[2..]; case TP_SB: /* Scan for IAC SB ... IAC SE sequence */ iac_str = " "; iac_str[0] = TP_IAC; se_str = " "; se_str[0] = TP_SE; if(sscanf(series, "%s" + iac_str + se_str + "%s", pre, post) == 2) { LOGD->write_syslog("Processing sub-neg: IAC SB '" + pre[1] + " " + debug_escape_str(pre[2..]) + "' IAC SE", LOG_VERBOSE); subnegotiation_string(pre[1..]); return post; } else { /* Don't have whole series yet */ return nil; } break; default: /* Unrecognized, ignore */ return series[1..]; } } /* * This function filters for newlines (CR,LF, or CRLF) and backspaces. * It then chops up input into the line array. Much code taken from * the Kernel Library binary connection object. */ static void crlfbs_filter(void) { int mode, len; string str, head, pre; while (this_object() && (mode=query_mode()) != MODE_BLOCK && mode != MODE_DISCONNECT) { /* We only bother to process the buffer for this stuff if there's a line waiting. */ if (sscanf(buffer, "%s\r\n%s", str, buffer) != 0 || sscanf(buffer, "%s\r\0%s", str, buffer) != 0 || sscanf(buffer, "%s\r%s", str, buffer) != 0 || sscanf(buffer, "%s\n%s", str, buffer) != 0) { while (sscanf(str, "%s\b%s", head, str) != 0) { /* Process 'DEL' character in a string with backspaces */ while (sscanf(head, "%s\x7f%s", pre, head) != 0) { len = strlen(pre); if (len != 0) { head = pre[0 .. len - 2] + head; } } len = strlen(head); if (len != 0) { str = head[0 .. len - 2] + str; } } /* Process 'DEL' character after all backspaces are gone */ while (sscanf(str, "%s\x7f%s", head, str) != 0) { len = strlen(head); if (len != 0) { str = head[0 .. len - 2] + str; } } input_lines += ({ str }); LOGD->write_syslog("MCC input: '" + str + "'", LOG_VERBOSE); } else { break; /* No more newline-delimited input. Out of full lines. */ } } } /* * Add new characters to buffer. Filter newlines, backspaces and * telnet IAC codes appropriately. If a full line of input has been * read, set input_line appropriately. */ private int new_telnet_input(string str) { string iac_series, iac_str, chunk, tmpbuf, post, series; iac_str = " "; iac_str[0] = TP_IAC; buffer += str; iac_series = ""; tmpbuf = ""; /* Note: can't use double_iac_filter function here, because then we might collapse double-IAC sequences in the wrong place in the input and it'd corrupt other IAC sequences. */ /* Scan for TP_IAC. */ while(sscanf(buffer, "%s" + iac_str + "%s", chunk, series) == 2) { tmpbuf += chunk; if(strlen(series) && series[0] == TP_IAC) { tmpbuf += iac_str; buffer = series[1..]; continue; } post = scan_iac_series(series); if(!post) { /* Found an incomplete IAC series, wait for the rest */ iac_series = iac_str + series; break; } buffer = post; } buffer = tmpbuf + buffer; crlfbs_filter(); /* Handle newline stuff, backspace and DEL. Chunk out complete lines into input_lines. */ buffer += iac_series; /* Add back incomplete IAC series, if any */ }