From be4f3836021a1083b11dd82394d592665f47b054 Mon Sep 17 00:00:00 2001 From: pascalandr Date: Sat, 18 Apr 2026 09:30:38 +0200 Subject: [PATCH] fix(desktop): allow self-signed remote HTTPS on Linux Use WebKitGTK TLS exceptions for remote windows so Linux no longer depends on system CA installation or sudo-managed trust stores. --- packages/tauri-app/Cargo.lock | 1 + packages/tauri-app/src-tauri/Cargo.toml | 3 + .../tauri-app/src-tauri/src/cert_manager.rs | 26 ++-- packages/tauri-app/src-tauri/src/linux_tls.rs | 88 ++++++++++++++ packages/tauri-app/src-tauri/src/main.rs | 114 +++++++++++++----- 5 files changed, 192 insertions(+), 40 deletions(-) create mode 100644 packages/tauri-app/src-tauri/src/linux_tls.rs diff --git a/packages/tauri-app/Cargo.lock b/packages/tauri-app/Cargo.lock index 1bcdb190..6e430c97 100644 --- a/packages/tauri-app/Cargo.lock +++ b/packages/tauri-app/Cargo.lock @@ -612,6 +612,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "url", + "webkit2gtk", "which", "windows-sys 0.59.0", ] diff --git a/packages/tauri-app/src-tauri/Cargo.toml b/packages/tauri-app/src-tauri/Cargo.toml index 63aa7899..8433b7b0 100644 --- a/packages/tauri-app/src-tauri/Cargo.toml +++ b/packages/tauri-app/src-tauri/Cargo.toml @@ -38,3 +38,6 @@ tauri-plugin-notification = "2" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security_Cryptography", "Win32_UI_Shell", "Win32_Security", "Win32_System_JobObjects"] } + +[target.'cfg(target_os = "linux")'.dependencies] +webkit2gtk = "2.0.2" diff --git a/packages/tauri-app/src-tauri/src/cert_manager.rs b/packages/tauri-app/src-tauri/src/cert_manager.rs index f77de377..9cfc9a7a 100644 --- a/packages/tauri-app/src-tauri/src/cert_manager.rs +++ b/packages/tauri-app/src-tauri/src/cert_manager.rs @@ -101,11 +101,20 @@ pub fn ensure_local_cert() -> Result { }) } -/// Returns true if the certificate has already been added to the Windows trust -/// store (indicated by the `.trusted` marker file). +fn trusted_marker_path() -> Result { + Ok(cert_dir()?.join(TRUSTED_MARKER)) +} + +fn write_trusted_marker() -> Result<(), String> { + fs::write(trusted_marker_path()?, "trusted") + .map_err(|e| format!("Failed to write trust marker: {e}")) +} + +/// Returns true if the certificate has already been added to the OS trust store +/// (indicated by the `.trusted` marker file). pub fn is_cert_trusted() -> bool { - cert_dir() - .map(|dir| dir.join(TRUSTED_MARKER).exists()) + trusted_marker_path() + .map(|path| path.exists()) .unwrap_or(false) } @@ -151,17 +160,12 @@ pub fn trust_cert_in_store(cert_der: &[u8]) -> Result<(), String> { } } - // Write marker file - let dir = cert_dir()?; - fs::write(dir.join(TRUSTED_MARKER), "trusted") - .map_err(|e| format!("Failed to write trust marker: {e}"))?; - + write_trusted_marker()?; Ok(()) } #[cfg(not(windows))] pub fn trust_cert_in_store(_cert_der: &[u8]) -> Result<(), String> { - // On non-Windows platforms, certificate trust is not yet implemented. - // The proxy will still work but the browser may show a warning. + // Non-Windows platforms use native webview-specific handling instead of OS trust-store writes. Ok(()) } diff --git a/packages/tauri-app/src-tauri/src/linux_tls.rs b/packages/tauri-app/src-tauri/src/linux_tls.rs new file mode 100644 index 00000000..abbb7773 --- /dev/null +++ b/packages/tauri-app/src-tauri/src/linux_tls.rs @@ -0,0 +1,88 @@ +use crate::AppState; +use tauri::{AppHandle, Manager, WebviewWindow}; +use url::Url; +use webkit2gtk::{WebContextExt, WebView, WebViewExt}; + +pub fn should_bootstrap_tls_navigation(target_url: &Url, skip_tls_verify: bool) -> bool { + skip_tls_verify && target_url.scheme() == "https" +} + +pub fn ensure_remote_window_tls_handler( + window: &WebviewWindow, + app_handle: &AppHandle, + window_label: &str, +) -> Result<(), String> { + { + let state = app_handle.state::(); + let mut handlers = state + .remote_tls_handlers + .lock() + .map_err(|err| err.to_string())?; + if !handlers.insert(window_label.to_string()) { + return Ok(()); + } + } + + let app_handle = app_handle.clone(); + let window_label = window_label.to_string(); + window + .with_webview(move |platform_webview| { + let webview = platform_webview.inner(); + let app_handle = app_handle.clone(); + let window_label = window_label.clone(); + webview.connect_load_failed_with_tls_errors(move |view, failing_uri, certificate, _| { + allow_remote_tls_certificate( + &app_handle, + &window_label, + view, + failing_uri, + certificate, + ) + }); + }) + .map_err(|err| err.to_string()) +} + +fn allow_remote_tls_certificate( + app_handle: &AppHandle, + window_label: &str, + view: &WebView, + failing_uri: &str, + certificate: &webkit2gtk::gio::TlsCertificate, +) -> bool { + let Ok(parsed_uri) = Url::parse(failing_uri) else { + return false; + }; + let Some(host) = parsed_uri.host_str() else { + return false; + }; + + let state = app_handle.state::(); + let skip_tls_verify = state + .remote_skip_tls_verify + .lock() + .ok() + .and_then(|values| values.get(window_label).copied()) + .unwrap_or(false); + if !skip_tls_verify { + return false; + } + + let expected_origin = state + .remote_origins + .lock() + .ok() + .and_then(|origins| origins.get(window_label).cloned()); + let parsed_origin = parsed_uri.origin().ascii_serialization(); + if expected_origin.as_deref() != Some(parsed_origin.as_str()) { + return false; + } + + let Some(context) = view.context() else { + return false; + }; + + context.allow_tls_certificate_for_host(certificate, host); + view.load_uri(failing_uri); + true +} diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs index 8c884017..721ddfea 100644 --- a/packages/tauri-app/src-tauri/src/main.rs +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -1,15 +1,21 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +#[cfg_attr(target_os = "linux", allow(dead_code))] mod cert_manager; mod cli_manager; +#[cfg(target_os = "linux")] +mod linux_tls; +#[cfg_attr(target_os = "linux", allow(dead_code))] mod remote_proxy; use cli_manager::{CliProcessManager, CliStatus}; use keepawake::KeepAwake; -use remote_proxy::{start_remote_proxy, ProxyTlsConfig, RemoteProxyHandle}; +#[cfg(not(target_os = "linux"))] +use remote_proxy::start_remote_proxy; +use remote_proxy::{ProxyTlsConfig, RemoteProxyHandle}; use serde::Deserialize; use serde_json::json; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; @@ -48,6 +54,8 @@ pub struct AppState { pub wake_lock: Mutex>, pub zoom_level: Mutex, pub remote_origins: Mutex>, + pub remote_skip_tls_verify: Mutex>, + pub remote_tls_handlers: Mutex>, pub remote_proxies: Mutex>, } @@ -174,7 +182,7 @@ fn intercept_navigation(webview: &Webview, url: &Url) -> bool { async fn open_remote_window_impl( app: AppHandle, payload: RemoteWindowPayload, - tls_config: Option, + _tls_config: Option, ) -> Result<(), String> { let parsed = Url::parse(&payload.base_url).map_err(|err| err.to_string())?; let label = format!("remote-{}", payload.id); @@ -184,41 +192,55 @@ async fn open_remote_window_impl( parsed.host_str().unwrap_or(payload.base_url.as_str()) ); - let state = app.state::(); - let reuses_existing_proxy = { - let proxies = state.remote_proxies.lock().map_err(|err| err.to_string())?; - proxies - .get(&label) - .map(|existing| existing.matches(&parsed, payload.skip_tls_verify)) - .unwrap_or(false) - }; + #[cfg(target_os = "linux")] + let window_url = parsed.clone(); - let local_url = if reuses_existing_proxy { - let proxies = state.remote_proxies.lock().map_err(|err| err.to_string())?; - proxies - .get(&label) - .map(|handle| handle.entry_url().clone()) - .ok_or_else(|| "Remote proxy disappeared before reuse".to_string())? - } else { - let new_proxy = - start_remote_proxy(parsed.clone(), payload.skip_tls_verify, tls_config).await?; - let local_url = new_proxy.entry_url().clone(); - let mut proxies = state.remote_proxies.lock().map_err(|err| err.to_string())?; - if let Some(existing) = proxies.remove(&label) { - existing.shutdown(); + #[cfg(not(target_os = "linux"))] + let window_url = { + let state = app.state::(); + let reuses_existing_proxy = { + let proxies = state.remote_proxies.lock().map_err(|err| err.to_string())?; + proxies + .get(&label) + .map(|existing| existing.matches(&parsed, payload.skip_tls_verify)) + .unwrap_or(false) + }; + + if reuses_existing_proxy { + let proxies = state.remote_proxies.lock().map_err(|err| err.to_string())?; + proxies + .get(&label) + .map(|handle| handle.entry_url().clone()) + .ok_or_else(|| "Remote proxy disappeared before reuse".to_string())? + } else { + let new_proxy = + start_remote_proxy(parsed.clone(), payload.skip_tls_verify, _tls_config).await?; + let local_url = new_proxy.entry_url().clone(); + let mut proxies = state.remote_proxies.lock().map_err(|err| err.to_string())?; + if let Some(existing) = proxies.remove(&label) { + existing.shutdown(); + } + proxies.insert(label.clone(), new_proxy); + local_url } - proxies.insert(label.clone(), new_proxy); - local_url }; app.state::() .remote_origins .lock() .map_err(|err| err.to_string())? - .insert(label.clone(), local_url.origin().ascii_serialization()); + .insert(label.clone(), window_url.origin().ascii_serialization()); + app.state::() + .remote_skip_tls_verify + .lock() + .map_err(|err| err.to_string())? + .insert(label.clone(), payload.skip_tls_verify); if let Some(existing) = app.get_webview_window(&label) { - let _ = existing.navigate(local_url.clone()); + #[cfg(target_os = "linux")] + linux_tls::ensure_remote_window_tls_handler(&existing, &app, &label)?; + + let _ = existing.navigate(window_url.clone()); let _ = existing.set_title(&title); let _ = existing.show(); let _ = existing.unminimize(); @@ -226,13 +248,32 @@ async fn open_remote_window_impl( return Ok(()); } - let window = WebviewWindowBuilder::new(&app, label.clone(), WebviewUrl::External(local_url)) + #[cfg(target_os = "linux")] + let initial_url = if linux_tls::should_bootstrap_tls_navigation(&window_url, payload.skip_tls_verify) + { + Url::parse("about:blank").map_err(|err| err.to_string())? + } else { + window_url.clone() + }; + + #[cfg(not(target_os = "linux"))] + let initial_url = window_url.clone(); + + let window = WebviewWindowBuilder::new(&app, label.clone(), WebviewUrl::External(initial_url.clone())) .title(title) .inner_size(1400.0, 900.0) .min_inner_size(800.0, 600.0) .build() .map_err(|err| err.to_string())?; + #[cfg(target_os = "linux")] + { + linux_tls::ensure_remote_window_tls_handler(&window, &app, &label)?; + if initial_url != window_url { + let _ = window.navigate(window_url.clone()); + } + } + let app_handle = app.clone(); let label_for_cleanup = label.clone(); window.on_window_event(move |event| { @@ -240,6 +281,12 @@ async fn open_remote_window_impl( if let Ok(mut origins) = app_handle.state::().remote_origins.lock() { origins.remove(&label_for_cleanup); } + if let Ok(mut values) = app_handle.state::().remote_skip_tls_verify.lock() { + values.remove(&label_for_cleanup); + } + if let Ok(mut handlers) = app_handle.state::().remote_tls_handlers.lock() { + handlers.remove(&label_for_cleanup); + } if let Ok(mut proxies) = app_handle.state::().remote_proxies.lock() { if let Some(handle) = proxies.remove(&label_for_cleanup) { handle.shutdown(); @@ -253,6 +300,12 @@ async fn open_remote_window_impl( #[tauri::command] async fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<(), String> { + #[cfg(target_os = "linux")] + { + return open_remote_window_impl(app, payload, None).await; + } + + #[cfg(not(target_os = "linux"))] let tls_config = match cert_manager::ensure_local_cert() { Ok(local_cert) => { if let Err(err) = cert_manager::trust_cert_in_store(&local_cert.cert_der) { @@ -269,6 +322,7 @@ async fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Res } }; + #[cfg(not(target_os = "linux"))] open_remote_window_impl(app, payload, tls_config).await } @@ -428,6 +482,8 @@ fn main() { wake_lock: Mutex::new(None), zoom_level: Mutex::new(DEFAULT_ZOOM_LEVEL), remote_origins: Mutex::new(HashMap::new()), + remote_skip_tls_verify: Mutex::new(HashMap::new()), + remote_tls_handlers: Mutex::new(HashSet::new()), remote_proxies: Mutex::new(HashMap::new()), }) .setup(|app| {