Fresh from the attic

2021, it’s time to make your telnet service for DOS

Vincent Jordan

--

Because Terminals are cool~

Tired of websites plagued with heavy JavaScript, ads and consent pop-up?
There were simpler times on the internet when everything was text-based.
A time where cathode-ray tube displayed 80 by 24 blocks of glowing and colorful characters.

But I don’t wanna to learn those pesky terminal escape characters!

Don’t worry, Python comes to the rescue. It has a module to generate the ASCII art, without the pain:
Rich — https://github.com/willmcgugan/rich
Learn more about Rich capabilities in this article or this one

Easy to change styles and colors with phpBB-inspired markup language

Blocks and tables automatically fill the terminal width:

Tables fit the terminal width with nice UTF-8 border characters

Note: there are various table box styles. See the docs or this command:

python -m rich.box

Wait a minute! Those UTF-8 border characters will not work on DOS.
Rich can produce pure ASCII tables too

Pure ASCII table

Okay, we are ready to make ASCII content, now the telnet service…

My simple telnet server in Python

Telnet is just text sent over TCP/IP. Let’s use Python’s socketserver.
There is an example in the official documentation:

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
def handle(self):
self.data = self.request.recv(1024).strip()
self.request.sendall(self.data.upper())

if __name__ == "__main__":
HOST, PORT = "localhost", 9999

with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
server.serve_forever()

When using StreamRequestHandler instead of BaseRequestHandler in this example, the handler can “use streams (file-like objects that simplify communication by providing the standard file interface).”

class MyTCPHandler(socketserver.StreamRequestHandler):
def handle(self):
# self.rfile is a file-like object created by the handler;
self.data = self.rfile.readline().strip()
# self.wfile is a file-like object used to write back
self.wfile.write(self.data.upper())
Hello telnet

Now the main question of this article:
➥ How do we connect Rich output to the socket server?

The Rich console object

What’s the difference from before, you may ask?

Using a Console object, you gain much more control on how your content is printed. See the reference manual for rich.console.Console.

Especially interesting is the file parameter. We can print to a given file.
Let’s come back to the telnet server example:

class MyTCPHandler(socketserver.StreamRequestHandler):
def handle(self):
self.data = self.rfile.readline().strip()
# self.wfile is a file-like object used to write back
self.wfile.write(self.data.upper())

console = Console(file=self.wfile, force_terminal=True, color_system="256", width=80)
console.print(self.data.upper().decode('ascii'), style="bold red")

Unfortunately, it does not work because self.wfile is byte-file, and Console expects a text-file:

TypeError: a bytes-like object is required, not 'str'

This problem is solved with the wrapper class: io.TextIOWrapper(docs).

class MyTCPHandler(socketserver.StreamRequestHandler):
def handle(self):
self.data = self.rfile.readline().strip()
# self.wfile is a file-like object used to write back
self.wfile.write(self.data.upper())

wfilet = io.TextIOWrapper(self.wfile)
console = Console(file=wfilet, force_terminal=True, color_system="256", width=80)
console.print(self.data.upper().decode('ascii'), style="bold red")
Success! \(^_^)/

Additional considerations for DOS

Unlike default Linux terminal which expects UTF-8 and \n as line termination, DOS will rather expect pure ASCII and \r\n¹.

Force ASCII encoding and \r\n line termination instead of \n:

wfilet = io.TextIOWrapper(self.wfile, encoding="ascii", newline="\r\n")

Note: since UTF-8 was designed in such a way to be a superset of ASCII, this config will work on Linux default terminal too. Even better, on Linux \r has no meaning and will be ignored.

Demo code

Find a complete sample code at:
https://github.com/vejipe/simple_telnet_server

Bonus from the sample code:
➥ Get a menu to choose UTF-8 (Linux) or ASCII (DOS) mode

UTF-8 for a pretty rounded box
ASCII for a warm feeling of nostalgia

Footnotes

[1] There is an interesting and historical reason for why \r should come before \n. See: https://stackoverflow.com/a/1761086

--

--