diff options
author | Quentin Dufour <quentin@deuxfleurs.fr> | 2023-11-30 16:53:04 +0100 |
---|---|---|
committer | Quentin Dufour <quentin@deuxfleurs.fr> | 2023-11-30 16:53:04 +0100 |
commit | 753903ee021e7aac7c0914883ae8d547148b4d95 (patch) | |
tree | 0bd8ecb2297f07d5282a25f4a44f293b41f5766f /src/cert_store.rs | |
parent | ca449ebff452345229953b6ba406acefb157385b (diff) | |
download | tricot-753903ee021e7aac7c0914883ae8d547148b4d95.tar.gz tricot-753903ee021e7aac7c0914883ae8d547148b4d95.zip |
implement feature
Diffstat (limited to 'src/cert_store.rs')
-rw-r--r-- | src/cert_store.rs | 169 |
1 files changed, 129 insertions, 40 deletions
diff --git a/src/cert_store.rs b/src/cert_store.rs index e25395d..7c6b4b3 100644 --- a/src/cert_store.rs +++ b/src/cert_store.rs @@ -67,58 +67,73 @@ impl CertStore { let mut t_last_check: HashMap<String, Instant> = HashMap::new(); - loop { - let mut domains: HashSet<String> = HashSet::new(); + // Collect data from proxy config + let mut static_domains: HashSet<String> = HashSet::new(); + let mut on_demand_checks: Vec<(glob::Pattern, Option<String>)> = vec![]; - // Collect domains that need a TLS certificate - // either from the proxy configuration (eagerly) - // or on reaction to a user request (lazily) - select! { + loop { + // Collect domains that need a TLS certificate + // either from the proxy configuration (eagerly) + // or on reaction to a user request (lazily) + let domains = select! { res = rx_proxy_config.changed() => { if res.is_err() { bail!("rx_proxy_config closed"); } + on_demand_checks.clear(); let proxy_config: Arc<ProxyConfig> = rx_proxy_config.borrow().clone(); for ent in proxy_config.entries.iter() { - // Eagerly generate certificates for domains that - // are not patterns - if let HostDescription::Hostname(domain) = &ent.url_prefix.host { + // Eagerly generate certificates for domains that + // are not patterns + match &ent.url_prefix.host { + HostDescription::Hostname(domain) => { if let Some((host, _port)) = domain.split_once(':') { - domains.insert(host.to_string()); + static_domains.insert(host.to_string()); + //domains.insert(host.to_string()); } else { - domains.insert(domain.clone()); + static_domains.insert(domain.clone()); + //domains.insert(domain.clone()); } - } - - // @TODO Register a map of - // UrlPrefix -> OnDemandTlsAskCheckUrl + }, + HostDescription::Pattern(pattern) => { + on_demand_checks.push((pattern.clone(), ent.on_demand_tls_ask.clone())); + } + } } + + // only static_domains are refreshed + static_domains.clone() } need_cert = rx_need_cert.recv() => { match need_cert { Some(dom) => { - domains.insert(dom); + let mut candidates: HashSet<String> = HashSet::new(); + + // collect certificates as much as possible + candidates.insert(dom); while let Ok(dom2) = rx_need_cert.try_recv() { - domains.insert(dom2); + candidates.insert(dom2); } + + self.domain_validation(candidates, &static_domains, on_demand_checks.as_slice()).await } None => bail!("rx_need_cert closed"), - }; + } } - } + }; - // Now that we have our list of domains to check, - // actually do something + // Now that we have our list of domains to check, + // actually do something for dom in domains.iter() { - // Exclude from the list domains that were checked less than 60 - // seconds ago + // Exclude from the list domains that were checked less than 60 + // seconds ago match t_last_check.get(dom) { Some(t) if Instant::now() - *t < Duration::from_secs(60) => continue, _ => t_last_check.insert(dom.to_string(), Instant::now()), }; - // Actual Let's Encrypt calls are done here (in sister function) + // Actual Let's Encrypt calls are done here (in sister function) debug!("Checking cert for domain: {}", dom); if let Err(e) = self.check_cert(dom).await { warn!("({}) Could not get certificate: {}", dom, e); @@ -127,6 +142,73 @@ impl CertStore { } } + async fn domain_validation( + &self, + candidates: HashSet<String>, + static_domains: &HashSet<String>, + checks: &[(glob::Pattern, Option<String>)], + ) -> HashSet<String> { + let mut domains: HashSet<String> = HashSet::new(); + + // Filter certificates... + for candidate in candidates.into_iter() { + // Disallow obvious wrong domains... + if !candidate.contains('.') || candidate.ends_with(".local") { + warn!("Probably not a publicly accessible domain, skipping (a self-signed certificate will be used)"); + continue; + } + + // Try to register domain as a static domain + if static_domains.contains(&candidate) { + trace!("domain {} validated as static domain", candidate); + domains.insert(candidate); + continue; + } + + // It's not a static domain, maybe an on-demand domain? + for (pattern, maybe_check_url) in checks.iter() { + // check glob pattern + if pattern.matches(&candidate) { + // if no check url is set, accept domain as long as it matches the pattern + let check_url = match maybe_check_url { + None => { + trace!( + "domain {} validated on glob pattern {} only", + candidate, + pattern + ); + domains.insert(candidate); + break; + } + Some(url) => url, + }; + + // if a check url is set, call it + match self.on_demand_tls_ask(check_url, &candidate).await { + Ok(()) => { + trace!( + "domain {} validated on glob pattern {} and on check url {}", + candidate, + pattern, + check_url + ); + domains.insert(candidate); + break; + } + Err(e) => { + warn!("domain {} validation refused on glob pattern {} and on check url {} with error: {}", candidate, pattern, check_url, e); + } + } + } + } + // Avoid DDoSing a backend + tokio::time::sleep(Duration::from_secs(2)).await; + } + + return domains; + } + + /// This function is also in charge of the refresh of the domain names fn get_cert_for_https(self: &Arc<Self>, domain: &str) -> Result<Arc<Cert>> { // Check if domain is authorized if !self @@ -199,15 +281,15 @@ impl CertStore { Ok(()) } - /// Check certificate ensure that the certificate is in the memory store - /// and that it does not need to be renewed. - /// - /// If it's not in the memory store, it tries to load it from Consul, - /// if it's not in Consul, it calls Let's Encrypt. - /// - /// If the certificate is outdated in the memory store, it tries to load - /// a more recent version in Consul, if the Consul version is also outdated, - /// it tries to renew it + /// Check certificate ensure that the certificate is in the memory store + /// and that it does not need to be renewed. + /// + /// If it's not in the memory store, it tries to load it from Consul, + /// if it's not in Consul, it calls Let's Encrypt. + /// + /// If the certificate is outdated in the memory store, it tries to load + /// a more recent version in Consul, if the Consul version is also outdated, + /// it tries to renew it pub async fn check_cert(self: &Arc<Self>, domain: &str) -> Result<()> { // First, try locally. { @@ -248,16 +330,10 @@ impl CertStore { self.renew_cert(domain).await } - /// This is the place where certificates are generated or renewed + /// This is the place where certificates are generated or renewed pub async fn renew_cert(self: &Arc<Self>, domain: &str) -> Result<()> { info!("({}) Renewing certificate", domain); - // Basic sanity check (we could add more kinds of checks here) - // This is just to help avoid getting rate-limited against ACME server - if !domain.contains('.') || domain.ends_with(".local") { - bail!("Probably not a publicly accessible domain, skipping (a self-signed certificate will be used)"); - } - // ---- Acquire lock ---- // the lock is acquired for half an hour, // so that in case of an error we won't retry before @@ -373,6 +449,19 @@ impl CertStore { Ok(()) } + async fn on_demand_tls_ask(&self, check_url: &str, domain: &str) -> Result<()> { + let httpcli = reqwest::Client::new(); + let chall_url = format!("{}?domain={}", check_url, domain); + info!("({}) On-demand TLS check", domain); + + let httpresp = httpcli.get(&chall_url).send().await?; + if httpresp.status() != reqwest::StatusCode::OK { + bail!("{} is not authorized for on-demand TLS", domain); + } + + Ok(()) + } + async fn check_domain_accessibility(&self, domain: &str, session: &str) -> Result<()> { // Returns Ok(()) only if domain is a correct domain name that // redirects to this server |