use std::fmt;
use std::sync::{Arc, Weak};
use std::time::Duration;
use tokio::task::JoinHandle;
#[cfg(feature = "ocsp")]
use tokio_util::sync::CancellationToken;
use crate::cache::{
Cache, CacheOptions, DEFAULT_OCSP_CHECK_INTERVAL, DEFAULT_RENEW_CHECK_INTERVAL,
};
#[cfg(feature = "ocsp")]
use crate::config::Config;
use crate::config::{ConfigOptions, OcspConfig};
use crate::storage::Storage;
#[derive(Clone)]
pub struct MaintenanceConfig {
pub renew_check_interval: Duration,
pub ocsp_check_interval: Duration,
pub ocsp: OcspConfig,
pub storage: Arc<dyn Storage>,
}
impl fmt::Debug for MaintenanceConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MaintenanceConfig")
.field("renew_check_interval", &self.renew_check_interval)
.field("ocsp_check_interval", &self.ocsp_check_interval)
.field("ocsp", &self.ocsp)
.finish_non_exhaustive()
}
}
impl MaintenanceConfig {
#[must_use]
pub fn new(storage: Arc<dyn Storage>) -> Self {
Self {
renew_check_interval: DEFAULT_RENEW_CHECK_INTERVAL,
ocsp_check_interval: DEFAULT_OCSP_CHECK_INTERVAL,
ocsp: OcspConfig::default(),
storage,
}
}
#[must_use]
pub fn with_storage(mut self, storage: Arc<dyn Storage>) -> Self {
self.storage = storage;
self
}
#[must_use]
pub fn with_renew_check_interval(mut self, interval: Duration) -> Self {
self.renew_check_interval = interval;
self
}
#[must_use]
pub fn with_ocsp_check_interval(mut self, interval: Duration) -> Self {
self.ocsp_check_interval = interval;
self
}
#[must_use]
pub fn with_ocsp(mut self, ocsp: OcspConfig) -> Self {
self.ocsp = ocsp;
self
}
pub fn apply_to(&self, cache: &mut CacheOptions, config: &mut ConfigOptions) {
cache.renew_check_interval = Some(self.renew_check_interval);
cache.ocsp_check_interval = Some(self.ocsp_check_interval);
config.storage = Some(Arc::clone(&self.storage));
config.ocsp = self.ocsp.clone();
}
pub fn apply_to_cache_options(&self, cache: &mut CacheOptions) {
cache.renew_check_interval = Some(self.renew_check_interval);
cache.ocsp_check_interval = Some(self.ocsp_check_interval);
}
pub fn apply_to_config_options(&self, config: &mut ConfigOptions) {
config.storage = Some(Arc::clone(&self.storage));
config.ocsp = self.ocsp.clone();
}
}
const MAX_PANIC_RESTARTS: u32 = 10;
pub(crate) fn spawn_maintainer(cache: Arc<Cache>, generation: u64) -> JoinHandle<()> {
let weak_cache = Arc::downgrade(&cache);
let stop = cache.stop_token();
drop(cache);
tokio::spawn(async move {
let completion = MaintenanceCompletionGuard {
cache: weak_cache.clone(),
generation,
};
let mut panic_count: u32 = 0;
loop {
let Some(cache) = weak_cache.upgrade() else {
return;
};
let renew_interval = cache
.options()
.renew_check_interval
.unwrap_or(DEFAULT_RENEW_CHECK_INTERVAL);
let ocsp_interval = cache
.options()
.ocsp_check_interval
.unwrap_or(DEFAULT_OCSP_CHECK_INTERVAL);
drop(cache);
let result = run_maintenance_loop(
weak_cache.clone(),
stop.clone(),
renew_interval,
ocsp_interval,
)
.await;
match result {
LoopExit::Stopped | LoopExit::Dropped => break,
LoopExit::Panicked => {
panic_count += 1;
if panic_count >= MAX_PANIC_RESTARTS {
tracing::error!("maintenance loop panicked too many times; giving up");
break;
}
tracing::warn!(panic_count, "maintenance loop panicked; restarting");
}
}
}
drop(completion);
})
}
struct MaintenanceCompletionGuard {
cache: Weak<Cache>,
generation: u64,
}
impl Drop for MaintenanceCompletionGuard {
fn drop(&mut self) {
if let Some(cache) = self.cache.upgrade() {
cache.mark_maintenance_finished(self.generation);
}
}
}
pub fn start_maintenance<M>(manager: &M) -> JoinHandle<()>
where
M: AsRef<crate::config::Config> + ?Sized,
{
let cache = Arc::clone(manager.as_ref().cache());
let weak_cache = Arc::downgrade(&cache);
let start_result = cache.start_maintenance_with_generation();
drop(cache);
tokio::spawn(async move {
match start_result {
Ok(generation) => {
Cache::observe_maintenance(weak_cache, generation).await;
}
Err(error) => {
tracing::error!(error = %error, "unable to start certificate maintenance");
}
}
})
}
pub async fn stop_maintenance<M>(manager: &M)
where
M: AsRef<crate::config::Config> + ?Sized,
{
manager.as_ref().cache().stop_and_wait().await;
}
enum LoopExit {
Stopped,
Dropped,
Panicked,
}
async fn run_maintenance_loop(
cache: Weak<Cache>,
stop: tokio_util::sync::CancellationToken,
renew_interval: Duration,
ocsp_interval: Duration,
) -> LoopExit {
let mut renew_ticker = tokio::time::interval(renew_interval);
renew_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
let mut ocsp_ticker = tokio::time::interval(ocsp_interval);
ocsp_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
let tick = tokio::select! {
() = stop.cancelled() => return LoopExit::Stopped,
_ = renew_ticker.tick() => Tick::Renew,
_ = ocsp_ticker.tick() => Tick::Ocsp,
};
let Some(cache_ref) = cache.upgrade() else {
return LoopExit::Dropped;
};
let outcome = tokio::spawn(async move {
match tick {
Tick::Renew => {
tracing::trace!("renewal sweep starting");
crate::maintain::renew_managed_certificates(&cache_ref).await;
}
Tick::Ocsp => {
tracing::trace!("OCSP sweep starting");
crate::maintain::update_ocsp_staples(&cache_ref).await;
}
}
})
.await;
if outcome.is_err() {
return LoopExit::Panicked;
}
}
}
enum Tick {
Renew,
Ocsp,
}
pub(crate) async fn renew_managed_certificates(cache: &Cache) {
let certs = cache.all_certs();
for cert in certs {
if !cert.managed() {
continue;
}
let Ok(cfg) = cache.config_for(&cert).await else {
continue;
};
if !cfg.cert_needs_renewal(&cert) {
continue;
}
let Some(name) = cert.names.first().cloned() else {
continue;
};
let ct = tokio_util::sync::CancellationToken::new();
let name2 = name.clone();
let submitted =
crate::runtime::global_job_manager().submit(&format!("renew_{name}"), move || {
let cfg = cfg.clone();
let name = name2.clone();
let ct = ct.clone();
Box::pin(async move { cfg.renew_cert(&ct, &name, false, false).await })
});
if let Ok(false) = submitted {
tracing::trace!(domain = %name, "renewal already queued");
}
}
}
#[cfg_attr(not(feature = "ocsp"), allow(unused_variables))]
pub(crate) async fn update_ocsp_staples(cache: &Cache) {
#[cfg(feature = "ocsp")]
{
use crate::events::{CertOcspRevokedData, EventKind};
let certs = cache.all_certs();
let mut updates: Vec<(
crate::certificate::Certificate,
crate::certificate::Certificate,
)> = Vec::new();
let mut revoked: Vec<(Arc<Config>, Vec<String>, bool)> = Vec::new();
for cert in &certs {
let Ok(cfg) = cache.config_for(cert).await else {
continue;
};
if cfg.options.ocsp.disable_stapling {
continue;
}
let stale = cert.ocsp.as_ref().is_none_or(|r| {
!crate::ocsp::is_fresh(
r.this_update,
r.next_update.unwrap_or(r.this_update),
time::OffsetDateTime::now_utc(),
r.responder_not_after,
)
});
if !stale {
continue;
}
let mut fresh = cert.clone();
let storage = cfg.ground_truth_storage();
let ct = tokio_util::sync::CancellationToken::new();
let transport = match crate::acme::transport::ReqwestTransport::new(
std::time::Duration::from_secs(30),
concat!("certmagic-rs/", env!("CARGO_PKG_VERSION")),
) {
Ok(t) => {
let t: Arc<dyn crate::acme::transport::Transport> = Arc::new(t);
t
}
Err(err) => {
tracing::warn!(error = %err, "transport build failed");
continue;
}
};
match crate::ocsp::staple_ocsp(
&ct,
&storage,
&cfg.options.ocsp,
&mut fresh,
transport.as_ref(),
)
.await
{
Ok(()) => {
if let Some(resp) = &fresh.ocsp
&& resp.status == crate::ocsp::OcspCertStatus::Revoked
{
revoked.push((
Arc::clone(&cfg),
fresh.names.clone(),
cfg.options.ocsp.replace_revoked,
));
}
updates.push((cert.clone(), fresh));
}
Err(err) => {
tracing::warn!(names = ?cert.names, error = %err, "OCSP refresh failed");
}
}
}
for (old, new) in updates {
cache.update_metadata(old.hash(), |cached| {
cached.ocsp = new.ocsp;
cached.ocsp_staple = new.ocsp_staple;
});
}
for (cfg, names, replace_revoked) in revoked {
let ct = tokio_util::sync::CancellationToken::new();
let _ = crate::events::emit(
cfg.options.on_event.as_ref(),
cfg.options.should_emit.as_ref(),
&ct,
EventKind::CertOcspRevoked(CertOcspRevokedData {
subjects: names.clone(),
certificate_hash: String::new(),
reason: None,
revoked_at: None,
}),
)
.await;
if !replace_revoked {
continue;
}
for name in names {
let cfg = Arc::clone(&cfg);
let ct = CancellationToken::new();
let job_name = format!("force_renew_{name}");
let name_for_job = name.clone();
let _ = crate::runtime::global_job_manager().submit(&job_name, move || {
let cfg = Arc::clone(&cfg);
let ct = ct.clone();
Box::pin(
async move { cfg.renew_cert_compromised(&ct, &name_for_job, false).await },
)
});
}
}
}
}