Imported from archive.
authorJeremy Stanley <fungi@yuggoth.org>
Tue, 13 Sep 2005 02:41:30 +0000 (02:41 +0000)
committerJeremy Stanley <fungi@yuggoth.org>
Tue, 13 Sep 2005 02:41:30 +0000 (02:41 +0000)
* 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

index 936d3ba..6326e77 100644 (file)
--- 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 ""