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:

screenshot screenshot

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.


  1. 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. 

  2. 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 shutdown() and close() 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. 

  3. With this setup, anyone who can make a request to the right port can restart Kodi up to 60 times per second. I'm happy to expose this functionality within my home network, but in other environments additional security measures would be appropriate.