nox.im · All Posts · All in Go · All in OpenBSD
This blog is written and deployed with Go Hugo from scratch and hosted with httpd on an OpenBSD instance. I emphathize simplicity and you can read about my thoughts on why I think Gemini is a groundbreaking reminder on why we should understand concepts bottom up.
The Gemini protocol is a lightweight alternative to http. It mandates TLS and uses a policy called “trust on first use” TOFU to encrypt connections to servers. While regular SSL certificates from Let’s Encrypt etc can be used, the concept of certificate authorities CAs is not present nor checked. The protocol doesn’t allow for user tracking, cookies and doesn’t even employ the concept of user agents. The complement for the document format html on the web on Gemini is called Gemtext. To connect to the Gemini version of this blog, you need a browser that supports the protocol, on MacOS I’d recommend lagrange.
Lagrange can be installed with
brew tap skyjake/lagrange
brew install lagrange
The hugo version of this blog uses a similar layout that will be available for the Gemini gemlog. There are no sidebars, a minimal footer and header per page. Generally the layout will look as follows:
./content/_index.md <-- the landing page, it has a list of N posts attached
./content/about.md <-- optional pages in the root
./content/posts/ <-- a section will have all posts listed chronologically
./content/posts/<date format>/<content slug> <-- single page
The Gemini version will retain the exact same format. We’re using the tool
md2gmi to generate gemtext documents from the Hugo markdown files
and the gemini file server gmifs. Both tools have a focus on
simplicity, do one job and do it well and are self contained without dependencies outside of the
standard library. To process each file from hugo, I chain md2gmi
with
hugoext. More on that in the next section.
This shares a similar philosophy than httpd that ships with OpenBSD. httpd is a very basic webserver that supports FastCGI and TLS. It serves static files and directories via optional auto-indexing. gmifs doesn’t (yet) support virtual servers nor FastCGI as there was no need for it. But it does have auto-indexing, caching, concurrent request limiting, logging and TLS support. If no certificate is provided gmifs will provision one automatically at boot to ease testing, since the Gemini protocol requires it.
The short version of what I run to generate Gemtext output from my hugo blog directory in the
./public
directory is this:
hugo --minify # html
hugoext -ext gmi -pipe md2gmi # gemtext
That’s it. If I want to test the Gemini site locally I simply run the command gmifs
. Nothing more.
gmifs
creates a self-singed certificate on boot if no other parameters are provided for
localhost
.
As engineers, we like composability. I’m using the tools hugoext
and md2gmi from the hugo directory. The tool md2gmi
converts
markdown to gemtext. The utility hugoext
parses the hugo config file and recreates the same file
structure for content files through an arbitrary output pipe extension for processing. The pipeline
stage is the md2gmi
tool.
By default, hugoext
skips drafts, uses pretty URLs and creates section listings:
...
skipping draft content/snippets/markdown-syntax.md (3771bytes)
skipping draft content/snippets/rich-content.md (691bytes)
processed content/_index.md (751bytes)
processed content/about.md (3854bytes)
written public/index.gmi (473bytes)
written public/about/index.gmi (1415bytes)
written section listing snippets to public/snippets/index.gmi
written section listing posts to public/posts/index.gmi
If you’ve installed Go with pkg_add go
we can use the toolchain to install the latest tagged
stable version of gmifs as follows:
go install github.com/n0x1m/gmifs@latest
Install the binary to /usr/local/bin
, as it’s “non-standard”, locally compiled and not managed by
ports.
doas mv ~/go/bin/gmifs /usr/local/bin/
Then we create a directory for logging and content
doas mkdir -p /var/www/logs/gemini
doas mkdir -p /var/www/htdocs/nox.im
we then create and start the gmifs daemon, create /etc/rc.d/gmifs
with the following content
#!/bin/ksh
#
# $OpenBSD: gmifs,v 1.0.2 2021/07/12 10:00:00 rpe Exp $
daemon="/usr/local/bin/gmifs"
daemon_flags="-addr 0.0.0.0:1965 -root /var/www/htdocs/nox.im \
-host nox.im -max-conns 256 -timeout 5 -debug -cache 256 \
-logs /var/www/logs/gemini \
-cert /etc/ssl/nox.im.fullchain.pem \
-key /etc/ssl/private/nox.im.key &"
rc_reload=NO
rc_bg=YES
. /etc/rc.d/rc.subr
pexp="/usr/local/bin/gmifs.*"
rc_start() {
${rcexec} "nohup ${daemon} ${daemon_flags}"
}
rc_stop() {
pkill -xf "${pexp}"
}
rc_restart() {
pkill -xf "^${pexp}"
${rcexec} "${daemon} ${daemon_flags}"
}
rc_check() {
pgrep -q -xf ${pexp}
}
rc_cmd $1
Notice that we’re reusing the same Let’s Encrypt certificates that httpd is using under the same domain.
doas rcctl start gmifs
doas rcctl enable gmifs
Optionally enable directory listings with -autoindex
or add a file vi /var/www/htdocs/nox.im/index.gmi
and it should be visible under gemini://nox.im
(or your
domain/ip). You can also see when you hit it in the access logs:
tail -f /var/www/logs/gemini/access.log
orwell.nox.im XXX.XXX.XXX.XXX - - [09/Jul/2021:17:13:06 +0000] "/" 20 - 754.385µs
orwell.nox.im XXX.XXX.XXX.XXX - - [09/Jul/2021:17:13:10 +0000] "/" 20 - 184.65µs
In a recent post on web analytics dashboard with GoAccess I’m piping the gmifs access logs to GoAccess for server side analytics.
Note, I’m serving both html with httpd and gemtext with gmifs from the very same directory, this looks like this:
orwell$ ls -l /var/www/htdocs/nox.im
-rw-r--r-- 1 dre daemon 10 Jul 9 13:34 index.gmi
-rw-r--r-- 1 dre daemon 5 Jul 1 08:06 index.html
If enabled, we can see the cache working favorably on the response times as we no longer get hit by the ssd io and serve from memory instead. Gmifs uses a fifo cache so a short buffer allows to be articles fast the majority of times while it still rotates new versions in without rebooting the server. As I add more pages and articles I’ll make the cache larger with the flag above.
We use newsyslog /etc/newsyslog.conf
to setup log rotation and retention for gmifs.
# logfile_name owner:group mode count size when flags
/var/www/logs/gemini/access.log 644 4 * $W0 Z
/var/www/logs/gemini/debug.log 644 7 250 * Z
I’ve added a snippet with a brief explanation of OpenBSD log rotation and the parameters used here for reference.
We’re all set to deploy our static file content with rsync now.
To deploy the two versions, the blog and the gemini capsule, we just have to push the ./public
directory to the serving directory on the server, /var/www/htdocs/nox.im
. We utilize rsync
(or openrsync
) from our local machine for this:
rsync -a -P --delete ./public/ dre@nox.im/var/www/htdocs/nox.im/
This can be added with the two compile steps into a makefile to publish content.
openssl s_client -quiet -crlf -servername nox.im -connect nox.im:1965 \
| awk '{ print "response: " $0 }'
gemini://nox.im/
The output if pasted will look like this:
$ openssl s_client -quiet -crlf -servername nox.im -connect nox.im:1965 \
> | awk '{ print "response: " $0 }'
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = R3
verify return:1
depth=0 CN = nox.im
verify return:1
gemini://nox.im/
response: 20 text/gemini; charset=utf-8
response: # Dre's log
response:
response: ```
response: ___
response: (o,o) < Fiat lux.
response: {`"'}
response: -"-"-
response: ```
response:
...
Enjoy!