Thomas Touhey
:
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.
This shell is not up anymore, due to not surviving to a server transition.
I’m supposed to make my mother’s company site, Teapots Up’cyclin, 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):
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:
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