From 3047d309b10646b2b5c0c4d442658d0fe4814d7e Mon Sep 17 00:00:00 2001
From: Jeremy Stanley <fungi@yuggoth.org>
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