nordisk/holger
server/lib/src/exposed/http.rs
//! HTTP/OCI gateway: the one non-gRPC ingress, letting HTTP-only clients (helm,
//! pip, cargo, OCI registry pushes via `oci://…`) reach the same repository
//! backends the tonic service in `grpc.rs` serves — each request is routed to a
//! backend and dispatched through its `handle_http2_request`. [`GatewaySettings`]
//! carries the shared [`FastRoutes`], auth config, optional TLS, body cap, and
//! the audit sink into every connection.
//!
//! Security-app rules, deliberately kept in lockstep with the gRPC path:
//! TLS terminates here and mTLS CN is lifted from the handshake once per
//! connection; reads (GET/HEAD) stay open and audit as `anonymous`, while writes
//! must pass `auth::validate_request` then `authorize_http_write` (RBAC mirror of
//! `grpc::authorize_write`, DELETE = admin, other writes = writer-or-admin,
//! fail-closed under a configured policy). Only `/healthz`, `/livez`, `/readyz`, and the
//! `/v2` OCI version probe are unauthenticated and unaudited. Routing keys off
//! the first path segment (`/v2/{name}/…` for OCI, else `/{repo}/…`) via
//! [`route_key`]; backends strip their own prefix from the passed-through path.
//!
//! Gotcha: the read-only write-gate (`backend.is_writable()`) and the body-size
//! cap both fire BEFORE the body is collected, so a rejected write never buffers
//! the upload — this is intentional, closing the DoS surface the pre-gate HTTP
//! path left open (it used to hand writes to a read-only backend that silently
//! swallowed them).
//
// HTTP/OCI gateway: bridges HTTP clients (helm, pip, cargo, OCI registry) to
// the repository backends' `handle_http2_request` handlers. Helm and friends
// speak HTTP, not gRPC, so this listener is required for any ecosystem interop;
// the modern Helm flow (`helm push/pull oci://…`) rides the OCI Distribution
// API served here.
//
// Security posture (this is a security application):
// * TLS is terminated here when the endpoint has `ron_tls`; client certs are
// requested but optional, so anonymous pulls and Kubernetes probes work
// while writes are gated on identity at the application layer.
// * mTLS identity (client-cert CN) is extracted once per connection and fed
// into auth alongside any Bearer token.
// * Request bodies are size-capped to bound memory use (upload DoS guard).
// * Reserved unauthenticated paths: GET /healthz, GET /livez, GET /readyz, GET /v2/.
//
// Routing: `/v2/{name}/…` → repo named by the first OCI name segment (OCI);
// `/{repo}/…` → repo named by the first path segment (classic). The full
// path+query is passed through; backends strip their own prefix.
use crate::lock::LockOrRecover as _;
use std::sync::Arc;
/// Per-repo listing cap for the `/metrics` artifact-count gauge. The gauge is an
/// operability signal, not an inventory, so a very large repo is counted up to
/// this bound rather than paying an unbounded scan on every scrape.
pub(crate) const METRICS_LIST_CAP: usize = 100_000;
/// How many rows the front page's "Recently changed" list carries.
///
/// A front page answers "is anything happening here", and a reader gets that
/// from a screenful. `releases` is deliberately uncapped — it is the roster a
/// package manager is pointed at — but a recency list of ten thousand rows is
/// a scroll, not an answer, and it would be the bulk of the `/-/releases`
/// document for nothing.
#[cfg_attr(not(feature = "front"), allow(dead_code))]
const FRONT_RECENT_ROWS: usize = 20;
/// Server ceiling on the `/-/search` per-kind hit cap, applied when the query
/// string is parsed so no caller-supplied `limit` ever reaches the engine above
/// it. A cross-repo search walks EVERY repository's listing, so `limit` is the
/// one knob that buys work with a single GET; it may only ever ask for LESS than
/// the server's own default. Raising the ceiling is a server decision, not a
/// query parameter.
const MAX_SEARCH_LIMIT: usize = crate::search::DEFAULT_SEARCH_LIMIT;
/// Connections the gateway serves at once. The slot is taken BEFORE `accept()`,
/// so anything over the ceiling waits in the kernel's listen backlog (and is
/// refused by the OS once that fills) instead of being accepted into a task,
/// socket and TLS session we then have to hold. Without it one peer could open
/// sockets until the process hit EMFILE and every door stopped answering.
const MAX_CONCURRENT_CONNECTIONS: usize = 1024;
/// How long a TLS peer has to complete its handshake. rustls waits for a
/// ClientHello forever; a peer that connects and says nothing used to hold a
/// task and a connection slot until it felt like leaving (the slowloris shape,
/// one layer below the header-read timeout `serve` already arms).
const TLS_HANDSHAKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
/// First back-off after a failed `accept()`, doubled on each consecutive
/// failure up to [`AcceptLimits::accept_backoff_max`]. A failing accept used to
/// `continue` straight into another accept — under EMFILE/ENFILE that is a hot
/// spin that burns a core and floods the log for as long as the condition lasts.
const ACCEPT_BACKOFF_MIN: std::time::Duration = std::time::Duration::from_millis(5);
use http_body_util::{BodyExt, Full, Limited, StreamBody};
use hyper::body::{Bytes, Frame, Incoming};
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::server::conn::auto::Builder as ConnBuilder;
use rustls::ServerConfig;
use tokio_rustls::TlsAcceptor;
use tokio_stream::wrappers::ReceiverStream;
use crate::audit::{AuditAction, AuditEvent, AuditLog};
use crate::auth::{self, AuthConfig};
use crate::exposed::fast_routes::FastRoutes;
use crate::exposed::tls::leaf_common_name;
/// The one reply body type of this door: a whole reply (`Full`) and a streamed
/// one (`StreamBody` off a backend's `fetch_chunks`) behind the same box, so a
/// large `GET` leaves in pieces while every other reply is unchanged.
type ReplyBody = http_body_util::combinators::BoxBody<Bytes, std::convert::Infallible>;
/// Everything a gateway connection needs, shared behind an `Arc`.
/// See [`GatewaySettings::admit_upload`].
pub type UploadAdmission = Arc<dyn Fn(&str, u64) -> Result<(), String> + Send + Sync>;
pub struct GatewaySettings {
pub routes: FastRoutes,
pub auth_config: Arc<AuthConfig>,
/// **The per-repository read scope**, shared verbatim with the gRPC services
/// ([`crate::grpc::HolgerGrpc::read_gate`]). Both transports serve the same
/// repositories, so a repository that demands a credential on one and not
/// the other would be a repository that does not demand one at all — the
/// hole the reserved-property floor already found once, when a Writer
/// refused at the gRPC door simply used this one.
pub read_gate: Arc<crate::auth::ReadGate>,
pub tls: Option<Arc<ServerConfig>>,
pub max_body_bytes: usize,
/// Append-only audit sink, shared with the gRPC services. Reads
/// (GET/HEAD) record as anonymous downloads; gated writes record the
/// resolved identity (Bearer sub / mTLS CN).
pub audit: Arc<dyn AuditLog>,
/// Network mutation posture. `ReadOnly` refuses every PUT/POST/PATCH/DELETE
/// (and the `/-/properties` write door) before the body is collected.
pub operation_mode: crate::OperationMode,
/// **What this box IS** — the declared role and the `twin ≥ primary` store
/// verdict, shared verbatim with the gRPC services
/// ([`crate::grpc::HolgerGrpc::twin`]). Both transports serve the same
/// repositories, so a door that accepted a write the other refuses would be
/// a twin that accepts writes.
///
/// A `Twin` refuses every mutation here by name (`holger-twin-1`) and
/// answers the read-only `GET /-/twin` door with its posture.
pub twin: Arc<crate::twin::TwinPosture>,
/// **The growth's write hold**, the SAME one the gRPC gates and every held
/// backend ask (`crate::grow::Hold`): while a growth drains, every mutation is
/// answered `503` + `Retry-After` by name (`holger-grow-1`).
pub grow: Arc<crate::grow::Hold>,
/// Security posture. In `Critical`, `/metrics` is served only to loopback peers.
pub security_profile: crate::SecurityProfile,
/// Hosted-SBOM store, shared with the `SbomService` gRPC RPCs. Backs the
/// read-only `GET /-/sbom` door (attach is gRPC/CLI-only). `None` ⇒ the door
/// answers 404 (SBOM hosting not configured on this server).
#[cfg(feature = "intake")]
pub sboms: Option<crate::sbom::SharedSbomStore>,
/// The digest-keyed attestation store, shared with the promote gate. Backs
/// the read-only OCI 1.1 referrers door (`GET /v2/<name>/referrers/<digest>`)
/// and the envelope door it points at. `None` ⇒ referrers answers an EMPTY
/// index, never a 404: "this server holds no evidence" and "this server does
/// not do evidence" look the same from an OCI client, and neither is a
/// missing page.
#[cfg(feature = "intake")]
pub attest: Option<std::sync::Arc<crate::attest::AttestStore>>,
/// **The evidence public key** (lane HOL-OCI-EVIDENCE): the public half of
/// `attestation.signer_cert`, served open and read-only at `/-/evidence-key` (PEM),
/// `.pem`, `.jwks` and `.crt`. `None` ⇒ those paths answer 404 and say why.
#[cfg(feature = "intake")]
pub evidence_key: Option<std::sync::Arc<crate::evidence_key::EvidenceKey>>,
/// **The box's repository signing key** (lane HOL-SIGNED-APT-DNF): its public half is
/// served open at `/-/repo-key.asc` (apt's `signed-by=`, dnf's `gpgkey=`). `None` ⇒ that
/// path answers 404 and names `holger repo-key create`.
pub repo_key: Option<std::sync::Arc<crate::repo_sign::RepoKey>>,
/// Custom-property store, shared with the gRPC `Search` property axis. Backs
/// the read-only `GET /-/properties` door AND the `property=` filter on
/// `GET /-/search` (property WRITES are CLI/in-process for now). `None` ⇒ the
/// door answers 404 and a `property=` filter matches nothing (fail-closed).
#[cfg(feature = "intake")]
pub properties: Option<crate::properties::SharedPropertyStore>,
/// The passkey ceremony store, backing the `/-/auth/passkey/…` doors. `None`
/// ⇒ that whole surface answers 404, which is how a console tells "this server
/// does not do passkeys" from "it does and refused you".
#[cfg(feature = "auth-passkey")]
pub passkeys: Option<crate::passkey::SharedPasskeyDoor>,
/// **The intake chain** ([`crate::promote::intake`]), shared with the gRPC
/// door. `Some` ⇒ an upload into its `incoming` repository is INGESTED
/// through the chain (gates, fail-closed audit, record), a write into its
/// other three repositories is refused by name, its incoming, quarantine and
/// approved repositories serve nothing, and its production repository serves
/// only AVAILABLE artifacts, from the sealed store. `None` ⇒ no chain.
#[cfg(feature = "intake")]
pub intake: Option<Arc<crate::promote::intake::IntakeChain>>,
/// **The box's import doors** (lane W4, [`crate::import_door`]), shared
/// with the gRPC `AdminService.ImportIntelBundle` / `ImportBundle` and the
/// inbox jobs: `POST /-/intel/import` and `POST /-/bundle/import`. `None` ⇒
/// both answer 404 by name.
#[cfg(feature = "intake")]
pub imports: Option<Arc<crate::import_door::ImportDoor>>,
/// Property keys the SERVER reads as policy, which only an ADMIN may write —
/// the configured set (`keep` by default) plus the promotion engine's
/// `required_property`.
///
/// This door writes the SAME store the gRPC `SetProperty` RPC does, so it must
/// apply the SAME floor. It did not, and a Writer refused at the gRPC door
/// simply used this one. See [`crate::properties::is_reserved_property`].
#[cfg(feature = "intake")]
pub reserved_properties: std::collections::BTreeSet<String>,
/// The promotion gate's required property, when promotion is configured. Part
/// of the reserved set above: forging it walks an unreviewed artifact into PROD.
#[cfg(feature = "intake")]
pub promotion_required_property: Option<(String, String)>,
/// **Upload admission** — asked before a PUT/POST body is read, with the
/// repository and the declared length; `Err(line)` refuses with 507 and
/// the line. Only the metered build fills it (`metered::Quota`, the SAME
/// check the gRPC `PutArtifact` asks); `None` everywhere else, so an open
/// build carries a slot and no plan.
pub admit_upload: Option<UploadAdmission>,
/// **Who may say what the public face is** (HOL-DOCFIND a): the endpoint's
/// `ron_trusted_proxies`. Only from a peer in it are `X-Forwarded-Proto` and
/// `X-Forwarded-Host` believed when a link is written ([`public_base`]). `None`: nobody.
pub trusted_proxies: Option<Arc<crate::listen::AllowList>>,
/// **Which configured door this gateway IS** — the exposed endpoint's
/// `ron_name`. The visitor tally counts per door, and a listener that did not
/// know its own name could only count per server.
pub door: String,
/// The per-door, per-repository request and distinct-consumer tally drawn on
/// the flow picture. ONE per server, shared with the gRPC services. See
/// [`crate::visitors`] for why no address is ever kept in it.
pub visitors: Arc<crate::visitors::Visitors>,
/// **When this server's console last called** (HOL-SYNC-SKEW): ONE per server, shared
/// with the gRPC services; marked here, by a call carrying the console's own proof.
pub console: crate::flow::ConsoleSeen,
/// The paired holgers ([`crate::peers`]), shared with the gRPC services and
/// drawn on the flow picture.
pub peers: Arc<crate::peers::PeerBoard>,
/// **The estate board's evidence** (lane E3): what `GET /-/estate.json` serves —
/// the SAME [`crate::topology::Sources`] the gRPC `AdminService.Topology` reads.
/// `None` (a bare gateway in a test) answers 404.
pub board: Option<crate::topology::Sources>,
/// **The artifact flow** (lane HOL-SVG-RON): its shape read from the RON at boot and
/// the quota reader. `GET /-/flow`, `/-/flow.svg` and `/-/flow.json` draw it with
/// [`crate::flow::live`], the function `AdminService.FlowSvg` calls. `None`
/// (`flow: (enabled: false)`, a bare gateway) answers 404.
pub flow: Option<crate::flow::Door>,
/// **The scan sandbox's probe door** (lane HOLGER-MEDIA): `GET /-/sandbox/probe`
/// runs the worker's `probe` through the intake chain's own sandbox
/// ([`ScanProbe`], set by the server when it has an intake chain). `None`
/// (a bare gateway, no intake chain, a build without `supply-chain`) answers 404.
pub scan_probe: Option<ScanProbe>,
/// **What `/readyz` asks** (lane B4, [`crate::ready`]): the storage, the data
/// root and the scan sandbox. `None` (a bare gateway in a test or a tool)
/// answers ready as soon as the door is bound.
pub ready: Option<ReadyCheck>,
/// **Download and upload admission** ([`crate::admission`]) — ONE per server,
/// shared with the gRPC door. `DoorAdmission::pass_through()` (no
/// `admission:` block) refuses nothing and enforces no share.
#[cfg(feature = "admission")]
pub admission: Arc<crate::admission::DoorAdmission>,
}
/// Bind the HTTP/OCI gateway and start its accept loop.
///
/// The bind happens **synchronously, in the caller's context**, so a failure
/// (EADDRINUSE from a second holger or any other process on the port, EACCES on
/// a privileged port under a non-root unit) is returned as an `Err` and reaches
/// `Holger::start()` → `holger-server start`, which exits non-zero.
///
/// It used to bind inside the spawned task, where the only way to report failure
/// was an `eprintln!` and a bare `return` while this function returned `Ok(())`
/// regardless. The process then reported full success — exit status 0,
/// `server_started=true`, systemd `active (running)` — and blocked on Ctrl+C
/// while serving nothing. Every client got connection-refused and no automated
/// check ever fired. Only the accept LOOP belongs in the background; readiness
/// is a caller-visible fact.
pub fn start_http_gateway(
addr: std::net::SocketAddr,
settings: Arc<GatewaySettings>,
) -> anyhow::Result<()> {
start_http_gateway_with_limits(addr, settings, AcceptLimits::default())
}
/// [`start_http_gateway`] for one `plane` of an endpoint (lane HOL-NCSC): the data listener
/// answers the admin doors with `holger-listen-3`, the admin listener the package doors; an
/// admin listener drops (and audits) every peer outside `allow`.
pub fn start_http_gateway_on(
addr: std::net::SocketAddr,
settings: Arc<GatewaySettings>,
plane: crate::listen::Plane,
allow: Option<Arc<crate::listen::AllowList>>,
) -> anyhow::Result<()> {
start_http_gateway_inner(addr, settings, AcceptLimits::default(), plane, allow)
}
/// The accept loop's resource ceilings. Production always uses
/// [`AcceptLimits::default`] (the module constants); the parameter exists so the
/// loop's refusals can be asserted in a test at millisecond scale instead of
/// being argued about — a cap nothing exercises is a cap nobody knows is wired.
#[derive(Clone, Copy, Debug)]
pub(crate) struct AcceptLimits {
/// Connections served at once; see [`MAX_CONCURRENT_CONNECTIONS`].
pub max_connections: usize,
/// Ceiling on one TLS handshake; see [`TLS_HANDSHAKE_TIMEOUT`].
pub tls_handshake_timeout: std::time::Duration,
/// Ceiling on the doubling back-off after a failed `accept()`.
pub accept_backoff_max: std::time::Duration,
}
impl Default for AcceptLimits {
fn default() -> Self {
Self {
max_connections: MAX_CONCURRENT_CONNECTIONS,
tls_handshake_timeout: TLS_HANDSHAKE_TIMEOUT,
accept_backoff_max: std::time::Duration::from_millis(500),
}
}
}
/// The next back-off after a failed `accept()`: [`ACCEPT_BACKOFF_MIN`] for the
/// first failure, doubling on each consecutive one, never above `max`. A
/// successful accept resets `previous` to `None`.
fn next_accept_backoff(
previous: Option<std::time::Duration>,
max: std::time::Duration,
) -> std::time::Duration {
match previous {
None => ACCEPT_BACKOFF_MIN.min(max),
Some(d) => d.saturating_mul(2).min(max),
}
}
pub(crate) fn start_http_gateway_with_limits(
addr: std::net::SocketAddr,
settings: Arc<GatewaySettings>,
limits: AcceptLimits,
) -> anyhow::Result<()> {
start_http_gateway_inner(addr, settings, limits, crate::listen::Plane::All, None)
}
fn start_http_gateway_inner(
addr: std::net::SocketAddr,
settings: Arc<GatewaySettings>,
limits: AcceptLimits,
plane: crate::listen::Plane,
allow: Option<Arc<crate::listen::AllowList>>,
) -> anyhow::Result<()> {
// Bind before spawning: std binds without needing the reactor, then
// `from_std` adopts the socket into tokio inside the task.
let std_listener = std::net::TcpListener::bind(addr)
.map_err(|e| anyhow::anyhow!("HTTP/OCI gateway failed to bind {}: {}", addr, e))?;
std_listener
.set_nonblocking(true)
.map_err(|e| anyhow::anyhow!("HTTP/OCI gateway {} set_nonblocking failed: {}", addr, e))?;
// A32-14, stays a task: the HTTP/OCI gateway's accept loop, for the server's life.
tokio::spawn(async move {
let listener = match tokio::net::TcpListener::from_std(std_listener) {
Ok(l) => l,
Err(e) => {
eprintln!("HTTP/OCI gateway failed to adopt listener {}: {}", addr, e);
return;
}
};
let scheme = if settings.tls.is_some() {
"https"
} else {
"http (cleartext)"
};
println!("HTTP/OCI gateway listening on {} [{}]", addr, scheme);
if settings.tls.is_none() {
log::warn!(
"SECURITY: HTTP/OCI gateway on {} runs WITHOUT TLS — credentials and \
artifacts travel in cleartext. Set ron_tls for any non-loopback use.",
addr
);
}
let acceptor = settings.tls.clone().map(TlsAcceptor::from);
// One permit per in-flight connection. Held by the connection's task, so
// it is released when the connection ends however it ends (clean close,
// reset, hyper's header-read timeout, a TLS handshake that timed out).
let slots = Arc::new(tokio::sync::Semaphore::new(limits.max_connections));
// `None` = the last accept succeeded; `Some(d)` = how long we waited after
// the previous consecutive failure.
let mut backoff: Option<std::time::Duration> = None;
loop {
// Take the slot BEFORE accepting. Over the ceiling the excess stays in
// the kernel's listen backlog instead of becoming a task we hold: the
// process cannot be pushed into EMFILE by a peer that only opens
// sockets, and the connections we DO serve keep their resources.
let Ok(permit) = slots.clone().acquire_owned().await else {
return; // semaphore closed — the gateway is going away
};
let (stream, peer) = match listener.accept().await {
Ok(conn) => {
backoff = None;
conn
}
Err(e) => {
// Back off instead of spinning: under EMFILE/ENFILE every
// accept fails instantly, and the bare `continue` this
// replaces burned a core and filled the log until the
// condition cleared.
let wait = next_accept_backoff(backoff, limits.accept_backoff_max);
log::debug!("gateway accept error: {} (retrying in {:?})", e, wait);
backoff = Some(wait);
drop(permit);
tokio::time::sleep(wait).await;
continue;
}
};
// HOL-NCSC: an admin listener takes only the peers its allow-list names.
if let Some(a) = &allow {
if !a.allows(peer.ip()) {
crate::refusals::record_to(
&settings.audit,
"admin listener",
"anonymous",
&peer.to_string(),
&crate::listen::AllowList::refusal(&peer),
);
drop(stream);
drop(permit);
continue;
}
}
// TCP_NODELAY on every connection (HOL-PERF-3, X1): a streamed body goes
// out in pieces, and with Nagle on, each small write waited for the
// client's delayed ACK, about 40 ms per small download.
let _ = stream.set_nodelay(true);
let peer = peer.to_string();
let settings = settings.clone();
let acceptor = acceptor.clone();
let handshake_timeout = limits.tls_handshake_timeout;
// A32-14, stays a task: one task per HTTP connection.
tokio::spawn(async move {
// Released when this task ends, whatever ends it.
let _permit = permit;
match acceptor {
Some(acceptor) => {
// A peer that connects and never sends a ClientHello used
// to hold this task forever — rustls has no deadline of
// its own, and hyper's header-read timeout is a layer too
// late to fire.
let tls_stream =
match tokio::time::timeout(handshake_timeout, acceptor.accept(stream))
.await
{
Ok(Ok(s)) => s,
Ok(Err(e)) => {
log::debug!("TLS handshake failed: {}", e);
crate::refusals::handshake_refused(
Some(&settings.audit),
"HTTPS door",
&peer,
&e.to_string(),
);
return;
}
Err(_) => {
log::debug!(
"TLS handshake from {} timed out after {:?}",
peer,
handshake_timeout
);
crate::refusals::handshake_refused(
Some(&settings.audit),
"HTTPS door",
&peer,
&format!("no handshake within {handshake_timeout:?}"),
);
return;
}
};
// Pull the mTLS identity (if any) out of the handshake.
let client_cn = tls_stream
.get_ref()
.1
.peer_certificates()
.and_then(leaf_common_name);
serve(TokioIo::new(tls_stream), settings, client_cn, peer, plane).await;
}
None => serve(TokioIo::new(stream), settings, None, peer, plane).await,
}
});
}
});
Ok(())
}
async fn serve<I>(
io: TokioIo<I>,
settings: Arc<GatewaySettings>,
client_cn: Option<String>,
peer: String,
plane: crate::listen::Plane,
) where
I: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + 'static,
{
let svc = service_fn(move |req| {
let settings = settings.clone();
let client_cn = client_cn.clone();
let peer = peer.clone();
async move {
let door = settings.door.clone();
// HOL-NCSC: each listener serves its own plane's doors.
let path = req.uri().path().to_owned();
let mut answer = if plane.serves(&path) {
handle(req, settings, client_cn, peer).await
} else {
Ok(text(404, &plane.refusal(&path)))
};
// Every answer names this Holger's security domain (lane HOL-CDS).
if let (Ok(resp), Some(v)) = (&mut answer, crate::boundary::domain_header_value()) {
resp.headers_mut().insert(crate::boundary::DOMAIN_HEADER, v);
}
// The flow's door box counts this door's 5xx answers (HOL-SVG-RON).
if let Ok(resp) = &answer {
crate::metrics::global().record_door_status(&door, resp.status().as_u16());
}
answer
}
});
// A timer arms hyper's header-read timeout (a connection that never
// finishes its request headers is dropped instead of holding a task,
// socket and TLS state forever — the slowloris shape) and HTTP/2 keep-alive
// pings.
let mut builder = ConnBuilder::new(TokioExecutor::new());
builder
.http1()
.timer(hyper_util::rt::TokioTimer::new())
.header_read_timeout(std::time::Duration::from_secs(15));
builder
.http2()
.timer(hyper_util::rt::TokioTimer::new())
.keep_alive_interval(Some(std::time::Duration::from_secs(30)))
.keep_alive_timeout(std::time::Duration::from_secs(20));
if let Err(e) = builder.serve_connection(io, svc).await {
log::debug!("gateway connection error: {}", e);
}
}
/// The Bearer token a request presents, if any. One spelling of the lookup so
/// the read gate, the write gates and the meta doors cannot drift apart on what
/// counts as a credential.
fn bearer_of(req: &Request<Incoming>) -> Option<&str> {
req.headers()
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
}
/// The credential a request presents, in EITHER spelling the ecosystems use.
///
/// `Bearer <token>` is what holger's own tools and every gRPC-shaped caller
/// send. `Basic base64(user:pass)` is what the package managers send, and they
/// do not offer a choice: `docker login` / `podman push`, `npm publish`,
/// `mvn deploy`, `pip`, `nuget push` and `helm push` all speak Basic and have
/// no Bearer mode at all. MEASURED on holger.rs 2026-09-21: a `podman push` to
/// the OCI door failed with `authentication required` and never retried,
/// because the door read Bearer only and answered the 401 with no challenge.
///
/// The token is taken from the PASSWORD field, which is where every one of
/// those clients puts a token, and from the username when the password is
/// empty, which is what a `-u <token>` invocation produces. The username is
/// otherwise ignored on purpose: the identity comes from the token (the
/// configured method resolves the subject), so honouring a username would
/// invent a second, weaker way to say who you are.
///
/// It resolves a STRING and nothing else — Basic is a transport spelling of the
/// same secret, not a second authentication method, so `validate_request` is
/// reached by one path and the gates cannot drift.
/// `POST /<repo>/legacy/` (twine's upload URL) or `POST /<repo>/`.
#[cfg(feature = "intake")]
fn is_twine_upload_path(path: &str, repo: &str) -> bool {
let rest = path.strip_prefix('/').unwrap_or(path);
let rest = rest.strip_prefix(repo).unwrap_or(rest);
matches!(rest.trim_matches('/'), "" | "legacy")
}
fn credential_of(req: &Request<Incoming>) -> Option<String> {
let raw = req.headers().get("authorization")?.to_str().ok()?;
if let Some(token) = raw.strip_prefix("Bearer ") {
return Some(token.to_owned());
}
// ★ cargo's spelling: the token VERBATIM, with no scheme at all. It is what
// the cargo registry specification prescribes and what `cargo publish`
// sends, so a registry that insists on a scheme cannot be published to.
// MEASURED 2026-09-21: `cargo publish --registry holger` answered 401 with
// the token on the wire and correct.
//
// A scheme is `<name> SP <parameter>`, so a value with NO space cannot be
// one — that is the whole discrimination, and it is why this arm can sit
// beside the other two without swallowing them. A malformed scheme reaches
// `validate_request` as a token and fails there, which is the same refusal
// it would have got.
if !raw.contains(' ') {
return (!raw.is_empty()).then(|| raw.to_owned());
}
let encoded = raw.strip_prefix("Basic ")?;
use base64::Engine as _;
let decoded = base64::engine::general_purpose::STANDARD
.decode(encoded.trim())
.ok()?;
let pair = String::from_utf8(decoded).ok()?;
// ★ `split_once`, not `split`: a token may itself contain a colon and
// splitting on every one would truncate it to its first segment — a
// credential that is refused for a reason nothing in the log explains.
let (user, pass) = pair.split_once(':')?;
let token = if pass.is_empty() { user } else { pass };
(!token.is_empty()).then(|| token.to_owned())
}
/// A 401 that a package manager can act on.
///
/// A bare 401 is where `podman push` stopped: the client asks once, is told
/// "no", and has nothing to retry WITH. `WWW-Authenticate` is the header that
/// turns a refusal into a challenge, and `Basic` is the scheme those clients
/// implement — so this is what makes a `docker login`ed push reach the door at
/// all. The realm is the server's name rather than a repository's: one
/// credential covers this holger, and a per-repository realm would ask a client
/// to log in again for every shelf.
fn unauthorized() -> Response<ReplyBody> {
Response::builder()
.status(401)
.header("Content-Type", "text/plain")
.header("WWW-Authenticate", "Basic realm=\"holger\"")
.body(Full::new(Bytes::from_static(b"Unauthorized")).boxed())
.expect("static response")
}
/// **A gated read with no sign-in, refused by name** (HCON-5): `[read-gated]` and the
/// repository, with the same challenge as [`unauthorized`], so a package client asks for a token.
fn read_gated(repo: &str) -> Response<ReplyBody> {
Response::builder()
.status(401)
.header("Content-Type", "text/plain")
.header("WWW-Authenticate", "Basic realm=\"holger\"")
.body(Full::new(Bytes::from(crate::posture::read_gated(repo))).boxed())
.expect("static response")
}
/// **A repository token refused by its own code**: the status, `[holger-token-<n>] words` as
/// the body, and on a 401 the challenge, so a package client says the server's sentence.
#[cfg(feature = "auth-passkey")]
fn token_refused(why: &crate::repo_token::Refused) -> Response<ReplyBody> {
let mut r = Response::builder()
.status(why.status)
.header("Content-Type", "text/plain");
if why.status == 401 {
r = r.header("WWW-Authenticate", "Basic realm=\"holger\"");
}
r.body(Full::new(Bytes::from(why.line())).boxed())
.expect("static response")
}
/// What a meta-door read resolved to.
///
/// `who` is the principal to audit. `identity` is the resolved principal itself,
/// which the cross-repo `/-/search` door needs rather than just its name: it
/// authorizes EACH repository it is about to walk, and `authorize_read` takes an
/// identity. `identity == None` means nothing was authenticated (the gate is off,
/// or no auth method is configured) — with the gate ON a `None` never gets past
/// `authorize_read`, so `identity.is_some()` is exactly "this caller
/// authenticated".
struct MetaRead {
who: String,
identity: Option<auth::AuthIdentity>,
}
impl MetaRead {
/// The open-read answer: nobody authenticated, and nobody had to.
fn anonymous() -> Self {
Self {
who: "anonymous".to_string(),
identity: None,
}
}
}
/// HCON-5: **`front: Gated` on a gated box needs a sign-in** for the front's doors. Not
/// [`gate_meta_read`]`("")`: on a box with public repositories the catalog answers an anonymous
/// caller (it lists the public ones), and the front then stayed open under `front: Gated`.
/// A signed-in caller still passes the Reader floor, as [`gate_meta_read`] holds it.
#[cfg(feature = "front")]
async fn front_needs_a_sign_in(
settings: &GatewaySettings,
req: &Request<Incoming>,
client_cn: Option<&str>,
) -> bool {
if !settings.auth_config.require_auth_for_reads {
return false;
}
match auth::validate_request(&settings.auth_config, bearer_of(req), client_cn).await {
Ok(Some(identity)) => crate::auth::authorize_read_identity_scoped(
&settings.auth_config,
&settings.read_gate,
Some(&identity),
"",
)
.is_err(),
_ => true,
}
}
/// Read-gate the cross-repo `/-/` doors. With `require_auth_for_reads` on, the
/// request must authenticate and pass the Reader floor for `repo` (`""` = the
/// global floor, for cross-repo `/-/search`); otherwise a ready 401/403 is
/// returned. With the gate OFF it returns the anonymous principal, preserving the
/// open-read posture — so behavior is unchanged unless the operator opted in
/// (and the Critical profile forces the gate on). Closes the gap where these
/// doors returned before the per-repo read gate.
async fn gate_meta_read(
settings: &GatewaySettings,
req: &Request<Incoming>,
client_cn: Option<&str>,
repo: &str,
) -> Result<MetaRead, Response<ReplyBody>> {
// Global gate, or this repository`s own `ron_require_auth`. `""` (the
// cross-repo floor for `/-/search`) is never a repository name, so it can
// only ever be gated globally.
if !settings.read_gate.applies(&settings.auth_config, repo) {
// …but with per-repository gates on, the cross-repo door must still
// know WHO is asking, or every gated repository is filtered out for
// everybody (`HolgerGrpc::authorize_catalog`, the gRPC twin). Resolved,
// not required: no credential is the anonymous caller.
if repo.is_empty() && settings.read_gate.catalog_resolves() {
if bearer_of(req).is_none() && client_cn.is_none() {
return Ok(MetaRead::anonymous());
}
return match auth::validate_request(&settings.auth_config, bearer_of(req), client_cn)
.await
{
Ok(identity) => Ok(MetaRead {
who: identity
.as_ref()
.map(|i| i.subject.clone())
.unwrap_or_else(|| "anonymous".to_string()),
identity,
}),
Err(_) => Err(text(401, "Unauthorized")),
};
}
return Ok(MetaRead::anonymous());
}
let identity =
match auth::validate_request(&settings.auth_config, bearer_of(req), client_cn).await {
Ok(id) => id,
Err(_) => return Err(text(401, "Unauthorized")),
};
if let Err((status, _detail)) = crate::auth::authorize_read_identity_scoped(
&settings.auth_config,
&settings.read_gate,
identity.as_ref(),
repo,
) {
let msg = if status == 403 {
"Forbidden"
} else {
"Unauthorized"
};
return Err(text(status, msg));
}
Ok(MetaRead {
who: identity
.as_ref()
.map(|i| i.subject.clone())
.unwrap_or_else(|| "anonymous".to_string()),
identity,
})
}
/// The estate board's HTTP path (lane E3): the board as JSON (H2 #1; the server draws nothing).
pub const ESTATE_JSON_PATH: &str = "/-/estate.json";
/// The scan sandbox's probe door (lane HOLGER-MEDIA): admin-read, JSON.
pub const SANDBOX_PROBE_PATH: &str = "/-/sandbox/probe";
/// What answers the probe door: the worker's `probe` through the server's own
/// sandbox, as JSON, or the refusal's words (a `holger-sandbox-<n>` tag).
/// `Ok(None)`: the server has no scan worker yet (no intake chain opened).
pub type ScanProbe = Arc<dyn Fn() -> Result<Option<serde_json::Value>, String> + Send + Sync>;
/// What answers `/readyz`: `Ok`, or every reason the server is not ready.
pub type ReadyCheck = Arc<dyn Fn() -> Result<(), String> + Send + Sync>;
/// **Admin-read gate for a `/-/` admin door** (the HTTP twin of the gRPC
/// `authorize_admin_identity_for(.., auditor_reads: true)`): with no auth method
/// configured the dev/test door is open, as every admin read is; otherwise the
/// request must authenticate (401), and with RBAC on its role must be Admin or its
/// duties include Auditor (403).
async fn gate_admin_read(
settings: &GatewaySettings,
req: &Request<Incoming>,
client_cn: Option<&str>,
) -> Result<(), Response<ReplyBody>> {
if settings.auth_config.methods.is_empty() {
return Ok(());
}
let identity =
match auth::validate_request(&settings.auth_config, bearer_of(req), client_cn).await {
Ok(Some(id)) => id,
Ok(None) | Err(_) => return Err(text(401, "Unauthorized")),
};
if settings.auth_config.rbac_enabled() {
let role = settings
.auth_config
.role_for_identity_in_repo("", &identity);
let auditor = settings
.auth_config
.duties_for_identity(&identity)
.contains(&auth::Duty::Auditor);
if !role.can_admin() && !auditor {
return Err(text(403, "Forbidden"));
}
}
Ok(())
}
/// **Who sent this request, for the visitor tally** — borrowed, never copied.
///
/// Only the LAST `X-Forwarded-For` line is read: the client is the right-most
/// entry, which is the address the proxy in front of this door wrote, and that
/// entry is always on the last line. [`crate::visitors::client_of`] believes it
/// only from a loopback peer.
fn visitor_of<'a>(
peer: &'a str,
headers: &'a hyper::HeaderMap,
) -> Option<crate::visitors::Client<'a>> {
crate::visitors::client_of(peer, last_xff(headers))
}
/// The last non-empty `X-Forwarded-For` line.
fn last_xff(headers: &hyper::HeaderMap) -> Option<&str> {
headers
.get_all("x-forwarded-for")
.iter()
.filter_map(|v| v.to_str().ok())
.rfind(|v| !v.trim().is_empty())
}
/// Is `peer` in the endpoint's `ron_trusted_proxies`?
fn from_trusted_proxy(peer: &str, trusted: Option<&crate::listen::AllowList>) -> bool {
trusted.is_some_and(|t| {
peer.parse::<std::net::SocketAddr>()
.is_ok_and(|a| t.allows(a.ip()))
})
}
/// **The client of a request** (SEC-REVIEW 2026-11-01 F1): [`visitor_of`], and from a peer in
/// `ron_trusted_proxies` (the estate's front dials from its WireGuard address, not loopback)
/// the right-most `X-Forwarded-For` entry that front wrote, believed as from loopback.
fn client_address<'a>(
peer: &'a str,
headers: &'a hyper::HeaderMap,
trusted: Option<&crate::listen::AllowList>,
) -> Option<crate::visitors::Client<'a>> {
if from_trusted_proxy(peer, trusted) {
if let Some(c) = crate::visitors::client_of("127.0.0.1:0", last_xff(headers)) {
return Some(c);
}
}
visitor_of(peer, headers)
}
/// The source an audit row records (F1): the forwarded client and the front it came through
/// (`<client> via <front>`) from a trusted proxy, else the peer.
fn audit_source(
peer: &str,
headers: &hyper::HeaderMap,
trusted: Option<&crate::listen::AllowList>,
) -> String {
let client = last_xff(headers)
.and_then(|l| l.rsplit(',').next())
.map(str::trim)
.filter(|c| c.parse::<std::net::IpAddr>().is_ok());
match client {
Some(c) if from_trusted_proxy(peer, trusted) => format!("{c} via {peer}"),
_ => peer.to_string(),
}
}
/// True if `peer` (a `host:port` socket-address string) is an IPv4/IPv6 loopback.
fn peer_is_loopback(peer: &str) -> bool {
peer.starts_with("127.")
|| peer.starts_with("[::1]")
|| peer.starts_with("::1")
|| peer.starts_with("localhost")
}
async fn handle(
req: Request<Incoming>,
settings: Arc<GatewaySettings>,
client_cn: Option<String>,
peer: String,
) -> Result<Response<ReplyBody>, std::convert::Infallible> {
// Request-handling clock: observed into the `holger_request_duration_seconds`
// histogram at each door's exit, right where the request counter is bumped, so
// latency is measured over the same handled-request population.
let started = std::time::Instant::now();
let method = req.method().as_str().to_owned();
let path = req.uri().path().to_owned();
let path_and_query = req
.uri()
.path_and_query()
.map(|pq| pq.as_str().to_owned())
.unwrap_or_else(|| path.clone());
// ★ The visitor tally: one request through THIS door, whatever it asks for
// and however it is answered. The repository half is counted below, where
// the path has been resolved to a configured repository.
settings.visitors.record(
crate::visitors::now_unix(),
Some(&settings.door),
None,
client_address(&peer, req.headers(), settings.trusted_proxies.as_deref()),
);
// ★ SEC-REVIEW 2026-11-01 P1: ONE canonical path, before any door. The router
// (`route_key`) drops empty segments and the scanned registry's view
// (`oci_push::rest_of`) does not, so `/v2//oci/…` keyed the `oci` repository and missed
// its view: an unscanned tag write, and staged or BLOCKED bytes served. Every door
// splits the path its own way, so a path that two splits could read differently is
// refused here, by name, for every verb.
if non_canonical_path(&path) {
return Ok(text(
400,
&holger_errcode::SERVER_PATH_NOT_CANONICAL.tag(format!(
"{path}: send the path without empty, `.` or `..` segments"
)),
));
}
// Checksum-verify-on-deploy (Artifactory `X-Checksum-*` convention): a client
// may advertise the digest of the bytes it is uploading. Capture the strong
// digests NOW — before `req.into_body()` consumes the request — so the write
// path can recompute and reject a corrupted upload before it reaches a
// backend. Header lookups are case-insensitive (canonical lowercase names).
let want_sha256 = req
.headers()
.get("x-checksum-sha256")
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
let want_sha512 = req
.headers()
.get("x-checksum-sha512")
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
// ReadOnly operation mode: refuse every network mutation here, BEFORE the
// request body is collected and before any write door (artifact PUT/POST/
// DELETE, the `/-/properties` write door). Reads (GET/HEAD) fall through to
// the normal auth path. /healthz/readyz stay reachable (they are GET).
//
// A TWIN is asked FIRST and on the same terms — every verb that is not a
// read — because a follower that took an upload would be a second primary with a
// different store. The answer NAMES the primary: on a twin, "write there"
// is an instruction and a bare 403 is an outage. Plan §3 W9;
// `holger-twin-1`.
// A box AWAITING ITS RECHARGE serves nothing — not a read, not a write —
// until the recharge is whole (`holger-twin-15`). The probes and `/-/twin`
// still answer: a supervisor and the recharge lane must see WHY.
if let Some(refusal) = settings.twin.serve_refusal() {
// `/-/estate.json` too: a held main is exactly the board an operator opens.
let probe = method == "GET"
&& matches!(
path.as_str(),
"/healthz" | "/livez" | "/readyz" | "/-/twin" | ESTATE_JSON_PATH
);
if !probe {
return Ok(text(503, &refusal));
}
}
let mutating = !matches!(method.as_str(), "GET" | "HEAD");
if mutating {
if let Some(refusal) = settings.twin.write_refusal() {
return Ok(text(403, &holger_errcode::TWIN_REFUSES_WRITES.tag(refusal)));
}
// A growth holds every upload while it drains (`crate::grow`): 503 with a
// Retry-After, the answer a client retries on — refused, never queued.
if let Some(held) = settings.grow.write_refusal() {
return Ok(Response::builder()
.status(503)
.header("Content-Type", "text/plain")
.header("Retry-After", crate::grow::RETRY_AFTER_SECS.to_string())
.body(Full::new(Bytes::from(holger_errcode::GROW_WRITES_HELD.tag(held))).boxed())
.expect("static response"));
}
}
if settings.operation_mode == crate::OperationMode::ReadOnly && mutating {
return Ok(text(403, "server is in read-only mode"));
}
// ── holger.rs's PUBLIC FRONT PAGE ───────────────────────────────────
// `GET /` answered `404 Unknown repository` until this arm: the one
// page served to somebody who is not anybody yet. The whole door — the
// bytes, the JSON, the cache headers and every refusal — lives in
// `holger-front`, a leaf that builds in seconds; this is the mount, and
// deliberately nothing more.
//
// It is asked BEFORE the repository router and can only ever claim `/`,
// `/holger.webp` and two paths under the already-reserved `/-/`
// namespace, so it cannot shadow a repository. `route` answers `None`
// for everything else and this falls straight through — the claim is
// proved on its own side by `a_repository_path_is_never_this_doors`.
//
// ★ **Outside the `if method == "GET"` block below, and that is not a
// detail.** Inside it, `HEAD /holger.webp` fell through to the repository
// router and answered `404 Unknown repository` — so every health check and
// every cache revalidation on the picture said the front page was not
// there — and `POST /` got the same 404 instead of a method refusal.
// Measured against a running dev-pair, which is the only way that class of
// bug is ever found: it compiles, and `GET /` looks perfect.
//
// ★ **Behind `feature = "front"` since 2026-09-17.** Without it `holger-front`
// is not a dependency of this crate, these paths fall through to the
// repository router, and the artifact contains no HTML at all — the shape the
// appliance tiers (`embedded`/`mini`/`mvp-server`) now build, and the shape
// the product server takes when a separate BFF host mounts the page instead.
#[cfg(feature = "front")]
if let Some(front_path) = holger_front_path(&path) {
// The same read gate `/-/search`, `/-/sbom`, `/-/properties` and
// `/metrics` pass. A server with `require_auth_for_reads` on has no
// public front page BY DEFINITION, and it says so by name
// (`holger-front-3`) rather than 401-ing a browser into a blank tab
// with nothing in it to search for.
// HCON-5: `access.front: Open` (the default) keeps the front page readable on a gated
// box; its release list is then only what the caller may read (anonymous: the public).
if !settings.read_gate.front_open()
&& front_needs_a_sign_in(&settings, &req, client_cn.as_deref()).await
{
return Ok(front_reply(holger_front::reads_gated()));
}
// Only `/-/releases` pays for a listing. The page, the picture and
// `/-/front` are constants, and walking every repository to serve a
// 578 kB image would be a listing per page load.
let catalogue = if front_path == holger_front::RELEASES_PATH {
front_catalogue(
&settings,
front_caller(&settings, &req, client_cn.as_deref()).await,
)
.await
} else {
holger_front::Catalogue::default()
};
let front = holger_front::Front {
// The server's own public name, from its environment — not
// guessed from the request's Host, which is whatever a proxy or
// a caller put there, and not from `location.origin` in the
// page, which would print the proxy's address on any
// deployment behind one. Absent is an honest answer: the page
// simply does not print the line.
base_url: std::env::var("HOLGER_PUBLIC_BASE_URL")
.ok()
.filter(|v| !v.is_empty()),
// ★ **The console's door, when this deployment has one.**
//
// `None` stays the answer — and the honest one — for a server
// with no console configured: holger-server itself authenticates
// with mTLS, OIDC and bearer tokens, doors for `cargo`, `pip`
// and `docker`, and it still issues no browser session of its
// own. The page then says `holger-front-2` by name rather than
// drawing a button wired to nothing.
//
// What changed is that the CONSOLE has a browser login — OIDC
// authorization-code with PKCE, in `holger-ui` — and the front
// page had no way to know, so it told every visitor this
// product has no browser login at all. That sentence is true of
// this server and false of holger.
//
// A PATH and not a URL, beside `base_url` because it is the same
// kind of value: the page's button follows only a same-site
// rooted path, which is its open-redirect defence, so the
// console must be reachable on this origin for the button to
// mean anything. `console_login` applies the page's own rule
// here and answers `None` for anything the page would drop, so
// a misconfiguration is the sentence the page already has
// instead of a button that does nothing when pressed.
//
// ★ HOLDIRECT (owner 2026-10-28, holger.rs): with the passkey sign-in on this
// server the front page's Log in runs the ceremony itself, one press, and lands on
// the console's home; `/login` stays for a direct link and for enrolment.
login: std::env::var("HOLGER_CONSOLE_PATH")
.ok()
.filter(|v| !v.is_empty())
.and_then(|p| holger_front::console_sign_in(&p, front_has_passkeys(&settings))),
};
let doors = holger_front::Doors {
front: &front,
catalogue: &catalogue,
};
if let Some(reply) = holger_front::route(&method, &path, &doors) {
crate::grpc::functional_status(
"holger-http/front",
"front_served",
reply.status == 200,
&format!("{path} {} {} bytes", reply.status, reply.body.len()),
);
return Ok(front_reply(reply));
}
}
// ── PASSKEY ceremony doors (WebAuthn / FIDO2) ────────────────────────────
//
// Reserved under `/-/` so they can never collide with a repository name, and
// placed HERE -- before the repository router and before the read-only gate's
// artifact rules -- because a login is not an artifact operation.
//
// THE LISTENER MATTERS. holger runs two of them with disjoint door sets: gRPC
// on `ron_url` and this HTTP gateway on `ron_http_url`. The console lane lost
// a day to exactly this, so it is written down: the ceremony is HTTP only, and
// a console pointed at the gRPC port gets 404 on every route below.
//
// Why these are on the HTTP door at all: a WebAuthn ceremony is driven by a
// browser calling `navigator.credentials.*`, and a browser speaks HTTP. The
// ceremony mints a bearer token, and THAT is what the gRPC door then accepts
// through the ordinary auth seam -- so the split surface is the design, not a
// gap in it.
#[cfg(feature = "auth-passkey")]
if path.starts_with("/-/auth/passkey") {
let Some(store) = settings.passkeys.clone() else {
// No `Passkey` method configured ⇒ the surface does not exist. A
// console reads this as "this server does not do passkeys", which is a
// different thing from "it does and refused you".
return Ok(text(404, "passkey login is not configured on this server"));
};
// The ceremony partition key. `peer` is the transport peer -- what the
// kernel saw -- and NOT a header, because a partition a caller can write
// is a suggestion rather than a partition.
//
// ONE exception, and it is a credential rather than a header: holger's
// console (holger-ui-server) carries every person's ceremony to this door,
// so every ceremony would otherwise share the console's address and one
// flood would close the login for everybody. A request that ALSO proves
// an ADMIN identity in `x-holger-proxy-authorization` (the console's own
// bearer) may name the client in `x-holger-ceremony-client`. An admin
// can enrol anybody anyway; trusting its word on whose ceremony this is
// grants nothing more, and an unproven hint is ignored.
let hinted = req
.headers()
.get("x-holger-ceremony-client")
.and_then(|v| v.to_str().ok())
.map(str::trim)
.filter(|c| !c.is_empty() && c.len() <= 64 && c.bytes().all(|b| b.is_ascii_graphic()))
.map(str::to_owned);
let proxy = req
.headers()
.get("x-holger-proxy-authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(str::to_owned);
// The partition key is edda's `ClientKey` (IPv4 by address, IPv6 by /64,
// A34-8): `split(':')` made every `[2001:…]` peer one client.
let mut client = peer.clone();
if let (Some(hint), Some(proxy)) = (hinted, proxy) {
if let Ok(Some(by)) =
auth::validate_request(&settings.auth_config, Some(&proxy), None).await
{
if settings.auth_config.role_for_identity(&by).can_admin() {
// holger-ui carries this ceremony with its own proof: the console called.
settings.console.mark();
client = format!(
"via {}: {}",
by.subject,
nornir_auth_webauthn::ceremony::ClientKey::of_peer(&hint)
);
}
}
}
// The config door is a GET and is deliberately open: `rp_id` is not a
// secret (every ceremony hands it to an anonymous caller already), and a
// console cannot tell an operator at a RENAMED box "your keys were
// registered for X, you are at Y" without knowing X. `bootstrap_spent`
// is the console's own long-standing public answer, moved here with the
// rule it reports: the bootstrap credential no longer enrols once an
// admin passkey exists.
if method == "GET" && path == "/-/auth/passkey/config" {
let mut config = store.config_json();
config["bootstrap_spent"] =
serde_json::json!(store.has_admin_passkey(&settings.auth_config));
let body = config.to_string();
crate::metrics::global().record_request(crate::metrics::Verb::Get, 200, 0);
return Ok(json_response(200, &body));
}
if method != "POST" {
return Ok(text(405, "Method not allowed"));
}
// Identity FIRST, before the body is collected: an unauthorized enrolment
// must not buy an attacker a body-sized allocation.
let bearer = credential_of(&req);
let identity = auth::validate_request(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
)
.await
.ok()
.flatten();
// Ceremony bodies are JSON of a few kilobytes; cap them hard. The import
// of a console's whole table is the one larger body, and it is bounded
// by the same cap (a few hundred credentials).
const PASSKEY_MAX_BODY: usize = 256 * 1024;
let collected = match Limited::new(req.into_body(), PASSKEY_MAX_BODY)
.collect()
.await
{
Ok(c) => c.to_bytes(),
Err(_) => return Ok(text(413, "Payload too large")),
};
let body: serde_json::Value = if collected.is_empty() {
serde_json::json!({})
} else {
match serde_json::from_slice(&collected) {
Ok(v) => v,
Err(_) => return Ok(text(400, "body is not JSON")),
}
};
// An admin floor shared by the administration doors. Minting an
// enrolment grant IS an admin action -- it is how somebody becomes an
// operator -- so it is gated exactly like one.
let require_admin =
|identity: &Option<auth::AuthIdentity>| -> Result<String, (u16, String)> {
match identity {
Some(id) if settings.auth_config.role_for_identity(id).can_admin() => {
Ok(id.audit_name())
}
Some(_) => Err((403, "this action requires an admin identity".to_owned())),
None => Err((401, "this action requires an admin identity".to_owned())),
}
};
// ...and step-up on top for the acts that hand out or take away a login:
// a passkey session must be user-verified and fresh (the console's rule,
// moved here); a machine credential is exempt.
let stepped_up = |identity: &Option<auth::AuthIdentity>| -> Result<(), (u16, String)> {
match identity {
Some(id) if !store.caller_stepped_up(id, bearer.as_deref()) => {
Err((403, crate::passkey::STEP_UP_REFUSAL.to_owned()))
}
_ => Ok(()),
}
};
let auth_cfg = &settings.auth_config;
let outcome: Result<serde_json::Value, (u16, String)> = match path.as_str() {
"/-/auth/passkey/login/start" => store.open_login(&body, &client),
"/-/auth/passkey/login/finish" => store.close_login(&body),
// A28 M9: a stale touch answered in place (the console's answer to `"code":"stepup"`).
"/-/auth/passkey/step-up/start" => {
store.open_step_up(identity.as_ref(), bearer.as_deref(), &client)
}
"/-/auth/passkey/step-up/finish" => {
store.close_step_up(&body, identity.as_ref(), bearer.as_deref())
}
"/-/auth/passkey/register/start" => {
// The admin path (no invite) is an enrolment by an admin's word: a passkey admin
// must be stepped up for it. An invite or a sign-up is its own credential.
if body.get("invite").is_none() && body.get("signup").is_none() {
stepped_up(&identity).and_then(|()| {
store.open_enrolment(
&body,
identity.as_ref(),
bearer.as_deref(),
auth_cfg,
&client,
)
})
} else {
store.open_enrolment(
&body,
identity.as_ref(),
bearer.as_deref(),
auth_cfg,
&client,
)
}
}
"/-/auth/passkey/register/finish" => store.close_enrolment(&body, auth_cfg),
"/-/auth/passkey/invites" => require_admin(&identity)
.and_then(|by| stepped_up(&identity).map(|()| by))
.and_then(|by| {
store.mint_invite_as(
&crate::passkey::with_owners_of(&body, identity.as_ref()),
&by,
bearer.as_deref(),
auth_cfg,
)
}),
"/-/auth/passkey/invites/list" => {
require_admin(&identity).map(|_| store.invites_json(auth_cfg))
}
"/-/auth/passkey/invites/revoke" => require_admin(&identity).and_then(|_| {
let id = body.get("id").and_then(|v| v.as_str()).unwrap_or_default();
store.withdraw_invite(id)
}),
// Who the caller is, for a console to render (never to decide with).
"/-/auth/passkey/session" => match &identity {
Some(id) => Ok(store.session_json(id, bearer.as_deref(), auth_cfg)),
None => Err((401, "no session".to_owned())),
},
// A person's own logout: every session of theirs ends. Always answers ok — a
// browser must be able to get rid of a session it cannot use.
"/-/auth/passkey/logout" => match bearer
.as_deref()
.filter(|b| crate::identity_host::is_session_token(b))
{
Some(b) => store
.logout(b)
.or_else(|_| Ok(serde_json::json!({ "ok": true }))),
None => Ok(serde_json::json!({ "ok": true })),
},
"/-/auth/passkey/sessions/revoke-all" => require_admin(&identity)
.and_then(|_| stepped_up(&identity))
.and_then(|()| store.revoke_all_sessions()),
"/-/auth/passkey/credentials/list" => {
require_admin(&identity).map(|_| store.credentials_json(auth_cfg))
}
"/-/auth/passkey/credentials/revoke" => require_admin(&identity)
.and_then(|by| stepped_up(&identity).map(|()| by))
.and_then(|by| {
let id = body.get("id").and_then(|v| v.as_str()).unwrap_or_default();
store.revoke_credential(id, &by, auth_cfg)
}),
_ => Err((404, "no such passkey door".to_owned())),
};
let (status, payload) = match outcome {
Ok(v) => (200u16, v.to_string()),
Err((status, msg)) => (status, crate::passkey::refusal_json(&msg).to_string()),
};
// Audited: enrolment and login are exactly the events an operator needs to
// be able to reconstruct afterwards.
let who = identity
.as_ref()
.map(|i| i.audit_name())
.unwrap_or_else(|| "anonymous".to_owned());
let ev = AuditEvent::new(&who, AuditAction::List, "-", &path, &peer, status, 0)
.with_detail(format!("passkey door {path}"));
if let Err(e) = settings.audit.record(ev) {
log::warn!("audit record failed: {e}");
}
crate::metrics::global().record_request(crate::metrics::Verb::Put, status, 0);
crate::metrics::global().observe_request_duration(started.elapsed().as_secs_f64());
return Ok(json_response(status, &payload));
}
// **The sign-up page's two choices** (lane SU1): holger-ui's `/signup` asks this door and
// renders what it answers — the ONE shop's account sign-up for this site, and its
// private-estate order, carrying the signed-in person's `estate` ticket. A visitor (no
// credential, or one that does not resolve) gets the links a visitor gets; never an error.
if path == "/-/signup" {
if method != "GET" && method != "HEAD" {
return Ok(text(405, "Method not allowed"));
}
let bearer = credential_of(&req);
let identity = auth::validate_request(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
)
.await
.ok()
.flatten();
let root = settings.board.as_ref().and_then(|b| b.data_root.clone());
let auth_cfg = settings.auth_config.clone();
let links = match tokio::task::spawn_blocking(move || {
crate::room::signup_links(root.as_deref(), &auth_cfg, identity.as_ref())
})
.await
{
Ok(l) => l,
Err(e) => {
return Ok(text(
500,
&format!("the sign-up links could not be made: {e}"),
))
}
};
let mut resp = json_response(
200,
&serde_json::to_string(&links).unwrap_or_else(|_| "{}".into()),
);
resp.headers_mut().insert(
"Cache-Control",
hyper::header::HeaderValue::from_static("no-store"),
);
return Ok(resp);
}
// **The relay's doors** (A12): holger's front runs the shared buy flow (monetize-site-buy)
// over the site's bearer at the ONE box, and asks THIS box who a request's person is, for
// that person's own actor ticket, and — with the console's own credential — whether a paid
// sign-up's account holds its passkey yet. The vendor's way back (`/paid/…`, `/cancel/…`)
// is the site's page (holger-ui), not this door's.
#[cfg(feature = "metered")]
if path == "/-/buy/me" || path == "/-/buy/ticket" || path == "/-/buy/account" {
let bearer = credential_of(&req);
let identity = auth::validate_request(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
)
.await
.ok()
.flatten();
let root = settings.board.as_ref().and_then(|b| b.data_root.clone());
let (status, payload) = match (path.as_str(), method.as_str()) {
("/-/buy/me", "GET") => {
let me = tokio::task::spawn_blocking(move || {
crate::room::buy_me(root.as_deref(), identity.as_ref())
})
.await
.unwrap_or_default();
(200u16, serde_json::to_value(&me).unwrap_or_default())
}
("/-/buy/ticket", "POST") => {
let collected = match Limited::new(req.into_body(), 4096).collect().await {
Ok(c) => c.to_bytes(),
Err(_) => return Ok(text(413, "Payload too large")),
};
let body: serde_json::Value =
serde_json::from_slice(&collected).unwrap_or(serde_json::Value::Null);
let purpose = body
.get("purpose")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_owned();
let caps: std::collections::BTreeMap<String, u64> = body
.get("caps")
.cloned()
.and_then(|c| serde_json::from_value(c).ok())
.unwrap_or_default();
let minted = tokio::task::spawn_blocking(move || {
crate::room::buy_ticket(
root.as_deref(),
identity.as_ref(),
&purpose,
&caps,
crate::clock::now_unix_ms(),
)
})
.await;
match minted {
Ok(Ok(ticket)) => (200, serde_json::json!({ "ticket": ticket })),
Ok(Err((status, why))) => (status, serde_json::json!({ "error": why })),
Err(e) => (
500,
serde_json::json!({ "error": format!("the ticket was not made: {e}") }),
),
}
}
("/-/buy/account", "GET") => match &identity {
None => (401, serde_json::json!({ "error": "the console asks this" })),
Some(id) if !settings.auth_config.role_for_identity(id).can_admin() => {
(403, serde_json::json!({ "error": "the console asks this" }))
}
Some(_) => {
let user = req
.uri()
.query()
.unwrap_or("")
.split('&')
.find_map(|kv| kv.strip_prefix("user="))
.unwrap_or("")
.to_owned();
let answer = tokio::task::spawn_blocking(move || {
crate::room::signup_account(root.as_deref(), &user)
})
.await
.unwrap_or("none");
(200, serde_json::json!({ "account": answer }))
}
},
_ => return Ok(text(405, "Method not allowed")),
};
let mut resp = json_response(status, &payload.to_string());
resp.headers_mut().insert(
"Cache-Control",
hyper::header::HeaderValue::from_static("no-store"),
);
return Ok(resp);
}
// **This box's generic settings** (owner decision 2026-10-26, item 5): bytes under a name
// the client chooses (`crate::settings`; a console's own switches). An administrator only;
// every write audited with who, the name and old -> new; bounded, a refusal naming its
// bound. GET answers `{"present", "value_hex"}`; PUT takes the value as the body and
// answers `{"had", "previous_hex"}`.
if let Some(name) = path.strip_prefix(crate::settings::DOOR) {
let name = name.to_owned();
let bearer = credential_of(&req);
let identity = auth::validate_request(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
)
.await
.ok()
.flatten();
let who = identity
.as_ref()
.map(|i| i.subject.clone())
.unwrap_or_else(|| "anonymous".to_owned());
let mut admin = identity
.as_ref()
.is_some_and(|id| settings.auth_config.role_for_identity(id).can_admin());
// **The console's proof READS, and never writes** (RA2-2). holger-ui sends its own bearer
// ONLY as the proof in `x-holger-proxy-authorization`, never as anyone's credential;
// with an administrator's proof the console reads its switch at start and after. A
// write needs the person's own credential in `authorization`: the proof is not one.
let mut who = who;
if !admin && method == "GET" {
let proof = req
.headers()
.get("x-holger-proxy-authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(str::to_owned);
if let Some(proof) = proof {
if let Ok(Some(by)) =
auth::validate_request(&settings.auth_config, Some(&proof), None).await
{
if settings.auth_config.role_for_identity(&by).can_admin() {
// holger-ui reads its setting with its own proof: the console called.
settings.console.mark();
who = format!("console proof of {}", by.subject);
admin = true;
}
}
}
}
if !admin {
let status = if identity.is_some() { 403 } else { 401 };
if method == "PUT" {
let ev = AuditEvent::new(&who, AuditAction::Other, "", &path, &peer, status, 0)
.with_detail(format!(
"setting {name}: refused, an administrator writes the settings"
));
if let Err(e) = settings.audit.record(ev) {
log::warn!("audit record failed: {e}");
}
}
return Ok(json_response(status, &serde_json::json!({ "error": "only an administrator reads or writes this box's settings" }).to_string()));
}
let Some(root) = settings.board.as_ref().and_then(|b| b.data_root.clone()) else {
return Ok(json_response(503, &serde_json::json!({ "error": "this box has no data root, so it keeps no settings" }).to_string()));
};
let (status, payload) = match method.as_str() {
"GET" => match tokio::task::spawn_blocking(move || crate::settings::get(&root, &name))
.await
{
Ok(Ok(v)) => (
200u16,
serde_json::json!({ "present": v.is_some(), "value_hex": crate::settings::hex(&v.unwrap_or_default()) }),
),
// A damaged file is NO value (RA2-10): never a 200 that reads as "nothing
// stored", so a console that asks keeps its switch OFF until a write recovers it.
Ok(Err(why)) if why.starts_with(crate::settings::DAMAGED) => {
log::error!("ALERT {why}: every read answers 503 until an administrator writes a setting");
(503, serde_json::json!({ "error": why, "damaged": true }))
}
Ok(Err(why)) => (400, serde_json::json!({ "error": why })),
Err(e) => (500, serde_json::json!({ "error": e.to_string() })),
},
"PUT" => {
let value = match Limited::new(req.into_body(), crate::settings::MAX_VALUE_BYTES + 1).collect().await {
Ok(c) => c.to_bytes().to_vec(),
Err(_) => return Ok(json_response(413, &serde_json::json!({ "error": format!("a setting value is at most {} bytes", crate::settings::MAX_VALUE_BYTES) }).to_string())),
};
// Audited BEFORE it is applied (RA2-9): the record (who, the name, old -> new)
// is written and flushed first, and a change the audit could not keep is refused.
let (n, v, audit, who, path, peer, len) = (
name.clone(),
value.clone(),
settings.audit.clone(),
who.clone(),
path.clone(),
peer.clone(),
value.len() as u64,
);
let changed = tokio::task::spawn_blocking(move || {
crate::settings::change(&root, &n, &v, |line| {
let ev =
AuditEvent::new(&who, AuditAction::Other, "", &path, &peer, 200, len)
.with_detail(line.to_owned());
audit
.record(ev)
.and_then(|()| audit.flush())
.map_err(|e| format!("{e:?}"))
})
})
.await;
match changed {
Ok(Ok(previous)) => (
200,
serde_json::json!({ "had": previous.is_some(), "previous_hex": crate::settings::hex(&previous.unwrap_or_default()) }),
),
Ok(Err(why)) if why.starts_with("the change was not audited") => {
log::error!("ALERT setting {name}: {why}");
(503, serde_json::json!({ "error": why }))
}
Ok(Err(why)) => (400, serde_json::json!({ "error": why })),
Err(e) => (500, serde_json::json!({ "error": e.to_string() })),
}
}
_ => return Ok(text(405, "Method not allowed")),
};
let mut resp = json_response(status, &payload.to_string());
resp.headers_mut().insert(
"Cache-Control",
hyper::header::HeaderValue::from_static("no-store"),
);
return Ok(resp);
}
// ★ **The front's report** (lane HOL-FLOW-FRONT): the estate front tells this box its
// doors, the names it serves and where each goes, proven under the key the pairing kept
// (`crate::front_report`). No bearer: the proof is the credential. A box with no data root
// (a bare gateway) has no front, and answers 404 as an older holger does.
if path == holger_downstream_rules::front::PATH {
let Some(root) = settings.board.as_ref().and_then(|b| b.data_root.clone()) else {
return Ok(text(404, "this holger keeps no front's report"));
};
if method != "POST" {
return Ok(text(405, "the front's report is a POST"));
}
let body = match Limited::new(req.into_body(), crate::front_report::MAX_BODY)
.collect()
.await
{
Ok(c) => c.to_bytes(),
Err(_) => return Ok(text(413, "the front's report is too large")),
};
let now_ms = crate::clock::now_unix_ms();
let done =
tokio::task::spawn_blocking(move || crate::front_report::heard(&root, &body, now_ms))
.await;
return Ok(match done {
Ok(Ok(kept)) => {
log::info!(
"front: the report of '{}' is kept ({} doors, {} routes)",
kept.report.front,
kept.report.doors.len(),
kept.report.routes.len()
);
json_response(
200,
&serde_json::json!({ "kept": true, "front": kept.report.front }).to_string(),
)
}
Ok(Err((status, why))) => {
// Anybody may knock (the repository route carries this door): a refusal is
// said at debug here; the front says its own refusal at warn.
log::debug!("front: {why}");
text(status, &why)
}
Err(e) => text(500, &format!("the front's report could not be heard: {e}")),
});
}
// **The artifact flow** (lane HOL-SVG-RON): the RON's graph, the estate and the
// counters, from `crate::flow::live` — the SAME function `AdminService.FlowSvg`
// calls, so the console and this door never draw two pictures. Admin-read (it
// names addresses). Mounted outside the GET block, so a HEAD or a write is answered
// by this door and never falls through to the repository router.
if let Some(flow_path) = holger_flow_path(&path) {
let Some(door) = settings.flow.clone() else {
return Ok(text(404, "the artifact flow is off on this holger"));
};
if method != "GET" && method != "HEAD" {
return Ok(text(405, "the artifact flow is read-only: GET"));
}
if let Err(resp) = gate_admin_read(&settings, &req, client_cn.as_deref()).await {
return Ok(resp);
}
let sources = crate::flow::Sources {
shape: door.shape,
repos: settings.routes.all_repos(),
audit: Arc::clone(&settings.audit),
visitors: Arc::clone(&settings.visitors),
console: settings.console.clone(),
board: settings.board.clone(),
#[cfg(feature = "intake")]
intake: settings.intake.clone(),
quota: door.quota,
};
// The zoom (HOL-FLOW-SMART): no `open` is level 1, `?open=main` level 2, `all` 3.
let open = crate::flow::Open::parse(&flow_open_words(req.uri().query().unwrap_or("")));
let drawn = tokio::task::spawn_blocking(move || {
let graph = crate::flow::live(&sources).zoom(&open);
match flow_path {
crate::flow::FLOW_SVG_PATH => (graph.render_svg(), "image/svg+xml; charset=utf-8"),
crate::flow::FLOW_JSON_PATH => (graph.to_json(), "application/json"),
_ => (
graph.render_page(graph.refresh_secs),
"text/html; charset=utf-8",
),
}
})
.await;
return Ok(match drawn {
Ok((body, ct)) => Response::builder()
.status(200)
.header("Content-Type", ct)
.header("Cache-Control", "no-store")
.header("X-Content-Type-Options", "nosniff")
.body(if method == "HEAD" {
Full::new(Bytes::new()).boxed()
} else {
Full::new(Bytes::from(body)).boxed()
})
.expect("static response"),
Err(e) => text(500, &format!("the artifact flow could not be drawn: {e}")),
});
}
// Unauthenticated, backend-less endpoints. Probes are not artifact
// operations, so they are not audited.
if method == "GET" {
// `/livez` is the same liveness probe under the name Kubernetes' own components use
// (`listen::is_probe` names it; HOL-DOCFIND g).
if path == "/healthz" || path == "/livez" {
return Ok(text(200, "ok"));
}
// ★ The repository-key door (lane HOL-SIGNED-APT-DNF): the public half of the key the
// deb and rpm views sign their metadata with, open on every box like the evidence key.
if path == crate::repo_sign::KEY_PATH {
return Ok(match settings.repo_key.as_ref() {
Some(k) => Response::builder()
.status(200)
.header("Content-Type", "application/pgp-keys")
.header("Cache-Control", "public, max-age=300")
.header("X-Content-Type-Options", "nosniff")
.body(Full::new(Bytes::from(k.public_armor().to_owned())).boxed())
.expect("static response"),
None => text(404, crate::repo_sign::NO_KEY),
});
}
// ★ The evidence public-key door (lane HOL-OCI-EVIDENCE): open on every box, a
// read-gated one too, before any gate. A verifier fetches it with no credential.
#[cfg(feature = "intake")]
if crate::evidence_key::is_door(&path) {
return Ok(
match settings.evidence_key.as_ref().and_then(|k| k.answer(&path)) {
Some((ctype, body)) => Response::builder()
.status(200)
.header("Content-Type", ctype)
.header("Cache-Control", "public, max-age=300")
.header("X-Content-Type-Options", "nosniff")
.body(Full::new(Bytes::from(body)).boxed())
.expect("static response"),
None => text(404, crate::evidence_key::NO_SIGNER),
},
);
}
// Readiness (lane B4): asked each time, off the reactor (it pings the
// scan sidecar and writes one small file).
if path == "/readyz" {
let Some(check) = settings.ready.clone() else {
return Ok(text(200, "ok"));
};
return Ok(match tokio::task::spawn_blocking(move || check()).await {
Ok(Ok(())) => text(200, "ok"),
Ok(Err(why)) => text(503, &format!("not ready: {why}")),
Err(e) => text(503, &format!("not ready: the check did not finish: {e}")),
});
}
// **The twin door** (plan §3 W9 / ?6). Read-only JSON: what this box is,
// which primary it follows, whether it accepts writes, and the
// `twin ≥ primary` store verdict with its named warning
// (`holger-twin-2`) when the twin cannot hold what the primary has used.
//
// Unauthenticated beside the probes, and it carries no secret: a role, a
// hostname the operator configured, and two byte counts. It exists so a
// promotion is never the moment somebody discovers the twin is too
// small. It never refuses — the invariant is REPORTED, not enforced.
if path == "/-/twin" {
return Ok(json_response(200, &settings.twin.door_json()));
}
// **The estate board** (lane E3): front | main | twin, the upstreams and the
// downstreams, as DATA (H2 #1) from `crate::topology` — the same rows as
// `AdminService.Topology`; holger-ui draws them. Admin-read (the rows name
// addresses).
if path == ESTATE_JSON_PATH {
if let Err(resp) = gate_admin_read(&settings, &req, client_cn.as_deref()).await {
return Ok(resp);
}
let Some(sources) = settings.board.clone() else {
return Ok(text(404, "this door carries no estate board"));
};
return Ok(
match tokio::task::spawn_blocking(move || crate::topology::board(&sources.gather()))
.await
{
Ok(reply) => Response::builder()
.status(200)
.header("Content-Type", "application/json")
.header("Cache-Control", "no-store")
.header("X-Content-Type-Options", "nosniff")
.body(
Full::new(Bytes::from(crate::topology::json(&reply).to_string()))
.boxed(),
)
.expect("static response"),
Err(e) => text(500, &format!("the estate board could not be gathered: {e}")),
},
);
}
// **The scan sandbox, looked at from inside** (lane HOLGER-MEDIA): the worker's
// `probe` mode through the same sandbox every scan runs in (no network, a
// read-only root, the scratch cap, uid 65532, no capability, seccomp). Admin-read,
// like the estate board; the install test's `sandbox` row reads it.
if path == SANDBOX_PROBE_PATH {
if let Err(resp) = gate_admin_read(&settings, &req, client_cn.as_deref()).await {
return Ok(resp);
}
let Some(probe) = settings.scan_probe.clone() else {
return Ok(text(404, "this door carries no scan sandbox to probe"));
};
return Ok(match tokio::task::spawn_blocking(move || probe()).await {
Ok(Ok(Some(v))) => Response::builder()
.status(200)
.header("Content-Type", "application/json")
.header("Cache-Control", "no-store")
.body(Full::new(Bytes::from(v.to_string())).boxed())
.expect("static response"),
Ok(Ok(None)) => text(
404,
"this door carries no intake chain, so there is no scan sandbox to probe",
),
Ok(Err(e)) => text(503, &e),
Err(e) => text(500, &format!("the probe task failed: {e}")),
});
}
if path == "/v2" || path == "/v2/" {
// The OCI version probe, and the ONE place a container client
// decides whether it will ever send a credential.
//
// MEASURED 2026-09-21 against a real `podman push`: answered 200
// with no `WWW-Authenticate`, the client concludes the registry
// wants nothing, and it then never sends the credential a
// `podman login` stored — not even after the upload POST refuses
// it. The push dies on `authentication required` while the correct
// credential sits unused on disk. That is the whole reason no image
// could be pushed into holger.
//
// So: probe WITH a credential, or on a server that configures no
// auth method at all ⇒ 200, exactly as before. Probe WITHOUT one on
// a server that HAS a method ⇒ 401 and the challenge, which is what
// makes the client attach its credential to everything that
// follows.
//
// ⚠ This does NOT close anonymous pulls. A 401 on the probe is a
// SUCCESSFUL v2 detection to a distribution client (it is how every
// registry that gates pushes answers it); the client records the
// challenge, and a puller with no credential goes on to request the
// manifest anyway, which this door still serves openly while
// `require_auth_for_reads` is false. The anonymous-pull proof is in
// `a_credentialless_pull_still_works_when_the_probe_challenges`.
let has_credential = credential_of(&req).is_some() || client_cn.is_some();
let gated = !settings.auth_config.methods.is_empty();
if gated && !has_credential {
return Ok(Response::builder()
.status(401)
.header("Docker-Distribution-Api-Version", "registry/2.0")
.header("WWW-Authenticate", "Basic realm=\"holger\"")
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from_static(
b"{\"errors\":[{\"code\":\"UNAUTHORIZED\",\"message\":\"authentication required\"}]}",
)).boxed())
.expect("static response"));
}
return Ok(Response::builder()
.status(200)
.header("Docker-Distribution-Api-Version", "registry/2.0")
.body(Full::new(Bytes::from_static(b"{}")).boxed())
.expect("static response"));
}
// Read-only cross-repo SEARCH door (parity §H). Reserved under `/-/` so it
// never collides with a repository name; open read (anonymous), audited as
// a List. Query axes come from the query string:
// /-/search?name=&namespace=&version=&checksum=&path=&repo=&repo=&limit=
// Returns the same JSON shape the engine produces (artifacts + paths +
// truncated). This is the read-only HTTP twin of the `SearchService` RPC.
if path == "/-/search" {
// Read gate (cross-repo → global Reader floor). Off unless
// require_auth_for_reads is set (forced on by the Critical profile).
let gate = match gate_meta_read(&settings, &req, client_cn.as_deref(), "").await {
Ok(g) => g,
Err(resp) => return Ok(resp),
};
let MetaRead {
mut who,
mut identity,
} = gate;
let query = parse_search_query(req.uri().query().unwrap_or(""));
// The CHECKSUM axis is the one axis that fetches and hashes every
// candidate (`search.rs`: `backend.fetch()` + sha256 per artifact).
// On an open-read door there is no principal to attribute or throttle
// that to, so one anonymous `GET /-/search?checksum=<64 hex>` turned
// the entire store into a hashing job. It now requires an
// authenticated identity: the gate resolved one when it is on, and
// with the gate off the door authenticates a request that actually
// carries a credential (so an open-read deployment that still issues
// tokens keeps the axis) — otherwise 403.
if query.checksum.is_some() {
if identity.is_none() {
identity = auth::validate_request(
&settings.auth_config,
bearer_of(&req),
client_cn.as_deref(),
)
.await
.ok()
.flatten();
if let Some(id) = identity.as_ref() {
who = id.subject.clone();
}
}
if identity.is_none() {
let ev = AuditEvent::new(
&who,
AuditAction::List,
"",
&path_and_query,
&peer,
403,
0,
)
.with_detail("checksum search axis requires an authenticated identity");
if let Err(e) = settings.audit.record(ev) {
log::warn!("audit record failed: {e}");
}
crate::grpc::functional_status(
"holger-http/search",
"checksum_axis_authenticated",
false,
"anonymous checksum search refused",
);
crate::metrics::global().record_request(crate::metrics::Verb::Search, 403, 0);
crate::metrics::global()
.observe_request_duration(started.elapsed().as_secs_f64());
return Ok(text(
403,
"the checksum search axis requires authentication",
));
}
}
// Per-repository READ AUTHORIZATION, not just the global floor. The
// door used to gate once at `repo = ""` and then hand `all_repos()`
// to the engine, so an identity carved out of one repository with a
// `Denied` role read it anyway — through search results instead of
// through its own path. Each repository is now authorized for THIS
// identity and the denied ones never enter the engine. With the gate
// off `authorize_read` is `Ok(())` for every repo, so the open-read
// posture is unchanged.
//
// Semantics are kept identical to the gRPC twin
// (`HolgerGrpc::readable_repos`, which `Search` / `ListRepositories`
// use): the denied repository is FILTERED OUT SILENTLY, not turned
// into a 403 for the whole query — a refusal naming the repository
// would itself confirm that a repository the caller may not read
// exists. (It is spelled out here rather than shared because that
// helper hangs off `HolgerGrpc`, and this door has only the
// `FastRoutes` + `AuthConfig` the helper is built from.)
let repos: Vec<(String, Arc<dyn traits::RepositoryBackendTrait>)> = settings
.routes
.all_repos()
.into_iter()
.filter(|(name, _)| {
crate::auth::authorize_read_identity_scoped(
&settings.auth_config,
&settings.read_gate,
identity.as_ref(),
name,
)
.is_ok()
})
.collect();
#[cfg(feature = "intake")]
let props = settings.properties.clone();
let results = tokio::task::spawn_blocking(move || {
#[cfg(feature = "intake")]
let lookup = props
.as_deref()
.map(|p| p as &dyn crate::search::PropertyLookup);
#[cfg(not(feature = "intake"))]
let lookup: Option<&dyn crate::search::PropertyLookup> = None;
crate::search::search_repos_with_properties(&repos, &query, lookup)
})
.await;
let hit_count = results
.as_ref()
.map(|r| (r.artifacts.len() + r.paths.len()) as u64)
.unwrap_or(0);
let (status, body) = match results {
Ok(r) => match serde_json::to_vec(&r) {
Ok(json) => (200u16, json),
Err(e) => (500, format!("serialize error: {e}").into_bytes()),
},
Err(e) => (500, format!("search task failed: {e}").into_bytes()),
};
// Audit the read as the resolved subject (real subject when gated).
let ev = AuditEvent::new(
who,
AuditAction::List,
"",
&path_and_query,
&peer,
status,
hit_count,
);
if let Err(e) = settings.audit.record(ev) {
log::warn!("audit record failed: {e}");
}
crate::grpc::functional_status(
"holger-http/search",
"search_served",
status == 200,
&format!("{hit_count} hits"),
);
crate::metrics::global().record_request(
crate::metrics::Verb::Search,
status,
body.len() as u64,
);
crate::metrics::global().observe_request_duration(started.elapsed().as_secs_f64());
let ct = if status == 200 {
"application/json"
} else {
"text/plain"
};
return Ok(Response::builder()
.status(status)
.header("Content-Type", ct)
.body(Full::new(Bytes::from(body)).boxed())
.unwrap_or_else(|_| text(500, "Internal error")));
}
// Read-only hosted-SBOM door (parity §6/§11). The read twin of the
// `SbomService.FetchSbom` RPC — attach stays gRPC/CLI-only. Coordinate
// comes from the query string:
// /-/sbom?repo=&namespace=&name=&version=
// Serves the hosted CycloneDX document byte-identical (200), or 404 when
// no SBOM is attached to that coordinate. Open read, audited as a Download.
#[cfg(feature = "intake")]
if path == "/-/sbom" {
// Read gate, scoped to the target repo (global floor if repo absent).
// The scope comes from the SAME parser the door serves from, so the
// repository the gate authorizes is by construction the repository
// the door reads. It used to come from a second, simpler parser that
// knew only `repo=` and did not percent-decode, so `?repository=...`
// or `?repo=team%2Da` scoped the gate somewhere other than the
// coordinate that was then served -- stepping around a per-repo
// `Denied` carve-out by respelling the query.
let scope = parse_sbom_coord(req.uri().query().unwrap_or("")).repository;
let who = match gate_meta_read(&settings, &req, client_cn.as_deref(), &scope).await {
Ok(g) => g.who,
Err(resp) => return Ok(resp),
};
let (status, ct, body, repo, label) =
serve_sbom_door(settings.sboms.as_ref(), req.uri().query().unwrap_or(""));
let ev = AuditEvent::new(
who,
AuditAction::Download,
&repo,
&label,
&peer,
status,
if status == 200 { body.len() as u64 } else { 0 },
);
if let Err(e) = settings.audit.record(ev) {
log::warn!("audit record failed: {e}");
}
crate::grpc::functional_status(
"holger-http/sbom",
"sbom_served",
status == 200,
&format!("{status} {label}"),
);
crate::metrics::global().record_request(
crate::metrics::Verb::Sbom,
status,
if status == 200 { body.len() as u64 } else { 0 },
);
crate::metrics::global().observe_request_duration(started.elapsed().as_secs_f64());
return Ok(Response::builder()
.status(status)
.header("Content-Type", ct)
.body(Full::new(Bytes::from(body)).boxed())
.unwrap_or_else(|_| text(500, "Internal error")));
}
// Read-only custom-PROPERTIES door (parity §8). The read twin of the
// `property` search axis — property WRITES are CLI/in-process for now.
// /-/properties?repo=&namespace=&name=&version=
// Serves the coordinate's `{ key: [values] }` map as JSON (200, `{}` when
// none), or 400 when the coordinate is incomplete. Open read, audited.
#[cfg(feature = "intake")]
if path == "/-/properties" {
// Read gate, scoped to the target repo (global floor if repo absent).
// The scope comes from the SAME parser the door serves from, so the
// repository the gate authorizes is by construction the repository
// the door reads. It used to come from a second, simpler parser that
// knew only `repo=` and did not percent-decode, so `?repository=...`
// or `?repo=team%2Da` scoped the gate somewhere other than the
// coordinate that was then served -- stepping around a per-repo
// `Denied` carve-out by respelling the query.
let scope = parse_sbom_coord(req.uri().query().unwrap_or("")).repository;
let who = match gate_meta_read(&settings, &req, client_cn.as_deref(), &scope).await {
Ok(g) => g.who,
Err(resp) => return Ok(resp),
};
let (status, body, repo, label) = serve_properties_door(
settings.properties.as_ref(),
req.uri().query().unwrap_or(""),
);
let ev = AuditEvent::new(who, AuditAction::List, &repo, &label, &peer, status, 0);
if let Err(e) = settings.audit.record(ev) {
log::warn!("audit record failed: {e}");
}
crate::grpc::functional_status(
"holger-http/properties",
"properties_served",
status == 200,
&format!("{status} {label}"),
);
let ct = if status == 200 {
"application/json"
} else {
"text/plain"
};
return Ok(Response::builder()
.status(status)
.header("Content-Type", ct)
.body(Full::new(Bytes::from(body)).boxed())
.unwrap_or_else(|_| text(500, "Internal error")));
}
// Read-only Prometheus scrape door (parity §K). Reserved (no `/-/` prefix
// — Prometheus scrapes the conventional `/metrics`), unaudited (it exposes
// only aggregate registry counters, not artifacts) but read-GATED like the
// other meta doors. Counters come from the process-global registry every
// verb bumps; the per-repo artifact gauge lists each backend at scrape
// time, so it runs under `spawn_blocking` off the async reactor.
if path == "/metrics" {
// Critical profile: `/metrics` is served only to loopback peers.
// Off-loopback scrapes get a 404 so the metrics surface is not exposed
// on a real interface at all — checked first, so a Critical node
// reveals nothing about its auth posture to a remote prober.
if settings.security_profile == crate::SecurityProfile::Critical
&& !peer_is_loopback(&peer)
{
return Ok(text(404, "Not found"));
}
// Read gate (global Reader floor), the same one `/-/search`,
// `/-/sbom` and `/-/properties` pass. `/metrics` is a read like any
// other: it names every repository on the server and, until this
// gate, did an unbounded per-repo listing for anyone who asked. Off
// unless `require_auth_for_reads` is set, so an existing scrape
// configuration is unchanged — and the Critical profile, which forces
// the flag on, no longer has one unauthenticated door left.
if let Err(resp) = gate_meta_read(&settings, &req, client_cn.as_deref(), "").await {
return Ok(resp);
}
let repos = settings.routes.all_repos();
let body = tokio::task::spawn_blocking(move || {
let repo_counts: Vec<(String, Option<usize>)> = repos
.iter()
.map(|(name, backend)| {
// Bounded listing: the gauge is a health signal, not a
// census, so a huge repo caps at METRICS_LIST_CAP. An
// errored backend reports None, NOT 0 -- a zero here
// would make a broken disk read as an empty repository.
// render_prometheus turns None into an absent count
// sample plus holger_repo_listable{repo}=0.
let n = backend.list(None, METRICS_LIST_CAP).map(|v| v.len()).ok();
(name.clone(), n)
})
.collect();
crate::metrics::global().render_prometheus(&repo_counts)
})
.await;
let (status, body) = match body {
Ok(text) => (200u16, text.into_bytes()),
Err(e) => (500u16, format!("metrics task failed: {e}").into_bytes()),
};
crate::grpc::functional_status(
"holger-http/metrics",
"metrics_served",
status == 200,
&format!("{} bytes", body.len()),
);
return Ok(Response::builder()
.status(status)
// Prometheus text exposition format 0.0.4.
.header("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
.body(Full::new(Bytes::from(body)).boxed())
.unwrap_or_else(|_| text(500, "Internal error")));
}
}
// ★ THE INTAKE CHAIN'S WIRE VERB (`crate::promote::duties`), the HTTP twin
// of gRPC `PromotionService.AdvanceArtifact` / `GetArtifactState`:
// ★ THE AUDIT TRAIL (lane S8), the HTTP twin of `AuditService`:
// GET /-/audit/trail?sha256=<hex> the artifact's ordered trail + the chain verdict
// GET /-/audit/verify the whole chain, first break named
// An Auditor or an Admin (`holger-audit-10`); audit off is `holger-audit-8`.
// GET /-/audit/downloads?ident=&repo=&since_nanos=&until_nanos=&limit=
// "what did C download" (stinky SB #19)
if path == "/-/audit/trail" || path == "/-/audit/verify" || path == "/-/audit/downloads" {
if method != "GET" {
return Ok(text(405, "Method not allowed"));
}
let bearer = credential_of(&req);
let identity = match auth::validate_request(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
)
.await
{
Ok(id) => id,
Err(_) => {
return Ok(text(
401,
&holger_errcode::AUTH_UNAUTHENTICATED.tag("invalid credentials"),
))
}
};
if let Err(m) =
crate::audit::trail::authorize_read(&settings.auth_config, identity.as_ref())
{
return Ok(text(403, &m));
}
let Some(source) = settings.audit.query_source() else {
return Ok(text(
409,
&holger_errcode::AUDIT_QUERY_UNAVAILABLE
.tag("this server's audit sink keeps no readable records (audit is off)"),
));
};
let raw_q = req.uri().query().unwrap_or("").to_owned();
let sha = raw_q
.split('&')
.find_map(|p| p.strip_prefix("sha256=").map(form_decode))
.unwrap_or_default();
let trail = path == "/-/audit/trail";
if path == "/-/audit/downloads" {
let param = |k: &str| {
raw_q
.split('&')
.find_map(|p| p.strip_prefix(&format!("{k}=")[..]).map(form_decode))
.unwrap_or_default()
};
let num = |k: &str| -> Result<i64, String> {
let v = param(k);
if v.trim().is_empty() {
Ok(0)
} else {
v.trim()
.parse::<i64>()
.map_err(|e| format!("{k}={v:?} is not a whole number: {e}"))
}
};
let q = match (num("since_nanos"), num("until_nanos"), num("limit")) {
(Ok(since), Ok(until), Ok(limit)) => crate::audit::query::DownloadQuery::of_wire(
¶m("ident"),
¶m("repo"),
since,
until,
limit.max(0) as u64,
),
(Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => {
return Ok(text(400, &holger_errcode::SERVER_BAD_REQUEST.tag(e)))
}
};
let found =
tokio::task::spawn_blocking(move || crate::audit::query::downloads(&source, &q))
.await
.unwrap_or_else(|e| {
Err(crate::audit::AuditError::Other(format!(
"the query task: {e}"
)))
});
crate::grpc::functional_status("holger-http/audit", "audit_read", found.is_ok(), &path);
return Ok(match found {
Ok(d) => Response::builder()
.status(200)
.header("Content-Type", "application/json")
.body(
Full::new(Bytes::from(serde_json::to_string(&d).unwrap_or_default()))
.boxed(),
)
.unwrap_or_else(|_| text(500, "response build failed")),
Err(e) => text(400, &holger_errcode::SERVER_BAD_REQUEST.tag(e.to_string())),
});
}
let reply = tokio::task::spawn_blocking(move || -> Result<String, (u16, String)> {
let report = match &source {
crate::audit::QuerySource::Directory(d) => {
Some(crate::audit::chain::verify_dir(d, &[]).map_err(|e| (500, e.tagged()))?)
}
crate::audit::QuerySource::Snapshot(_) => None,
};
let chain = report.as_ref().map(|r| {
serde_json::json!({
"intact": r.is_intact(), "records": r.records, "checkpoints": r.checkpoints,
"signatures_verified": r.signatures_verified, "signatures_unchecked": r.signatures_unchecked,
"unsigned_checkpoints": r.unsigned_checkpoints, "head": r.head,
"break_line": r.first_break.as_ref().map(|b| b.line),
"break_seq": r.first_break.as_ref().and_then(|b| b.seq),
"break_reason": r.first_break.as_ref().map(|b| b.to_string()),
"summary": r.summary(),
})
});
if !trail {
return chain.map(|c| c.to_string()).ok_or((
409,
holger_errcode::AUDIT_QUERY_UNAVAILABLE.tag("this server's audit sink keeps no chain to verify"),
));
}
let entries = crate::audit::trail::artifact_trail(&source, &sha)
.map_err(|e| (400, holger_errcode::SERVER_BAD_REQUEST.tag(e)))?;
Ok(serde_json::json!({
"sha256": sha.trim().trim_start_matches("sha256:").to_ascii_lowercase(),
"entries": entries,
"chain": chain,
})
.to_string())
})
.await
.unwrap_or_else(|_| Err((500, "dispatch task failed".into())));
crate::grpc::functional_status("holger-http/audit", "audit_read", reply.is_ok(), &path);
return Ok(match reply {
Ok(body) => Response::builder()
.status(200)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(body)).boxed())
.unwrap_or_else(|_| text(500, "response build failed")),
Err((code, m)) => text(code, &m),
});
}
// POST /-/intake/advance?namespace=&name=&version=&verb=advance|quarantine|release|block&to=&reason=
// GET /-/intake/state?namespace=&name=&version=
// The caller's duties decide (separation of duties); the twin, growth and
// read-only holds above already refused a POST that must not mutate.
#[cfg(feature = "intake")]
if path == "/-/intake/advance" || path == "/-/intake/state" {
let advance = path == "/-/intake/advance";
if (advance && method != "POST") || (!advance && method != "GET") {
return Ok(text(405, "Method not allowed"));
}
let Some(chain) = settings.intake.clone() else {
return Ok(text(
404,
&holger_errcode::SERVER_CAPABILITY_NOT_CONFIGURED
.tag("this server declares no intake chain (`promotion.intake`)"),
));
};
let bearer = credential_of(&req);
let identity = match auth::validate_request(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
)
.await
{
Ok(id) => id,
Err(_) => {
return Ok(text(
401,
&holger_errcode::AUTH_UNAUTHENTICATED.tag("invalid credentials"),
))
}
};
if settings.auth_config.rbac_enabled() && identity.is_none() {
return Ok(text(
401,
&holger_errcode::AUTH_UNAUTHENTICATED
.tag("an authenticated identity is required to use the intake chain"),
));
}
let raw_q = req.uri().query().unwrap_or("").to_owned();
let coord = parse_sbom_coord(&raw_q);
let mut verb = String::new();
let mut to = String::new();
let mut reason = String::new();
for pair in raw_q.split('&').filter(|p| !p.is_empty()) {
let (k, v) = match pair.split_once('=') {
Some((k, v)) => (k, form_decode(v)),
None => (pair, String::new()),
};
match k {
"verb" => verb = v,
"to" => to = v,
"reason" => reason = v,
_ => {}
}
}
let auth_cfg = Arc::clone(&settings.auth_config);
let peer_s = peer.clone();
let reply = tokio::task::spawn_blocking(move || -> Result<String, crate::promote::intake::IntakeError> {
use crate::promote::duties;
use crate::promote::intake::{ArtifactState, IntakeError};
if advance {
let Some(action) = duties::Action::parse(&verb) else {
return Err(IntakeError::Duty(duties::DutyError::NotPermitted {
principal: duties::principal_of(identity.as_ref()),
held: Vec::new(),
verb: format!("'{verb}' (verbs: advance, quarantine, release, block)"),
needs: Vec::new(),
}));
};
let to = if to.trim().is_empty() { None } else { Some(ArtifactState::parse(&to)?) };
let ask = duties::AdvanceAsk { id: coord.id, action, to, reason, evidence: Vec::new(), source: peer_s };
let a = duties::advance_as(&chain, &auth_cfg, identity.as_ref(), ask)?;
Ok(serde_json::json!({
"from_state": a.from.name(), "state": a.state.name(),
"approvals": a.approvals, "approvals_required": a.approvals_required,
"evidence": a.evidence, "sha256": a.sha256, "actor": a.actor, "message": a.message,
})
.to_string())
} else {
let v = duties::read_as(&chain, &auth_cfg, identity.as_ref(), &coord.id)?;
let last = v.history.last();
Ok(serde_json::json!({
"source": last.map(|e| e.origin.clone()).unwrap_or_default(),
"effective_policy": last.and_then(|e| e.effective_policy.clone()),
"state": v.state.name(), "sha256": v.sha256, "importer": v.importer,
"approvals": v.pending.len(), "approvals_required": v.approvals_required,
"history": v.history,
"pending_approvals": v.pending.iter().map(|p| p.evidence()).collect::<Vec<_>>(),
})
.to_string())
}
})
.await
.unwrap_or_else(|_| Err(crate::promote::intake::IntakeError::Store("dispatch task failed".into())));
crate::grpc::functional_status("holger-http/intake", "intake_verb", reply.is_ok(), &path);
return Ok(match reply {
Ok(body) => Response::builder()
.status(200)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(body)).boxed())
.unwrap_or_else(|_| text(500, "Internal error")),
Err(e) => text(e.status(), &e.tagged()),
});
}
// ★ THE DEPENDENCY SETS (lane HOL-DEPSET-APPROVE, `crate::pull_intake::depset`): a
// pull's whole closure, approved or refused as one set by the caller's duties.
// GET /-/intake/sets[?status=awaiting-approval] every set the caller may read
// GET /-/intake/set?id= one set, its members and states
// POST /-/intake/set/approve?id=&reason= the caller's vote on every waiting member
// POST /-/intake/set/refuse?id=&reason= every waiting member BLOCKED
#[cfg(all(feature = "intake", feature = "jobs"))]
if matches!(
path.as_str(),
"/-/intake/sets" | "/-/intake/set" | "/-/intake/set/approve" | "/-/intake/set/refuse"
) {
let post = path.starts_with("/-/intake/set/");
if (post && method != "POST") || (!post && method != "GET") {
return Ok(text(405, "Method not allowed"));
}
let Some(chain) = settings.intake.clone() else {
return Ok(text(
404,
&holger_errcode::SERVER_CAPABILITY_NOT_CONFIGURED
.tag("this server declares no intake chain (`promotion.intake`)"),
));
};
if chain.pull().is_none() {
return Ok(text(
404,
&holger_errcode::SERVER_CAPABILITY_NOT_CONFIGURED
.tag("this server pulls nothing through its intake chain (`pull_through`), so it has no dependency sets"),
));
}
let bearer = credential_of(&req);
let identity = match auth::validate_request(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
)
.await
{
Ok(id) => id,
Err(_) => {
return Ok(text(
401,
&holger_errcode::AUTH_UNAUTHENTICATED.tag("invalid credentials"),
))
}
};
if settings.auth_config.rbac_enabled() && identity.is_none() {
return Ok(text(
401,
&holger_errcode::AUTH_UNAUTHENTICATED
.tag("an authenticated identity is required to read or judge dependency sets"),
));
}
let (mut id, mut reason, mut status) = (String::new(), String::new(), String::new());
let mut revision = String::new();
for pair in req
.uri()
.query()
.unwrap_or("")
.split('&')
.filter(|p| !p.is_empty())
{
let (k, v) = match pair.split_once('=') {
Some((k, v)) => (k, form_decode(v)),
None => (pair, String::new()),
};
match k {
"id" => id = v,
"revision" => revision = v,
"reason" => reason = v,
"status" => status = v,
_ => {}
}
}
let auth_cfg = Arc::clone(&settings.auth_config);
let (peer_s, which) = (peer.clone(), path.clone());
let reply = tokio::task::spawn_blocking(move || -> (u16, String) {
let Some(pull) = chain.pull() else {
return (404, "no pull-through".into());
};
let ok = |v: serde_json::Value| (200, v.to_string());
let answer = |r: Result<crate::pull_intake::depset::SetAnswer, crate::pull_intake::depset::SetError>| match r {
Ok(a) => ok(serde_json::to_value(a).unwrap_or_default()),
Err(e) => (e.status, e.message),
};
match which.as_str() {
"/-/intake/sets" => ok(serde_json::json!({
"sets": pull.sets_as(&chain, &auth_cfg, identity.as_ref(), Some(status.as_str())),
"approvals_required": chain.approvals_required(),
})),
"/-/intake/set" => match pull.set_as(&chain, &auth_cfg, identity.as_ref(), &id) {
Ok(v) => ok(serde_json::to_value(v).unwrap_or_default()),
Err(e) => (e.status, e.message),
},
"/-/intake/set/approve" => answer(pull.approve_set(
&chain, &auth_cfg, identity.as_ref(), &id, &revision, &reason, &peer_s,
)),
_ => answer(pull.refuse_set(
&chain, &auth_cfg, identity.as_ref(), &id, &revision, &reason, &peer_s,
)),
}
})
.await
.unwrap_or_else(|_| (500, "dispatch task failed".into()));
crate::grpc::functional_status("holger-http/intake", "intake_sets", reply.0 < 400, &path);
return Ok(if reply.0 == 200 {
Response::builder()
.status(200)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(reply.1)).boxed())
.unwrap_or_else(|_| text(500, "Internal error"))
} else {
text(reply.0, &reply.1)
});
}
// ★ THE SYNC DOORS (Holger v2.0 H4, `crate::sync`): the upstream's manifest, the
// downstream's ack, the board's sync data, an invalidation, a stick's manifest.
#[cfg(feature = "sync")]
if path.starts_with("/-/sync/") {
return Ok(sync_door(req, &settings, client_cn.as_deref(), &peer, &method, &path).await);
}
// ★ THE BOX'S IMPORT DOORS (lane W4, `crate::import_door`), the HTTP twin of
// gRPC `AdminService.ImportIntelBundle` / `ImportBundle`, the same code as the
// CLI verbs and the inbox jobs:
// POST /-/intel/import body: the signed intel bundle
// POST /-/bundle/import body: the signed update / airgap bundle
// POST /-/bundle/first-import?vendor=&product=&sequence= an admin authorizes a first import
// (`?first_import=` is REMOVED, lane HOL-CDS: `holger-import-10`).
// Repository Admin or Security Approver (`holger-duty-2` otherwise); the
// twin, growth and read-only holds above already refused a POST that must
// not mutate. The body streams into a staged file under the data root.
#[cfg(feature = "intake")]
if path == "/-/bundle/first-import" {
if method != "POST" {
return Ok(text(405, "Method not allowed"));
}
let Some(door) = settings.imports.clone() else {
return Ok(text(
404,
&holger_errcode::SERVER_CAPABILITY_NOT_CONFIGURED
.tag("this server has no import door"),
));
};
let bearer = credential_of(&req);
let identity = match auth::validate_request(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
)
.await
{
Ok(id) => id,
Err(_) => {
return Ok(text(
401,
&holger_errcode::AUTH_UNAUTHENTICATED.tag("invalid credentials"),
))
}
};
if settings.auth_config.rbac_enabled() && identity.is_none() {
return Ok(text(
401,
&holger_errcode::AUTH_UNAUTHENTICATED
.tag("an authenticated admin authorizes a first import"),
));
}
let principal = match crate::import_door::ImportDoor::authorize_admin(
&settings.auth_config,
identity.as_ref(),
) {
Ok(p) => p,
Err(e) => return Ok(text(e.status, &e.tagged())),
};
let q: Vec<(String, String)> = req
.uri()
.query()
.unwrap_or("")
.split('&')
.filter_map(|p| {
p.split_once('=')
.map(|(k, v)| (k.to_string(), form_decode(v)))
})
.collect();
let get = |k: &str| {
q.iter()
.find(|(n, _)| n == k)
.map(|(_, v)| v.clone())
.unwrap_or_default()
};
let sequence = get("sequence").parse::<u64>().unwrap_or(0);
let (vendor, product, source) = (get("vendor"), get("product"), peer.clone());
let r = tokio::task::spawn_blocking(move || {
door.allow_first_import(&vendor, &product, sequence, &principal, &source)
})
.await;
return Ok(match r {
Ok(Ok(g)) => Response::builder()
.status(200)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(serde_json::to_vec(&g).unwrap_or_default())).boxed())
.unwrap_or_else(|_| text(500, "Internal error")),
Ok(Err(e)) => text(e.status, &e.tagged()),
Err(_) => text(500, "the authorization task failed"),
});
}
#[cfg(feature = "intake")]
if path == "/-/intel/import" || path == "/-/bundle/import" {
use crate::import_door::{ImportDoor, ImportKind};
let kind = if path == "/-/intel/import" {
ImportKind::Intel
} else {
ImportKind::Bundle
};
if method != "POST" {
return Ok(text(405, "Method not allowed"));
}
let Some(door) = settings.imports.clone() else {
return Ok(text(
404,
&holger_errcode::SERVER_CAPABILITY_NOT_CONFIGURED
.tag("this server has no import door"),
));
};
let bearer = credential_of(&req);
let identity = match auth::validate_request(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
)
.await
{
Ok(id) => id,
Err(_) => {
return Ok(text(
401,
&holger_errcode::AUTH_UNAUTHENTICATED.tag("invalid credentials"),
))
}
};
if settings.auth_config.rbac_enabled() && identity.is_none() {
return Ok(text(
401,
&holger_errcode::AUTH_UNAUTHENTICATED
.tag("an authenticated identity is required to import a bundle"),
));
}
let principal = match ImportDoor::authorize(&settings.auth_config, identity.as_ref(), kind)
{
Ok(p) => p,
Err(e) => {
// HOL-NCSC: an unauthorised importer leaves an audit row.
let who = crate::promote::duties::principal_of(identity.as_ref());
crate::refusals::record_to(
&settings.audit,
"import authorization",
&who,
&peer,
&format!("{} import by {who}: {}", kind.as_str(), e.tagged()),
);
return Ok(text(e.status, &e.tagged()));
}
};
if req
.uri()
.query()
.unwrap_or("")
.split('&')
.any(|p| p == "first_import" || p.starts_with("first_import="))
{
let e = crate::import_door::first_import_flag_refusal("`?first_import=`");
return Ok(text(e.status, &e.tagged()));
}
let mut staged = match door.stage() {
Ok(s) => s,
Err(e) => return Ok(text(e.status, &e.tagged())),
};
let mut limited = Limited::new(req.into_body(), settings.max_body_bytes);
loop {
match limited.frame().await {
Some(Ok(frame)) => {
if let Ok(chunk) = frame.into_data() {
if let Err(e) = staged.write(&chunk) {
return Ok(text(e.status, &e.tagged()));
}
}
}
Some(Err(_)) => return Ok(text(413, "Payload too large")),
None => break,
}
}
if let Err(e) = staged.finish() {
return Ok(text(e.status, &e.tagged()));
}
let source = peer.clone();
let reply = tokio::task::spawn_blocking(
move || -> Result<String, crate::import_door::ImportError> {
let body = match kind {
ImportKind::Intel => serde_json::to_string(&door.import_intel(
&staged.path,
None,
&principal,
&source,
)?),
ImportKind::Bundle => serde_json::to_string(&door.import_bundle(
&staged.path,
None,
&Default::default(),
&principal,
&source,
)?),
};
drop(staged);
Ok(body.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}")))
},
)
.await
.unwrap_or_else(|_| {
Err(crate::import_door::ImportError {
code: &holger_errcode::IMPORT_STAGING_FAILED,
message: "the import task failed".into(),
status: 500,
})
});
crate::grpc::functional_status(
"holger-http/import",
"bundle_imported",
reply.is_ok(),
&path,
);
return Ok(match reply {
Ok(body) => Response::builder()
.status(200)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(body)).boxed())
.unwrap_or_else(|_| text(500, "Internal error")),
Err(e) => text(e.status, &e.tagged()),
});
}
// GET /-/intake/evidence?sha256= every stored scan-evidence record (W5)
// GET /-/intake/sbom?sha256= the SBOM the newest record carries
// The SAME read as gRPC `PromotionService.GetArtifactEvidence`
// (`duties::evidence_as`, by the coordinate holding these bytes): the read
// duty of GetArtifactState, so a Developer reads only AVAILABLE bytes.
#[cfg(feature = "intake")]
if path == "/-/intake/evidence" || path == crate::promote::duties::SBOM_ROUTE {
if method != "GET" {
return Ok(text(405, "Method not allowed"));
}
let sbom_only = path == crate::promote::duties::SBOM_ROUTE;
let Some(chain) = settings.intake.clone() else {
return Ok(text(
404,
&holger_errcode::SERVER_CAPABILITY_NOT_CONFIGURED
.tag("this server declares no intake chain (`promotion.intake`)"),
));
};
let bearer = credential_of(&req);
let identity = match auth::validate_request(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
)
.await
{
Ok(id) => id,
Err(_) => {
return Ok(text(
401,
&holger_errcode::AUTH_UNAUTHENTICATED.tag("invalid credentials"),
))
}
};
if settings.auth_config.rbac_enabled() && identity.is_none() {
return Ok(text(
401,
&holger_errcode::AUTH_UNAUTHENTICATED
.tag("an authenticated identity is required to read intake evidence"),
));
}
let raw_q = req.uri().query().unwrap_or("").to_owned();
let sha = raw_q
.split('&')
.filter_map(|p| p.split_once('='))
.find(|(k, _)| *k == "sha256")
.map(|(_, v)| form_decode(v))
.unwrap_or_default();
let digest =
match crate::attest::ArtifactDigest::sha256(sha.trim().trim_start_matches("sha256:")) {
Ok(d) => d,
Err(e) => return Ok(text(400, &e.tagged())),
};
let auth_cfg = Arc::clone(&settings.auth_config);
let reply = tokio::task::spawn_blocking(move || -> Result<(u16, &'static str, Vec<u8>), crate::promote::intake::IntakeError> {
let v = crate::promote::duties::evidence_by_sha256_as(&chain, &auth_cfg, identity.as_ref(), digest.hex())?;
if sbom_only {
return Ok(match v.sbom() {
Some(doc) => (200, "application/vnd.cyclonedx+json", serde_json::to_vec_pretty(doc).unwrap_or_default()),
None => (
404,
"text/plain",
holger_errcode::SERVER_SBOM_UNKNOWN
.tag(format!("no stored scan evidence about sha256 {} carries an SBOM", v.sha256))
.into_bytes(),
),
});
}
use base64::Engine as _;
let b64 = |b: &[u8]| base64::engine::general_purpose::STANDARD.encode(b);
let records: Vec<serde_json::Value> = v
.stored
.iter()
.map(|r| {
serde_json::json!({
"id": r.id, "scanned_at": r.evidence.scanned_at,
"verdict": r.evidence.verdict.as_str(), "reasons": r.evidence.reasons,
"evidence": r.evidence,
"envelope": r.envelope.as_deref().map(|e| String::from_utf8_lossy(e).into_owned()),
"signed": r.envelope.is_some(),
"stale": r.stale,
})
})
.collect();
let body = serde_json::json!({
"sha256": v.sha256,
"state": v.state.name(),
"policy": v.policy,
"records": records,
"signer_cert_der_base64": if v.signer_cert.is_empty() { None } else { Some(b64(&v.signer_cert)) },
"sbom_link": v.sbom_link,
});
Ok((200, "application/json", body.to_string().into_bytes()))
})
.await
.unwrap_or_else(|_| Err(crate::promote::intake::IntakeError::Store("dispatch task failed".into())));
crate::grpc::functional_status(
"holger-http/intake",
"intake_evidence",
reply.is_ok(),
&path,
);
return Ok(match reply {
Ok((code, ct, body)) => Response::builder()
.status(code)
.header("Content-Type", ct)
.body(Full::new(Bytes::from(body)).boxed())
.unwrap_or_else(|_| text(500, "Internal error")),
Err(e) => text(e.status(), &e.tagged()),
});
}
// Custom-PROPERTIES WRITE door (parity §8): set/remove a property remotely —
// the write twin of the read door above + the gRPC `PropertyService.SetProperty`
// RPC (property write was CLI/in-process only before this).
// PUT|POST /-/properties?repo=&namespace=&name=&version=&key=&value=&value=…
// DELETE /-/properties?repo=&…&key= (remove the key)
// Write-gated (writer/admin), SCOPED to the TARGET repo from the query string
// (not the `-` path segment), mirroring the gRPC write gate. Values ride in
// repeatable `value=` params, so there is no artifact body / checksum gate.
#[cfg(feature = "intake")]
if path == "/-/properties" && matches!(method.as_str(), "PUT" | "POST" | "DELETE") {
let raw_q = req.uri().query().unwrap_or("").to_owned();
let coord = parse_sbom_coord(&raw_q);
// AuthN: resolve identity (OIDC sub / mTLS CN), or `None` when auth is open.
let bearer = credential_of(&req);
let identity = match auth::validate_request(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
)
.await
{
Ok(id) => id,
Err(_) => return Ok(text(401, "Unauthorized")),
};
// …and an ADMIN floor on top for the keys the SERVER ITSELF reads as
// policy — the promotion gate's required property,
// and the retention pin. Identical rule and identical placement to the
// gRPC `SetProperty` handler, because this door writes the same store: a
// Writer refused there must not simply come here instead. Applied BEFORE
// the write gate's own refusal is recorded, so a reserved key is refused
// the same way whether or not properties are configured at all.
//
// A note on method: PUT/POST with no `value=` REMOVES the key (see
// `serve_properties_write`), so removal rides the writer-gated verb, not
// the admin-gated DELETE. That is precisely why the floor cannot be left
// to `authorize_http_write`'s DELETE-is-admin rule.
{
let (key, _) = property_key_and_values(&raw_q);
if !key.is_empty()
&& crate::properties::is_reserved_property(
&key,
settings.promotion_required_property.as_ref(),
&settings.reserved_properties,
)
&& settings.auth_config.rbac_enabled()
{
let permitted = identity.as_ref().is_some_and(|id| {
settings
.auth_config
.role_for_identity_in_repo(&coord.repository, id)
.can_admin()
});
if !permitted {
let who = identity
.as_ref()
.map(|i| i.subject.as_str())
.unwrap_or("anonymous");
let status = if identity.is_some() { 403 } else { 401 };
let ev = AuditEvent::new(
who,
AuditAction::Upload,
&coord.repository,
&path,
&peer,
status,
0,
)
.with_detail(format!("reserved property '{key}' requires admin"));
if let Err(e) = settings.audit.record(ev) {
log::warn!("audit record failed: {e}");
}
crate::grpc::functional_status(
"holger-http/properties",
"reserved_property_permitted",
false,
&key,
);
return Ok(text(status, "Forbidden"));
}
}
}
// AuthZ (writer/admin), scoped to the coordinate's repository.
if let Err((status, detail)) = authorize_http_write_floored(
&settings.auth_config,
&settings.read_gate,
identity.as_ref(),
&method,
&coord.repository,
) {
let who = identity
.as_ref()
.map(|i| i.subject.as_str())
.unwrap_or("anonymous");
let ev = AuditEvent::new(
who,
AuditAction::Upload,
&coord.repository,
&path,
&peer,
status,
0,
)
.with_detail(detail.to_string());
if let Err(e) = settings.audit.record(ev) {
log::warn!("audit record failed: {e}");
}
let msg = if status == 403 {
"Forbidden"
} else {
"Unauthorized"
};
return Ok(text(status, msg));
}
let who = identity
.map(|i| i.subject)
.unwrap_or_else(|| "anonymous".to_string());
let (status, body, repo, label) =
serve_properties_write(settings.properties.as_ref(), &method, &raw_q);
let action = if method == "DELETE" {
AuditAction::Delete
} else {
AuditAction::Upload
};
let ev = AuditEvent::new(&who, action, &repo, &label, &peer, status, 0);
if let Err(e) = settings.audit.record(ev) {
log::warn!("audit record failed: {e}");
}
crate::grpc::functional_status(
"holger-http/properties_write",
"property_written",
status == 200,
&format!("{status} {label}"),
);
crate::metrics::global().record_request(
if method == "DELETE" {
crate::metrics::Verb::Delete
} else {
crate::metrics::Verb::Put
},
status,
0,
);
crate::metrics::global().observe_request_duration(started.elapsed().as_secs_f64());
let ct = if status == 200 {
"application/json"
} else {
"text/plain"
};
return Ok(Response::builder()
.status(status)
.header("Content-Type", ct)
.body(Full::new(Bytes::from(body)).boxed())
.unwrap_or_else(|_| text(500, "Internal error")));
}
// Map the HTTP verb to an audit action: reads (GET/HEAD) are downloads,
// DELETE removes, everything else (PUT/POST/PATCH) stores.
let is_read = method == "GET" || method == "HEAD";
let action = if is_read {
AuditAction::Download
} else if method == "DELETE" {
AuditAction::Delete
} else {
AuditAction::Upload
};
// Repository name for the audit record: the first OCI/classic path segment.
let repo_key = route_key(&path).unwrap_or_default().to_owned();
// The repository half of the visitor tally. Only a name the configuration
// declares is counted (the tally ignores any other), so a probe for
// `/no-such-repo/x` cannot grow it.
settings.visitors.record(
crate::visitors::now_unix(),
None,
Some(&repo_key),
client_address(&peer, req.headers(), settings.trusted_proxies.as_deref()),
);
// Prometheus verb for this request, via the ONE shared audit-action→verb
// classifier (Law #5 — same mapping the gRPC `record_audit` choke point uses).
// The artifact door only ever produces Download/Upload/Delete here, all of
// which classify; `Get` is a safe default that can't be reached.
let metric_verb =
crate::metrics::Verb::from_audit_action(action).unwrap_or(crate::metrics::Verb::Get);
// Best-effort audit append, mirroring the gRPC `record_audit` helper. Every
// outcome also bumps the process-global metrics registry (`/metrics`), so a
// verb, its bytes served, and any 5xx are counted at the one exit point.
// F1: the client and the front it came through, from a trusted proxy.
let source = audit_source(&peer, req.headers(), settings.trusted_proxies.as_deref());
let record = |ident: &str, status: u16, bytes: u64, detail: &str| {
crate::metrics::global().record_request(metric_verb, status, bytes);
crate::metrics::global().observe_request_duration(started.elapsed().as_secs_f64());
let mut ev = AuditEvent::new(ident, action, &repo_key, &path, &source, status, bytes);
if !detail.is_empty() {
ev = ev.with_detail(detail);
}
if let Err(e) = settings.audit.record(ev) {
log::warn!("audit record failed: {e}");
}
};
// Writes require a valid identity (Bearer or mTLS CN) and, when role policy
// is configured, authorization. Reads are OPEN BY DEFAULT — the airgap mirror
// posture — and record as the anonymous principal, UNLESS the operator opted
// into the read gate (`require_auth_for_reads`), in which case a read
// authenticates exactly like a write and is authorized against the Reader
// floor, scoped to the target repo. Opt-in: with the gate off this branch is
// the original two lines.
// The writer's identity, kept for the intake chain's Importer rule (lane W7).
#[allow(unused_mut, unused_variables)]
let mut writer: Option<auth::AuthIdentity> = None;
// A reader's identity, when the read named a valid one: a scanned registry lets a pusher
// read its own staged manifest back by digest (lane HOL-FIELD-2).
#[allow(unused_mut, unused_variables)]
let mut reader: Option<auth::AuthIdentity> = None;
let ident = if is_read {
if settings.read_gate.applies(&settings.auth_config, &repo_key) {
let bearer = credential_of(&req);
let identity = match auth::validate_request_for(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
&repo_key,
false,
)
.await
{
Ok(id) => id,
#[cfg(feature = "auth-passkey")]
Err(auth::DoorRefusal::Token(why)) => {
record("anonymous", why.status, 0, why.code.code);
return Ok(token_refused(&why));
}
Err(_) => {
record("anonymous", 401, 0, "unauthorized (read gate)");
crate::grpc::functional_status(
"holger-http/read_gate",
"rbac_read_permitted",
false,
"invalid or missing read credentials",
);
if bearer.is_none() && client_cn.is_none() {
return Ok(read_gated(&repo_key));
}
return Ok(unauthorized());
}
};
if let Err((status, detail)) = crate::auth::authorize_read_identity_scoped(
&settings.auth_config,
&settings.read_gate,
identity.as_ref(),
&repo_key,
) {
let who = identity
.as_ref()
.map(|i| i.subject.as_str())
.unwrap_or("anonymous");
record(who, status, 0, detail);
crate::grpc::functional_status(
"holger-http/read_gate",
"rbac_read_permitted",
false,
detail,
);
if status == 401 {
return Ok(read_gated(&repo_key));
}
return Ok(text(status, "Forbidden"));
}
#[cfg(feature = "intake")]
{
reader = identity.clone();
}
// Gated reads audit the REAL subject, not "anonymous" — the whole point
// of authenticating a download is knowing who took it.
let who = identity
.map(|i| i.subject)
.unwrap_or_else(|| "anonymous".to_string());
crate::grpc::functional_status(
"holger-http/read_gate",
"rbac_read_permitted",
true,
&who,
);
who
} else {
// An OPEN read stays open — a missing or invalid credential reads as
// anonymous, exactly as before — but a VALID one names its reader,
// for the audit and for admission: the anonymous day share's own
// refusal says "or authenticate" (MEASURED on the holger mock
// 2026-09-29: an operator's authenticated fetch-back was charged to
// the anonymous 5 GB/day and refused).
let bearer = credential_of(&req);
if bearer.is_some() || client_cn.is_some() {
match auth::validate_request_for(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
&repo_key,
false,
)
.await
{
Ok(Some(id)) => {
#[cfg(feature = "intake")]
{
reader = Some(id.clone());
}
id.subject
}
// A token that is revoked, expired or whose owner is gone is told so even
// on an open read: its holder must learn it stopped working. A live token
// for another repository reads here as anyone may.
#[cfg(feature = "auth-passkey")]
Err(auth::DoorRefusal::Token(why)) if why.status == 401 => {
record("anonymous", why.status, 0, why.code.code);
return Ok(token_refused(&why));
}
_ => "anonymous".to_string(),
}
} else {
"anonymous".to_string()
}
}
} else {
let bearer = credential_of(&req);
// AuthN: resolve the identity (OIDC sub / mTLS CN), or `None` when auth is
// open (no methods configured).
let identity = match auth::validate_request_for(
&settings.auth_config,
bearer.as_deref(),
client_cn.as_deref(),
&repo_key,
true,
)
.await
{
Ok(id) => id,
#[cfg(feature = "auth-passkey")]
Err(auth::DoorRefusal::Token(why)) => {
record("anonymous", why.status, 0, why.code.code);
return Ok(token_refused(&why));
}
Err(_) => {
record("anonymous", 401, 0, "unauthorized");
return Ok(unauthorized());
}
};
// AuthZ (RBAC) — mirrors `grpc::authorize_write`, scoped to the target repo
// so a per-repo ACL (`repo_roles`) governs this write. `cargo yank` is cargo's
// DELETE of `…/yank`: on the chain's cargo door it deletes nothing (it is recorded
// beside the chain), so it is a write, and the door asks for its duty
// (HOL-CARGO-PUBLISH-DOOR).
#[cfg(feature = "intake")]
let yank_on_door = method == "DELETE"
&& path.ends_with("/yank")
&& settings
.intake
.as_ref()
.is_some_and(|c| c.cargo_door() == Some(repo_key.as_str()));
#[cfg(not(feature = "intake"))]
let yank_on_door = false;
if let Err((status, detail)) = authorize_http_write_floored(
&settings.auth_config,
&settings.read_gate,
identity.as_ref(),
if yank_on_door { "PUT" } else { &method },
&repo_key,
) {
let who = identity
.as_ref()
.map(|i| i.subject.as_str())
.unwrap_or("anonymous");
record(who, status, 0, detail);
// ★ A write refused for want of a credential is answered with the
// CHALLENGE, not a bare 401: that is what makes `docker push`,
// `npm publish` and `mvn deploy` send the credential they were given
// instead of stopping at the first refusal.
if status == 401 {
return Ok(unauthorized());
}
return Ok(text(status, "Forbidden"));
}
#[cfg(feature = "intake")] // read only by the intake chain's rules
{
writer = identity.clone();
}
identity
.map(|i| i.subject)
.unwrap_or_else(|| "anonymous".to_string())
};
let backend = match routes_lookup(&settings.routes, &path) {
Some(b) => b,
None => {
record(&ident, 404, 0, "unknown repository");
return Ok(text(404, "Unknown repository"));
}
};
// ★ DOWNLOAD / UPLOAD ADMISSION (`crate::admission`). Asked once the
// repository and the principal are known and BEFORE any byte is read or a
// body collected: a consumer over its share is answered at once with 503 +
// Retry-After — never parked. A DELETE is not asked (it moves no bytes and
// is how a full repository gets back under its limit). With no
// `admission:` block this plans `Full`, refuses nothing and holds no share.
#[cfg(not(feature = "admission"))]
let ticket: Option<Arc<crate::admission::Ticket>> = None;
#[cfg(feature = "admission")]
let ticket: Option<Arc<crate::admission::Ticket>> = if method == "DELETE" {
None
} else {
let who = admission_who(&settings, &ident, &peer, req.headers());
let (work, hint) = if is_read {
(crate::admission::WorkKind::Download, 0)
} else {
(
crate::admission::WorkKind::Upload,
declared_body_len(&req, settings.max_body_bytes) as u64,
)
};
match settings.admission.enter(&who, work, hint) {
Ok(t) => Some(Arc::new(t)),
Err(busy) => {
record(&ident, 503, 0, &busy.why);
crate::grpc::functional_status(
"holger-http/admission",
"admitted",
false,
&busy.why,
);
return Ok(retry_later(
busy.retry_after_secs,
&holger_admission_line(&busy.why),
));
}
}
};
// ★ THE INTAKE CHAIN (`crate::promote::intake`). A repository in the chain
// is never served by its backend: incoming, quarantine and approved serve
// nothing, and production serves an AVAILABLE artifact from the SEALED
// store. A write into quarantine, approved or production is refused by
// name; the chain alone moves artifacts there.
#[cfg(feature = "intake")]
let intake_role = settings
.intake
.as_ref()
.and_then(|c| c.role_of(&repo_key).map(|r| (Arc::clone(c), r)));
// ★ THE CHAIN'S NPM DOOR (`crate::promote::npm`, lane P5): the repository
// `promotion.intake.npm` names holds nothing of its own. `npm publish` is INGESTED into
// the chain's incoming repository; the packument, the tarballs and the dist-tags are a
// view of the AVAILABLE set. Unpublish and every rewrite are refused by policy.
#[cfg(feature = "intake")]
let npm_door: Option<(
Arc<crate::promote::intake::IntakeChain>,
crate::promote::npm::Route,
)> = settings
.intake
.as_ref()
.filter(|c| c.npm_door() == Some(repo_key.as_str()))
.map(|c| {
(
Arc::clone(c),
crate::promote::npm::route(
path.strip_prefix(&format!("/{repo_key}")).unwrap_or(""),
),
)
});
#[cfg(feature = "intake")]
let npm_write = npm_door.is_some() && !is_read;
#[cfg(not(feature = "intake"))]
let npm_write = false;
#[cfg(feature = "intake")]
if let Some((chain, route)) = &npm_door {
use crate::promote::intake::IntakeError;
use crate::promote::npm::{NpmError, Route};
if is_read {
let base = chain
.npm_base()
.map(str::to_string)
.unwrap_or_else(|| npm_request_base(&req, &settings, &peer));
let tarball_base = format!("{base}/{repo_key}");
// A tarball streams from the sealed store (HOL-PERF-3).
if let Some(id) = crate::promote::intake::IntakeChain::npm_tarball_id(route) {
let c2 = Arc::clone(chain);
let opened = tokio::task::spawn_blocking(move || {
c2.serve_stream(&id, STREAM_READ_PIECE_BYTES)
})
.await
.unwrap_or_else(|_| Err(IntakeError::Store("dispatch task failed".into())));
return Ok(match opened {
Ok(Some(chunks)) => {
let len = chunks.len.unwrap_or(0);
record(&ident, 200, len, "npm: a view of the AVAILABLE set");
let mut builder = Response::builder()
.status(200)
.header("Content-Type", "application/octet-stream")
.header("X-Holger-Intake-State", "AVAILABLE");
if let Some(n) = chunks.len {
builder = builder.header("Content-Length", n);
}
let body = if method == "HEAD" {
Full::new(Bytes::new()).boxed()
} else {
if let Some(t) = &ticket {
t.add_net_out(len);
}
reply_body(chunks, ticket.clone(), path.clone()).await
};
builder
.body(body)
.unwrap_or_else(|_| text(500, "Internal error"))
}
Ok(None) => {
record(&ident, 404, 0, "npm: nothing AVAILABLE here");
npm_reply(404, &serde_json::json!({ "error": "not found" }))
}
Err(e) => {
record(&ident, e.status(), 0, &e.tagged());
log::warn!("npm read of {path} failed: {e}");
npm_refusal(&e)
}
});
}
let (c2, r2) = (Arc::clone(chain), route.clone());
let read = tokio::task::spawn_blocking(move || c2.npm_read(&r2, &tarball_base))
.await
.unwrap_or_else(|_| Err(IntakeError::Store("dispatch task failed".into())));
return Ok(match read {
Ok(Some((ctype, body))) => {
let len = body.len() as u64;
record(&ident, 200, len, "npm: a view of the AVAILABLE set");
if let Some(t) = &ticket {
t.add_net_out(len);
}
let body = if method == "HEAD" { Bytes::new() } else { body };
Response::builder()
.status(200)
.header("Content-Type", ctype)
.header("Content-Length", len)
.header("X-Holger-Intake-State", "AVAILABLE")
.body(Full::new(body).boxed())
.unwrap_or_else(|_| text(500, "Internal error"))
}
Ok(None) => {
record(&ident, 404, 0, "npm: nothing AVAILABLE here");
npm_reply(404, &serde_json::json!({ "error": "not found" }))
}
Err(e) => {
record(&ident, e.status(), 0, &e.tagged());
log::warn!("npm read of {path} failed: {e}");
npm_refusal(&e)
}
});
}
// A write: what it is decides who may make it, before a byte of the body is read.
let refusal = match (method.as_str(), route) {
("PUT", Route::Packument(_)) => {
crate::promote::duties::may_ingest(&settings.auth_config, writer.as_ref())
.err()
.map(IntakeError::Duty)
}
("PUT" | "DELETE", Route::DistTag { .. }) => {
crate::promote::npm::may_tag(&settings.auth_config, writer.as_ref())
.err()
.map(IntakeError::Duty)
}
(_, Route::Rewrite(_)) | ("DELETE", _) => Some(IntakeError::Npm(NpmError::Rewrite(
format!("{method} {path}"),
))),
_ => Some(IntakeError::Npm(NpmError::Publish(format!(
"{method} {path} is not a write the npm door takes (npm publish, npm dist-tag)"
)))),
};
if let Some(e) = refusal {
record(&ident, e.status(), 0, &e.tagged());
return Ok(refuse_unread(req, npm_refusal(&e)).await);
}
}
// ★ THE CHAIN'S CARGO DOOR (`crate::promote::cargo`, lane HOL-CARGO-PUBLISH-DOOR): the
// repository `promotion.intake.cargo` names holds nothing of its own. `cargo publish` is
// INGESTED into the chain's incoming repository; the sparse index and the downloads are a
// view of the AVAILABLE set; `cargo yank` is recorded for AVAILABLE versions only.
#[cfg(feature = "intake")]
let cargo_door: Option<(
Arc<crate::promote::intake::IntakeChain>,
crate::promote::cargo::Route,
)> = settings
.intake
.as_ref()
.filter(|c| c.cargo_door() == Some(repo_key.as_str()))
.map(|c| {
(
Arc::clone(c),
crate::promote::cargo::route(
path.strip_prefix(&format!("/{repo_key}")).unwrap_or(""),
),
)
});
#[cfg(feature = "intake")]
let cargo_write = cargo_door.is_some() && !is_read;
#[cfg(not(feature = "intake"))]
let cargo_write = false;
#[cfg(feature = "intake")]
if let Some((chain, route)) = &cargo_door {
use crate::promote::cargo::{CargoError, Route};
use crate::promote::intake::IntakeError;
if is_read {
if let Route::Download { name, version } = route {
let Some(id) = chain.cargo_download_id(name, version) else {
return Ok(cargo_reply(
404,
&crate::promote::cargo::errors_doc("not found"),
));
};
let c2 = Arc::clone(chain);
let opened = tokio::task::spawn_blocking(move || {
c2.serve_stream(&id, STREAM_READ_PIECE_BYTES)
})
.await
.unwrap_or_else(|_| Err(IntakeError::Store("dispatch task failed".into())));
return Ok(match opened {
Ok(Some(chunks)) => {
let len = chunks.len.unwrap_or(0);
record(&ident, 200, len, "cargo: a view of the AVAILABLE set");
let mut builder = Response::builder()
.status(200)
.header("Content-Type", "application/octet-stream")
.header("X-Holger-Intake-State", "AVAILABLE");
if let Some(n) = chunks.len {
builder = builder.header("Content-Length", n);
}
let body = if method == "HEAD" {
Full::new(Bytes::new()).boxed()
} else {
if let Some(t) = &ticket {
t.add_net_out(len);
}
reply_body(chunks, ticket.clone(), path.clone()).await
};
builder
.body(body)
.unwrap_or_else(|_| text(500, "Internal error"))
}
Ok(None) => {
record(&ident, 404, 0, "cargo: nothing AVAILABLE here");
cargo_reply(
404,
&crate::promote::cargo::errors_doc(&format!(
"{name}@{version} is not AVAILABLE here"
)),
)
}
Err(e) => {
record(&ident, e.status(), 0, &e.tagged());
log::warn!("cargo read of {path} failed: {e}");
cargo_reply(e.status(), &crate::promote::cargo::errors_doc(&e.tagged()))
}
});
}
let (ctype, body): (&str, Option<Vec<u8>>) = match route {
Route::Config => {
let base = chain
.cargo_base()
.map(str::to_string)
.unwrap_or_else(|| npm_request_base(&req, &settings, &peer));
let gated = settings.read_gate.applies(&settings.auth_config, &repo_key);
(
"application/json",
Some(crate::promote::cargo::config_json(&base, &repo_key, gated)),
)
}
Route::Index(name) => {
let (c2, n2) = (Arc::clone(chain), name.clone());
let lines = tokio::task::spawn_blocking(move || c2.cargo_index(&n2))
.await
.ok()
.flatten();
("text/plain", lines.map(String::into_bytes))
}
_ => ("text/plain", None),
};
return Ok(match body {
Some(body) => {
let len = body.len() as u64;
record(&ident, 200, len, "cargo: a view of the AVAILABLE set");
if let Some(t) = &ticket {
t.add_net_out(len);
}
let body = if method == "HEAD" { Vec::new() } else { body };
Response::builder()
.status(200)
.header("Content-Type", ctype)
.header("Content-Length", len)
.header("X-Holger-Intake-State", "AVAILABLE")
.body(Full::new(Bytes::from(body)).boxed())
.unwrap_or_else(|_| text(500, "Internal error"))
}
None => {
record(&ident, 404, 0, "cargo: nothing AVAILABLE here");
text(
404,
"not found: nothing of it is AVAILABLE in this Holger's intake chain",
)
}
});
}
// A write: what it is decides who may make it, before a byte of the body is read.
let refusal = match (method.as_str(), route) {
("PUT", Route::Publish) => {
crate::promote::duties::may_ingest(&settings.auth_config, writer.as_ref())
.err()
.map(IntakeError::Duty)
}
("DELETE", Route::Yank { .. }) | ("PUT", Route::Unyank { .. }) => {
crate::promote::cargo::may_yank(&settings.auth_config, writer.as_ref())
.err()
.map(IntakeError::Duty)
}
_ => Some(IntakeError::Cargo(CargoError::Publish(format!(
"{method} {path} is not a write the cargo door takes (cargo publish, cargo yank)"
)))),
};
if let Some(e) = refusal {
record(&ident, e.status(), 0, &e.tagged());
return Ok(refuse_unread(
req,
cargo_reply(e.status(), &crate::promote::cargo::errors_doc(&e.tagged())),
)
.await);
}
}
// ★ A SCANNED REGISTRY (`crate::promote::oci_push`, lane HOL-OCI-EVIDENCE): a writable
// `oci` repository of a chain Holger serves only what its pushes brought AVAILABLE; a write
// needs the Importer duty (refused before its body), a DELETE is refused, and a manifest
// PUT by tag takes the image into the chain once its body is read (below).
#[cfg(feature = "intake")]
let oci_push: Option<Arc<crate::promote::intake::IntakeChain>> = settings
.intake
.as_ref()
.filter(|c| c.scans_pushes_to(&repo_key))
.cloned();
#[cfg(feature = "intake")]
let oci_push_manifest: Option<(String, String)> = match &oci_push {
Some(_) if !is_read && method == "PUT" => {
crate::promote::oci_push::rest_of(&path_and_query, &repo_key)
.and_then(crate::promote::oci_push::manifest_target)
}
_ => None,
};
#[cfg(not(feature = "intake"))]
let oci_push_manifest: Option<(String, String)> = None;
#[cfg(feature = "intake")]
if let Some(chain) = &oci_push {
// SEC-REVIEW 2026-11-01 P3: a scanned registry answers only on its OCI door's paths.
// A path outside `/v2/<repo>/` (`/oci/_blob/<hex>`) missed the view and reached the
// raw backend, which holds staged and BLOCKED pushes.
if crate::promote::oci_push::rest_of(&path_and_query, &repo_key).is_none() {
let e = crate::promote::intake::IntakeError::DoorRefused {
repo: repo_key.clone(),
why: format!("a scanned registry answers only on its OCI door, /v2/{repo_key}/…"),
};
let status = if is_read { 404 } else { e.status() };
record(&ident, status, 0, &e.tagged());
return Ok(refuse_unread(req, text(status, &e.tagged())).await);
}
if is_read {
if let Some(rest) =
crate::promote::oci_push::rest_of(&path_and_query, &repo_key).map(str::to_string)
{
let (c2, b2, attest, repo2) = (
Arc::clone(chain),
Arc::clone(&backend),
settings.attest.clone(),
repo_key.clone(),
);
// A pusher: a named reader who may write this registry and ingest.
let pusher = reader.is_some()
&& authorize_http_write_floored(
&settings.auth_config,
&settings.read_gate,
reader.as_ref(),
"PUT",
&repo_key,
)
.is_ok()
&& crate::promote::duties::may_ingest(&settings.auth_config, reader.as_ref())
.is_ok();
let r = tokio::task::spawn_blocking(move || {
c2.oci_push()
.map(|p| p.read(&c2, b2.as_ref(), attest.as_deref(), &repo2, &rest, pusher))
})
.await
.ok()
.flatten();
if let Some(r) = r {
record(&ident, r.status, r.body.len() as u64, &r.note);
return Ok(oci_push_reply(r, &method, ticket.as_deref()));
}
}
} else if method == "DELETE" {
let e = crate::promote::oci_push::delete_refused(&repo_key);
record(&ident, e.status(), 0, &e.tagged());
return Ok(refuse_unread(req, text(e.status(), &e.tagged())).await);
} else if let Err(e) =
crate::promote::duties::may_ingest(&settings.auth_config, writer.as_ref())
{
let e = crate::promote::intake::IntakeError::Duty(e);
record(&ident, e.status(), 0, &e.tagged());
return Ok(refuse_unread(req, text(e.status(), &e.tagged())).await);
}
}
// The chain's bundles door (lane HOL-BUNDLES-DOOR): a PUT is a signed Holger bundle,
// staged, verified in the `verify-bundle` job against the door's trust policy, and only
// then stored as it came; anything else is refused by name (`holger-bundles-1`). A DELETE
// keeps the repository's own rule; every other write is refused.
#[cfg(feature = "intake")]
if !is_read
&& settings
.intake
.as_ref()
.is_some_and(|c| c.bundles_door() == Some(repo_key.as_str()))
&& method != "DELETE"
{
let refuse = |status: u16, why: String| {
let line = holger_errcode::BUNDLES_REFUSED.tag(why);
record(&ident, status, 0, &line);
text(status, &line)
};
if method != "PUT" {
return Ok(refuse_unread(
req,
refuse(
405,
format!("the bundles door '{repo_key}' takes a PUT of a signed Holger bundle"),
),
)
.await);
}
let Some(door) = settings.imports.clone() else {
return Ok(refuse_unread(
req,
refuse(
503,
"this server has no import door to verify a bundle with".into(),
),
)
.await);
};
let mut staged = match door.stage() {
Ok(s) => s,
Err(e) => return Ok(refuse_unread(req, refuse(e.status, e.tagged())).await),
};
let mut limited = Limited::new(req.into_body(), settings.max_body_bytes);
loop {
match limited.frame().await {
Some(Ok(frame)) => {
if let Ok(chunk) = frame.into_data() {
if let Err(e) = staged.write(&chunk) {
return Ok(refuse(e.status, e.tagged()));
}
}
}
Some(Err(_)) => {
record(&ident, 413, 0, "payload too large");
return Ok(text(413, "Payload too large"));
}
None => break,
}
}
if let Err(e) = staged.finish() {
return Ok(refuse(e.status, e.tagged()));
}
let actor = ident.clone();
let from = source.clone();
let store = Arc::clone(&backend);
let pq = path_and_query.clone();
let done = tokio::task::spawn_blocking(move || {
let stored = door
.store_verified(&staged.path, store.as_ref(), &pq, &actor, &from)
.map(|(what, n, (status, headers, body))| (what, n, status, headers, body));
drop(staged);
stored
})
.await;
return Ok(match done {
Ok(Ok((what, n, status, headers, body))) => {
record(&ident, status, n, &format!("bundle published: {what}"));
let mut b = Response::builder().status(status);
for (k, v) in headers {
b = b.header(k, v);
}
b.body(Full::new(Bytes::from(body)).boxed())
.unwrap_or_else(|_| text(500, "Internal error"))
}
Ok(Err(e)) => {
record(&ident, e.status, 0, &e.tagged());
text(e.status, &e.tagged())
}
Err(_) => refuse(500, "the bundle verification task failed".into()),
});
}
// P19 (lane HOL-FIELD-2): a write into a repository outside the chain where the policy
// requires a scan would be served unscanned, so it is refused by name before its body.
#[cfg(feature = "intake")]
if !is_read && intake_role.is_none() && oci_push.is_none() {
if let Some(refusal) = settings
.intake
.as_ref()
.and_then(|c| c.write_refusal(&repo_key))
{
let line = refusal.tagged();
record(&ident, refusal.status(), 0, &line);
return Ok(refuse_unread(req, text(refusal.status(), &line)).await);
}
}
#[cfg(feature = "intake")]
if let Some((chain, role)) = &intake_role {
use crate::promote::intake::IntakeRepo;
// The production repository's PEP 503 simple index, over the AVAILABLE
// set, with each file's `#sha256=` from the intake record: an unmodified
// `pip install --index-url …/production/simple/` installs from it.
if is_read && *role == IntakeRepo::Production {
let sub = path
.strip_prefix(&format!("/{repo_key}/"))
.unwrap_or("")
.to_string();
if sub == "simple" || sub.starts_with("simple/") {
let chain = Arc::clone(chain);
// PEP 691 JSON when the client asks for it by `Accept` (pip does).
let json = crate::promote::intake::wants_pep691(
req.headers().get("accept").and_then(|v| v.to_str().ok()),
);
// ★ A Python pull-through source: the project page is pulled into
// the chain when nothing of it is AVAILABLE ("not yet" until then).
#[cfg(feature = "jobs")]
{
let (c2, s2) = (Arc::clone(&chain), sub.clone());
if let Some(r) = tokio::task::spawn_blocking(move || {
crate::pull_intake::door::pip_index(&c2, &s2, json)
})
.await
.unwrap_or(None)
{
record(&ident, r.status, r.body.len() as u64, &r.note);
return Ok(pull_door_reply(r, &method, ticket.as_deref()));
}
}
let index = tokio::task::spawn_blocking(move || chain.simple_index_as(&sub, json))
.await
.unwrap_or(None);
return Ok(match index {
Some((ctype, body)) => {
let len = body.len() as u64;
record(
&ident,
200,
len,
"intake: simple index over the AVAILABLE set",
);
let body = if method == "HEAD" { Bytes::new() } else { body };
Response::builder()
.status(200)
.header("Content-Type", ctype)
.header("Content-Length", len)
.body(Full::new(body).boxed())
.unwrap_or_else(|_| text(500, "Internal error"))
}
None => {
record(
&ident,
404,
0,
"intake: no such project among the AVAILABLE set",
);
text(404, "Not found")
}
});
}
}
// ★ An OCI pull-through source: `/v2/<production>/<source>/<image>/…`.
#[cfg(feature = "jobs")]
if is_read && *role == IntakeRepo::Production {
// With its query: a referrers request may filter by `artifactType` (HOL-REFERRERS).
if let Some(rest) = path_and_query
.strip_prefix(&format!("/v2/{repo_key}/"))
.map(str::to_string)
{
let (c2, attest) = (Arc::clone(chain), settings.attest.clone());
if let Some(r) = tokio::task::spawn_blocking(move || {
crate::pull_intake::door::oci(&c2, &rest, attest.as_deref())
})
.await
.unwrap_or(None)
{
record(&ident, r.status, r.body.len() as u64, &r.note);
return Ok(pull_door_reply(r, &method, ticket.as_deref()));
}
}
}
// ★ An npm, Maven or crates pull-through source: `/<production>/<source>/…`.
#[cfg(feature = "jobs")]
if is_read && *role == IntakeRepo::Production {
if let Some(sub) = path
.strip_prefix(&format!("/{repo_key}/"))
.map(str::to_string)
{
let (c2, base) = (
Arc::clone(chain),
format!("{}/{repo_key}", npm_request_base(&req, &settings, &peer)),
);
// A gated production repository's crates sources say `auth-required`, so cargo
// sends its token past `config.json` (HOL-DOCFIND b).
let gated = settings.read_gate.applies(&settings.auth_config, &repo_key);
if let Some(r) = tokio::task::spawn_blocking(move || {
crate::pull_intake::door::remote(&c2, &sub, &base, gated)
})
.await
.unwrap_or(None)
{
record(&ident, r.status, r.body.len() as u64, &r.note);
return Ok(pull_door_reply(r, &method, ticket.as_deref()));
}
}
}
if is_read {
let id = match role {
IntakeRepo::Production => backend.coordinate(&path_and_query),
_ => None,
};
#[cfg(feature = "jobs")]
let pulled_id = id.clone();
// HOL-PERF-3: streamed from the sealed blob file, verified once per file.
let served = match id {
Some(id) => {
let chain = Arc::clone(chain);
tokio::task::spawn_blocking(move || {
chain.serve_stream(&id, STREAM_READ_PIECE_BYTES)
})
.await
.unwrap_or_else(|_| {
Err(crate::promote::intake::IntakeError::Store(
"dispatch task failed".into(),
))
})
}
None => Ok(None),
};
return Ok(match served {
Ok(Some(chunks)) => {
let len = chunks.len.unwrap_or(0);
record(
&ident,
200,
len,
"intake: AVAILABLE, served from the sealed store",
);
let mut builder = Response::builder()
.status(200)
.header("Content-Type", "application/octet-stream")
.header("X-Holger-Intake-State", "AVAILABLE");
if let Some(n) = chunks.len {
builder = builder.header("Content-Length", n);
}
let body = if method == "HEAD" {
Full::new(Bytes::new()).boxed()
} else {
if let Some(t) = &ticket {
t.add_net_out(len);
}
reply_body(chunks, ticket.clone(), path.clone()).await
};
builder
.body(body)
.unwrap_or_else(|_| text(500, "Internal error"))
}
Ok(None) => {
// PEP 658/714: `<wheel>.metadata`, the sealed wheel's core metadata.
if let Some(wheel) = path_and_query
.split('?')
.next()
.and_then(|p| p.strip_suffix(".metadata"))
.filter(|p| p.ends_with(".whl"))
{
if let Some(wid) = backend.coordinate(wheel) {
let c2 = Arc::clone(chain);
if let Some((meta, _)) =
tokio::task::spawn_blocking(move || c2.core_metadata(&wid))
.await
.unwrap_or(None)
{
let len = meta.len() as u64;
record(&ident, 200, len, "intake: a wheel's core metadata (PEP 658), from the sealed wheel");
let body = if method == "HEAD" {
Vec::new()
} else {
meta.as_ref().clone()
};
return Ok(Response::builder()
The file goes on; the console shows all of it.