Sula: A Gemini protocol server written in Scryer Prolog

triska2 pts0 comments

Sula | Javier SagredoSula<br>3 min · 498 words · Javier Sagredo<br>Table of Contents<br>/'su.la/ for the suffix of capsule in Spanish, Cápsula.

Git<br>A Gemini protocol server written in<br>Scryer Prolog.<br>Requirements#<br>sula depends on a patched Scryer Prolog which can be found<br>here (branch js/fixes). The required<br>patches are:<br>A native '$copy_stream'/2 builtin used for streaming binary file bodies to<br>TLS clients without materialising the contents on the Prolog heap.<br>A fix to library(pio)&rsquo;s buffer_prepare_for_n/5 so that lazy reads from<br>process pipes (and other streams whose at_end_of_stream/1 never reports<br>true) terminate on EOF instead of spinning.<br>A non-blocking poll loop in socket_server_accept/4 that checks Scryer&rsquo;s<br>INTERRUPT flag, so SIGINT becomes a catchable '$interrupt_thrown'<br>exception instead of being trapped behind a blocking syscall.<br>A port to rustls.<br>A modification of tls_server_negotiate to include the optional client<br>certificate.<br>Build and install the patched Scryer:<br>git clone https://git.sagredo.dev/scryer-prolog -b js/fixes<br>cd scryer-prolog<br>cargo install --path .<br>openssl(1) must also be on PATH — it&rsquo;s invoked at startup to read the<br>CN from the configured identity certificate and verify it matches the<br>configured hostname.<br>Running#<br>./sula.pl --addr HOST:PORT --hostname NAME --content DIR --certs DIR<br>sula.pl is a polyglot script: bash detects scryer-prolog on PATH and<br>execs it with sula:run, halt as the entry goal.<br>Example:<br>./sula.pl \<br>--addr 127.0.0.1:1965 \<br>--hostname gmi.example.dev \<br>--content ./site \<br>--certs .<br>CLI options#<br>All options accept any order. Anything unrecognised is silently dropped.<br>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#<br>Ctrl+C triggers a clean shutdown: the listening socket is closed, the<br>top-level catch logs Shutting down, and the process exits 0.<br>Features#<br>TLS via rustls, PKCS#12 identity files.<br>Hostname verification: at startup, cert_is_for_hostname/2 shells out to<br>openssl x509 and asserts the cert&rsquo;s CN matches --hostname.<br>Content negotiation by extension via mime/2, populated at startup from<br>/etc/mime.types (parsed by a DCG in mime.pl). text/gemini is added<br>for .gmi.<br>Text responses sent via format/3; binary responses streamed in native<br>code through copy_stream/2 (file → TLS socket, no Prolog heap traffic).<br>Per-connection error handling: TLS handshake failures and mid-stream<br>client disconnects are logged and the loop continues. Other errors<br>re-throw and surface at the top level.<br>Graceful shutdown on SIGINT via the patched Scryer<br>socket_server_accept/4.<br>Layout#<br>sula.pl Polyglot launcher + main sula module (run/0, request loop).<br>config.pl CLI parsing (DCG) and config accessors (cert/1, addr/1, ...).<br>cert.pl Certificate loading + hostname-vs-CN check.<br>mime.pl /etc/mime.types parser (DCG) and mime/2 facts.<br>request.pl Request line reader.<br>gemini_uri.pl Gemini URI DCG (gemini://host[:port]/path[?query]).<br>ip.pl IP address recognition (rejected as Gemini hosts).<br>response.pl Response status code DCG.<br>log.pl Tagged log_msg/3.<br>banner.pl Reads banner.txt and emits it line-by-line via display_banner/1.<br>Planned features#<br>Use key and cert instead of identity.p12<br>Client certificates<br>Load configuration from a configuration file<br>Save and load users<br>Run CGI scripts<br>All status codes<br>Rate limiting<br>Virtual hosting<br>File logging<br>Multi-threading or kind of?<br>Hot reload?

sula scryer prolog hostname gemini mime

Related Articles