use std::future::Future;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use tracing::{error, info, warn};
use acme_proxy_core::config::Config;
use acme_proxy_store::db::Database;
pub mod assembly;
pub mod generation;
pub mod logging;
pub mod profile;
pub mod reload;
pub mod roles;
pub mod sockets;
pub mod supervisor;
#[cfg(test)]
mod tests;
pub use assembly::{Assembly, GenerationParts};
pub use roles::{ProcessRole, RoleSet};
pub use sockets::check_metrics_config;
use generation::{Generation, announce_profile, build_generation};
use sockets::{
Sockets, announce_admin_listener, announce_metrics_listener, bind_admin, bind_metrics,
bound_address,
};
use supervisor::{Cells, supervise_reloads};
pub async fn run(
roles: RoleSet,
config: Arc<Config>,
database: Arc<Database>,
) -> anyhow::Result<()> {
let listener = match roles.has(ProcessRole::Acme) {
false => None,
true => Some(
TcpListener::bind(&config.server.bind_address)
.await
.map_err(|error| {
error!(event = "server_socket_bind_failed",
outcome = "failure",
bind_address = %config.server.bind_address,
error = %error);
anyhow::anyhow!("cannot bind {}: {error}", config.server.bind_address)
})?,
),
};
let (reload_handle, reloads) = crate::reload::channel();
let _hangups = AbortOnDrop(tokio::spawn(watch_for_hangup(reload_handle)));
let fatal = |error: &anyhow::Error| {
error!(event = "server_fatal_error", outcome = "failure", error = %error);
};
let admin_listener = match roles.has(ProcessRole::Admin) {
false => None,
true => bind_admin(&config).await.inspect_err(fatal)?,
};
let metrics_listener = bind_metrics(&config).await.inspect_err(fatal)?;
serve_on_with_reloads(
roles,
config,
database,
Sockets {
acme: listener,
admin: admin_listener,
metrics: metrics_listener,
},
shutdown_signal(),
reloads,
)
.await
.inspect_err(fatal)
}
#[cfg(unix)]
async fn watch_for_hangup(handle: crate::reload::ReloadHandle) {
let mut hangups = match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()) {
Ok(stream) => stream,
Err(error) => {
error!(event = "server_signal_handler_failed", outcome = "failure", signal = "SIGHUP", error = %error);
return;
}
};
while hangups.recv().await.is_some() {
if !handle.trigger() {
error!(
event = "server_reload_supervisor_gone",
outcome = "failure",
signal = "SIGHUP",
"the reload supervisor is not accepting requests; restart the process to \
apply a configuration change"
);
}
}
}
#[cfg(not(unix))]
async fn watch_for_hangup(_handle: crate::reload::ReloadHandle) {
std::future::pending::<()>().await;
}
pub async fn serve_on(
config: Arc<Config>,
database: Arc<Database>,
listener: TcpListener,
shutdown: impl Future<Output = ()> + Send + 'static,
) -> anyhow::Result<()> {
let admin_listener = bind_admin(&config).await?;
let metrics_listener = bind_metrics(&config).await?;
serve_on_with(
config,
database,
listener,
admin_listener,
metrics_listener,
shutdown,
)
.await
}
pub async fn serve_on_with(
config: Arc<Config>,
database: Arc<Database>,
listener: TcpListener,
admin_listener: Option<TcpListener>,
metrics_listener: Option<TcpListener>,
shutdown: impl Future<Output = ()> + Send + 'static,
) -> anyhow::Result<()> {
serve_on_with_reloads(
RoleSet::default(),
config,
database,
Sockets {
acme: Some(listener),
admin: admin_listener,
metrics: metrics_listener,
},
shutdown,
crate::reload::Reloads::none(),
)
.await
}
pub async fn serve_on_with_reloads(
roles: RoleSet,
config: Arc<Config>,
database: Arc<Database>,
sockets: Sockets,
shutdown: impl Future<Output = ()> + Send + 'static,
reloads: crate::reload::Reloads,
) -> anyhow::Result<()> {
let Sockets {
acme: listener,
admin: admin_listener,
metrics: metrics_listener,
} = sockets;
info!(
event = "server_startup",
outcome = "success",
roles = %roles.labels().join(","),
bind_address = %config.server.bind_address,
base_url = %config.server.base_url,
tls = config.server.tls.enabled,
database_database_url = %acme_proxy_core::logfields::redact_url(&config.database.url)
);
apply_or_require_schema(roles, &database).await?;
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
let _shutdown_relay = AbortOnDrop(tokio::spawn(async move {
shutdown.await;
let _ = shutdown_tx.send(true);
}));
let job_queue = acme_proxy_jobs::jobs::JobQueue::new(database.clone(), &config.jobs);
let resolved = config.resolve_profiles().inspect_err(|error| {
error!(event = "profile_init_failed", outcome = "failure", error = %error);
})?;
let (assembly, parts) = Assembly::new(
roles,
&resolved,
database.clone(),
job_queue.clone(),
&config,
)
.inspect_err(|error| {
error!(event = "profile_init_failed", outcome = "failure", error = %error);
})?;
let assembly = Arc::new(assembly);
if roles.has(ProcessRole::Worker) {
store_first_crls(&parts.signers).await;
}
let generation = build_generation(roles, &config, &resolved, &assembly, &parts, None)
.inspect_err(|error| {
error!(event = "profile_init_failed", outcome = "failure", error = %error);
})?;
for profile in &generation.profiles {
announce_profile(profile).await;
}
let Generation {
profiles: _,
acme_app,
admin_app,
job_registry,
tls,
admin_tls,
logins,
} = generation;
let (registry_tx, registry_rx) = tokio::sync::watch::channel(Arc::new(job_registry));
let (jobs_tx, jobs_rx) = tokio::sync::watch::channel(Arc::new(config.jobs.clone()));
let mut job_runner = roles.has(ProcessRole::Worker).then(|| {
AbortOnDrop(acme_proxy_jobs::jobs::spawn_runner_watching(
job_queue,
registry_rx,
jobs_rx,
shutdown_rx.clone(),
))
});
if !roles.has(ProcessRole::Worker) {
warn!(
event = "server_role_no_worker",
outcome = "advisory",
roles = %roles.labels().join(","),
"this process runs no worker, so nothing here drains the job queue: \
challenge validation, notifications and the periodic sweeps all wait for a \
process started with `--role worker`"
);
}
if roles.has(ProcessRole::Acme) {
info!(
event = "server_listening",
outcome = "success",
bind_address = %config.server.bind_address,
protocol = if tls.is_some() { "https" } else { "http" }
);
}
let admin_bound = bound_address(admin_listener.as_ref(), &config.admin.bind_address);
let metrics_bound = bound_address(metrics_listener.as_ref(), &config.metrics.bind_address);
let (acme_socket, acme_handle) = acme_proxy_net::listener::spawn("acme", listener, tls);
let (admin_socket, admin_handle) =
acme_proxy_net::listener::spawn("admin", admin_listener, admin_tls);
let (metrics_socket, metrics_handle) =
acme_proxy_net::listener::spawn("metrics", metrics_listener, None);
let (acme_router_tx, acme_router_rx) = crate::reload::router_channel(acme_app);
let acme = serve_role(
crate::reload::swappable(acme_router_rx),
acme_socket,
shutdown_rx.clone(),
);
let (admin_router_tx, admin_router_rx) =
crate::reload::router_channel(admin_app.unwrap_or_default());
let admin = serve_role(
crate::reload::swappable(admin_router_rx),
admin_socket,
shutdown_rx.clone(),
);
if roles.has(ProcessRole::Admin) && config.admin.enabled {
announce_admin_listener(&config, &database, &admin_bound).await;
}
if config.metrics.enabled {
announce_metrics_listener(&metrics_bound);
}
let metrics = serve_role(
acme_proxy_protocol::router::metrics_app(assembly.metrics.clone()),
metrics_socket,
shutdown_rx,
);
let _reload_supervisor = AbortOnDrop(tokio::spawn(supervise_reloads(
roles,
reloads,
config.clone(),
resolved,
assembly,
Cells {
acme_router: acme_router_tx,
admin_router: admin_router_tx,
job_registry: registry_tx,
jobs: jobs_tx,
acme: acme_handle,
admin: admin_handle,
metrics: metrics_handle,
},
logins,
)));
tokio::try_join!(acme, admin, metrics)?;
if let Some(mut runner) = job_runner.take()
&& tokio::time::timeout(RUNNER_SHUTDOWN_BUDGET, runner.take())
.await
.is_err()
{
warn!(
event = "job_runner_shutdown_timed_out",
outcome = "failure",
budget_ms = acme_proxy_core::logfields::millis(RUNNER_SHUTDOWN_BUDGET),
"the job runner did not stop in time; its leases expire on their own"
);
}
Ok(())
}
const RUNNER_SHUTDOWN_BUDGET: std::time::Duration = std::time::Duration::from_secs(10);
pub(crate) async fn store_first_crls(signers: &acme_proxy_signer::SignerSet) {
let mut done: std::collections::HashSet<String> = std::collections::HashSet::new();
for (_, backend) in signers.by_profile() {
if let Some(refresher) = backend.crl_refresher()
&& done.insert(refresher.issuer().to_string())
{
let _ = refresher.refresh().await;
}
}
}
async fn apply_or_require_schema(roles: RoleSet, database: &Arc<Database>) -> anyhow::Result<()> {
if roles.has(ProcessRole::Worker) {
return database.migrate().await.map_err(|error| {
error!(event = "db_migration_failed", outcome = "failure", error = %error);
anyhow::anyhow!("cannot apply the database migrations: {error}")
});
}
let pending = database.pending_migrations().await.map_err(|error| {
error!(event = "server_schema_check_failed", outcome = "failure", error = %error);
anyhow::anyhow!("cannot read the database schema version: {error}")
})?;
if pending.is_empty() {
return Ok(());
}
error!(
event = "server_schema_behind",
outcome = "failure",
pending = pending.len(),
roles = %roles.labels().join(","),
);
anyhow::bail!(
"the database is {} migration(s) behind and this process does not run the `worker` \
role, which owns the schema: run `acme-proxy migrate` (or start the worker) first",
pending.len()
)
}
fn serve_role(
app: axum::Router,
socket: acme_proxy_net::listener::RoleSocket,
shutdown: tokio::sync::watch::Receiver<bool>,
) -> impl Future<Output = std::io::Result<()>> + Send {
axum::serve(
socket,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(on_shutdown(shutdown))
.into_future()
}
async fn on_shutdown(mut receiver: tokio::sync::watch::Receiver<bool>) {
let _ = receiver.wait_for(|ready| *ready).await;
}
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}
struct AbortOnDrop(tokio::task::JoinHandle<()>);
impl AbortOnDrop {
fn take(&mut self) -> tokio::task::JoinHandle<()> {
std::mem::replace(&mut self.0, tokio::spawn(std::future::ready(())))
}
}
impl Drop for AbortOnDrop {
fn drop(&mut self) {
self.0.abort();
}
}