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.
46pub struct TlsBuildContext {
47  pub tls_ports: HashMap<u16, CustomSniResolver>,
48  pub tls_port_locks: HashMap<u16, SniResolverLock>,
49  pub nonencrypted_ports: HashSet<u16>,
50  pub certified_keys_to_preload: HashMap<u16, Vec<Arc<CertifiedKey>>>,
51  pub used_sni_hostnames: HashSet<(u16, Option<String>)>,
52  pub automatic_tls_used_sni_hostnames: HashSet<(u16, Option<String>)>,
53  pub acme_tls_alpn_01_resolvers: HashMap<u16, TlsAlpn01Resolver>,
54  pub acme_tls_alpn_01_resolver_locks: HashMap<u16, Arc<RwLock<Vec<TlsAlpn01DataLock>>>>,
55  pub acme_http_01_resolvers: Arc<RwLock<Vec<Http01DataLock>>>,
56  pub acme_configs: Vec<AcmeConfig>,
57  pub acme_on_demand_configs: Vec<AcmeOnDemandConfig>,
58  pub acme_on_demand_tx: Sender<(String, u16)>,
59  pub acme_on_demand_rx: Receiver<(String, u16)>,
60}
61
62impl Default for TlsBuildContext {
63  fn default() -> Self {
64    let (acme_on_demand_tx, acme_on_demand_rx) = async_channel::unbounded();
65    Self {
66      tls_ports: HashMap::new(),
67      tls_port_locks: HashMap::new(),
68      nonencrypted_ports: HashSet::new(),
69      certified_keys_to_preload: HashMap::new(),
70      used_sni_hostnames: HashSet::new(),
71      automatic_tls_used_sni_hostnames: HashSet::new(),
72      acme_tls_alpn_01_resolvers: HashMap::new(),
73      acme_tls_alpn_01_resolver_locks: HashMap::new(),
74      acme_http_01_resolvers: Arc::new(RwLock::new(Vec::new())),
75      acme_configs: Vec::new(),
76      acme_on_demand_configs: Vec::new(),
77      acme_on_demand_tx,
78      acme_on_demand_rx,
79    }
80  }
81}
82
83/// Reads the default port from the given server configuration.
84pub fn read_default_port(config: Option<&ferron_common::config::ServerConfiguration>, is_https: bool) -> Option<u16> {
85  let fallback = if is_https { 443 } else { 80 };
86  config
87    .and_then(|c| {
88      if is_https {
89        get_entry!("default_https_port", c)
90      } else {
91        get_entry!("default_http_port", c)
92      }
93    })
94    .and_then(|e| e.values.first())
95    .map_or(Some(fallback), |v| {
96      if v.is_null() {
97        None
98      } else {
99        Some(v.as_i128().unwrap_or(fallback as i128) as u16)
100      }
101    })
102}
103
104/// Resolves the SNI hostname from the given filters.
105pub fn resolve_sni_hostname(filters: &ServerConfigurationFilters) -> Option<String> {
106  filters.hostname.clone().or_else(|| {
107    if filters.ip.is_some_and(|ip| ip.is_loopback()) {
108      // Host blocks with "localhost" specified will have "localhost" SNI hostname
109      return Some("localhost".to_string());
110    }
111
112    // !!! UNTESTED, many clients don't send SNI hostname when accessing via IP address anyway
113    match filters.ip {
114      Some(IpAddr::V4(addr)) => Some(addr.to_string()),
115      Some(IpAddr::V6(addr)) => Some(format!("[{addr}]")),
116      None => None,
117    }
118  })
119}
120
121/// Ensures that a TLS SNI resolver exists for the given port.
122///
123/// If the resolver does not already exist, it is created along with its
124/// associated resolver lock and inserted into the context.
125///
126/// Returns a mutable reference to the resolver for further configuration.
127fn ensure_tls_port_resolver(ctx: &mut TlsBuildContext, port: u16) -> &mut CustomSniResolver {
128  ctx.tls_ports.entry(port).or_insert_with(|| {
129    let list = Arc::new(RwLock::new(HostnameRadixTree::new()));
130    ctx.tls_port_locks.insert(port, list.clone());
131    CustomSniResolver::with_resolvers(list)
132  })
133}
134
135/// Configures a manually provided TLS certificate and private key.
136///
137/// This function:
138/// - Loads and validates the certificate and private key
139/// - Registers the certificate for preloading
140/// - Installs an SNI resolver (or fallback resolver) for the given port
141///
142/// Manual TLS always takes precedence over automatic TLS.
143pub fn handle_manual_tls(
144  ctx: &mut TlsBuildContext,
145  crypto_provider: &CryptoProvider,
146  port: u16,
147  sni_hostname: Option<String>,
148  cert_path: &str,
149  key_path: &str,
150) -> anyhow::Result<()> {
151  let certs = load_certs(cert_path).map_err(|e| anyhow::anyhow!("Cannot load certificate {cert_path}: {e}"))?;
152
153  let key = load_private_key(key_path).map_err(|e| anyhow::anyhow!("Cannot load key {key_path}: {e}"))?;
154
155  let signing_key = crypto_provider
156    .key_provider
157    .load_private_key(key)
158    .map_err(|e| anyhow::anyhow!("Invalid private key {key_path}: {e}"))?;
159
160  let certified_key = Arc::new(CertifiedKey::new(certs, signing_key));
161
162  ctx
163    .certified_keys_to_preload
164    .entry(port)
165    .or_default()
166    .push(certified_key.clone());
167
168  let resolver = Arc::new(OneCertifiedKeyResolver::new(certified_key));
169  let sni_resolver = ensure_tls_port_resolver(ctx, port);
170
171  match &sni_hostname {
172    Some(host) => sni_resolver.load_host_resolver(host, resolver),
173    None => sni_resolver.load_fallback_resolver(resolver),
174  }
175
176  ctx.used_sni_hostnames.insert((port, sni_hostname));
177  Ok(())
178}
179
180/// Parses ACME challenge type from server configuration.
181fn parse_challenge_type(
182  server: &ferron_common::config::ServerConfiguration,
183) -> anyhow::Result<(ChallengeType, HashMap<String, String>)> {
184  let entry = get_entry!("auto_tls_challenge", server);
185
186  let ty = entry
187    .and_then(|e| e.values.first())
188    .and_then(|v| v.as_str())
189    .unwrap_or("tls-alpn-01")
190    .to_uppercase();
191
192  let params = entry
193    .map(|e| {
194      e.props
195        .iter()
196        .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
197        .collect()
198    })
199    .unwrap_or_default();
200
201  let challenge = match ty.as_str() {
202    "HTTP-01" => ChallengeType::Http01,
203    "TLS-ALPN-01" => ChallengeType::TlsAlpn01,
204    "DNS-01" => ChallengeType::Dns01,
205    _ => anyhow::bail!("Unsupported ACME challenge type: {ty}"),
206  };
207
208  Ok((challenge, params))
209}
210
211/// Checks if the server should be skipped.
212pub fn should_skip_server(server: &ferron_common::config::ServerConfiguration) -> bool {
213  server.filters.is_global_non_host() || (server.filters.is_global() && server.entries.is_empty())
214}
215
216/// Obtains the certificate and key for a manual TLS entry in server configuration.
217pub fn manual_tls_entry(server: &ferron_common::config::ServerConfiguration) -> Option<(&str, &str)> {
218  let tls_entry = get_entry!("tls", server)?;
219
220  if tls_entry.values.len() != 2 {
221    return None;
222  }
223
224  let cert = tls_entry.values[0].as_str()?;
225  let key = tls_entry.values[1].as_str()?;
226
227  Some((cert, key))
228}
229
230/// Handles non-encrypted ports for a server configuration.
231pub fn handle_nonencrypted_ports(
232  ctx: &mut TlsBuildContext,
233  server: &ferron_common::config::ServerConfiguration,
234  default_http_port: Option<u16>,
235) {
236  // If TLS is explicitly configured, don't add HTTP
237  if get_entry!("tls", server).is_some() {
238    return;
239  }
240
241  // If automatic TLS is explicitly enabled, HTTP is usually disabled
242  if get_entry!("auto_tls", server)
243    .and_then(|e| e.values.first())
244    .and_then(|v| v.as_bool())
245    .unwrap_or(false)
246  {
247    return;
248  }
249
250  if let Some(port) = server.filters.port.or(default_http_port) {
251    ctx.nonencrypted_ports.insert(port);
252  }
253}
254
255/// Configures automatic TLS (ACME) for a server configuration.
256///
257/// Depending on the server configuration, this function may:
258/// - Configure eager (startup-time) ACME
259/// - Configure on-demand ACME
260/// - Skip automatic TLS if required conditions are not met
261///
262/// This function does not perform ACME issuance itself; it only wires the
263/// required resolvers and configuration objects.
264pub fn handle_automatic_tls(
265  ctx: &mut TlsBuildContext,
266  server: &ferron_common::config::ServerConfiguration,
267  port: u16,
268  sni_hostname: Option<String>,
269  crypto_provider: Arc<CryptoProvider>,
270  memory_acme_account_cache_data: Arc<RwLock<HashMap<String, Vec<u8>>>>,
271) -> anyhow::Result<Option<LogMessage>> {
272  let on_demand = get_entry!("auto_tls_on_demand", server)
273    .and_then(|e| e.values.first())
274    .and_then(|v| v.as_bool())
275    .unwrap_or(false);
276
277  // Reject IP-based identifiers
278  if let Some(host) = &sni_hostname {
279    if host.parse::<IpAddr>().is_ok() {
280      return Ok(Some(LogMessage::new(
281        format!(
282          "Ferron's automatic TLS functionality doesn't support IP address-based identifiers, \
283            skipping SNI host \"{host}\"..."
284        ),
285        true,
286      )));
287    }
288  }
289
290  // Automatic TLS requires SNI unless global
291  if sni_hostname.is_none() && !server.filters.is_global() && !server.filters.is_global_non_host() {
292    return Ok(Some(LogMessage::new(
293      "Skipping automatic TLS for a host without a SNI hostname...".to_string(),
294      true,
295    )));
296  }
297
298  let (challenge_type, challenge_params) = parse_challenge_type(server)?;
299
300  if let Some(sni_hostname) = &sni_hostname {
301    let is_wildcard_domain = sni_hostname.starts_with("*.");
302    if is_wildcard_domain && !on_demand {
303      match &challenge_type {
304        ChallengeType::Http01 => {
305          return Ok(Some(LogMessage::new(
306            format!(
307              "HTTP-01 ACME challenge doesn't support wildcard hostnames, skipping SNI host \"{sni_hostname}\"..."
308            ),
309            true,
310          )));
311        }
312        ChallengeType::TlsAlpn01 => {
313          return Ok(Some(LogMessage::new(
314            format!(
315              "TLS-ALPN-01 ACME challenge doesn't support wildcard hostnames, skipping SNI host \"{sni_hostname}\"..."
316            ),
317            true,
318          )));
319        }
320        _ => (),
321      }
322    }
323  }
324
325  // DNS provider only applies to DNS-01
326  let dns_provider = if challenge_type == ChallengeType::Dns01 {
327    let provider = challenge_params
328      .get("provider")
329      .ok_or_else(|| anyhow::anyhow!("DNS-01 challenge requires a provider"))?;
330    Some(ferron_load_modules::get_dns_provider(provider, &challenge_params).map_err(|e| anyhow::anyhow!(e))?)
331  } else {
332    None
333  };
334
335  if on_demand {
336    build_on_demand_acme(
337      ctx,
338      server,
339      port,
340      sni_hostname,
341      challenge_type,
342      dns_provider,
343      crypto_provider,
344    )?;
345  } else if let Some(host) = sni_hostname {
346    build_eager_acme(
347      ctx,
348      server,
349      port,
350      host,
351      challenge_type,
352      dns_provider,
353      crypto_provider,
354      memory_acme_account_cache_data,
355    )?;
356  }
357
358  Ok(None)
359}
360
361/// Builds an on-demand ACME configuration.
362///
363/// On-demand ACME defers certificate issuance until a client connects and
364/// requests a hostname that does not yet have a certificate. This is typically
365/// used for wildcard or dynamic hostnames.
366fn build_on_demand_acme(
367  ctx: &mut TlsBuildContext,
368  server: &ferron_common::config::ServerConfiguration,
369  port: u16,
370  sni_hostname: Option<String>,
371  challenge_type: ChallengeType,
372  dns_provider: Option<Arc<dyn ferron_common::dns::DnsProvider + Send + Sync>>,
373  crypto_provider: Arc<CryptoProvider>,
374) -> anyhow::Result<()> {
375  // TLS-ALPN-01 requires a dedicated resolver
376  if challenge_type == ChallengeType::TlsAlpn01 {
377    let resolver_list = Arc::new(RwLock::new(Vec::new()));
378    ctx.acme_tls_alpn_01_resolver_locks.insert(port, resolver_list.clone());
379
380    ctx
381      .acme_tls_alpn_01_resolvers
382      .insert(port, TlsAlpn01Resolver::with_resolvers(resolver_list));
383  }
384
385  // Install fallback sender into SNI resolver
386  let fallback_sender = ctx.acme_on_demand_tx.clone();
387  let sni_resolver = ensure_tls_port_resolver(ctx, port);
388  sni_resolver.load_fallback_sender(fallback_sender, port);
389
390  let rustls_client_config =
391    super::acme::build_rustls_client_config(server, crypto_provider).map_err(|e| anyhow::anyhow!(e))?;
392
393  let config = AcmeOnDemandConfig {
394    rustls_client_config,
395    challenge_type,
396    contact: get_entry!("auto_tls_contact", server)
397      .and_then(|e| e.values.first())
398      .and_then(|v| v.as_str())
399      .map(|c| vec![format!("mailto:{c}")])
400      .unwrap_or_default(),
401    directory: super::acme::resolve_acme_directory(server),
402    eab_key: super::acme::parse_eab(server)?,
403    profile: get_entry!("auto_tls_profile", server)
404      .and_then(|e| e.values.first())
405      .and_then(|v| v.as_str())
406      .map(str::to_string),
407    cache_path: super::acme::resolve_acme_cache_path(server)?,
408    sni_resolver_lock: ctx
409      .tls_port_locks
410      .get(&port)
411      .cloned()
412      .unwrap_or_else(|| Arc::new(RwLock::new(HostnameRadixTree::new()))),
413    tls_alpn_01_resolver_lock: ctx
414      .acme_tls_alpn_01_resolver_locks
415      .get(&port)
416      .cloned()
417      .unwrap_or_else(|| Arc::new(RwLock::new(Vec::new()))),
418    http_01_resolver_lock: ctx.acme_http_01_resolvers.clone(),
419    dns_provider,
420    sni_hostname,
421    port,
422  };
423
424  ctx.acme_on_demand_configs.push(config);
425  ctx.automatic_tls_used_sni_hostnames.insert((port, None));
426
427  Ok(())
428}
429
430/// Builds an eager (startup-time) ACME configuration.
431///
432/// Eager ACME requests and maintains certificates proactively at startup,
433/// before any client traffic is received. This is typically used for known
434/// hostnames and static configurations.
435#[allow(clippy::too_many_arguments)]
436fn build_eager_acme(
437  ctx: &mut TlsBuildContext,
438  server: &ferron_common::config::ServerConfiguration,
439  port: u16,
440  sni_hostname: String,
441  challenge_type: ChallengeType,
442  dns_provider: Option<Arc<dyn ferron_common::dns::DnsProvider + Send + Sync>>,
443  crypto_provider: Arc<CryptoProvider>,
444  memory_acme_account_cache_data: Arc<RwLock<HashMap<String, Vec<u8>>>>,
445) -> anyhow::Result<()> {
446  let certified_key_lock = Arc::new(RwLock::new(None));
447  let tls_alpn_01_data_lock = Arc::new(RwLock::new(None));
448  let http_01_data_lock = Arc::new(RwLock::new(None));
449
450  let rustls_client_config =
451    super::acme::build_rustls_client_config(server, crypto_provider).map_err(|e| anyhow::anyhow!(e))?;
452  let (account_cache_path, certificate_cache_path) = super::acme::resolve_cache_paths(server, port, &sni_hostname)?;
453
454  let save_paths = get_entry!("auto_tls_save_data", server).and_then(|e| {
455    e.values
456      .first()
457      .and_then(|v| v.as_str())
458      .and_then(|v| PathBuf::from_str(v).ok())
459      .and_then(|v| {
460        e.values
461          .get(1)
462          .and_then(|v| v.as_str())
463          .and_then(|v| PathBuf::from_str(v).ok())
464          .map(|v2| (v, v2))
465      })
466  });
467
468  let acme_config = AcmeConfig {
469    rustls_client_config,
470    domains: vec![sni_hostname.clone()],
471    challenge_type: challenge_type.clone(),
472    contact: get_entry!("auto_tls_contact", server)
473      .and_then(|e| e.values.first())
474      .and_then(|v| v.as_str())
475      .map(|c| vec![format!("mailto:{c}")])
476      .unwrap_or_default(),
477    directory: super::acme::resolve_acme_directory(server),
478    eab_key: super::acme::parse_eab(server)?,
479    profile: get_entry!("auto_tls_profile", server)
480      .and_then(|e| e.values.first())
481      .and_then(|v| v.as_str())
482      .map(str::to_string),
483    account_cache: if let Some(account_cache_path) = account_cache_path {
484      AcmeCache::File(account_cache_path)
485    } else {
486      AcmeCache::Memory(memory_acme_account_cache_data)
487    },
488    certificate_cache: if let Some(certificate_cache_path) = certificate_cache_path {
489      AcmeCache::File(certificate_cache_path)
490    } else {
491      AcmeCache::Memory(Default::default())
492    },
493    certified_key_lock: certified_key_lock.clone(),
494    tls_alpn_01_data_lock: tls_alpn_01_data_lock.clone(),
495    http_01_data_lock: http_01_data_lock.clone(),
496    dns_provider,
497    renewal_info: None,
498    account: None,
499    save_paths,
500    post_obtain_command: get_entry!("auto_tls_post_obtain_command", server)
501      .and_then(|e| e.values.first())
502      .and_then(|v| v.as_str())
503      .map(str::to_string),
504  };
505
506  ctx.acme_configs.push(acme_config);
507
508  // Wire challenge resolvers
509  match challenge_type {
510    ChallengeType::Http01 => {
511      ctx.acme_http_01_resolvers.blocking_write().push(http_01_data_lock);
512    }
513    ChallengeType::TlsAlpn01 => {
514      let resolver = ctx.acme_tls_alpn_01_resolvers.entry(port).or_insert_with(|| {
515        let list = Arc::new(RwLock::new(Vec::new()));
516        ctx.acme_tls_alpn_01_resolver_locks.insert(port, list.clone());
517        TlsAlpn01Resolver::with_resolvers(list)
518      });
519
520      resolver.load_resolver(tls_alpn_01_data_lock);
521    }
522    _ => {}
523  }
524
525  // Install SNI resolver
526  let acme_resolver = Arc::new(crate::acme::AcmeResolver::new(certified_key_lock));
527  let sni_resolver = ensure_tls_port_resolver(ctx, port);
528  sni_resolver.load_host_resolver(&sni_hostname, acme_resolver);
529
530  ctx.automatic_tls_used_sni_hostnames.insert((port, Some(sni_hostname)));
531
532  Ok(())
533}