server/lib/tests/door_checksum_and_reserved_paths.rs
//! The HTTP/OCI door's request-shape guarantees, over a LIVE gateway.
//!
//! Two things the unit tests around `verify_upload_checksum` / `route_key` cannot
//! prove on their own, because both depend on the *handler* wiring them up in the
//! right order:
//!
//! * **checksum-verify-on-deploy** — an `X-Checksum-Sha256` / `-Sha512` header
//! that does not match the received bytes must refuse the write with 400 and
//! leave NOTHING stored. The digests are captured before the body is consumed,
//! so a regression there fails open (the upload lands unverified) and is
//! invisible to a pure-function test.
//! * **the reserved `/-/` namespace** — every `/-/…` path keys on the segment
//! `-`, so a meta door that is not configured answers the same 404 as any
//! unknown path. Probing must not reveal whether a marketplace is mounted.
//!
//! Offline, no auth configured (the default open-read posture), one temp store.
use std::io::Write;
use traits::test_support::hold_port;
use holger_server_lib::{read_ron_config, wire_holger, Holger};
struct Running {
_data: tempfile::TempDir,
_cfg: tempfile::NamedTempFile,
base: String,
}
/// One writable `raw` repository behind a cleartext HTTP door on `http`.
fn start(grpc: u16, http: u16) -> Running {
let data = tempfile::tempdir().unwrap();
let ron = format!(
r#"(
repositories: [
(ron_name: "raw", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "default")),
ron_data_dir: Some("{data}")),
],
exposed_endpoints: [
(ron_name: "default", ron_url: "127.0.0.1:{grpc}", ron_http_url: Some("127.0.0.1:{http}")),
],
storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{data}") ],
)"#,
data = data.path().display(),
);
let mut cfg = tempfile::NamedTempFile::new().unwrap();
cfg.write_all(ron.as_bytes()).unwrap();
let mut holger: Holger = read_ron_config(cfg.path()).unwrap();
holger.instantiate_backends().unwrap();
wire_holger(&mut holger).unwrap();
holger.start().unwrap();
Running {
_data: data,
_cfg: cfg,
base: format!("http://127.0.0.1:{http}"),
}
}
async fn wait_healthy(c: &reqwest::Client, base: &str) {
for _ in 0..100 {
if let Ok(r) = c.get(format!("{base}/healthz")).send().await {
if r.status() == 200 {
return;
}
}
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
}
panic!("gateway never became healthy at {base}");
}
fn sha256_hex(b: &[u8]) -> String {
use sha2::{Digest, Sha256};
hex::encode(Sha256::digest(b))
}
fn sha512_hex(b: &[u8]) -> String {
use sha2::{Digest, Sha512};
hex::encode(Sha512::digest(b))
}
/// **A digest the client advertised must match the bytes that arrived, or the
/// write is refused and nothing is stored.**
///
/// RED-when-broken: if the headers were read after the body is consumed (or the
/// gate moved below the backend dispatch), a corrupted upload would land and
/// only the response would be wrong — an integrity guarantee that silently
/// stopped guaranteeing.
#[tokio::test(flavor = "multi_thread")]
async fn a_mismatched_checksum_refuses_the_write_and_stores_nothing() {
let r = start(hold_port(), hold_port());
let c = tls_roots(reqwest::Client::builder())
.build()
.expect("reqwest client");
wait_healthy(&c, &r.base).await;
const GOOD: &[u8] = b"the artifact bytes, exactly as advertised";
// The truth: a correct digest is accepted and the bytes come back.
let put = c
.put(format!("{}/raw/good.bin", r.base))
.header("X-Checksum-Sha256", sha256_hex(GOOD))
.body(GOOD)
.send()
.await
.unwrap();
assert!(
put.status().is_success(),
"a verified upload must be stored, got {}",
put.status()
);
let got = c
.get(format!("{}/raw/good.bin", r.base))
.send()
.await
.unwrap();
assert_eq!(got.status(), 200);
assert_eq!(
got.bytes().await.unwrap().as_ref(),
GOOD,
"byte-identical round trip"
);
// Uppercase hex is the same digest (the compare is case-insensitive), and so
// is a differently-cased header name.
let put = c
.put(format!("{}/raw/upper.bin", r.base))
.header("x-checksum-SHA256", sha256_hex(GOOD).to_uppercase())
.body(GOOD)
.send()
.await
.unwrap();
assert!(
put.status().is_success(),
"uppercase hex is the same digest, got {}",
put.status()
);
// The lie: a digest for OTHER bytes refuses the write.
let put = c
.put(format!("{}/raw/corrupt.bin", r.base))
.header(
"X-Checksum-Sha256",
sha256_hex(b"what the client MEANT to send"),
)
.body(GOOD)
.send()
.await
.unwrap();
assert_eq!(put.status(), 400, "a mismatched sha256 must be refused");
assert!(
put.text()
.await
.unwrap()
.contains("sha256 checksum mismatch"),
"the refusal says which digest failed"
);
// …and nothing was stored under that path.
let got = c
.get(format!("{}/raw/corrupt.bin", r.base))
.send()
.await
.unwrap();
assert_eq!(
got.status(),
404,
"a refused upload must leave nothing behind"
);
// sha512 is verified on the same terms, and a correct sha256 does not excuse
// a wrong sha512.
let put = c
.put(format!("{}/raw/corrupt512.bin", r.base))
.header("X-Checksum-Sha256", sha256_hex(GOOD))
.header("X-Checksum-Sha512", sha512_hex(b"different bytes entirely"))
.body(GOOD)
.send()
.await
.unwrap();
assert_eq!(put.status(), 400, "a mismatched sha512 must be refused too");
assert!(put
.text()
.await
.unwrap()
.contains("sha512 checksum mismatch"));
let got = c
.get(format!("{}/raw/corrupt512.bin", r.base))
.send()
.await
.unwrap();
assert_eq!(got.status(), 404);
// Verification stays OPT-IN: no header, no gate.
let put = c
.put(format!("{}/raw/unverified.bin", r.base))
.body(GOOD)
.send()
.await
.unwrap();
assert!(
put.status().is_success(),
"an unadvertised upload is still accepted"
);
}
/// The unauthenticated door surface: the probes answer, `/metrics` scrapes, and
/// **every `/-/…` path that is not a mounted meta door answers a plain 404** —
/// including `/-/marketplace/*` on a server with no `marketplace` block, so
/// probing cannot tell a server that sells subscriptions from one that does not.
///
/// RED-when-broken: a `/-/…` path that fell through to routing would be looked
/// up as a repository called `-`; a marketplace path that answered anything but
/// 404 would advertise the fulfilment surface to the open internet.
#[tokio::test(flavor = "multi_thread")]
async fn the_probes_answer_and_the_reserved_namespace_reveals_nothing() {
let r = start(hold_port(), hold_port());
let c = tls_roots(reqwest::Client::builder())
.build()
.expect("reqwest client");
wait_healthy(&c, &r.base).await;
for probe in ["/healthz", "/readyz"] {
let resp = c.get(format!("{}{probe}", r.base)).send().await.unwrap();
assert_eq!(resp.status(), 200, "{probe}");
assert_eq!(resp.text().await.unwrap(), "ok", "{probe}");
}
// The OCI version probe, which clients hit before they authenticate.
for probe in ["/v2", "/v2/"] {
let resp = c.get(format!("{}{probe}", r.base)).send().await.unwrap();
assert_eq!(resp.status(), 200, "{probe}");
assert_eq!(
resp.headers()
.get("docker-distribution-api-version")
.and_then(|v| v.to_str().ok()),
Some("registry/2.0"),
"{probe}"
);
}
// Prometheus scrape (the default profile serves it on any interface).
let metrics = c.get(format!("{}/metrics", r.base)).send().await.unwrap();
assert_eq!(metrics.status(), 200);
assert!(
metrics
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.starts_with("text/plain; version=0.0.4"),
"the Prometheus text exposition content type"
);
// No `marketplace` block ⇒ every fulfilment path is a plain 404, the same
// answer as any unknown reserved path. Nothing distinguishes them.
let landing = c
.get(format!("{}/-/marketplace/landing?token=probe", r.base))
.send()
.await
.unwrap();
assert_eq!(
landing.status(),
404,
"an unmounted marketplace reveals nothing"
);
let callback = c
.get(format!("{}/-/marketplace/callback?code=x&state=y", r.base))
.send()
.await
.unwrap();
assert_eq!(callback.status(), 404);
let webhook = c
.post(format!("{}/-/marketplace/webhook", r.base))
.body(r#"{"id":"op-1","action":"Unsubscribe"}"#)
.send()
.await
.unwrap();
assert_eq!(
webhook.status(),
404,
"the lifecycle door is not mounted either"
);
// An unknown reserved path is a 404 too — it is never looked up as a
// repository, and never served by one.
for path in ["/-/", "/-/nope", "/-/marketplace", "/-/search/extra"] {
let resp = c.get(format!("{}{path}", r.base)).send().await.unwrap();
assert_eq!(resp.status(), 404, "{path}");
}
// The meta doors that ARE mounted still answer on the open-read default.
let search = c
.get(format!("{}/-/search?name=nothing-is-stored-here", r.base))
.send()
.await
.unwrap();
assert_eq!(
search.status(),
200,
"cross-repo search is an open read by default"
);
assert!(
search.text().await.unwrap().starts_with('{'),
"the search door answers JSON"
);
// An unknown repository is a 404 that names no path and no backend.
let miss = c
.get(format!("{}/no-such-repo/x", r.base))
.send()
.await
.unwrap();
assert_eq!(miss.status(), 404);
assert_eq!(miss.text().await.unwrap(), "Unknown repository");
}
/// reqwest 0.13 with reqwest 0.12 `rustls-tls` behaviour (2026-10-02): the ring
/// provider and the bundled Mozilla roots (webpki), not 0.13's aws-lc-rs + OS
/// trust store. Roots added later on the builder are merged in, as in 0.12.
#[allow(dead_code)]
fn tls_roots(b: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
let _ = rustls::crypto::ring::default_provider().install_default();
b.tls_certs_only(
webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.filter_map(|c| reqwest::Certificate::from_der(c).ok()),
)
}