ferron/setup/
tls.rs

1//! TLS and ACME configuration builder utilities.
2//!
3//! This module is responsible for translating server configuration entries
4//! into concrete TLS listener state, SNI resolvers, and ACME configurations.
5//!
6//! Responsibilities include:
7//! - Manual TLS certificate loading
8//! - Automatic TLS (ACME) configuration
9//! - On-demand vs eager ACME flows
10//! - Resolver wiring per TLS port
11//!
12//! This module is intentionally side-effectful and mutates `TlsBuildContext`
13//! as part of the build process.
14
15use 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/// Accumulates TLS and ACME-related state while building listener configuration.
36///
37/// This struct is mutated during server configuration processing and later
38/// consumed by the runtime to:
39/// - Spawn TLS listeners
40/// - Preload certificates
41/// - Run ACME background tasks
42/// - Handle on-demand certificate issuance
43///
44/// It intentionally groups multiple maps and locks to avoid threading a large
45/// number of parameters through builder functions.
46#[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
84/// Reads the default port from the given server configuration.
85pub 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
105/// Resolves the SNI hostname from the given filters.
106pub 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      // Host blocks with "localhost" specified will have "localhost" SNI hostname
110      return Some("localhost".to_string());
111    }
112
113    None
114  })
115}
116
117/// Ensures that a TLS SNI resolver exists for the given port.
118///
119/// If the resolver does not already exist, it is created along with its
120/// associated resolver lock and inserted into the context.
121///
122/// Returns a mutable reference to the resolver for further configuration.
123fn 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
131/// Configures a manually provided TLS certificate and private key.
132///
133/// This function:
134/// - Loads and validates the certificate and private key
135/// - Registers the certificate for preloading
136/// - Installs an SNI resolver (or fallback resolver) for the given port
137///
138/// Manual TLS always takes precedence over automatic TLS.
139pub 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
177/// Parses ACME challenge type from server configuration.
178fn 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
208/// Checks if the server should be skipped.
209pub 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
213/// Obtains the certificate and key for a manual TLS entry in server configuration.
214pub 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
227/// Handles non-encrypted ports for a server configuration.
228pub fn handle_nonencrypted_ports(
229  ctx: &mut TlsBuildContext,
230  server: &ferron_common::config::ServerConfiguration,
231  default_http_port: Option<u16>,
232) {
233  // If TLS is explicitly configured, don't add HTTP
234  if get_entry!("tls", server).is_some() {
235    return;
236  }
237
238  // If automatic TLS is explicitly enabled, HTTP is usually disabled
239  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
252/// Configures automatic TLS (ACME) for a server configuration.
253///
254/// Depending on the server configuration, this function may:
255/// - Configure eager (startup-time) ACME
256/// - Configure on-demand ACME
257/// - Skip automatic TLS if required conditions are not met
258///
259/// This function does not perform ACME issuance itself; it only wires the
260/// required resolvers and configuration objects.
261pub 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  // Automatic TLS requires SNI unless global
276  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  // DNS provider only applies to DNS-01
315  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/// Builds an on-demand ACME configuration.
353///
354/// On-demand ACME defers certificate issuance until a client connects and
355/// requests a hostname that does not yet have a certificate. This is typically
356/// used for wildcard or dynamic hostnames.
357#[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  // TLS-ALPN-01 requires a dedicated resolver
369  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  // Install fallback sender into SNI resolver
381  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/// Builds an eager (startup-time) ACME configuration.
426///
427/// Eager ACME requests and maintains certificates proactively at startup,
428/// before any client traffic is received. This is typically used for known
429/// hostnames and static configurations.
430#[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  // Wire challenge resolvers
513  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  // Install SNI resolver
530  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}