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.
This commit is contained in:
1
packages/tauri-app/Cargo.lock
generated
1
packages/tauri-app/Cargo.lock
generated
@@ -612,6 +612,7 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"which",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -101,11 +101,20 @@ pub fn ensure_local_cert() -> Result<LocalCert, String> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<PathBuf, String> {
|
||||
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(())
|
||||
}
|
||||
|
||||
88
packages/tauri-app/src-tauri/src/linux_tls.rs
Normal file
88
packages/tauri-app/src-tauri/src/linux_tls.rs
Normal file
@@ -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::<AppState>();
|
||||
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::<AppState>();
|
||||
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
|
||||
}
|
||||
@@ -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<Option<KeepAwake>>,
|
||||
pub zoom_level: Mutex<f64>,
|
||||
pub remote_origins: Mutex<HashMap<String, String>>,
|
||||
pub remote_skip_tls_verify: Mutex<HashMap<String, bool>>,
|
||||
pub remote_tls_handlers: Mutex<HashSet<String>>,
|
||||
pub remote_proxies: Mutex<HashMap<String, RemoteProxyHandle>>,
|
||||
}
|
||||
|
||||
@@ -174,7 +182,7 @@ fn intercept_navigation<R: Runtime>(webview: &Webview<R>, url: &Url) -> bool {
|
||||
async fn open_remote_window_impl(
|
||||
app: AppHandle,
|
||||
payload: RemoteWindowPayload,
|
||||
tls_config: Option<ProxyTlsConfig>,
|
||||
_tls_config: Option<ProxyTlsConfig>,
|
||||
) -> 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::<AppState>();
|
||||
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::<AppState>();
|
||||
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::<AppState>()
|
||||
.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::<AppState>()
|
||||
.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::<AppState>().remote_origins.lock() {
|
||||
origins.remove(&label_for_cleanup);
|
||||
}
|
||||
if let Ok(mut values) = app_handle.state::<AppState>().remote_skip_tls_verify.lock() {
|
||||
values.remove(&label_for_cleanup);
|
||||
}
|
||||
if let Ok(mut handlers) = app_handle.state::<AppState>().remote_tls_handlers.lock() {
|
||||
handlers.remove(&label_for_cleanup);
|
||||
}
|
||||
if let Ok(mut proxies) = app_handle.state::<AppState>().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| {
|
||||
|
||||
Reference in New Issue
Block a user