feat: serve gemini behind a tls terminator
This commit is contained in:
parent
dc2114a4d2
commit
c29364d8e0
8 changed files with 395 additions and 39 deletions
39
README.md
39
README.md
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
[](LICENSE) [](https://ai-declaration.md)
|
||||
|
||||
Serve folders of Markdown to different domains over HTTP, [Spartan](https://portal.mozz.us/gemini/spartan.mozz.us/), [Nex](https://nightfall.city/nex/info/specification.txt) and [Gopher](https://www.rfc-editor.org/rfc/rfc1436), rendered on request with no build step.
|
||||
Serve folders of Markdown to different domains over HTTP, [Gemini](https://geminiprotocol.net/docs/specification.gmi), [Spartan](https://portal.mozz.us/gemini/spartan.mozz.us/), [Nex](https://nightfall.city/nex/info/specification.txt) and [Gopher](https://www.rfc-editor.org/rfc/rfc1436), rendered on request with no build step.
|
||||
|
||||
A Rust re-implementation of [smolweb](https://code.randogoth.com/randogoth/smolweb), which serves one folder from one process with its output formats hardcoded and its configuration in per-file frontmatter. itsybitsy changes all three: one process serves many domains, each output format is a separate crate behind a cargo feature, and configuration lives in a TOML file per content directory.
|
||||
|
||||
## Status
|
||||
|
||||
It serves. HTTP, Spartan, Nex and Gopher are working across five output formats, with virtual hosting wherever the protocol carries a hostname. Gemini is the one protocol left.
|
||||
It serves. All five protocols work across five output formats, with virtual hosting wherever the protocol carries a hostname. Gemini expects TLS to be terminated in front of it, which is the one deployment requirement itsybitsy does not satisfy by itself.
|
||||
|
||||
| Area | State |
|
||||
| --- | --- |
|
||||
|
|
@ -21,7 +21,8 @@ It serves. HTTP, Spartan, Nex and Gopher are working across five output formats,
|
|||
| FIGlet banners and hyphenation, behind features | implemented |
|
||||
| WML decks and card sub-documents, behind a feature | implemented |
|
||||
| Gopher, with dot-stuffing and the RFC 4266 root menu | implemented |
|
||||
| Gemini | planned |
|
||||
| Gemini, behind a TLS terminator | implemented |
|
||||
| Built-in TLS, so no terminator is needed | not planned for now |
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
@ -108,6 +109,8 @@ Configuration keys are not carried over verbatim either: md2txt accepts three sp
|
|||
|
||||
wapdown's own parser produces neither tables nor lists, so its WML decks render a bullet list as literal text and drop tables entirely; both come out as real markup here, lists as marked lines and tables as `<table>`, which WML has.
|
||||
|
||||
Gemini diverges in three places. smolweb answers 59 (bad request) to any URL that is not `gemini://`, which tells a client its request was malformed when an `https://` URL is merely for somewhere this server does not fetch from; that is 53 (proxy request refused) here, and 59 is kept for a URL that genuinely does not parse. smolweb builds absolute redirect targets from the port it is bound to, which is wrong behind a terminator; these are relative. And smolweb terminates TLS itself with a certificate named per site, so `tls` is not a configuration key here at all — naming one is an error rather than a setting that quietly does nothing.
|
||||
|
||||
smolweb inherits five invented directives from its two renderer libraries: `{.card}`, `{.include}`, `![[ ]]`, `#[label](art.txt)` and MultiMarkdown attribute lists (`{: .center}`) — two incompatible brace grammars, with art alignment expressible two different ways in one line. None of it appeared in real content, so itsybitsy expresses the same capabilities with standard constructs instead. Parsing once rather than once per library also means the text formats gain setext headings and the WAP formats gain tables and ASCII art, none of which their original parser handled.
|
||||
|
||||
## Output formats
|
||||
|
|
@ -150,12 +153,40 @@ A banner that will not fit the line, names a font this build lacks, or is asked
|
|||
| HTTP | `Host` header | `GET` and `HEAD`, negotiated by `Accept` |
|
||||
| Spartan | the host field of its request line | One format per listener |
|
||||
| Nex | nothing; its listener names one site | No status line at all |
|
||||
| Gemini | the authority of the URL it is sent | TLS terminated in front; one format per listener |
|
||||
| Gopher | nothing; its listener names one site | Item type 0 and the root menu |
|
||||
|
||||
A protocol with no hostname in its requests cannot be routed by one, so its listener names a single site outright and configuration refuses to leave that implicit once more than one site exists. HTTP and Spartan fall back to `default_site` when a request names a host this server does not know.
|
||||
A protocol with no hostname in its requests cannot be routed by one, so its listener names a single site outright and configuration refuses to leave that implicit once more than one site exists. HTTP, Gemini and Spartan fall back to `default_site` when a request names a host this server does not know.
|
||||
|
||||
Neither Nex nor Gopher has a redirect status, so a canonical target is resolved server-side and the real content comes back on the first request rather than a bounce.
|
||||
|
||||
### Gemini and TLS
|
||||
|
||||
itsybitsy does not speak TLS. The Gemini listener expects plaintext, so a TLS wrapper terminates in front of it and forwards to a loopback port:
|
||||
|
||||
```toml
|
||||
[listener.gemini]
|
||||
protocol = "gemini"
|
||||
# The terminator owns 1965; this is its back end.
|
||||
bind = "127.0.0.1:11965"
|
||||
formats = ["gemtext"]
|
||||
default_site = "smol"
|
||||
```
|
||||
|
||||
```ini
|
||||
; /etc/stunnel/gemini.conf
|
||||
[gemini]
|
||||
accept = 1965
|
||||
connect = 127.0.0.1:11965
|
||||
cert = /var/lib/itsybitsy/gemini.pem
|
||||
```
|
||||
|
||||
`stunnel` and `ghostunnel` are built for exactly this and support SNI, so each virtual host can present its own certificate. Use a long-lived self-signed certificate rather than an ACME one: Gemini clients pin the fingerprint they first saw, and a renewal that changes it is indistinguishable from an interception.
|
||||
|
||||
Two things follow from terminating in front, and neither is a limitation that can be configured away. Every connection appears to come from the terminator, so request logs show its address unless it speaks the PROXY protocol, which itsybitsy does not yet read. And a client certificate never reaches us, so statuses 60 to 62 are not implementable — itsybitsy serves static documents and has nothing to authenticate, so only the logs are poorer for it.
|
||||
|
||||
Redirects are relative references, which the spec permits and which are the only correct form here: the port this listener is bound to is the terminator's back end, not a port any client reached, so an absolute URL built from it would point somewhere unpublished.
|
||||
|
||||
Gopher serves item type 0 (text) and one menu: a prefix-less `gopher://host/` means item type 1 by RFC 4266, so an empty selector gets a one-item menu pointing at `/` rather than the root document, which would be the wrong type. Text items are dot-stuffed and terminated with a lone dot; binary items are sent raw, since dot-stuffing would corrupt them and a terminator would become part of the file. There are no generated directory listings and no type 7 search.
|
||||
|
||||
## Card sub-documents
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue