Skip to main content

acme_proxy/cli/
mod.rs

1//! The command tree, and the startup path itself.
2//!
3//! **Nothing here prints or exits.** Every command body returns
4//! `Result<(), CliError>` and [`dispatch`] routes to it, so each arm is a plain
5//! function a test can call and assert on rather than an unreachable dead end.
6//! `src/main.rs` is where that `Result` becomes an exit status, and it is the
7//! only place in the project that calls `std::process::exit` — a library whose
8//! failure mode is ending the process is one nothing else can use.
9//!
10//! Startup is split on the socket boundary, which is what lets a test drive the
11//! whole path on an ephemeral port with its own shutdown future instead of a
12//! process signal:
13//!
14//! - `serve` binds `server.bind_address`, installs the `SIGHUP` handler, and
15//!   hands the socket on.
16//! - [`serve_on`] validates the admin configuration and binds that socket too,
17//!   when `[admin]` is enabled.
18//! - [`serve_on_with`] does everything else — profile resolution, deduplicated
19//!   signer backends, per-profile filters and validators, TLS, the job registry
20//!   (every signer's handlers, notification delivery and the four table sweeps),
21//!   the runner draining it, and `axum::serve` with connect info attached.
22//!
23//! That assembly is [`build_generation`], and it is called again on every
24//! reload rather than only at startup — so the two cannot drift, and a
25//! subsystem added to one is added to the other by construction. What a reload
26//! may change, and what it refuses by name, is [`crate::reload`]'s to say;
27//! [`serve_on_with_reloads`] is where the two meet.
28//!
29//! The logic behind each admin subcommand lives in [`crate::admin`], not here;
30//! this module is the `clap` surface over it. [`logging`] turns `[logging]` into
31//! an installed subscriber, validating every value before installing anything.
32//!
33//! What a command *prints* is [`render`]'s, and how it is coloured is
34//! [`style`]'s. Those renderings sit here rather than in [`crate::admin`]
35//! because they have exactly one consumer — the terminal — where the JSON ones
36//! beside them are a wire format the web admin parses too. [`dispatch`]
37//! resolves one [`Palette`] and threads it down; `nonce` and `upstream` take
38//! none, printing only fixed text.
39
40use std::future::Future;
41use std::io::{BufRead, IsTerminal};
42use std::net::SocketAddr;
43use std::sync::Arc;
44use std::time::Duration;
45
46use clap::{Parser, Subcommand};
47use clap_complete::aot::Shell;
48use tokio::net::TcpListener;
49use tracing::{error, info, warn};
50
51pub mod account;
52pub mod audit;
53pub mod eab;
54pub mod filter;
55pub mod generate;
56mod logging;
57
58/// Installs the `[logging]` configuration. Re-exported because `main.rs` is
59/// what calls it — see [`dispatch`].
60pub use logging::init_logging;
61/// The `--log-level` flag and the per-invocation decision it feeds. Re-exported
62/// for `main.rs`, which is where the subscriber is installed.
63pub use logging::{LogLevel, LoggingPlan, init_command_logging, plan_logging};
64pub mod nonce;
65pub mod order;
66pub mod profile;
67pub mod render;
68pub mod style;
69pub mod upstream;
70pub mod webadmin;
71pub mod window;
72
73pub use account::AccountCommand;
74pub use audit::AuditCommand;
75pub use eab::EabCommand;
76pub use nonce::NonceCommand;
77pub use order::OrderCommand;
78pub use profile::ProfileCommand;
79pub use upstream::UpstreamCommand;
80pub use webadmin::AdminCommand;
81
82use crate::cli::filter::FilterCommand;
83pub use crate::cli::style::{ColorChoice, Palette};
84use crate::config::Config;
85use crate::sqlite::db::Database;
86use crate::{Profile, build_app, tls};
87
88#[derive(Parser)]
89#[command(
90    name = "acme-proxy",
91    version = env!("CARGO_PKG_VERSION"),
92    about = "ACME server, plus admin commands for its database"
93)]
94pub struct Cli {
95    /// Skip interactive "Are you sure?" confirmation on destructive commands.
96    #[arg(short = 'y', long, global = true)]
97    pub yes: bool,
98
99    /// When to colour human-readable output. `--json` output never carries it.
100    #[arg(long, value_enum, default_value_t = ColorChoice::Auto, global = true)]
101    pub color: ColorChoice,
102
103    /// Emit log records for this run, at this level, on stderr. Without it an
104    /// admin command prints only its own output; `serve` uses `[logging]`.
105    #[arg(long, value_enum, global = true)]
106    pub log_level: Option<LogLevel>,
107
108    #[command(subcommand)]
109    pub command: Option<Command>,
110}
111
112#[derive(Subcommand)]
113pub enum Command {
114    /// Run the ACME HTTP(S) server. Default when no subcommand is given.
115    Serve,
116    /// Inspect and manage ACME accounts.
117    Account {
118        #[command(subcommand)]
119        command: AccountCommand,
120    },
121    /// Inspect and manage ACME orders.
122    Order {
123        #[command(subcommand)]
124        command: OrderCommand,
125    },
126    /// Read and prune the CA's audit trail.
127    Audit {
128        #[command(subcommand)]
129        command: AuditCommand,
130    },
131    /// Nonce table maintenance.
132    Nonce {
133        #[command(subcommand)]
134        command: NonceCommand,
135    },
136    /// Inspect the ACME endpoints this configuration mounts.
137    Profile {
138        #[command(subcommand)]
139        command: ProfileCommand,
140    },
141    /// Manage External Account Binding (EAB) credentials.
142    Eab {
143        #[command(subcommand)]
144        command: EabCommand,
145    },
146    /// Read and test the access policy of an endpoint.
147    Filter {
148        #[command(subcommand)]
149        command: FilterCommand,
150    },
151    /// Manage this server's own account at the upstream ACME server
152    /// (`signer.backend = "relay"`).
153    Upstream {
154        #[command(subcommand)]
155        command: UpstreamCommand,
156    },
157    /// Manage the web admin's operators and their sessions. This is how the
158    /// panel is bootstrapped: it has no sign-up page.
159    Admin {
160        #[command(subcommand)]
161        command: AdminCommand,
162    },
163    /// Print a shell completion script on stdout.
164    Completions {
165        /// The shell to generate for.
166        #[arg(value_enum)]
167        shell: Shell,
168    },
169    /// Print this binary's man page, in roff, on stdout.
170    Man,
171}
172
173/// Picks the profile a command acts on.
174///
175/// `--profile` is optional only when the configuration defines exactly one:
176/// most per-profile sections would otherwise be acted on ambiguously, and
177/// guessing is worse than asking. Shared by `upstream` and `filter`, which had
178/// grown one copy each.
179pub(crate) fn resolve_profile(
180    config: &Config,
181    wanted: Option<&str>,
182) -> Result<crate::config::ProfileConfig, CliError> {
183    let profiles = config
184        .resolve_profiles()
185        .map_err(|error| CliError(format!("configuration error: {error}")))?;
186
187    match wanted {
188        Some(name) => profiles
189            .into_iter()
190            .find(|profile| profile.name == name)
191            .ok_or_else(|| CliError(format!("no profile named `{name}` in this configuration"))),
192        None if profiles.len() == 1 => Ok(profiles.into_iter().next().expect("length checked")),
193        None => {
194            let names: Vec<&str> = profiles.iter().map(|p| p.name.as_str()).collect();
195            Err(CliError(format!(
196                "this configuration defines several profiles ({}); say which one with --profile",
197                names.join(", ")
198            )))
199        }
200    }
201}
202
203/// A command that could not complete, carrying the message to print.
204///
205/// Every failing branch below returns one of these instead of calling
206/// `std::process::exit` where it stands: `main.rs` is the single place that
207/// prints and exits, so each command body stays a plain function a test can
208/// call and assert on.
209#[derive(Debug, PartialEq, Eq, thiserror::Error)]
210#[error("{0}")]
211pub struct CliError(pub String);
212
213impl From<sqlx::Error> for CliError {
214    fn from(error: sqlx::Error) -> Self {
215        Self(format!("database error: {error}"))
216    }
217}
218
219/// Routes a parsed command to its handler.
220///
221/// The library's entry point. Everything above it — parsing argv, loading the
222/// configuration, installing the subscriber, opening the database, printing a
223/// failure and exiting — lives in `src/main.rs`, because those are the
224/// binary's job and not a library's: nothing that links this crate can use a
225/// function whose failure mode is `std::process::exit`.
226///
227/// Takes the `--color` *choice* rather than a resolved [`Palette`], and
228/// resolves it here: the answer depends on whether this process's stdout is a
229/// terminal and on `NO_COLOR`, and neither belongs in `main.rs`, which is
230/// excluded from the coverage floor precisely because nothing in it is
231/// reachable from a test.
232pub async fn dispatch(
233    command: Option<Command>,
234    yes: bool,
235    color: ColorChoice,
236    reader: &mut impl BufRead,
237    config: &Arc<Config>,
238    database: Arc<Database>,
239) -> Result<(), CliError> {
240    let palette = Palette::resolve(
241        color,
242        std::io::stdout().is_terminal(),
243        std::env::var("NO_COLOR").ok().as_deref(),
244    );
245    match command.unwrap_or(Command::Serve) {
246        Command::Serve => serve(config.clone(), database).await,
247        Command::Account { command } => {
248            account::run_account_command(command, yes, palette, reader, config, database).await
249        }
250        Command::Order { command } => {
251            order::run_order_command(command, yes, palette, reader, config, database).await
252        }
253        Command::Audit { command } => {
254            audit::run_audit_command(command, yes, palette, reader, database).await
255        }
256        Command::Nonce { command } => {
257            nonce::run_nonce_command(command, yes, reader, config, database).await
258        }
259        Command::Profile { command } => {
260            profile::run_profile_command(command, palette, config).await
261        }
262        Command::Eab { command } => eab::run_eab_command(command, palette, database).await,
263        Command::Filter { command } => filter::run_filter_command(command, palette, config).await,
264        Command::Upstream { command } => {
265            upstream::run_upstream_command(command, reader, config).await
266        }
267        Command::Admin { command } => {
268            webadmin::run_admin_command(command, yes, palette, reader, config, database).await
269        }
270        // Reachable here, though `main.rs` answers both before it opens
271        // anything: an `unreachable!()` would be dead code under the coverage
272        // floor, and routing them keeps this a total function over `Command`.
273        command @ (Command::Completions { .. } | Command::Man) => {
274            generate::write(&command, &mut std::io::stdout().lock())
275        }
276    }
277}
278
279/// Binds the configured socket and runs the ACME HTTP(S) server until a
280/// shutdown signal arrives.
281pub async fn serve(config: Arc<Config>, database: Arc<Database>) -> Result<(), CliError> {
282    let listener = TcpListener::bind(&config.server.bind_address)
283        .await
284        .map_err(|error| {
285            error!(event = "server_socket_bind_failed", outcome = "failure", bind_address = %config.server.bind_address, error = %error);
286            CliError(format!(
287                "cannot bind {}: {error}",
288                config.server.bind_address
289            ))
290        })?;
291
292    // Installed here, before anything slow: `SIGHUP`'s default disposition is
293    // *terminate*, so until the handler exists a reload signal kills the
294    // process. `serve_on_with_reloads` does profile assembly and the relay's
295    // first upstream contact before it binds anything, which is exactly the
296    // window an operator's `systemctl reload` could land in.
297    let (reload_handle, reloads) = crate::reload::channel();
298    let _hangups = AbortOnDrop(tokio::spawn(watch_for_hangup(reload_handle)));
299
300    let admin_listener = bind_admin(&config).await.map_err(|error| {
301        error!(event = "server_fatal_error", outcome = "failure", error = %error);
302        CliError(error.to_string())
303    })?;
304    let metrics_listener = bind_metrics(&config).await.map_err(|error| {
305        error!(event = "server_fatal_error", outcome = "failure", error = %error);
306        CliError(error.to_string())
307    })?;
308
309    serve_on_with_reloads(
310        config,
311        database,
312        listener,
313        admin_listener,
314        metrics_listener,
315        shutdown_signal(),
316        reloads,
317    )
318    .await
319    .map_err(|error| {
320        error!(event = "server_fatal_error", outcome = "failure", error = %error);
321        CliError(error.to_string())
322    })
323}
324
325/// Turns every `SIGHUP` into a reload request, for the life of the process.
326///
327/// Unlike the shutdown signal, this one does not consume its stream: an operator
328/// reloads repeatedly, and a handler that fired once would leave the second
329/// `SIGHUP` back at its default disposition — killing the server.
330#[cfg(unix)]
331async fn watch_for_hangup(handle: crate::reload::ReloadHandle) {
332    let mut hangups = match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()) {
333        Ok(stream) => stream,
334        Err(error) => {
335            error!(event = "server_signal_handler_failed", outcome = "failure", signal = "SIGHUP", error = %error);
336            return;
337        }
338    };
339    while hangups.recv().await.is_some() {
340        handle.trigger();
341    }
342}
343
344/// No `SIGHUP` off Unix, so there is nothing to watch for.
345#[cfg(not(unix))]
346async fn watch_for_hangup(_handle: crate::reload::ReloadHandle) {
347    std::future::pending::<()>().await;
348}
349
350/// Validates the admin configuration and binds its socket when it is enabled.
351///
352/// Shared by [`serve`] and [`serve_on`] rather than living in one of them: both
353/// need it, and the validation must happen **before anything binds**, so a
354/// misconfigured panel cannot take the ACME listener down with it halfway
355/// through startup.
356async fn bind_admin(config: &Arc<Config>) -> anyhow::Result<Option<TcpListener>> {
357    crate::webadmin::check_config(config).inspect_err(|error| {
358        error!(event = "admin_config_invalid", outcome = "failure", error = %error);
359    })?;
360
361    match config.admin.enabled {
362        false => Ok(None),
363        true => Ok(Some(
364            TcpListener::bind(&config.admin.bind_address)
365                .await
366                .inspect_err(|error| {
367                    error!(event = "admin_socket_bind_failed",
368                           outcome = "failure",
369                           bind_address = %config.admin.bind_address,
370                           error = %error);
371                })?,
372        )),
373    }
374}
375
376/// Refuses a `[metrics]` bind address that collides with another listener's.
377///
378/// Pure, so a reload runs the same check before rebinding anything — the twin of
379/// [`crate::webadmin::check_config`], and beside it in `apply_reload` for the
380/// same reason: a listener configuration that would not start must not be one a
381/// running server can be reloaded into.
382///
383/// Three listeners, so the check is pairwise. `webadmin` already refuses
384/// admin-versus-server; these are the two pairs it cannot see. Checked even when
385/// `[admin]` is off, since enabling the panel later must not be what surfaces a
386/// latent conflict.
387///
388/// # Errors
389///
390/// Names the other key when the two addresses are equal.
391pub fn check_metrics_config(config: &Config) -> anyhow::Result<()> {
392    if !config.metrics.enabled {
393        return Ok(());
394    }
395
396    let bind = &config.metrics.bind_address;
397    for (name, other) in [
398        ("server.bind_address", &config.server.bind_address),
399        ("admin.bind_address", &config.admin.bind_address),
400    ] {
401        if bind == other && (name != "admin.bind_address" || config.admin.enabled) {
402            error!(event = "metrics_config_invalid",
403                   outcome = "failure",
404                   bind_address = %bind);
405            anyhow::bail!(
406                "metrics.bind_address and {name} are both `{bind}`: the metrics endpoint is a \
407                 separate listener and cannot share a socket (give it its own port)"
408            );
409        }
410    }
411    Ok(())
412}
413
414/// Binds the metrics socket when `[metrics]` is on, refusing a collision first.
415///
416/// The twin of [`bind_admin`], and the same shape for the same reason: a socket
417/// that cannot be bound must stop startup rather than leave the process running
418/// with one of its three listeners silently missing.
419///
420/// Deliberately **no loopback check**. `webadmin::check_config` refuses a
421/// non-loopback admin bind without TLS because that listener's cookie is always
422/// `Secure`, which a browser will not store over plain HTTP, so the failure
423/// would be invisible. Nothing here has a cookie: a metrics port reachable from
424/// a Prometheus host on another machine is the intended deployment, and the
425/// firewall is what bounds it.
426async fn bind_metrics(config: &Arc<Config>) -> anyhow::Result<Option<TcpListener>> {
427    check_metrics_config(config)?;
428    if !config.metrics.enabled {
429        return Ok(None);
430    }
431
432    let bind = &config.metrics.bind_address;
433    let listener = TcpListener::bind(bind).await.inspect_err(|error| {
434        error!(event = "metrics_socket_bind_failed",
435               outcome = "failure",
436               bind_address = %bind,
437               error = %error);
438    })?;
439    Ok(Some(listener))
440}
441
442/// Assembles and serves the application over an already-bound socket.
443///
444/// Split from [`serve`] on the socket boundary: a caller supplying its own
445/// listener and its own `shutdown` future can drive the whole startup path —
446/// profile assembly, TLS, backend resume, the nonce reaper, both `axum::serve`
447/// arms — without owning a fixed port or a process signal.
448///
449/// Binds the web admin socket itself when `[admin]` is enabled. The signature
450/// is unchanged, and `admin.enabled` is false by default, so every existing
451/// caller is untouched; a test that wants to drive *both* listeners supplies
452/// its own pair through [`serve_on_with`].
453pub async fn serve_on(
454    config: Arc<Config>,
455    database: Arc<Database>,
456    listener: TcpListener,
457    shutdown: impl Future<Output = ()> + Send + 'static,
458) -> anyhow::Result<()> {
459    let admin_listener = bind_admin(&config).await?;
460    let metrics_listener = bind_metrics(&config).await?;
461    serve_on_with(
462        config,
463        database,
464        listener,
465        admin_listener,
466        metrics_listener,
467        shutdown,
468    )
469    .await
470}
471
472/// [`serve_on`] with all three sockets supplied.
473///
474/// The full version, split on the same boundary and for the same reason: a
475/// caller handing in three ephemeral ports can drive the whole startup path —
476/// including that one shutdown signal stops all of them — without owning a
477/// fixed port or a process signal.
478pub async fn serve_on_with(
479    config: Arc<Config>,
480    database: Arc<Database>,
481    listener: TcpListener,
482    admin_listener: Option<TcpListener>,
483    metrics_listener: Option<TcpListener>,
484    shutdown: impl Future<Output = ()> + Send + 'static,
485) -> anyhow::Result<()> {
486    serve_on_with_reloads(
487        config,
488        database,
489        listener,
490        admin_listener,
491        metrics_listener,
492        shutdown,
493        crate::reload::Reloads::none(),
494    )
495    .await
496}
497
498/// [`serve_on_with`], serving configuration reloads as well as requests.
499///
500/// The variant `serve` uses, so a `SIGHUP` rebuilds both routers, the job
501/// registry, the notifier map and both TLS acceptors behind the sockets that are
502/// already bound. Every other caller goes through [`serve_on_with`] and gets a
503/// source that never fires, which costs one task that ends immediately.
504///
505/// See [`crate::reload`] for what a reload may change and what it refuses.
506pub async fn serve_on_with_reloads(
507    config: Arc<Config>,
508    database: Arc<Database>,
509    listener: TcpListener,
510    admin_listener: Option<TcpListener>,
511    metrics_listener: Option<TcpListener>,
512    shutdown: impl Future<Output = ()> + Send + 'static,
513    reloads: crate::reload::Reloads,
514) -> anyhow::Result<()> {
515    info!(
516        event = "server_startup",
517        outcome = "success",
518        bind_address = %config.server.bind_address,
519        base_url = %config.server.base_url,
520        tls = config.server.tls.enabled,
521        database_database_url = %config.database.url
522    );
523
524    // One `shutdown` future, several consumers: both listeners and the job
525    // runner. Created here rather than beside `axum::serve` below so a signal
526    // arriving *during* startup is not ignored — profile assembly and the
527    // relay's first upstream contact both happen before anything binds. The
528    // relay task is held under `AbortOnDrop` so an error path below does not
529    // leak a task parked on a signal that will never arrive.
530    let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
531    let _shutdown_relay = AbortOnDrop(tokio::spawn(async move {
532        shutdown.await;
533        let _ = shutdown_tx.send(true);
534    }));
535
536    // The enqueue side of the durable queue. Built before the profiles because
537    // a signer backend that defers issuance is handed one at construction, and
538    // process-wide for the reason `[audit]` is: one table, one runner, and a
539    // per-endpoint retry budget would make a job's pacing depend on which
540    // profile happened to queue it.
541    let job_queue = crate::jobs::JobQueue::new(database.clone(), &config.jobs);
542
543    let resolved = config.resolve_profiles().inspect_err(|error| {
544        error!(event = "profile_init_failed", outcome = "failure", error = %error);
545    })?;
546    // Everything that outlives a configuration generation — the signer backends
547    // above all, which are carried rather than rebuilt. See `crate::Assembly`.
548    let (assembly, parts) =
549        crate::Assembly::new(&resolved, database.clone(), job_queue.clone(), &config).inspect_err(
550            |error| {
551                error!(event = "profile_init_failed", outcome = "failure", error = %error);
552            },
553        )?;
554    let assembly = Arc::new(assembly);
555
556    let generation =
557        build_generation(&config, &resolved, &assembly, &parts, None).inspect_err(|error| {
558            error!(event = "profile_init_failed", outcome = "failure", error = %error);
559        })?;
560
561    // Every endpoint in the first generation came up, so every one is announced.
562    // A reload announces only the endpoints it *added* — see
563    // `supervise_reloads`, which shares this function for exactly that reason.
564    for profile in &generation.profiles {
565        announce_profile(profile).await;
566    }
567
568    let Generation {
569        profiles: _,
570        acme_app,
571        admin_app,
572        job_registry,
573        tls,
574        admin_tls,
575        logins,
576    } = generation;
577
578    // The one task that drains the queue. Held under `AbortOnDrop` for the same
579    // reason the reapers are — an un-cancelled loop holding an `Arc<Database>`
580    // per `serve_on` call — and, unlike the relay tasks this replaced, it does
581    // *not* need to outlive this function: the work is durable now, so a job cut
582    // short is re-claimed from its own row rather than lost. It takes the same
583    // shutdown signal as both listeners, so a stop is graceful rather than an
584    // abort: it releases its leases on the way out, and a restart therefore
585    // re-claims its own work immediately instead of waiting one out.
586    //
587    // Neither the registry it drains nor the `[jobs]` section it paces itself
588    // from is a value: both are cells a reload republishes, so a changed
589    // retention, a rebuilt notify map and a retuned lease or concurrency all
590    // reach the runner without restarting it. `jobs.max_attempts` is the third
591    // piece and does not come through here — it belongs to the enqueue side, so
592    // it is published onto `job_queue` itself.
593    let (registry_tx, registry_rx) = tokio::sync::watch::channel(Arc::new(job_registry));
594    let (jobs_tx, jobs_rx) = tokio::sync::watch::channel(Arc::new(config.jobs.clone()));
595    let _job_runner = AbortOnDrop(crate::jobs::spawn_runner_watching(
596        job_queue,
597        registry_rx,
598        jobs_rx,
599        shutdown_rx.clone(),
600    ));
601
602    info!(
603        event = "server_listening",
604        outcome = "success",
605        bind_address = %config.server.bind_address,
606        protocol = if tls.is_some() { "https" } else { "http" }
607    );
608
609    // One accept loop per role, each owning a socket a reload can replace and a
610    // TLS mode it can switch — see `crate::listener`. `axum::serve` below is
611    // handed one of these instead of a `TcpListener` and therefore outlives
612    // every rebind, which is what removes the listener from the list of things
613    // only a restart can change.
614    let admin_bound = bound_address(admin_listener.as_ref(), &config.admin.bind_address);
615    let metrics_bound = bound_address(metrics_listener.as_ref(), &config.metrics.bind_address);
616    let (acme_socket, acme_handle) = crate::listener::spawn("acme", Some(listener), tls);
617    let (admin_socket, admin_handle) = crate::listener::spawn("admin", admin_listener, admin_tls);
618    let (metrics_socket, metrics_handle) =
619        crate::listener::spawn("metrics", metrics_listener, None);
620
621    // Behind a swap cell rather than served directly, so a configuration reload
622    // can replace the whole router without the socket moving. The cell is what
623    // `axum::serve` holds; `acme_app` itself is only ever generation one.
624    let (acme_router_tx, acme_router_rx) = crate::reload::router_channel(acme_app);
625    let acme = serve_role(
626        crate::reload::swappable(acme_router_rx),
627        acme_socket,
628        shutdown_rx.clone(),
629    );
630
631    // Opened whether or not the panel is on, unlike the app inside it: with
632    // `admin.enabled` reloadable, a cell created only when the panel starts
633    // would be the one thing a reload turning it on could not reach. An empty
634    // `Router` answers `404` to everything, which is also what the panel being
635    // switched off later publishes here.
636    let (admin_router_tx, admin_router_rx) =
637        crate::reload::router_channel(admin_app.unwrap_or_default());
638    let admin = serve_role(
639        crate::reload::swappable(admin_router_rx),
640        admin_socket,
641        shutdown_rx.clone(),
642    );
643    if config.admin.enabled {
644        announce_admin_listener(&config, &database, &admin_bound).await;
645    }
646
647    // The third listener. Served directly rather than through a
648    // `reload::router_channel` like the other two, and the asymmetry is
649    // deliberate: this router has one route whose only state is the registry,
650    // and the registry is carried across generations rather than rebuilt (see
651    // `Assembly`), so a new generation could put nothing new in it.
652    if config.metrics.enabled {
653        announce_metrics_listener(&metrics_bound);
654    }
655    let metrics = serve_role(
656        crate::metrics_app(assembly.metrics.clone()),
657        metrics_socket,
658        shutdown_rx,
659    );
660
661    // The supervisor owns every cell sender from here on, which is what makes it
662    // the only writer: a generation is published by one task or by nobody.
663    // Aborted on drop, so an error return below does not leave it parked on a
664    // channel nothing will ever send to.
665    let _reload_supervisor = AbortOnDrop(tokio::spawn(supervise_reloads(
666        reloads,
667        config.clone(),
668        resolved,
669        assembly,
670        Cells {
671            acme_router: acme_router_tx,
672            admin_router: admin_router_tx,
673            job_registry: registry_tx,
674            jobs: jobs_tx,
675            acme: acme_handle,
676            admin: admin_handle,
677            metrics: metrics_handle,
678        },
679        logins,
680    )));
681
682    // Nothing is drained here any more. A notification in flight at shutdown is
683    // a `notify_deliver` row, not a spawned task: the runner released its lease
684    // on the way out and whoever starts next claims it. That is what replaced a
685    // best-effort five-second drain which still lost anything slower than it.
686    tokio::try_join!(acme, admin, metrics)?;
687    Ok(())
688}
689
690/// Serves `app` on one role's socket until the process shuts down.
691///
692/// One shape for all three roles, where there used to be a boxed future per
693/// listener per TLS arm: [`crate::listener::RoleSocket`] is the same type
694/// whether the role is speaking TLS, speaking cleartext or — a socket having
695/// been closed by a reload — not serving at all, so the four cases collapse into
696/// this one call. Its future lives for the process: a rebind replaces what is
697/// underneath it, never the `axum::serve` above.
698fn serve_role(
699    app: axum::Router,
700    socket: crate::listener::RoleSocket,
701    shutdown: tokio::sync::watch::Receiver<bool>,
702) -> impl Future<Output = std::io::Result<()>> + Send {
703    axum::serve(
704        socket,
705        app.into_make_service_with_connect_info::<SocketAddr>(),
706    )
707    .with_graceful_shutdown(on_shutdown(shutdown))
708    .into_future()
709}
710
711/// Everything one configuration generation contributes, built and validated
712/// before any of it is published.
713///
714/// The unit exists so startup and reload cannot drift: both go through
715/// [`build_generation`], so a subsystem added to one is added to the other by
716/// construction rather than by remembering.
717pub(crate) struct Generation {
718    /// Kept so startup can announce them; a reload drops them on the floor,
719    /// since `profile_mounted` is a lifecycle event and not a heartbeat.
720    profiles: Vec<Arc<Profile>>,
721    acme_app: axum::Router,
722    admin_app: Option<axum::Router>,
723    job_registry: crate::jobs::JobRegistry,
724    tls: Option<tls::TlsSettings>,
725    admin_tls: Option<tls::TlsSettings>,
726    /// The limiter this generation ended up with, for the next one to carry.
727    logins: Option<Arc<crate::webadmin::LoginLimiter>>,
728}
729
730/// Builds one generation: profiles, both routers, the job registry, the audit
731/// trail and both TLS acceptors.
732///
733/// Fallible throughout and side-effect-free on the *serving* state: nothing here
734/// touches a cell, so a failure leaves whatever is already running exactly as it
735/// was. That is what makes an atomic reload possible — everything is built
736/// first, and only a complete success publishes anything.
737///
738/// `dispatchers` is passed in rather than built here because a reload needs to
739/// hold it back: it is published to the long-lived [`crate::notify::Notifiers`]
740/// handle at swap time, *before* the routers, so a request served by the new
741/// generation cannot queue a delivery the job runner's map does not know.
742///
743/// Whether the panel is part of this generation is read from `admin.enabled`
744/// rather than from whether a socket exists. It used to be the latter, which
745/// was the same answer while the key was frozen and is the wrong one now that a
746/// reload can turn the panel on: the app, its TLS, its session sweep and its
747/// login limiter all have to appear in the generation *before* there is a
748/// listener to serve them.
749pub(crate) fn build_generation(
750    config: &Arc<Config>,
751    resolved: &[crate::config::ProfileConfig],
752    assembly: &crate::Assembly,
753    parts: &crate::GenerationParts,
754    previous_logins: Option<&crate::webadmin::LoginLimiter>,
755) -> anyhow::Result<Generation> {
756    let admin_enabled = config.admin.enabled;
757    let database = assembly.database.clone();
758    let profiles = Profile::build_all_with(config, resolved, parts)?;
759
760    let tls = tls::from_config(&config.server)
761        .inspect_err(|error| {
762            error!(event = "tls_init_failed", outcome = "failure", error = %error);
763        })?
764        .map(|acceptor| {
765            tls::TlsSettings::new(
766                acceptor,
767                Duration::from_millis(config.server.tls.handshake_timeout_ms),
768            )
769        });
770
771    let admin_tls = match admin_enabled {
772        false => None,
773        true => tls::admin_from_config(&config.admin)
774            .inspect_err(|error| {
775                error!(event = "admin_tls_init_failed", outcome = "failure", error = %error);
776            })?
777            .map(|acceptor| {
778                tls::TlsSettings::new(
779                    acceptor,
780                    Duration::from_millis(config.admin.tls.handshake_timeout_ms),
781                )
782            }),
783    };
784
785    // Every subsystem with background work, in one registry. **Nothing here
786    // registers a handler per backend**: the registry refuses a second handler
787    // for one kind outright, since two would each claim about half the rows, and
788    // two profiles over different `[signer]` sections are two backends that
789    // `build_backends` deliberately does not collapse. So each backend hands
790    // over *state* and one handler is built over all of it.
791    let mut job_registry = crate::jobs::JobRegistry::new();
792    // The CRL ledgers are collected once per distinct backend, since two
793    // profiles sharing one CA share one ledger and pruning it twice a day would
794    // be pointless work. The identity is kept as a `usize` rather than the
795    // pointer itself, so this function's caller stays `Send`; it is spawned.
796    let mut registered: Vec<usize> = Vec::new();
797    let mut pruners: Vec<Arc<dyn crate::signer::CrlPruner>> = Vec::new();
798    // The relay backends are collected per *profile*, because that is the key a
799    // job row is dispatched on — and because taking the profile list from the
800    // backend would take a stale one: a backend whose configuration did not
801    // move is reused verbatim across a reload, so a profile newly mounted onto
802    // it is not in any list it remembers.
803    let mut relays: Vec<(String, crate::signer::relay::RelayState)> = Vec::new();
804    for profile in &profiles {
805        relays.extend(
806            profile
807                .signer
808                .relay_state()
809                .map(|state| (profile.name.clone(), state)),
810        );
811        let identity = Arc::as_ptr(&profile.signer).cast::<()>() as usize;
812        if registered.contains(&identity) {
813            continue;
814        }
815        registered.push(identity);
816        pruners.extend(profile.signer.crl_pruner());
817    }
818    // The periodic CRL prune, over whichever CAs keep a ledger of their own.
819    // Registered only when there is one, the way the audit sweep is registered
820    // only for a non-zero retention.
821    if !pruners.is_empty() {
822        job_registry
823            .register(Arc::new(crate::signer::local_ca::sweep::CrlSweepJob::new(
824                pruners,
825            )))
826            .inspect_err(|error| {
827                error!(event = "job_registry_init_failed", outcome = "failure", error = %error);
828            })?;
829    }
830    // Relayed issuance, over every relay profile at once. Registered only when
831    // some profile relays, for `CrlSweepJob`'s reason: a deployment with none
832    // has no row of this kind to claim.
833    if !relays.is_empty() {
834        job_registry
835            .register(Arc::new(crate::signer::relay::flow::RelayJob::new(
836                database.clone(),
837                relays,
838            )))
839            .inspect_err(|error| {
840                error!(event = "job_registry_init_failed", outcome = "failure", error = %error);
841            })?;
842    }
843    // Notification delivery. The same shape as the two above and the reason it
844    // is: one handler for every profile, holding the whole
845    // `profile name -> dispatcher` map, with a job row naming its own profile.
846    // Registered unconditionally — a profile with no `[notify]`
847    // backends queues nothing, so the handler simply never claims a row, and
848    // making the registration conditional would mean a row queued before a
849    // configuration change had nobody to run it.
850    //
851    // It takes the *handle*, not this generation's map: the handler is
852    // registered per generation but must read whichever map is current, and a
853    // row queued by a reloaded router names a slot id only the new one has.
854    job_registry
855        .register(Arc::new(crate::notify::NotifyJob::new(
856            assembly.notifiers.clone(),
857        )))
858        .inspect_err(|error| {
859            error!(event = "job_registry_init_failed", outcome = "failure", error = %error);
860        })?;
861
862    // The expiry digest, registered only when some profile asked for one
863    // (`notify.expiry.lead_days`), the way `CrlSweepJob` is registered only
864    // when there is a ledger to prune. It takes the `Notifiers` handle rather
865    // than the profiles' own dispatchers for `NotifyJob`'s reason above, and
866    // the queue because its per-profile rows are something it maintains on
867    // every pass rather than only at `recover`.
868    if let Some(digest) = crate::notify::expiry::ExpiryDigestJob::from_profiles(
869        resolved,
870        assembly.notifiers.clone(),
871        database.clone(),
872        assembly.jobs.clone(),
873    ) {
874        job_registry
875            .register(Arc::new(digest))
876            .inspect_err(|error| {
877                error!(event = "job_registry_init_failed", outcome = "failure", error = %error);
878            })?;
879    }
880
881    // The periodic table sweeps. Each is one self-rescheduling row rather than
882    // its own interval loop, so a sweep that dies is reclaimed by lease expiry
883    // and its schedule survives a restart. Their `recover` is also the startup
884    // sweep — it queues at `run_at = now`, so the runner performs the first pass
885    // on its way into the loop and there is nothing to run separately here.
886    let ttl = Duration::from_secs(config.nonce.ttl_seconds);
887    let mut sweeps = vec![crate::jobs::SweepJob::nonces(database.clone(), ttl)];
888    // `0` keeps everything for ever on both of these, and is a handler not
889    // registered rather than a sweep with a cutoff at the epoch.
890    if config.audit.retention_days > 0 {
891        sweeps.push(crate::jobs::SweepJob::audit(
892            database.clone(),
893            config.audit.retention_days,
894        ));
895    }
896    if config.jobs.retention_days > 0 {
897        sweeps.push(crate::jobs::SweepJob::jobs(
898            database.clone(),
899            config.jobs.retention_days,
900        ));
901    }
902    // One handler covering every mounted profile, since the registry refuses a
903    // second handler for one kind. A profile keeping everything (`0`) is left
904    // out of the list rather than swept with a cutoff at the epoch.
905    let order_retention: Vec<(String, u64)> = resolved
906        .iter()
907        .filter(|profile| profile.sections.order.retention_days > 0)
908        .map(|profile| (profile.name.clone(), profile.sections.order.retention_days))
909        .collect();
910    if !order_retention.is_empty() {
911        sweeps.push(crate::jobs::SweepJob::orders(
912            database.clone(),
913            order_retention,
914        ));
915    }
916    if admin_enabled {
917        sweeps.push(crate::jobs::SweepJob::admin_sessions(
918            database.clone(),
919            Duration::from_secs(config.admin.session_idle_timeout_seconds),
920            config.admin.session_ttl_seconds,
921        ));
922    }
923    for sweep in sweeps {
924        job_registry
925            .register(Arc::new(sweep))
926            .inspect_err(|error| {
927                error!(event = "job_registry_init_failed", outcome = "failure", error = %error);
928            })?;
929    }
930
931    // The CA's audit trail: one per process, shared by every profile's router
932    // and by the web admin listener, because `[audit]` is process-wide. Built
933    // here rather than in `Profile::build_all` for exactly that reason — it is
934    // not a per-endpoint subsystem.
935    let auditor = Arc::new(
936        crate::audit::Auditor::from_config(
937            &config.audit,
938            &config.dns,
939            database.clone(),
940            // The registry the certificate counters land in. Carried by
941            // `Assembly`, so it is the same one across every generation and the
942            // same one `metrics_app` serves from.
943            assembly.metrics.clone(),
944        )
945        .inspect_err(|error| {
946            error!(event = "audit_init_failed", outcome = "failure", error = %error);
947        })?,
948    );
949
950    // Built **before** `build_app`, which consumes `profiles`. The admin state
951    // needs the same profiles (revoking an order resolves that order's own
952    // signer), and `build_admin_app` takes a slice precisely so the ordering
953    // is a signature constraint rather than a borrow error to rediscover.
954    let (admin_app, logins) = match admin_enabled {
955        false => (None, None),
956        true => {
957            let (router, logins) = crate::webadmin::build_admin_app_with_logins(
958                database.clone(),
959                config.clone(),
960                &profiles,
961                auditor.clone(),
962                previous_logins,
963            );
964            (Some(router), Some(logins))
965        }
966    };
967    let acme_app = build_app(
968        database,
969        config.clone(),
970        profiles.clone(),
971        auditor,
972        assembly.metrics.clone(),
973    );
974
975    Ok(Generation {
976        profiles,
977        acme_app,
978        admin_app,
979        job_registry,
980        tls,
981        admin_tls,
982        logins,
983    })
984}
985
986/// The cells one generation is published into.
987///
988/// Held by the supervisor and by nothing else. Every field is a `watch::Sender`,
989/// and `send_replace` is synchronous — so publishing a generation is a run of
990/// sends with no `.await` between them, which no other task can interleave with.
991/// That is what makes a reload atomic without a lock.
992struct Cells {
993    acme_router: tokio::sync::watch::Sender<axum::routing::RouterIntoService<axum::body::Body>>,
994    admin_router: tokio::sync::watch::Sender<axum::routing::RouterIntoService<axum::body::Body>>,
995    job_registry: tokio::sync::watch::Sender<Arc<crate::jobs::JobRegistry>>,
996    /// The runner's own pacing. Separate from the registry above because the two
997    /// reach it by different routes: the registry carries what a *handler*
998    /// captured, this carries what the *loop* re-reads each pass.
999    jobs: tokio::sync::watch::Sender<Arc<crate::config::JobsConfig>>,
1000    /// The three sockets. Each carries its role's TLS mode as well, since both
1001    /// are read by the same accept loop and both are published the same
1002    /// synchronous way — see [`crate::listener::ListenerHandle`].
1003    acme: crate::listener::ListenerHandle,
1004    admin: crate::listener::ListenerHandle,
1005    metrics: crate::listener::ListenerHandle,
1006}
1007
1008/// Serves reload requests for the life of the process.
1009///
1010/// One task, so reloads are serialised: two overlapping rebuilds could publish
1011/// their cells interleaved, and the second-newest generation would win some of
1012/// them. Ends when the last [`crate::reload::ReloadHandle`] is dropped, which is
1013/// what makes [`crate::reload::Reloads::none`] cost nothing.
1014async fn supervise_reloads(
1015    mut reloads: crate::reload::Reloads,
1016    mut config: Arc<Config>,
1017    mut resolved: Vec<crate::config::ProfileConfig>,
1018    assembly: Arc<crate::Assembly>,
1019    cells: Cells,
1020    mut logins: Option<Arc<crate::webadmin::LoginLimiter>>,
1021) {
1022    let mut generation: u64 = 1;
1023
1024    while let Some(request) = reloads.recv().await {
1025        let started = std::time::Instant::now();
1026        info!(
1027            event = "server_config_reload_requested",
1028            outcome = "progress",
1029            generation = generation,
1030        );
1031
1032        // The build phase runs on a blocking thread, and that is not a
1033        // precaution: `RelaySigner::from_config` contacts the upstream the first
1034        // time it is built for an account with no `kid` sidecar yet, on a scoped
1035        // OS thread it then *joins*. Mounting a relay profile by `SIGHUP` would
1036        // otherwise park a runtime worker for as long as
1037        // `signer.relay.poll_timeout_secs` allows — five minutes by default —
1038        // with every connection that worker was polling parked behind it. The
1039        // publish phase stays on this task, where its lack of an await point is
1040        // what makes a generation unobservable half-applied.
1041        let outcome = {
1042            let config = config.clone();
1043            let resolved = resolved.clone();
1044            let assembly = assembly.clone();
1045            let logins = logins.clone();
1046            tokio::task::spawn_blocking(move || {
1047                prepare_reload(&config, &resolved, &assembly, logins.as_deref())
1048            })
1049            .await
1050            .unwrap_or_else(|error| {
1051                Err(crate::reload::ReloadError::Build(format!(
1052                    "the reload build task did not finish: {error}"
1053                )))
1054            })
1055        }
1056        .map(|prepared| {
1057            publish_reload(
1058                prepared,
1059                &config,
1060                &assembly,
1061                &cells,
1062                generation + 1,
1063                started,
1064            )
1065        });
1066
1067        match outcome {
1068            Ok(reloaded) => {
1069                let report = reloaded.report;
1070                config = reloaded.config;
1071                resolved = reloaded.resolved;
1072                logins = reloaded.logins;
1073                generation = report.generation;
1074                info!(
1075                    event = "server_config_reloaded",
1076                    outcome = "success",
1077                    generation = report.generation,
1078                    profiles = ?report.profiles,
1079                    job_kinds = ?report.job_kinds,
1080                    tls_reloaded = report.tls_reloaded,
1081                    admin_tls_reloaded = report.admin_tls_reloaded,
1082                    listeners_rebound = ?report.listeners_rebound,
1083                    logging_reloaded = report.logging_reloaded,
1084                    duration_ms = crate::millis(report.duration),
1085                );
1086                // After the reload's own line, and under the new configuration,
1087                // since that is what these describe. Each is the same
1088                // announcement startup makes for a listener that has just come
1089                // up — including the panel's two warnings, which is why this is
1090                // here rather than inside the synchronous publishing run.
1091                for (role, address) in reloaded.opened {
1092                    match role {
1093                        Role::Acme => info!(
1094                            event = "server_listening",
1095                            outcome = "success",
1096                            bind_address = %address,
1097                            protocol = if config.server.tls.enabled { "https" } else { "http" }
1098                        ),
1099                        Role::Admin => {
1100                            announce_admin_listener(&config, &assembly.database, &address).await;
1101                        }
1102                        Role::Metrics => announce_metrics_listener(&address),
1103                    }
1104                }
1105                // An endpoint this reload mounted really did come up, so it gets
1106                // the same announcement and the same notification startup makes
1107                // for one. An endpoint that was *already* mounted stays silent:
1108                // `profile_mounted` is a lifecycle event and not a heartbeat,
1109                // and re-firing it per `SIGHUP` would make the notify surface
1110                // noisiest in exactly the config-managed deployments that would
1111                // least want it. Here rather than in the publishing run because
1112                // dispatching reaches the database.
1113                for profile in reloaded.mounted {
1114                    announce_profile(&profile).await;
1115                }
1116                if let Some(respond) = request.respond {
1117                    let _ = respond.send(Ok(report));
1118                }
1119            }
1120            Err(error) => {
1121                // Two names, because they are two different things for whoever
1122                // is reading: a refusal is a configuration an operator must
1123                // change, a failure is one the server could not build.
1124                match &error {
1125                    crate::reload::ReloadError::Frozen { .. } => warn!(
1126                        event = "server_config_reload_refused",
1127                        outcome = "failure",
1128                        generation = generation,
1129                        reason = error.kind(),
1130                        error = %error,
1131                    ),
1132                    _ => error!(
1133                        event = "server_config_reload_failed",
1134                        outcome = "failure",
1135                        generation = generation,
1136                        reason = error.kind(),
1137                        error = %error,
1138                    ),
1139                }
1140                if let Some(respond) = request.respond {
1141                    let _ = respond.send(Err(error));
1142                }
1143            }
1144        }
1145    }
1146}
1147
1148/// One of the three sockets this process may hold.
1149///
1150/// An enum rather than the `&'static str` the log field wants, so the reload
1151/// path's per-role handling is exhaustive: a fourth listener would be a compile
1152/// error at every point that has to decide something about one, which is exactly
1153/// how the third arrived with `bind_metrics` and `check_metrics_config` in
1154/// place and nothing else remembering it existed.
1155#[derive(Clone, Copy, PartialEq, Eq)]
1156enum Role {
1157    Acme,
1158    Admin,
1159    Metrics,
1160}
1161
1162impl Role {
1163    /// The `listener` field every log line about this socket carries.
1164    fn label(self) -> &'static str {
1165        match self {
1166            Self::Acme => "acme",
1167            Self::Admin => "admin",
1168            Self::Metrics => "metrics",
1169        }
1170    }
1171
1172    /// The key an operator edits to move this socket, for a refusal to name.
1173    fn bind_key(self) -> &'static str {
1174        match self {
1175            Self::Acme => "server.bind_address",
1176            Self::Admin => "admin.bind_address",
1177            Self::Metrics => "metrics.bind_address",
1178        }
1179    }
1180}
1181
1182/// What a reload does to one role's socket.
1183///
1184/// Built while a failure can still refuse the whole reload, applied once nothing
1185/// can fail — the same build-then-publish split every other part of a generation
1186/// makes, applied to the one resource that cannot simply be constructed twice.
1187enum SocketPlan {
1188    /// The role's address and enablement are both unchanged. Note this is
1189    /// decided from the *configuration*, never from the address actually bound:
1190    /// a caller supplying its own socket (every test that binds `127.0.0.1:0`)
1191    /// is entitled to one that does not match the file, and rebinding it out
1192    /// from under them would be this feature breaking its own callers.
1193    Keep,
1194    /// Serve this newly bound socket: the role was switched on, or its address
1195    /// moved. Connections already established are untouched — hyper owns those,
1196    /// and only the socket beneath them changes.
1197    Serve(TcpListener),
1198    /// Release the socket: the role was switched off.
1199    Close,
1200}
1201
1202/// The three roles' socket plans, and what to say about them afterwards.
1203struct SocketPlans {
1204    acme: SocketPlan,
1205    admin: SocketPlan,
1206    metrics: SocketPlan,
1207    /// The resolved address of each freshly bound socket, so the announcement
1208    /// after the swap names where the listener actually landed.
1209    bound: Vec<(Role, String)>,
1210}
1211
1212impl SocketPlans {
1213    /// The roles whose socket this reload moved, for [`ReloadReport`] — which is
1214    /// what a test waits on and what an operator greps.
1215    ///
1216    /// [`ReloadReport`]: crate::reload::ReloadReport
1217    fn rebound(&self) -> Vec<&'static str> {
1218        self.bound.iter().map(|(role, _)| role.label()).collect()
1219    }
1220
1221    /// Hands each socket to its accept loop. Synchronous and infallible, so it
1222    /// sits inside the publishing run beside the routers.
1223    fn publish(self, cells: &Cells) {
1224        for (role, plan, handle) in [
1225            (Role::Acme, self.acme, &cells.acme),
1226            (Role::Admin, self.admin, &cells.admin),
1227            (Role::Metrics, self.metrics, &cells.metrics),
1228        ] {
1229            match plan {
1230                SocketPlan::Keep => {}
1231                SocketPlan::Serve(listener) => handle.serve(listener),
1232                SocketPlan::Close => {
1233                    handle.close();
1234                    info!(
1235                        event = "server_listener_stopped",
1236                        outcome = "success",
1237                        listener = role.label(),
1238                        "switched off by a configuration reload: the socket is released \
1239                         and nothing new is accepted on it"
1240                    );
1241                }
1242            }
1243        }
1244    }
1245}
1246
1247/// Decides, and performs, every bind this reload needs.
1248///
1249/// The ordering rule the whole reload path rests on, applied to sockets: bind
1250/// first, so a bad address refuses the reload rather than having already
1251/// dropped the live one. Two addresses that differ as strings but collide in
1252/// the kernel — `[::]:3000` against `0.0.0.0:3000` — make that bind fail with
1253/// `EADDRINUSE`, which is the safe direction: the running socket is still
1254/// serving and the refusal names the key.
1255///
1256/// A `tls.enabled` flip does not appear here at all. The mode is read per
1257/// connection (see [`crate::listener`]), so turning TLS on or off keeps the
1258/// socket exactly where it is — which is what makes the one case a bind-first
1259/// scheme could not serve, an unchanged address, not a case.
1260fn plan_sockets(
1261    applied: &Config,
1262    proposed: &Config,
1263) -> Result<SocketPlans, crate::reload::ReloadError> {
1264    let mut bound = Vec::new();
1265    let mut plan = |role: Role,
1266                    was: Option<&str>,
1267                    now: Option<&str>|
1268     -> Result<SocketPlan, crate::reload::ReloadError> {
1269        match (was, now) {
1270            (None, None) => Ok(SocketPlan::Keep),
1271            (Some(_), None) => Ok(SocketPlan::Close),
1272            (Some(was), Some(now)) if was == now => Ok(SocketPlan::Keep),
1273            (_, Some(now)) => {
1274                let listener = crate::listener::bind_blocking(now).map_err(|error| {
1275                    error!(event = "server_socket_bind_failed",
1276                           outcome = "failure",
1277                           listener = role.label(),
1278                           bind_address = %now,
1279                           error = %error);
1280                    crate::reload::ReloadError::Build(format!(
1281                        "`{}` is `{now}`, which cannot be bound: {error}",
1282                        role.bind_key()
1283                    ))
1284                })?;
1285                bound.push((role, bound_address(Some(&listener), now)));
1286                Ok(SocketPlan::Serve(listener))
1287            }
1288        }
1289    };
1290
1291    // The ACME listener is never switched off — there is no `server.enabled`,
1292    // and a CA serving no ACME would be a process with nothing to do.
1293    let acme = plan(
1294        Role::Acme,
1295        Some(&applied.server.bind_address),
1296        Some(&proposed.server.bind_address),
1297    )?;
1298    let admin = plan(
1299        Role::Admin,
1300        applied
1301            .admin
1302            .enabled
1303            .then_some(applied.admin.bind_address.as_str()),
1304        proposed
1305            .admin
1306            .enabled
1307            .then_some(proposed.admin.bind_address.as_str()),
1308    )?;
1309    let metrics = plan(
1310        Role::Metrics,
1311        applied
1312            .metrics
1313            .enabled
1314            .then_some(applied.metrics.bind_address.as_str()),
1315        proposed
1316            .metrics
1317            .enabled
1318            .then_some(proposed.metrics.bind_address.as_str()),
1319    )?;
1320
1321    Ok(SocketPlans {
1322        acme,
1323        admin,
1324        metrics,
1325        bound,
1326    })
1327}
1328
1329/// What one successful reload hands back to the supervisor: the report to log,
1330/// and the pieces of state the *next* reload compares against.
1331///
1332/// A struct rather than the tuple this used to return — which needed
1333/// `#[allow(clippy::type_complexity)]` and left the caller destructuring four
1334/// same-shaped values positionally, where swapping two would still compile.
1335/// The same move `ProfileParts` made, for the same reason.
1336struct Reloaded {
1337    report: crate::reload::ReloadReport,
1338    config: Arc<Config>,
1339    resolved: Vec<crate::config::ProfileConfig>,
1340    logins: Option<Arc<crate::webadmin::LoginLimiter>>,
1341    /// Each socket this reload bound, with the address it landed on. Announced
1342    /// by the supervisor rather than here, because saying a listener is up
1343    /// reaches the database (the panel's "nobody can sign in yet" warning) and
1344    /// [`publish_reload`] has no await point to spend on it — deliberately, that
1345    /// being what keeps its publishing run uninterruptible.
1346    opened: Vec<(Role, String)>,
1347    /// The endpoints this reload **added**, for the same reason and announced in
1348    /// the same place: `profile_mounted` is dispatched to the `[notify]`
1349    /// backends, which queues a job row.
1350    mounted: Vec<Arc<Profile>>,
1351}
1352
1353/// Everything a reload built and validated, waiting to be published.
1354///
1355/// The build/publish split is the whole shape of a reload, and making it two
1356/// values rather than two halves of one function buys the thing the split was
1357/// always claiming: [`prepare_reload`] can run wherever it likes — it runs on a
1358/// blocking thread, since building a `relay` backend can contact its upstream —
1359/// while [`publish_reload`] stays on the supervisor task, where having no await
1360/// point is what makes a generation unobservable half-applied.
1361struct Prepared {
1362    config: Arc<Config>,
1363    resolved: Vec<crate::config::ProfileConfig>,
1364    parts: crate::GenerationParts,
1365    generation: Generation,
1366    sockets: SocketPlans,
1367    logging: logging::PreparedLogging,
1368    /// Whether `RUST_LOG` is what the filter came from, so the publish phase can
1369    /// say when an edited `logging.filter` changed nothing, and which of the
1370    /// two outranking layers is why.
1371    logging_filter_source: logging::FilterSource,
1372    /// The endpoints in this generation that the previous one did not mount.
1373    mounted: Vec<Arc<Profile>>,
1374    /// The endpoints the previous generation mounted and this one does not.
1375    unmounted: Vec<String>,
1376}
1377
1378/// The build half of one reload: everything that can fail, and everything that
1379/// can block.
1380///
1381/// Nothing here touches a cell, so a failure anywhere leaves the running
1382/// generation exactly as it was — which is the property the whole "atomic,
1383/// refuse by name" decision exists for. Every socket this reload needs is bound
1384/// here too, where a port already in use is still a refusal rather than a
1385/// listener already dropped.
1386///
1387/// Run on a blocking thread by [`supervise_reloads`], because building a `relay`
1388/// backend for the first time contacts its upstream synchronously.
1389fn prepare_reload(
1390    config: &Arc<Config>,
1391    resolved: &[crate::config::ProfileConfig],
1392    assembly: &crate::Assembly,
1393    logins: Option<&crate::webadmin::LoginLimiter>,
1394) -> Result<Prepared, crate::reload::ReloadError> {
1395    use crate::reload::{Applied, ReloadError, check_frozen};
1396
1397    // Re-read from scratch: `Config::load` consults the file *and* the
1398    // `ACME_PROXY_*` environment, so a reload sees whatever the process would
1399    // see if it restarted right now.
1400    let next = Arc::new(Config::load().map_err(|error| ReloadError::Load(error.to_string()))?);
1401    let next_resolved = next
1402        .resolve_profiles()
1403        .map_err(|error| ReloadError::Load(error.to_string()))?;
1404
1405    check_frozen(
1406        &Applied {
1407            config,
1408            profiles: resolved,
1409        },
1410        &Applied {
1411            config: &next,
1412            profiles: &next_resolved,
1413        },
1414    )?;
1415
1416    // Built here rather than published here: a bad `logging.target` must refuse
1417    // the whole reload with the message startup would have printed, not leave a
1418    // half-swapped generation behind. The same build-then-publish split
1419    // `Assembly::build_parts` makes.
1420    // `flag_override()` is how a `--log-level` typed at startup survives a
1421    // `SIGHUP`: the stack is rebuilt from the file, so without re-reading it
1422    // the reload would silently demote the server to `logging.filter`.
1423    let logging = logging::prepare_logging(&next.logging, logging::flag_override())
1424        .map_err(ReloadError::Build)?;
1425    let logging_filter_source = logging.filter_source;
1426
1427    // The same validation startup runs before either socket binds, so a panel
1428    // that would refuse to start refuses to be reloaded into. It also compiles
1429    // every `admin.template_dir` override, which is what keeps a broken one a
1430    // failed reload rather than a 500 in a browser.
1431    crate::webadmin::check_config(&next).map_err(|error| ReloadError::Build(error.to_string()))?;
1432    // Its twin for the third listener: `webadmin::check_config` sees the
1433    // admin-versus-server pair, this one sees the two it cannot.
1434    check_metrics_config(&next).map_err(|error| ReloadError::Build(error.to_string()))?;
1435
1436    // Every socket this reload needs is bound **here**, where a failure is still
1437    // a refusal: a port already taken, an address that does not resolve, a
1438    // privileged port after a `setcap` was lost. Past the publish phase nothing
1439    // can fail, so the running listeners are never dropped for a configuration
1440    // that then turns out not to work.
1441    let sockets = plan_sockets(config, &next)?;
1442
1443    // The egress clients, the notification dispatchers and the signer backends.
1444    // The last is where a newly mounted endpoint gets a backend, a removed one's
1445    // is left out, and an edited `[signer]` is rebuilt over the live instance's
1446    // in-memory state — see `signer::build_backends`. It is also the one step
1447    // that can make a network call, hence this whole function's blocking thread.
1448    let parts = assembly
1449        .build_parts(&next_resolved, &next)
1450        .map_err(|error| ReloadError::Build(error.to_string()))?;
1451    let generation = build_generation(&next, &next_resolved, assembly, &parts, logins)
1452        .map_err(|error| ReloadError::Build(error.to_string()))?;
1453
1454    // Compared by name against what is running, not against what is written
1455    // down: `resolve_profiles` has already dropped every `enabled = false`
1456    // entry, so this is the set of endpoints actually served.
1457    let running: std::collections::HashSet<&str> = resolved
1458        .iter()
1459        .map(|profile| profile.name.as_str())
1460        .collect();
1461    let mounted = generation
1462        .profiles
1463        .iter()
1464        .filter(|profile| !running.contains(profile.name.as_str()))
1465        .cloned()
1466        .collect();
1467    let next_names: std::collections::HashSet<&str> = next_resolved
1468        .iter()
1469        .map(|profile| profile.name.as_str())
1470        .collect();
1471    let unmounted = resolved
1472        .iter()
1473        .map(|profile| profile.name.clone())
1474        .filter(|name| !next_names.contains(name.as_str()))
1475        .collect();
1476
1477    Ok(Prepared {
1478        config: next,
1479        resolved: next_resolved,
1480        parts,
1481        generation,
1482        sockets,
1483        logging,
1484        logging_filter_source,
1485        mounted,
1486        unmounted,
1487    })
1488}
1489
1490/// The publish half: infallible, synchronous, and uninterruptible.
1491///
1492/// There is no `.await` in here, and that is load-bearing rather than
1493/// incidental. `watch::Sender::send_replace` and
1494/// `mpsc::UnboundedSender::send` are both synchronous, so a run of them with no
1495/// await point between cannot be interleaved — no task can observe a generation
1496/// half-applied, and no lock is needed to say so.
1497///
1498/// The order matters in three places. `[logging]` goes **first**, because an
1499/// operator who raised the level did it to see what happens next, starting with
1500/// this reload's own completion line. The notifier map, the signer set and the
1501/// job registry go **before** the routers: a request served by the new
1502/// generation queues a `notify_deliver` row naming a slot id from the new
1503/// configuration, and a `NotifyJob` still holding the old map would retire it —
1504/// permanently, since an unknown backend id is a `Failed`, not a `Retry`. And
1505/// the TLS mode goes before the socket, so a freshly bound listener's very first
1506/// connection is already accepted under this generation's settings.
1507fn publish_reload(
1508    prepared: Prepared,
1509    applied: &Arc<Config>,
1510    assembly: &crate::Assembly,
1511    cells: &Cells,
1512    generation: u64,
1513    started: std::time::Instant,
1514) -> Reloaded {
1515    use crate::reload::ReloadReport;
1516
1517    let Prepared {
1518        config: next,
1519        resolved: next_resolved,
1520        parts,
1521        generation: built,
1522        sockets,
1523        logging,
1524        logging_filter_source,
1525        mounted,
1526        unmounted,
1527    } = prepared;
1528
1529    let logging_reloaded = logging::publish_logging(logging);
1530
1531    let report = ReloadReport {
1532        generation,
1533        profiles: built
1534            .profiles
1535            .iter()
1536            .map(|profile| profile.name.clone())
1537            .collect(),
1538        job_kinds: built.job_registry.kinds(),
1539        tls_reloaded: built.tls.is_some(),
1540        admin_tls_reloaded: built.admin_tls.is_some(),
1541        listeners_rebound: sockets.rebound(),
1542        logging_reloaded,
1543        duration: started.elapsed(),
1544    };
1545    let next_logins = built.logins.clone();
1546
1547    assembly.publish_notifiers(parts.dispatchers);
1548    // The set the *next* reload compares against, and the point at which the
1549    // backends this one dropped are finally released — after their replacements
1550    // were built and adopted their state, never before.
1551    assembly.publish_signers(parts.signers);
1552    cells
1553        .job_registry
1554        .send_replace(Arc::new(built.job_registry));
1555
1556    // `[jobs]` in its two halves, both synchronous and neither able to fail —
1557    // which is what lets them sit in this run rather than needing a build phase
1558    // of their own. The runner re-derives its pacing from the cell on its next
1559    // pass; `max_attempts` goes to the queue instead, because it is the enqueue
1560    // side that reads it, and it sets the budget for work queued from here on
1561    // rather than for the rows already waiting.
1562    cells.jobs.send_replace(Arc::new(next.jobs.clone()));
1563    assembly.jobs.set_max_attempts(next.jobs.max_attempts);
1564
1565    cells.acme.set_tls(built.tls);
1566    cells.admin.set_tls(built.admin_tls);
1567    let opened = sockets.bound.clone();
1568    sockets.publish(cells);
1569
1570    cells
1571        .acme_router
1572        .send_replace(built.acme_app.into_service::<axum::body::Body>());
1573    // An empty router when the panel is off, which is what a request arriving
1574    // on a connection established a moment before it was switched off now gets:
1575    // closing the socket stops the next client, and this stops that one.
1576    cells.admin_router.send_replace(
1577        built
1578            .admin_app
1579            .unwrap_or_default()
1580            .into_service::<axum::body::Body>(),
1581    );
1582
1583    // Said here rather than by the supervisor because it needs nothing but a
1584    // name, unlike the mounting half, which dispatches a notification.
1585    for profile in unmounted {
1586        warn!(
1587            event = "profile_unmounted",
1588            outcome = "advisory",
1589            profile = %profile,
1590            "the endpoint is no longer served: its accounts and orders stay in the \
1591             database and come back if it is mounted again, but any issuance still in \
1592             flight for it has no handler left to finish it"
1593        );
1594    }
1595
1596    // `--log-level` and `RUST_LOG` both outrank `logging.filter` on a reload
1597    // exactly as they do at startup — the two disagreeing would be worse — but
1598    // that makes an edited `logging.filter` a silent no-op, which is the one
1599    // outcome an operator would read as "my reload did not land". Said only
1600    // when both halves hold: something outranked the file, *and* the file's
1601    // filter actually moved. `source` names which, since the two are unset in
1602    // different places.
1603    if logging_filter_source.outranks_config() && applied.logging.filter != next.logging.filter {
1604        warn!(
1605            event = "server_logging_filter_overridden",
1606            outcome = "advisory",
1607            source = logging_filter_source.as_str(),
1608            configured = %next.logging.filter,
1609        );
1610    }
1611
1612    Reloaded {
1613        report,
1614        config: next,
1615        resolved: next_resolved,
1616        logins: next_logins,
1617        opened,
1618        mounted,
1619    }
1620}
1621
1622/// The one announcement an endpoint that has just come up makes: a log line and
1623/// a `[notify]` lifecycle event.
1624///
1625/// Shared by startup and by a reload that mounted a new endpoint, so the two
1626/// cannot drift — before the profile set could reload there was only one caller
1627/// and the sharing was not needed.
1628async fn announce_profile(profile: &Arc<Profile>) {
1629    info!(
1630        event = "profile_mounted",
1631        outcome = "success",
1632        profile = %profile.name,
1633        directory = %profile.directory_url(),
1634        challenge_bypass = profile.challenges.is_bypassed(),
1635        eab_enabled = profile.eab.enabled
1636    );
1637    profile
1638        .notify
1639        .dispatch(crate::notify::NotifyEvent::ProfileMounted(
1640            crate::notify::ProfileMountedData {
1641                profile: profile.name.clone(),
1642            },
1643        ))
1644        .await;
1645}
1646
1647/// A future that completes when the shutdown relay fires.
1648async fn on_shutdown(mut receiver: tokio::sync::watch::Receiver<bool>) {
1649    // An error means the sender was dropped, which only happens when the relay
1650    // task itself is gone — treat it as "shut down" rather than parking
1651    // forever.
1652    let _ = receiver.wait_for(|ready| *ready).await;
1653}
1654
1655/// Says the panel is up, and warns about the two states that make it useless.
1656///
1657/// Run whenever the admin listener **opens** — at startup, and again on a
1658/// reload that turns `admin.enabled` on. Separate from the serving path since
1659/// that no longer starts or stops per role: the socket is what comes and goes,
1660/// and this is what an operator needs told when it does.
1661async fn announce_admin_listener(config: &Arc<Config>, database: &Arc<Database>, bound: &str) {
1662    // A listener nobody holds an account for is a running service with no way
1663    // in; say so once, naming the command that fixes it.
1664    if crate::sqlite::admin_user::AdminUser::list_all(database)
1665        .await
1666        .is_ok_and(|users| users.is_empty())
1667    {
1668        warn!(
1669            event = "admin_no_users",
1670            outcome = "advisory",
1671            "the web admin is enabled but has no operators: create one with \
1672               `acme-proxy admin user create <username>`"
1673        );
1674    }
1675
1676    // Repeated on every start while it holds, the `challenge_validation_bypassed`
1677    // treatment: these operators can still sign in, they are simply made to
1678    // enrol before their session becomes usable, and that stays worth seeing
1679    // for exactly as long as it is true.
1680    if config.admin.require_mfa
1681        && let Ok(count) = crate::admin::mfa::operators_without_a_factor(database.clone()).await
1682        && count > 0
1683    {
1684        warn!(
1685            event = "admin_mfa_enrolment_pending",
1686            outcome = "advisory",
1687            count = count,
1688            "admin.require_mfa is on and some operators have no second factor: \
1689               their next sign-in will require enrolment before the session is usable"
1690        );
1691    }
1692
1693    // Swept once now, then on an interval: sessions outlive a restart, so a
1694    // startup-only sweep would leak every one an operator never signed out of.
1695    let idle = Duration::from_secs(config.admin.session_idle_timeout_seconds);
1696    if let Err(error) = crate::sqlite::admin_session::AdminSession::cleanup(idle, database).await {
1697        error!(event = "admin_session_cleanup_failed", outcome = "failure", error = %error);
1698    }
1699
1700    // The **resolved** address, so a `:0` bind is discoverable.
1701    info!(
1702        event = "admin_listening",
1703        outcome = "success",
1704        bind_address = %bound,
1705        protocol = if config.admin.tls.enabled { "https" } else { "http" },
1706        base_url = %config.admin.base_url
1707    );
1708}
1709
1710/// The metrics listener's own one-line announcement, and its standing warning.
1711fn announce_metrics_listener(bound: &str) {
1712    info!(
1713        event = "metrics_listening",
1714        outcome = "success",
1715        bind_address = %bound,
1716        "unauthenticated by design: the port is the boundary, so firewall it"
1717    );
1718}
1719
1720/// The address a socket ended up on, falling back to what was configured.
1721///
1722/// The two differ for `:0` and for a caller that supplied its own listener; the
1723/// resolved one is what an operator needs, and the configured one is all there
1724/// is to say when the socket cannot answer.
1725fn bound_address(listener: Option<&TcpListener>, configured: &str) -> String {
1726    listener
1727        .and_then(|listener| listener.local_addr().ok())
1728        .map_or_else(|| configured.to_string(), |address| address.to_string())
1729}
1730
1731async fn shutdown_signal() {
1732    let ctrl_c = async {
1733        tokio::signal::ctrl_c()
1734            .await
1735            .expect("failed to install Ctrl+C handler");
1736    };
1737
1738    #[cfg(unix)]
1739    let terminate = async {
1740        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
1741            .expect("failed to install signal handler")
1742            .recv()
1743            .await;
1744    };
1745
1746    #[cfg(not(unix))]
1747    let terminate = std::future::pending::<()>();
1748
1749    tokio::select! {
1750        _ = ctrl_c => {},
1751        _ = terminate => {},
1752    }
1753}
1754
1755/// Aborts a background task when it goes out of scope.
1756struct AbortOnDrop(tokio::task::JoinHandle<()>);
1757
1758impl Drop for AbortOnDrop {
1759    fn drop(&mut self) {
1760        self.0.abort();
1761    }
1762}
1763
1764#[cfg(test)]
1765mod tests {
1766    use super::*;
1767
1768    /// `--version` exists and reports the crate version. The bug report
1769    /// template tells people to run it, and clap generates the flag only
1770    /// because `#[command(version = …)]` says so — drop that and the first
1771    /// instruction on the form starts erroring out.
1772    #[test]
1773    fn version_flag_reports_the_crate_version() {
1774        let Err(error) = Cli::try_parse_from(["acme-proxy", "--version"]) else {
1775            panic!("--version parsed as a command rather than printing a version");
1776        };
1777        assert_eq!(error.kind(), clap::error::ErrorKind::DisplayVersion);
1778        assert!(error.to_string().contains(env!("CARGO_PKG_VERSION")));
1779    }
1780
1781    /// `--log-level` is global like `--yes` and `--color`, so it may be given
1782    /// on either side of the subcommand — which is the whole reason an
1783    /// operator reaches for it, having already typed the command once.
1784    #[test]
1785    fn log_level_is_a_global_flag_with_a_closed_set_of_values() {
1786        let cli = Cli::try_parse_from(["acme-proxy", "account", "list"]).unwrap();
1787        assert_eq!(
1788            cli.log_level, None,
1789            "absent by default: an admin command says nothing unless asked",
1790        );
1791
1792        for argv in [
1793            ["acme-proxy", "--log-level", "debug", "account", "list"],
1794            ["acme-proxy", "account", "list", "--log-level", "debug"],
1795        ] {
1796            let cli = Cli::try_parse_from(argv).unwrap();
1797            assert_eq!(cli.log_level, Some(LogLevel::Debug), "{argv:?}");
1798        }
1799
1800        let cli = Cli::try_parse_from(["acme-proxy", "serve", "--log-level", "off"]).unwrap();
1801        assert_eq!(cli.log_level, Some(LogLevel::Off));
1802
1803        // A `value_enum`, so an unrecognised level is refused with the six
1804        // spellings listed rather than treated as a filter directive.
1805        let Err(error) =
1806            Cli::try_parse_from(["acme-proxy", "account", "list", "--log-level", "loud"])
1807        else {
1808            panic!("`--log-level loud` must be refused");
1809        };
1810        assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue);
1811    }
1812
1813    #[test]
1814    fn parse_cli_subcommands() {
1815        let cli = Cli::try_parse_from(["acme-proxy"]).unwrap();
1816        assert!(cli.command.is_none());
1817
1818        let cli = Cli::try_parse_from(["acme-proxy", "serve"]).unwrap();
1819        assert!(matches!(cli.command, Some(Command::Serve)));
1820
1821        let cli = Cli::try_parse_from(["acme-proxy", "account", "list", "--json"]).unwrap();
1822        assert!(matches!(
1823            cli.command,
1824            Some(Command::Account {
1825                command: AccountCommand::List {
1826                    json: true,
1827                    profile: None,
1828                    limit: window::DEFAULT_LIMIT,
1829                    offset: 0
1830                }
1831            })
1832        ));
1833
1834        let cli = Cli::try_parse_from(["acme-proxy", "account", "show", "acct-1"]).unwrap();
1835        assert!(matches!(
1836            cli.command,
1837            Some(Command::Account {
1838                command: AccountCommand::Show { id, json: false }
1839            }) if id == "acct-1"
1840        ));
1841
1842        let cli = Cli::try_parse_from([
1843            "acme-proxy",
1844            "account",
1845            "update-contact",
1846            "acct-1",
1847            "--contact",
1848            "mailto:test@example.com",
1849        ])
1850        .unwrap();
1851        assert!(matches!(
1852            cli.command,
1853            Some(Command::Account {
1854                command: AccountCommand::UpdateContact { id, contact }
1855            }) if id == "acct-1" && contact == vec!["mailto:test@example.com"]
1856        ));
1857
1858        let cli = Cli::try_parse_from(["acme-proxy", "account", "deactivate", "acct-1"]).unwrap();
1859        assert!(matches!(
1860            cli.command,
1861            Some(Command::Account {
1862                command: AccountCommand::Deactivate { id }
1863            }) if id == "acct-1"
1864        ));
1865
1866        let cli = Cli::try_parse_from(["acme-proxy", "-y", "account", "delete", "acct-1"]).unwrap();
1867        assert!(cli.yes);
1868        assert!(matches!(
1869            cli.command,
1870            Some(Command::Account {
1871                command: AccountCommand::Delete { id }
1872            }) if id == "acct-1"
1873        ));
1874
1875        let cli = Cli::try_parse_from([
1876            "acme-proxy",
1877            "order",
1878            "list",
1879            "--account-id",
1880            "acct-1",
1881            "--status",
1882            "pending",
1883            "--json",
1884        ])
1885        .unwrap();
1886        assert!(matches!(
1887            cli.command,
1888            Some(Command::Order {
1889                command: OrderCommand::List {
1890                    profile: None,
1891                    account_id: Some(a),
1892                    status: Some(s),
1893                    expiring_in: None,
1894                    hide_superseded: false,
1895                    limit: window::DEFAULT_LIMIT,
1896                    offset: 0,
1897                    json: true
1898                }
1899            }) if a == "acct-1" && s == "pending"
1900        ));
1901
1902        let cli = Cli::try_parse_from([
1903            "acme-proxy",
1904            "order",
1905            "list",
1906            "--expiring-in",
1907            "30",
1908            "--hide-superseded",
1909        ])
1910        .unwrap();
1911        assert!(matches!(
1912            cli.command,
1913            Some(Command::Order {
1914                command: OrderCommand::List {
1915                    expiring_in: Some(30),
1916                    hide_superseded: true,
1917                    status: None,
1918                    account_id: None,
1919                    profile: None,
1920                    limit: window::DEFAULT_LIMIT,
1921                    offset: 0,
1922                    json: false
1923                }
1924            })
1925        ));
1926
1927        let cli = Cli::try_parse_from(["acme-proxy", "order", "show", "ord-1"]).unwrap();
1928        assert!(matches!(
1929            cli.command,
1930            Some(Command::Order {
1931                command: OrderCommand::Show { id, json: false }
1932            }) if id == "ord-1"
1933        ));
1934
1935        let cli = Cli::try_parse_from(["acme-proxy", "order", "delete", "ord-1"]).unwrap();
1936        assert!(matches!(
1937            cli.command,
1938            Some(Command::Order {
1939                command: OrderCommand::Delete { id }
1940            }) if id == "ord-1"
1941        ));
1942
1943        let cli = Cli::try_parse_from(["acme-proxy", "order", "revoke", "ord-1", "--reason", "1"])
1944            .unwrap();
1945        assert!(matches!(
1946            cli.command,
1947            Some(Command::Order {
1948                command: OrderCommand::Revoke { id, reason: Some(1) }
1949            }) if id == "ord-1"
1950        ));
1951
1952        let cli =
1953            Cli::try_parse_from(["acme-proxy", "nonce", "cleanup", "--ttl-seconds", "60"]).unwrap();
1954        assert!(matches!(
1955            cli.command,
1956            Some(Command::Nonce {
1957                command: NonceCommand::Cleanup {
1958                    ttl_seconds: Some(60)
1959                }
1960            })
1961        ));
1962
1963        let cli = Cli::try_parse_from([
1964            "acme-proxy",
1965            "eab",
1966            "create",
1967            "--label",
1968            "test-key",
1969            "--json",
1970        ])
1971        .unwrap();
1972        assert!(matches!(
1973            cli.command,
1974            Some(Command::Eab {
1975                command: EabCommand::Create { label: Some(l), profile: None, json: true }
1976            }) if l == "test-key"
1977        ));
1978
1979        let cli = Cli::try_parse_from(["acme-proxy", "eab", "list"]).unwrap();
1980        assert!(matches!(
1981            cli.command,
1982            Some(Command::Eab {
1983                command: EabCommand::List {
1984                    limit: 50,
1985                    offset: 0,
1986                    json: false
1987                }
1988            })
1989        ));
1990
1991        // The four commands added with `TODO.md`'s "last few asymmetries": a
1992        // detail for the one listable object that had none, and the three reads
1993        // the panel could already answer and the host could not.
1994        let cli = Cli::try_parse_from(["acme-proxy", "order", "chain", "ord-1"]).unwrap();
1995        assert!(matches!(
1996            cli.command,
1997            Some(Command::Order {
1998                command: OrderCommand::Chain { id }
1999            }) if id == "ord-1"
2000        ));
2001
2002        let cli = Cli::try_parse_from(["acme-proxy", "nonce", "count", "--json"]).unwrap();
2003        assert!(matches!(
2004            cli.command,
2005            Some(Command::Nonce {
2006                command: NonceCommand::Count { json: true }
2007            })
2008        ));
2009
2010        let cli = Cli::try_parse_from(["acme-proxy", "profile", "list"]).unwrap();
2011        assert!(matches!(
2012            cli.command,
2013            Some(Command::Profile {
2014                command: ProfileCommand::List { json: false }
2015            })
2016        ));
2017
2018        let cli = Cli::try_parse_from(["acme-proxy", "admin", "user", "show", "alice"]).unwrap();
2019        assert!(matches!(
2020            cli.command,
2021            Some(Command::Admin {
2022                command: AdminCommand::User {
2023                    command: crate::cli::webadmin::AdminUserCommand::Show { username, json: false }
2024                }
2025            }) if username == "alice"
2026        ));
2027
2028        // The window the three formerly unwindowed listings grew, defaulted the
2029        // same way as the four that already had one.
2030        let cli =
2031            Cli::try_parse_from(["acme-proxy", "admin", "user", "list", "--limit", "2"]).unwrap();
2032        assert!(matches!(
2033            cli.command,
2034            Some(Command::Admin {
2035                command: AdminCommand::User {
2036                    command: crate::cli::webadmin::AdminUserCommand::List {
2037                        limit: 2,
2038                        offset: 0,
2039                        json: false
2040                    }
2041                }
2042            })
2043        ));
2044
2045        let cli =
2046            Cli::try_parse_from(["acme-proxy", "admin", "session", "list", "--offset=5"]).unwrap();
2047        assert!(matches!(
2048            cli.command,
2049            Some(Command::Admin {
2050                command: AdminCommand::Session {
2051                    command: crate::cli::webadmin::AdminSessionCommand::List {
2052                        username: None,
2053                        limit: window::DEFAULT_LIMIT,
2054                        offset: 5,
2055                        json: false
2056                    }
2057                }
2058            })
2059        ));
2060
2061        let cli = Cli::try_parse_from(["acme-proxy", "eab", "show", "kid-1", "--json"]).unwrap();
2062        assert!(matches!(
2063            cli.command,
2064            Some(Command::Eab {
2065                command: EabCommand::Show { kid, json: true }
2066            }) if kid == "kid-1"
2067        ));
2068
2069        let cli = Cli::try_parse_from(["acme-proxy", "upstream", "register", "--eab-kid", "kid-1"])
2070            .unwrap();
2071        assert!(matches!(
2072            cli.command,
2073            Some(Command::Upstream {
2074                command: UpstreamCommand::Register { eab_kid: Some(kid), eab_hmac_key_file: None, profile: None }
2075            }) if kid == "kid-1"
2076        ));
2077
2078        // Registering against an upstream that needs no credential.
2079        let cli = Cli::try_parse_from(["acme-proxy", "upstream", "register"]).unwrap();
2080        assert!(matches!(
2081            cli.command,
2082            Some(Command::Upstream {
2083                command: UpstreamCommand::Register {
2084                    eab_kid: None,
2085                    eab_hmac_key_file: None,
2086                    profile: None,
2087                }
2088            })
2089        ));
2090
2091        // The secret itself has no flag: it is stdin- or file-only, never argv.
2092        assert!(
2093            Cli::try_parse_from(["acme-proxy", "upstream", "register", "--eab-hmac-key", "s"])
2094                .is_err(),
2095            "an EAB secret must not be accepted on the command line"
2096        );
2097
2098        let cli = Cli::try_parse_from(["acme-proxy", "upstream", "show", "--json"]).unwrap();
2099        assert!(matches!(
2100            cli.command,
2101            Some(Command::Upstream {
2102                command: UpstreamCommand::Show {
2103                    json: true,
2104                    profile: None
2105                }
2106            })
2107        ));
2108
2109        let cli = Cli::try_parse_from(["acme-proxy", "eab", "revoke", "kid-1"]).unwrap();
2110        assert!(matches!(
2111            cli.command,
2112            Some(Command::Eab {
2113                command: EabCommand::Revoke { kid }
2114            }) if kid == "kid-1"
2115        ));
2116
2117        let cli = Cli::try_parse_from(["acme-proxy", "admin", "user", "create", "alice"]).unwrap();
2118        assert!(matches!(
2119            cli.command,
2120            Some(Command::Admin {
2121                command: AdminCommand::User {
2122                    command: crate::cli::webadmin::AdminUserCommand::Create {
2123                        username,
2124                        password_file: None
2125                    }
2126                }
2127            }) if username == "alice"
2128        ));
2129
2130        let cli = Cli::try_parse_from([
2131            "acme-proxy",
2132            "admin",
2133            "user",
2134            "passwd",
2135            "alice",
2136            "--password-file",
2137            "/run/secrets/pw",
2138        ])
2139        .unwrap();
2140        assert!(matches!(
2141            cli.command,
2142            Some(Command::Admin {
2143                command: AdminCommand::User {
2144                    command: crate::cli::webadmin::AdminUserCommand::Passwd {
2145                        username,
2146                        password_file: Some(path)
2147                    }
2148                }
2149            }) if username == "alice" && path == std::path::Path::new("/run/secrets/pw")
2150        ));
2151
2152        // The password itself has no flag, for the same reason the EAB secret
2153        // has none: argv is visible in `ps` and lands in shell history.
2154        for command in ["create", "passwd"] {
2155            assert!(
2156                Cli::try_parse_from([
2157                    "acme-proxy",
2158                    "admin",
2159                    "user",
2160                    command,
2161                    "alice",
2162                    "--password",
2163                    "hunter2",
2164                ])
2165                .is_err(),
2166                "`admin user {command}` must not accept a password on the command line"
2167            );
2168        }
2169
2170        // `--color` is global like `--yes`, so it may sit anywhere on the line,
2171        // and an unknown value is refused by clap rather than falling back to
2172        // `auto` — the same rule `--status`/`--event` follow, for the same
2173        // reason: a silently ignored value looks exactly like a working one.
2174        let cli = Cli::try_parse_from(["acme-proxy", "account", "list", "--color", "never"])
2175            .expect("--color is global and accepts `never`");
2176        assert_eq!(cli.color, ColorChoice::Never);
2177
2178        let cli = Cli::try_parse_from(["acme-proxy", "--color", "always", "account", "list"])
2179            .expect("--color is global, so it may precede the subcommand");
2180        assert_eq!(cli.color, ColorChoice::Always);
2181
2182        assert_eq!(
2183            Cli::try_parse_from(["acme-proxy", "account", "list"])
2184                .unwrap()
2185                .color,
2186            ColorChoice::Auto,
2187            "unset means auto"
2188        );
2189
2190        assert!(
2191            Cli::try_parse_from(["acme-proxy", "account", "list", "--color", "sometimes"]).is_err(),
2192            "an unknown --color value must be refused, not ignored"
2193        );
2194
2195        let cli = Cli::try_parse_from(["acme-proxy", "admin", "user", "totp", "status", "alice"])
2196            .unwrap();
2197        assert!(matches!(
2198            cli.command,
2199            Some(Command::Admin {
2200                command: AdminCommand::User {
2201                    command: crate::cli::webadmin::AdminUserCommand::Totp {
2202                        command: crate::cli::webadmin::AdminUserTotpCommand::Status {
2203                            username,
2204                            json: false
2205                        }
2206                    }
2207                }
2208            }) if username == "alice"
2209        ));
2210
2211        let cli = Cli::try_parse_from([
2212            "acme-proxy",
2213            "admin",
2214            "user",
2215            "totp",
2216            "recovery-codes",
2217            "alice",
2218        ])
2219        .unwrap();
2220        assert!(matches!(
2221            cli.command,
2222            Some(Command::Admin {
2223                command: AdminCommand::User {
2224                    command: crate::cli::webadmin::AdminUserCommand::Totp {
2225                        command: crate::cli::webadmin::AdminUserTotpCommand::RecoveryCodes {
2226                            username
2227                        }
2228                    }
2229                }
2230            }) if username == "alice"
2231        ));
2232
2233        // There is no `enrol` from a terminal, deliberately: it would put the
2234        // base32 secret in scrollback and shell history. See the doc comment on
2235        // `AdminUserTotpCommand`.
2236        assert!(
2237            Cli::try_parse_from(["acme-proxy", "admin", "user", "totp", "enrol", "alice"]).is_err()
2238        );
2239
2240        let cli =
2241            Cli::try_parse_from(["acme-proxy", "-y", "admin", "user", "delete", "alice"]).unwrap();
2242        assert!(cli.yes);
2243        assert!(matches!(
2244            cli.command,
2245            Some(Command::Admin {
2246                command: AdminCommand::User {
2247                    command: crate::cli::webadmin::AdminUserCommand::Delete { username }
2248                }
2249            }) if username == "alice"
2250        ));
2251
2252        let cli =
2253            Cli::try_parse_from(["acme-proxy", "admin", "session", "list", "--json"]).unwrap();
2254        assert!(matches!(
2255            cli.command,
2256            Some(Command::Admin {
2257                command: AdminCommand::Session {
2258                    command: crate::cli::webadmin::AdminSessionCommand::List {
2259                        username: None,
2260                        limit: 50,
2261                        offset: 0,
2262                        json: true
2263                    }
2264                }
2265            })
2266        ));
2267
2268        // `--user` and `--all` answer the same question two ways; clap refuses
2269        // both rather than letting one silently win.
2270        assert!(
2271            Cli::try_parse_from([
2272                "acme-proxy",
2273                "admin",
2274                "session",
2275                "revoke",
2276                "--user",
2277                "alice",
2278                "--all",
2279            ])
2280            .is_err(),
2281            "--user and --all are mutually exclusive"
2282        );
2283    }
2284
2285    #[test]
2286    fn a_database_error_renders_as_a_cli_error() {
2287        let error = CliError::from(sqlx::Error::PoolClosed);
2288        assert!(error.to_string().starts_with("database error: "), "{error}");
2289    }
2290
2291    /// Every arm reaches its command handler. `Serve` is deliberately absent —
2292    /// it owns a socket, and [`serve_on`] is what the tests below drive.
2293    #[tokio::test]
2294    async fn dispatch_routes_each_command() {
2295        let database = Arc::new(Database::connect_in_memory().await.unwrap());
2296        let config = Arc::new(Config::default());
2297        let mut reader: &[u8] = &[];
2298
2299        let commands = vec![
2300            Command::Account {
2301                command: AccountCommand::List {
2302                    profile: None,
2303                    limit: window::DEFAULT_LIMIT,
2304                    offset: 0,
2305                    json: false,
2306                },
2307            },
2308            Command::Order {
2309                command: OrderCommand::List {
2310                    profile: None,
2311                    account_id: None,
2312                    status: None,
2313                    expiring_in: None,
2314                    hide_superseded: false,
2315                    limit: window::DEFAULT_LIMIT,
2316                    offset: 0,
2317                    json: false,
2318                },
2319            },
2320            Command::Nonce {
2321                command: NonceCommand::Cleanup {
2322                    ttl_seconds: Some(1),
2323                },
2324            },
2325            Command::Nonce {
2326                command: NonceCommand::Count { json: false },
2327            },
2328            Command::Eab {
2329                command: EabCommand::List {
2330                    limit: 50,
2331                    offset: 0,
2332                    json: false,
2333                },
2334            },
2335            Command::Man,
2336            Command::Completions {
2337                shell: clap_complete::aot::Shell::Bash,
2338            },
2339            // `Profile` is deliberately absent for `Upstream`'s reason below,
2340            // arrived at from the other end: it resolves the profiles, and
2341            // `Config::default()` mounts none, so it reports that rather than
2342            // listing nothing. Its arm is driven from `cli::profile`'s own
2343            // tests, against a configuration that has some.
2344            // `Upstream` is deliberately absent: it acts on a *profile's*
2345            // `[signer.relay]`, and this config has none, so it now
2346            // reports that rather than silently reading the global base
2347            // section nothing serves from. Covered in `cli::upstream`'s own
2348            // tests, which supply a configuration with profiles.
2349        ];
2350        for command in commands {
2351            dispatch(
2352                Some(command),
2353                true,
2354                ColorChoice::Never,
2355                &mut reader,
2356                &config,
2357                database.clone(),
2358            )
2359            .await
2360            .expect("every command must succeed against an empty database");
2361        }
2362    }
2363
2364    /// A failing command's message reaches [`dispatch`]'s caller rather than
2365    /// exiting the process where it was raised.
2366    #[tokio::test]
2367    async fn dispatch_propagates_a_command_failure() {
2368        let database = Arc::new(Database::connect_in_memory().await.unwrap());
2369        let config = Arc::new(Config::default());
2370        let mut reader: &[u8] = &[];
2371
2372        let error = dispatch(
2373            Some(Command::Account {
2374                command: AccountCommand::Show {
2375                    id: "acct-nope".to_string(),
2376                    json: false,
2377                },
2378            }),
2379            true,
2380            ColorChoice::Never,
2381            &mut reader,
2382            &config,
2383            database,
2384        )
2385        .await
2386        .expect_err("an unknown account must fail");
2387        assert_eq!(error, CliError("no such account: acct-nope".to_string()));
2388    }
2389
2390    mod serving {
2391        use super::*;
2392        use tokio::io::{AsyncReadExt, AsyncWriteExt};
2393        use tokio::net::TcpStream;
2394
2395        /// A single-profile configuration whose CA and TLS material all live
2396        /// under `dir`, so a test never touches the repository.
2397        fn config_in(dir: impl AsRef<std::path::Path>, tls: bool) -> Config {
2398            let dir = dir.as_ref();
2399            let _lock = crate::config::ENV_LOCK
2400                .lock()
2401                .unwrap_or_else(std::sync::PoisonError::into_inner);
2402            let ca = dir.join("ca");
2403            let body = format!(
2404                r#"
2405                [server]
2406                bind_address = "127.0.0.1:0"
2407                base_url = "http://localhost:3000"
2408
2409                [server.tls]
2410                enabled = {tls}
2411                cert_path = "{dir}/server.pem"
2412                key_path = "{dir}/server.key"
2413
2414                [profiles.default]
2415                signer.local_ca.cert_path = "{ca}.pem"
2416                signer.local_ca.key_path = "{ca}.key"
2417                signer.local_ca.crl_path = "{ca}.crl"
2418                "#,
2419                dir = dir.display(),
2420                ca = ca.display(),
2421            );
2422            std::fs::write(dir.join("config.toml"), body).unwrap();
2423            // SAFETY: the lock above makes this the only thread touching the
2424            // environment, and the variable is removed before returning.
2425            unsafe {
2426                std::env::set_var("ACME_PROXY_CONFIG", dir.join("config").to_str().unwrap());
2427            }
2428            let config = Config::load().expect("the configuration must load");
2429            unsafe {
2430                std::env::remove_var("ACME_PROXY_CONFIG");
2431            }
2432            config
2433        }
2434
2435        /// Two profiles, each relaying to an upstream of its own.
2436        ///
2437        /// The two `[signer]` sections must genuinely differ — different
2438        /// `directory_url` *and* `account_key_path` — or `build_backends`
2439        /// collapses them to one shared backend and the case under test never
2440        /// arises. `signer_paths` would refuse a shared account key outright.
2441        fn two_relay_profiles(
2442            dir: impl AsRef<std::path::Path>,
2443            first: &crate::signer::relay::testsrv::Upstream,
2444            second: &crate::signer::relay::testsrv::Upstream,
2445        ) -> Config {
2446            let dir = dir.as_ref();
2447            let _lock = crate::config::ENV_LOCK
2448                .lock()
2449                .unwrap_or_else(std::sync::PoisonError::into_inner);
2450            let body = format!(
2451                r#"
2452                [server]
2453                bind_address = "127.0.0.1:0"
2454                base_url = "http://localhost:3000"
2455
2456                [profiles.first]
2457                signer.backend = "relay"
2458                signer.relay.directory_url = "{first_url}"
2459                signer.relay.account_key_path = "{dir}/first.key"
2460
2461                [profiles.second]
2462                signer.backend = "relay"
2463                signer.relay.directory_url = "{second_url}"
2464                signer.relay.account_key_path = "{dir}/second.key"
2465                "#,
2466                first_url = first.directory_url(),
2467                second_url = second.directory_url(),
2468                dir = dir.display(),
2469            );
2470            std::fs::write(dir.join("config.toml"), body).unwrap();
2471            // SAFETY: the lock above makes this the only thread touching the
2472            // environment, and the variable is removed before returning.
2473            unsafe {
2474                std::env::set_var("ACME_PROXY_CONFIG", dir.join("config").to_str().unwrap());
2475            }
2476            let config = Config::load().unwrap();
2477            unsafe {
2478                std::env::remove_var("ACME_PROXY_CONFIG");
2479            }
2480            config
2481        }
2482
2483        fn temp_dir() -> crate::testutil::TempDir {
2484            crate::testutil::TempDir::new("serve")
2485        }
2486
2487        /// Boots `serve_on` on an ephemeral loopback port and returns it with
2488        /// the handle and the trigger that shuts it back down.
2489        async fn boot(
2490            config: Config,
2491        ) -> (
2492            SocketAddr,
2493            tokio::sync::oneshot::Sender<()>,
2494            tokio::task::JoinHandle<anyhow::Result<()>>,
2495        ) {
2496            let database = Arc::new(Database::connect_in_memory().await.unwrap());
2497            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2498            let addr = listener.local_addr().unwrap();
2499            let (tx, rx) = tokio::sync::oneshot::channel::<()>();
2500            let handle = tokio::spawn(serve_on(Arc::new(config), database, listener, async {
2501                let _ = rx.await;
2502            }));
2503            (addr, tx, handle)
2504        }
2505
2506        /// The cleartext path: real socket, real router, real graceful
2507        /// shutdown. `/health` is a root route, so this also proves the app
2508        /// `serve_on` assembles is the one `build_app` produces.
2509        #[tokio::test]
2510        async fn a_cleartext_server_answers_then_shuts_down() {
2511            let dir = temp_dir();
2512            let (addr, shutdown, handle) = boot(config_in(&dir, false)).await;
2513
2514            let mut stream = TcpStream::connect(addr).await.unwrap();
2515            stream
2516                .write_all(b"GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
2517                .await
2518                .unwrap();
2519            let mut response = String::new();
2520            stream.read_to_string(&mut response).await.unwrap();
2521            assert!(response.starts_with("HTTP/1.1 200 OK"), "{response}");
2522
2523            shutdown.send(()).unwrap();
2524            handle
2525                .await
2526                .unwrap()
2527                .expect("a clean shutdown is not an error");
2528        }
2529
2530        /// The same path with `server.tls.enabled`, which swaps the listener
2531        /// for a `TlsListener` — a different `axum::serve` arm, and the only
2532        /// place `TlsListener::spawn` is wired up in production.
2533        #[tokio::test]
2534        async fn a_tls_server_answers_over_a_real_handshake() {
2535            use tokio_rustls::TlsConnector;
2536            use tokio_rustls::rustls::pki_types::ServerName;
2537
2538            let dir = temp_dir();
2539            let (addr, shutdown, handle) = boot(config_in(&dir, true)).await;
2540
2541            // The generated certificate is self-signed, so the client must not
2542            // try to verify it — the point here is the listener, not the trust
2543            // chain.
2544            let client =
2545                crate::challenge::tls_alpn_01::accept_any_client_config(&[b"http/1.1"]).unwrap();
2546            let stream = TcpStream::connect(addr).await.unwrap();
2547            let mut tls = TlsConnector::from(client)
2548                .connect(ServerName::try_from("localhost").unwrap(), stream)
2549                .await
2550                .unwrap();
2551            tls.write_all(b"GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
2552                .await
2553                .unwrap();
2554            let mut response = String::new();
2555            tls.read_to_string(&mut response).await.unwrap();
2556            assert!(response.starts_with("HTTP/1.1 200 OK"), "{response}");
2557
2558            shutdown.send(()).unwrap();
2559            handle
2560                .await
2561                .unwrap()
2562                .expect("a clean shutdown is not an error");
2563        }
2564
2565        /// The three-listener path: one shutdown signal, three sockets, and
2566        /// each one serving only what belongs to it.
2567        ///
2568        /// This is the regression test for the `watch` split — before it, the
2569        /// `shutdown` future was consumed once and only one listener stopped —
2570        /// and now also for the metrics listener being genuinely *separate*:
2571        /// `/metrics` answering on the ACME port would put issuance volume and
2572        /// every profile name on the public socket, which is exactly what
2573        /// giving it its own port is for.
2574        #[tokio::test]
2575        async fn all_three_listeners_serve_and_one_signal_stops_them() {
2576            let dir = temp_dir();
2577            let mut config = config_in(&dir, false);
2578            config.admin.enabled = true;
2579            config.metrics.enabled = true;
2580
2581            let database = Arc::new(Database::connect_in_memory().await.unwrap());
2582            let acme_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2583            let admin_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2584            let metrics_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2585            let acme_addr = acme_listener.local_addr().unwrap();
2586            let admin_addr = admin_listener.local_addr().unwrap();
2587            let metrics_addr = metrics_listener.local_addr().unwrap();
2588            assert_ne!(acme_addr, admin_addr);
2589            assert_ne!(acme_addr, metrics_addr);
2590            assert_ne!(admin_addr, metrics_addr);
2591
2592            let (tx, rx) = tokio::sync::oneshot::channel::<()>();
2593            let handle = tokio::spawn(serve_on_with(
2594                Arc::new(config),
2595                database,
2596                acme_listener,
2597                Some(admin_listener),
2598                Some(metrics_listener),
2599                async {
2600                    let _ = rx.await;
2601                },
2602            ));
2603
2604            // `/health` is on both of the two that have it.
2605            for addr in [acme_addr, admin_addr] {
2606                let response = get(addr, "/health").await;
2607                assert!(
2608                    response.starts_with("HTTP/1.1 200 OK"),
2609                    "{addr}: {response}"
2610                );
2611            }
2612
2613            // The admin API answers on the admin socket (401, since this
2614            // request carries no session) and is absent from the ACME one.
2615            let admin = get(admin_addr, "/api/accounts").await;
2616            assert!(admin.starts_with("HTTP/1.1 401"), "{admin}");
2617            let acme = get(acme_addr, "/api/accounts").await;
2618            assert!(acme.starts_with("HTTP/1.1 404"), "{acme}");
2619
2620            // And the converse: ACME is on the ACME socket only.
2621            let directory = get(acme_addr, "/profile/default/directory").await;
2622            assert!(directory.starts_with("HTTP/1.1 200 OK"), "{directory}");
2623            let no_directory = get(admin_addr, "/profile/default/directory").await;
2624            assert!(no_directory.starts_with("HTTP/1.1 404"), "{no_directory}");
2625
2626            // The exposition is on its own socket...
2627            let metrics = get(metrics_addr, "/metrics").await;
2628            assert!(metrics.starts_with("HTTP/1.1 200 OK"), "{metrics}");
2629            assert!(metrics.contains("acme_proxy_requests_total"), "{metrics}");
2630            // ...and on **neither** of the other two. The whole point of the
2631            // third listener is that firewalling this port is what controls who
2632            // can read it.
2633            for addr in [acme_addr, admin_addr] {
2634                let leaked = get(addr, "/metrics").await;
2635                assert!(leaked.starts_with("HTTP/1.1 404"), "{addr}: {leaked}");
2636            }
2637
2638            // One signal, all three stop.
2639            tx.send(()).unwrap();
2640            tokio::time::timeout(Duration::from_secs(10), handle)
2641                .await
2642                .expect("every listener must stop on one signal")
2643                .unwrap()
2644                .expect("a clean shutdown is not an error");
2645        }
2646
2647        /// Three listeners means the collision check is pairwise, and
2648        /// `webadmin::check_config` only covers admin-versus-server.
2649        #[tokio::test]
2650        async fn a_metrics_bind_colliding_with_another_listener_is_refused() {
2651            let dir = temp_dir();
2652
2653            for (other, set) in [
2654                (
2655                    "server.bind_address",
2656                    Box::new(|c: &mut Config| {
2657                        c.metrics.bind_address = c.server.bind_address.clone()
2658                    }) as Box<dyn Fn(&mut Config)>,
2659                ),
2660                (
2661                    "admin.bind_address",
2662                    Box::new(|c: &mut Config| {
2663                        c.admin.enabled = true;
2664                        c.metrics.bind_address = c.admin.bind_address.clone();
2665                    }),
2666                ),
2667            ] {
2668                let mut config = config_in(&dir, false);
2669                config.metrics.enabled = true;
2670                set(&mut config);
2671
2672                let error = bind_metrics(&Arc::new(config))
2673                    .await
2674                    .expect_err("a shared socket must not start");
2675                let message = error.to_string();
2676                assert!(message.contains(other), "{message}");
2677            }
2678        }
2679
2680        /// The admin bind is only a conflict when the panel is actually going
2681        /// to bind it. Both default to loopback ports, so refusing on the
2682        /// *value* alone would reject a configuration that works.
2683        #[tokio::test]
2684        async fn a_metrics_bind_matching_a_disabled_admin_is_allowed() {
2685            let dir = temp_dir();
2686            let mut config = config_in(&dir, false);
2687            config.metrics.enabled = true;
2688            config.admin.enabled = false;
2689            config.metrics.bind_address = config.admin.bind_address.clone();
2690
2691            let listener = bind_metrics(&Arc::new(config))
2692                .await
2693                .expect("a disabled panel holds no socket");
2694            assert!(listener.is_some());
2695        }
2696
2697        /// Off by default, and off means no socket at all rather than one
2698        /// answering 404.
2699        #[tokio::test]
2700        async fn metrics_disabled_binds_nothing() {
2701            let dir = temp_dir();
2702            let config = config_in(&dir, false);
2703            assert!(!config.metrics.enabled);
2704
2705            assert!(bind_metrics(&Arc::new(config)).await.unwrap().is_none());
2706        }
2707
2708        /// The admin listener's **own** TLS arm.
2709        ///
2710        /// `[server.tls]` and `[admin.tls]` are separate settings with separate
2711        /// certificate paths, on purpose — the two listeners answer to
2712        /// different names — and they go through separate `axum::serve` arms in
2713        /// `serve_admin`. `a_tls_server_answers_over_a_real_handshake` covers
2714        /// the ACME one; this one was the only `TlsListener::spawn` call site
2715        /// in the crate with no test at all, which for the listener that
2716        /// carries an operator's session cookie is the wrong one to miss.
2717        #[tokio::test]
2718        async fn the_admin_listener_answers_over_its_own_tls() {
2719            use tokio_rustls::TlsConnector;
2720            use tokio_rustls::rustls::pki_types::ServerName;
2721
2722            let dir = temp_dir();
2723            let mut config = config_in(&dir, false);
2724            config.admin.enabled = true;
2725            // A distinct certificate from the ACME listener's, which is the
2726            // whole reason these are two settings.
2727            config.admin.tls.enabled = true;
2728            config.admin.tls.cert_path = dir.as_ref().join("admin.pem").display().to_string();
2729            config.admin.tls.key_path = dir.as_ref().join("admin.key").display().to_string();
2730            // `check_config` refuses a non-loopback bind without TLS; with TLS
2731            // on it is allowed, and this exercises that branch too.
2732            config.admin.base_url = "https://localhost:3001".to_string();
2733
2734            let database = Arc::new(Database::connect_in_memory().await.unwrap());
2735            let acme_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2736            let admin_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2737            let acme_addr = acme_listener.local_addr().unwrap();
2738            let admin_addr = admin_listener.local_addr().unwrap();
2739
2740            let (tx, rx) = tokio::sync::oneshot::channel::<()>();
2741            let handle = tokio::spawn(serve_on_with(
2742                Arc::new(config),
2743                database,
2744                acme_listener,
2745                Some(admin_listener),
2746                // This test is about the admin listener's own TLS arm; the
2747                // metrics listener has none.
2748                None,
2749                async {
2750                    let _ = rx.await;
2751                },
2752            ));
2753
2754            // The ACME socket is still cleartext — the two settings really are
2755            // independent, which a shared switch would hide.
2756            let acme = get(acme_addr, "/health").await;
2757            assert!(acme.starts_with("HTTP/1.1 200 OK"), "{acme}");
2758
2759            // The admin socket needs a handshake. Self-signed, so the client
2760            // verifies nothing: the listener is the subject, not the chain.
2761            let client =
2762                crate::challenge::tls_alpn_01::accept_any_client_config(&[b"http/1.1"]).unwrap();
2763            let stream = TcpStream::connect(admin_addr).await.unwrap();
2764            let mut tls = TlsConnector::from(client)
2765                .connect(ServerName::try_from("localhost").unwrap(), stream)
2766                .await
2767                .expect("the admin listener must complete a handshake");
2768            tls.write_all(
2769                b"GET /api/accounts HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
2770            )
2771            .await
2772            .unwrap();
2773            let mut response = String::new();
2774            tls.read_to_string(&mut response).await.unwrap();
2775            assert!(
2776                response.starts_with("HTTP/1.1 401"),
2777                "the admin API answers over TLS, unauthenticated: {response}"
2778            );
2779
2780            // The certificate really was written to the admin paths, not the
2781            // server's — a shared path would make one listener overwrite the
2782            // other's key on every start.
2783            assert!(dir.as_ref().join("admin.pem").exists());
2784            assert!(dir.as_ref().join("admin.key").exists());
2785            assert!(!dir.as_ref().join("server.pem").exists());
2786
2787            tx.send(()).unwrap();
2788            tokio::time::timeout(Duration::from_secs(10), handle)
2789                .await
2790                .expect("both listeners must stop")
2791                .unwrap()
2792                .expect("a clean shutdown is not an error");
2793        }
2794
2795        /// With `[admin]` off — the default — nothing is bound but the ACME
2796        /// socket, and the panel's routes do not exist anywhere.
2797        #[tokio::test]
2798        async fn the_admin_listener_is_absent_by_default() {
2799            let dir = temp_dir();
2800            let config = config_in(&dir, false);
2801            assert!(!config.admin.enabled, "the default must stay off");
2802            let (addr, shutdown, handle) = boot(config).await;
2803
2804            let response = get(addr, "/api/accounts").await;
2805            assert!(response.starts_with("HTTP/1.1 404"), "{response}");
2806
2807            shutdown.send(()).unwrap();
2808            handle.await.unwrap().unwrap();
2809        }
2810
2811        /// A `[admin]` section that cannot work stops the whole process before
2812        /// either socket is bound — it must not leave the ACME listener up and
2813        /// the panel silently missing.
2814        #[tokio::test]
2815        async fn an_invalid_admin_section_refuses_to_serve() {
2816            let dir = temp_dir();
2817            let mut config = config_in(&dir, false);
2818            config.admin.enabled = true;
2819            // Non-loopback with TLS off: the `Secure` cookie would never be
2820            // stored, so this is a hard startup error.
2821            config.admin.bind_address = "0.0.0.0:0".to_string();
2822
2823            let database = Arc::new(Database::connect_in_memory().await.unwrap());
2824            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2825            let error = serve_on(Arc::new(config), database, listener, std::future::ready(()))
2826                .await
2827                .expect_err("a panel that cannot work must not start");
2828            assert!(error.to_string().contains("is not loopback"), "{error}");
2829        }
2830
2831        /// Sends one request and returns the raw response.
2832        async fn get(addr: SocketAddr, path: &str) -> String {
2833            let mut stream = TcpStream::connect(addr).await.unwrap();
2834            stream
2835                .write_all(
2836                    format!("GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
2837                        .as_bytes(),
2838                )
2839                .await
2840                .unwrap();
2841            let mut response = String::new();
2842            stream.read_to_string(&mut response).await.unwrap();
2843            response
2844        }
2845
2846        /// Two profiles relaying to **different** upstreams start.
2847        ///
2848        /// The whole shape of the reported bug: each profile's `[signer]`
2849        /// section differs, so `build_backends` keeps them apart, so there are
2850        /// two relay backends — and asking each for its own job handler made
2851        /// `JobRegistry::register` refuse the second for kind
2852        /// `signer_relay_issue`, taking the process down before the socket was
2853        /// ever served. This is the end-to-end form of
2854        /// `signer::relay::tests::multi_profile`, and the only test here that
2855        /// exercises `build_generation` assembling the registry from more than
2856        /// one relay.
2857        #[tokio::test(flavor = "multi_thread")]
2858        async fn two_profiles_relaying_to_different_upstreams_start() {
2859            use crate::signer::relay::testsrv;
2860
2861            let first = testsrv::start(testsrv::Script::default()).await;
2862            let second = testsrv::start(testsrv::Script::default()).await;
2863            let dir = temp_dir();
2864            let config = two_relay_profiles(&dir, &first, &second);
2865
2866            let (addr, shutdown, handle) = boot(config).await;
2867            let mut stream = TcpStream::connect(addr).await.unwrap();
2868            stream
2869                .write_all(b"GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
2870                .await
2871                .unwrap();
2872            let mut response = String::new();
2873            stream.read_to_string(&mut response).await.unwrap();
2874            assert!(response.starts_with("HTTP/1.1 200 OK"), "{response}");
2875
2876            shutdown.send(()).unwrap();
2877            handle
2878                .await
2879                .unwrap()
2880                .expect("two relay profiles must not refuse to start");
2881        }
2882
2883        /// A configuration mounting nothing fails before the socket is ever
2884        /// served, rather than starting a server that answers 404 everywhere.
2885        #[tokio::test]
2886        async fn a_configuration_with_no_profile_refuses_to_serve() {
2887            let database = Arc::new(Database::connect_in_memory().await.unwrap());
2888            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2889            let error = serve_on(
2890                Arc::new(Config::default()),
2891                database,
2892                listener,
2893                std::future::ready(()),
2894            )
2895            .await
2896            .expect_err("a server with no endpoint must not start");
2897            assert!(error.to_string().contains("profile"), "{error}");
2898        }
2899
2900        /// `Serve` is a `dispatch` arm like any other: its failure travels back
2901        /// as a value instead of taking the process down where it happened.
2902        #[tokio::test]
2903        async fn dispatch_serve_reports_a_startup_failure() {
2904            let database = Arc::new(Database::connect_in_memory().await.unwrap());
2905            // Binds fine, but mounts no endpoint — so it fails inside
2906            // `serve_on` rather than at the socket.
2907            let mut config = Config::default();
2908            config.server.bind_address = "127.0.0.1:0".to_string();
2909            let mut reader: &[u8] = &[];
2910
2911            let error = dispatch(
2912                Some(Command::Serve),
2913                true,
2914                ColorChoice::Never,
2915                &mut reader,
2916                &Arc::new(config),
2917                database,
2918            )
2919            .await
2920            .expect_err("a server with no endpoint must not start");
2921            assert!(error.to_string().contains("profile"), "{error}");
2922        }
2923
2924        /// Unreadable TLS material stops startup instead of silently falling
2925        /// back to cleartext on a port operators believe is HTTPS.
2926        #[tokio::test]
2927        async fn unusable_tls_material_stops_startup() {
2928            let dir = temp_dir();
2929            let mut config = config_in(&dir, true);
2930            std::fs::write(dir.join("server.pem"), "not a certificate").unwrap();
2931            std::fs::write(dir.join("server.key"), "not a key").unwrap();
2932            config.server.tls.cert_path = dir.join("server.pem").display().to_string();
2933            config.server.tls.key_path = dir.join("server.key").display().to_string();
2934
2935            let database = Arc::new(Database::connect_in_memory().await.unwrap());
2936            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2937            let error = serve_on(Arc::new(config), database, listener, std::future::ready(()))
2938                .await
2939                .expect_err("unreadable TLS material must not start a server");
2940            assert!(!error.to_string().is_empty());
2941        }
2942
2943        /// `serve` binds `server.bind_address` itself, so an unusable one is
2944        /// reported rather than panicking.
2945        #[tokio::test]
2946        async fn an_unbindable_address_is_reported() {
2947            let database = Arc::new(Database::connect_in_memory().await.unwrap());
2948            let mut config = Config::default();
2949            // A port on an address this process does not hold.
2950            config.server.bind_address = "192.0.2.1:1".to_string();
2951            let error = serve(Arc::new(config), database)
2952                .await
2953                .expect_err("binding an unroutable address must fail");
2954            assert!(error.to_string().contains("192.0.2.1:1"), "{error}");
2955        }
2956    }
2957}