Today I want to talk about telnet. Yes, telnet is stupid and you should use ssh. Sometimes you can’t though. For example if there is some old legacy app that is sitting on some remote server that is not even yours and the only way to interact with it is via telnet session. We have one of these things at work – it is a little app running on a mail server that allows people to set up an auto-reply message when they go on vacation.
In theory this is something that every user should be able to do themselves. The instructions are simple enough. Go to Start, Run, paste this telnet command into the box, hit enter, follow prompts on the screen. The interface is easy to use, if a little clunky. I never had any issues with it. And yet, no one ever wants to deal with. That black window with the dreaded blinking cursor is incredibly scary it seems. Most people call the help desk and request a message to be set up for them instead. This is not the end of the world, but it’s backwards. This system does not have a main administration panel where we could set these things up from above. So when someone calls in with a request, the helpdesk must actually telnet to the system and use their username and password.
So I started thinking if we could dumb-down this process enough to make it accessible to our average luser. The idea was to have a GUI layer shielding the user from the scary telnet stuff. I hacked up a tiny little prototype app in Python and it looked a bit like this:
It is very simplistic – kinda ugly actually. It’s Python using the Tkinter widgets for GUI-fication. It is less than 30 lines of code once again demonstrating that Python can be very terse and concise language despite it’s strict whitespace regime – even when coding GUI apps. Observe:
from Tkinter import * class AutoReply: def __init__(self): self.root = Tk() self.root.title("DG Auto Reply Tool") self.root.resizable(0,0) self.text = "" self.email = StringVar() self.onoff = StringVar() self.reply = Text(self.root) self.reply.grid(row=0, column=0, columnspan=5) eml = Entry(self.root, textvariable=self.email) eml.grid(row=1, column=0) status = Label(self.root, textvariable=self.onoff) status.grid(row=1, column=3) button = Button(self.root, text="Update", command=self.update) button.grid(row=1, column=1) toggle = Button(self.root, text="Auto Reply:", command=self.toggle) toggle.grid(row=1, column=2) self.root.mainloop() def update(self): pass def toggle(self): pass if __name__ == "__main__": app = AutoReply()
This is what Python is good at – creating usable software really fast. Next step was to figure out how this GUI layer would communicate with the telnet application. I started thinking about sockets and streams and all that fun stuff. That was my first instinct – just open a socket and dump messages to it and try to see if I can get anything back. I did something like this before, and it was a lot of tedious coding just to get the communication between the client and server. Of course I was using Java to do it, which means there was a lot of tedious coding there in general.
Python on the other hand ships with a built in telnet library (aptly named telnetlib) and all you really need to do to connect to a server and start sending messages is this:
I didn’t want to do all of that.
import telnetlib HOST = "my.remote.host" PORT = 1337 user = "my-username" password = "my-password" tn = telnetlib.Telnet(HOST, PORT) tn.read_until("login: ") tn.write(user + "\n") tn.read_until("Password: ") tn.write(password + "\n")
You just follow this pattern for all your other interactions with the server. You read_until and then you write and etc. I abstracted this into a tiny self contained class, and made my GUI initialize it in the constructor and then call it every time it needed to send or receive data. I kept it simple – each transaction was self contained, connecting to the server, logging in, interfacing with the app and then disconnecting. This way I didn’t have to worry about keeping the connection alive, timeouts, disconnects and could pretty much rely that the app running on the server will be in predictable state when I connect to it.
The rest was just a bit of creative screen scraping. The read_until method returns a string, which I could break into lines, and then parse each line to extract the information I needed. It was pretty easy to isolate the important data and discard everything else. I’m not going to post the code here because it is really customized to the particular application, and thus probably useless to most people.
The GUI works pretty well, but it is synchronous – it will block until the read/write transactions are accomplished which is probably not the worst thing that could happen. I’m pretty sure I could do it asynchronously but it would take more effort and make no huge impact on usability. The blocking as it is right now actually works like a feedback mechanism that shows the user when sending and receiving is taking place.
My app is actually more user friendly because it allows you to edit the message as a whole in a familiar editor window. The telnet app forces you to type the message line by line. The only way to change a line already in place is to delete it, append new line to the end of the message and then move it up one line at a time until it is in the right place. Oh, and pressing backspace inserts the lovely ^H instead of actually deleting the character. So the GUI is much friendlier and I deal with the craziness by simply nuking the message and then re-inserting it every time the user clicks on update.
I haven’t unleashed it on unsuspecting users yet – it’s still buggy and unfinished. I’m just throwing it out here to show how easy it is to hack things like that together with just a little bit of Python. The complete app is tiny (only slightly over 100 lines of code – including white space). The only problem is that most of my users are not running Python. This of course could be solved by using py2exe but then I’m basically converting 3KB of python into 30 MB of crap. Not a perfect solution there. I could either deploy python, ship the app as a huge native package, or rewrite it in C#. What would you do?