X-Git-Url: https://mudpy.org/gitweb?a=blobdiff_plain;f=mudpy.py;h=af61429a6148383e86e13bc8240f645b34711d16;hb=ab7fb385d9b58cd6ca9b0156e8f1780f5ce3e750;hp=b25340cf3c617a7b40fec1a7b4bf502fc8841b71;hpb=5016c3731daf450cc0f3defce0c3e66854270356;p=mudpy.git diff --git a/mudpy.py b/mudpy.py index b25340c..af61429 100644 --- a/mudpy.py +++ b/mudpy.py @@ -11,6 +11,8 @@ from os.path import abspath, dirname, exists, isabs, join as path_join from random import choice, randrange from socket import AF_INET, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, socket from stat import S_IMODE, ST_MODE +from syslog import LOG_PID, LOG_INFO, LOG_DAEMON, closelog, openlog, syslog +from telnetlib import DO, DONT, ECHO, EOR, GA, IAC, LINEMODE, SB, SE, SGA, WILL, WONT from time import asctime, sleep class Element: @@ -198,13 +200,15 @@ class User: self.connection = None self.authenticated = False self.password_tries = 0 - self.state = "entering_account_name" + self.state = "initial" self.menu_seen = False self.error = "" self.input_queue = [] self.output_queue = [] self.partial_input = "" self.echoing = True + self.terminator = IAC+GA + self.negotiation_pause = 0 self.avatar = None self.account = None @@ -298,7 +302,7 @@ class User: """Send the user their current menu.""" if not self.menu_seen: self.menu_choices = get_menu_choices(self) - self.send(get_menu(self.state, self.error, self.echoing, self.menu_choices), "") + self.send(get_menu(self.state, self.error, self.echoing, self.terminator, self.menu_choices), "") self.menu_seen = True self.error = False self.adjust_echoing() @@ -312,37 +316,34 @@ class User: """Remove a user from the list of connected users.""" universe.userlist.remove(self) - def send(self, output, eol="$(eol)"): + def send(self, output, eol="$(eol)", raw=False): """Send arbitrary text to a connected user.""" - # only when there is actual output - #if output: + # unless raw mode is on, clean it up all nice and pretty + if not raw: - # start with a newline, append the message, then end - # with the optional eol string passed to this function - # and the ansi escape to return to normal text - output = "\r\n" + output + eol + chr(27) + "[0m" + # we'll take out GA or EOR and add them back on the end + if output.endswith(IAC+GA) or output.endswith(IAC+EOR): + terminate = True + output = output[:-2] + else: terminate = False - # find and replace macros in the output - output = replace_macros(self, output) + # start with a newline, append the message, then end + # with the optional eol string passed to this function + # and the ansi escape to return to normal text + output = "\r\n" + output + eol + chr(27) + "[0m" - # wrap the text at 80 characters - # TODO: prompt user for preferred wrap width - output = wrap_ansi_text(output, 80) + # find and replace macros in the output + output = replace_macros(self, output) - # drop the formatted output into the output queue - self.output_queue.append(output) + # wrap the text at 80 characters + output = wrap_ansi_text(output, 80) - # try to send the last item in the queue, remove it and - # flag that menu display is not needed - try: - self.connection.send(self.output_queue[0]) - self.output_queue.remove(self.output_queue[0]) - self.menu_seen = False + # tack the terminator back on + if terminate: output += self.terminator - # but if we can't, that's okay too - except: - pass + # drop the output into the user's output queue + self.output_queue.append(output) def pulse(self): """All the things to do to the user per increment.""" @@ -352,16 +353,30 @@ class User: self.state = "disconnecting" self.menu_seen = False + # if output is paused, decrement the counter + if self.state == "initial": + if self.negotiation_pause: self.negotiation_pause -= 1 + else: self.state = "entering_account_name" + # show the user a menu as needed - self.show_menu() + else: self.show_menu() # disconnect users with the appropriate state - if self.state == "disconnecting": - self.quit() + if self.state == "disconnecting": self.quit() # the user is unique and not flagged to disconnect else: + # try to send the last item in the queue and remove it + if self.output_queue: + try: + self.connection.send(self.output_queue[0]) + del self.output_queue[0] + + # but if we can't, that's okay too + except: + pass + # check for input and add it to the queue self.enqueue_input() @@ -383,6 +398,9 @@ class User: # tack this on to any previous partial self.partial_input += input_data + # reply to and remove any IAC negotiation codes + self.negotiate_telnet_options() + # separate multiple input lines new_input_lines = self.partial_input.split("\n") @@ -400,8 +418,20 @@ class User: # iterate over the remaining lines for line in new_input_lines: + # remove a trailing carriage return + if line.endswith("\r"): line = line.rstrip("\r") + + # log non-printable characters remaining + removed = filter(lambda x: (x < " " or x > "~"), line) + if removed: + logline = "Non-printable characters from " + if self.account and self.account.get("name"): logline += self.account.get("name") + ": " + else: logline += "unknown user: " + logline += repr(removed) + log(logline) + # filter out non-printables - line = filter(lambda x: x>=' ' and x<='~', line) + line = filter(lambda x: " " <= x <= "~", line) # strip off extra whitespace line = line.strip() @@ -409,6 +439,73 @@ class User: # put on the end of the queue self.input_queue.append(line) + def negotiate_telnet_options(self): + """Reply to/remove partial_input telnet negotiation options.""" + + # start at the begining of the input + position = 0 + + # make a local copy to play with + text = self.partial_input + + # as long as we haven't checked it all + while position < len(text): + + # jump to the first IAC you find + position = text.find(IAC, position) + + # if there wasn't an IAC in the input, skip to the end + if position < 0: position = len(text) + + # replace a double (literal) IAC if there's an LF later + elif len(text) > position+1 and text[position+1] == IAC: + if text.find("\n", position) > 0: text = text.replace(IAC+IAC, IAC) + else: position += 1 + position += 1 + + # this must be an option negotiation + elif len(text) > position+2 and text[position+1] in (DO, DONT, WILL, WONT): + + negotiation = text[position+1:position+3] + + # if we turned echo off, ignore the confirmation + if not self.echoing and negotiation == DO+ECHO: pass + + # allow LINEMODE + elif negotiation == WILL+LINEMODE: self.send(IAC+DO+LINEMODE, raw=True) + + # if the client likes EOR instead of GA, make a note of it + elif negotiation == DO+EOR: self.terminator = IAC+EOR + elif negotiation == DONT+EOR and self.terminator == IAC+EOR: + self.terminator = IAC+GA + + # if the client doesn't want GA, oblige + elif negotiation == DO+SGA and self.terminator == IAC+GA: + self.terminator = "" + self.send(IAC+WILL+SGA, raw=True) + + # we don't want to allow anything else + elif text[position+1] == DO: self.send(IAC+WONT+text[position+2], raw=True) + elif text[position+1] == WILL: self.send(IAC+DONT+text[position+2], raw=True) + + # strip the negotiation from the input + text = text.replace(text[position:position+3], "") + + # get rid of IAC SB .* IAC SE + elif len(text) > position+4 and text[position:position+2] == IAC+SB: + end_subnegotiation = text.find(IAC+SE, position) + if end_subnegotiation > 0: text = text[:position] + text[end_subnegotiation+2:] + else: position += 1 + + # otherwise, strip out a two-byte IAC command + elif len(text) > position+2: text = text.replace(text[position:position+2], "") + + # and this means we got the begining of an IAC + else: position += 1 + + # replace the input with our cleaned-up text + self.partial_input = text + def can_run(self, command): """Check if the user can run this command object.""" @@ -477,6 +574,11 @@ def log(message): # send the timestamp and message to standard output print(timestamp + " " + message) + # send the message to the system log + openlog("mudpy", LOG_PID, LOG_INFO | LOG_DAEMON) + syslog(message) + closelog() + def wrap_ansi_text(text, width): """Wrap text with arbitrary width while ignoring ANSI colors.""" @@ -707,12 +809,19 @@ def check_for_connection(listening_socket): # set the user's ipa from the connection's ipa user.address = address[0] + # let the client know we WILL EOR + user.send(IAC+WILL+EOR, raw=True) + user.negotiation_pause = 2 + # return the new user object return user -def get_menu(state, error=None, echoing=True, choices={}): +def get_menu(state, error=None, echoing=True, terminator="", choices=None): """Show the correct menu text to a user.""" + # make sure we don't reuse a mutable sequence by default + if choices is None: choices = {} + # begin with a telnet echo command sequence if needed message = get_echo_sequence(state, echoing) @@ -731,6 +840,9 @@ def get_menu(state, error=None, echoing=True, choices={}): # display a message indicating if echo is off message += get_echo_message(state) + # tack on EOR or GA to indicate the prompt will not be followed by CRLF + message += terminator + # return the assembly of various strings defined above return message @@ -739,15 +851,15 @@ def menu_echo_on(state): return universe.categories["menu"][state].getboolean("echo", True) def get_echo_sequence(state, echoing): - """Build the appropriate IAC ECHO sequence as needed.""" + """Build the appropriate IAC WILL/WONT ECHO sequence as needed.""" # if the user has echo on and the menu specifies it should be turned # off, send: iac + will + echo + null - if echoing and not menu_echo_on(state): return chr(255) + chr(251) + chr(1) + chr(0) + if echoing and not menu_echo_on(state): return IAC+WILL+ECHO # if echo is not set to off in the menu and the user curently has echo # off, send: iac + wont + echo + null - elif not echoing and menu_echo_on(state): return chr(255) + chr(252) + chr(1) + chr(0) + elif not echoing and menu_echo_on(state): return IAC+WONT+ECHO # default is not to send an echo control sequence at all else: return ""