browser-tests/src/rig.rs
//! **The rig**: a real holger, the shared fake ONE box, and the console as holger's front
//! serves it, on loopback.
//!
//! * holger: holger-server-lib in this process, booted from holger-recipe's
//! `estate_ron::medium()` (the declaration every estate ships: the intake chain, two-person
//! approval, the audit chain, the duties) with its paths in a temporary directory, the scan
//! in process, and the passkey plane a paired box adds from its feed, at `localhost`.
//! * the ONE box and Stripe: monetize-site-buy `testkit::FakeOneBox`.
//! * the console: holger-estate-front `console::router`, the very router the front serves on
//! :443 (holger-ui-server's bundle and pass-through, the buy pages' shared scripts, and the
//! shared relay merged in front), over the bundle [`crate::bundle::dist`] built.
//!
//! The browser reaches the console at `http://localhost:<port>`, a secure context, so the
//! passkey ceremonies run against the browser's virtual authenticator.
use std::collections::BTreeMap;
use std::io::Write as _;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use holger_estate_front::buy::{self, ShopFeed};
use holger_estate_front::console::Wiring;
use holger_recipe::estate_ron;
use holger_server_lib::auth::AuthMethodConfig;
use holger_server_lib::Holger;
use monetize_site_buy::testkit::FakeOneBox;
/// holger's console bearer (the estate's static token, subject `console`).
pub const CONSOLE: &str = "console-bearer-of-sufficient-length-0123456789";
/// The site's own bearer at the ONE box.
pub const SITE_BEARER: &str = "holger-site-bearer-for-browser-tests";
/// The downstream connector [`Rig::start_with_graph`] declares (`sync.downstreams`).
pub const GRAPH_DOWNSTREAM: &str = "lab-holger";
/// The upstream sources [`Rig::start_with_graph`] declares: two upstreams and an upload door.
/// `cloud`'s door is a closed loopback port: its mirror never completes a cycle, as the board
/// must then say. It mirrors into the raw `bundles`: the estate's `crates` is the chain's cargo
/// door (holger c6ef70be), which holds nothing of its own, so a mirror into it is refused at
/// boot.
pub const GRAPH_SOURCES: &str = r#" sources: [
(name: "crates-io", kind: crates, serves: ["crates"]),
(name: "cloud", kind: holger, url: "http://127.0.0.1:9", repositories: [(ron_from: "crates", ron_to: "bundles")]),
(name: "team", kind: upload),
],
"#;
/// The names the graph's board must draw: each source and the downstream.
pub const GRAPH_ROWS: [&str; 4] = ["crates-io", "cloud", "team", GRAPH_DOWNSTREAM];
/// The wheel the console walks through the intake chain.
/// The front [`Rig::pair_with_front`] names, and the names it reports ([`Rig::front_reports`]).
pub const FRONT: &str = "front";
pub const FRONT_CONSOLE_HOST: &str = "www.holger.test";
pub const FRONT_REPO_HOST: &str = "repo.holger.test";
/// The path of holger's flow drawing, as the console asks for it (gRPC-web, `/grpc/…`).
pub const FLOW_SVG: &str = "/holger.v1.AdminService/FlowSvg";
/// What [`Rig::refuse_flow`] has the console answer each `FlowSvg` ask with (gRPC
/// `UNAVAILABLE`), as a holger that is restarting would.
pub const FLOW_DOWN: &str = "holger is restarting";
pub const WHEEL: &str = "demo_pkg-1.0.0-py3-none-any.whl";
/// **One running rig.** Dropped at the end of a test (holger's threads end with the process).
pub struct Rig {
pub data: tempfile::TempDir,
pub holger: Holger,
pub fake: FakeOneBox,
/// `http://localhost:<port>`: the console, as the browser reaches it.
pub base: String,
/// holger's HTTP door, `http://127.0.0.1:<port>` (the harness's own asks).
pub door: String,
/// How long the console holds `POST …/passkey/register/start` before passing it on, in
/// milliseconds ([`Rig::slow_register_start`]); 0 passes it at once.
slow_start_ms: std::sync::Arc<std::sync::atomic::AtomicU64>,
slow_login_ms: std::sync::Arc<std::sync::atomic::AtomicU64>,
/// [`Rig::sell_nothing`]: the console answers `GET /-/signup` as a private instance's
/// holger does (no shop sells here).
sells_nothing: std::sync::Arc<std::sync::atomic::AtomicBool>,
/// [`Rig::offer_set`]: the one dependency set the set doors answer with (SEC-REVIEW D4).
set: std::sync::Arc<std::sync::Mutex<Option<OfferedSet>>>,
/// Every `FlowSvg` ask the console passed on, and whether it is refused ([`Rig::refuse_flow`]).
flow: FlowAsks,
_cfg: tempfile::NamedTempFile,
}
/// **The console's asks for the flow drawing** (F14): the zoom each one named, in order, and
/// the switch that has them refused.
#[derive(Clone, Default)]
struct FlowAsks {
asked: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
refused: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
/// The zoom a `FlowSvg` ask names: `FlowSvgRequest.open` (field 1, a short string), read off
/// its gRPC-web frame (a flag byte and a four-byte length, then the message; nothing open is
/// an empty message).
fn asked_open(frame: &[u8]) -> String {
match frame.get(5..).unwrap_or_default() {
[0x0a, len, rest @ ..] if *len < 0x80 && usize::from(*len) <= rest.len() => String::from_utf8_lossy(&rest[..usize::from(*len)]).into_owned(),
_ => String::new(),
}
}
/// **The dependency set the rig's set doors offer** ([`Rig::offer_set`]). The estate's medium
/// declares no pull-through source, so holger itself has no set to show; the rig answers
/// `GET /-/intake/sets` and `POST /-/intake/set/approve|refuse` in holger's shape and on
/// holger's rule (holger-server-lib `pull_intake::depset`, SEC-REVIEW D4): a vote naming
/// another revision than the set has now is refused with 409 and holger's
/// `holger-depset-4` words, and nothing is cast.
#[derive(Clone, Debug)]
pub struct OfferedSet {
pub id: String,
pub revision: String,
/// The votes cast, as `(verb, revision named)`.
pub votes: Vec<(String, String)>,
}
impl OfferedSet {
fn sets_body(&self) -> String {
serde_json::json!({
"sets": [{
"id": self.id, "revision": self.revision, "source": "pypi", "ecosystem": "python",
"root": "app==1.0", "at": 1, "status": "awaiting-approval",
"summary": "2 of 2 member(s) wait for approval; 0 of 2 approver(s) approved the whole set",
"members": [
{"name": "pypi/app-1.0-py3-none-any.whl", "role": "requested", "via": "", "state": "policy-checked", "sha256": "aa", "approvals": 0, "approvers": [], "reason": "", "warnings": []},
{"name": "pypi/dep-1.0-py3-none-any.whl", "role": "dependency", "via": "app==1.0", "state": "policy-checked", "sha256": "bb", "approvals": 0, "approvers": [], "reason": "", "warnings": []}
],
"blockers": [], "holds": [], "approvals_required": 2, "approvers": [], "awaiting": 2, "available": 0, "notes": []
}],
"approvals_required": 2
})
.to_string()
}
/// holger's answer to one vote: `(status, body)`.
fn judge(&mut self, verb: &str, query: &str) -> (u16, String) {
let named = query
.split('&')
.find_map(|p| p.strip_prefix("revision="))
.unwrap_or("")
.to_string();
let label = format!("dependency set {} (app==1.0)", self.id);
if named != self.revision {
let said = if named.is_empty() { "none".to_string() } else { format!("'{named}'") };
return (
409,
holger_server_lib::errcode::DEPSET_STALE.tag(format!(
"{label} is at revision {} and the vote names {said}: read the set again and judge what it shows",
self.revision
)),
);
}
self.votes.push((verb.to_string(), named));
(200, serde_json::json!({"id": self.id, "status": "awaiting-approval", "moved": 2, "message": format!("owner {verb}d {label}: 2 vote(s) cast")}).to_string())
}
}
/// The path whose answer [`Rig::slow_register_start`] holds back.
pub const REGISTER_START: &str = "/-/auth/passkey/register/start";
/// The sign-in's start, which the rig can hold back too (RA2-6).
pub const LOGIN_START: &str = "/-/auth/passkey/login/start";
/// holger's sign-up door: what the ONE shop sells on this root ([`Rig::sell_nothing`]).
pub const SIGNUP_DOOR: &str = "/-/signup";
/// What a private instance's holger answers on [`SIGNUP_DOOR`]: no shop, nothing sold.
pub const NOTHING_SOLD: &str = r#"{"account_url":"","private_offered":false,"private_url":"","signed_in":false,"account":"","plan":"","grow_account_url":"","instance":"","grow_instance_url":"","words":""}"#;
/// **The doors the rig declares for holger** (HUI-PORT): the gRPC door, the HTTP door and the
/// replication door, each a loopback port the rig HOLDS (a bound listener) from the moment it
/// picks it until [`Doors::release`], just before holger binds them. The replication door is
/// declared at its own held port (`replication_door.ron_bind`), never left at holger's default,
/// the gRPC port + 1, which nothing held.
struct Doors {
grpc: u16,
http: u16,
replication: u16,
held: Vec<std::net::TcpListener>,
}
impl Doors {
fn hold() -> Doors {
let held: Vec<std::net::TcpListener> = (0..3).map(|_| std::net::TcpListener::bind("127.0.0.1:0").expect("a loopback port for holger")).collect();
let port = |i: usize| held[i].local_addr().unwrap().port();
Doors { grpc: port(0), http: port(1), replication: port(2), held }
}
/// The declaration `ron` with its doors moved onto the held ports.
fn declare(&self, ron: &str) -> String {
let default = "replication_door: Some((ron_bind: None,";
assert!(ron.contains(default), "the declaration has a replication door to move:\n{ron}");
ron.replace("\"0.0.0.0:50051\"", &format!("\"127.0.0.1:{}\"", self.grpc))
.replace("\"0.0.0.0:8443\"", &format!("\"127.0.0.1:{}\"", self.http))
.replace(default, &format!("replication_door: Some((ron_bind: Some(\"127.0.0.1:{}\"),", self.replication))
}
/// Let go of every held port, right before holger opens its doors on them.
fn release(self) {
drop(self.held);
}
}
/// The intel database a box gets by import, installed by hand: no advisory, a fresh snapshot.
fn install_intel(dir: &Path) {
use holger_intel::chrono::Utc;
use holger_intel::{AdvisorySet, InstalledIntel, LocalIntelDb};
let set = AdvisorySet { schema: holger_intel::set::SET_SCHEMA.into(), generated_at: Utc::now(), sources: vec![], advisories: vec![] };
let db = LocalIntelDb::open(dir);
let bytes = set.to_json_bytes().unwrap();
db.stage_generation(1, &bytes).unwrap();
db.activate(&InstalledIntel {
schema: holger_intel::db::INSTALLED_SCHEMA.into(),
bundle_id: "holger-intel-1".into(),
sequence: 1,
supplier: holger_intel::DEFAULT_SUPPLIER.into(),
snapshot_at: Utc::now(),
imported_at: Utc::now(),
candidate_digest: "00".into(),
release_signer_fingerprint: "00".into(),
advisories_file: LocalIntelDb::generation_file(1),
advisories_sha256: holger_server_lib::security::blob_digest_hex(&bytes),
advisories: 0,
})
.unwrap();
}
impl Rig {
/// **Start everything.** The console's port is taken first: holger's passkey origin names it.
pub async fn start() -> Rig {
Self::start_with(None).await
}
/// **Start everything, the box an upstream** with a `sync:` block (Holger v2.0 H4): its
/// sync key minted under the rig's data, one enrolled downstream `downstream` (its key
/// minted here too; nothing ever acknowledges for it) and an acknowledgement deadline of
/// `deadline_secs`. The board door (`GET /-/sync/board`) then serves this upstream's board.
pub async fn start_upstream_of(downstream: &str, deadline_secs: u64) -> Rig {
Self::start_with(Some((downstream.to_string(), deadline_secs))).await
}
/// **Start everything, the box declaring a graph** (HBOARD-1): the estate's declaration
/// plus `sources:` (two upstream sources, `crates-io` served as fetched into `crates` and
/// the upstream holger `cloud` mirrored into `crates`, and the upload door `team`) and a
/// `sync:` block with ONE downstream connector, [`GRAPH_DOWNSTREAM`]. The board must
/// draw each of them.
pub async fn start_with_graph() -> Rig {
Self::start_inner(Some((GRAPH_DOWNSTREAM.to_string(), 3600)), true, None).await
}
/// **Start everything, holger redrawing its flow every `secs`** (F14): the estate's
/// declaration with `flow: (refresh_secs: secs)`, the interval holger's model hands the
/// console (`0` is never).
pub async fn start_with_flow_refresh(secs: u32) -> Rig {
Self::start_inner(None, false, Some(secs)).await
}
async fn start_with(sync: Option<(String, u64)>) -> Rig {
Self::start_inner(sync, false, None).await
}
async fn start_inner(sync: Option<(String, u64)>, graph: bool, flow_refresh: Option<u32>) -> Rig {
let dist = crate::bundle::dist().to_path_buf();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("a loopback port for the console");
let port = listener.local_addr().unwrap().port();
let base = format!("http://localhost:{port}");
let data = tempfile::tempdir().unwrap();
let doors = Doors::hold();
let (grpc, http) = (doors.grpc, doors.http);
let d = data.path().display().to_string();
let ron = estate_ron::medium()
.replace(&format!("\"{}/", holger_recipe::packaging::BOX_HOLGER_DATA), &format!("\"{d}/holger/"))
.replace(&format!("\"{}/", estate_ron::BOX_APPLIANCE_ROOT), &format!("\"{d}/"))
.replace(&format!("\"{}/", estate_ron::RELEASE_ROOT), &format!("\"{d}/"))
// The scan runs in this process; the shipped declaration keeps the sandbox required.
.replace("rules: (sandbox: required),", "rules: (sandbox: off),")
// No clamd on a test rig: the malware scan is the sandbox's, and the sandbox is off.
.replace("clamd.sock\"), required: true)", "clamd.sock\"), required: false)");
let ron = doors.declare(&ron);
assert!(!ron.contains("\"/data"), "every path moved:\n{ron}");
let ron = match &sync {
None => ron,
Some((downstream, deadline)) => {
use holger_server_lib::sync::protocol::SyncKey;
let keys = data.path().join("sync-keys");
std::fs::create_dir_all(&keys).unwrap();
let own = keys.join("cloud.key");
SyncKey::open_or_mint(&own).expect("the upstream's sync key");
let theirs = SyncKey::open_or_mint(&keys.join(format!("{downstream}.key"))).expect("the downstream's key").public_hex();
let i = ron.rfind(')').expect("the declaration closes");
format!(
"{} sync: Some((name: \"cloud\", key: Some(\"{}\"), downstreams: [(name: \"{downstream}\", key: \"{theirs}\")], ack_deadline_secs: {deadline})),\n{}",
&ron[..i],
own.display(),
&ron[i..]
)
}
};
let ron = if graph {
// `crates-io` serves `crates` as fetched, past the intake chain: since SEC-REVIEW
// P5 (holger 7c6ebb3f) a box whose policy requires a scan refuses that at boot
// (`holger-intake-17`). The graph is about what the board and the Security page
// draw, so this box requires no scan; the estate's own declaration keeps its.
let ron = ron.replacen("scan: Some((required: [vulnerability, sbom],", "scan: Some((required: [],", 1);
let i = ron.rfind(')').expect("the declaration closes");
format!("{}{GRAPH_SOURCES}{}", &ron[..i], &ron[i..])
} else {
ron
};
let ron = match flow_refresh {
Some(secs) => {
assert!(!ron.contains("\n flow: "), "the declaration has a flow block of its own:\n{ron}");
let i = ron.rfind(')').expect("the declaration closes");
format!("{} flow: (refresh_secs: {secs}),\n{}", &ron[..i], &ron[i..])
}
None => ron,
};
write_key_file(&data.path().join(estate_ron::CONSOLE_TOKEN_FILE), CONSOLE);
std::fs::write(data.path().join(estate_ron::KORP_ROOT_STAGED), estate_ron::KORP_ROOT_PEM).unwrap();
install_intel(&data.path().join("holger/intel"));
let mut cfg = tempfile::NamedTempFile::new().unwrap();
cfg.write_all(ron.as_bytes()).unwrap();
let mut holger = holger_server_lib::read_ron_config(cfg.path()).expect("the estate's declaration parses");
holger.resolve_estate(Some("localhost"));
// The passkey plane, as a paired box adds it from the feed's `passkey_rp_id`.
holger.auth.as_mut().expect("the estate declares auth").methods.push(AuthMethodConfig::Passkey {
rp_id: "localhost".into(),
rp_origin: base.clone(),
rp_name: "holger".into(),
session_key_file: None,
bootstrap_until_first_admin: true,
});
holger.instantiate_backends().expect("the backends");
holger_server_lib::wire_holger(&mut holger).expect("holger wires");
// The console's path on this origin, as every estate box declares it (holger-recipe
// `packaging`: `HOLGER_CONSOLE_PATH` = `BOX_CONSOLE_PATH`): holger's front page draws its
// Log in from it. One value for every rig, set before the first holger of the process.
static CONSOLE_PATH: std::sync::Once = std::sync::Once::new();
CONSOLE_PATH.call_once(|| std::env::set_var("HOLGER_CONSOLE_PATH", holger_recipe::packaging::BOX_CONSOLE_PATH));
doors.release();
holger.start().expect("holger starts");
let door = format!("http://127.0.0.1:{http}");
wait_for(&format!("{door}/healthz")).await;
let fake = FakeOneBox::spawn("holger", SITE_BEARER).await;
// A ONE box that checks Holger's tickets, as the real one does: Holger's grow doors need a
// checked, unspent ticket (owner 2026-10-26), and holger mints a fresh one per ask.
fake.verify_tickets();
let feed: BTreeMap<String, String> = [
(buy::FEED_MONETIZE_BEARER, SITE_BEARER.to_string()),
(buy::FEED_CONSOLE_PRODUCT, "holger".to_string()),
(buy::FEED_FLEET, fake.url()),
(buy::FEED_PASSKEY_RP, "localhost".to_string()),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
let shop: ShopFeed = buy::shop_feed_of(&feed).expect("the feed sells here").expect("a usable feed");
let w = Wiring { appliance: Some(format!("127.0.0.1:{grpc}")), door: Some(format!("127.0.0.1:{http}")), token: Some(CONSOLE.into()), shop_bearer: None, shop: Some(shop), refused: Vec::new() };
let app = holger_estate_front::console::router(&w, Some(dist)).expect("the console's router");
// ★ A slow registration start (A29-20): right after paying, the start waits on the box
// and its ONE box; the rig can hold it back as long as a buyer's did.
let slow_start_ms = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
let hold = slow_start_ms.clone();
// ★ And a slow sign-in start (RA2-6): the plain Log in's start, held back the same way.
let slow_login_ms = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
let hold_login = slow_login_ms.clone();
// ★ A root that sells nothing (HOL-LOGIN): a private instance's holger, its sign-up door
// answering no shop.
let sells_nothing = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let nothing = sells_nothing.clone();
// ★ The flow drawing's asks (F14): written down with the zoom each names, and refused
// while the test says so.
let set: std::sync::Arc<std::sync::Mutex<Option<OfferedSet>>> = Default::default();
let offered = set.clone();
let flow = FlowAsks::default();
let flow_asks = flow.clone();
let app = app.layer(axum::middleware::from_fn(move |mut req: axum::extract::Request, next: axum::middleware::Next| {
let private = req.uri().path() == SIGNUP_DOOR && nothing.load(std::sync::atomic::Ordering::SeqCst);
let set_answer = {
let mut guard = offered.lock().unwrap();
match (guard.as_mut(), req.uri().path()) {
(Some(s), "/-/intake/sets") => Some((200, s.sets_body())),
(Some(s), "/-/intake/set/approve") => Some(s.judge("approve", req.uri().query().unwrap_or(""))),
(Some(s), "/-/intake/set/refuse") => Some(s.judge("refuse", req.uri().query().unwrap_or(""))),
_ => None,
}
};
let ms = match req.uri().path() {
REGISTER_START => hold.load(std::sync::atomic::Ordering::SeqCst),
LOGIN_START => hold_login.load(std::sync::atomic::Ordering::SeqCst),
_ => 0,
};
let flow = flow_asks.clone();
async move {
if let Some((status, body)) = set_answer {
use axum::response::IntoResponse as _;
let ctype = if status == 200 { "application/json" } else { "text/plain" };
return (axum::http::StatusCode::from_u16(status).unwrap(), [(axum::http::header::CONTENT_TYPE, ctype)], body).into_response();
}
if req.uri().path().ends_with(FLOW_SVG) {
let (parts, body) = req.into_parts();
let frame = axum::body::to_bytes(body, 1 << 20).await.unwrap_or_default();
flow.asked.lock().unwrap().push(asked_open(&frame));
if flow.refused.load(std::sync::atomic::Ordering::SeqCst) {
use axum::response::IntoResponse as _;
let message = FLOW_DOWN.replace(' ', "%20");
return ([("content-type", "application/grpc-web+proto"), ("grpc-status", "14"), ("grpc-message", message.as_str())], "").into_response();
}
req = axum::extract::Request::from_parts(parts, axum::body::Body::from(frame));
}
if private {
use axum::response::IntoResponse as _;
return ([(axum::http::header::CONTENT_TYPE, "application/json")], NOTHING_SOLD).into_response();
}
if ms > 0 {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
}
next.run(req).await
}
}));
tokio::spawn(async move { axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await });
Rig { data, holger, fake, base, door, slow_start_ms, slow_login_ms, sells_nothing, set, flow, _cfg: cfg }
}
/// **This holger's estate has a front** (HOL-FLOW-FRONT): the pairing feed names it, as a
/// paired box's pairing loop sets it ([`FRONT`]). Process-wide: a test that calls it runs
/// in a test binary of its own.
pub fn pair_with_front(&self) {
use holger_server_lib::topology::{set_pairing, PairedBox, Pairing};
set_pairing(Some(Pairing {
estate: "localhost".into(),
me: "main".into(),
boxes: vec![
PairedBox { name: "main".into(), role: "primary".into(), ..Default::default() },
PairedBox { name: FRONT.into(), role: "front".into(), ..Default::default() },
],
not_after_unix: 0,
}));
}
/// ★ **The front reports its doors and routes to this holger** (HOL-FLOW-FRONT), the way the
/// front does: the key this box keeps for its front (what its pairing loop derives from the
/// fed console keys), a report proven under it, `POST`ed to holger's own HTTP door. Answers
/// the door's status.
pub fn front_reports(&self) -> u16 {
use holger_server_lib::front_report::{self, front};
let key = front::key_of(&[0x5a; 32]);
front_report::keep_key(&self.holger.data_root(), &key).expect("the front's key is kept");
let route = |host: &str, to: &str, what: &str| front::FrontRoute { host: host.into(), door: "https".into(), to: to.into(), what: what.into() };
let report = front::FrontReport {
front: FRONT.into(),
at_unix: holger_server_lib::clock::now_unix_secs() as i64,
doors: vec![
front::FrontDoor { name: "https".into(), listen: "tcp/443".into(), what: "the web face: TLS with the estate's certificate".into() },
front::FrontDoor { name: "http".into(), listen: "tcp/80".into(), what: "the certificate's challenges, and the way to https".into() },
],
routes: vec![
route(FRONT_CONSOLE_HOST, front::TO_CONSOLE, "holger-ui, and its pass-through to main's two doors"),
route(FRONT_REPO_HOST, front::TO_MAIN, "main http :8443, gRPC :50051 (holger.*)"),
],
console_url: format!("https://{FRONT_CONSOLE_HOST}"),
};
let sent = front::Sent { proof: front::proof(&key, &report), report };
let body = serde_json::to_vec(&sent).expect("the report is JSON");
match ureq::post(&format!("{}{}", self.door, front::PATH)).header("content-type", "application/json").send(&body[..]) {
Ok(r) => r.status().as_u16(),
Err(ureq::Error::StatusCode(c)) => c,
Err(e) => panic!("the front's report could not be sent: {e}"),
}
}
/// ★ **The graph's downstream reports its console's address** (HOL-FLOW-FRONT): one sync
/// report from [`GRAPH_DOWNSTREAM`], signed with its key as its mirror signs (the report, its
/// RON summary and the summary's console address, each with its own proof), heard by this
/// upstream's hub. Only on a rig from [`Rig::start_with_graph`].
pub fn downstream_reports_console(&self, console_url: &str) {
use holger_server_lib::grpc::holger_grow_proto::{DownstreamPoint, DownstreamReportRequest, DownstreamSummary};
use holger_server_lib::sync::protocol::SyncKey;
use holger_server_lib::sync::reports::{console_text, report_text, summary_text};
use base64::Engine as _;
let hub = self.holger.intake().expect("the intake chain").sync().expect("this rig was started as an upstream");
let key = SyncKey::open_or_mint(&self.data.path().join("sync-keys").join(format!("{GRAPH_DOWNSTREAM}.key"))).expect("the downstream's key");
let now = holger_server_lib::clock::now_unix_secs() as i64;
let sign = |text: &str| base64::engine::general_purpose::STANDARD.encode(key.sign_report(GRAPH_DOWNSTREAM, "cloud", text.as_bytes(), now).expect("signed").to_bytes());
let mut req = DownstreamReportRequest {
points: vec![DownstreamPoint { repository: "crates".into(), applied_boot: 0, applied_seq: 0, last_complete_unix_ms: now * 1000 }],
every_secs: 60,
summary: Some(DownstreamSummary {
boundary: "Fetch".into(),
repositories: vec!["crates".into(), "pypi".into()],
first_import: "admin".into(),
domain: "lab".into(),
console_url: console_url.into(),
..Default::default()
}),
..Default::default()
};
req.summary_proof = sign(&summary_text(&req).expect("a summary"));
if let Some(text) = console_text(&req) {
req.console_proof = sign(&text);
}
let proof = sign(&report_text(&req));
hub.heard(&proof, &req, None, 0, 0, now * 1000).expect("the upstream hears its downstream");
}
/// **Issue an invalidation on this upstream** (what `holger sync invalidate` does): every
/// enrolled downstream must acknowledge it within the deadline.
pub fn invalidate(&self, coordinate: &str, detail: &str) {
let hub = self.holger.intake().expect("the intake chain").sync().expect("this rig was started as an upstream");
hub.invalidate("security", "browser-test", coordinate, None, holger_server_lib::sync::protocol::Reason::Malware, detail).expect("the invalidation");
}
/// **Hold every registration start back by `by`** before the console passes it on (a slow
/// box right after paying); [`std::time::Duration::ZERO`] passes it at once again.
pub fn slow_register_start(&self, by: std::time::Duration) {
self.slow_start_ms.store(by.as_millis() as u64, std::sync::atomic::Ordering::SeqCst);
}
/// Hold the plain sign-in's start (`/-/auth/passkey/login/start`) back by `by` (RA2-6).
pub fn slow_login_start(&self, by: std::time::Duration) {
self.slow_login_ms.store(by.as_millis() as u64, std::sync::atomic::Ordering::SeqCst);
}
/// **This root sells nothing from now on** (a private instance): `GET /-/signup` answers
/// [`NOTHING_SOLD`], as a holger with no shop does.
pub fn sell_nothing(&self) {
self.sells_nothing.store(true, std::sync::atomic::Ordering::SeqCst);
}
/// **The set doors offer one dependency set at `revision`** ([`OfferedSet`]).
pub fn offer_set(&self, id: &str, revision: &str) {
*self.set.lock().unwrap() = Some(OfferedSet { id: id.into(), revision: revision.into(), votes: Vec::new() });
}
/// **A re-pull changed the set**: its revision is `revision` now; the page still shows the old.
pub fn repull_set(&self, revision: &str) {
if let Some(s) = self.set.lock().unwrap().as_mut() {
s.revision = revision.into();
}
}
/// The votes the set doors took, as `(verb, revision named)`.
pub fn set_votes(&self) -> Vec<(String, String)> {
self.set.lock().unwrap().as_ref().map(|s| s.votes.clone()).unwrap_or_default()
}
/// **Every ask for the flow drawing** the console passed on (`AdminService.FlowSvg`), in
/// order: the zoom each named (`main`, `""` for nothing open, `all`).
pub fn flow_asks(&self) -> Vec<String> {
self.flow.asked.lock().unwrap().clone()
}
/// **Refuse every ask for the flow drawing from now on** (`yes`), as a holger that is
/// restarting answers ([`FLOW_DOWN`]); `false` passes them on again.
pub fn refuse_flow(&self, yes: bool) {
self.flow.refused.store(yes, std::sync::atomic::Ordering::SeqCst);
}
/// holger's own data root (`<tmp>/holger`).
pub fn holger_root(&self) -> PathBuf {
self.data.path().join("holger")
}
/// `base` + `path`.
pub fn at(&self, path: &str) -> String {
format!("{}{path}", self.base)
}
/// A way back from the fake vendor (`https://localhost/…`), on this rig's console.
pub fn back(&self, url: &str) -> String {
url.replacen("https://localhost", &self.base, 1)
}
/// **What the ONE box's pulled `AddPrincipal` makes on the box** once a paid sign-up's
/// account exists (holger `fleet_box::declare_sold`): the sold record names the account's
/// repository and the vendor's session, nobody's yet. The pull channel is not part of the
/// browser's flow, so the rig writes the record the box writes.
pub fn the_box_makes_the_account(&self, account: &str, session: &str) {
holger_server_lib::sold::update(&self.holger_root(), |sold| {
sold.insert(account.to_owned(), holger_server_lib::sold::SoldRepository { template: "bundles".into(), signup: session.to_owned(), private: true, ..Default::default() });
Ok(())
})
.expect("the sold record");
}
/// One ask of holger's HTTP door with the console's bearer: `(status, body)`.
pub fn console_post(&self, path_and_query: &str, body: Option<serde_json::Value>) -> (u16, String) {
let agent = ureq::Agent::config_builder().http_status_as_error(false).build().new_agent();
let mut res = match body {
Some(b) => agent.post(&format!("{}{path_and_query}", self.door)).header("authorization", &format!("Bearer {CONSOLE}")).header("content-type", "application/json").send(b.to_string()),
None => agent.post(&format!("{}{path_and_query}", self.door)).header("authorization", &format!("Bearer {CONSOLE}")).send_empty(),
}
.expect("holger's door answers");
let status = res.status().as_u16();
(status, res.body_mut().read_to_string().unwrap_or_default())
}
/// One GET of holger's HTTP door with the console's bearer: `(status, body)`.
pub fn console_get(&self, path_and_query: &str) -> (u16, String) {
let agent = ureq::Agent::config_builder().http_status_as_error(false).build().new_agent();
let mut res = agent.get(&format!("{}{path_and_query}", self.door)).header("authorization", &format!("Bearer {CONSOLE}")).call().expect("holger's door answers");
let status = res.status().as_u16();
(status, res.body_mut().read_to_string().unwrap_or_default())
}
/// **An invite into `group`**, minted with the console's bearer (the admin's invite page
/// stands in): the secret a `/login?invite=` link carries.
pub fn invite(&self, display: &str, group: &str) -> String {
let (status, body) = self.console_post("/-/auth/passkey/invites", Some(serde_json::json!({ "display": display, "groups": [group] })));
assert_eq!(status, 200, "the console mints an invite: {body}");
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
v["invite"].as_str().expect("the invite secret").to_owned()
}
}
async fn wait_for(url: &str) {
for _ in 0..400 {
let url = url.to_owned();
let ok = tokio::task::spawn_blocking(move || ureq::get(&url).call().map(|r| r.status().as_u16() == 200).unwrap_or(false)).await.unwrap_or(false);
if ok {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
panic!("{url} never answered");
}
/// A small but well-formed wheel (stored-only zip with METADATA and WHEEL), written to `dir`.
pub fn write_wheel(dir: &Path) -> PathBuf {
fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xFFFF_FFFFu32;
for b in data {
crc ^= *b as u32;
for _ in 0..8 {
crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 };
}
}
!crc
}
fn member(out: &mut Vec<u8>, central: &mut Vec<u8>, name: &str, body: &[u8]) {
let crc = crc32(body);
let offset = out.len() as u32;
out.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
out.extend_from_slice(&[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
out.extend_from_slice(&crc.to_le_bytes());
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&(name.len() as u16).to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(name.as_bytes());
out.extend_from_slice(body);
central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
central.extend_from_slice(&[20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
central.extend_from_slice(&crc.to_le_bytes());
central.extend_from_slice(&(body.len() as u32).to_le_bytes());
central.extend_from_slice(&(body.len() as u32).to_le_bytes());
central.extend_from_slice(&(name.len() as u16).to_le_bytes());
central.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
central.extend_from_slice(&offset.to_le_bytes());
central.extend_from_slice(name.as_bytes());
}
let mut out = Vec::new();
let mut central = Vec::new();
member(&mut out, &mut central, "demo_pkg/__init__.py", b"__version__ = \"1.0.0\"\n");
member(&mut out, &mut central, "demo_pkg-1.0.0.dist-info/METADATA", b"Metadata-Version: 2.1\nName: demo-pkg\nVersion: 1.0.0\nLicense-Expression: MIT\n");
member(&mut out, &mut central, "demo_pkg-1.0.0.dist-info/WHEEL", b"Wheel-Version: 1.0\nGenerator: holger-test\nRoot-Is-Purelib: true\nTag: py3-none-any\n");
let cd_offset = out.len() as u32;
let cd_len = central.len() as u32;
out.extend_from_slice(¢ral);
out.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
out.extend_from_slice(&[0, 0, 0, 0, 3, 0, 3, 0]);
out.extend_from_slice(&cd_len.to_le_bytes());
out.extend_from_slice(&cd_offset.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
let path = dir.join(WHEEL);
std::fs::write(&path, out).unwrap();
path
}
/// **A key file, as holger reads one** (`holger-key-1`): the console token is a bearer, so
/// holger refuses it when its group or others may read it (0600 or 0400 only).
fn write_key_file(path: &Path, contents: &str) {
use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
let mut f = std::fs::OpenOptions::new().write(true).create(true).truncate(true).mode(0o600).open(path).unwrap();
// `mode` holds for a new file only: a file that was there keeps its mode, so close it too.
f.set_permissions(std::fs::Permissions::from_mode(0o600)).unwrap();
f.write_all(contents.as_bytes()).unwrap();
}
#[cfg(test)]
mod doors_tests {
use super::*;
/// ★ **The rig writes the console token as a key file, 0600** (Henrik, 2026-10-31): holger
/// refuses a token file others can read (`holger-key-1`), so a rig that wrote it 0644 or
/// 0664 (the umask) started no holger at all, and every browser test was red.
#[test]
fn the_rig_writes_the_console_token_0600() {
use std::os::unix::fs::PermissionsExt as _;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("token");
write_key_file(&path, "secret");
assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);
// Written again over a file that was too open, it is closed again.
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o664)).unwrap();
write_key_file(&path, "secret");
assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);
assert_eq!(std::fs::read_to_string(&path).unwrap(), "secret");
}
/// ★ HUI-PORT: **every door the rig declares is a port it holds** until holger opens it. The
/// replication door used to be left to holger's default (the gRPC port + 1), a port the rig
/// never held, and a listener elsewhere could take it first: "the replication door cannot
/// open on 127.0.0.1:<port>: Address already in use … REFUSING TO START".
#[test]
fn the_rig_declares_only_doors_it_holds() {
let doors = Doors::hold();
let ron = doors.declare(&estate_ron::medium());
let (grpc, http, replication) = (doors.grpc, doors.http, doors.replication);
assert!(ron.contains(&format!("\"127.0.0.1:{grpc}\"")) && ron.contains(&format!("\"127.0.0.1:{http}\"")), "the gRPC and HTTP doors are the held ports");
assert!(ron.contains(&format!("replication_door: Some((ron_bind: Some(\"127.0.0.1:{replication}\")")), "the replication door is declared at a held port, not left at gRPC + 1:\n{ron}");
for (door, port) in [("gRPC", grpc), ("HTTP", http), ("replication", replication)] {
assert!(std::net::TcpListener::bind(("127.0.0.1", port)).is_err(), "the {door} door's port {port} is not held by the rig");
}
doors.release();
}
}