From 5733a783fb4d2d6306ef07d7c94bd6438eaaabfd Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Thu, 28 May 2026 01:37:16 +0200 Subject: [PATCH] feat: follow certificate check parameter in autoconfig --- src/configure.rs | 7 +++ src/configure/auto_mozilla.rs | 5 +- src/configure/auto_outlook.rs | 5 +- src/login_param.rs | 9 ++++ src/net.rs | 1 + src/net/http.rs | 95 +++++++++++++++++++++-------------- 6 files changed, 81 insertions(+), 41 deletions(-) diff --git a/src/configure.rs b/src/configure.rs index e070ce499..9106f4084 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -680,6 +680,8 @@ async fn get_autoconfig( param: &EnteredLoginParam, param_domain: &str, ) -> Option> { + let accept_invalid_certificates = param.certificate_checks.accept_invalid_certificates(); + // Make sure to not encode `.` as `%2E` here. // Some servers like murena.io on 2024-11-01 produce incorrect autoconfig XML // when address is encoded. @@ -696,6 +698,7 @@ async fn get_autoconfig( "https://autoconfig.{param_domain}/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}" ), ¶m.addr, + accept_invalid_certificates, ) .await { @@ -710,6 +713,7 @@ async fn get_autoconfig( "https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}" ), ¶m.addr, + accept_invalid_certificates, ) .await { @@ -721,6 +725,7 @@ async fn get_autoconfig( if let Ok(res) = outlk_autodiscover( ctx, format!("https://{param_domain}/autodiscover/autodiscover.xml"), + accept_invalid_certificates, ) .await { @@ -731,6 +736,7 @@ async fn get_autoconfig( if let Ok(res) = outlk_autodiscover( ctx, format!("https://autodiscover.{param_domain}/autodiscover/autodiscover.xml",), + accept_invalid_certificates, ) .await { @@ -743,6 +749,7 @@ async fn get_autoconfig( ctx, &format!("https://autoconfig.thunderbird.net/v1.1/{param_domain}"), ¶m.addr, + accept_invalid_certificates, ) .await { diff --git a/src/configure/auto_mozilla.rs b/src/configure/auto_mozilla.rs index f08aba93c..c74b9b644 100644 --- a/src/configure/auto_mozilla.rs +++ b/src/configure/auto_mozilla.rs @@ -10,7 +10,7 @@ use quick_xml::events::{BytesStart, Event}; use super::{Error, ServerParams}; use crate::context::Context; use crate::log::warn; -use crate::net::read_url; +use crate::net::read_url_with_tls; use crate::provider::{Protocol, Socket}; #[derive(Debug)] @@ -249,8 +249,9 @@ pub(crate) async fn moz_autoconfigure( context: &Context, url: &str, addr: &str, + accept_invalid_certificates: bool, ) -> Result, Error> { - let xml_raw = read_url(context, url).await?; + let xml_raw = read_url_with_tls(context, url, !accept_invalid_certificates).await?; let res = parse_serverparams(addr, &xml_raw); if let Err(err) = &res { diff --git a/src/configure/auto_outlook.rs b/src/configure/auto_outlook.rs index 40bbcfe5e..8db39a612 100644 --- a/src/configure/auto_outlook.rs +++ b/src/configure/auto_outlook.rs @@ -10,7 +10,7 @@ use quick_xml::events::Event; use super::{Error, ServerParams}; use crate::context::Context; use crate::log::warn; -use crate::net::read_url; +use crate::net::read_url_with_tls; use crate::provider::{Protocol, Socket}; /// Result of parsing a single `Protocol` tag. @@ -196,10 +196,11 @@ fn protocols_to_serverparams(protocols: Vec) -> Vec { pub(crate) async fn outlk_autodiscover( context: &Context, mut url: String, + accept_invalid_certificates: bool, ) -> Result, Error> { /* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */ for _i in 0..10 { - let xml_raw = read_url(context, &url).await?; + let xml_raw = read_url_with_tls(context, &url, !accept_invalid_certificates).await?; let res = parse_xml(&xml_raw); if let Err(err) = &res { warn!(context, "{}", err); diff --git a/src/login_param.rs b/src/login_param.rs index 871ad59b2..1b80c3fe3 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -56,6 +56,15 @@ pub enum EnteredCertificateChecks { AcceptInvalidCertificates2 = 3, } +impl EnteredCertificateChecks { + pub(crate) fn accept_invalid_certificates(self) -> bool { + matches!( + self, + Self::AcceptInvalidCertificates | Self::AcceptInvalidCertificates2 + ) + } +} + /// Login parameters for a single IMAP server. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct EnteredImapLoginParam { diff --git a/src/net.rs b/src/net.rs index b96c5dac0..01cf1c03c 100644 --- a/src/net.rs +++ b/src/net.rs @@ -23,6 +23,7 @@ pub(crate) mod session; pub(crate) mod tls; use dns::lookup_host_with_cache; +pub(crate) use http::read_url_with_tls; pub use http::{Response as HttpResponse, read_url, read_url_blob}; use tls::wrap_tls; diff --git a/src/net/http.rs b/src/net/http.rs index 062245f39..1922be7a3 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -13,7 +13,7 @@ use crate::context::Context; use crate::log::warn; use crate::net::proxy::ProxyConfig; use crate::net::session::SessionStream; -use crate::net::tls::wrap_rustls; +use crate::net::tls::wrap_tls; use crate::tools::time; /// User-Agent for HTTP requests if a resource usage policy requires it. @@ -35,7 +35,16 @@ pub struct Response { /// Retrieves the text contents of URL using HTTP GET request. pub async fn read_url(context: &Context, url: &str) -> Result { - let response = read_url_blob(context, url).await?; + read_url_with_tls(context, url, true).await +} + +/// Retrieves the text contents of URL using HTTP GET request. +pub(crate) async fn read_url_with_tls( + context: &Context, + url: &str, + strict_tls: bool, +) -> Result { + let response = read_url_blob_with_tls(context, url, strict_tls).await?; let text = String::from_utf8_lossy(&response.blob); Ok(text.to_string()) } @@ -43,6 +52,7 @@ pub async fn read_url(context: &Context, url: &str) -> Result { async fn get_http_sender( context: &Context, parsed_url: hyper::Uri, + strict_tls: bool, ) -> Result> where B: hyper::body::Body + 'static + Send, @@ -76,37 +86,29 @@ where let port = parsed_url.port_u16().unwrap_or(443); let (use_sni, load_cache) = (true, true); - if let Some(proxy_config) = proxy_config_opt { + let tcp_stream: Box = if let Some(proxy_config) = proxy_config_opt { let proxy_stream = proxy_config .connect(context, host, port, load_cache) .await?; - let tls_stream = wrap_rustls( - host, - port, - use_sni, - "", - proxy_stream, - &context.tls_session_store, - &context.spki_hash_store, - &context.sql, - ) - .await?; - Box::new(tls_stream) + Box::new(proxy_stream) } else { let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?; - let tls_stream = wrap_rustls( - host, - port, - use_sni, - "", - tcp_stream, - &context.tls_session_store, - &context.spki_hash_store, - &context.sql, - ) - .await?; - Box::new(tls_stream) - } + Box::new(tcp_stream) + }; + + let tls_stream = wrap_tls( + strict_tls, + host, + port, + use_sni, + "", + tcp_stream, + &context.tls_session_store, + &context.spki_hash_store, + &context.sql, + ) + .await?; + Box::new(tls_stream) } _ => bail!("Unknown URL scheme"), }; @@ -260,7 +262,7 @@ pub(crate) async fn http_cache_cleanup(context: &Context) -> Result<()> { /// Fetches URL and updates the cache. /// /// URL is fetched regardless of whether there is an existing result in the cache. -async fn fetch_url(context: &Context, original_url: &str) -> Result { +async fn fetch_url(context: &Context, original_url: &str, strict_tls: bool) -> Result { let mut url = original_url.to_string(); // Follow up to 10 http-redirects @@ -269,7 +271,7 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result { .parse::() .with_context(|| format!("Failed to parse URL {url:?}"))?; - let mut sender = get_http_sender(context, parsed_url.clone()).await?; + let mut sender = get_http_sender(context, parsed_url.clone(), strict_tls).await?; let authority = parsed_url .authority() .context("URL has no authority")? @@ -339,8 +341,10 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result { mimetype, encoding, }; - info!(context, "Inserting {original_url:?} into cache."); - http_cache_put(context, &url, &response).await?; + if strict_tls { + info!(context, "Inserting {original_url:?} into cache."); + http_cache_put(context, &url, &response).await?; + } return Ok(response); } @@ -349,6 +353,23 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result { /// Retrieves the binary contents of URL using HTTP GET request. pub async fn read_url_blob(context: &Context, url: &str) -> Result { + read_url_blob_with_tls(context, url, true).await +} + +/// Retrieves the binary contents of URL using HTTP GET request. +pub(crate) async fn read_url_blob_with_tls( + context: &Context, + url: &str, + strict_tls: bool, +) -> Result { + if !strict_tls { + info!( + context, + "Fetching {url:?} without HTTP cache due to relaxed TLS." + ); + return fetch_url(context, url, strict_tls).await; + } + if let Some((response, is_stale)) = http_cache_get(context, url).await? { info!(context, "Returning {url:?} from cache."); if is_stale { @@ -357,7 +378,7 @@ pub async fn read_url_blob(context: &Context, url: &str) -> Result { tokio::spawn(async move { // Fetch URL in background to update the cache. info!(context, "Fetching stale {url:?} in background."); - if let Err(err) = fetch_url(&context, &url).await { + if let Err(err) = fetch_url(&context, &url, true).await { warn!(context, "Failed to revalidate {url:?}: {err:#}."); } }); @@ -366,7 +387,7 @@ pub async fn read_url_blob(context: &Context, url: &str) -> Result { } info!(context, "Not found {url:?} in cache, fetching."); - let response = fetch_url(context, url).await?; + let response = fetch_url(context, url, true).await?; Ok(response) } @@ -384,7 +405,7 @@ pub(crate) async fn post_empty(context: &Context, url: &str) -> Result<(String, bail!("POST requests to non-HTTPS URLs are not allowed"); } - let mut sender = get_http_sender(context, parsed_url.clone()).await?; + let mut sender = get_http_sender(context, parsed_url.clone(), true).await?; let authority = parsed_url .authority() .context("URL has no authority")? @@ -418,7 +439,7 @@ pub(crate) async fn post_string(context: &Context, url: &str, body: String) -> R bail!("POST requests to non-HTTPS URLs are not allowed"); } - let mut sender = get_http_sender(context, parsed_url.clone()).await?; + let mut sender = get_http_sender(context, parsed_url.clone(), true).await?; let authority = parsed_url .authority() .context("URL has no authority")? @@ -449,7 +470,7 @@ pub(crate) async fn post_form( } let encoded_body = serde_urlencoded::to_string(form).context("Failed to encode data")?; - let mut sender = get_http_sender(context, parsed_url.clone()).await?; + let mut sender = get_http_sender(context, parsed_url.clone(), true).await?; let authority = parsed_url .authority() .context("URL has no authority")?