#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 */
}