Messing With Telnet

Telnet is a nice simple protocol for remote text based interaction. I wrote an interactive TUI canvas with it. Try it out here:

$ telnet jott.live 8000

You can use arrow keys to move around, type keys to change your character, and hit space to "stamp" the character. (Source code here.)

HackerNews may be slowing this down a bit, discussion here. I've spawned up ports 8000, 8001 and 8002

How I Built It

In my head I roughly envisioned users interacting with a shared terminal environment.

Telnet seemed like the best protocol to use, so I began by skimming the telnet spec and gradually implementing what seemed to be required.

Telnet is run atop TCP, so I started with import sockets and began poking and prodding at it. I chose this method over thoroughly reading the spec.

The telnet protocol begins with an option negotitation stage that is easily skipped. I ran through the telnet option codes and guessed that the code for "End of subnegotiation parameters" (SE) might be all the server needs to look for. Once that code is found, I assumed, the rest of the data will be raw user input.

Turns out that this guess almost works. Empirically the SE was followed by IAC DONT ECHO. After reskimming the spec, I concluded that IAC seemed reasonably necessary. Not sure about the "DONT ECHO" part, but my brittle code assumes it will always be present.

Unfortunately, I couldn't get away with no option negotiation. Telnet by default (at least on my machine) requires line breaks before sending user input to the server. To get around this I began by manually hitting the escape key from the client side, typing mode character and hitting enter:

$ telnet localhost 8000
^]
telnet> mode character

which made for quite a poor experience. Googling around I found that the server can actually request character mode. Sending those codes immediately after ignoring the client's codes ended up working.

I now had the ability to process every key a user typed after they telnetted into the server. I tested out some ANSI escape codes and found that it was quite easy to make the experience immersive. The two most important ANSI sequences ended up being:

HIDE_CURSOR = b"\033[?25l"
MOVE_CURSOR = lambda x,y : "\033[{};{}H".format(y,x).encode('ascii')

which basically allow you to recreate all of ncurses, provided you know the size of the user's terminal windows.

It is not immediately apparent how best to query the user's terminal size from telnet. My telnet client wasn't sending NAWS, and I couldn't be bothered to induce that with a proper negotiation procedure.

However, I found a nice little trick that involves moving the cursor to some very large size and sending a "request cursor" ANSI code. The result is the cursor moves to the bottom edge of the terminal window and returns a sequence that looks like '\x1b[25;80R' (a terminal size of 25x80). This only took a few lines to get working.

From this point forward I was quite happy with the APIs I'd discovered and the project became a basic exercise in program design. I hope you enjoy it :)