pub mod distributed;
pub mod dns;
pub mod http;
pub mod tls_alpn;
use std::sync::Arc;
use async_trait::async_trait;
use tokio_util::sync::CancellationToken;
use crate::error::Result;
#[derive(Debug, Clone)]
pub struct SolvableChallenge {
pub kind: String,
pub token: String,
pub url: String,
pub identifier: String,
pub key_authorization: String,
}
impl SolvableChallenge {
pub fn new(
kind: &str,
token: &str,
url: &str,
identifier: &str,
account: &crate::acme::protocol::AccountKey,
) -> Result<Self> {
Ok(Self {
kind: kind.to_owned(),
token: token.to_owned(),
url: url.to_owned(),
identifier: identifier.to_owned(),
key_authorization: crate::acme::key_authorization(token, &account.thumbprint_b64()?)?,
})
}
}
#[async_trait]
pub trait Solver: Send + Sync + std::fmt::Debug {
async fn present(&self, ct: &CancellationToken, chal: &SolvableChallenge) -> Result<()>;
async fn wait(&self, _ct: &CancellationToken, _chal: &SolvableChallenge) -> Result<()> {
Ok(())
}
async fn cleanup(&self, chal: &SolvableChallenge);
}
#[derive(Debug, Default)]
pub struct ActiveChallenges {
map: std::sync::Mutex<std::collections::HashMap<String, SolvableChallenge>>,
}
impl ActiveChallenges {
fn global() -> &'static ActiveChallenges {
static REG: std::sync::OnceLock<ActiveChallenges> = std::sync::OnceLock::new();
REG.get_or_init(ActiveChallenges::default)
}
pub(crate) fn insert(key: &str, chal: &SolvableChallenge) {
if let Ok(mut map) = ActiveChallenges::global().map.lock() {
map.insert(key.to_owned(), chal.clone());
}
}
pub(crate) fn remove(key: &str) {
if let Ok(mut map) = ActiveChallenges::global().map.lock() {
map.remove(key);
}
}
#[must_use]
pub fn get(kind: &str, identifier: &str) -> Option<SolvableChallenge> {
let needle = identifier.to_lowercase();
let map = ActiveChallenges::global().map.lock().ok()?;
map.values()
.find(|c| c.kind == kind && c.identifier.to_lowercase() == needle)
.cloned()
}
}
pub(crate) fn challenge_key(chal: &SolvableChallenge) -> String {
crate::crypto::sha256_hex(chal.url.as_bytes())
}
#[derive(Debug)]
pub struct SolverWrapper<S: Solver> {
inner: S,
}
impl<S: Solver> SolverWrapper<S> {
#[must_use]
pub fn new(inner: S) -> Self {
Self { inner }
}
}
#[async_trait]
impl<S: Solver> Solver for SolverWrapper<S> {
async fn present(&self, ct: &CancellationToken, chal: &SolvableChallenge) -> Result<()> {
self.inner.present(ct, chal).await?;
ActiveChallenges::insert(&challenge_key(chal), chal);
Ok(())
}
async fn wait(&self, ct: &CancellationToken, chal: &SolvableChallenge) -> Result<()> {
self.inner.wait(ct, chal).await
}
async fn cleanup(&self, chal: &SolvableChallenge) {
ActiveChallenges::remove(&challenge_key(chal));
self.inner.cleanup(chal).await;
}
}
#[must_use]
pub fn get_acme_challenge(kind: &str, identifier: &str) -> Option<SolvableChallenge> {
ActiveChallenges::get(kind, identifier)
}
#[must_use]
pub fn wrapped<S: Solver>(inner: S) -> Arc<SolverWrapper<S>> {
Arc::new(SolverWrapper::new(inner))
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Default)]
struct RecordingSolver {
present_calls: std::sync::atomic::AtomicUsize,
cleanup_calls: std::sync::atomic::AtomicUsize,
}
#[async_trait]
impl Solver for RecordingSolver {
async fn present(&self, _ct: &CancellationToken, _chal: &SolvableChallenge) -> Result<()> {
self.present_calls
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(())
}
async fn cleanup(&self, _chal: &SolvableChallenge) {
self.cleanup_calls
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
}
fn sample_challenge() -> SolvableChallenge {
SolvableChallenge {
kind: "dns-01".into(),
token: "tok".into(),
url: "https://ca/chal/xyz".into(),
identifier: "example.com".into(),
key_authorization: "tok.thumb".into(),
}
}
#[tokio::test]
async fn wrapper_registers_and_unregisters() {
let solver = wrapped(RecordingSolver::default());
let chal = sample_challenge();
assert!(get_acme_challenge("dns-01", "example.com").is_none());
solver
.present(&CancellationToken::new(), &chal)
.await
.unwrap();
assert!(get_acme_challenge("dns-01", "example.com").is_some());
solver.cleanup(&chal).await;
assert!(get_acme_challenge("dns-01", "example.com").is_none());
}
#[tokio::test]
async fn wait_defaults_to_ok() {
let solver = wrapped(RecordingSolver::default());
solver
.wait(&CancellationToken::new(), &sample_challenge())
.await
.unwrap();
}
}