A simple webhook with Netcat and systemd
A webhook is a little program that listens for web requests and triggers a given action when it receives one. It lets you use simple web protocols and tools to trigger an action on a certain system, from anywhere.
Unix-like systems make it easy to string together commands to perform just about any action you’d like. Even a webhook itself can be implemented with a one-line shell script and some system config.
In the following example I’ll show the webhook that I run on my GNU/Linux home server so I can restart Kodi (the free and open-source media center software) from a button on my phone.
Handling a request
With GNU Netcat1 we can write a shell command to wait for a single web request, answer it, and restart Kodi:
echo -e "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r" | \
nc -l 0.0.0.0 -p 7777 -c; \
sudo systemctl restart kodi
The nc
command here waits until it receives a connection on port 7777, on any
interface. Once a connection is established, it reads the data we piped into
its standard input and sends it over the network to the client. When it’s
reached the end of the input data, it closes (-c
) the connection and exits.
The data we send here is just enough to gracefully2 close a HTTP connection:
HTTP/1.1 204 No Content Connection: close
Handling successive requests
Many GNU/Linux distributions use systemd to manage start-up and system services.
Here’s a systemd service definition that will run our shell command, and
restart it after it finishes handling each request. I put this in
/usr/lib/systemd/system/kodi-restart.service
:
[Unit]
Description=Kodi Restart webhook
After=network.target
[Service]
User=http
Type=simple
ExecStart=/bin/bash -xc 'echo -e "HTTP/1.1 204 No Content\\r\\nConnection: close\\r\\n\\r" | nc -l 0.0.0.0 -p 7777 -c; sudo systemctl restart kodi'
Restart=always
StartLimitInterval=1min
StartLimitBurst=60
[Install]
WantedBy=multi-user.target
Note that the newline escaping (\\r\\n
) now has double slashes: one for Bash,
one for systemd.
The command is run as the http
user (more on this later). The Type=simple
says that we’re just executing a normal command, not one that launces a
separate background processes. We want to Restart=always
, whenever the
command exits. The StartLimit
options say that the command can be restarted
up to 60 times per minute before the service is marked as failed.
With the service definition in place, we can start the service:
$ sudo systemctl start kodi-restart
And check that it’s running correctly:
$ systemctl status kodi-restart
● kodi-restart.service - Kodi Restart webhook
Loaded: loaded (/usr/lib/systemd/system/kodi-restart.service; disabled; vendor preset: disabled)
Active: active (running) since Tue 2017-05-30 22:38:09 CEST; 11s ago
Main PID: 24341 (bash)
CGroup: /system.slice/kodi-restart.service
├─24341 /bin/bash -xc echo -e "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r" | nc -l 0.0.0.0 -p 7777 -c; sudo systemctl restart kodi
└─24343 nc -l 0.0.0.0 -p 7777 -c
May 30 22:38:09 hostname systemd[1]: Started Kodi Restart webhook.
May 30 22:38:09 hostname bash[24341]: + echo -e 'HTTP/1.1 204 No Content\r\nConnection: close\r\n\r'
May 30 22:38:09 hostname bash[24341]: + nc -l 0.0.0.0 -p 7777 -c
To have it automatically start after rebooting the system, we can enable it:
$ sudo systemctl enable kodi-restart
Created symlink /etc/systemd/system/multi-user.target.wants/kodi-restart.service → /usr/lib/systemd/system/kodi-restart.service.
Granting permissions
Our new service runs as the http
user, but that user doesn’t yet have
permission to run the restart command.
We can grant the appropriate3 permissions with the following
sudoers configuration, in a new
file named /etc/sudoers.d/http-kodi-restart
:
http ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart kodi
Remote control
This webhook can be triggered from any web browser, but a helper app makes for a nicer remote control experience.
HTTP Shortcuts is a simple, open-source (MIT) Android app that lets you create shortcuts for web requests, and place buttons on your home screen to trigger them.
Here’s the one I set up for restarting Kodi:
Conclusion
A one-line shell script wrapped in a systemd service definition is a good way to expose webhooks for triggering a few simple actions within a trusted network.
More demanding scenarios may call for validation or processing of request data, authentication and authorization, handling of concurrent requests, or TLS encryption. In those cases you’ll want a more sophisticated tool.
The webhook project is an open-source (MIT) tool, written in Go, that might be a good fit.
-
There are quite a few Netcat implementations. The original implementation was rewritten to add IPv6 support for BSDs. GNU Netcat is a full rewrite. Ncat is an implementation included in the Nmap suite. Each one implements similar functionality, but has its own set of options. Here I’m using GNU Netcat. ↩
-
This response, when delivered completely, is enough to gracefully respond to the HTTP request. However, sometimes the client will receive an incomplete response:
Kodi Restart failed: sendto failed: EPIPE (Broken pipe)
The problem seems to be a Netcat issue that others have also observed:
in the past I’ve been burnt by race conditions where the connection is closed before the last few bytes to arrive on stdin have actually been transmitted over it
In the GNU Netcat source code, the connection is closed by a
shutdown()
, followed immedately by aclose()
. Some people recommend waiting for a read of size zero between theshutdown()
andclose()
calls, so perhaps that would resolve the issue.Another way to have the connection closed cleanly would be to leave off the
-c
option, so that Netcat keeps the connection open until it is closed by the client, which it will only do after it has read the full HTTP 204 response. ↩ -
With this setup, anyone who can make a request to the right port can restart Kodi up to 60 times per minute. I’m happy to expose this functionality within my home network, but in other environments additional security measures would be appropriate. ↩