use std::sync::Arc;
use crate::acme::challenge::Http01Tokens;
use crate::acme::store::{AcmeStore, CertId, StoredCert};
use crate::config::AcmeConfig;
use crate::custom_domain::{
CustomDomainCertCache, CustomDomainRegistry, DomainIssuer, DomainVerifier, ExpectedIngress,
IssuanceLimiter, IssuedCertificate, apply_verification, grade_dns_verification,
grade_ownership,
};
use crate::scheduler::SchedulerCoordinator;
use crate::task::TaskCoordination;
use rustls::crypto::CryptoProvider;
pub const CUSTOM_DOMAIN_TASK: &str = "custom_domain_certificates";
const VERIFY_CONCURRENCY: usize = 16;
pub type ReporterFn = Arc<dyn Fn(String) + Send + Sync>;
pub type RecoveryFn = Arc<dyn Fn() + Send + Sync>;
#[must_use]
pub fn cert_id_for(hostname: &str) -> CertId {
CertId::from_domains(&[hostname.to_owned()])
}
fn elapsed_since(base_unix: i64, started: tokio::time::Instant) -> i64 {
base_unix.saturating_add(i64::try_from(started.elapsed().as_secs()).unwrap_or(i64::MAX))
}
pub struct AcmeDomainIssuer {
config: AcmeConfig,
store: Arc<dyn AcmeStore>,
tokens: Http01Tokens,
}
impl std::fmt::Debug for AcmeDomainIssuer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AcmeDomainIssuer").finish_non_exhaustive()
}
}
impl AcmeDomainIssuer {
#[must_use]
pub const fn new(config: AcmeConfig, store: Arc<dyn AcmeStore>, tokens: Http01Tokens) -> Self {
Self {
config,
store,
tokens,
}
}
async fn order(&self, hostname: &str) -> Result<IssuedCertificate, String> {
use instant_acme::{Identifier, NewOrder};
let account =
crate::acme::renewal::load_or_register_account(self.store.as_ref(), &self.config)
.await?;
let identifiers = [Identifier::Dns(hostname.to_owned())];
let mut order = account
.new_order(&NewOrder::new(&identifiers))
.await
.map_err(|e| format!("failed to create ACME order for {hostname}: {e}"))?;
let published = crate::acme::renewal::answer_http01(&self.tokens, &mut order).await?;
let ready = crate::acme::renewal::await_order_ready(&mut order, None).await;
drop(published);
ready?;
let (csr_der, key_pem) = crate::acme::renewal::generate_csr(&[hostname.to_owned()])?;
order
.finalize_csr(&csr_der)
.await
.map_err(|e| format!("failed to finalize order for {hostname}: {e}"))?;
let chain_pem = order
.poll_certificate(&instant_acme::RetryPolicy::default())
.await
.map_err(|e| format!("failed to download certificate for {hostname}: {e}"))?;
Ok(IssuedCertificate { chain_pem, key_pem })
}
}
impl DomainIssuer for AcmeDomainIssuer {
fn issue<'a>(
&'a self,
hostname: &'a str,
) -> futures::future::BoxFuture<'a, Result<IssuedCertificate, String>> {
Box::pin(self.order(hostname))
}
}
pub struct CustomDomainTask {
pub registry: Arc<CustomDomainRegistry>,
pub cache: Arc<CustomDomainCertCache>,
pub certs: Arc<dyn AcmeStore>,
pub provider: Arc<CryptoProvider>,
pub verifier: Arc<dyn DomainVerifier>,
pub issuer: Arc<dyn DomainIssuer>,
pub limiter: Arc<IssuanceLimiter>,
pub ingress: ExpectedIngress,
pub renew_before_days: u32,
pub reporter: ReporterFn,
pub recovery: Option<RecoveryFn>,
pub coordinator: Arc<dyn SchedulerCoordinator>,
pub leadership_degraded: bool,
pub cert_store_paths: Option<Arc<crate::acme::store::FsAcmeStore>>,
pub retained_cert_ids: std::collections::HashSet<String>,
}
impl std::fmt::Debug for CustomDomainTask {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CustomDomainTask")
.field("domains", &self.registry.len())
.field("renew_before_days", &self.renew_before_days)
.finish_non_exhaustive()
}
}
pub struct PruneOnlyCustomDomainPruner {
pub registry: Arc<CustomDomainRegistry>,
pub cache: Arc<CustomDomainCertCache>,
pub certs: Arc<dyn AcmeStore>,
pub limiter: Arc<IssuanceLimiter>,
pub cert_store_paths: Option<Arc<crate::acme::store::FsAcmeStore>>,
pub retained_cert_ids: std::collections::HashSet<String>,
pub reporter: ReporterFn,
pub recovery: Option<RecoveryFn>,
}
impl PruneOnlyCustomDomainPruner {
pub async fn offboard(&self, hostname: &str) -> std::io::Result<bool> {
let Ok(host) = crate::custom_domain::normalize_hostname(hostname) else {
return Ok(false);
};
self.offboard_guarded(&host, |_| true).await
}
async fn offboard_guarded(
&self,
host: &str,
guard: impl FnOnce(&crate::custom_domain::CustomDomain) -> bool,
) -> std::io::Result<bool> {
let removed = self.registry.remove_if(host, guard).await?;
let purged = if self.registry.get(host).is_none() {
self.purge_local(host).await
} else {
true
};
if removed && purged {
self.clear_alert_if_healthy(crate::custom_domain::now_unix());
}
Ok(removed)
}
async fn offboard_if_unchanged(
&self,
expected: &crate::custom_domain::CustomDomain,
) -> std::io::Result<bool> {
let Ok(host) = crate::custom_domain::normalize_hostname(&expected.hostname) else {
return Ok(false);
};
let removed = self
.offboard_guarded(&host, |current| {
current.tenant == expected.tenant
&& current.status == expected.status
&& current.registered_at_unix == expected.registered_at_unix
})
.await?;
if !removed {
tracing::debug!(
hostname = %host,
"skipping a retention offboard: the domain changed while the sweep was running"
);
}
Ok(removed)
}
async fn purge_local(&self, host: &str) -> bool {
self.cache.remove(host);
self.limiter.forget(host);
if let Err(e) = self.certs.delete_cert(&cert_id_for(host)).await {
let message = format!(
"offboarded custom domain {host} but could not delete its certificate: {e}. The \
certificate and private key are still on disk"
);
tracing::warn!("{message}");
(self.reporter)(message);
return false;
}
true
}
fn clear_alert_if_healthy(&self, now_unix: i64) {
if let Some(recovery) = &self.recovery
&& self.registry.health_report(now_unix).is_empty()
{
recovery();
}
}
pub async fn offboard_tenant(&self, tenant: &str) -> std::io::Result<usize> {
let hostnames: Vec<String> = self
.registry
.list_for_tenant(tenant)
.into_iter()
.map(|d| d.hostname)
.collect();
let mut removed = 0;
for hostname in hostnames {
let Ok(host) = crate::custom_domain::normalize_hostname(&hostname) else {
continue;
};
if self
.offboard_guarded(&host, |current| current.tenant == tenant)
.await?
{
removed += 1;
}
}
Ok(removed)
}
async fn prune_orphan_certs(&self, dry_run: bool) -> Result<u64, String> {
let Some(fs) = self.cert_store_paths.as_ref() else {
return Ok(0);
};
let stored = fs
.list_certs()
.map_err(|e| format!("failed to enumerate stored certificates: {e}"))?;
let fresh = self
.registry
.stored_records()
.await
.map_err(|e| format!("failed to re-read the custom-domain registry: {e}"))?;
let live: std::collections::HashSet<String> = self
.registry
.list()
.into_iter()
.chain(fresh)
.map(|d| cert_id_for(&d.hostname).as_str().to_owned())
.collect();
let mut removed = 0;
for (id, chain, key) in stored {
if live.contains(id.as_str()) || self.retained_cert_ids.contains(id.as_str()) {
continue;
}
if dry_run {
removed += 1;
continue;
}
let mut deleted = true;
for path in [&key, &chain] {
if let Err(e) = std::fs::remove_file(path) {
tracing::warn!(path = %path.display(), "failed to remove an orphaned certificate: {e}");
deleted = false;
}
}
if deleted {
removed += 1;
}
}
Ok(removed)
}
}
impl crate::custom_domain::CustomDomainPruner for PruneOnlyCustomDomainPruner {
fn prune(
&self,
cutoff_unix: i64,
dry_run: bool,
) -> futures::future::BoxFuture<'_, Result<u64, String>> {
Box::pin(async move {
if !self.registry.is_hydrated() {
return Err(
"refusing to prune: the custom-domain registry did not load at boot, so \
every certificate would look orphaned"
.to_owned(),
);
}
let mut removed = 0_u64;
let fresh: std::collections::HashMap<String, crate::custom_domain::CustomDomain> = self
.registry
.stored_records()
.await
.map_err(|e| format!("failed to re-read the custom-domain registry: {e}"))?
.into_iter()
.map(|d| (d.hostname.clone(), d))
.collect();
for domain in self.registry.list() {
let still_abandoned_in_store = fresh.get(&domain.hostname).is_some_and(|stored| {
stored.status == domain.status
&& stored.tenant == domain.tenant
&& stored.registered_at_unix == domain.registered_at_unix
});
if domain.status == crate::custom_domain::DomainStatus::PendingDns
&& domain.registered_at_unix < cutoff_unix
&& still_abandoned_in_store
{
if dry_run {
removed += 1;
continue;
}
if self
.offboard_if_unchanged(&domain)
.await
.map_err(|e| format!("failed to offboard {}: {e}", domain.hostname))?
{
removed += 1;
}
}
}
removed += self.prune_orphan_certs(dry_run).await?;
Ok(removed)
})
}
fn offboard_domain<'a>(
&'a self,
hostname: &'a str,
) -> futures::future::BoxFuture<'a, std::io::Result<bool>> {
Box::pin(self.offboard(hostname))
}
fn offboard_tenant_domains<'a>(
&'a self,
tenant: &'a str,
) -> futures::future::BoxFuture<'a, std::io::Result<usize>> {
Box::pin(self.offboard_tenant(tenant))
}
}
impl CustomDomainTask {
pub async fn run(
&self,
interval: std::time::Duration,
shutdown: tokio_util::sync::CancellationToken,
) {
self.warm_all().await;
self.hydrate_limiter(crate::custom_domain::now_unix());
loop {
tokio::select! {
() = self.tick(crate::custom_domain::now_unix()) => {}
() = shutdown.cancelled() => break,
}
tokio::select! {
() = tokio::time::sleep(interval) => {}
() = shutdown.cancelled() => break,
}
}
}
pub async fn tick(&self, now_unix: i64) {
let started = tokio::time::Instant::now();
let at = move || elapsed_since(now_unix, started);
futures::StreamExt::for_each_concurrent(
futures::stream::iter(self.registry.pending_verification(now_unix)),
VERIFY_CONCURRENCY,
|domain| async move { self.verify_one(&domain.hostname, now_unix).await },
)
.await;
for domain in self.registry.due_for_issuance(now_unix) {
self.issue_one(
&domain.hostname,
&domain.tenant,
domain.verification_token.as_deref(),
at(),
)
.await;
}
for domain in self
.registry
.due_for_renewal(now_unix, self.renew_before_days)
{
if !self.still_points_here(&domain, now_unix).await {
continue;
}
self.issue_one(
&domain.hostname,
&domain.tenant,
domain.verification_token.as_deref(),
at(),
)
.await;
}
for domain in self.registry.list() {
if domain.is_servable()
&& domain.is_due(now_unix)
&& !self.certificate_present(&domain.hostname)
{
tracing::warn!(
hostname = %domain.hostname,
"custom domain is active but its certificate is missing; re-ordering"
);
self.issue_one(
&domain.hostname,
&domain.tenant,
domain.verification_token.as_deref(),
at(),
)
.await;
}
}
}
fn certificate_present(&self, hostname: &str) -> bool {
let Some(fs) = self.cert_store_paths.as_ref() else {
return true;
};
let Some((chain_path, key_path)) = fs.find_cert_for_domains(&[hostname.to_owned()]) else {
return false;
};
if self.cache.get(hostname).is_some() {
return true;
}
let (Ok(chain), Ok(key)) = (std::fs::read(&chain_path), std::fs::read(&key_path)) else {
return false;
};
match crate::tls::certified_key_from_pem(&chain, &key, &self.provider) {
Ok(_) => true,
Err(e) => {
tracing::warn!(
hostname,
"stored custom-domain certificate is present but unusable ({e}); \
treating it as missing so the repair pass re-orders it"
);
false
}
}
}
async fn still_points_here(
&self,
domain: &crate::custom_domain::CustomDomain,
now_unix: i64,
) -> bool {
let hostname = domain.hostname.as_str();
let observed = self.verifier.observe(hostname).await;
if matches!(observed, crate::custom_domain::ObservedTarget::None) {
return true;
}
let outcome = grade_dns_verification(&observed, &self.effective_ingress().await);
if outcome.is_verified() {
return true;
}
tracing::warn!(
hostname,
"skipping renewal: the domain no longer points at this deployment"
);
let reason = outcome
.reason()
.unwrap_or_else(|| "the domain no longer points at this deployment".to_owned());
self.record_failure(
hostname,
&domain.tenant,
domain.verification_token.as_deref(),
now_unix,
format!(
"renewal skipped: {reason}. The certificate will expire unless the record is \
restored or the domain is offboarded"
),
true,
)
.await;
false
}
async fn verify_one(&self, hostname: &str, now_unix: i64) {
let Some(mut domain) = self.registry.get(hostname) else {
return;
};
if domain.needs_verification_token() {
if let Err(e) = self
.registry
.adopt_verification_token(hostname, now_unix)
.await
{
tracing::warn!(
hostname,
"could not give a custom domain its verification token: {e}"
);
return;
}
match self.registry.get(hostname) {
Some(adopted) if adopted.verification_token.is_some() => domain = adopted,
_ => return,
}
}
let observed = self.verifier.observe(hostname).await;
let mut outcome = grade_dns_verification(&observed, &self.effective_ingress().await);
if outcome.is_verified() {
let txt = self
.verifier
.observe_txt(&domain.verification_record_name())
.await;
outcome = grade_ownership(&domain, &txt);
}
let backoff = i64::try_from(
self.limiter
.backoff_for(domain.consecutive_failures.saturating_add(1)),
)
.unwrap_or(i64::MAX);
if let Err(e) =
apply_verification(&self.registry, &domain, &outcome, now_unix, backoff).await
{
tracing::warn!(
hostname,
"failed to persist custom-domain verification: {e}"
);
}
}
async fn effective_ingress(&self) -> ExpectedIngress {
let mut ingress = self.ingress.clone();
let Some(host) = ingress.hostname.clone() else {
return ingress;
};
if let crate::custom_domain::ObservedTarget::Addresses(addrs) =
self.verifier.observe(&host).await
{
for addr in addrs {
match addr {
std::net::IpAddr::V4(v4) if !ingress.ipv4.contains(&v4) => {
ingress.ipv4.push(v4);
}
std::net::IpAddr::V6(v6) if !ingress.ipv6.contains(&v6) => {
ingress.ipv6.push(v6);
}
_ => {}
}
}
}
ingress
}
async fn issue_one(&self, hostname: &str, tenant: &str, token: Option<&str>, now_unix: i64) {
if self.leadership_degraded {
self.record_failure(
hostname,
tenant,
token,
now_unix,
"refusing to order: a distributed scheduler backend is configured but its \
coordinator is unavailable in this process, so a lease would not exclude the \
other replicas",
true,
)
.await;
return;
}
if !self.within_budget(hostname, tenant, token, now_unix).await {
return;
}
let tick_key = format!("custom-domain:{hostname}");
let lease = match self
.coordinator
.try_acquire(CUSTOM_DOMAIN_TASK, &tick_key, TaskCoordination::Fleet)
.await
{
Ok(Some(lease)) => lease,
Ok(None) => {
self.warm(hostname).await;
return;
}
Err(e) => {
self.record_failure(
hostname,
tenant,
token,
now_unix,
format!("leader election failed: {e}"),
true,
)
.await;
return;
}
};
match self.registry.record_issuing_for(hostname, tenant).await {
Ok(true) => {}
Ok(false) => {
tracing::debug!(
hostname,
tenant,
"abandoning a custom-domain order: the hostname is no longer this tenant's to \
order for"
);
if let Err(e) = lease.release().await {
tracing::warn!(hostname, error = %e, "failed to release the custom-domain lease");
}
return;
}
Err(e) => {
self.record_failure(
hostname,
tenant,
token,
now_unix,
format!("refusing to order: the issuing state could not be persisted ({e})"),
true,
)
.await;
if let Err(e) = lease.release().await {
tracing::warn!(hostname, error = %e, "failed to release the custom-domain lease");
}
return;
}
}
self.limiter.record_attempt(hostname, now_unix);
self.persist_attempt(hostname, tenant, token, now_unix)
.await;
let order_started = tokio::time::Instant::now();
let outcome = self.issuer.issue(hostname).await;
let now_unix = elapsed_since(now_unix, order_started);
if let Err(e) = lease.release().await {
tracing::warn!(hostname, error = %e, "failed to release the custom-domain lease");
}
let issued = match outcome {
Ok(issued) => issued,
Err(e) => {
self.record_failure(hostname, tenant, token, now_unix, e, true)
.await;
return;
}
};
if let Err(e) = self.install(hostname, tenant, &issued, now_unix).await {
self.record_failure(hostname, tenant, token, now_unix, e, true)
.await;
}
}
async fn install(
&self,
hostname: &str,
tenant: &str,
issued: &IssuedCertificate,
now_unix: i64,
) -> Result<(), String> {
match self.registry.get(hostname) {
Some(record) if record.tenant == tenant => {}
Some(record) => {
return Err(format!(
"discarding the certificate: {hostname} now belongs to tenant {}",
record.tenant
));
}
None => {
return Err(format!(
"discarding the certificate: {hostname} was offboarded while the order ran"
));
}
}
let stored = StoredCert {
chain_pem: issued.chain_pem.clone(),
key_pem: issued.key_pem.clone(),
};
let not_after = crate::tls::leaf_not_after_from_pem(stored.chain_pem.as_bytes())?;
let certified = crate::tls::certified_key_from_pem(
stored.chain_pem.as_bytes(),
stored.key_pem.as_bytes(),
&self.provider,
)?;
self.certs
.save_cert(&cert_id_for(hostname), &stored)
.await
.map_err(|e| format!("failed to persist the certificate for {hostname}: {e}"))?;
let activated = self
.registry
.record_active_for(hostname, tenant, now_unix, not_after)
.await
.map_err(|e| format!("failed to persist the active state for {hostname}: {e}"))?;
if !activated {
self.cache.remove(hostname);
match self.certs.load_cert(&cert_id_for(hostname)).await {
Ok(Some(current)) if current.chain_pem == stored.chain_pem => {
if let Err(e) = self.certs.delete_cert(&cert_id_for(hostname)).await {
tracing::warn!(hostname, "failed to delete a superseded certificate: {e}");
}
}
Ok(Some(_)) => tracing::debug!(
hostname,
"leaving the stored certificate alone: it is the successor's, not this order's"
),
Ok(None) => {}
Err(e) => tracing::warn!(
hostname,
"cannot tell whether the stored certificate is this order's, so leaving it: {e}"
),
}
return Err(format!(
"discarding the certificate: {hostname} changed hands while it was being issued"
));
}
self.cache.insert(hostname, certified);
tracing::info!(hostname, not_after, "custom domain is active");
if crate::custom_domain::needs_renewal(not_after, self.renew_before_days, now_unix) {
let backoff = i64::try_from(self.limiter.max_backoff()).unwrap_or(i64::MAX);
let _ = self
.registry
.record_failure(
hostname,
now_unix,
format!(
"the issued certificate is already inside its renew-before window ({} \
days); lower [server.tls.acme] renew_before_days below the certificate \
lifetime",
self.renew_before_days
),
backoff,
)
.await;
return Ok(());
}
self.clear_alert_if_healthy(now_unix);
Ok(())
}
pub fn hydrate_limiter(&self, now_unix: i64) {
self.limiter.hydrate(
self.registry.list().into_iter().flat_map(|d| {
let hostname = d.hostname;
d.issuance_attempts_unix
.into_iter()
.map(move |at| (hostname.clone(), at))
}),
now_unix,
);
}
async fn within_budget(
&self,
hostname: &str,
tenant: &str,
token: Option<&str>,
now_unix: i64,
) -> bool {
let decision = self.limiter.check(hostname, now_unix);
if decision.is_allowed() {
return true;
}
if let (Some(reason), Some(wait)) = (decision.reason(), decision.retry_after_secs()) {
self.defer(
hostname,
tenant,
token,
now_unix.saturating_add(wait),
reason,
)
.await;
}
false
}
async fn persist_attempt(
&self,
hostname: &str,
tenant: &str,
token: Option<&str>,
now_unix: i64,
) {
if let Err(e) = self
.registry
.record_attempt_for_registration(hostname, tenant, token, now_unix)
.await
{
tracing::warn!(
hostname,
error = %e,
"could not persist a custom-domain issuance attempt; the budget will not survive \
a restart"
);
}
}
async fn defer(
&self,
hostname: &str,
tenant: &str,
token: Option<&str>,
retry_at_unix: i64,
reason: String,
) {
match self
.registry
.defer_for_registration(hostname, tenant, token, retry_at_unix, reason)
.await
{
Ok(true) => {}
Ok(false) => tracing::debug!(
hostname,
tenant,
"discarded a custom-domain deferral: the hostname changed hands or was \
re-registered meanwhile"
),
Err(e) => tracing::warn!(
hostname,
error = %e,
"could not record a custom-domain issuance deferral"
),
}
}
async fn record_failure(
&self,
hostname: &str,
tenant: &str,
token: Option<&str>,
now_unix: i64,
reason: impl Into<String>,
alert: bool,
) {
let reason = reason.into();
let failures = self
.registry
.get(hostname)
.filter(|d| d.tenant == tenant && d.verification_token.as_deref() == token)
.map_or(0, |d| d.consecutive_failures)
.saturating_add(1);
let backoff = i64::try_from(self.limiter.backoff_for(failures)).unwrap_or(i64::MAX);
match self
.registry
.record_failure_for_registration(
hostname,
tenant,
token,
now_unix,
reason.clone(),
backoff,
)
.await
{
Ok(true) => {}
Ok(false) => {
tracing::debug!(
hostname,
tenant,
"discarded a custom-domain failure: the hostname changed hands or was \
re-registered while the order was in flight"
);
return;
}
Err(e) => {
tracing::warn!(hostname, "failed to persist custom-domain failure: {e}");
}
}
let message =
format!("custom domain {hostname} (tenant {tenant}) failed to issue: {reason}");
tracing::warn!("{message}");
if alert {
(self.reporter)(message);
}
}
pub(crate) fn pruner(&self) -> PruneOnlyCustomDomainPruner {
PruneOnlyCustomDomainPruner {
registry: Arc::clone(&self.registry),
cache: Arc::clone(&self.cache),
certs: Arc::clone(&self.certs),
limiter: Arc::clone(&self.limiter),
cert_store_paths: self.cert_store_paths.clone(),
retained_cert_ids: self.retained_cert_ids.clone(),
reporter: Arc::clone(&self.reporter),
recovery: self.recovery.clone(),
}
}
fn clear_alert_if_healthy(&self, now_unix: i64) {
self.pruner().clear_alert_if_healthy(now_unix);
}
pub async fn offboard(&self, hostname: &str) -> std::io::Result<bool> {
self.pruner().offboard(hostname).await
}
pub async fn offboard_tenant(&self, tenant: &str) -> std::io::Result<usize> {
self.pruner().offboard_tenant(tenant).await
}
pub async fn warm(&self, hostname: &str) -> bool {
let Ok(host) = crate::custom_domain::normalize_hostname(hostname) else {
return false;
};
let hostname = host.as_str();
if !self.registry.is_servable(hostname) {
return false;
}
match self.certs.load_cert(&cert_id_for(hostname)).await {
Ok(Some(stored)) => match crate::tls::certified_key_from_pem(
stored.chain_pem.as_bytes(),
stored.key_pem.as_bytes(),
&self.provider,
) {
Ok(certified) => {
self.cache.insert(hostname, certified);
true
}
Err(e) => {
tracing::warn!(
hostname,
"stored custom-domain certificate is unusable: {e}"
);
false
}
},
Ok(None) => false,
Err(e) => {
tracing::warn!(hostname, "failed to read a custom-domain certificate: {e}");
false
}
}
}
async fn warm_all(&self) {
let mut active: Vec<_> = self
.registry
.list()
.into_iter()
.filter(crate::custom_domain::CustomDomain::is_servable)
.collect();
active.sort_by_key(|d| std::cmp::Reverse(d.activated_at_unix.unwrap_or(0)));
for domain in active.into_iter().take(self.cache.capacity()) {
self.warm(&domain.hostname).await;
}
}
}
impl crate::custom_domain::CustomDomainPruner for CustomDomainTask {
fn prune(
&self,
cutoff_unix: i64,
dry_run: bool,
) -> futures::future::BoxFuture<'_, Result<u64, String>> {
let pruner = self.pruner();
Box::pin(async move {
<PruneOnlyCustomDomainPruner as crate::custom_domain::CustomDomainPruner>::prune(
&pruner,
cutoff_unix,
dry_run,
)
.await
})
}
fn offboard_domain<'a>(
&'a self,
hostname: &'a str,
) -> futures::future::BoxFuture<'a, std::io::Result<bool>> {
let pruner = self.pruner();
Box::pin(async move {
<PruneOnlyCustomDomainPruner as crate::custom_domain::CustomDomainPruner>::offboard_domain(
&pruner, hostname,
)
.await
})
}
fn offboard_tenant_domains<'a>(
&'a self,
tenant: &'a str,
) -> futures::future::BoxFuture<'a, std::io::Result<usize>> {
let pruner = self.pruner();
Box::pin(async move {
<PruneOnlyCustomDomainPruner as crate::custom_domain::CustomDomainPruner>::offboard_tenant_domains(
&pruner, tenant,
)
.await
})
}
}
#[derive(Debug)]
pub struct FsSniCertSource {
store: Arc<crate::acme::store::FsAcmeStore>,
provider: Arc<CryptoProvider>,
}
impl FsSniCertSource {
#[must_use]
pub const fn new(
store: Arc<crate::acme::store::FsAcmeStore>,
provider: Arc<CryptoProvider>,
) -> Self {
Self { store, provider }
}
}
impl crate::custom_domain::SniCertSource for FsSniCertSource {
fn load(&self, hostname: &str) -> Option<Arc<rustls::sign::CertifiedKey>> {
let (chain_path, key_path) = self.store.find_cert_for_domains(&[hostname.to_owned()])?;
let chain = std::fs::read(&chain_path).ok()?;
let key = std::fs::read(&key_path).ok()?;
match crate::tls::certified_key_from_pem(&chain, &key, &self.provider) {
Ok(certified) => Some(certified),
Err(e) => {
tracing::warn!(
hostname,
"stored custom-domain certificate is unusable: {e}"
);
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_hostname_gets_its_own_cert_id() {
assert_ne!(cert_id_for("a.test"), cert_id_for("b.test"));
assert_eq!(cert_id_for("a.test"), cert_id_for("a.test"));
}
}