crates/estate-front/src/web/tests.rs
//! **The whole certificate road, driven end to end on loopback** — a small
//! ACME server (RFC 8555, the parts `instant-acme` speaks) written for these
//! tests, because no pure-Rust ACME test server exists to reuse (pebble is Go;
//! the one Rust crate, `coyote`, links openssl and postgres). It is honest
//! where it matters: it VALIDATES the http-01 challenge by fetching it from
//! the front's own :80 listener and checking the key authorization against
//! the account key's JWK thumbprint (RFC 7638), and it signs the CSR the front
//! sends with a CA of its own.
#![cfg_attr(not(feature = "acme"), allow(dead_code, unused_imports))] // A32-11: the fake ACME directory serves only the `acme` tests.
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
use axum::extract::{Path as UrlPath, State};
use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::Router;
use base64::Engine as _;
use super::*;
const B64: base64::engine::GeneralPurpose = base64::engine::general_purpose::URL_SAFE_NO_PAD;
struct Fake {
base: String,
/// Where the front's :80 is — the validator dials it with the name as Host.
front_http: SocketAddr,
ca_cert: rcgen::Certificate,
ca_key: rcgen::KeyPair,
thumbprint: Mutex<Option<String>>,
names: Mutex<Vec<String>>,
valid: Mutex<Vec<bool>>,
cert: Mutex<Option<String>>,
/// How many certificates were issued — the rate-limit tests count it.
issued: Mutex<u32>,
/// How many times the directory was read, and orders placed: zero while DNS is wrong.
asked: Mutex<u32>,
orders: Mutex<u32>,
/// Per authorization: whether its challenge was tried (a tried, failed one is `invalid`,
/// as Let's Encrypt answers a failed validation).
tried: Mutex<Vec<bool>>,
/// Refuse every new order (a rate limit, say): an ACME failure that is not a validation.
refuse_orders: Mutex<bool>,
}
type F = Arc<Fake>;
fn nonce() -> [(header::HeaderName, String); 1] {
[(header::HeaderName::from_static("replay-nonce"), format!("n{}", rand_core::RngCore::next_u64(&mut rand_core::OsRng)))]
}
/// `(protected header, payload)` of a flattened JWS.
fn jws(body: &str) -> (serde_json::Value, serde_json::Value) {
let v: serde_json::Value = serde_json::from_str(body).unwrap();
let dec = |k: &str| -> serde_json::Value {
let s = v[k].as_str().unwrap_or("");
if s.is_empty() {
serde_json::Value::Null
} else {
serde_json::from_slice(&B64.decode(s).unwrap()).unwrap()
}
};
(dec("protected"), dec("payload"))
}
fn json(status: StatusCode, location: Option<String>, body: serde_json::Value) -> Response {
let mut r = (status, nonce(), [(header::CONTENT_TYPE, "application/json".to_string())], body.to_string()).into_response();
if let Some(l) = location {
r.headers_mut().insert(header::LOCATION, l.parse().unwrap());
}
r
}
async fn directory(State(f): State<F>) -> Response {
*f.asked.lock().unwrap() += 1;
let b = &f.base;
json(StatusCode::OK, None, serde_json::json!({"newNonce": format!("{b}/nonce"), "newAccount": format!("{b}/acct"), "newOrder": format!("{b}/order")}))
}
async fn new_nonce() -> Response {
(StatusCode::OK, nonce()).into_response()
}
async fn new_account(State(f): State<F>, body: String) -> Response {
let (protected, _) = jws(&body);
let jwk = &protected["jwk"];
// RFC 7638: the required members, lexicographic, no whitespace.
let canon = format!(r#"{{"crv":"{}","kty":"{}","x":"{}","y":"{}"}}"#, jwk["crv"].as_str().unwrap(), jwk["kty"].as_str().unwrap(), jwk["x"].as_str().unwrap(), jwk["y"].as_str().unwrap());
*f.thumbprint.lock().unwrap() = Some(B64.encode(<sha2::Sha256 as sha2::Digest>::digest(canon.as_bytes())));
json(StatusCode::CREATED, Some(format!("{}/acct/1", f.base)), serde_json::json!({"status": "valid"}))
}
fn order_body(f: &Fake) -> serde_json::Value {
let names = f.names.lock().unwrap().clone();
let valid = f.valid.lock().unwrap().clone();
let cert = f.cert.lock().unwrap().is_some();
let failed = f.tried.lock().unwrap().iter().zip(valid.iter()).any(|(t, v)| *t && !*v);
let status = if cert {
"valid"
} else if failed {
"invalid"
} else if !valid.is_empty() && valid.iter().all(|v| *v) {
"ready"
} else {
"pending"
};
let mut o = serde_json::json!({
"status": status,
"identifiers": names.iter().map(|n| serde_json::json!({"type": "dns", "value": n})).collect::<Vec<_>>(),
"authorizations": (0..names.len()).map(|i| format!("{}/authz/{i}", f.base)).collect::<Vec<_>>(),
"finalize": format!("{}/finalize", f.base),
});
if cert {
o["certificate"] = serde_json::Value::String(format!("{}/cert", f.base));
}
o
}
async fn new_order(State(f): State<F>, body: String) -> Response {
let (_, payload) = jws(&body);
if *f.refuse_orders.lock().unwrap() {
*f.orders.lock().unwrap() += 1;
return json(StatusCode::TOO_MANY_REQUESTS, None, serde_json::json!({"type": "urn:ietf:params:acme:error:rateLimited", "detail": "too many orders (fake)", "status": 429}));
}
let names: Vec<String> = payload["identifiers"].as_array().unwrap().iter().map(|i| i["value"].as_str().unwrap().to_string()).collect();
*f.valid.lock().unwrap() = vec![false; names.len()];
*f.tried.lock().unwrap() = vec![false; names.len()];
*f.orders.lock().unwrap() += 1;
*f.names.lock().unwrap() = names;
*f.cert.lock().unwrap() = None;
json(StatusCode::CREATED, Some(format!("{}/order/1", f.base)), order_body(&f))
}
async fn order(State(f): State<F>) -> Response {
json(StatusCode::OK, None, order_body(&f))
}
fn authz_body(f: &Fake, i: usize) -> serde_json::Value {
let name = f.names.lock().unwrap()[i].clone();
let valid = f.valid.lock().unwrap()[i];
let tried = f.tried.lock().unwrap()[i];
let s = if valid { "valid" } else if tried { "invalid" } else { "pending" };
serde_json::json!({"status": s, "identifier": {"type": "dns", "value": name},
"challenges": [{"type": "http-01", "url": format!("{}/chall/{i}", f.base), "token": format!("tok{i}"), "status": s}]})
}
async fn authz(State(f): State<F>, UrlPath(i): UrlPath<usize>) -> Response {
json(StatusCode::OK, None, authz_body(&f, i))
}
/// **The validation**: GET the token from the front's :80 with the name as
/// Host, and compare with `token.thumbprint`.
async fn challenge(State(f): State<F>, UrlPath(i): UrlPath<usize>) -> Response {
let name = f.names.lock().unwrap()[i].clone();
let token = format!("tok{i}");
let expected = format!("{token}.{}", f.thumbprint.lock().unwrap().clone().unwrap());
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
// The name may point elsewhere (a dead address): that is a failed validation.
let mut got = String::new();
if let Ok(mut tcp) = tokio::net::TcpStream::connect(f.front_http).await {
tcp.write_all(format!("GET /.well-known/acme-challenge/{token} HTTP/1.1\r\nHost: {name}\r\nConnection: close\r\n\r\n").as_bytes()).await.unwrap();
let _ = tcp.read_to_string(&mut got).await;
}
let ok = got.starts_with("HTTP/1.1 200") && got.ends_with(&expected);
f.valid.lock().unwrap()[i] = ok;
f.tried.lock().unwrap()[i] = true;
json(StatusCode::OK, None, serde_json::json!({"type": "http-01", "url": format!("{}/chall/{i}", f.base), "token": token, "status": if ok { "valid" } else { "invalid" }}))
}
async fn finalize(State(f): State<F>, body: String) -> Response {
let (_, payload) = jws(&body);
let der = B64.decode(payload["csr"].as_str().unwrap()).unwrap();
let csr = rcgen::CertificateSigningRequestParams::from_der(&der.into()).unwrap();
let leaf = csr.signed_by(&f.ca_cert, &f.ca_key).unwrap();
*f.cert.lock().unwrap() = Some(format!("{}{}", leaf.pem(), f.ca_cert.pem()));
*f.issued.lock().unwrap() += 1;
json(StatusCode::OK, Some(format!("{}/order/1", f.base)), order_body(&f))
}
async fn certificate(State(f): State<F>) -> Response {
(StatusCode::OK, nonce(), [(header::CONTENT_TYPE, "application/pem-certificate-chain".to_string())], f.cert.lock().unwrap().clone().unwrap()).into_response()
}
/// Start the fake ACME server; answers its directory URL and the server.
async fn fake_acme(front_http: SocketAddr) -> (String, F) {
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://{}", l.local_addr().unwrap());
let ca_key = rcgen::KeyPair::generate().unwrap();
let mut p = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
p.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
p.distinguished_name.push(rcgen::DnType::CommonName, "fake acme CA");
let ca_cert = p.self_signed(&ca_key).unwrap();
let f = Arc::new(Fake { base: base.clone(), front_http, ca_cert, ca_key, thumbprint: Mutex::new(None), names: Mutex::new(vec![]), valid: Mutex::new(vec![]), cert: Mutex::new(None), issued: Mutex::new(0), asked: Mutex::new(0), orders: Mutex::new(0), tried: Mutex::new(vec![]), refuse_orders: Mutex::new(false) });
let app = Router::new()
.route("/dir", get(directory))
.route("/nonce", get(new_nonce).head(new_nonce))
.route("/acct", post(new_account))
.route("/order", post(new_order))
.route("/order/1", post(order))
.route("/authz/{i}", post(authz))
.route("/chall/{i}", post(challenge))
.route("/finalize", post(finalize))
.route("/cert", post(certificate))
.with_state(Arc::clone(&f));
tokio::spawn(async move { axum::serve(l, app).await.unwrap() });
(format!("{base}/dir"), f)
}
/// GET `path` over TLS from `addr` presenting SNI `name`, trusting `ca_pem`.
async fn https_get(addr: SocketAddr, name: &str, ca_pem: &str, path: &str) -> Result<String, String> {
use rustls::pki_types::pem::PemObject as _;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
let mut roots = rustls::RootCertStore::empty();
for c in rustls::pki_types::CertificateDer::pem_slice_iter(ca_pem.as_bytes()) {
roots.add(c.unwrap()).unwrap();
}
let cfg = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())).with_safe_default_protocol_versions().unwrap().with_root_certificates(roots).with_no_client_auth();
let tcp = tokio::net::TcpStream::connect(addr).await.map_err(|e| e.to_string())?;
let sni = rustls::pki_types::ServerName::try_from(name.to_string()).unwrap();
let mut tls = tokio_rustls::TlsConnector::from(Arc::new(cfg)).connect(sni, tcp).await.map_err(|e| e.to_string())?;
tls.write_all(format!("GET {path} HTTP/1.1\r\nHost: {name}\r\nConnection: close\r\n\r\n").as_bytes()).await.map_err(|e| e.to_string())?;
let mut out = String::new();
let _ = tls.read_to_string(&mut out).await;
Ok(out)
}
async fn listeners() -> (tokio::net::TcpListener, tokio::net::TcpListener, SocketAddr, SocketAddr) {
let a = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let b = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let (x, y) = (a.local_addr().unwrap(), b.local_addr().unwrap());
(a, b, x, y)
}
async fn until<T>(what: &str, mut f: impl FnMut() -> Option<T>) -> T {
for _ in 0..200 {
if let Some(t) = f() {
return t;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
panic!("timed out waiting for {what}");
}
/// **Nothing fed, nothing kept**: the front asks ACME, answers the http-01
/// challenge on its own :80, keeps the certificate on its data disk and
/// serves it on :443 — no restart anywhere. A second front on the same data
/// disk (a reboot) serves the kept one and does NOT ask again.
#[cfg(feature = "acme")]
#[tokio::test(flavor = "multi_thread")]
async fn with_nothing_fed_the_front_gets_its_certificate_by_acme_and_serves_it_live() {
let dir = tempfile::tempdir().unwrap();
let (l80, l443, a80, a443) = listeners().await;
let (directory, acme) = fake_acme(a80).await;
let feed: BTreeMap<String, String> = BTreeMap::from([(FEED_ACME_DIRECTORY.to_string(), directory.clone()), (FEED_WEB_HOSTS.to_string(), "example.test, www.example.test".to_string())]);
let f2 = feed.clone();
let web = start_on_probed(dir.path(), "ignored.estate".into(), Arc::new(move || f2.clone()), l80, l443, here());
until("the ACME certificate", || web.resolver.any().then_some(())).await;
let ca = acme.ca_cert.pem();
let page = https_get(a443, "www.example.test", &ca, "/health").await.unwrap();
assert!(page.starts_with("HTTP/1.1 200") && page.ends_with("ok\n"), "{page}");
// Both names are in it: the bare site's handshake is valid, and its answer the 301 to www (WWW-CANON).
let bare = https_get(a443, "example.test", &ca, "/health").await.unwrap();
assert_eq!(status_and_location(&bare), (301, Some("https://www.example.test/health".into())), "{bare}");
let kept = certs::kept(dir.path(), "example.test", SystemTime::now()).unwrap().unwrap();
assert_eq!(kept.names, vec!["example.test".to_string(), "www.example.test".to_string()]);
assert_eq!(*acme.issued.lock().unwrap(), 1);
// The site is swapped live: the next connection gets the new router.
web.app.set(Router::new().route("/health", get(|| async { "swapped\n" })));
assert!(https_get(a443, "www.example.test", &ca, "/health").await.unwrap().ends_with("swapped\n"));
// :80 redirects everything that is not a challenge; the bare site straight to www (WWW-CANON).
let mut tcp = tokio::net::TcpStream::connect(a80).await.unwrap();
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
tcp.write_all(b"GET /some/page?x=1 HTTP/1.1\r\nHost: example.test\r\nConnection: close\r\n\r\n").await.unwrap();
let mut got = String::new();
tcp.read_to_string(&mut got).await.unwrap();
assert!(got.starts_with("HTTP/1.1 301") && got.to_ascii_lowercase().contains("location: https://www.example.test/some/page?x=1"), "{got}");
// A reboot on the same disk: served from the disk, ACME not asked.
let again = Web::new(dir.path());
let cfg = WebConfig::from("x", &feed);
let (due, _) = again.look(&cfg, &feed, SystemTime::now());
assert!(!due && again.resolver.any(), "the kept certificate serves; no second order");
}
/// **A rebuild carries the certificate over**: fed with more than thirty days
/// left, it is served and kept, and ACME is never asked — Let's Encrypt's
/// weekly five are not spent on a rebuild. Near expiry, ACME is asked and its
/// certificate replaces the fed one live.
#[cfg(feature = "acme")]
#[tokio::test(flavor = "multi_thread")]
async fn a_fed_certificate_is_served_first_and_acme_is_asked_only_near_its_expiry() {
let dir = tempfile::tempdir().unwrap();
let (l80, l443, a80, a443) = listeners().await;
let (directory, acme) = fake_acme(a80).await;
let (chain, key) = certs::tests::minted(&["example.test"], 80);
let feed = Arc::new(Mutex::new(BTreeMap::from([
(FEED_ACME_DIRECTORY.to_string(), directory.clone()),
(FEED_WEB_HOSTS.to_string(), "example.test".to_string()),
(certs::fed_cert_key("example.test"), chain.clone()),
(certs::fed_key_key("example.test"), key.clone()),
])));
let f2 = Arc::clone(&feed);
let web = start_on_probed(dir.path(), "x".into(), Arc::new(move || f2.lock().unwrap().clone()), l80, l443, here());
until("the fed certificate", || web.resolver.any().then_some(())).await;
// The self-signed fed certificate is its own root.
assert!(https_get(a443, "example.test", &chain, "/health").await.unwrap().starts_with("HTTP/1.1 200"));
assert_eq!(certs::kept(dir.path(), "example.test", SystemTime::now()).unwrap().unwrap().chain_pem, chain, "kept on the disk");
tokio::time::sleep(Duration::from_millis(500)).await;
assert_eq!(*acme.issued.lock().unwrap(), 0, "a fed certificate with 80 days left: ACME is not asked");
// Near expiry (10 days left) — asked, and served live.
let (c10, k10) = certs::tests::minted(&["example.test"], 10);
let web2 = Web::new(tempfile::tempdir().unwrap().path());
let near = BTreeMap::from([
(FEED_ACME_DIRECTORY.to_string(), directory),
(FEED_WEB_HOSTS.to_string(), "example.test".to_string()),
(certs::fed_cert_key("example.test"), c10),
(certs::fed_key_key("example.test"), k10),
]);
let cfg = WebConfig::from("x", &near);
let (due, _) = web2.look(&cfg, &near, SystemTime::now());
assert!(due, "ten days left: renew");
assert!(web2.resolver.any(), "the fed one serves meanwhile");
}
#[test]
fn the_web_config_defaults_to_the_estate_and_lets_encrypt() {
let c = WebConfig::from("Example.test", &BTreeMap::new());
assert_eq!(c, WebConfig { hosts: vec!["example.test".into()], directory: acme::LETS_ENCRYPT.into(), contact: None, callback_only: vec![], control_host: None });
let c = WebConfig::from("example.test", &BTreeMap::from([(FEED_WEB_HOSTS.to_string(), " example.test ,www.example.test".to_string()), (FEED_ACME_CONTACT.to_string(), "ops@example.test".to_string())]));
assert_eq!(c.hosts, vec!["example.test", "www.example.test"]);
assert_eq!(c.name(), "example.test");
assert_eq!(c.contact.as_deref(), Some("ops@example.test"));
}
/// A malformed fed certificate is refused by name and the kept one serves.
#[test]
fn a_bad_fed_certificate_is_refused_by_name_and_the_kept_one_serves() {
let dir = tempfile::tempdir().unwrap();
let (c, k) = certs::tests::minted(&["example.test"], 80);
certs::keep(dir.path(), "example.test", &certs::Cert::parse("example.test", &c, &k, SystemTime::now()).unwrap()).unwrap();
let (other_c, _) = certs::tests::minted(&["example.test"], 80);
let feed = BTreeMap::from([(certs::fed_cert_key("example.test"), other_c), (certs::fed_key_key("example.test"), k)]);
let web = Web::new(dir.path());
let cfg = WebConfig::from("example.test", &feed);
let (due, lines) = web.look(&cfg, &feed, SystemTime::now());
assert!(!due && web.resolver.any());
assert!(lines.iter().any(|l| l.contains("[tls-key-mismatch]")), "{lines:?}");
}
/// **No ACME before the first feed; a new feed is looked at at once.**
/// MEASURED 2026-09-26 (loki, mock bench run 8): the one look a minute ran at
/// boot, asked Let's Encrypt production for the bare estate name before the
/// installer had fed anything, and the certificate fed two seconds later was
/// not kept before the install closed — the hand-over was empty.
#[test]
fn acme_waits_for_the_first_feed_and_a_changed_feed_is_looked_at_now() {
let empty = BTreeMap::new();
let fed = BTreeMap::from([(FEED_WEB_HOSTS.to_string(), "example.test".to_string())]);
assert!(!acme_may_ask(true, &empty, true), "nothing fed: ACME is not asked");
assert!(acme_may_ask(true, &fed, true));
assert!(!acme_may_ask(false, &fed, true), "not due: not asked");
assert!(!acme_may_ask(true, &fed, false), "the switch off: not asked (AIRGAP B1)");
assert!(look_now(None, &empty), "the first look is at once");
assert!(!look_now(Some((&empty, Duration::from_secs(3))), &empty), "unchanged and recent: no look");
assert!(look_now(Some((&empty, Duration::from_secs(3))), &fed), "the feed changed: look now");
assert!(look_now(Some((&fed, LOOK_EVERY)), &fed), "a minute passed: look again");
}
/// End to end: a front that boots unfed asks nobody, and the certificate the
/// installer feeds a moment later is served and KEPT within seconds — what the
/// hand-over at the close reads.
#[tokio::test(flavor = "multi_thread")]
async fn a_certificate_fed_after_boot_is_kept_within_seconds_and_acme_was_never_asked() {
let dir = tempfile::tempdir().unwrap();
let (l80, l443, a80, _a443) = listeners().await;
let (directory, acme) = fake_acme(a80).await;
let feed = Arc::new(Mutex::new(BTreeMap::new()));
let f2 = Arc::clone(&feed);
let web = start_on_probed(dir.path(), "example.test".into(), Arc::new(move || f2.lock().unwrap().clone()), l80, l443, here());
tokio::time::sleep(Duration::from_millis(500)).await;
assert!(!web.resolver.any());
let (chain, key) = certs::tests::minted(&["example.test"], 80);
*feed.lock().unwrap() = BTreeMap::from([
(FEED_ACME_DIRECTORY.to_string(), directory),
(FEED_WEB_HOSTS.to_string(), "example.test".to_string()),
(certs::fed_cert_key("example.test"), chain.clone()),
(certs::fed_key_key("example.test"), key),
]);
let t0 = std::time::Instant::now();
until("the fed certificate", || web.resolver.any().then_some(())).await;
assert!(t0.elapsed() < LOOK_EVERY, "looked at when the feed changed, not a minute later");
assert_eq!(certs::kept(dir.path(), "example.test", SystemTime::now()).unwrap().unwrap().chain_pem, chain, "kept for the hand-over");
assert_eq!(*acme.issued.lock().unwrap(), 0, "ACME was never asked");
}
/// Lane E 3: `POST /callback/<vendor>` reaches monetize on loopback with the
/// body byte for byte and `Stripe-Signature` intact, monetize's status comes
/// back, and every other path is still the web face.
#[tokio::test(flavor = "multi_thread")]
async fn a_vendor_webhook_reaches_monetize_with_its_signature_and_its_answer_comes_back() {
use tower::ServiceExt as _;
// A stand-in for monetize's h1 callback: 401 unless the signature is the one sent.
async fn monetize(UrlPath(vendor): UrlPath<String>, headers: axum::http::HeaderMap, body: axum::body::Bytes) -> Response {
let sig = headers.get("stripe-signature").and_then(|v| v.to_str().ok()).unwrap_or("");
let status = if sig == "t=1,v1=abc" { StatusCode::OK } else { StatusCode::UNAUTHORIZED };
(status, format!("{vendor}|{}|{}", sig, String::from_utf8_lossy(&body))).into_response()
}
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let to = l.local_addr().unwrap();
tokio::spawn(async move { axum::serve(l, Router::new().route("/callback/{vendor}", post(monetize))).await.unwrap() });
let face = Router::new().fallback(|| async { "the console" });
let app = serve::with_callbacks(face, to);
let body = r#"{"id":"evt_1","type":"checkout.session.completed"}"#;
let req = |sig: &str, path: &str| {
axum::http::Request::post(path).header("stripe-signature", sig).header(header::CONTENT_TYPE, "application/json").body(axum::body::Body::from(body)).unwrap()
};
let ok = app.clone().oneshot(req("t=1,v1=abc", "/callback/stripe")).await.unwrap();
assert_eq!(ok.status(), StatusCode::OK);
let text = String::from_utf8(axum::body::to_bytes(ok.into_body(), 1 << 20).await.unwrap().to_vec()).unwrap();
assert_eq!(text, format!("stripe|t=1,v1=abc|{body}"), "the body and the signature arrive untouched");
let forged = app.clone().oneshot(req("t=1,v1=forged", "/callback/stripe")).await.unwrap();
assert_eq!(forged.status(), StatusCode::UNAUTHORIZED, "monetize's refusal is handed back");
let bad = app.clone().oneshot(req("x", "/callback/..%2Fadmin")).await.unwrap();
assert_eq!(bad.status(), StatusCode::NOT_FOUND);
let other = app.clone().oneshot(axum::http::Request::get("/").body(axum::body::Body::empty()).unwrap()).await.unwrap();
assert_eq!(String::from_utf8(axum::body::to_bytes(other.into_body(), 1 << 20).await.unwrap().to_vec()).unwrap(), "the console");
}
/// OM-7: a Stripe webhook is per product instance (`[vendors.stripe.<instance>]`,
/// `POST /callback/stripe/<instance>`): the front carries BOTH segments to
/// monetize — vendor and instance — with the signature intact, the
/// one-segment `/callback/<vendor>` still passes, and a third segment, an
/// empty one or a smuggled `..` is refused at the front, never forwarded.
#[tokio::test(flavor = "multi_thread")]
async fn a_per_instance_stripe_webhook_reaches_monetize_with_its_vendor_and_instance() {
use tower::ServiceExt as _;
// A stand-in for monetize's door: `/callback/{*vendor}`, the vendor id is `stripe/<instance>`.
async fn monetize(UrlPath(vendor): UrlPath<String>, headers: axum::http::HeaderMap, body: axum::body::Bytes) -> Response {
let sig = headers.get("stripe-signature").and_then(|v| v.to_str().ok()).unwrap_or("");
(StatusCode::OK, format!("{vendor}|{}|{}", sig, String::from_utf8_lossy(&body))).into_response()
}
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let to = l.local_addr().unwrap();
tokio::spawn(async move { axum::serve(l, Router::new().route("/callback/{*vendor}", post(monetize))).await.unwrap() });
let face = Router::new().fallback(|| async { (StatusCode::NOT_FOUND, "the console") });
let app = serve::with_callbacks(face, to);
let body = r#"{"id":"evt_1","type":"checkout.session.completed"}"#;
let send = |path: &str| {
let req = axum::http::Request::post(path).header("stripe-signature", "t=1,v1=abc").header(header::CONTENT_TYPE, "application/json").body(axum::body::Body::from(body)).unwrap();
app.clone().oneshot(req)
};
let text = |r: Response| async move { String::from_utf8(axum::body::to_bytes(r.into_body(), 1 << 20).await.unwrap().to_vec()).unwrap() };
for instance in ["gunnar", "holger-test2"] {
let hook = send(&format!("/callback/stripe/{instance}")).await.unwrap();
assert_eq!(hook.status(), StatusCode::OK, "/callback/stripe/{instance} reaches monetize, not a 404");
assert_eq!(text(hook).await, format!("stripe/{instance}|t=1,v1=abc|{body}"), "the instance arrives with the vendor, the body and signature untouched");
}
let solo = send("/callback/stripe").await.unwrap();
assert_eq!(solo.status(), StatusCode::OK, "the one-segment path still passes");
assert_eq!(text(solo).await, format!("stripe|t=1,v1=abc|{body}"));
for bad in ["/callback/stripe/gunnar/extra", "/callback/stripe/..%2Fadmin", "/callback/stripe/gun%2Fnar", "/callback/stripe/", "/callback//gunnar"] {
let r = send(bad).await.unwrap();
assert_eq!(r.status(), StatusCode::NOT_FOUND, "{bad} is refused");
assert_ne!(text(r).await.split('|').count(), 3, "{bad} never reached monetize");
}
}
/// **A callback-only host** (`web_callback_only`, the Stripe account's webhook
/// name): `/callback/<vendor>` passes, every other path is a 308 to the same
/// path on the first host; the other hosts are untouched; the first host and
/// a name that is not served are never narrowed.
#[tokio::test(flavor = "multi_thread")]
async fn a_callback_only_host_answers_callbacks_and_sends_the_rest_to_the_apex() {
use tower::ServiceExt as _;
let feed = BTreeMap::from([
(FEED_WEB_HOSTS.to_string(), "example.test,www.example.test,pay.example.dk".to_string()),
(FEED_WEB_CALLBACK_ONLY.to_string(), " Pay.Example.dk ,example.test,nothere.example".to_string()),
]);
let cfg = WebConfig::from("example.test", &feed);
assert_eq!(cfg.callback_only, vec!["pay.example.dk"], "served names only, never the first");
let app = serve::App::new(Router::new().route("/callback/{vendor}", post(|| async { "callback" })).route("/callback/{vendor}/{instance}", post(|| async { "callback" })).fallback(|| async { "the console" }));
app.set_callback_only(&cfg.callback_only, cfg.name());
let router = serve::narrowed(app.current(), app.callback_only());
let send = |host: &str, method: &str, path: &str| {
let req = axum::http::Request::builder().method(method).uri(path).header(header::HOST, host).body(axum::body::Body::empty()).unwrap();
router.clone().oneshot(req)
};
let hook = send("pay.example.dk:443", "POST", "/callback/stripe").await.unwrap();
assert_eq!(hook.status(), StatusCode::OK);
assert_eq!(axum::body::to_bytes(hook.into_body(), 1 << 20).await.unwrap(), "callback");
let per_instance = send("pay.example.dk", "POST", "/callback/stripe/gunnar").await.unwrap();
assert_eq!(axum::body::to_bytes(per_instance.into_body(), 1 << 20).await.unwrap(), "callback", "a per-instance webhook passes too (OM-7)");
let page = send("pay.example.dk", "GET", "/login?next=%2F").await.unwrap();
assert_eq!(page.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(page.headers()[header::LOCATION], "https://example.test/login?next=%2F");
let apex = send("example.test", "GET", "/login").await.unwrap();
assert_eq!(axum::body::to_bytes(apex.into_body(), 1 << 20).await.unwrap(), "the console", "the apex is untouched");
let www = send("www.example.test", "GET", "/").await.unwrap();
assert_eq!(www.status(), StatusCode::OK);
// Nothing fed: nothing narrowed.
app.set_callback_only(&[], "example.test");
assert!(app.callback_only().is_none());
let open = serve::narrowed(app.current(), app.callback_only());
let req = axum::http::Request::get("/").header(header::HOST, "pay.example.dk").body(axum::body::Body::empty()).unwrap();
assert_eq!(open.oneshot(req).await.unwrap().status(), StatusCode::OK);
}
/// **The repository route, through the real :443 router**: a request to the
/// routed name reaches the primary's HTTP (HTTP/1.1, the name it asked for as
/// `Host`, `x-forwarded-proto: https`), a gRPC call to the same name its gRPC
/// (HTTP/2 cleartext), and any other name is the face, untouched. (The test of
/// gunnar's `front-product-key` lane `repo_route.rs`, moved with the route.)
#[tokio::test(flavor = "multi_thread")]
async fn a_routed_name_reaches_the_primarys_http_and_a_grpc_call_its_grpc() {
use tower::ServiceExt as _;
async fn upstream(tag: &'static str) -> std::net::SocketAddrV4 {
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let SocketAddr::V4(at) = l.local_addr().unwrap() else { unreachable!() };
let app = Router::new().fallback(move |req: axum::extract::Request| async move {
let host = req.headers().get(header::HOST).and_then(|h| h.to_str().ok()).unwrap_or("-").to_string();
let proto = req.headers().get("x-forwarded-proto").and_then(|h| h.to_str().ok()).unwrap_or("-").to_string();
format!("{tag} {:?} host={host} proto={proto} path={}", req.version(), req.uri().path())
});
tokio::spawn(async move { axum::serve(l, app).await.unwrap() });
at
}
let (h, g) = (upstream("http").await, upstream("grpc").await);
let app = serve::App::new(Router::new().fallback(|| async { "console" }));
assert!(app.set_routed(Some(serve::Routed { hosts: vec!["repo.example.test".into()], http: h, grpc: g, grpc_services: vec!["x.v1.Repo".into()] })));
assert!(!app.set_routed(app.routed()), "the same route again is no change");
let r = app.router_now();
let ask = |host: &str, grpc: bool| {
let mut b = axum::http::Request::post("/api/v1/crates/new").header("host", host);
if grpc {
b = b.header("content-type", "application/grpc").header("te", "trailers");
}
b.body(axum::body::Body::from("x")).unwrap()
};
let text = |r: Response| async move { String::from_utf8(axum::body::to_bytes(r.into_body(), 1 << 16).await.unwrap().to_vec()).unwrap() };
let a = text(r.clone().oneshot(ask("repo.example.test", false)).await.unwrap()).await;
assert_eq!(a, "http HTTP/1.1 host=repo.example.test proto=https path=/api/v1/crates/new");
let mut call = ask("repo.example.test", true);
*call.uri_mut() = "/x.v1.Repo/Get".parse().unwrap();
let b = text(r.clone().oneshot(call).await.unwrap()).await;
assert!(b.starts_with("grpc HTTP/2.0 ") && b.ends_with("path=/x.v1.Repo/Get"), "{b}");
assert_eq!(text(r.oneshot(ask("example.test", false)).await.unwrap()).await, "console");
// Nothing routed: the face alone.
app.set_routed(None);
assert_eq!(text(app.router_now().oneshot(ask("repo.example.test", false)).await.unwrap()).await, "console");
}
/// ★ **The front names the public face to the primary, and only the front does** (HOL-DOCFIND,
/// Holger 3.1.1): every routed request reaches the primary with `x-forwarded-proto: https` and
/// `x-forwarded-host` the name the client asked for, written by the front. What the client
/// sent in `x-forwarded-proto`, `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-prefix`,
/// `x-forwarded-for` and `forwarded` is never passed through: the primary trusts these
/// headers from the front (Holger's `ron_trusted_proxies`), so a passed-through value would be
/// a client writing the links the primary hands everybody.
#[tokio::test(flavor = "multi_thread")]
async fn the_front_overwrites_the_forwarded_headers_and_never_passes_the_clients() {
use tower::ServiceExt as _;
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let SocketAddr::V4(up) = l.local_addr().unwrap() else { unreachable!() };
let echo = Router::new().fallback(|req: axum::extract::Request| async move {
let h = |k: &str| {
req.headers()
.get_all(k)
.iter()
.map(|v| v.to_str().unwrap_or("?").to_string())
.collect::<Vec<_>>()
.join(",")
};
format!(
"proto={} host={} xhost={} port={} prefix={} for={} forwarded={}",
h("x-forwarded-proto"),
h("host"),
h("x-forwarded-host"),
h("x-forwarded-port"),
h("x-forwarded-prefix"),
h("x-forwarded-for"),
h("forwarded")
)
});
tokio::spawn(async move { axum::serve(l, echo).await.unwrap() });
let app = serve::App::new(Router::new().fallback(|| async { "console" }));
app.set_routed(Some(serve::Routed { hosts: vec!["repo.example.test".into()], http: up, grpc: up, grpc_services: Vec::new() }));
let req = axum::http::Request::get("/production/npmjs/is-number")
.header("host", "repo.example.test")
.header("x-forwarded-proto", "http")
.header("x-forwarded-host", "evil.example")
.header("x-forwarded-port", "80")
.header("x-forwarded-prefix", "/evil")
.header("x-forwarded-for", "203.0.113.66")
.header("forwarded", "for=203.0.113.66;proto=http;host=evil.example")
.body(axum::body::Body::empty())
.unwrap();
let got = app.router_now().oneshot(req).await.unwrap();
let got = String::from_utf8(axum::body::to_bytes(got.into_body(), 1 << 16).await.unwrap().to_vec()).unwrap();
// No connection address in a oneshot: the front names no client, and passes none through.
assert_eq!(got, "proto=https host=repo.example.test xhost=repo.example.test port= prefix= for= forwarded=");
}
/// ★ **RA5-1 (the edda half): the front carries only the gRPC services the product declares.**
/// The repository route carried EVERY gRPC call on a routed name to the primary, so whatever the
/// primary serves on its gRPC door was reachable from the internet: a stranger's
/// `nornir.identity.v1.IdentityService/SignUp` on `repo.<customer>` reached a private box's
/// identity core. A call to a service the route does not carry is refused HERE (`UNIMPLEMENTED`,
/// audited) and never reaches the primary.
#[tokio::test(flavor = "multi_thread")]
async fn a_grpc_call_to_a_service_the_product_did_not_declare_is_refused_at_the_front() {
use tower::ServiceExt as _;
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let SocketAddr::V4(up) = l.local_addr().unwrap() else { unreachable!() };
tokio::spawn(async move { axum::serve(l, Router::new().fallback(|| async { "the primary" })).await.unwrap() });
let app = serve::App::new(Router::new().fallback(|| async { "console" }));
let carried = vec!["holger.".to_string(), "x.v1.Repo/Get".to_string()];
app.set_routed(Some(serve::Routed { hosts: vec!["repo.example.test".into()], http: up, grpc: up, grpc_services: carried.clone() }));
let r = app.router_now();
let call = |path: &str| axum::http::Request::post(path).header("host", "repo.example.test").header("content-type", "application/grpc").header("te", "trailers").body(axum::body::Body::empty()).unwrap();
let text = |r: Response| async move { String::from_utf8(axum::body::to_bytes(r.into_body(), 1 << 16).await.unwrap().to_vec()).unwrap() };
let sign_up = r.clone().oneshot(call("/nornir.identity.v1.IdentityService/SignUp")).await.unwrap();
assert_eq!(sign_up.headers().get("grpc-status").map(|v| v.to_str().unwrap().to_owned()).as_deref(), Some("12"), "refused at the front as UNIMPLEMENTED");
assert_ne!(text(sign_up).await, "the primary", "an undeclared service never reaches the primary");
for path in ["/holger.v1.GrowService/Status", "/holger.room.v1.RoomService/Rooms", "/x.v1.Repo/Get"] {
let a = r.clone().oneshot(call(path)).await.unwrap();
assert!(a.headers().get("grpc-status").is_none(), "{path} is carried");
assert_eq!(text(a).await, "the primary", "{path}");
}
for path in ["/x.v1.Repo/Put", "/holgerx.v1.S/M", "/holger.v1.GrowService/Status/more", "/api/v1/crates/new", "/"] {
assert!(!serve::carries(&carried, path), "{path} is not carried");
}
// Plain HTTP on the same name is the repository's, as before.
let page = r.oneshot(axum::http::Request::get("/nornir.identity.v1.IdentityService/SignUp").header("host", "repo.example.test").body(axum::body::Body::empty()).unwrap()).await.unwrap();
assert_eq!(text(page).await, "the primary");
// A route that declares no service carries no gRPC call at all.
app.set_routed(Some(serve::Routed { hosts: vec!["repo.example.test".into()], http: up, grpc: up, grpc_services: Vec::new() }));
let none = app.router_now().oneshot(call("/holger.v1.GrowService/Status")).await.unwrap();
assert_eq!(none.headers().get("grpc-status").map(|v| v.to_str().unwrap().to_owned()).as_deref(), Some("12"));
}
/// **RA1-16: a callback-only name is never carried to the primary**, even when
/// the route names it (a feed, or a caller of [`serve::App::set_routed`], that
/// got it wrong): on that name a gRPC call and a page are the narrowing's 308
/// to the first host, `/callback/<vendor>` is the face's, and the other routed
/// names still reach the primary.
#[tokio::test(flavor = "multi_thread")]
async fn a_callback_only_name_is_never_carried_to_the_primary_even_when_routed() {
use tower::ServiceExt as _;
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let SocketAddr::V4(up) = l.local_addr().unwrap() else { unreachable!() };
tokio::spawn(async move { axum::serve(l, Router::new().fallback(|| async { "the primary" })).await.unwrap() });
let app = serve::App::new(Router::new().route("/callback/{vendor}", post(|| async { "callback" })).fallback(|| async { "console" }));
app.set_callback_only(&["pay.example.test".to_string()], "example.test");
app.set_routed(Some(serve::Routed { hosts: vec!["pay.example.test".into(), "repo.example.test".into()], http: up, grpc: up, grpc_services: vec!["holger.".into()] }));
let r = app.router_now();
let ask = |host: &str, path: &str, grpc: bool| {
let mut b = axum::http::Request::post(path).header("host", host);
if grpc {
b = b.header("content-type", "application/grpc").header("te", "trailers");
}
b.body(axum::body::Body::empty()).unwrap()
};
let text = |r: Response| async move { String::from_utf8(axum::body::to_bytes(r.into_body(), 1 << 16).await.unwrap().to_vec()).unwrap() };
let grpc = r.clone().oneshot(ask("pay.example.test", "/holger.v1.GrowService/Status", true)).await.unwrap();
assert_eq!(grpc.status(), StatusCode::PERMANENT_REDIRECT, "a gRPC call on a callback-only name is not carried");
assert_eq!(grpc.headers()[header::LOCATION], "https://example.test/holger.v1.GrowService/Status");
let page = r.clone().oneshot(ask("pay.example.test", "/api/v1/crates/new", false)).await.unwrap();
assert_eq!(page.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(text(r.clone().oneshot(ask("pay.example.test", "/callback/stripe", false)).await.unwrap()).await, "callback");
assert_eq!(text(r.oneshot(ask("repo.example.test", "/api/v1/crates/new", false)).await.unwrap()).await, "the primary", "the other routed name still routes");
}
/// ★ **An early refusal is carried back while the body is still owed.** The
/// primary answers a PUT that DECLARES more than it will take (holger's
/// `[holger-metered-6]`, 507, before a byte of the body is read); the route must
/// carry that answer back rather than wait for a body the client will never
/// send (MEASURED on holger's mock 2026-09-28: a declared 10 TiB PUT through vm1
/// got no answer at all).
#[tokio::test(flavor = "multi_thread")]
async fn an_early_refusal_is_carried_back_before_the_body() {
use tower::ServiceExt as _;
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let SocketAddr::V4(h) = l.local_addr().unwrap() else { unreachable!() };
let upstream = Router::new().fallback(|req: axum::extract::Request| async move {
let declared: u64 = req.headers().get(header::CONTENT_LENGTH).and_then(|v| v.to_str().ok()).and_then(|v| v.parse().ok()).unwrap_or(0);
if declared > 1 << 20 {
return (StatusCode::INSUFFICIENT_STORAGE, "[holger-metered-6] over the paid cap").into_response();
}
"taken".into_response()
});
tokio::spawn(async move { axum::serve(l, upstream).await.unwrap() });
let app = serve::App::new(Router::new().fallback(|| async { "console" }));
app.set_routed(Some(serve::Routed { hosts: vec!["repo.example.test".into()], http: h, grpc: h, grpc_services: vec!["holger.".into()] }));
// A body that never comes.
struct Never;
impl hyper::body::Body for Never {
type Data = axum::body::Bytes;
type Error = std::io::Error;
fn poll_frame(self: std::pin::Pin<&mut Self>, _: &mut std::task::Context<'_>) -> std::task::Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
std::task::Poll::Pending
}
}
let req = axum::http::Request::put("/bundles/over-cap.bin")
.header("host", "repo.example.test")
.header(header::CONTENT_LENGTH, (10u64 << 40).to_string())
.body(axum::body::Body::new(Never))
.unwrap();
let answer = tokio::time::timeout(std::time::Duration::from_secs(10), app.router_now().oneshot(req)).await;
let r = answer.expect("the refusal came back while the body was still owed").unwrap();
assert_eq!(r.status(), StatusCode::INSUFFICIENT_STORAGE);
// And over a real connection, the way a client meets vm1: the head alone.
let front = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let at = front.local_addr().unwrap();
let face = app.router_now();
tokio::spawn(async move { axum::serve(front, face).await.unwrap() });
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
let mut c = tokio::net::TcpStream::connect(at).await.unwrap();
c.write_all(format!("PUT /bundles/over-cap.bin HTTP/1.1\r\nhost: repo.example.test\r\ncontent-length: {}\r\n\r\n", 10u64 << 40).as_bytes()).await.unwrap();
let mut buf = vec![0u8; 256];
let n = tokio::time::timeout(std::time::Duration::from_secs(10), c.read(&mut buf)).await.expect("an answer on the head alone").unwrap();
let head = String::from_utf8_lossy(&buf[..n]).to_string();
assert!(head.starts_with("HTTP/1.1 507"), "{head}");
}
#[tokio::test(flavor = "multi_thread")]
async fn monetizes_own_host_carries_its_control_api_only_with_a_signature_and_every_other_host_is_unchanged() {
use tower::ServiceExt as _;
let feed = BTreeMap::from([
(FEED_WEB_HOSTS.to_string(), "holger.rs,www.holger.rs,pay.example.dk,monetize.holger.rs".to_string()),
(FEED_WEB_CALLBACK_ONLY.to_string(), "pay.example.dk,monetize.holger.rs".to_string()),
(FEED_MONETIZE_CONTROL_HOST.to_string(), " Monetize.Holger.rs ".to_string()),
]);
let cfg = WebConfig::from("holger.rs", &feed);
assert_eq!(cfg.control_host.as_deref(), Some("monetize.holger.rs"));
assert_eq!(cfg.callback_only, vec!["pay.example.dk"], "monetize's host is never narrowed to callbacks");
assert_eq!(WebConfig::from("holger.rs", &BTreeMap::from([(FEED_MONETIZE_CONTROL_HOST.to_string(), "monetize.holger.rs".to_string())])).control_host, None, "a name the front does not serve is not taken");
let first = BTreeMap::from([(FEED_WEB_HOSTS.to_string(), "holger.rs".to_string()), (FEED_MONETIZE_CONTROL_HOST.to_string(), "holger.rs".to_string())]);
assert_eq!(WebConfig::from("holger.rs", &first).control_host, None, "the first host is the console's, never monetize's");
// A stand-in for monetize on loopback: protocol, path, bearer, signature.
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let to = l.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(
l,
Router::new().fallback(|req: axum::extract::Request| async move {
let h = |n: &str| req.headers().get(n).and_then(|v| v.to_str().ok()).unwrap_or("-").to_string();
format!("{:?}|{}|{}|{}", req.version(), req.uri().path(), h("authorization"), h("monetize-authorization"))
}),
)
.await
.unwrap()
});
let app = serve::App::new(Router::new().fallback(|| async { "console" }));
app.set_callback_only(&cfg.callback_only, cfg.name());
app.set_control_host(cfg.control_host.as_deref());
let ask = |app: &serve::App, host: &str, path: &str, grpc: bool| {
let mut b = axum::http::Request::post(path).header("host", host).header("authorization", "Bearer the-callers-own").header("monetize-authorization", "monetize-sshsig-v1 sig");
if grpc {
b = b.header("content-type", "application/grpc").header("te", "trailers");
}
app.router_now().oneshot(b.body(axum::body::Body::empty()).unwrap())
};
let text = |r: Response| async move { String::from_utf8(axum::body::to_bytes(r.into_body(), 1 << 16).await.unwrap().to_vec()).unwrap() };
// No monetize target (an open front): nothing is carried.
assert_eq!(text(ask(&app, "monetize.holger.rs", "/monetize.v1.Products/List", true).await.unwrap()).await, "console");
app.set_callbacks(Some(to));
assert_eq!(text(ask(&app, "monetize.holger.rs", "/monetize.v1.Products/List", true).await.unwrap()).await, "HTTP/2.0|/monetize.v1.Products/List|-|monetize-sshsig-v1 sig", "gRPC over h2c, the signature kept, NO bearer: only SSHSIG from outside");
assert_eq!(text(ask(&app, "monetize.holger.rs:443", "/callback/stripe", false).await.unwrap()).await, "HTTP/1.1|/callback/stripe|-|monetize-sshsig-v1 sig", "a callback on monetize's host is monetize's");
assert_eq!(text(ask(&app, "monetize.holger.rs", "/", false).await.unwrap()).await, "HTTP/1.1|/|-|monetize-sshsig-v1 sig", "nothing of the console on monetize's host");
// Every other host: as before.
assert_eq!(text(ask(&app, "holger.rs", "/monetize.v1.Products/List", true).await.unwrap()).await, "console", "the apex carries no control API");
let hook = text(ask(&app, "holger.rs", "/callback/stripe", false).await.unwrap()).await;
assert!(hook.ends_with("|/callback/stripe|Bearer the-callers-own|monetize-sshsig-v1 sig"), "/callback/<vendor> on the apex is unchanged: {hook}");
let narrowed = ask(&app, "pay.example.dk", "/login", false).await.unwrap();
assert_eq!(narrowed.status(), StatusCode::PERMANENT_REDIRECT, "a callback-only host still 308s");
app.set_control_host(None);
assert_eq!(text(ask(&app, "monetize.holger.rs", "/", false).await.unwrap()).await, "console", "nothing fed: nothing routed");
}
// ── DNS first (owner 2026-10-25: certificates must fix themselves) ─────────────────────
/// **An injected resolver**: every name resolves to `to`; this front's own public
/// address is `own`. No live DNS is ever asked from a test.
struct TestDns {
own: Vec<std::net::IpAddr>,
to: Mutex<Vec<std::net::IpAddr>>,
resolved: Mutex<u32>,
}
impl dns::Dns for TestDns {
fn resolve(&self, _host: &str) -> Result<Vec<std::net::IpAddr>, String> {
*self.resolved.lock().unwrap() += 1;
let to = self.to.lock().unwrap().clone();
if to.is_empty() {
Err("Name does not resolve".into())
} else {
Ok(to)
}
}
fn own(&self) -> Vec<std::net::IpAddr> {
self.own.clone()
}
}
const OURS: &str = "198.51.100.7";
const THEIRS: &str = "203.0.113.9";
fn test_dns(to: &[&str]) -> Arc<TestDns> {
Arc::new(TestDns { own: vec![OURS.parse().unwrap()], to: Mutex::new(to.iter().map(|s| s.parse().unwrap()).collect()), resolved: Mutex::new(0) })
}
/// A fast probe: a look every 200 ms (the DNS re-check stands for the minute), the feed
/// read every 50 ms.
fn probe_with(d: Arc<TestDns>) -> Probe {
Probe { dns: d, clock: Arc::new(SystemTime::now), look_every: Duration::from_millis(200), tick: Duration::from_millis(50) }
}
/// Every name points here.
fn here() -> Probe {
probe_with(test_dns(&[OURS]))
}
/// An address nothing listens on: an ACME validator dialling it fails, as Let's Encrypt
/// does for a name that points at another box.
async fn dead_addr() -> SocketAddr {
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
l.local_addr().unwrap()
}
fn acme_feed(directory: &str) -> BTreeMap<String, String> {
BTreeMap::from([(FEED_ACME_DIRECTORY.to_string(), directory.to_string()), (FEED_WEB_HOSTS.to_string(), "example.test".to_string())])
}
/// ★ **A name that does not point here is not an ACME failure.** test2's ONE box had no A
/// record: every look asked Let's Encrypt, failed its validation and doubled the persisted
/// backoff, toward six hours. With DNS checked first: no order, no backoff, the reason said.
#[cfg(feature = "acme")]
#[tokio::test(flavor = "multi_thread")]
async fn a_name_that_does_not_point_here_asks_no_acme_and_grows_no_backoff() {
let dir = tempfile::tempdir().unwrap();
let (l80, l443, _a80, _a443) = listeners().await;
let (directory, acme) = fake_acme(dead_addr().await).await;
let d = test_dns(&[THEIRS]);
let feed = acme_feed(&directory);
let web = start_on_probed(dir.path(), "x".into(), Arc::new(move || feed.clone()), l80, l443, probe_with(Arc::clone(&d)));
tokio::time::sleep(Duration::from_millis(1500)).await;
assert!(!web.resolver.any());
assert_eq!(*acme.orders.lock().unwrap(), 0, "Let's Encrypt was asked for a name that points elsewhere");
assert!(!dir.path().join("tls/example.test/acme-next").exists(), "the backoff grew: {:?}", acme::next_attempt(dir.path(), "example.test"));
assert!(*d.resolved.lock().unwrap() >= 3, "the name is resolved again on every look, not once");
}
/// ★ **The moment the name points here, ACME is asked** — within one DNS check, not after a
/// backoff earned while the record was missing.
#[cfg(feature = "acme")]
#[tokio::test(flavor = "multi_thread")]
async fn the_moment_the_name_points_here_acme_is_asked_within_one_dns_check() {
let dir = tempfile::tempdir().unwrap();
let (l80, l443, a80, _a443) = listeners().await;
let (directory, acme) = fake_acme(a80).await;
let d = test_dns(&[]);
let feed = acme_feed(&directory);
let web = start_on_probed(dir.path(), "x".into(), Arc::new(move || feed.clone()), l80, l443, probe_with(Arc::clone(&d)));
tokio::time::sleep(Duration::from_millis(1000)).await;
assert_eq!(*acme.orders.lock().unwrap(), 0, "no A record yet: not asked");
*d.to.lock().unwrap() = vec![OURS.parse().unwrap()];
let t0 = std::time::Instant::now();
until("the ACME certificate", || web.resolver.any().then_some(())).await;
assert!(t0.elapsed() < Duration::from_millis(200 * 4 + 2000), "asked within a DNS check of the record appearing ({:?})", t0.elapsed());
assert_eq!(*acme.issued.lock().unwrap(), 1);
}
/// ★ **A backoff an older front wrote is not held against a name that points here.** Before
/// this change every failure (a missing A record too) wrote `acme-next`, with no word of
/// what failed; a box updated while inside a six-hour wait must not sit it out.
#[cfg(feature = "acme")]
#[tokio::test(flavor = "multi_thread")]
async fn a_backoff_an_older_front_wrote_does_not_hold_a_name_that_now_points_here() {
let dir = tempfile::tempdir().unwrap();
let in_6h = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() + 6 * 3600;
crate::write_secret(dir.path(), "tls/example.test/acme-next", &format!("{in_6h} 6\n")).unwrap();
let (l80, l443, a80, _a443) = listeners().await;
let (directory, acme) = fake_acme(a80).await;
let feed = acme_feed(&directory);
let web = start_on_probed(dir.path(), "x".into(), Arc::new(move || feed.clone()), l80, l443, here());
until("the ACME certificate", || web.resolver.any().then_some(())).await;
assert_eq!(*acme.issued.lock().unwrap(), 1);
}
/// **A real ACME failure still backs off**: the name points here, yet the validation fails
/// (tcp/80 is not reached). The failure is recorded as a validation failure, waits fifteen
/// minutes, and is not repeated inside them.
#[cfg(feature = "acme")]
#[tokio::test(flavor = "multi_thread")]
async fn a_real_acme_validation_failure_still_backs_off() {
let dir = tempfile::tempdir().unwrap();
let (l80, l443, _a80, _a443) = listeners().await;
let (directory, acme) = fake_acme(dead_addr().await).await;
let feed = acme_feed(&directory);
let _web = start_on_probed(dir.path(), "x".into(), Arc::new(move || feed.clone()), l80, l443, here());
until("the failed attempt", || dir.path().join("tls/example.test/acme-next").exists().then_some(())).await;
tokio::time::sleep(Duration::from_millis(1000)).await;
assert_eq!(*acme.orders.lock().unwrap(), 1, "asked once, then waits");
let w = acme::wait(dir.path(), "example.test").expect("a wait is kept");
assert_eq!((w.failures, w.kind), (1, Some(acme::FailKind::Validation)));
let left = w.at.duration_since(SystemTime::now()).unwrap();
assert!(left > Duration::from_secs(14 * 60) && left <= Duration::from_secs(15 * 60), "{left:?}");
}
// ── the status report, "try now", and the one-hour alert ───────────────────────────────
/// A clock the test moves: the real time plus `ahead`.
fn moved_clock(ahead: Arc<Mutex<Duration>>) -> Arc<dyn Fn() -> SystemTime + Send + Sync> {
Arc::new(move || SystemTime::now() + *ahead.lock().unwrap())
}
fn status_of(root: &std::path::Path, host: &str) -> Option<status::HostStatus> {
status::report(root, SystemTime::now()).into_iter().find(|s| s.host == host)
}
/// ★ **The report**: a name that does not point here says so, with no next attempt (DNS is
/// re-checked, nothing waits), "try now" allowed; once it points here and the certificate
/// is issued: valid until its notAfter, no reason left.
#[cfg(feature = "acme")]
#[tokio::test(flavor = "multi_thread")]
async fn the_status_says_valid_until_next_attempt_and_the_last_reason() {
let dir = tempfile::tempdir().unwrap();
let (l80, l443, a80, _a443) = listeners().await;
let (directory, _acme) = fake_acme(a80).await;
let d = test_dns(&[THEIRS]);
let feed = acme_feed(&directory);
let web = start_on_probed(dir.path(), "x".into(), Arc::new(move || feed.clone()), l80, l443, probe_with(Arc::clone(&d)));
let s = until("a status for example.test", || status_of(dir.path(), "example.test").filter(|s| s.last_reason.is_some())).await;
assert_eq!(s.last_kind.as_deref(), Some("dns"));
assert_eq!(s.last_reason.as_deref(), Some("dns: example.test does not point at this box (it resolves to 203.0.113.9)"));
assert_eq!((s.valid_until, s.next_attempt, s.failures, s.try_now), (None, None, 0, true));
assert!(s.no_certificate_since.is_some() && s.alert.is_none());
assert_eq!(s.hosts, vec!["example.test".to_string()]);
*d.to.lock().unwrap() = vec![OURS.parse().unwrap()];
until("the ACME certificate", || web.resolver.any().then_some(())).await;
let s = until("the issued status", || status_of(dir.path(), "example.test").filter(|s| s.valid_until.is_some())).await;
let kept = certs::kept(dir.path(), "example.test", SystemTime::now()).unwrap().unwrap();
assert_eq!(s.valid_until, Some(kept.not_after.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()));
assert_eq!((s.last_kind, s.last_reason, s.no_certificate_since, s.next_attempt), (None, None, None, None));
}
/// ★ **"Try now" skips the wait only when the last failure was not a validation failure**:
/// Let's Encrypt counts failed validations (five per name per hour); any other failure (the
/// order refused, the account, the network) may be tried again at once.
#[cfg(feature = "acme")]
#[tokio::test(flavor = "multi_thread")]
async fn try_now_skips_the_wait_only_after_a_failure_that_was_not_a_validation() {
let now = SystemTime::now();
let dir = tempfile::tempdir().unwrap();
acme::failed_as(dir.path(), "v.test", now, acme::FailKind::Validation);
let refused = status::try_now(dir.path(), "v.test", now).unwrap_err();
assert!(refused.contains("validation"), "{refused}");
assert!(acme::wait(dir.path(), "v.test").unwrap().at > now, "the wait is kept");
assert!(!status_of(dir.path(), "v.test").unwrap().try_now);
assert!(status::try_now(dir.path(), "v.test", now + Duration::from_secs(16 * 60)).is_ok(), "once the wait ran out there is nothing to skip");
acme::failed_as(dir.path(), "a.test", now, acme::FailKind::Acme);
assert!(status_of(dir.path(), "a.test").unwrap().try_now);
status::try_now(dir.path(), "a.test", now).unwrap();
let w = acme::wait(dir.path(), "a.test").unwrap();
assert!(w.at <= now && w.failures == 1, "the wait is skipped, the count kept: {w:?}");
// End to end: the order is refused (not a validation), so the front waits fifteen
// minutes; "try now" has it ask again at once.
let dir = tempfile::tempdir().unwrap();
let (l80, l443, a80, _a443) = listeners().await;
let (directory, acme) = fake_acme(a80).await;
*acme.refuse_orders.lock().unwrap() = true;
let feed = acme_feed(&directory);
let _web = start_on_probed(dir.path(), "x".into(), Arc::new(move || feed.clone()), l80, l443, here());
until("the refused order", || (*acme.orders.lock().unwrap() == 1 && acme::wait(dir.path(), "example.test").is_some()).then_some(())).await;
tokio::time::sleep(Duration::from_millis(600)).await;
assert_eq!(*acme.orders.lock().unwrap(), 1, "waits");
assert_eq!(acme::wait(dir.path(), "example.test").unwrap().kind, Some(acme::FailKind::Acme));
*acme.refuse_orders.lock().unwrap() = false;
status::try_now(dir.path(), "example.test", SystemTime::now()).unwrap();
until("the ACME certificate after try now", || (*acme.issued.lock().unwrap() == 1).then_some(())).await;
}
/// ★ **The one-hour alert**: no valid certificate for more than an hour is an `ALERT
/// tls.no-certificate` with its reason (the ONE box mails every `ALERT <kind>` line).
#[cfg(feature = "acme")]
#[tokio::test(flavor = "multi_thread")]
async fn a_host_without_a_valid_certificate_for_an_hour_is_an_alert_with_its_reason() {
let t = SystemTime::now();
assert_eq!(status::alert_line("a.test", t, t + Duration::from_secs(59 * 60), "dns: x"), None);
assert_eq!(
status::alert_line("a.test", t, t + Duration::from_secs(61 * 60), "dns: a.test does not point at this box (it resolves to nothing)").as_deref(),
Some("ALERT tls.no-certificate: a.test has had no valid certificate for 1 h 1 min: dns: a.test does not point at this box (it resolves to nothing)")
);
let dir = tempfile::tempdir().unwrap();
let (l80, l443, _a80, _a443) = listeners().await;
let (directory, _acme) = fake_acme(dead_addr().await).await;
let ahead = Arc::new(Mutex::new(Duration::ZERO));
let probe = Probe { clock: moved_clock(Arc::clone(&ahead)), ..probe_with(test_dns(&[])) };
let feed = acme_feed(&directory);
let _web = start_on_probed(dir.path(), "x".into(), Arc::new(move || feed.clone()), l80, l443, probe);
let s = until("the first status", || status_of(dir.path(), "example.test").filter(|s| s.no_certificate_since.is_some())).await;
assert!(s.alert.is_none(), "not yet an hour");
*ahead.lock().unwrap() = Duration::from_secs(61 * 60);
let later = || status::report(dir.path(), SystemTime::now() + Duration::from_secs(61 * 60)).into_iter().find(|s| s.host == "example.test");
let s = until("the alert", || later().filter(|s| s.alert.is_some())).await;
until("the loop saying it", || status::Record::load(dir.path(), "example.test").alerted.map(|_| ())).await;
let a = s.alert.unwrap();
assert!(a.starts_with("ALERT tls.no-certificate: example.test has had no valid certificate for 1 h"), "{a}");
assert!(a.ends_with("dns: example.test does not point at this box (it resolves to nothing: Name does not resolve)"), "{a}");
}
/// **The DNS check is part of ACME**: a front with a valid certificate, or one not fed yet,
/// resolves nothing (no new outbound where ACME is not asked).
#[tokio::test(flavor = "multi_thread")]
async fn a_front_that_does_not_ask_acme_resolves_nothing() {
let dir = tempfile::tempdir().unwrap();
let (l80, l443, a80, _a443) = listeners().await;
let (directory, acme) = fake_acme(a80).await;
let (chain, key) = certs::tests::minted(&["example.test"], 80);
let mut feed = acme_feed(&directory);
feed.insert(certs::fed_cert_key("example.test"), chain);
feed.insert(certs::fed_key_key("example.test"), key);
let d = test_dns(&[OURS]);
let web = start_on_probed(dir.path(), "x".into(), Arc::new(move || feed.clone()), l80, l443, probe_with(Arc::clone(&d)));
until("the fed certificate", || web.resolver.any().then_some(())).await;
let (l80, l443, _, _) = listeners().await;
let unfed = test_dns(&[OURS]);
let _quiet = start_on_probed(tempfile::tempdir().unwrap().path(), "x".into(), Arc::new(BTreeMap::new), l80, l443, probe_with(Arc::clone(&unfed)));
tokio::time::sleep(Duration::from_millis(800)).await;
assert_eq!((*d.resolved.lock().unwrap(), *unfed.resolved.lock().unwrap(), *acme.asked.lock().unwrap()), (0, 0, 0));
let s = status_of(dir.path(), "example.test").unwrap();
assert!(s.valid_until.is_some() && s.last_reason.is_none() && s.alert.is_none(), "{s:?}");
}
/// ★ **The routed gRPC door is rate-limited per caller and audited** (holger lane B3, the
/// pattern of Gunnar's A6: the front forwards `application/grpc` to the main's control plane
/// with no logic of its own). One address may make [`serve::ROUTE_GRPC_BURST`] calls at once
/// and [`serve::ROUTE_GRPC_PER_SEC`] a second after that; the next is answered HERE as gRPC's
/// `RESOURCE_EXHAUSTED` with a `retry-after`, never carried. Another address is not held back.
/// Plain HTTP to the repository host is not counted (package clients fetch in bursts).
#[tokio::test(flavor = "multi_thread")]
async fn the_routed_grpc_door_is_rate_limited_per_caller() {
use tower::ServiceExt as _;
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let SocketAddr::V4(g) = l.local_addr().unwrap() else { unreachable!() };
tokio::spawn(async move { axum::serve(l, Router::new().fallback(|| async { "carried" })).await.unwrap() });
let app = serve::App::new(Router::new().fallback(|| async { "console" }));
app.set_routed(Some(serve::Routed { hosts: vec!["repo.example.test".into()], http: g, grpc: g, grpc_services: vec!["holger.".into()] }));
let r = app.router_now();
let ask = |ip: [u8; 4], grpc: bool| {
let mut b = axum::http::Request::post("/holger.v1.AdminService/Health").header("host", "repo.example.test");
if grpc {
b = b.header("content-type", "application/grpc").header("te", "trailers");
}
let mut req = b.body(axum::body::Body::from("x")).unwrap();
req.extensions_mut().insert(axum::extract::ConnectInfo(SocketAddr::from((ip, 40000))));
req
};
let busy = [192, 0, 2, 77];
// Twice the burst at once: the burst is carried, the rest is refused at the front
// (the bucket refills at ROUTE_GRPC_PER_SEC, so a slow host may carry a few more).
let mut calls = tokio::task::JoinSet::new();
for _ in 0..2 * serve::ROUTE_GRPC_BURST {
let (r, req) = (r.clone(), ask(busy, true));
calls.spawn(async move { r.oneshot(req).await.unwrap() });
}
let (mut carried, mut refused) = (0u32, Vec::new());
while let Some(a) = calls.join_next().await {
let a = a.unwrap();
match a.headers().get("grpc-status").and_then(|v| v.to_str().ok()) {
None => carried += 1,
Some(_) => refused.push(a),
}
}
assert!(carried >= serve::ROUTE_GRPC_BURST, "the burst is carried: {carried}");
assert!(!refused.is_empty(), "twice the burst at once is over the rate: {carried} carried");
let over = refused.pop().unwrap();
assert_eq!(over.status(), StatusCode::OK, "a gRPC refusal is an HTTP 200 with a grpc-status");
assert_eq!(over.headers().get("grpc-status").and_then(|v| v.to_str().ok()), Some("8"), "RESOURCE_EXHAUSTED");
assert!(over.headers().get("retry-after").is_some(), "it says when to come back");
let calm = r.clone().oneshot(ask([192, 0, 2, 78], true)).await.unwrap();
assert!(calm.headers().get("grpc-status").is_none(), "another caller is not held back");
let http = r.clone().oneshot(ask(busy, false)).await.unwrap();
assert_eq!(http.status(), StatusCode::OK, "plain HTTP to the repository is not counted");
assert!(http.headers().get("grpc-status").is_none());
}
#[test]
fn a_callers_bucket_refills_at_its_rate_up_to_its_burst() {
let mut b = serve::Bucket::full(3, 2);
let t0 = std::time::Instant::now();
assert!(b.take(t0) && b.take(t0) && b.take(t0));
assert!(!b.take(t0), "the burst is spent");
assert!(b.take(t0 + std::time::Duration::from_millis(600)), "one token back after half a second at 2/s");
assert!(!b.take(t0 + std::time::Duration::from_millis(600)));
let mut later = b.clone();
for _ in 0..3 {
assert!(later.take(t0 + std::time::Duration::from_secs(60)), "refilled to the burst, never past it");
}
assert!(!later.take(t0 + std::time::Duration::from_secs(60)));
assert!(!later.idle(t0 + std::time::Duration::from_secs(60)) && later.idle(t0 + std::time::Duration::from_secs(62)), "a quiet caller's bucket is full again");
assert!(serve::Bucket::full(1, 1).idle(t0), "a fresh bucket is idle");
}
/// ★ RA2-3: **a caller is an IPv6 /64 (an IPv4 address)**, so one /64 is not 2^64 callers.
#[test]
fn a_caller_is_one_ipv6_slash_64_or_one_ipv4_address() {
let k = |s: &str| serve::caller_key(s.parse().unwrap());
assert_eq!(k("2001:db8:1:2::1"), k("2001:db8:1:2:ffff:ffff:ffff:ffff"), "one /64, one caller");
assert_ne!(k("2001:db8:1:2::1"), k("2001:db8:1:3::1"), "another /64, another caller");
assert_ne!(k("192.0.2.1"), k("192.0.2.2"), "an IPv4 address is its own caller");
assert_eq!(k("::ffff:192.0.2.1"), k("192.0.2.1"), "an IPv4-mapped address is the IPv4 caller");
}
/// ★ RA2-3: **a full map evicts its oldest callers, never every bucket.** A flooder whose
/// bucket is empty stays refused when fresh callers fill the map.
#[test]
fn a_full_map_drops_the_oldest_caller_and_keeps_the_flooders_bucket() {
let t0 = std::time::Instant::now();
let cap = 4;
let mut all = serve::Callers::new(cap);
let (burst, rate) = (serve::ROUTE_GRPC_BURST, serve::ROUTE_GRPC_PER_SEC);
for (i, key) in ["a", "b", "c"].iter().enumerate() {
// Busy callers: each spends its whole burst, so none is idle.
for _ in 0..burst {
all.take(key, t0 + std::time::Duration::from_millis(i as u64), burst, rate);
}
}
let flood = t0 + std::time::Duration::from_millis(10);
while all.take("flooder", flood, burst, rate) {}
assert!(!all.take("flooder", flood, burst, rate), "the flooder is over its rate");
for (i, fresh) in ["d", "e", "f"].iter().enumerate() {
assert!(all.take(fresh, flood + std::time::Duration::from_micros(i as u64 + 1), burst, rate), "a fresh caller is admitted");
}
assert!(all.len() <= cap, "the map is bounded: {}", all.len());
assert!(!all.take("flooder", flood + std::time::Duration::from_micros(5), burst, rate), "a full map did not reset the flooder's bucket");
assert!(!all.contains("a"), "the oldest caller is the one dropped");
}
/// ★ RA4-23: **a /48 that rotates its /64s shares one site bucket.** Each fresh /64 still has
/// its own full burst, but every call also spends its /48's, so 65,536 /64s are not 65,536
/// bursts. Another /48 and an IPv4 caller are not held back.
#[test]
fn a_slash_48_rotating_its_slash_64s_shares_one_site_bucket() {
let t0 = std::time::Instant::now();
let mut lim = serve::Limiter::new(serve::ROUTE_GRPC_CALLERS);
let ip = |s: String| s.parse::<std::net::IpAddr>().unwrap();
let admitted = (0..4000u32).filter(|i| lim.admit(ip(format!("2001:db8:1:{:x}::1", i)), t0)).count();
assert_eq!(admitted, serve::ROUTE_GRPC_SITE_BURST as usize, "one /48's fresh /64s share the site's burst");
assert!(lim.admit(ip("2001:db8:2::1".into()), t0), "another /48 is its own site");
assert!(lim.admit(ip("192.0.2.7".into()), t0), "an IPv4 caller has no site tier");
assert!(lim.admit(ip("2001:db8:1:7::1".into()), t0 + std::time::Duration::from_secs(1)), "the site refills at its rate");
assert_eq!(serve::site_key(ip("2001:db8:1:2::1".into())), serve::site_key(ip("2001:db8:1:ffff::9".into())), "one /48, one site");
assert_ne!(serve::site_key(ip("2001:db8:1::1".into())), serve::site_key(ip("2001:db8:2::1".into())));
assert_eq!(serve::site_key(ip("::ffff:192.0.2.1".into())), None, "an IPv4-mapped caller is IPv4");
assert!(lim.callers().len() <= serve::ROUTE_GRPC_CALLERS, "the map is bounded");
}
/// ★ RA4-23: **a full map drops its least recently seen caller without a scan.** 200,000 fresh
/// callers through a full 4096-bucket map (each one evicts): an O(4096) scan per eviction is
/// over a billion steps under the mutex; the LRU is a log step each.
#[test]
fn a_full_map_evicts_in_log_time_not_by_a_scan() {
let t0 = std::time::Instant::now();
let mut all = serve::Callers::new(serve::ROUTE_GRPC_CALLERS);
let (burst, rate) = (serve::ROUTE_GRPC_BURST, serve::ROUTE_GRPC_PER_SEC);
for i in 0..serve::ROUTE_GRPC_CALLERS {
// Busy: none is idle, so nothing is forgotten for free.
for _ in 0..2 {
all.take(&format!("busy-{i}"), t0, burst, rate);
}
}
let begun = std::time::Instant::now();
for i in 0..200_000u32 {
assert!(all.take(&format!("fresh-{i}"), t0, burst, rate));
}
assert!(begun.elapsed() < std::time::Duration::from_secs(5), "200,000 evictions took {:?}", begun.elapsed());
assert_eq!(all.len(), serve::ROUTE_GRPC_CALLERS);
assert!(all.contains("fresh-199999") && !all.contains("fresh-0") && !all.contains("busy-0"), "the least recently seen go first");
// Seen again, a caller is recent: the next eviction takes the one after it.
let mut lru = serve::Callers::new(3);
for k in ["a", "b", "c"] {
lru.take(k, t0, burst, rate);
}
lru.take("a", t0, burst, rate);
lru.take("d", t0, burst, rate);
assert!(lru.contains("a") && !lru.contains("b") && lru.contains("c") && lru.contains("d"), "{lru:?}");
}
/// ★ RA2-3: **plain HTTP to a routed host's credential doors is counted too** (holger's
/// `/-/settings/` and `/-/auth/` doors travel as plain HTTP on the repository host), and
/// answered 429 with a retry-after past the rate; package paths are not counted.
#[tokio::test(flavor = "multi_thread")]
async fn plain_http_to_the_credential_doors_is_rate_limited_per_caller() {
use tower::ServiceExt as _;
assert!(serve::counted_http("/-/settings/console.public-site") && serve::counted_http("/-/auth/passkey/login/start"));
assert!(!serve::counted_http("/crates/serde/1.0.0/download") && !serve::counted_http("/-/twin"));
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let SocketAddr::V4(h) = l.local_addr().unwrap() else { unreachable!() };
tokio::spawn(async move { axum::serve(l, Router::new().fallback(|| async { "carried" })).await.unwrap() });
let app = serve::App::new(Router::new().fallback(|| async { "console" }));
app.set_routed(Some(serve::Routed { hosts: vec!["repo.example.test".into()], http: h, grpc: h, grpc_services: vec!["holger.".into()] }));
let r = app.router_now();
let ask = |path: &str| {
let mut req = axum::http::Request::get(path).header("host", "repo.example.test").body(axum::body::Body::empty()).unwrap();
req.extensions_mut().insert(axum::extract::ConnectInfo(SocketAddr::from(([198, 51, 100, 9], 40000))));
req
};
let mut refused = None;
for _ in 0..2 * serve::ROUTE_GRPC_BURST {
let a = r.clone().oneshot(ask("/-/settings/console.public-site")).await.unwrap();
if a.status() == StatusCode::TOO_MANY_REQUESTS {
refused = Some(a);
break;
}
}
let over = refused.expect("twice the burst to a credential door is over the rate");
assert!(over.headers().get("retry-after").is_some(), "it says when to come back");
let pkg = r.clone().oneshot(ask("/crates/serde/1.0.0/download")).await.unwrap();
assert_eq!(pkg.status(), StatusCode::OK, "a package path is not counted");
}
// ── A32-11 (step 2): what a product's front needs from the shared front ─────────────────
/// ★ **AIRGAP B1: with the switch off, ACME is never asked** (moved from gunnar-front's fork).
/// The feed names a live (fake) ACME directory and nothing valid is fed or kept; the front serves
/// nothing, says ACME is switched off (`status::KIND_OFF`), and the directory saw no request.
#[tokio::test(flavor = "multi_thread")]
async fn with_the_acme_switch_off_no_acme_directory_is_asked() {
assert!(Hooks::default().acme_on == cfg!(feature = "acme"), "on wherever the client is built, unless the product says otherwise");
let dir = tempfile::tempdir().unwrap();
let (l80, l443, a80, _a443) = listeners().await;
let (directory, acme) = fake_acme(a80).await;
let feed = BTreeMap::from([(FEED_ACME_DIRECTORY.to_string(), directory), (FEED_WEB_HOSTS.to_string(), "front.test".to_string())]);
let web = start_on_hooked(dir.path(), "x".into(), Arc::new(move || feed.clone()), l80, l443, Hooks { acme_on: false, ..Hooks::default() }, here());
tokio::time::sleep(Duration::from_millis(1500)).await;
assert!(!web.resolver.any(), "nothing fed or kept: nothing to serve");
assert_eq!(*acme.asked.lock().unwrap(), 0, "the ACME directory was asked with the switch off");
let rec = status::Record::load(dir.path(), "front.test");
assert_eq!(rec.kind.as_deref(), Some(status::KIND_OFF), "{rec:?}");
}
/// Stinky row 25 (Gunnar 2026-10-10, now the shared front's): the certificate loop's said lines
/// are a bounded memory; a line with a changing number is a new line each look.
#[test]
fn the_lines_said_once_are_a_bounded_memory() {
let mut said = SaidOnce::default();
assert!(said.first_time("tls: serving front.test, 80 more days"));
assert!(!said.first_time("tls: serving front.test, 80 more days"), "said once");
for day in 0..10_000 {
said.first_time(&format!("tls: serving front.test, {day} more days"));
}
assert!(said.len() <= SAID_CAP, "{} lines remembered, the bound is {SAID_CAP}", said.len());
assert!(!said.first_time("tls: serving front.test, 9999 more days"), "the latest line is still remembered");
}
/// ★ **A product's outer layer wraps every answer on :443** ([`Hooks::outer`], installed through the crate-only `App::set_outer`): the
/// face, the callback narrowing and the repository route inside it, the product's own layers
/// (Gunnar: the CLI hole, its login names, git's paths, its access log) outside.
#[tokio::test]
async fn a_products_outer_layer_wraps_every_answer() {
use tower::ServiceExt as _;
let app = serve::App::new(Router::new().route("/", get(|| async { "face" })));
app.set_outer(Some(Arc::new(|r: Router| r.layer(axum::middleware::map_response(|mut res: Response| async move {
res.headers_mut().insert("x-outer", "1".parse().unwrap());
res
})))));
for path in ["/", "/nothing-here"] {
let res = app.router_now().oneshot(axum::http::Request::builder().uri(path).body(axum::body::Body::empty()).unwrap()).await.unwrap();
assert_eq!(res.headers().get("x-outer").map(|v| v.to_str().unwrap()), Some("1"), "{path}");
}
app.set_outer(None);
let res = app.router_now().oneshot(axum::http::Request::builder().uri("/").body(axum::body::Body::empty()).unwrap()).await.unwrap();
assert!(res.headers().get("x-outer").is_none());
}
/// ★ **Every look tells the product what it read** ([`Hooks::on_look`]): Gunnar sets its login
/// names and its hole's names from the same config the certificate loop runs with.
#[tokio::test(flavor = "multi_thread")]
async fn every_look_hands_the_product_its_config() {
let dir = tempfile::tempdir().unwrap();
let (l80, l443, _a80, _a443) = listeners().await;
let seen = Arc::new(Mutex::new(Vec::<Vec<String>>::new()));
let s2 = Arc::clone(&seen);
let feed = BTreeMap::from([(FEED_WEB_HOSTS.to_string(), "a.test,b.test".to_string())]);
let hooks = Hooks { acme_on: false, on_look: Some(Arc::new(move |cfg: &WebConfig| s2.lock().unwrap().push(cfg.hosts.clone()))), ..Hooks::default() };
let _web = start_on_hooked(dir.path(), "x".into(), Arc::new(move || feed.clone()), l80, l443, hooks, here());
tokio::time::sleep(Duration::from_millis(800)).await;
assert_eq!(seen.lock().unwrap().first(), Some(&vec!["a.test".to_string(), "b.test".to_string()]));
}
/// ★ **The product chooses its TLS provider** ([`Hooks::tls`]; Gunnar: X25519MLKEM768 first):
/// the :443 config is built with it.
#[test]
fn the_tls_config_takes_the_products_provider() {
let dir = tempfile::tempdir().unwrap();
let resolver = Arc::new(certs::Resolver::default());
let _ = dir;
let provider = Arc::new(rustls::crypto::ring::default_provider());
let cfg = serve::tls_config_with(resolver, Some(Arc::clone(&provider))).expect("a config");
assert_eq!(cfg.alpn_protocols, vec![b"h2".to_vec(), b"http/1.1".to_vec()]);
assert_eq!(cfg.crypto_provider().kx_groups.len(), provider.kx_groups.len());
}
/// ★ RA4-22: **a product installs its outer layer one way, through its [`Hooks`]**: a web half
/// made with hooks serves through the outer layer from the first router on; one made without
/// has none; and `App::set_outer` is not public API (two ways let Gunnar set it twice).
#[tokio::test]
async fn a_products_outer_layer_is_installed_one_way_through_its_hooks() {
use tower::ServiceExt as _;
let outer: serve::Outer = Arc::new(|face: Router| Router::new().route("/outer", get(|| async { "the product's outer layer" })).fallback_service(face));
let hooks = Hooks { outer: Some(outer), ..Hooks::default() };
let ask = |p: &str| axum::http::Request::get(p).body(axum::body::Body::empty()).unwrap();
let web = Web::with_hooks(tempfile::tempdir().unwrap().path(), &hooks);
let r = web.app.router_now().oneshot(ask("/outer")).await.unwrap();
assert_eq!(r.status(), StatusCode::OK, "the hooks' outer layer is on before anything is served");
let r = web.app.router_now().oneshot(ask("/health")).await.unwrap();
assert_eq!(r.status(), StatusCode::OK, "the face is inside it");
let plain = Web::new(tempfile::tempdir().unwrap().path());
let r = plain.app.router_now().oneshot(ask("/outer")).await.unwrap();
assert_ne!(r.status(), StatusCode::OK, "no hooks, no outer layer");
let serve_rs = include_str!("serve.rs");
assert!(!serve_rs.contains(&["pub fn ", "set_outer"].concat()), "App::set_outer is crate-only: Hooks::outer is the one way");
}
// ─── RA5-22: the :443 door's bounds ─────────────────────────────────────────
/// A :443 door on loopback with `limits`, serving `/health` with a certificate for `door.test`;
/// its address and the certificate to trust.
async fn bounded_door(limits: serve::ConnLimits) -> (SocketAddr, String) {
let (chain, key) = certs::tests::minted(&["door.test"], 30);
let cert = certs::Cert::parse("door.test", &chain, &key, SystemTime::now()).unwrap();
let resolver = Arc::new(certs::Resolver::default());
resolver.set("door.test", Arc::clone(&cert.key));
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(serve::tls_config(resolver).unwrap()));
let app = serve::App::new(Router::new().route("/health", get(|| async { "ok\n" })));
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = l.local_addr().unwrap();
// A32-14 test-only: the door under test, for the test's life.
tokio::spawn(serve::serve_https_limited(l, acceptor, app, limits));
(addr, chain)
}
/// A TLS client to `addr` from the local address `from` (another loopback address is another peer).
async fn tls_from(addr: SocketAddr, from: std::net::IpAddr, ca_pem: &str) -> tokio_rustls::client::TlsStream<tokio::net::TcpStream> {
use rustls::pki_types::pem::PemObject as _;
let mut roots = rustls::RootCertStore::empty();
for c in rustls::pki_types::CertificateDer::pem_slice_iter(ca_pem.as_bytes()) {
roots.add(c.unwrap()).unwrap();
}
let cfg = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())).with_safe_default_protocol_versions().unwrap().with_root_certificates(roots).with_no_client_auth();
let tcp = raw_from(addr, from).await;
let sni = rustls::pki_types::ServerName::try_from("door.test".to_string()).unwrap();
tokio_rustls::TlsConnector::from(Arc::new(cfg)).connect(sni, tcp).await.expect("the TLS handshake")
}
async fn raw_from(addr: SocketAddr, from: std::net::IpAddr) -> tokio::net::TcpStream {
let sock = tokio::net::TcpSocket::new_v4().unwrap();
sock.bind(SocketAddr::new(from, 0)).unwrap();
sock.connect(addr).await.unwrap()
}
/// Whether the door closed `io` within `within` (end of stream or a reset), reading what it sent.
async fn closed_within<S: tokio::io::AsyncRead + Unpin>(io: &mut S, within: Duration) -> bool {
use tokio::io::AsyncReadExt as _;
let mut buf = [0u8; 4096];
let until = tokio::time::Instant::now() + within;
loop {
match tokio::time::timeout_at(until, io.read(&mut buf)).await {
Err(_) => return false,
Ok(Ok(0)) | Ok(Err(_)) => return true,
Ok(Ok(_)) => continue,
}
}
}
const SLOW: Duration = Duration::from_secs(30);
/// ★ RA5-22: **one peer holds at most its share of :443.** Past the per-peer cap its next
/// connection is closed at once; another peer is served meanwhile.
#[tokio::test(flavor = "multi_thread")]
async fn one_peer_holds_at_most_its_share_of_the_door() {
let limits = serve::ConnLimits { total: 1024, per_peer: 2, handshake: SLOW, header_read: SLOW, idle: SLOW };
let (addr, ca) = bounded_door(limits).await;
let one: std::net::IpAddr = "127.0.0.1".parse().unwrap();
let mut held = Vec::new();
for _ in 0..2 {
held.push(raw_from(addr, one).await);
}
tokio::time::sleep(Duration::from_millis(200)).await;
let mut third = raw_from(addr, one).await;
assert!(closed_within(&mut third, Duration::from_secs(3)).await, "past its share a peer's connection is closed at once");
let mut other = tls_from(addr, "127.0.0.2".parse().unwrap(), &ca).await;
use tokio::io::AsyncWriteExt as _;
other.write_all(b"GET /health HTTP/1.1\r\nHost: door.test\r\nConnection: close\r\n\r\n").await.unwrap();
let mut page = String::new();
let _ = tokio::io::AsyncReadExt::read_to_string(&mut other, &mut page).await;
assert!(page.starts_with("HTTP/1.1 200") && page.ends_with("ok\n"), "another peer is served: {page}");
drop(held);
tokio::time::sleep(Duration::from_millis(300)).await;
let mut again = tls_from(addr, one, &ca).await;
again.write_all(b"GET /health HTTP/1.1\r\nHost: door.test\r\nConnection: close\r\n\r\n").await.unwrap();
let mut page = String::new();
let _ = tokio::io::AsyncReadExt::read_to_string(&mut again, &mut page).await;
assert!(page.starts_with("HTTP/1.1 200"), "a peer that let go is served again: {page}");
}
/// ★ RA5-22: **a silent connection is closed**: after its handshake, no request and no byte
/// for the idle time.
#[tokio::test(flavor = "multi_thread")]
async fn a_silent_connection_is_closed() {
let limits = serve::ConnLimits { total: 1024, per_peer: 64, handshake: SLOW, header_read: SLOW, idle: Duration::from_secs(1) };
let (addr, ca) = bounded_door(limits).await;
let mut tls = tls_from(addr, "127.0.0.1".parse().unwrap(), &ca).await;
assert!(closed_within(&mut tls, Duration::from_secs(5)).await, "a connection that says nothing is closed");
}
/// ★ RA5-22: **a request's head has a deadline**: a client that trickles header lines (each
/// within the idle time) is closed when the head is not in after the header-read time.
#[tokio::test(flavor = "multi_thread")]
async fn a_trickled_request_head_is_cut_off() {
use tokio::io::AsyncWriteExt as _;
let limits = serve::ConnLimits { total: 1024, per_peer: 64, handshake: SLOW, header_read: Duration::from_secs(1), idle: Duration::from_secs(5) };
let (addr, ca) = bounded_door(limits).await;
let tls = tls_from(addr, "127.0.0.1".parse().unwrap(), &ca).await;
let (mut rd, mut wr) = tokio::io::split(tls);
// A32-14 test-only: the slow client, until the door hangs up.
let trickle = tokio::spawn(async move {
let _ = wr.write_all(b"GET /health HTTP/1.1\r\nHost: door.test\r\n").await;
for n in 0..40 {
tokio::time::sleep(Duration::from_millis(250)).await;
if wr.write_all(format!("X-Slow-{n}: x\r\n").as_bytes()).await.is_err() {
return;
}
}
});
assert!(closed_within(&mut rd, Duration::from_secs(6)).await, "the head did not arrive in time: the door hangs up");
trickle.abort();
}
// ─── WWW-CANON: the bare `<site>` is a 301 to `www.<site>`, the one origin ──────
/// One request over TLS to `addr` with SNI and `Host` `name`, trusting `ca_pem`: the whole raw answer.
async fn https_ask(addr: SocketAddr, name: &str, ca_pem: &str, method: &str, path: &str) -> String {
https_ask_as(addr, name, ca_pem, method, path, "application/json").await
}
/// [`https_ask`] with the request's `Content-Type`.
async fn https_ask_as(addr: SocketAddr, name: &str, ca_pem: &str, method: &str, path: &str, content_type: &str) -> String {
use rustls::pki_types::pem::PemObject as _;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
let mut roots = rustls::RootCertStore::empty();
for c in rustls::pki_types::CertificateDer::pem_slice_iter(ca_pem.as_bytes()) {
roots.add(c.unwrap()).unwrap();
}
let cfg = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())).with_safe_default_protocol_versions().unwrap().with_root_certificates(roots).with_no_client_auth();
let tcp = tokio::net::TcpStream::connect(addr).await.unwrap();
let sni = rustls::pki_types::ServerName::try_from(name.to_string()).unwrap();
let mut tls = tokio_rustls::TlsConnector::from(Arc::new(cfg)).connect(sni, tcp).await.expect("a valid TLS handshake for the name");
tls.write_all(format!("{method} {path} HTTP/1.1\r\nHost: {name}\r\nContent-Length: 2\r\nContent-Type: {content_type}\r\nConnection: close\r\n\r\n{{}}").as_bytes()).await.unwrap();
let mut out = String::new();
let _ = tls.read_to_string(&mut out).await;
out
}
/// One plain request to :80 at `addr` with `Host` `host`: the whole raw answer.
async fn http_ask(addr: SocketAddr, host: &str, path: &str) -> String {
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
let mut tcp = tokio::net::TcpStream::connect(addr).await.unwrap();
tcp.write_all(format!("GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n").as_bytes()).await.unwrap();
let mut out = String::new();
let _ = tcp.read_to_string(&mut out).await;
out
}
/// The status line's code and the `Location` header of a raw answer.
fn status_and_location(raw: &str) -> (u16, Option<String>) {
let code = raw.split_whitespace().nth(1).and_then(|c| c.parse().ok()).unwrap_or(0);
let location = raw.lines().take_while(|l| !l.is_empty()).find_map(|l| l.split_once(':').filter(|(k, _)| k.eq_ignore_ascii_case("location")).map(|(_, v)| v.trim().to_string()));
(code, location)
}
/// A started front for `example.test`, `www.example.test` and `git.example.test` (one fed
/// certificate for all three, ACME off), its face counting every passkey ceremony it starts;
/// the :80 and :443 addresses, the certificate to trust, the counter.
async fn www_front(dir: &std::path::Path) -> (SocketAddr, SocketAddr, String, Arc<Mutex<u32>>) {
let (l80, l443, a80, a443) = listeners().await;
let (chain, key) = certs::tests::minted(&["example.test", "www.example.test", "git.example.test"], 80);
let feed = BTreeMap::from([
(FEED_WEB_HOSTS.to_string(), "example.test,www.example.test,git.example.test".to_string()),
(certs::fed_cert_key("example.test"), chain.clone()),
(certs::fed_key_key("example.test"), key),
]);
let hooks = Hooks { acme_on: false, ..Hooks::default() };
let web = start_on_hooked(dir, "example.test".into(), Arc::new(move || feed.clone()), l80, l443, hooks, here());
let ceremonies = Arc::new(Mutex::new(0u32));
let started = |c: Arc<Mutex<u32>>| {
move || async move {
*c.lock().unwrap() += 1;
"a passkey ceremony started"
}
};
web.app.set(
Router::new()
.route("/auth/passkey/login/start", post(started(Arc::clone(&ceremonies))))
.route("/auth/passkey/register/start", post(started(Arc::clone(&ceremonies))))
.route("/-/auth/passkey/login/start", post(started(Arc::clone(&ceremonies))))
.route("/callback/{vendor}", post(|| async { "a vendor's webhook" }))
.fallback(|| async { "the console" }),
);
until("the fed certificate", || web.resolver.any().then_some(())).await;
// The look that served the certificate set the names on the same pass; give it its tick.
tokio::time::sleep(Duration::from_millis(300)).await;
(a80, a443, chain, ceremonies)
}
/// ★ WWW-CANON (owner 2026-10-30: the sites are www.gunnar.rs and www.holger.rs): **every page
/// request to the bare `<site>` is a 301 to `https://www.<site>` with the same path and query**,
/// over the bare name's own certificate (the redirect is valid HTTPS), and the face serves
/// nothing there.
#[tokio::test(flavor = "multi_thread")]
async fn the_bare_site_is_a_301_to_www_with_its_path_and_query() {
let dir = tempfile::tempdir().unwrap();
let (_a80, a443, ca, _) = www_front(dir.path()).await;
for path in ["/", "/nordisk/gunnar/issues?q=open%20bug&page=2", "/login?next=%2Fadmin", "/health", "/llms.txt"] {
for method in ["GET", "HEAD"] {
let raw = https_ask(a443, "example.test", &ca, method, path).await;
assert_eq!(status_and_location(&raw), (301, Some(format!("https://www.example.test{path}"))), "{method} {path} on the bare site: {raw}");
assert!(!raw.contains("the console"), "the face serves nothing on the bare site: {raw}");
}
}
}
/// ★ WWW-CANON: **www serves the site, and the other names are unchanged** — the face, no
/// redirect; a bare name whose `www.` is not served is not redirected either.
#[tokio::test(flavor = "multi_thread")]
async fn www_and_the_other_names_serve_the_site() {
let dir = tempfile::tempdir().unwrap();
let (_a80, a443, ca, _) = www_front(dir.path()).await;
for name in ["www.example.test", "git.example.test"] {
let raw = https_ask(a443, name, &ca, "GET", "/nordisk/gunnar/issues?q=x").await;
assert_eq!(status_and_location(&raw), (200, None), "{name}: {raw}");
assert!(raw.ends_with("the console"), "{name}: {raw}");
}
}
/// ★ WWW-CANON: **a passkey ceremony is never started on the bare host** — the console's origin
/// is www: every passkey door there is a 308 to the same door on www (the method and body
/// kept, so a client that re-sends lands on the canonical origin) and the face's handler is
/// never reached on the bare host. The same doors on www start it.
#[tokio::test(flavor = "multi_thread")]
async fn a_passkey_ceremony_is_never_started_on_the_bare_host() {
let dir = tempfile::tempdir().unwrap();
let (_a80, a443, ca, ceremonies) = www_front(dir.path()).await;
for path in ["/auth/passkey/login/start", "/auth/passkey/register/start", "/-/auth/passkey/login/start"] {
let raw = https_ask(a443, "example.test", &ca, "POST", path).await;
assert_eq!(status_and_location(&raw), (308, Some(format!("https://www.example.test{path}"))), "POST {path} on the bare site: {raw}");
}
assert_eq!(*ceremonies.lock().unwrap(), 0, "no ceremony was started on the bare host");
let raw = https_ask(a443, "www.example.test", &ca, "POST", "/auth/passkey/login/start").await;
assert_eq!(status_and_location(&raw).0, 200, "{raw}");
assert_eq!(*ceremonies.lock().unwrap(), 1, "www starts it");
}
/// ★ WWW-CANON: **machines on the bare host are not redirected** — a gRPC call (a CLI dialing
/// `https://<site>`) and a payment vendor's webhook (`/callback/<vendor>`, a vendor that does
/// not follow redirects) reach what they reached before.
#[tokio::test(flavor = "multi_thread")]
async fn grpc_and_vendor_webhooks_on_the_bare_host_are_not_redirected() {
let dir = tempfile::tempdir().unwrap();
let (_a80, a443, ca, _) = www_front(dir.path()).await;
let raw = https_ask_as(a443, "example.test", &ca, "POST", "/gunnar.v1.Repos/List", "application/grpc").await;
assert_eq!(status_and_location(&raw), (200, None), "a gRPC call: {raw}");
let raw = https_ask(a443, "example.test", &ca, "POST", "/callback/stripe").await;
assert_eq!(status_and_location(&raw), (200, None), "a webhook: {raw}");
assert!(raw.ends_with("a vendor's webhook"), "{raw}");
}
/// ★ WWW-CANON on :80: `http://<site>/…` goes straight to `https://www.<site>/…` (301, path
/// and query kept); www's :80 redirect is unchanged (308 to its own https).
#[tokio::test(flavor = "multi_thread")]
async fn port_80_sends_the_bare_site_straight_to_www() {
let dir = tempfile::tempdir().unwrap();
let (a80, _a443, _ca, _) = www_front(dir.path()).await;
let raw = http_ask(a80, "example.test", "/a/b?c=d").await;
assert_eq!(status_and_location(&raw), (301, Some("https://www.example.test/a/b?c=d".into())), "{raw}");
let raw = http_ask(a80, "www.example.test", "/a/b?c=d").await;
assert_eq!(status_and_location(&raw), (308, Some("https://www.example.test/a/b?c=d".into())), "{raw}");
}
/// ★ WWW-CANON: **the canonical name** a product builds its URLs and its console's origin
/// from: `www.<first>` when the feed serves it, else the first host.
#[test]
fn the_canonical_name_is_www_of_the_first_host_when_served() {
let cfg = |hosts: &str| WebConfig::from("x", &BTreeMap::from([(FEED_WEB_HOSTS.to_string(), hosts.to_string())]));
assert_eq!(cfg("gunnar.rs,www.gunnar.rs,git.gunnar.rs").name(), "gunnar.rs", "the certificate's name and the relying party stay the bare site");
assert_eq!(cfg("gunnar.rs,www.gunnar.rs,git.gunnar.rs").canonical(), "www.gunnar.rs");
assert_eq!(cfg("acme.gunnar.rs,www.acme.gunnar.rs,git.acme.example").canonical(), "www.acme.gunnar.rs");
assert_eq!(cfg("holger.mock.invalid").canonical(), "holger.mock.invalid", "no www served: the first host");
assert_eq!(cfg("www.example.com").canonical(), "www.example.com");
}
/// ★ WWW-CANON, pure: which names are sent to their `www.` — any case, a port, a trailing dot;
/// never `www.` itself, never a name whose `www.` is not served, never a name not served.
#[test]
fn which_names_are_sent_to_www() {
let hosts: Vec<String> = ["example.test", "www.example.test", "git.example.test"].map(String::from).to_vec();
assert_eq!(serve::www_of("example.test", &hosts).as_deref(), Some("www.example.test"));
assert_eq!(serve::www_of("Example.Test:443", &hosts).as_deref(), Some("www.example.test"));
assert_eq!(serve::www_of("example.test.", &hosts).as_deref(), Some("www.example.test"));
assert_eq!(serve::www_of("www.example.test", &hosts), None, "www is the site");
assert_eq!(serve::www_of("git.example.test", &hosts), None, "git.<site> stays as it is");
assert_eq!(serve::www_of("other.test", &hosts), None, "not served");
assert_eq!(serve::www_of("holger.mock.invalid", &["holger.mock.invalid".to_string()]), None, "no www served: served as itself");
let mut grpc = axum::http::HeaderMap::new();
grpc.insert(header::CONTENT_TYPE, "application/grpc+proto".parse().unwrap());
assert!(serve::stays_on_bare(&grpc, "/gunnar.v1.Repos/List"));
assert!(serve::stays_on_bare(&axum::http::HeaderMap::new(), "/callback/stripe/gunnar"));
assert!(!serve::stays_on_bare(&axum::http::HeaderMap::new(), "/auth/passkey/login/start"));
}