2021, it’s time to make your telnet service for DOS
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
Blocks and tables automatically fill the terminal width:
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
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())
Now the main question of this article:
➥ How do we connect Rich output to the socket server?
The Rich console object
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")
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
Footnotes
[1] There is an interesting and historical reason for why \r
should come before \n
. See: https://stackoverflow.com/a/1761086