nox.im · All Posts · All in Go · All in OpenBSD

Gemini Gemlog alongside a Go Hugo Blog

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 nox.im via Gemini Protocol

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.

hugoext - Convert Hugo Markdown to Gemtext

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

Gemini file server - gmifs on OpenBSD

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

Configuring gmifs

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.

Logrotate

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.

Deploy Gemini Capsule

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.

Query with OpenSSL Client

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!


Published on Friday, Jul 9, 2021. Last modified on Tuesday, Mar 15, 2022.
Go back

If you’d like to support me, follow me on Twitter or buy me a coffee. Use Bitcoin
BTC address: bc1q6zjzekdjhp44aws36hdavzc5hhf9p9xnx9j7cv