-- Leo's gemini proxy

-- Connecting to nox.im:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini; charset=utf-8

Gemini Gemlog alongside a Go Hugo Blog


This blog is written and deployed with Go Hugo from scratch[1] and hosted with httpd on an OpenBSD[2] instance. I emphathize simplicity and you can read about my thoughts on why I think Gemini is a groundbreaking reminder[3] on why we should understand concepts bottom up.


1: Go Hugo from scratch

2: httpd on an OpenBSD

3: Gemini is a groundbreaking reminder


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


1: lagrange


Lagrange nox.im via Gemini Protocol[1]


1: 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[1] to generate gemtext documents from the Hugo markdown files and the gemini file server gmifs[2]. 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[3]. More on that in the next section.


1: md2gmi

2: gmifs

3: hugoext


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[1] and md2gmi[2] 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.


1: hugoext

2: md2gmi


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[1] I'm piping the gmifs access logs to GoAccess for server side analytics.


1: web analytics dashboard with GoAccess


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[1] and the parameters used here for reference.


1: snippet with a brief explanation of OpenBSD log rotation


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!


-- Response ended

-- Page fetched on Sun Apr 28 08:42:21 2024