From c8a0506ea7e53d3ef6c348282e1be6fc91b749a3 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Sun, 26 Aug 2018 17:42:15 +0000 Subject: [PATCH] Support clients using CR+NUL to signal EOL IETF RFC 854 requires that a Telnet server accept CR+NUL interchangeably with CR+LF to indicate end of an input line from any NVT (client). CR+NUL also happens to be the default behavior of popular Telnet clients specifically when communicating on TCP port 23 (as opposed to non-default ports where more liberal protocol fallbacks get employed). Previously these clients would need to `set crlf` in their .telnetrc or at a telnet> command prompt as a workaround. Alter the selftest framework to send \r\0 from the NVT as an EOL to make sure this does not regress, and add a test to explicitly end a command with a \r\n just to make sure we can continue to support CR+LF from clients. --- mudpy/misc.py | 8 ++++++-- mudpy/telnet.py | 6 ++++-- mudpy/tests/selftest.py | 14 +++++++++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/mudpy/misc.py b/mudpy/misc.py index bae94b6..4e43353 100644 --- a/mudpy/misc.py +++ b/mudpy/misc.py @@ -843,11 +843,15 @@ class User: mudpy.telnet.negotiate_telnet_options(self) # separate multiple input lines - new_input_lines = self.partial_input.split(b"\n") + new_input_lines = self.partial_input.split(b"\r\0") + if len(new_input_lines) == 1: + new_input_lines = new_input_lines[0].split(b"\r\n") # if input doesn't end in a newline, replace the # held partial input with the last line of it - if not self.partial_input.endswith(b"\n"): + if not ( + self.partial_input.endswith(b"\r\0") or + self.partial_input.endswith(b"\r\n")): self.partial_input = new_input_lines.pop() # otherwise, chop off the extra null input and reset diff --git a/mudpy/telnet.py b/mudpy/telnet.py index 245f44a..ed99faf 100644 --- a/mudpy/telnet.py +++ b/mudpy/telnet.py @@ -125,9 +125,11 @@ def negotiate_telnet_options(user): # the byte following the IAC is our command command = text[position+1] - # replace a double (literal) IAC if there's an LF later + # replace a double (literal) IAC if there's a CR+NUL or CR+LF later if command is IAC: - if text.find(b"\n", position) > 0: + if ( + text.find(b"\r\0", position) > 0 or + text.find(b"\r\n", position) > 0): position += 1 text = text[:position] + text[position + 1:] else: diff --git a/mudpy/tests/selftest.py b/mudpy/tests/selftest.py index 799ac50..179871e 100644 --- a/mudpy/tests/selftest.py +++ b/mudpy/tests/selftest.py @@ -158,18 +158,25 @@ test_admin_setup = ( (2, "Whom would you like to awaken?", ""), ) +test_crlf_eol = ( + # Send a CR+LF at the end of the line instead of the default CR+NUL, + # to make sure they're treated the same + (2, "> ", b"say I use CR+LF as my EOL, not CR+NUL.\r\n"), + (2, r'You say, "I use CR\+LF as my EOL, not CR\+NUL\.".*> ', ""), +) + test_telnet_iac = ( # Send a double (escaped) IAC byte within other text, which should get # unescaped and deduplicated to a single \xff in the buffer and then # the line of input discarded as a non-ASCII sequence - (2, "> ", b"say argle\xff\xffbargle\r\n"), + (2, "> ", b"say argle\xff\xffbargle\r\0"), (2, r"Non-ASCII characters from admin: b'say argle\\xffbargle'.*> ", ""), ) test_telnet_unknown = ( # Send an unsupported negotiation command #127 which should get filtered # from the line of input - (2, "> ", b"say glop\xff\x7fglyf\r\n"), + (2, "> ", b"say glop\xff\x7fglyf\r\0"), (2, r'Unknown Telnet IAC command 127 ignored\..*"Glopglyf\.".*> ', ""), ) @@ -296,6 +303,7 @@ dialogue = ( (test_actor_disappears, "actor spontaneous disappearance"), (test_account1_teardown, "second account teardown"), (test_admin_setup, "admin account setup"), + (test_crlf_eol, "send crlf from the client as eol"), (test_telnet_iac, "escape stray telnet iac bytes"), (test_telnet_unknown, "strip unknown telnet command"), (test_admin_restriction, "restricted admin commands"), @@ -446,7 +454,7 @@ def main(): break if type(answer) is str: tlog("luser%s sending: %s" % (conversant, answer), quiet=True) - lusers[conversant].write(("%s\r\n" % answer).encode("utf-8")) + lusers[conversant].write(("%s\r\0" % answer).encode("utf-8")) captures[conversant] += "%s\r\n" % answer elif type(answer) is bytes: tlog("luser%s sending raw bytes: %s" % (conversant, answer), -- 2.11.0