use dashmap::DashMap;
use std::sync::Arc;
use tracing::{debug, trace};
pub const ACME_CHALLENGE_PREFIX: &str = "/.well-known/acme-challenge/";
#[derive(Debug)]
pub struct ChallengeManager {
challenges: Arc<DashMap<String, String>>,
}
impl ChallengeManager {
pub fn new() -> Self {
Self {
challenges: Arc::new(DashMap::new()),
}
}
pub fn add_challenge(&self, token: &str, key_authorization: &str) {
debug!(token = %token, "Registering ACME HTTP-01 challenge");
self.challenges
.insert(token.to_string(), key_authorization.to_string());
}
pub fn remove_challenge(&self, token: &str) {
if self.challenges.remove(token).is_some() {
debug!(token = %token, "Removed ACME challenge");
}
}
pub fn get_response(&self, token: &str) -> Option<String> {
let result = self.challenges.get(token).map(|v| v.clone());
if result.is_some() {
trace!(token = %token, "ACME challenge token found");
} else {
trace!(token = %token, "ACME challenge token not found");
}
result
}
pub fn extract_token(path: &str) -> Option<&str> {
path.strip_prefix(ACME_CHALLENGE_PREFIX)
}
pub fn pending_count(&self) -> usize {
self.challenges.len()
}
pub fn clear(&self) {
let count = self.challenges.len();
self.challenges.clear();
if count > 0 {
debug!(cleared = count, "Cleared all pending ACME challenges");
}
}
}
impl Default for ChallengeManager {
fn default() -> Self {
Self::new()
}
}
impl Clone for ChallengeManager {
fn clone(&self) -> Self {
Self {
challenges: Arc::clone(&self.challenges),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_and_get_challenge() {
let manager = ChallengeManager::new();
manager.add_challenge("test-token", "test-key-auth");
let response = manager.get_response("test-token");
assert_eq!(response, Some("test-key-auth".to_string()));
}
#[test]
fn test_get_nonexistent_challenge() {
let manager = ChallengeManager::new();
let response = manager.get_response("nonexistent");
assert_eq!(response, None);
}
#[test]
fn test_remove_challenge() {
let manager = ChallengeManager::new();
manager.add_challenge("test-token", "test-key-auth");
assert_eq!(manager.pending_count(), 1);
manager.remove_challenge("test-token");
assert_eq!(manager.pending_count(), 0);
let response = manager.get_response("test-token");
assert_eq!(response, None);
}
#[test]
fn test_extract_token() {
assert_eq!(
ChallengeManager::extract_token("/.well-known/acme-challenge/abc123"),
Some("abc123")
);
assert_eq!(
ChallengeManager::extract_token("/.well-known/acme-challenge/"),
Some("")
);
assert_eq!(ChallengeManager::extract_token("/other/path"), None);
assert_eq!(
ChallengeManager::extract_token("/.well-known/acme-challenge"),
None
);
}
#[test]
fn test_clear_challenges() {
let manager = ChallengeManager::new();
manager.add_challenge("token1", "auth1");
manager.add_challenge("token2", "auth2");
assert_eq!(manager.pending_count(), 2);
manager.clear();
assert_eq!(manager.pending_count(), 0);
}
#[test]
fn test_clone_shares_state() {
let manager1 = ChallengeManager::new();
let manager2 = manager1.clone();
manager1.add_challenge("token", "auth");
assert_eq!(manager2.get_response("token"), Some("auth".to_string()));
}
}