The Art Of The Bodge: Using OpenBSD's httpd, slowcgi, hotplugd
I've recently become interested in photography after picking up a $20 camera from an op-shop in town. I've got two cameras that I really use; one for some weird circuit-bending stuff, and one regular camera for 'normal' photography.
Both of these cameras use SD cards which can make getting at the images on them a bit of a pain. I either have to get out a laptop, or hope my phone's USB port decides to co-operate with me. This is fine if I'm looking to get all the images off of a camera anyway, and I do have scripts set up to automate the process, but I still wanted a faster solution for just getting at one or two pictures with minimal readers, devices, etc.
So for my next trick, watch on in awe as I bodge together a solution to this problem using only things in the OpenBSD base system.
This isn't going to be a tutorial but rather a thousand-foot overview of what I've done, with some dotfiles sprinkled throughout.
The Vision
My server, or rather an old thinkpad I leave on, has an SD card slot. I'd love to be able to plug in an SD card or camera then have the images viewable on a password-protected webpage.
In the past I'd probably do something silly like using a webserver framework like rocket to build an authenticated web-app that could serve these files and which handled the automatic mounting of SD cards as they were detected. This time however, I'm going to take a more unixy-approach.
The Stack
My server already serves multiple sites using OpenBSD's httpd webserver that ships in the base system. We'll use this to serve files. httpd also supports using files generated by htpasswd for http basic authentication. That'll be enough to secure the webpage.
For handling https, OpenBSD's relayd can act as a reverse-proxy, while acme-client can grab us our Let's Encrypt certificates. Thanks to Michael W Lucas' Httpd and Relayd Mastery Book for helping me out with that one.
We'll need to mount the SD cards automatically too. For that, hotplugd comes in handy. hotplugd allows you to define scripts that run automatically on device attachment (/etc/hotplug/attach). We'll use the disklabel utility to set the duids of our SD cards, then watch for them in our hotplugd script.
For viewing our images, we'll create a simple CGI script. CGI scripts are basically just programs that spit out http responses. OpenBSD's httpd can use slowcgi to capture the output of these scripts. Thanks again to Michael.
Remember to read the manpages and make sure all the daemons you need are enabled and started. Even the best code only works if it's running.
Configuring httpd
While the configuration of relayd and acme-client isn't trivial, it's beyond the scope of this article. Let's begin by having a look at the server block for this project in httpd.conf.
server "camera.baggins.family" {
listen on 127.0.0.1 port 80
authenticate with "/htpasswd"
root "/htdocs/camera.baggins.family"
location "/" {
block return 301 "https://camera.baggins.family/photos.cgi"
}
location "*.cgi" {
fastcgi socket "/run/slowcgi.sock"
root "/cgi-bin"
}
}
authenticate with "/htpasswd" is what enables http basic authentication. The path here is relative to httpd's chroot, placing the file at /var/www/htpasswd on the host. We can create this file with htpasswd <path> <username> which will then prompt for a password. Extra users can be added with the same command.
The first location block defines a block option that will redirect us to the CGI script if we just load up camera.baggins.family.
The final location block for *.cgi configures httpd to use fastcgi over the socket at /run/slowcgi.sock to run our executables in /cgi-bin.
Configuring hotplugd
The following is the /etc/hotplug/attach script for hotplugd. This daemon handles mounting the SD cards once they're detected by the OS.
#!/bin/sh
DEVCLASS=$1
DEVNAME=$2
case $DEVCLASS in
2)
# Disks
DUID=`/sbin/disklabel $2 2>&1 | \
sed -n '/^duid: /s/^duid: //p'`
case $DUID in
ba84893034985743)
# Camera SD Card
mkdir -p /var/www/htdocs/camera.baggins.family/mnt
mount_msdos -u www ba84893034985743.i /var/www/htdocs/camera.baggins.family/mnt
;;
ba84893034985742)
# Camera SD Card
mkdir -p /var/www/htdocs/camera.baggins.family/mnt
mount_msdos -u www ba84893034985742.i /var/www/htdocs/camera.baggins.family/mnt
;;
esac
;;
esac
These magic numbers, ba84893034985742 and ba84893034985743 are duids. These are unique disk identifiers that can be set/read with disklabel's interactive mode with disklabel -E.
This script mounts the i partition of the cards at /var/www/htdocs/camera.baggins.family under the www user.
The CGI Script
This CGI script is installed at /var/www/htdocs/camera.baggins.family/cgi-bin/photos.cgi. When httpd gets an authenticated request for https://camera.baggins.family/photos.cgi it hands the request off to slowcgi which runs photos.cgi in turn. The output of the script becomes the response.
#!/bin/sh
cd /htdocs/camera.baggins.family/mnt/DCIM
echo_image () {
IMAGE_PATH="/mnt/DCIM/$1"
echo "<a href='$IMAGE_PATH'>"
echo " <img src='/mnt/DCIM/$1'></img>"
echo "</a>"
}
echo 'Content-Type: text/html'
echo ""
echo "<!DOCTYPE HTML>"
echo "<html>"
echo "<head>"
echo ' <link rel="stylesheet" href="/style.css">'
echo ' <meta name="viewport" content="width=device-width, initial-scale=1.0">'
echo "</head>"
echo "<body>"
for image in */*; do
echo_image "$image"
done
echo "</body>"
echo "</html>"
photos.cgi runs inside the chroot at /var/www. This means anything the script depends on must be copied into /var/www. All this script requires is that we copy /bin/sh to /var/www/bin/sh. This will need to be automated, preferably on boot. This way the sh binary is updated automatically when we update the server. I find /etc/rc.local the easiest way to do this.
Useful?
Honestly, yeah! It's pretty nice to be able to pull a picture off a camera on demand like this. Maybe I'll add CGI scripts for deleting or backing up images, but this will do for now.