From 3047d309b10646b2b5c0c4d442658d0fe4814d7e Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Tue, 13 Sep 2005 02:41:30 +0000 Subject: [PATCH] Imported from archive. * mudpy.py (User.enqueue_input, User.negotiate_telnet_options): Implemented a real Telnet option negotiation stack, paving the way for more robust client integration. (get_echo_sequence): Refactored echo handling in password prompts to rely on Telnet option negotiation. (get_menu): Added an RFC 885 end of record prompt terminator for improved MUD client support. --- mudpy.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/mudpy.py b/mudpy.py index 936d3ba..6326e77 100644 --- a/mudpy.py +++ b/mudpy.py @@ -11,6 +11,7 @@ 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 telnetlib import DO, DONT, ECHO, EOR, IAC, WILL, WONT from time import asctime, sleep class Element: @@ -315,9 +316,6 @@ class User: def send(self, output, eol="$(eol)"): """Send arbitrary text to a connected user.""" - # only when there is actual output - #if 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 @@ -327,18 +325,15 @@ class User: output = replace_macros(self, output) # wrap the text at 80 characters - # TODO: prompt user for preferred wrap width output = wrap_ansi_text(output, 80) # drop the formatted output into the output queue self.output_queue.append(output) - # try to send the last item in the queue, remove it and - # flag that menu display is not needed + # try to send the last item in the queue and remove it try: self.connection.send(self.output_queue[0]) - self.output_queue.remove(self.output_queue[0]) - self.menu_seen = False + del self.output_queue[0] # but if we can't, that's okay too except: @@ -356,8 +351,7 @@ class User: 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: @@ -383,6 +377,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 +397,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 +418,47 @@ 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 and move on + elif len(text) > position+1 and text[position+1] == IAC: + text = text.replace(IAC+IAC, IAC) + position += 1 + + # this must be an option negotiation + elif len(text) > position+2 and text[position+1] in (DO, DONT, WILL, WONT): + + # if we turned echo off, ignore the confirmation + if not self.echoing and text[position+1:position+3] == DO+ECHO: pass + + # we don't want to allow anything else + elif text[position+1] in (DO, WILL): self.send(IAC+WONT+text[position+2]) + + # strip the negotiation from the input + text = text.replace(text[position:position+3], "") + + # otherwise, strip out a two-byte IAC command + else: text = text.replace(text[position:position+2], "") + + # 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.""" @@ -731,6 +781,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 IAC EOR to indicate the prompt will not be followed by CRLF + message += IAC+EOR + # return the assembly of various strings defined above return message @@ -739,15 +792,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 "" -- 2.11.0