1use std::{
2 collections::{HashMap, HashSet},
3 error::Error,
4 path::PathBuf,
5 str::FromStr,
6 sync::Arc,
7 time::Duration,
8};
9
10use base64::Engine;
11use instant_acme::{ExternalAccountKey, LetsEncrypt};
12use rustls::{client::WebPkiServerVerifier, crypto::CryptoProvider, ClientConfig};
13use rustls_platform_verifier::BuilderVerifierExt;
14use tokio::sync::RwLock;
15use xxhash_rust::xxh3::xxh3_128;
16
17use crate::acme::{
18 add_domain_to_cache, convert_on_demand_config, provision_certificate, AcmeConfig, AcmeOnDemandConfig,
19};
20use ferron_common::{get_entry, get_value, util::match_hostname};
21use ferron_common::{logging::ErrorLogger, util::NoServerVerifier};
22
23pub fn build_rustls_client_config(
25 server_configuration: &ferron_common::config::ServerConfiguration,
26 crypto_provider: Arc<CryptoProvider>,
27) -> Result<ClientConfig, Box<dyn Error + Send + Sync>> {
28 build_raw_rustls_client_config(
29 get_value!("auto_tls_no_verification", server_configuration)
30 .and_then(|v| v.as_bool())
31 .unwrap_or(false),
32 crypto_provider,
33 )
34}
35
36fn build_raw_rustls_client_config(
38 no_verification: bool,
39 crypto_provider: Arc<CryptoProvider>,
40) -> Result<ClientConfig, Box<dyn Error + Send + Sync>> {
41 Ok(
42 (if no_verification {
43 ClientConfig::builder_with_provider(crypto_provider.clone())
44 .with_safe_default_protocol_versions()?
45 .dangerous()
46 .with_custom_certificate_verifier(Arc::new(NoServerVerifier::new()))
47 } else if let Ok(client_config) = BuilderVerifierExt::with_platform_verifier(
48 ClientConfig::builder_with_provider(crypto_provider.clone()).with_safe_default_protocol_versions()?,
49 ) {
50 client_config
51 } else {
52 ClientConfig::builder_with_provider(crypto_provider.clone())
53 .with_safe_default_protocol_versions()?
54 .with_webpki_verifier(
55 WebPkiServerVerifier::builder(Arc::new(rustls::RootCertStore {
56 roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
57 }))
58 .build()?,
59 )
60 })
61 .with_no_client_auth(),
62 )
63}
64
65pub fn resolve_acme_directory(server_configuration: &ferron_common::config::ServerConfiguration) -> String {
67 if let Some(directory) = get_value!("auto_tls_directory", server_configuration).and_then(|v| v.as_str()) {
68 directory.to_string()
69 } else if get_value!("auto_tls_letsencrypt_production", server_configuration)
70 .and_then(|v| v.as_bool())
71 .unwrap_or(true)
72 {
73 LetsEncrypt::Production.url().to_string()
74 } else {
75 LetsEncrypt::Staging.url().to_string()
76 }
77}
78
79pub fn parse_eab(
81 server_configuration: &ferron_common::config::ServerConfiguration,
82) -> Result<Option<Arc<ExternalAccountKey>>, anyhow::Error> {
83 Ok(
84 if let Some((Some(eab_key_id), Some(eab_key_hmac))) =
85 get_entry!("auto_tls_eab", server_configuration).map(|entry| {
86 (
87 entry.values.first().and_then(|v| v.as_str()),
88 entry.values.get(1).and_then(|v| v.as_str()),
89 )
90 })
91 {
92 match base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(eab_key_hmac.trim_end_matches('=')) {
93 Ok(decoded_key) => Some(Arc::new(ExternalAccountKey::new(eab_key_id.to_string(), &decoded_key))),
94 Err(err) => Err(anyhow::anyhow!("Failed to decode EAB key HMAC: {}", err))?,
95 }
96 } else {
97 None
98 },
99 )
100}
101
102pub fn resolve_acme_cache_path(
103 server_configuration: &ferron_common::config::ServerConfiguration,
104) -> Result<Option<PathBuf>, anyhow::Error> {
105 let acme_default_directory = dirs::data_local_dir().and_then(|mut p| {
106 p.push("ferron-acme");
107 p.into_os_string().into_string().ok()
108 });
109 Ok(
110 if let Some(acme_cache_path_str) =
111 get_value!("auto_tls_cache", server_configuration).map_or(acme_default_directory.as_deref(), |v| {
112 if v.is_null() {
113 None
114 } else if let Some(v) = v.as_str() {
115 Some(v)
116 } else {
117 acme_default_directory.as_deref()
118 }
119 })
120 {
121 Some(PathBuf::from_str(acme_cache_path_str).map_err(|_| anyhow::anyhow!("Invalid ACME cache path"))?)
122 } else {
123 None
124 },
125 )
126}
127
128pub fn resolve_cache_paths(
130 server_configuration: &ferron_common::config::ServerConfiguration,
131 port: u16,
132 sni_hostname: &str,
133) -> Result<(Option<PathBuf>, Option<PathBuf>), anyhow::Error> {
134 let acme_cache_path_option = resolve_acme_cache_path(server_configuration)?;
135 let (account_cache_path, cert_cache_path) = if let Some(mut pathbuf) = acme_cache_path_option {
136 let base_pathbuf = pathbuf.clone();
137 let append_hash = base64::engine::general_purpose::URL_SAFE_NO_PAD
138 .encode(xxh3_128(format!("{port}-{sni_hostname}").as_bytes()).to_be_bytes());
139 pathbuf.push(append_hash);
140 (Some(base_pathbuf), Some(pathbuf))
141 } else {
142 (None, None)
143 };
144 Ok((account_cache_path, cert_cache_path))
145}
146
147#[allow(clippy::too_many_arguments)]
149pub async fn background_acme_task(
150 acme_configs: Vec<AcmeConfig>,
151 acme_on_demand_configs: Vec<AcmeOnDemandConfig>,
152 memory_acme_account_cache_data: Arc<RwLock<HashMap<String, Vec<u8>>>>,
153 acme_on_demand_rx: async_channel::Receiver<(String, u16)>,
154 on_demand_tls_ask_endpoint: Option<hyper::Uri>,
155 on_demand_tls_ask_endpoint_verify: bool,
156 acme_logger: ErrorLogger,
157 crypto_provider: Arc<CryptoProvider>,
158 existing_combinations: HashSet<(String, u16)>,
159) {
160 let acme_logger = Arc::new(acme_logger);
161
162 let acme_configs_mutex = Arc::new(tokio::sync::Mutex::new(acme_configs));
164
165 let prevent_file_race_conditions_sem = Arc::new(tokio::sync::Semaphore::new(1));
166
167 if !acme_on_demand_configs.is_empty() {
168 tokio::spawn(background_on_demand_acme_task(
170 existing_combinations,
171 acme_on_demand_rx,
172 on_demand_tls_ask_endpoint,
173 on_demand_tls_ask_endpoint_verify,
174 acme_logger.clone(),
175 crypto_provider,
176 acme_configs_mutex.clone(),
177 acme_on_demand_configs,
178 memory_acme_account_cache_data,
179 prevent_file_race_conditions_sem,
180 ));
181 }
182
183 loop {
184 for acme_config in &mut *acme_configs_mutex.lock().await {
185 if let Err(acme_error) = provision_certificate(acme_config, &acme_logger).await {
186 acme_logger
187 .log(&format!("Error while obtaining a TLS certificate: {acme_error}"))
188 .await
189 }
190 }
191 tokio::time::sleep(Duration::from_secs(10)).await;
192 }
193}
194
195#[allow(clippy::too_many_arguments)]
197#[inline]
198pub async fn background_on_demand_acme_task(
199 existing_combinations: HashSet<(String, u16)>,
200 acme_on_demand_rx: async_channel::Receiver<(String, u16)>,
201 on_demand_tls_ask_endpoint: Option<hyper::Uri>,
202 on_demand_tls_ask_endpoint_verify: bool,
203 acme_logger: Arc<ErrorLogger>,
204 crypto_provider: Arc<CryptoProvider>,
205 acme_configs_mutex: Arc<tokio::sync::Mutex<Vec<AcmeConfig>>>,
206 acme_on_demand_configs: Vec<AcmeOnDemandConfig>,
207 memory_acme_account_cache_data: Arc<RwLock<HashMap<String, Vec<u8>>>>,
208 prevent_file_race_conditions_sem: Arc<tokio::sync::Semaphore>,
209) {
210 let acme_on_demand_configs = Arc::new(acme_on_demand_configs);
211 let mut existing_combinations = existing_combinations;
212 while let Ok(received_data) = acme_on_demand_rx.recv().await {
213 let on_demand_tls_ask_endpoint = on_demand_tls_ask_endpoint.clone();
214 if let Some(on_demand_tls_ask_endpoint) = on_demand_tls_ask_endpoint {
215 let mut url_parts = on_demand_tls_ask_endpoint.into_parts();
216 let path_and_query_str = if let Some(path_and_query) = url_parts.path_and_query {
217 let query = path_and_query.query();
218 let query = if let Some(query) = query {
219 format!("{}&domain={}", query, urlencoding::encode(&received_data.0))
220 } else {
221 format!("domain={}", urlencoding::encode(&received_data.0))
222 };
223 format!("{}?{}", path_and_query.path(), query)
224 } else {
225 format!("/?domain={}", urlencoding::encode(&received_data.0))
226 };
227 url_parts.path_and_query = Some(match path_and_query_str.parse() {
228 Ok(parsed) => parsed,
229 Err(err) => {
230 acme_logger
231 .log(&format!(
232 "Error while formatting the URL for on-demand TLS request: {err}"
233 ))
234 .await;
235 continue;
236 }
237 });
238 let endpoint_url = match hyper::Uri::from_parts(url_parts) {
239 Ok(parsed) => parsed,
240 Err(err) => {
241 acme_logger
242 .log(&format!(
243 "Error while formatting the URL for on-demand TLS request: {err}"
244 ))
245 .await;
246 continue;
247 }
248 };
249 let crypto_provider = crypto_provider.clone();
250 let ask_closure = async {
251 let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
252 .build::<_, http_body_util::Empty<hyper::body::Bytes>>(
253 hyper_rustls::HttpsConnectorBuilder::new()
254 .with_tls_config(build_raw_rustls_client_config(
255 !on_demand_tls_ask_endpoint_verify,
256 crypto_provider,
257 )?)
258 .https_or_http()
259 .enable_http1()
260 .enable_http2()
261 .build(),
262 );
263 let request = hyper::Request::builder()
264 .method(hyper::Method::GET)
265 .uri(endpoint_url)
266 .body(http_body_util::Empty::<hyper::body::Bytes>::new())?;
267 let response = client.request(request).await?;
268
269 Ok::<_, Box<dyn Error + Send + Sync>>(response.status().is_success())
270 };
271 match ask_closure.await {
272 Ok(true) => (),
273 Ok(false) => {
274 acme_logger
275 .log(&format!(
276 "The TLS certificate cannot be issued for \"{}\" hostname",
277 &received_data.0
278 ))
279 .await;
280 continue;
281 }
282 Err(err) => {
283 acme_logger
284 .log(&format!(
285 "Error while determining if the TLS certificate can be issued for \"{}\" hostname: {err}",
286 &received_data.0
287 ))
288 .await;
289 continue;
290 }
291 }
292 }
293 if existing_combinations.contains(&received_data) {
294 continue;
295 } else {
296 existing_combinations.insert(received_data.clone());
297 }
298 let (sni_hostname, port) = received_data;
299 let acme_configs_mutex = acme_configs_mutex.clone();
300 let acme_on_demand_configs = acme_on_demand_configs.clone();
301 let memory_acme_account_cache_data = memory_acme_account_cache_data.clone();
302 let prevent_file_race_conditions_sem = prevent_file_race_conditions_sem.clone();
303 tokio::spawn(async move {
304 for acme_on_demand_config in acme_on_demand_configs.iter() {
305 if match_hostname(acme_on_demand_config.sni_hostname.as_deref(), Some(&sni_hostname))
306 && acme_on_demand_config.port == port
307 {
308 let sem_guard = prevent_file_race_conditions_sem.acquire().await;
309 add_domain_to_cache(acme_on_demand_config, &sni_hostname)
310 .await
311 .unwrap_or_default();
312 drop(sem_guard);
313
314 acme_configs_mutex.lock().await.push(
315 convert_on_demand_config(
316 acme_on_demand_config,
317 sni_hostname.clone(),
318 memory_acme_account_cache_data,
319 )
320 .await,
321 );
322 break;
323 }
324 }
325 });
326 }
327}