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.
Handling a request
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
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
[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
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: Started Kodi Restart webhook. May 30 22:38:09 hostname bash: + echo -e 'HTTP/1.1 204 No Content\r\nConnection: close\r\n\r' May 30 22:38:09 hostname bash: + 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.
Our new service runs as the
http user, but that user doesn’t yet have
permission to run the restart command.
http ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart kodi
This webhook can be triggered from any web browser, but a helper app makes for a nicer remote control experience.
Here’s the one I set up for restarting Kodi:
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.
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 a
close(). Some people recommend waiting for a read of size zero between the
close()calls, so perhaps that would resolve the issue.
Another way to have the connection closed cleanly would be to leave off the
-coption, 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. ↩