/'su.la/ for the suffix of capsule in Spanish, Cápsula.

sula logo

Git

A Gemini protocol server written in Scryer Prolog.

Requirements

sula depends on a patched Scryer Prolog which can be found here (branch js/fixes). The required patches are:

  • A native '$copy_stream'/2 builtin used for streaming binary file bodies to TLS clients without materialising the contents on the Prolog heap.
  • A fix to library(pio)’s buffer_prepare_for_n/5 so that lazy reads from process pipes (and other streams whose at_end_of_stream/1 never reports true) terminate on EOF instead of spinning.
  • A non-blocking poll loop in socket_server_accept/4 that checks Scryer’s INTERRUPT flag, so SIGINT becomes a catchable '$interrupt_thrown' exception instead of being trapped behind a blocking syscall.
  • A port to rustls.
  • A modification of tls_server_negotiate to include the optional client certificate.

Build and install the patched Scryer:

git clone https://git.sagredo.dev/scryer-prolog -b js/fixes
cd scryer-prolog
cargo install --path .

openssl(1) must also be on PATH — it’s invoked at startup to read the CN from the configured identity certificate and verify it matches the configured hostname.

Running

./sula.pl --addr HOST:PORT --hostname NAME --content DIR --certs DIR

sula.pl is a polyglot script: bash detects scryer-prolog on PATH and execs it with sula:run, halt as the entry goal.

Example:

./sula.pl \
    --addr 127.0.0.1:1965 \
    --hostname gmi.example.dev \
    --content ./site \
    --certs .

CLI options

All options accept any order. Anything unrecognised is silently dropped.

OptionMeaningDefault
--addr HOST:PORTBind address and port for the listening socket127.0.0.1:1965
--hostname NAMEExpected CN of the certificate. Startup aborts on mismatchlocalhost
--content DIRRoot directory for served files./site
--certs DIRDirectory containing cert.pem and key.pem.

Stopping the server

Ctrl+C triggers a clean shutdown: the listening socket is closed, the top-level catch logs Shutting down, and the process exits 0.

Features

  • TLS via rustls, PKCS#12 identity files.
  • Hostname verification: at startup, cert_is_for_hostname/2 shells out to openssl x509 and asserts the cert’s CN matches --hostname.
  • Content negotiation by extension via mime/2, populated at startup from /etc/mime.types (parsed by a DCG in mime.pl). text/gemini is added for .gmi.
  • Text responses sent via format/3; binary responses streamed in native code through copy_stream/2 (file → TLS socket, no Prolog heap traffic).
  • Per-connection error handling: TLS handshake failures and mid-stream client disconnects are logged and the loop continues. Other errors re-throw and surface at the top level.
  • Graceful shutdown on SIGINT via the patched Scryer socket_server_accept/4.

Layout

sula.pl        Polyglot launcher + main sula module (run/0, request loop).
config.pl      CLI parsing (DCG) and config accessors (cert/1, addr/1, ...).
cert.pl        Certificate loading + hostname-vs-CN check.
mime.pl        /etc/mime.types parser (DCG) and mime/2 facts.
request.pl     Request line reader.
gemini_uri.pl  Gemini URI DCG (gemini://host[:port]/path[?query]).
ip.pl          IP address recognition (rejected as Gemini hosts).
response.pl    Response status code DCG.
log.pl         Tagged log_msg/3.
banner.pl      Reads banner.txt and emits it line-by-line via display_banner/1.

Planned features

  • Use key and cert instead of identity.p12
  • Client certificates
  • Load configuration from a configuration file
  • Save and load users
  • Run CGI scripts
  • All status codes
  • Rate limiting
  • Virtual hosting
  • File logging
  • Multi-threading or kind of?
  • Hot reload?