1use std::collections::{HashMap, HashSet};
16use std::net::IpAddr;
17use std::path::PathBuf;
18use std::str::FromStr;
19use std::sync::Arc;
20
21use async_channel::{Receiver, Sender};
22use ferron_common::config::ServerConfigurationFilters;
23use ferron_common::get_entry;
24use ferron_common::logging::LogMessage;
25use instant_acme::ChallengeType;
26use rustls::crypto::CryptoProvider;
27use rustls::sign::CertifiedKey;
28use tokio::sync::RwLock;
29
30use crate::acme::{AcmeCache, AcmeConfig, AcmeOnDemandConfig, Http01DataLock, TlsAlpn01DataLock, TlsAlpn01Resolver};
31use crate::util::{
32 load_certs, load_private_key, CustomSniResolver, HostnameRadixTree, OneCertifiedKeyResolver, SniResolverLock,
33};
34
35#[allow(clippy::type_complexity)]
47pub struct TlsBuildContext {
48 pub tls_ports: HashMap<(Option<IpAddr>, u16), CustomSniResolver>,
49 pub tls_port_locks: HashMap<(Option<IpAddr>, u16), SniResolverLock>,
50 pub nonencrypted_ports: HashSet<u16>,
51 pub certified_keys_to_preload: HashMap<(Option<IpAddr>, u16), Vec<Arc<CertifiedKey>>>,
52 pub used_sni_hostnames: HashSet<((Option<IpAddr>, u16), Option<String>)>,
53 pub automatic_tls_used_sni_hostnames: HashSet<((Option<IpAddr>, u16), Option<String>)>,
54 pub acme_tls_alpn_01_resolvers: HashMap<(Option<IpAddr>, u16), TlsAlpn01Resolver>,
55 pub acme_tls_alpn_01_resolver_locks: HashMap<(Option<IpAddr>, u16), Arc<RwLock<Vec<TlsAlpn01DataLock>>>>,
56 pub acme_http_01_resolvers: Arc<RwLock<Vec<Http01DataLock>>>,
57 pub acme_configs: Vec<AcmeConfig>,
58 pub acme_on_demand_configs: Vec<AcmeOnDemandConfig>,
59 pub acme_on_demand_tx: Sender<(String, u16)>,
60 pub acme_on_demand_rx: Receiver<(String, u16)>,
61}
62
63impl Default for TlsBuildContext {
64 fn default() -> Self {
65 let (acme_on_demand_tx, acme_on_demand_rx) = async_channel::unbounded();
66 Self {
67 tls_ports: HashMap::new(),
68 tls_port_locks: HashMap::new(),
69 nonencrypted_ports: HashSet::new(),
70 certified_keys_to_preload: HashMap::new(),
71 used_sni_hostnames: HashSet::new(),
72 automatic_tls_used_sni_hostnames: HashSet::new(),
73 acme_tls_alpn_01_resolvers: HashMap::new(),
74 acme_tls_alpn_01_resolver_locks: HashMap::new(),
75 acme_http_01_resolvers: Arc::new(RwLock::new(Vec::new())),
76 acme_configs: Vec::new(),
77 acme_on_demand_configs: Vec::new(),
78 acme_on_demand_tx,
79 acme_on_demand_rx,
80 }
81 }
82}
83
84pub fn read_default_port(config: Option<&ferron_common::config::ServerConfiguration>, is_https: bool) -> Option<u16> {
86 let fallback = if is_https { 443 } else { 80 };
87 config
88 .and_then(|c| {
89 if is_https {
90 get_entry!("default_https_port", c)
91 } else {
92 get_entry!("default_http_port", c)
93 }
94 })
95 .and_then(|e| e.values.first())
96 .map_or(Some(fallback), |v| {
97 if v.is_null() {
98 None
99 } else {
100 Some(v.as_i128().unwrap_or(fallback as i128) as u16)
101 }
102 })
103}
104
105pub fn resolve_sni_hostname(filters: &ServerConfigurationFilters) -> Option<String> {
107 filters.hostname.clone().or_else(|| {
108 if filters.ip.is_some_and(|ip| ip.is_loopback()) {
109 return Some("localhost".to_string());
111 }
112
113 None
114 })
115}
116
117fn ensure_tls_port_resolver(ctx: &mut TlsBuildContext, port: u16, ip: Option<IpAddr>) -> &mut CustomSniResolver {
124 ctx.tls_ports.entry((ip, port)).or_insert_with(|| {
125 let list = Arc::new(RwLock::new(HostnameRadixTree::new()));
126 ctx.tls_port_locks.insert((ip, port), list.clone());
127 CustomSniResolver::with_resolvers(list)
128 })
129}
130
131pub fn handle_manual_tls(
140 ctx: &mut TlsBuildContext,
141 crypto_provider: &CryptoProvider,
142 port: u16,
143 ip: Option<IpAddr>,
144 sni_hostname: Option<String>,
145 cert_path: &str,
146 key_path: &str,
147) -> anyhow::Result<()> {
148 let certs = load_certs(cert_path).map_err(|e| anyhow::anyhow!("Cannot load certificate {cert_path}: {e}"))?;
149
150 let key = load_private_key(key_path).map_err(|e| anyhow::anyhow!("Cannot load key {key_path}: {e}"))?;
151
152 let signing_key = crypto_provider
153 .key_provider
154 .load_private_key(key)
155 .map_err(|e| anyhow::anyhow!("Invalid private key {key_path}: {e}"))?;
156
157 let certified_key = Arc::new(CertifiedKey::new(certs, signing_key));
158
159 ctx
160 .certified_keys_to_preload
161 .entry((ip, port))
162 .or_default()
163 .push(certified_key.clone());
164
165 let resolver = Arc::new(OneCertifiedKeyResolver::new(certified_key));
166 let sni_resolver = ensure_tls_port_resolver(ctx, port, ip);
167
168 match &sni_hostname {
169 Some(host) => sni_resolver.load_host_resolver(host, resolver),
170 None => sni_resolver.load_fallback_resolver(resolver),
171 }
172
173 ctx.used_sni_hostnames.insert(((ip, port), sni_hostname));
174 Ok(())
175}
176
177fn parse_challenge_type(
179 server: &ferron_common::config::ServerConfiguration,
180) -> anyhow::Result<(ChallengeType, HashMap<String, String>)> {
181 let entry = get_entry!("auto_tls_challenge", server);
182
183 let ty = entry
184 .and_then(|e| e.values.first())
185 .and_then(|v| v.as_str())
186 .unwrap_or("tls-alpn-01")
187 .to_uppercase();
188
189 let params = entry
190 .map(|e| {
191 e.props
192 .iter()
193 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
194 .collect()
195 })
196 .unwrap_or_default();
197
198 let challenge = match ty.as_str() {
199 "HTTP-01" => ChallengeType::Http01,
200 "TLS-ALPN-01" => ChallengeType::TlsAlpn01,
201 "DNS-01" => ChallengeType::Dns01,
202 _ => anyhow::bail!("Unsupported ACME challenge type: {ty}"),
203 };
204
205 Ok((challenge, params))
206}
207
208pub fn should_skip_server(server: &ferron_common::config::ServerConfiguration) -> bool {
210 server.filters.is_global_non_host() || (server.filters.is_global() && server.entries.is_empty())
211}
212
213pub fn manual_tls_entry(server: &ferron_common::config::ServerConfiguration) -> Option<(&str, &str)> {
215 let tls_entry = get_entry!("tls", server)?;
216
217 if tls_entry.values.len() != 2 {
218 return None;
219 }
220
221 let cert = tls_entry.values[0].as_str()?;
222 let key = tls_entry.values[1].as_str()?;
223
224 Some((cert, key))
225}
226
227pub fn handle_nonencrypted_ports(
229 ctx: &mut TlsBuildContext,
230 server: &ferron_common::config::ServerConfiguration,
231 default_http_port: Option<u16>,
232) {
233 if get_entry!("tls", server).is_some() {
235 return;
236 }
237
238 if get_entry!("auto_tls", server)
240 .and_then(|e| e.values.first())
241 .and_then(|v| v.as_bool())
242 .unwrap_or(false)
243 {
244 return;
245 }
246
247 if let Some(port) = server.filters.port.or(default_http_port) {
248 ctx.nonencrypted_ports.insert(port);
249 }
250}
251
252pub fn handle_automatic_tls(
262 ctx: &mut TlsBuildContext,
263 server: &ferron_common::config::ServerConfiguration,
264 port: u16,
265 ip: Option<IpAddr>,
266 sni_hostname: Option<String>,
267 crypto_provider: Arc<CryptoProvider>,
268 memory_acme_account_cache_data: Arc<RwLock<HashMap<String, Vec<u8>>>>,
269) -> anyhow::Result<Option<LogMessage>> {
270 let on_demand = get_entry!("auto_tls_on_demand", server)
271 .and_then(|e| e.values.first())
272 .and_then(|v| v.as_bool())
273 .unwrap_or(false);
274
275 if sni_hostname.is_none()
277 && !server.filters.is_global()
278 && !server.filters.is_global_non_host()
279 && server.filters.ip.is_none()
280 {
281 return Ok(Some(LogMessage::new(
282 "Skipping automatic TLS for a host without a SNI hostname...".to_string(),
283 true,
284 )));
285 }
286
287 let (challenge_type, challenge_params) = parse_challenge_type(server)?;
288
289 if let Some(sni_hostname) = &sni_hostname {
290 let is_wildcard_domain = sni_hostname.starts_with("*.");
291 if is_wildcard_domain && !on_demand {
292 match &challenge_type {
293 ChallengeType::Http01 => {
294 return Ok(Some(LogMessage::new(
295 format!(
296 "HTTP-01 ACME challenge doesn't support wildcard hostnames, skipping SNI host \"{sni_hostname}\"..."
297 ),
298 true,
299 )));
300 }
301 ChallengeType::TlsAlpn01 => {
302 return Ok(Some(LogMessage::new(
303 format!(
304 "TLS-ALPN-01 ACME challenge doesn't support wildcard hostnames, skipping SNI host \"{sni_hostname}\"..."
305 ),
306 true,
307 )));
308 }
309 _ => (),
310 }
311 }
312 }
313
314 let dns_provider = if challenge_type == ChallengeType::Dns01 {
316 let provider = challenge_params
317 .get("provider")
318 .ok_or_else(|| anyhow::anyhow!("DNS-01 challenge requires a provider"))?;
319 Some(ferron_load_modules::get_dns_provider(provider, &challenge_params).map_err(|e| anyhow::anyhow!(e))?)
320 } else {
321 None
322 };
323
324 if on_demand {
325 build_on_demand_acme(
326 ctx,
327 server,
328 port,
329 ip,
330 sni_hostname,
331 challenge_type,
332 dns_provider,
333 crypto_provider,
334 )?;
335 } else {
336 build_eager_acme(
337 ctx,
338 server,
339 port,
340 ip,
341 sni_hostname,
342 challenge_type,
343 dns_provider,
344 crypto_provider,
345 memory_acme_account_cache_data,
346 )?;
347 }
348
349 Ok(None)
350}
351
352#[allow(clippy::too_many_arguments)]
358fn build_on_demand_acme(
359 ctx: &mut TlsBuildContext,
360 server: &ferron_common::config::ServerConfiguration,
361 port: u16,
362 ip: Option<IpAddr>,
363 sni_hostname: Option<String>,
364 challenge_type: ChallengeType,
365 dns_provider: Option<Arc<dyn ferron_common::dns::DnsProvider + Send + Sync>>,
366 crypto_provider: Arc<CryptoProvider>,
367) -> anyhow::Result<()> {
368 if challenge_type == ChallengeType::TlsAlpn01 {
370 let resolver_list = Arc::new(RwLock::new(Vec::new()));
371 ctx
372 .acme_tls_alpn_01_resolver_locks
373 .insert((ip, port), resolver_list.clone());
374
375 ctx
376 .acme_tls_alpn_01_resolvers
377 .insert((ip, port), TlsAlpn01Resolver::with_resolvers(resolver_list));
378 }
379
380 let fallback_sender = ctx.acme_on_demand_tx.clone();
382 let sni_resolver = ensure_tls_port_resolver(ctx, port, ip);
383 sni_resolver.load_fallback_sender(fallback_sender, port);
384
385 let rustls_client_config =
386 super::acme::build_rustls_client_config(server, crypto_provider).map_err(|e| anyhow::anyhow!(e))?;
387
388 let config = AcmeOnDemandConfig {
389 rustls_client_config,
390 challenge_type,
391 contact: get_entry!("auto_tls_contact", server)
392 .and_then(|e| e.values.first())
393 .and_then(|v| v.as_str())
394 .map(|c| vec![format!("mailto:{c}")])
395 .unwrap_or_default(),
396 directory: super::acme::resolve_acme_directory(server),
397 eab_key: super::acme::parse_eab(server)?,
398 profile: get_entry!("auto_tls_profile", server)
399 .and_then(|e| e.values.first())
400 .and_then(|v| v.as_str())
401 .map(str::to_string),
402 cache_path: super::acme::resolve_acme_cache_path(server)?,
403 sni_resolver_lock: ctx
404 .tls_port_locks
405 .get(&(ip, port))
406 .cloned()
407 .unwrap_or_else(|| Arc::new(RwLock::new(HostnameRadixTree::new()))),
408 tls_alpn_01_resolver_lock: ctx
409 .acme_tls_alpn_01_resolver_locks
410 .get(&(ip, port))
411 .cloned()
412 .unwrap_or_else(|| Arc::new(RwLock::new(Vec::new()))),
413 http_01_resolver_lock: ctx.acme_http_01_resolvers.clone(),
414 dns_provider,
415 sni_hostname,
416 port,
417 };
418
419 ctx.acme_on_demand_configs.push(config);
420 ctx.automatic_tls_used_sni_hostnames.insert(((ip, port), None));
421
422 Ok(())
423}
424
425#[allow(clippy::too_many_arguments)]
431fn build_eager_acme(
432 ctx: &mut TlsBuildContext,
433 server: &ferron_common::config::ServerConfiguration,
434 port: u16,
435 ip: Option<IpAddr>,
436 sni_hostname: Option<String>,
437 challenge_type: ChallengeType,
438 dns_provider: Option<Arc<dyn ferron_common::dns::DnsProvider + Send + Sync>>,
439 crypto_provider: Arc<CryptoProvider>,
440 memory_acme_account_cache_data: Arc<RwLock<HashMap<String, Vec<u8>>>>,
441) -> anyhow::Result<()> {
442 let certified_key_lock = Arc::new(RwLock::new(None));
443 let tls_alpn_01_data_lock = Arc::new(RwLock::new(None));
444 let http_01_data_lock = Arc::new(RwLock::new(None));
445
446 let domain = if let Some(sni) = &sni_hostname {
447 sni.clone()
448 } else if let Some(ip) = ip {
449 ip.to_canonical().to_string()
450 } else {
451 return Ok(());
452 };
453
454 let rustls_client_config =
455 super::acme::build_rustls_client_config(server, crypto_provider).map_err(|e| anyhow::anyhow!(e))?;
456 let (account_cache_path, certificate_cache_path) = super::acme::resolve_cache_paths(server, port, &domain)?;
457
458 let save_paths = get_entry!("auto_tls_save_data", server).and_then(|e| {
459 e.values
460 .first()
461 .and_then(|v| v.as_str())
462 .and_then(|v| PathBuf::from_str(v).ok())
463 .and_then(|v| {
464 e.values
465 .get(1)
466 .and_then(|v| v.as_str())
467 .and_then(|v| PathBuf::from_str(v).ok())
468 .map(|v2| (v, v2))
469 })
470 });
471
472 let acme_config = AcmeConfig {
473 rustls_client_config,
474 domains: vec![domain],
475 challenge_type: challenge_type.clone(),
476 contact: get_entry!("auto_tls_contact", server)
477 .and_then(|e| e.values.first())
478 .and_then(|v| v.as_str())
479 .map(|c| vec![format!("mailto:{c}")])
480 .unwrap_or_default(),
481 directory: super::acme::resolve_acme_directory(server),
482 eab_key: super::acme::parse_eab(server)?,
483 profile: get_entry!("auto_tls_profile", server)
484 .and_then(|e| e.values.first())
485 .and_then(|v| v.as_str())
486 .map(str::to_string),
487 account_cache: if let Some(account_cache_path) = account_cache_path {
488 AcmeCache::File(account_cache_path)
489 } else {
490 AcmeCache::Memory(memory_acme_account_cache_data)
491 },
492 certificate_cache: if let Some(certificate_cache_path) = certificate_cache_path {
493 AcmeCache::File(certificate_cache_path)
494 } else {
495 AcmeCache::Memory(Default::default())
496 },
497 certified_key_lock: certified_key_lock.clone(),
498 tls_alpn_01_data_lock: tls_alpn_01_data_lock.clone(),
499 http_01_data_lock: http_01_data_lock.clone(),
500 dns_provider,
501 renewal_info: None,
502 account: None,
503 save_paths,
504 post_obtain_command: get_entry!("auto_tls_post_obtain_command", server)
505 .and_then(|e| e.values.first())
506 .and_then(|v| v.as_str())
507 .map(str::to_string),
508 };
509
510 ctx.acme_configs.push(acme_config);
511
512 match challenge_type {
514 ChallengeType::Http01 => {
515 ctx.acme_http_01_resolvers.blocking_write().push(http_01_data_lock);
516 }
517 ChallengeType::TlsAlpn01 => {
518 let resolver = ctx.acme_tls_alpn_01_resolvers.entry((ip, port)).or_insert_with(|| {
519 let list = Arc::new(RwLock::new(Vec::new()));
520 ctx.acme_tls_alpn_01_resolver_locks.insert((ip, port), list.clone());
521 TlsAlpn01Resolver::with_resolvers(list)
522 });
523
524 resolver.load_resolver(tls_alpn_01_data_lock);
525 }
526 _ => {}
527 }
528
529 let acme_resolver = Arc::new(crate::acme::AcmeResolver::new(certified_key_lock));
531 let sni_resolver = ensure_tls_port_resolver(ctx, port, ip);
532 if let Some(sni) = &sni_hostname {
533 sni_resolver.load_host_resolver(sni, acme_resolver);
534 } else {
535 sni_resolver.load_fallback_resolver(acme_resolver);
536 }
537
538 ctx.automatic_tls_used_sni_hostnames.insert(((ip, port), sni_hostname));
539
540 Ok(())
541}