Skip to main content

rusmes_acme/
http01.rs

1//! HTTP-01 challenge handler for ACME
2
3use crate::{AcmeError, Result};
4use std::collections::HashMap;
5use std::sync::Arc;
6use tokio::sync::RwLock;
7use tracing::{debug, info};
8
9/// HTTP-01 challenge handler
10///
11/// Handles HTTP-01 challenges by serving challenge responses on:
12/// `http://<domain>/.well-known/acme-challenge/<token>`
13#[derive(Clone)]
14pub struct Http01Handler {
15    /// Map of token -> key_authorization
16    challenges: Arc<RwLock<HashMap<String, String>>>,
17}
18
19impl Http01Handler {
20    /// Create a new HTTP-01 challenge handler
21    pub fn new() -> Self {
22        Self {
23            challenges: Arc::new(RwLock::new(HashMap::new())),
24        }
25    }
26
27    /// Add a challenge response
28    pub async fn add_challenge(&self, token: String, key_authorization: String) {
29        let mut challenges = self.challenges.write().await;
30        challenges.insert(token.clone(), key_authorization);
31        info!("Added HTTP-01 challenge for token: {}", token);
32    }
33
34    /// Get challenge response for a token
35    pub async fn get_challenge(&self, token: &str) -> Option<String> {
36        let challenges = self.challenges.read().await;
37        let response = challenges.get(token).cloned();
38        debug!(
39            "HTTP-01 challenge lookup for token {}: {:?}",
40            token,
41            response.is_some()
42        );
43        response
44    }
45
46    /// Remove a challenge
47    pub async fn remove_challenge(&self, token: &str) {
48        let mut challenges = self.challenges.write().await;
49        challenges.remove(token);
50        info!("Removed HTTP-01 challenge for token: {}", token);
51    }
52
53    /// Clear all challenges
54    pub async fn clear(&self) {
55        let mut challenges = self.challenges.write().await;
56        challenges.clear();
57        info!("Cleared all HTTP-01 challenges");
58    }
59
60    /// Handle HTTP-01 challenge request
61    ///
62    /// This should be integrated with the HTTP server to serve challenges at:
63    /// GET /.well-known/acme-challenge/{token}
64    pub async fn handle_request(&self, token: &str) -> Result<String> {
65        self.get_challenge(token).await.ok_or_else(|| {
66            AcmeError::ChallengeFailed(format!("Challenge token not found: {}", token))
67        })
68    }
69
70    /// Get the well-known path for a token
71    pub fn well_known_path(token: &str) -> String {
72        format!("/.well-known/acme-challenge/{}", token)
73    }
74
75    /// Verify challenge can be accessed
76    pub async fn verify_accessibility(&self, domain: &str, token: &str) -> Result<bool> {
77        let url = format!("http://{}/.well-known/acme-challenge/{}", domain, token);
78
79        match reqwest::get(&url).await {
80            Ok(response) => {
81                if response.status().is_success() {
82                    let body = response.text().await?;
83                    let expected = self.get_challenge(token).await;
84
85                    if let Some(expected_value) = expected {
86                        Ok(body == expected_value)
87                    } else {
88                        Ok(false)
89                    }
90                } else {
91                    Ok(false)
92                }
93            }
94            Err(_) => Ok(false),
95        }
96    }
97}
98
99impl Default for Http01Handler {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[tokio::test]
110    async fn test_http01_handler_creation() {
111        let handler = Http01Handler::new();
112        let result = handler.get_challenge("test").await;
113        assert_eq!(result, None);
114    }
115
116    #[tokio::test]
117    async fn test_add_and_get_challenge() {
118        let handler = Http01Handler::new();
119
120        handler
121            .add_challenge("test-token".to_string(), "test-key-auth".to_string())
122            .await;
123
124        let result = handler.get_challenge("test-token").await;
125        assert_eq!(result, Some("test-key-auth".to_string()));
126    }
127
128    #[tokio::test]
129    async fn test_remove_challenge() {
130        let handler = Http01Handler::new();
131
132        handler
133            .add_challenge("test-token".to_string(), "test-key-auth".to_string())
134            .await;
135
136        let result = handler.get_challenge("test-token").await;
137        assert!(result.is_some());
138
139        handler.remove_challenge("test-token").await;
140
141        let result = handler.get_challenge("test-token").await;
142        assert_eq!(result, None);
143    }
144
145    #[tokio::test]
146    async fn test_clear_all_challenges() {
147        let handler = Http01Handler::new();
148
149        handler
150            .add_challenge("token1".to_string(), "auth1".to_string())
151            .await;
152        handler
153            .add_challenge("token2".to_string(), "auth2".to_string())
154            .await;
155
156        handler.clear().await;
157
158        assert_eq!(handler.get_challenge("token1").await, None);
159        assert_eq!(handler.get_challenge("token2").await, None);
160    }
161
162    #[tokio::test]
163    async fn test_handle_request_success() {
164        let handler = Http01Handler::new();
165
166        handler
167            .add_challenge("test-token".to_string(), "test-key-auth".to_string())
168            .await;
169
170        let result = handler.handle_request("test-token").await;
171        assert!(result.is_ok());
172        assert_eq!(result.unwrap(), "test-key-auth");
173    }
174
175    #[tokio::test]
176    async fn test_handle_request_not_found() {
177        let handler = Http01Handler::new();
178
179        let result = handler.handle_request("nonexistent").await;
180        assert!(result.is_err());
181    }
182
183    #[test]
184    fn test_well_known_path() {
185        let path = Http01Handler::well_known_path("test-token");
186        assert_eq!(path, "/.well-known/acme-challenge/test-token");
187    }
188
189    #[tokio::test]
190    async fn test_multiple_challenges() {
191        let handler = Http01Handler::new();
192
193        for i in 0..10 {
194            handler
195                .add_challenge(format!("token-{}", i), format!("auth-{}", i))
196                .await;
197        }
198
199        for i in 0..10 {
200            let result = handler.get_challenge(&format!("token-{}", i)).await;
201            assert_eq!(result, Some(format!("auth-{}", i)));
202        }
203    }
204
205    #[tokio::test]
206    async fn test_overwrite_challenge() {
207        let handler = Http01Handler::new();
208
209        handler
210            .add_challenge("token".to_string(), "auth1".to_string())
211            .await;
212        handler
213            .add_challenge("token".to_string(), "auth2".to_string())
214            .await;
215
216        let result = handler.get_challenge("token").await;
217        assert_eq!(result, Some("auth2".to_string()));
218    }
219}