sentinel_proxy/acme/
challenge.rs1use dashmap::DashMap;
7use std::sync::Arc;
8use tracing::{debug, trace};
9
10pub const ACME_CHALLENGE_PREFIX: &str = "/.well-known/acme-challenge/";
12
13#[derive(Debug)]
24pub struct ChallengeManager {
25 challenges: Arc<DashMap<String, String>>,
27}
28
29impl ChallengeManager {
30 pub fn new() -> Self {
32 Self {
33 challenges: Arc::new(DashMap::new()),
34 }
35 }
36
37 pub fn add_challenge(&self, token: &str, key_authorization: &str) {
47 debug!(token = %token, "Registering ACME HTTP-01 challenge");
48 self.challenges
49 .insert(token.to_string(), key_authorization.to_string());
50 }
51
52 pub fn remove_challenge(&self, token: &str) {
56 if self.challenges.remove(token).is_some() {
57 debug!(token = %token, "Removed ACME challenge");
58 }
59 }
60
61 pub fn get_response(&self, token: &str) -> Option<String> {
66 let result = self.challenges.get(token).map(|v| v.clone());
67 if result.is_some() {
68 trace!(token = %token, "ACME challenge token found");
69 } else {
70 trace!(token = %token, "ACME challenge token not found");
71 }
72 result
73 }
74
75 pub fn extract_token(path: &str) -> Option<&str> {
80 path.strip_prefix(ACME_CHALLENGE_PREFIX)
81 }
82
83 pub fn pending_count(&self) -> usize {
85 self.challenges.len()
86 }
87
88 pub fn clear(&self) {
92 let count = self.challenges.len();
93 self.challenges.clear();
94 if count > 0 {
95 debug!(cleared = count, "Cleared all pending ACME challenges");
96 }
97 }
98}
99
100impl Default for ChallengeManager {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106impl Clone for ChallengeManager {
107 fn clone(&self) -> Self {
108 Self {
109 challenges: Arc::clone(&self.challenges),
110 }
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn test_add_and_get_challenge() {
120 let manager = ChallengeManager::new();
121
122 manager.add_challenge("test-token", "test-key-auth");
123
124 let response = manager.get_response("test-token");
125 assert_eq!(response, Some("test-key-auth".to_string()));
126 }
127
128 #[test]
129 fn test_get_nonexistent_challenge() {
130 let manager = ChallengeManager::new();
131
132 let response = manager.get_response("nonexistent");
133 assert_eq!(response, None);
134 }
135
136 #[test]
137 fn test_remove_challenge() {
138 let manager = ChallengeManager::new();
139
140 manager.add_challenge("test-token", "test-key-auth");
141 assert_eq!(manager.pending_count(), 1);
142
143 manager.remove_challenge("test-token");
144 assert_eq!(manager.pending_count(), 0);
145
146 let response = manager.get_response("test-token");
147 assert_eq!(response, None);
148 }
149
150 #[test]
151 fn test_extract_token() {
152 assert_eq!(
153 ChallengeManager::extract_token("/.well-known/acme-challenge/abc123"),
154 Some("abc123")
155 );
156
157 assert_eq!(
158 ChallengeManager::extract_token("/.well-known/acme-challenge/"),
159 Some("")
160 );
161
162 assert_eq!(ChallengeManager::extract_token("/other/path"), None);
163
164 assert_eq!(
165 ChallengeManager::extract_token("/.well-known/acme-challenge"),
166 None
167 );
168 }
169
170 #[test]
171 fn test_clear_challenges() {
172 let manager = ChallengeManager::new();
173
174 manager.add_challenge("token1", "auth1");
175 manager.add_challenge("token2", "auth2");
176 assert_eq!(manager.pending_count(), 2);
177
178 manager.clear();
179 assert_eq!(manager.pending_count(), 0);
180 }
181
182 #[test]
183 fn test_clone_shares_state() {
184 let manager1 = ChallengeManager::new();
185 let manager2 = manager1.clone();
186
187 manager1.add_challenge("token", "auth");
188
189 assert_eq!(manager2.get_response("token"), Some("auth".to_string()));
191 }
192}