Thomas Touhey

:

Faites un service shell, comme dans les années 80 !


Récemment, j’ai regardé WarGames, un film classique sur les années 80 et la technologie de l’époque. Je ne vais ni le décrire ni le critiquer ici, mais alors que je voyais le personnage principal communiquer avec les serveurs d’entreprise (et d’école) au travers d’un simple terminal, j’ai réalisé qu’il n’y en avait plus beaucoup aujourd’hui.

!

Le shell n'est plus en ligne, n'ayant pas survécu à une migration.

Jee repoussais le fait de faire le site de l’auto-entreprise de ma mère, Teapots Up’cyclin. Mais j’ai décidé d’en faire un shell.

Il est disponible tandis que je rédige cet article, vous pouvez l’essayer en tapant ceci dans un terminal UNIX (je suppose que les utilisateurs de Microsoft Windows devront utiliser PuTTY ou équivalent) :

ssh visitor@www.teapots-upcyclin.com

Dans cet article, j’expliquerai comment je l’ai réalisé.

Réalisation du shell.

Avant de rendre le shell accessible depuis Internet, le shell a dû être construit. J’avais déjà fait un module Python pour décoder les données brutes pour Teapots, en somme un dossier avec des fichiers ressemblant à de l’INI et des images, organisés d’une façon normée, pour un générateur de site web statique que j’avais commencé à réaliser. J’ai alors pensé que je ferais le shell dans ce même langage pour ne pas refaire cette procédure de décodage.

Un ami à moi m’a dit que je devrais utiliser la fonction getline dans le module linecache. Mais j’ai trouvé mieux : le module cmd, qui s’occupe de toute la base pour moi, et je n’aurai alors plus qu’à implémenter les commandes… et d’autres petites choses. Je vous recommande de lire la page avant de continuer, puisque je ne vais pas répéter ce qui s’y trouve.

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:

1
2
3
4
5
6
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):

1
2
3
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 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:

1
2
3
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

Amateur d'informatique.