Skip to main content

rust_serv/auto_tls/
challenge.rs

1//! ACME Challenge handling
2
3use std::collections::HashMap;
4use std::sync::Arc;
5use tokio::sync::RwLock;
6
7/// Challenge token and key authorization
8#[derive(Debug, Clone)]
9pub struct ChallengeToken {
10    /// Token for the challenge
11    pub token: String,
12    /// Key authorization (token + thumbprint)
13    pub key_authorization: String,
14}
15
16/// HTTP-01 challenge handler
17/// 
18/// Stores challenge tokens that need to be served at:
19/// GET /.well-known/acme-challenge/{token}
20#[derive(Debug, Clone, Default)]
21pub struct ChallengeHandler {
22    /// Map of token -> key_authorization
23    challenges: Arc<RwLock<HashMap<String, String>>>,
24}
25
26impl ChallengeHandler {
27    /// Create a new challenge handler
28    pub fn new() -> Self {
29        Self {
30            challenges: Arc::new(RwLock::new(HashMap::new())),
31        }
32    }
33
34    /// Add a challenge token
35    pub async fn add_challenge(&self, token: String, key_authorization: String) {
36        let mut challenges = self.challenges.write().await;
37        challenges.insert(token, key_authorization);
38    }
39
40    /// Remove a challenge token
41    pub async fn remove_challenge(&self, token: &str) {
42        let mut challenges = self.challenges.write().await;
43        challenges.remove(token);
44    }
45
46    /// Get key authorization for a token
47    pub async fn get_key_authorization(&self, token: &str) -> Option<String> {
48        let challenges = self.challenges.read().await;
49        challenges.get(token).cloned()
50    }
51
52    /// Check if path is an ACME challenge path
53    pub fn is_challenge_path(path: &str) -> bool {
54        path.starts_with("/.well-known/acme-challenge/")
55    }
56
57    /// Extract token from challenge path
58    /// Returns None if path is not a valid challenge path
59    pub fn extract_token(path: &str) -> Option<&str> {
60        path.strip_prefix("/.well-known/acme-challenge/")
61    }
62
63    /// Handle an ACME challenge request
64    /// Returns the key authorization if found
65    pub async fn handle_challenge(&self, path: &str) -> Option<String> {
66        if !Self::is_challenge_path(path) {
67            return None;
68        }
69
70        let token = Self::extract_token(path)?;
71        self.get_key_authorization(token).await
72    }
73
74    /// Clear all challenges
75    pub async fn clear(&self) {
76        let mut challenges = self.challenges.write().await;
77        challenges.clear();
78    }
79
80    /// Get number of active challenges
81    pub async fn challenge_count(&self) -> usize {
82        let challenges = self.challenges.read().await;
83        challenges.len()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[tokio::test]
92    async fn test_challenge_handler_basic() {
93        let handler = ChallengeHandler::new();
94        
95        handler.add_challenge("token123".to_string(), "key_auth_456".to_string()).await;
96        
97        let result = handler.get_key_authorization("token123").await;
98        assert_eq!(result, Some("key_auth_456".to_string()));
99        
100        handler.remove_challenge("token123").await;
101        let result = handler.get_key_authorization("token123").await;
102        assert_eq!(result, None);
103    }
104
105    #[tokio::test]
106    async fn test_is_challenge_path() {
107        assert!(ChallengeHandler::is_challenge_path("/.well-known/acme-challenge/abc123"));
108        assert!(!ChallengeHandler::is_challenge_path("/health"));
109        assert!(!ChallengeHandler::is_challenge_path("/.well-known/acme-challenge"));
110        assert!(!ChallengeHandler::is_challenge_path("/other/path"));
111    }
112
113    #[test]
114    fn test_extract_token() {
115        assert_eq!(
116            ChallengeHandler::extract_token("/.well-known/acme-challenge/abc123"),
117            Some("abc123")
118        );
119        assert_eq!(
120            ChallengeHandler::extract_token("/.well-known/acme-challenge/"),
121            Some("")
122        );
123        assert_eq!(
124            ChallengeHandler::extract_token("/other/path"),
125            None
126        );
127    }
128
129    #[tokio::test]
130    async fn test_handle_challenge() {
131        let handler = ChallengeHandler::new();
132        handler.add_challenge("abc123".to_string(), "xyz789".to_string()).await;
133
134        let result = handler.handle_challenge("/.well-known/acme-challenge/abc123").await;
135        assert_eq!(result, Some("xyz789".to_string()));
136
137        let result = handler.handle_challenge("/.well-known/acme-challenge/notfound").await;
138        assert_eq!(result, None);
139
140        let result = handler.handle_challenge("/health").await;
141        assert_eq!(result, None);
142    }
143
144    #[tokio::test]
145    async fn test_clear_challenges() {
146        let handler = ChallengeHandler::new();
147        handler.add_challenge("token1".to_string(), "auth1".to_string()).await;
148        handler.add_challenge("token2".to_string(), "auth2".to_string()).await;
149        
150        assert_eq!(handler.challenge_count().await, 2);
151        
152        handler.clear().await;
153        assert_eq!(handler.challenge_count().await, 0);
154    }
155
156    #[tokio::test]
157    async fn test_challenge_handler_clone() {
158        let handler1 = ChallengeHandler::new();
159        handler1.add_challenge("token".to_string(), "auth".to_string()).await;
160        
161        let handler2 = handler1.clone();
162        let result = handler2.get_key_authorization("token").await;
163        assert_eq!(result, Some("auth".to_string()));
164    }
165
166    #[tokio::test]
167    async fn test_concurrent_access() {
168        let handler = Arc::new(ChallengeHandler::new());
169        let mut handles = vec![];
170
171        for i in 0..10 {
172            let h = handler.clone();
173            handles.push(tokio::spawn(async move {
174                h.add_challenge(format!("token{}", i), format!("auth{}", i)).await;
175            }));
176        }
177
178        for handle in handles {
179            handle.await.unwrap();
180        }
181
182        assert_eq!(handler.challenge_count().await, 10);
183    }
184}