Thomas Touhey

Computer enthusiast.

Make a shell service, like in the 80s!

Recently, I watched Wargames, a classic film from the 80s around technology. I'm not going to describe it or criticize it here, but as I saw the main character communicating with company (and school) servers using a simple terminal interface, I realized there wasn't many left today.

I'm supposed to make my mother's company site, Teapots Upcycl'in, for quite a long time now - I'm delaying this as, as you might've noticed, I'm a complete noob in web design (I'm more into functionnal/back-end stuff). But I decided I would make a shell for it.

It's available as I'm writing, you can try it by typing this into a UNIX terminal (I guess Microsoft Windows users will have to use a PuTTY-like):

ssh visitor@www.teapots-upcyclin.com

In this article, I'll explain how I achieved this.

Making the shell

Before making the shell accessible from the Internet, the shell has to be made. As I already made a Python3 module for decoding raw Teapots data (which is roughly a folder with INI-like files and images, organized in some way), which I made for a static website generator I started to make. I thought I'd make the shell using Python3 so the decoding part doesn't need to be re-done.

A friend of mine told me I should use the getline function in the linecache module. But I found better: the cmd module, that basically does all the basic things for me, I'll just have to implement the commands... and other things. I advise you to read the page before reading what's next in this section, as I'm not going to repeat most of it.

Let's start with basic things. You'll have to start by making a derivating class from cmd.Cmd (the one in the official example is called TurtleShell). To change the intro, you just have to change the intro member, same thing with the prompt. Then, to implement commands, you just have to make do_<command> methods that will take the argument string (which I split using shlex.split). You can also set the default command (when the command isn't recognized), and manage Control-D (EOF) by implementing the EOF command. The rest of the basics are on the reference page, but you might be interesting in some advanced Python3 shell hacking: this is what is coming next.

First of all, control the tab completion (I don't know why it isn't documented). This is achieved by the completenames method, which will by default look for class members which name start with do_. You can override it in your shell class. In the Teapots Shell, I wanted to hide the ugly EOF command, and also wanted to hide some secret commands. here's more or less how it is implemented:

class YourShell(cmd.Cmd):
    public_commands = ["help", "exit", "list"]
    # ...
    def completenames(self, text, *ignored):
        lt = len(text)
        return [c for c in self.public_commands if c[:lt] == text]

Then, the commands. They were quite simple to make: we start by splitting the argument string by using shlex.split (in a wrapper, that sends ['--help'] if shlex.split returns an exception), then we check if "--help" or "-h" is in the arguments and if we have the correct number of positional arguments, we act using these. In order to keep the class clean, some functions are external and are called with the shell (self) and the splitted arguments.

Interruptions like SIGINT (Control-C) can be managed using try / exception with the KeyboardInterrupt exception. I've managed it at the cmdloop() level, so it acts like an EOF signal, but in order to do nothing, one would need to override onecmd (where, if I remember well, the command is read and interpreted), which I didn't bother to.

The locale management is achieved quite simply. Locales are in YAML files (like fr.yml or en.yml) in a subdirectory where they are alone (in the Teapots Shell case). They just have to be found and listed to know what locales are available (fr and en). A global variable in the shell indicates the default locales it will be looking for (DEFAULT_LOCALES = ["fr", "en"]), and if it doesn't find any of these, it will take the first one available (not necessarily the first one found, a dictionary in Python is unordered!). The found locales and their data is a global variable, the shell's current locale is local (no pun intended); from there, I think you can easily manage locales in your shell.

Colors are easy to manage on your own, but it can be quite ugly. I got the termcolor module to do that for me. Its colored method is quite powerful and clear :)

I think all the interesting things have been written down now. I won't release the Teapots Shell code as I have put some secret things in there, for fun - but with what I told you above, if you know how to code in Python3, it really shouldn't be too difficult. Just try to make it easy to use for your users, and that to the small details; for example, if the command is unknown on the Teapots Shell, the default method will first look if the "command" is an article ID, and if it is the case, it will run show <command>! It's nice when the shell wants to help you :)

Putting your shell on a real server

So now we have a shell that works perfectly, and we want users to connect to it from their home for example. The first step for that is to choose the protocol we want to use. I first thought about Telnet (the unsecure ancestor of SSH) because I knew the Star Wars ASCII animation over Telnet on Blinkenlights.nl that was using it; but then, after some research, I've found out it was really insecure, and I wanted a technology where I could put a safe login on (maybe not for Teapots, but it could come handy in some future projects). So I chose SSH.

Now, on Debian GNU/Linux (which my server runs on), I'm using the OpenSSH server (the default one). There are two possibilities I could have chosen: run a different server just for public connections and make the server for "normal" (administration) connections on another port (eventually protected by port knocking), or just use the normal server and allow only some accounts to be publicly available - that's the simplest possibility, but it's limited, so I chose it.

Now, making the user and assigning the shell to it isn't too difficult. First of all, you have to declare your shell as a shell by appending it to the /etc/shells file. Then, to create the user and assign the shell to him (as a superuser, using su, sudo or any equivalent; oh, and there might be a shorter way of doing so, but this works):

useradd visitor
passwd -d visitor
chsh -s /usr/bin/myshell visitor

Once this is done, you should try to login as your user to see if the right shell comes, e.g. with the following command:

sudo su visitor

You don't want anonymous people to use a "real" shell and visit the files in your server (if you manage permissions correctly, I agree it shouldn't be a problem, but most people don't do and configuration files are visible by any user, so that's a risk you don't want to take).

Once this is done, it's time to be confronted to a problem with OpenSSH: login is disabled for accounts that don't have a password. You can do this simply, by telling everyone the password for the visitor account is visitor, for example, but I thought that wasn't simple enough for anyone to use it.

Here's how I managed to remove the password validation step for the visitor account. Remember that I'm not a security expert and that this maneuver can be dangerous - if you don't trust me, which is normal as I'm a simple guy on the Internet you probably don't know, check by yourself if it's safe.

You'll have to modify PAM files and the SSH daemon config to get around it. In the PAM folder (/etc/pam.d), open the common-auth file and replace nullok_secure by nullok. You can now open the SSH daemon config file (/etc/ssh/sshd_config), go right to the end and add something like this:

Match User visitor
    PasswordAuthentication yes
    PermitEmptyPasswords yes

Restart the SSH daemon (on Debian GNU/Linux, service sshd restart) or reboot the machine (radical but efficient), and that's it, the visitor account should be available to anyone!

Conclusion

In case that wasn't obvious enough, I'm a CLI lover, and being able to connect and interact with a service using my shell is like a fantasy, come true with the Teapots Shell. I know there are other ways of doing this, like by communicating with an HTTP REST API, but that's not quite the same thing: with a distant shell, you have more control over the user behaviour, and you can more easily make animations and interactive applications (ever heard of sshtron?) while keeping some features secret more easily (reverse engineering is harder when the object you're trying to hack is not on a machine you can't control). Of course, it's not for everyone (some people don't even know what a shell is!), but it's something quite fun to do.

I want to see more shells out there, with one implementing an interactive and multiplayer chess game! :D

Thomas Touhey

Computer enthusiast.