Inspired by Simon Willison’s advice, I’ve decided to write about this weird dumb project that I’ve been doing since a couple of weeks ago.
In my home network, I have many remote control functions that need to be called externally. These functions are generally written and deployed as shell scripts, stuff like “powering on a computer”, “turning off a device”, “beep with a specific sound”, and so on.
Now, if I wanted to have perfect security, I should run these scripts via SSH, ideally with a user created for this purpose. But because I’m a lazy person, I want to be able to run this with minimal effort and simple authentication. shell2http is the perfect solution to do just that – it allows the execution of shell scripts via HTTP, in a way that’s very similar to the old CGI style of building web applications.
Using shell2http and a systemd service have been perfect for my own needs. But it gets annoying having to connect to the machine via SSH, changing the systemd unit file, running daemon-reload and restart everytime I want to add or remove scripts.
So I had this great (or terrible) idea to make a web administration interface where I could change the available scripts, username and password used for authentication, and restart the service with the newly configured parameters.
However, I wanted to make the easiest possible deployment, using shell2http as the “application server” and Python as the language for building the app, due to the fact that I’m very familiar with it, and it’s present by default in all major Linux distributions. To achieve these goals my constraints were the following:
- It should be written in a single Python file
Starting from the systemd unit file
When I started making this project, my unit file was very simple, something like this:
-form is particularly useful for my needs, because it can convert GET parameters
and POST form data into environment variables (ie: GET
parameter is passed into
the executable as an environment variable
File uploads are also supported, see the documentation for more details.
My first change was to remove the path-script declarations from this file and put
in a separated
.env file. The way to do this is to add a
I’ve already put there a
/admin route to serve this app.
Meanwhile, the environment file will have this format:
SH_BASIC_AUTH is used natively by shell2http to add optional basic authorization.
SH_ROUTES is where we’ll end up putting the path-script declarations. This way,
it gets very easy to programmatically edit the configurable parameters,
and it’s not necessary to call
daemon-reload anymore for every little change
restart will do it).
Building the web interface
To concentrate on functionality while minimizing design-related stress, I stole a pre-existing classless CSS library and saved it into a Python string. I chose: sakura.css.
For the base HTML structure, I’m using the relatively obscure
the standard library:
The problem with this method, is that it gets very ugly when there’s a need to generate more dynamic HTML – template languages were invented for a reason!
For these pages, I’ve decided to use a nicer (but very slow) way of building HTML
pages, by using the standard library
ElementTree, which can be used to build
HTML/XHTML documents, and more generally, XML documents.
To simplify and make the page-building process more intuitive and HTML-like,
I created wrapper classes called
HtmlElement that enables writing
a document as follows:
It looks nice and beautiful, but I don’t recommend doing this for serious projects. If you really want to explore this approach, take a look at the projects pyxl and mixt. They’re modifying the Python language (with monkey patching and other cool ways) by allowing the use of HTML-like syntax natively, akin to JSX.
I think this model of building web applications has so much unexplored potential, combined with libraries like Tailwind CSS and htmx, now you just need a single language to write pages and components.
Making a development server
People generally know that Python has a built-in simple static/CGI web server,
python -m http.server, which is a very handy tool. What many don’t know is
that it’s really easy to build your own custom development web server on
top of the standard library
wsgiref module, by implementing the WSGI
protocol in your app.
I say development server, because when writing for production, there’s many edge cases that are quite hard to get right, that’s why micro-frameworks like Flask and Bottle exist in the first place.
I think this feature is quite nice, because this way you don’t need shell2http just to see if the pages are being rendered correctly or if the forms are behaving as expected.
This is the full web-server implementation:
render(variables) is where all the logic/routing/rendering is done. To run this
wsgi() function in the Python’s builtin WSGI server, forever, just call:
This is a single-threaded webserver, but you can customize it to be threaded or to use a thread-pool, like this:
How neat! I could use this solution to fully replace shell2http, but I’m not inclined to, because I trust way more that project than my own code :)
Here is some screenshots of the app. It is kinda ugly at the moment, but it is what it is…
To make this project perfect for me, it should automatically download and install shell2http binary when missing, create a systemd unit file, and update itself from the repository. The look and feel could also be improved, I guess.
I hope you found anything useful or at least interesting here. I’m not recommending this project or shell2http to anyone, this all feels like a big security nightmare, but if you’re really inclined to use this, at least run it behind a reverse proxy with a better authentication mechanism :)
If you want to take a look at the full code, this is the repository: https://github.com/hjalves/s2h-admin.
The code is not that interesting apart from the stuff I mentioned here, and it’s very messy, but I appreciate if you have any suggestion on how to improve it.
Thanks for reading 😊