/* * NAME: connection.c * DESCRIPTION: manage a generic network connection */ # define DEBUG 0 inherit "/std/core"; inherit "/std/string"; inherit "/std/data"; # if DEBUG inherit "/std/vartext"; # else # define var2str(x) "" # endif # include <objects.h> # include <moo/verb.h> # include <moo/config.h> # include <moo/command.h> # include <moo/perms.h> # define F_DESTRUCTING 0x01 /* to preserve this object in close() */ # define F_PROGRAMMING 0x02 /* set if we are in the middle of .program */ # define F_BACKDOOR 0x04 /* enabled backdoor eval? */ # define F_PREFIXED 0x08 /* seen PREFIX label? */ # define F_PROMPTED 0x10 /* seen prompt yet? */ # define F_BOOTSTRAP 0x20 /* currently bootstrapping? */ # define F_LPCONLY 0x40 /* not associated with a MOO object? */ # define F_TIMELIMIT 0x80 /* connection will timeout? */ static string ip_number; /* TCP/IP address of this connection */ static string ip_name; /* TCP/IP hostname of this connection */ private int port_number; /* TCP/IP remote port number */ static int pfd; /* pseudo file-descriptor */ private int time_connect; /* time at connect */ private int time_input; /* time at last input */ private int max_object; /* for remembering the number of objects */ private object listener; /* associated port object (usually #0) */ private object player; /* associated player object */ static int player_id; /* player object number (or negative num) */ private int timeout_handle; /* for removing call_out(connect_timeout) */ private int flags; /* status flags */ static mixed *susp_data; /* passing read information to a task */ private string oob_prefix; /* out-of-band command prefix */ private object input_to_obj; /* passing read information to LPC */ private string input_to_func; /* LPC function */ private string prefix, suffix; /* PREFIX, SUFFIX strings */ private string prompt; /* prompt string */ /* * NAME: create() * DESCRIPTION: called when object is created */ static void create(void) { ::create(); oob_prefix = CONFIG->query(CF_OOB_PREFIX); } void notify(string msg); /* * NAME: set_listener() * DESCRIPTION: change our port listener object (usually #0) */ void set_listener(object ob) { listener = ob; } /* * NAME: get_listener() * DESCRIPTION: return our port listener object (usually #0) */ object get_listener(void) { return listener; } /* * NAME: set_prefix() * DESCRIPTION: modify or delete this connection's output prefix */ void set_prefix(string str) { prefix = strlen(str) ? str : 0; } /* * NAME: set_suffix() * DESCRIPTION: modify or delete this connection's output suffix */ void set_suffix(string str) { suffix = strlen(str) ? str : 0; } /* * NAME: get_delimiters() * DESCRIPTION: return the current output prefix and suffix */ string *get_delimiters(void) { return ({ prefix, suffix }); } /* * NAME: set_prompt() * DESCRIPTION: change this connection's prompt */ void set_prompt(string str) { prompt = strlen(str) ? str : 0; } /* * NAME: get_prompt() * DESCRIPTION: return this connection's current prompt */ string get_prompt(void) { return prompt ? prompt : ""; } /* * NAME: set_noecho() * DESCRIPTION: to be overridden as necessary */ int set_noecho(void) { return E_INVARG; } /* * NAME: set_bootstrap() * DESCRIPTION: the server is currently bootstrapping */ void set_bootstrap(int bool) { if (bool) flags |= F_BOOTSTRAP; else flags &= ~F_BOOTSTRAP; } /* * NAME: set_backdoor() * DESCRIPTION: allow this connection to use the built-in eval */ void set_backdoor(int bool) { if (bool) flags |= F_BACKDOOR; else flags &= ~F_BACKDOOR; } /* * NAME: prefix() * DESCRIPTION: send this connection's current output prefix, if any */ void prefix(void) { if (prefix) notify(prefix); flags |= F_PREFIXED; } void send(string msg); /* * NAME: prompt() * DESCRIPTION: send this connection's current prompt, if any */ void prompt(void) { if (prompt && ! (flags & F_PROMPTED)) send(prompt); flags |= F_PROMPTED; } /* * NAME: suffix() * DESCRIPTION: send this connection's current ouput suffix, if any */ void suffix(void) { if (suffix && (flags & F_PREFIXED)) notify(suffix); flags &= ~F_PREFIXED; if (! (flags & F_DESTRUCTING)) prompt(); } /* * NAME: finish() * DESCRIPTION: called after command processing is finished (or an error) */ void finish(void) { suffix(); } /* * NAME: parse_line() * DESCRIPTION: parse a line of input using MOO command parsing semantics */ private string *parse_line(string line) { int i, sz, special; int in_space, in_quote, start_word; string *words, argstr; if (! (sz = strlen(line))) return ({ "" }); for (i = 0; i < sz && (line[i] == ' ' || line[i] == '\t'); ++i); if (i < sz) { switch (line[i]) { case '\"': special = 1, line = subst(line, i, 1, "say "); break; case ':': special = 1, line = subst(line, i, 1, "emote "); break; case ';': special = 1, line = subst(line, i, 1, "eval "); break; } } in_space = 1; in_quote = 0; start_word = -1; words = ({ }); for (i = 0, sz = strlen(line); i < sz; ++i) { if (in_space) { if (line[i] == ' ') continue; in_space = 0; start_word = i; /* pass through */ } if (line[i] == '\\') { line = subst(line, i, 1, ""); --sz; continue; } if (line[i] == '\"') { line = subst(line, i, 1, ""); --sz; --i; in_quote = (! in_quote); continue; } if (line[i] == ' ' && (! in_quote)) { words += ({ line[start_word .. i - 1] }); in_space = 1; if (! argstr) argstr = line[i + 1 ..]; continue; } } if (! in_space) words += ({ line[start_word ..] }); if (! argstr) argstr = ""; if (! special && (sz = strlen(argstr)) && argstr[0] == ' ') { /* not special -- strip leading blanks */ for (i = 0; i < sz && argstr[i] == ' '; ++i); argstr = argstr[i ..]; } return ({ argstr }) + words; } /* * NAME: filter_strings() * DESCRIPTION: pull out strings from an array and lowercase them */ private string *filter_strings(MOOVAL var) { int i, j, sz; MOOVAL *list; string *strs; if (! LSTP(var)) return ({ }); list = LSTVAL(var); strs = allocate(sz = sizeof(list)); for (i = 0, j = -1; i < sz; ++i) if (STRP(list[i])) strs[++j] = tolower(STRVAL(list[i])); return strs[.. j]; } /* * NAME: fetch_names() * DESCRIPTION: return an array of nearby objects and names */ private mixed *fetch_names(void) { int i; object ob, *list; string **names; list = player->get_contents(); if (ob = player->get_location()) list += ob->get_contents(); for (names = allocate(i = sizeof(list)); i--; ) { ob = list[i]; names[i] = filter_strings(ob->safe_get_property("aliases")) + ({ tolower(ob->get_name()) }); } return ({ list, names }); } /* * NAME: match_object() * DESCRIPTION: locate objects based on description */ private int match_object(string descrip, object *list, string **names) { int i, j, sz, count, match, len; if (! strlen(descrip)) return MATCH_NOTHING; if (sscanf(descrip, "#%d", i) && MOOOBJ(i)) return i; descrip = tolower(descrip); if (descrip == "me") return OBJNUM(player); if (descrip == "here") { object ob; ob = player->get_location(); return ob ? OBJNUM(ob) : -1; } sz = sizeof(list); for (i = 0; i < sz; ++i) for (j = sizeof(names[i]); j--; ) if (descrip == names[i][j]) { if (count++) return MATCH_AMBIGUOUS; match = OBJNUM(list[i]); break; } if (count) return match; for (len = strlen(descrip), i = 0; i < sz; ++i) for (j = sizeof(names[i]); j--; ) if (descrip == leftstr(names[i][j], len)) { if (count++) return MATCH_AMBIGUOUS; match = OBJNUM(list[i]); break; } if (count) return match; return MATCH_FAIL; } /* * NAME: parse_cmd() * DESCRIPTION: finish parsing a MOO command */ private mixed *parse_cmd(string *args) { mixed *ret; string *split, **names; object *list; split = global->match_preposition(args); ret = fetch_names(); list = ret[0]; names = ret[1]; return ({ match_object(split[0], list, names), split[1] == "" ? MATCH_NOTHING : global->prep_code(split[1]), match_object(split[2], list, names) }) + split; } /* * NAME: server_msg() * DESCRIPTION: display a server message to the connection */ static void server_msg(string message) { if (CONFIG->query(CF_SERVER_MSGS)) notify(message); } /* * NAME: destruct() * DESCRIPTION: destroy this object (perhaps from call_out) */ static void destruct(void) { destruct_object(this_object()); } /* * NAME: boot() * DESCRIPTION: terminate this user's connection */ varargs void boot(string message) { if (message) server_msg(message); flags |= F_DESTRUCTING; finish(); destruct(); } /* * NAME: get_id() * DESCRIPTION: return the pfd/object id of this connection */ int get_id(void) { return player_id; } /* * NAME: get_connection_name() * DESCRIPTION: return a descriptive string for this connection */ string get_connection_name(void) { return (string) pfd + " from " + ((ip_name && strlen(ip_name)) ? ip_name : ip_number); } /* * NAME: description() * DESCRIPTION: return a description string for this connection */ string description(void) { return global->user_description(player_id) + " on " + get_connection_name(); } /* * NAME: connect_timeout() * DESCRIPTION: user is taking too long */ static void connect_timeout(void) { global->log_msg("TIMEOUT: " + description()); boot("*** Timed-out waiting for login. ***"); } /* * NAME: remove_timelimit() * DESCRIPTION: stop limiting the user's idleness */ private void remove_timelimit(void) { if (flags & F_TIMELIMIT) { remove_call_out(timeout_handle); flags &= ~F_TIMELIMIT; } } /* * NAME: set_timelimit() * DESCRIPTION: limit the user's idleness */ private void set_timelimit(void) { int seconds; remove_timelimit(); if (seconds = CONFIG->query(CF_CONNECT_TIMEOUT)) { timeout_handle = call_out("connect_timeout", seconds); flags |= F_TIMELIMIT; } } /* * NAME: connect_player() * DESCRIPTION: (re)connect a player */ private int connect_player(object ob, int id, int created) { object other_conn; remove_timelimit(); time_connect = time(); player = ob; if (other_conn = global->get_connection_obj(player)) { global->redirect(this_object(), player_id, player); other_conn->boot("*** Redirecting connection to new port ***"); server_msg("*** Redirecting old connection to this port ***"); player_id = id; return 1; /* reconnected */ } else { global->connect(this_object(), player_id, player); server_msg(created ? "*** Created ***" : "*** Connected ***"); player_id = id; return 0; /* connected */ } } /* * NAME: connect_lpc() * DESCRIPTION: attach this connection to an LPC object */ static void connect_lpc(object obj) { remove_timelimit(); time_connect = time(); input_to_obj = obj; input_to_func = "receive_message"; flags |= F_LPCONLY; global->connect_lpc(this_object(), player_id, obj); server_msg("*** Connected to " + object_name(obj) + " ***"); obj->open(); } /* * NAME: do_login_command() * DESCRIPTION: process a line of input for an unlogged-in connection */ void do_login_command(JS_PROTO, string *args, string line) { object ob; string verb; int i; JS_BEGIN; max_object = global->get_max_object(); if (! listener) return; JS_PREP(1); RET = listener->call_verb(JS_DATA(1), "do_login_command", server_vars(player_id, listener, "do_login_command", line, LSTVAL(STRLIST2MOO(args))...)); JS_END; if (player_id >= 0 || (! STRP(RET) && (! OBJP(RET) || ! (ob = MOOOBJ(OBJVAL(RET))) || ! ob->is_player()))) return; if (STRP(RET)) /* LPC object */ { object obj; catch(obj = load_object(STRVAL(RET))); if (obj == 0) return; connect_lpc(obj); return; } /* logged in */ if (OBJVAL(RET) > max_object) /* user created */ { connect_player(ob, OBJVAL(RET), 1); verb = "user_created"; } else /* regular (re)connection */ { verb = connect_player(ob, OBJVAL(RET), 0) ? "user_reconnected" : "user_connected"; } if (! listener) return; JS_PREP(2); RET = listener->call_verb(JS_DATA(2), verb, server_vars(OBJVAL(RET), listener, verb, 0, RET)); JS_END; JS_END; } /* * NAME: input_to() * DESCRIPTION: prepare to return the next input line to LPC */ int input_to(object where, string what) { if (susp_data || input_to_obj) return 0; input_to_obj = where; input_to_func = what; return 1; } /* * NAME: seed_read() * DESCRIPTION: prepare to return the next input line for a read() */ int seed_read(JS_PROTO, int binary, mixed *jumpstack) { if (susp_data || input_to_obj) return E_INVARG; susp_data = ({ JS_DATA(bjump), binary, jumpstack }); return E_NONE; } /* * NAME: unseed() * DESCRIPTION: the suspended read() task was killed */ void unseed(void) { susp_data = 0; } /* * NAME: builtin_eval() * DESCRIPTION: evaluate MOO code unconditionally (backdoor) */ void builtin_eval(JS_PROTO, string code) { object vobj; mixed *ast; int i, sz, flags; string err; JS_BEGIN; ast = parser->main(code + ";"); if (! ast[0]) { ast = ast[1]; for (i = 0, sz = sizeof(ast); i < sz; ++i) notify(ast[i]); notify(sz + " error" + (sz == 1 ? "." : "s.")); return; } vobj = global->compile_verb(ast[1], 0); /* don't call vobj->ref(); let it self-destruct after executing */ flags = IF_DEBUG | IF_WIZARD; info = ({ flags, player_id, ({ OBJ(player_id), /* player */ OBJ(-1), /* this */ OBJ(player_id), /* caller */ LST( ({ }) ), /* args */ STR(""), /* argstr */ STR(""), /* verb */ OBJ(-1), /* dobj */ STR(""), /* dobjstr */ STR(""), /* prepstr */ OBJ(-1), /* iobj */ STR(""), /* iobjstr */ STD_VARS }), 0, "", -1, player_id, global->take_task(), 0, vobj, 0, global->get_max_depth(), 0 }); JS_PREP(1); RET = vobj->main(JS_DATA(1)); JS_END; debug("stack == " + var2str(stack)); notify("=> " + moo2str(RET, 1)); JS_END; } /* * NAME: do_oob_command() * DESCRIPTION: process an out-of-band command */ void do_oob_command(JS_PROTO, string *args, string line) { JS_BEGIN; if (listener) { JS_PREP(1); RET = listener->call_verb(JS_DATA(1), "do_out_of_band_command", server_vars(player_id, listener, "do_out_of_band_command", line, LSTVAL(STRLIST2MOO(args))...)); JS_END; } JS_END; } /* * NAME: program() * DESCRIPTION: initiate programming of a verb */ private void program(string obj, string verb) { mixed *ret; object ob, *list; int objnum; string **names; ret = fetch_names(); list = ret[0]; names = ret[1]; objnum = match_object(obj, list, names); if (! (ob = MOOOBJ(objnum))) { notify("I don't see \"" + obj + "\" here."); return; } notify("** Warning: `.program' is not supported."); notify("Now ignoring program for " + ob->get_name() + ":" + verb + "(x). Use \".\" to end."); flags |= F_PROGRAMMING; } /* * NAME: program_line() * DESCRIPTION: receive a line of MOO code */ private void program_line(string line) { if (line == ".") { flags &= ~F_PROGRAMMING; notify("Stopped programming."); prompt(); } } /* * NAME: do_read() * DESCRIPTION: pass a line of input to a suspended read() task */ static void do_read(string line) { mixed *stack, *info, *js; int binary; stack = susp_data[0]; info = susp_data[1]; binary = susp_data[4]; js = susp_data[5]; susp_data = 0; global->task_started(info[I_TASKID]); stack[SP + 1] = line ? (binary ? BUF(line) : STR(line)) : ERR(E_INVARG); debug("stack == " + var2str(stack)); call_other(js[1], js[2], js[3]...); } /* * NAME: do_input_to() * DESCRIPTION: pass input to an LPC object */ private mixed do_input_to(string line) { object where; string what; where = input_to_obj; what = input_to_func; if (! (flags & F_LPCONLY)) { input_to_obj = 0; input_to_func = 0; } return call_other(where, what, line); } /* * NAME: do_huh() * DESCRIPTION: a command was not recognized */ int do_huh(JS_PROTO, object room, string verb, mixed *parsed) { JS_BEGIN; JS_PREP(1); RET = room->call_verb(JS_INIT, "huh", ({ OBJ(player_id), /* player */ OBJ_OBJNUM(room), /* this */ OBJ(player_id), /* caller */ STRLIST2MOO(parsed[C_ARGS]), /* args */ STR(parsed[C_ARGSTR]), /* argstr */ STR(verb), /* verb */ OBJ(parsed[C_DOBJ]), /* dobj */ STR(parsed[C_DOBJSTR]), /* dobjstr */ STR(parsed[C_PREPSTR]), /* prepstr */ OBJ(parsed[C_IOBJ]), /* iobj */ STR(parsed[C_IOBJSTR]), /* iobjstr */ STD_VARS }) ); JS_END; return ! STWP(RET); JS_END; } /* * NAME: do_command() * DESCRIPTION: give the db a chance to process this command */ int do_command(JS_PROTO, string *args, string line) { JS_BEGIN; if (! listener) return 0; JS_PREP(1); if (catch(RET = listener->call_verb(JS_DATA(1), "do_command", server_vars(player_id, listener, "do_command", line, LSTVAL(STRLIST2MOO(args))...)))) return 0; JS_END; return TRUTHOF(RET); JS_END; } /* * NAME: want_binary() * DESCRIPTION: return true iff we want to process input as binary data */ static int want_binary(void) { return susp_data && susp_data[4]; } /* * NAME: process_data() * DESCRIPTION: handle raw binary input */ static void process_data(string data) { time_input = time(); flags &= ~F_PROMPTED; if (flags & F_TIMELIMIT) set_timelimit(); /* susp_data != 0 because want_binary() was called before we got here */ do_read(data); } /* * NAME: sanitize() * DESCRIPTION: make a string contain only valid MOO string characters */ private string sanitize(string str) { int i, c; for (i = strlen(str); i--; ) if (((c = str[i]) < ' ' && c != '\t') || c > '~') str = str[.. i - 1] + str[i + 1 ..]; return str; } /* * NAME: process_line() * DESCRIPTION: handle a single line of input */ static void process_line(string line) { object room, ob; string *args, verb, lverb, argstr; mixed *parsed; int i; time_input = time(); flags &= ~F_PROMPTED; if (flags & F_TIMELIMIT) set_timelimit(); /* check for LPC input_to() */ if (input_to_obj || (flags & F_LPCONLY)) { if (input_to_obj && (do_input_to(line) || (flags & F_LPCONLY))) return; if (flags & F_LPCONLY) { boot("*** LPC connection broken ***"); return; } } line = sanitize(line); /* check for out-of-band command */ if (oob_prefix && strlen(line) >= strlen(oob_prefix) && line[.. strlen(oob_prefix) - 1] == oob_prefix) { args = parse_line(line); do_oob_command(JS_INIT, args[1 ..], line); return; } /* check for suspended read() */ if (susp_data) { do_read(line); return; } /* check for .program text */ if (flags & F_PROGRAMMING) { program_line(line); return; } /* check for ;; backdoor */ if ((flags & F_BACKDOOR) && strlen(line) >= 2 && line[0 .. 1] == ";;") { builtin_eval(JS_INIT, line[2 ..]); return; } args = parse_line(line); argstr = args[0]; args = args[1 ..]; /* check for un-logged-in connection */ if (! player) { do_login_command(JS_INIT, args, line); return; } /* try in-db command parsing */ if (do_command(JS_INIT, args, line)) return; /* check for empty line */ if (! sizeof(args)) return; verb = args[0]; /* check for built-in verbs */ if (verb == "PREFIX") { set_prefix(argstr); return; } if (verb == "SUFFIX") { set_suffix(argstr); return; } if (verbname_match(".pr*ogram", verb) && programmerp(player_id)) { string obj, vname; if (sizeof(args) != 2) { notify("Usage: .program object:verb"); return; } if (sscanf(args[1], "%s:%s", obj, vname) != 2) { notify("You must specify a verb; " + "use the format object:verb."); return; } program(obj, vname); return; } /* regular command; do parsing */ parsed = ({ args[1 ..], argstr }) + parse_cmd(args); prefix(); lverb = tolower(verb); if (player->command(lverb, verb, parsed, player, 0)) return; room = player->get_location(); if (room && room->command(lverb, verb, parsed, player, 0)) return; ob = MOOOBJ(parsed[C_DOBJ]); if (ob && ob->command(lverb, verb, parsed, player, 0)) return; ob = MOOOBJ(parsed[C_IOBJ]); if (ob && ob->command(lverb, verb, parsed, player, 0)) return; if (! room || ! do_huh(JS_INIT, room, verb, parsed)) notify(CONFIG->query(CF_HUH_FAILED_MSG)); } /* * NAME: resolved_name() * DESCRIPTION: called to receive this connection's host name when it is known */ void resolved_name(string name) { ip_name = name; } /* * NAME: init() * DESCRIPTION: setup initial connection data */ static void init(string msg) { time_connect = 0; time_input = time(); ip_number = query_ip_number(this_object()); ip_name = DNS->get_name(ip_number, "resolved_name"); pfd = global->assign_pfd(this_object()); player_id = global->accept(this_object(), msg); } /* * NAME: open() * DESCRIPTION: called by DGD when connection is opened */ static void open(void) { init("ACCEPT"); if (flags & F_BOOTSTRAP) boot("*** The server is currently bootstrapping (try again later) ***"); else if (flags & F_BACKDOOR) notify("*** Backdoor access for this connection enabled ***"); else { set_timelimit(); process_line(""); } } /* * NAME: client_booted() * DESCRIPTION: called by close() when the client has closed the connection */ void client_booted(JS_PROTO) { JS_BEGIN; global->log_msg("CLIENT DISCONNECTED: " + description()); if (player && listener) { JS_PREP(1); RET = listener->call_verb(JS_DATA(1), "user_client_disconnected", server_vars(player_id, listener, "user_client_disconnected", 0, OBJ(player_id))); JS_END; } JS_END; } /* * NAME: cancel_susp() * DESCRIPTION: rid susp_data */ static void cancel_susp(void) { if (susp_data) call_out("do_read", 0, 0); } /* * NAME: close() * DESCRIPTION: called by DGD when connection is closed / object destructed */ static void close(void) { cancel_susp(); call_out("destruct", 1); if ((flags & F_LPCONLY) && input_to_obj) input_to_obj->close(); if (! (flags & F_DESTRUCTING)) client_booted(JS_INIT); } /* * NAME: get_connected_seconds() * DESCRIPTION: return number of seconds this user has been connected */ int get_connected_seconds(void) { return time_connect ? time() - time_connect : -1; } /* * NAME: get_idle_seconds() * DESCRIPTION: return number of seconds this user has been idle */ int get_idle_seconds(void) { return time() - time_input; } /* * NAME: set_port_number() * DESCRIPTION: called to configure the remote TCP/IP port for this connection */ void set_port_number(int num) { port_number = num; } /* * NAME: get_port_number() * DESCRIPTION: return this connection's remote port number */ int get_port_number(void) { return port_number; }