use std::sync::Arc;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;
use crate::error::{Error, Result};
use crate::solvers::Solver;
use crate::storage::STORAGE_KEYS;
#[must_use]
pub fn challenge_tokens_prefix(issuer_key: &str) -> String {
format!("{}/challenge_tokens", STORAGE_KEYS.certs_prefix(issuer_key))
}
#[must_use]
pub fn challenge_tokens_key(issuer_key: &str, identifier: &str) -> String {
format!(
"{}/{}.json",
challenge_tokens_prefix(issuer_key),
STORAGE_KEYS.safe(identifier)
)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublishedChallenge {
pub kind: String,
pub token: String,
pub url: String,
pub identifier: String,
pub key_authorization: String,
}
impl From<&crate::solvers::SolvableChallenge> for PublishedChallenge {
fn from(c: &crate::solvers::SolvableChallenge) -> Self {
Self {
kind: c.kind.clone(),
token: c.token.clone(),
url: c.url.clone(),
identifier: c.identifier.clone(),
key_authorization: c.key_authorization.clone(),
}
}
}
impl From<PublishedChallenge> for crate::solvers::SolvableChallenge {
fn from(p: PublishedChallenge) -> Self {
crate::solvers::SolvableChallenge {
kind: p.kind,
token: p.token,
url: p.url,
identifier: p.identifier,
key_authorization: p.key_authorization,
}
}
}
pub async fn load_published(
storage: &dyn crate::storage::Storage,
issuer_keys: &[String],
identifier: &str,
) -> Result<Option<crate::solvers::SolvableChallenge>> {
for issuer_key in issuer_keys {
let key = challenge_tokens_key(issuer_key, identifier);
match storage.load(&key).await {
Ok(data) => {
if data.is_empty() {
continue;
}
let published: PublishedChallenge = serde_json::from_slice(&data).map_err(|e| {
Error::Storage(crate::error::StorageError::Other(format!(
"challenge token decode: {e}"
)))
})?;
return Ok(Some(published.into()));
}
Err(Error::Storage(crate::error::StorageError::NotFound(_))) => continue,
Err(err) => return Err(err),
}
}
Ok(None)
}
#[derive(Debug)]
pub struct DistributedSolver {
pub storage: Arc<dyn crate::storage::Storage>,
pub issuer_key: String,
pub inner: Arc<dyn Solver>,
}
#[async_trait]
impl Solver for DistributedSolver {
async fn present(
&self,
ct: &CancellationToken,
chal: &crate::solvers::SolvableChallenge,
) -> Result<()> {
crate::solvers::http::register_distributed_storage(&self.storage, &self.issuer_key);
let key = challenge_tokens_key(&self.issuer_key, &chal.identifier);
let data = serde_json::to_vec(&PublishedChallenge::from(chal)).map_err(|e| {
Error::Storage(crate::error::StorageError::Other(format!("encode: {e}")))
})?;
self.storage.store(&key, &data).await?;
if let Err(err) = self.inner.present(ct, chal).await {
let _ = self.storage.delete(&key).await;
return Err(err);
}
Ok(())
}
async fn wait(
&self,
ct: &CancellationToken,
chal: &crate::solvers::SolvableChallenge,
) -> Result<()> {
self.inner.wait(ct, chal).await
}
async fn cleanup(&self, chal: &crate::solvers::SolvableChallenge) {
let key = challenge_tokens_key(&self.issuer_key, &chal.identifier);
if let Err(err) = self.storage.delete(&key).await {
tracing::warn!(key = %key, error = %err, "distributed challenge cleanup failed");
}
self.inner.cleanup(chal).await;
}
}
#[cfg(all(test, feature = "file-storage"))]
mod tests {
use super::*;
fn sample() -> crate::solvers::SolvableChallenge {
crate::solvers::SolvableChallenge {
kind: "http-01".into(),
token: "tok".into(),
url: "https://ca/chal".into(),
identifier: "example.com".into(),
key_authorization: "tok.thumb".into(),
}
}
#[test]
fn key_layout_is_stable() {
assert_eq!(
challenge_tokens_key("acme-v02", "example.com"),
"certificates/acme-v02/challenge_tokens/example.com.json"
);
assert_eq!(
challenge_tokens_key("acme-v02", "*.example.com"),
"certificates/acme-v02/challenge_tokens/wildcard_.example.com.json"
);
}
#[tokio::test]
async fn publish_load_delete_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let storage: Arc<dyn crate::storage::Storage> =
crate::storage::FileStorage::new(dir.path());
let chal = sample();
#[derive(Debug)]
struct NoopSolver;
#[async_trait]
impl Solver for NoopSolver {
async fn present(
&self,
_: &CancellationToken,
_: &crate::solvers::SolvableChallenge,
) -> Result<()> {
Ok(())
}
async fn cleanup(&self, _: &crate::solvers::SolvableChallenge) {}
}
let solver = DistributedSolver {
storage: Arc::clone(&storage),
issuer_key: "acme-v02".into(),
inner: Arc::new(NoopSolver),
};
let ct = CancellationToken::new();
solver.present(&ct, &chal).await.unwrap();
let loaded = load_published(
solver.storage.as_ref(),
&["acme-v02".to_string()],
"example.com",
)
.await
.unwrap();
assert_eq!(loaded.unwrap().key_authorization, "tok.thumb");
solver.cleanup(&chal).await;
let gone = load_published(
solver.storage.as_ref(),
&["acme-v02".to_string()],
"example.com",
)
.await
.unwrap();
assert!(gone.is_none());
}
}