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

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'/2builtin used for streaming binary file bodies to TLS clients without materialising the contents on the Prolog heap. - A fix to
library(pio)’sbuffer_prepare_for_n/5so that lazy reads from process pipes (and other streams whoseat_end_of_stream/1never reports true) terminate on EOF instead of spinning. - A non-blocking poll loop in
socket_server_accept/4that checks Scryer’sINTERRUPTflag, soSIGINTbecomes a catchable'$interrupt_thrown'exception instead of being trapped behind a blocking syscall. - A port to
rustls. - A modification of
tls_server_negotiateto 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.
| Option | Meaning | Default |
|---|---|---|
--addr HOST:PORT | Bind address and port for the listening socket | 127.0.0.1:1965 |
--hostname NAME | Expected CN of the certificate. Startup aborts on mismatch | localhost |
--content DIR | Root directory for served files | ./site |
--certs DIR | Directory 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/2shells out toopenssl x509and asserts the cert’sCNmatches--hostname. - Content negotiation by extension via
mime/2, populated at startup from/etc/mime.types(parsed by a DCG inmime.pl).text/geminiis added for.gmi. - Text responses sent via
format/3; binary responses streamed in native code throughcopy_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
SIGINTvia the patched Scryersocket_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?