1pub mod distributed;
12pub mod dns;
13pub mod http;
14pub mod tls_alpn;
15
16use std::sync::Arc;
17
18use async_trait::async_trait;
19use tokio_util::sync::CancellationToken;
20
21use crate::error::Result;
22
23#[derive(Debug, Clone)]
26pub struct SolvableChallenge {
27 pub kind: String,
29 pub token: String,
31 pub url: String,
33 pub identifier: String,
35 pub key_authorization: String,
37}
38
39impl SolvableChallenge {
40 pub fn new(
45 kind: &str,
46 token: &str,
47 url: &str,
48 identifier: &str,
49 account: &crate::acme::protocol::AccountKey,
50 ) -> Result<Self> {
51 Ok(Self {
52 kind: kind.to_owned(),
53 token: token.to_owned(),
54 url: url.to_owned(),
55 identifier: identifier.to_owned(),
56 key_authorization: crate::acme::key_authorization(token, &account.thumbprint_b64()?)?,
57 })
58 }
59}
60
61#[async_trait]
63pub trait Solver: Send + Sync + std::fmt::Debug {
64 async fn present(&self, ct: &CancellationToken, chal: &SolvableChallenge) -> Result<()>;
66
67 async fn wait(&self, _ct: &CancellationToken, _chal: &SolvableChallenge) -> Result<()> {
70 Ok(())
71 }
72
73 async fn cleanup(&self, chal: &SolvableChallenge);
76}
77
78#[derive(Debug, Default)]
84pub struct ActiveChallenges {
85 map: std::sync::Mutex<std::collections::HashMap<String, SolvableChallenge>>,
86}
87
88impl ActiveChallenges {
89 fn global() -> &'static ActiveChallenges {
90 static REG: std::sync::OnceLock<ActiveChallenges> = std::sync::OnceLock::new();
91 REG.get_or_init(ActiveChallenges::default)
92 }
93
94 pub(crate) fn insert(key: &str, chal: &SolvableChallenge) {
96 if let Ok(mut map) = ActiveChallenges::global().map.lock() {
97 map.insert(key.to_owned(), chal.clone());
98 }
99 }
100
101 pub(crate) fn remove(key: &str) {
103 if let Ok(mut map) = ActiveChallenges::global().map.lock() {
104 map.remove(key);
105 }
106 }
107
108 #[must_use]
112 pub fn get(kind: &str, identifier: &str) -> Option<SolvableChallenge> {
113 let needle = identifier.to_lowercase();
114 let map = ActiveChallenges::global().map.lock().ok()?;
115 map.values()
116 .find(|c| c.kind == kind && c.identifier.to_lowercase() == needle)
117 .cloned()
118 }
119}
120
121pub(crate) fn challenge_key(chal: &SolvableChallenge) -> String {
123 crate::crypto::sha256_hex(chal.url.as_bytes())
124}
125
126#[derive(Debug)]
129pub struct SolverWrapper<S: Solver> {
130 inner: S,
131}
132
133impl<S: Solver> SolverWrapper<S> {
134 #[must_use]
136 pub fn new(inner: S) -> Self {
137 Self { inner }
138 }
139}
140
141#[async_trait]
142impl<S: Solver> Solver for SolverWrapper<S> {
143 async fn present(&self, ct: &CancellationToken, chal: &SolvableChallenge) -> Result<()> {
144 self.inner.present(ct, chal).await?;
145 ActiveChallenges::insert(&challenge_key(chal), chal);
146 Ok(())
147 }
148
149 async fn wait(&self, ct: &CancellationToken, chal: &SolvableChallenge) -> Result<()> {
150 self.inner.wait(ct, chal).await
151 }
152
153 async fn cleanup(&self, chal: &SolvableChallenge) {
154 ActiveChallenges::remove(&challenge_key(chal));
155 self.inner.cleanup(chal).await;
156 }
157}
158
159#[must_use]
162pub fn get_acme_challenge(kind: &str, identifier: &str) -> Option<SolvableChallenge> {
163 ActiveChallenges::get(kind, identifier)
164}
165
166#[must_use]
168pub fn wrapped<S: Solver>(inner: S) -> Arc<SolverWrapper<S>> {
169 Arc::new(SolverWrapper::new(inner))
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[derive(Debug, Default)]
177 struct RecordingSolver {
178 present_calls: std::sync::atomic::AtomicUsize,
179 cleanup_calls: std::sync::atomic::AtomicUsize,
180 }
181
182 #[async_trait]
183 impl Solver for RecordingSolver {
184 async fn present(&self, _ct: &CancellationToken, _chal: &SolvableChallenge) -> Result<()> {
185 self.present_calls
186 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
187 Ok(())
188 }
189
190 async fn cleanup(&self, _chal: &SolvableChallenge) {
191 self.cleanup_calls
192 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
193 }
194 }
195
196 fn sample_challenge() -> SolvableChallenge {
197 SolvableChallenge {
198 kind: "dns-01".into(),
199 token: "tok".into(),
200 url: "https://ca/chal/xyz".into(),
201 identifier: "example.com".into(),
202 key_authorization: "tok.thumb".into(),
203 }
204 }
205
206 #[tokio::test]
207 async fn wrapper_registers_and_unregisters() {
208 let solver = wrapped(RecordingSolver::default());
209 let chal = sample_challenge();
210
211 assert!(get_acme_challenge("dns-01", "example.com").is_none());
212 solver
213 .present(&CancellationToken::new(), &chal)
214 .await
215 .unwrap();
216 assert!(get_acme_challenge("dns-01", "example.com").is_some());
217 solver.cleanup(&chal).await;
218 assert!(get_acme_challenge("dns-01", "example.com").is_none());
219 }
220
221 #[tokio::test]
222 async fn wait_defaults_to_ok() {
223 let solver = wrapped(RecordingSolver::default());
224 solver
225 .wait(&CancellationToken::new(), &sample_challenge())
226 .await
227 .unwrap();
228 }
229}