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) :
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