feat: serve gemini behind a tls terminator

This commit is contained in:
randogoth 2026-10-04 22:04:46 +03:00
parent dc2114a4d2
commit c29364d8e0
8 changed files with 395 additions and 39 deletions

View file

@ -66,6 +66,17 @@ bind = "127.0.0.1:0"
formats = ["gemtext"]
default_site = "one"
[listener.gemini]
protocol = "gemini"
bind = "127.0.0.1:0"
formats = ["gemtext"]
default_site = "one"
[listener.gemstrict]
protocol = "gemini"
bind = "127.0.0.1:0"
formats = ["gemtext"]
[listener.nex]
protocol = "nex"
bind = "127.0.0.1:0"
@ -367,6 +378,97 @@ fn spartan_refuses_a_malformed_request_line() {
assert_eq!(server.send("spartan", b"one.test / notanumber\r\n"), "4 Bad request\r\n");
}
// -- Gemini --------------------------------------------------------------
//
// TLS is terminated in front of this listener, so these tests speak plaintext
// to it exactly as a terminator's back end would.
#[test]
fn gemini_serves_gemtext() {
let server = Server::start();
let response = server.send("gemini", b"gemini://one.test/\r\n");
assert!(response.starts_with("20 text/gemini; charset=utf-8\r\n"), "{response}");
assert!(response.contains("# One"), "{response}");
}
#[test]
fn gemini_routes_by_the_url_authority() {
let server = Server::start();
assert!(server.send("gemini", b"gemini://one.test/\r\n").contains("# One"));
assert!(server.send("gemini", b"gemini://two.test/\r\n").contains("# Two"));
}
#[test]
fn gemini_accepts_the_port_in_the_authority() {
// A client that reached a non-default port puts it in the URL it sends.
let server = Server::start();
assert!(server.send("gemini", b"gemini://one.test:1965/\r\n").contains("# One"));
}
#[test]
fn gemini_redirects_the_md_form_to_a_relative_target() {
// Relative rather than absolute: the port this socket is bound to is the
// terminator's back end, not the one the client reached.
let server = Server::start();
assert_eq!(server.send("gemini", b"gemini://one.test/about.md\r\n"), "31 /about\r\n");
}
#[test]
fn gemini_reports_a_missing_page() {
let server = Server::start();
assert_eq!(server.send("gemini", b"gemini://one.test/nope\r\n"), "51 Not found\r\n");
}
#[test]
fn gemini_serves_a_raw_file_with_its_media_type() {
let server = Server::start();
let response = server.send("gemini", b"gemini://one.test/img.png\r\n");
assert!(response.starts_with("20 image/png\r\n"), "{response}");
assert!(response.ends_with("not really a png"), "{response}");
}
#[test]
fn gemini_falls_back_to_the_default_site_for_an_unknown_host() {
let server = Server::start();
assert!(server.send("gemini", b"gemini://elsewhere.test/\r\n").contains("# One"));
}
#[test]
fn gemini_refuses_an_unknown_host_without_a_default_site() {
let server = Server::start();
let response = server.send("gemstrict", b"gemini://elsewhere.test/\r\n");
assert_eq!(response, "53 Proxy request refused\r\n");
}
#[test]
fn gemini_refuses_another_scheme_as_a_proxy_request() {
let server = Server::start();
let response = server.send("gemini", b"https://one.test/\r\n");
assert_eq!(response, "53 Proxy request refused\r\n");
}
#[test]
fn gemini_rejects_a_request_that_is_not_an_absolute_url() {
let server = Server::start();
let response = server.send("gemini", b"/about\r\n");
assert!(response.starts_with("59 Bad request"), "{response}");
}
#[test]
fn gemini_rejects_userinfo_and_fragments() {
let server = Server::start();
assert!(server.send("gemini", b"gemini://u@one.test/\r\n").starts_with("59 "));
assert!(server.send("gemini", b"gemini://one.test/about#top\r\n").starts_with("59 "));
}
#[test]
fn gemini_rejects_a_url_over_the_spec_limit() {
// 1024 bytes of URL plus CRLF is the most the spec allows.
let server = Server::start();
let long = format!("gemini://one.test/{}\r\n", "a".repeat(1100));
assert!(server.send("gemini", long.as_bytes()).starts_with("59 "), "oversized URL");
}
// -- Nex -----------------------------------------------------------------
#[test]
@ -542,12 +644,19 @@ protocol = "http"
bind = "127.0.0.1:0"
formats = ["wml", "html"]
default_site = "one"
[listener.gemini]
protocol = "gemini"
bind = "127.0.0.1:0"
formats = ["gemtext"]
default_site = "one"
"#,
root = root.to_str().unwrap(),
);
let config: ServerConfig = toml::from_str(&text).unwrap();
let registry = Arc::new(itsybitsy::registry::build());
let formats: Vec<String> = ["wml", "html"].iter().map(|s| s.to_string()).collect();
let formats: Vec<String> =
["wml", "html", "gemtext"].iter().map(|s| s.to_string()).collect();
let sites = Arc::new(SiteSet::build(&config, registry.clone(), formats).unwrap());
let bound = serve::bind(&config, registry, sites).unwrap();
@ -599,6 +708,15 @@ default_site = "one"
assert_eq!(split(&response).0, "HTTP/1.1 404 Not Found");
}
#[test]
fn a_gemini_client_is_redirected_to_the_parent_page() {
// Gemini never serves WML, so a card URL always resolves to the whole
// document rather than to a card that does not exist in gemtext.
let server = wml_server();
let response = server.send("gemini", b"gemini://one.test/trail/weather\r\n");
assert_eq!(response, "31 /trail\r\n");
}
#[test]
fn the_index_deck_links_out_to_each_card() {
let server = wml_server();