From 016c7bda4a8895633f1d1c077e9b9c38f45de1ec Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 20 Apr 2026 08:49:50 +0100 Subject: [PATCH] fix(tauri): use in-app certificate install confirmation --- packages/tauri-app/src-tauri/src/main.rs | 59 ++++++------------- .../lib/i18n/messages/en/folderSelection.ts | 5 ++ .../lib/i18n/messages/es/folderSelection.ts | 5 ++ .../lib/i18n/messages/fr/folderSelection.ts | 5 ++ .../lib/i18n/messages/he/folderSelection.ts | 5 ++ .../lib/i18n/messages/ja/folderSelection.ts | 5 ++ .../lib/i18n/messages/ru/folderSelection.ts | 5 ++ .../i18n/messages/zh-Hans/folderSelection.ts | 5 ++ packages/ui/src/lib/native/remote-window.ts | 24 ++++++++ 9 files changed, 78 insertions(+), 40 deletions(-) diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs index d8204059..92a0f634 100644 --- a/packages/tauri-app/src-tauri/src/main.rs +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -20,7 +20,6 @@ use tauri::webview::Webview; use tauri::{ AppHandle, Emitter, Manager, Runtime, WebviewUrl, WebviewWindowBuilder, WindowEvent, Wry, }; -use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; use tauri_plugin_global_shortcut::{ Code as ShortcutCode, GlobalShortcutExt, Shortcut, ShortcutState, }; @@ -78,34 +77,6 @@ fn schedule_remote_proxy_session_cleanup(app: AppHandle, session_id: String) { }); } -async fn confirm_local_certificate_install(app: &AppHandle) -> Result { - let (sender, receiver) = std::sync::mpsc::sync_channel(1); - - let mut dialog = app - .dialog() - .message( - "CodeNomad needs to install a local certificate to open self-signed HTTPS remote windows. This certificate is only used for local desktop proxy traffic on your machine. Your operating system may show a second certificate prompt after this.", - ) - .title("Install Local Certificate") - .kind(MessageDialogKind::Warning) - .buttons(MessageDialogButtons::OkCancelCustom( - "Continue".into(), - "Cancel".into(), - )); - - if let Some(window) = app.get_webview_window("main") { - dialog = dialog.parent(&window); - } - - dialog.show(move |accepted| { - let _ = sender.send(accepted); - }); - - tauri::async_runtime::spawn_blocking(move || receiver.recv().unwrap_or(false)) - .await - .map_err(|err| err.to_string()) -} - async fn cleanup_remote_proxy_session(app: &AppHandle, session_id: &str) -> Result<(), String> { let status = app.state::().manager.status(); let Some(base_url) = status.url else { @@ -367,6 +338,24 @@ async fn open_remote_window_impl( Ok(()) } +#[tauri::command] +fn needs_local_certificate_install() -> Result { + #[cfg(not(target_os = "linux"))] + { + let local_cert = cert_manager::ensure_local_cert().map_err(|err| { + format!("Failed to load the local HTTPS certificate for the remote proxy window: {err}") + })?; + return cert_manager::needs_trust_in_store(&local_cert.ca_cert_der).map_err(|err| { + format!("Failed to inspect the local CodeNomad certificate trust state: {err}") + }); + } + + #[cfg(target_os = "linux")] + { + Ok(false) + } +} + #[tauri::command] async fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<(), String> { #[cfg(not(target_os = "linux"))] @@ -379,17 +368,6 @@ async fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Res "Failed to load the local HTTPS certificate for the remote proxy window: {err}" ) })?; - if cert_manager::needs_trust_in_store(&local_cert.ca_cert_der).map_err(|err| { - format!("Failed to inspect the local CodeNomad certificate trust state: {err}") - })? { - let accepted = confirm_local_certificate_install(&app).await?; - if !accepted { - return Err( - "CodeNomad needs the local certificate to be trusted before it can open self-signed HTTPS remote windows." - .to_string(), - ); - } - } if let Err(err) = cert_manager::trust_cert_in_store(&local_cert.ca_cert_der) { return Err(format!( "Failed to trust the local CodeNomad CA certificate. Accept the certificate installation prompt and try again: {err}" @@ -598,6 +576,7 @@ fn main() { cli_restart, wake_lock_start, wake_lock_stop, + needs_local_certificate_install, open_remote_window ]) .on_menu_event(|app_handle, event| { diff --git a/packages/ui/src/lib/i18n/messages/en/folderSelection.ts b/packages/ui/src/lib/i18n/messages/en/folderSelection.ts index 16eac13e..841086c5 100644 --- a/packages/ui/src/lib/i18n/messages/en/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/en/folderSelection.ts @@ -69,5 +69,10 @@ export const folderSelectionMessages = { "folderSelection.servers.dialog.connecting": "Connecting...", "folderSelection.servers.dialog.errorRequired": "Server name and URL are required.", "folderSelection.servers.dialog.errorConnect": "Could not connect to the remote server.", + "folderSelection.servers.certificateInstall.title": "Install Local Certificate", + "folderSelection.servers.certificateInstall.confirmMessage": "CodeNomad needs to install a local certificate to open self-signed HTTPS remote windows. This certificate is only used for local desktop proxy traffic on your machine. Your operating system may show a second certificate prompt after this.", + "folderSelection.servers.certificateInstall.confirmLabel": "Continue", + "folderSelection.servers.certificateInstall.cancelLabel": "Cancel", + "folderSelection.servers.certificateInstall.cancelled": "CodeNomad needs the local certificate to be trusted before it can open self-signed HTTPS remote windows.", "folderSelection.sidecars.button": "Open SideCar", } as const diff --git a/packages/ui/src/lib/i18n/messages/es/folderSelection.ts b/packages/ui/src/lib/i18n/messages/es/folderSelection.ts index ad618a94..1c1e47dc 100644 --- a/packages/ui/src/lib/i18n/messages/es/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/es/folderSelection.ts @@ -69,5 +69,10 @@ export const folderSelectionMessages = { "folderSelection.servers.dialog.connecting": "Conectando...", "folderSelection.servers.dialog.errorRequired": "El nombre y la URL del servidor son obligatorios.", "folderSelection.servers.dialog.errorConnect": "No se pudo conectar al servidor remoto.", + "folderSelection.servers.certificateInstall.title": "Instalar certificado local", + "folderSelection.servers.certificateInstall.confirmMessage": "CodeNomad necesita instalar un certificado local para abrir ventanas remotas HTTPS autofirmadas. Este certificado solo se usa para el trafico del proxy local de escritorio en tu equipo. Es posible que tu sistema operativo muestre un segundo aviso de certificado despues de esto.", + "folderSelection.servers.certificateInstall.confirmLabel": "Continuar", + "folderSelection.servers.certificateInstall.cancelLabel": "Cancelar", + "folderSelection.servers.certificateInstall.cancelled": "CodeNomad necesita que el certificado local sea de confianza antes de poder abrir ventanas remotas HTTPS autofirmadas.", "folderSelection.sidecars.button": "Open SideCar", } as const diff --git a/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts b/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts index 1372a78e..6932e434 100644 --- a/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts @@ -69,5 +69,10 @@ export const folderSelectionMessages = { "folderSelection.servers.dialog.connecting": "Connexion...", "folderSelection.servers.dialog.errorRequired": "Le nom du serveur et l'URL sont requis.", "folderSelection.servers.dialog.errorConnect": "Impossible de se connecter au serveur distant.", + "folderSelection.servers.certificateInstall.title": "Installer le certificat local", + "folderSelection.servers.certificateInstall.confirmMessage": "CodeNomad doit installer un certificat local pour ouvrir des fenetres distantes HTTPS auto-signees. Ce certificat est utilise uniquement pour le trafic du proxy local de bureau sur votre machine. Votre systeme d'exploitation peut afficher une seconde invite de certificat apres cela.", + "folderSelection.servers.certificateInstall.confirmLabel": "Continuer", + "folderSelection.servers.certificateInstall.cancelLabel": "Annuler", + "folderSelection.servers.certificateInstall.cancelled": "CodeNomad a besoin que le certificat local soit approuve avant de pouvoir ouvrir des fenetres distantes HTTPS auto-signees.", "folderSelection.sidecars.button": "Open SideCar", } as const diff --git a/packages/ui/src/lib/i18n/messages/he/folderSelection.ts b/packages/ui/src/lib/i18n/messages/he/folderSelection.ts index 6e772cd1..69de36c0 100644 --- a/packages/ui/src/lib/i18n/messages/he/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/he/folderSelection.ts @@ -69,5 +69,10 @@ export const folderSelectionMessages = { "folderSelection.servers.dialog.connecting": "מתחבר...", "folderSelection.servers.dialog.errorRequired": "שם השרת והכתובת הם שדות חובה.", "folderSelection.servers.dialog.errorConnect": "לא ניתן היה להתחבר לשרת המרוחק.", + "folderSelection.servers.certificateInstall.title": "התקנת אישור מקומי", + "folderSelection.servers.certificateInstall.confirmMessage": "CodeNomad צריך להתקין אישור מקומי כדי לפתוח חלונות HTTPS מרוחקים עם אישור בחתימה עצמית. האישור הזה משמש רק לתעבורת ה-proxy המקומי של האפליקציה במחשב שלך. ייתכן שמערכת ההפעלה תציג לאחר מכן בקשת אישור נוספת.", + "folderSelection.servers.certificateInstall.confirmLabel": "המשך", + "folderSelection.servers.certificateInstall.cancelLabel": "ביטול", + "folderSelection.servers.certificateInstall.cancelled": "CodeNomad צריך שהאישור המקומי יהיה מהימן לפני שיוכל לפתוח חלונות HTTPS מרוחקים עם אישור בחתימה עצמית.", "folderSelection.sidecars.button": "Open SideCar", } as const diff --git a/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts b/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts index 82edb63f..b1b2f478 100644 --- a/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts @@ -69,5 +69,10 @@ export const folderSelectionMessages = { "folderSelection.servers.dialog.connecting": "接続中...", "folderSelection.servers.dialog.errorRequired": "サーバー名と URL は必須です。", "folderSelection.servers.dialog.errorConnect": "リモートサーバーに接続できませんでした。", + "folderSelection.servers.certificateInstall.title": "ローカル証明書をインストール", + "folderSelection.servers.certificateInstall.confirmMessage": "CodeNomad は自己署名 HTTPS のリモートウィンドウを開くために、ローカル証明書をインストールする必要があります。この証明書は、このマシン上のローカルデスクトッププロキシ通信にのみ使用されます。この後、OS が追加の証明書プロンプトを表示する場合があります。", + "folderSelection.servers.certificateInstall.confirmLabel": "続行", + "folderSelection.servers.certificateInstall.cancelLabel": "キャンセル", + "folderSelection.servers.certificateInstall.cancelled": "自己署名 HTTPS のリモートウィンドウを開くには、CodeNomad のローカル証明書を信頼する必要があります。", "folderSelection.sidecars.button": "Open SideCar", } as const diff --git a/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts b/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts index dd293097..bc88c601 100644 --- a/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts @@ -69,5 +69,10 @@ export const folderSelectionMessages = { "folderSelection.servers.dialog.connecting": "Подключение...", "folderSelection.servers.dialog.errorRequired": "Имя сервера и URL обязательны.", "folderSelection.servers.dialog.errorConnect": "Не удалось подключиться к удаленному серверу.", + "folderSelection.servers.certificateInstall.title": "Установить локальный сертификат", + "folderSelection.servers.certificateInstall.confirmMessage": "CodeNomad должен установить локальный сертификат, чтобы открывать удаленные HTTPS-окна с самоподписанным сертификатом. Этот сертификат используется только для трафика локального настольного прокси на вашем устройстве. После этого ваша операционная система может показать второе предупреждение о сертификате.", + "folderSelection.servers.certificateInstall.confirmLabel": "Продолжить", + "folderSelection.servers.certificateInstall.cancelLabel": "Отмена", + "folderSelection.servers.certificateInstall.cancelled": "CodeNomad должен доверять локальному сертификату, прежде чем сможет открывать удаленные HTTPS-окна с самоподписанным сертификатом.", "folderSelection.sidecars.button": "Open SideCar", } as const diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts index e3ea8727..445cdb00 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts @@ -69,5 +69,10 @@ export const folderSelectionMessages = { "folderSelection.servers.dialog.connecting": "连接中...", "folderSelection.servers.dialog.errorRequired": "服务器名称和 URL 为必填项。", "folderSelection.servers.dialog.errorConnect": "无法连接到远程服务器。", + "folderSelection.servers.certificateInstall.title": "安装本地证书", + "folderSelection.servers.certificateInstall.confirmMessage": "CodeNomad 需要安装本地证书,才能打开使用自签名 HTTPS 的远程窗口。此证书仅用于你这台设备上的本地桌面代理流量。之后你的操作系统可能还会显示第二个证书提示。", + "folderSelection.servers.certificateInstall.confirmLabel": "继续", + "folderSelection.servers.certificateInstall.cancelLabel": "取消", + "folderSelection.servers.certificateInstall.cancelled": "CodeNomad 需要先信任本地证书,才能打开使用自签名 HTTPS 的远程窗口。", "folderSelection.sidecars.button": "Open SideCar", } as const diff --git a/packages/ui/src/lib/native/remote-window.ts b/packages/ui/src/lib/native/remote-window.ts index 8412c6b3..54506bda 100644 --- a/packages/ui/src/lib/native/remote-window.ts +++ b/packages/ui/src/lib/native/remote-window.ts @@ -1,5 +1,7 @@ import { invoke } from "@tauri-apps/api/core" import type { RemoteServerProfile } from "../../../../server/src/api-types" +import { showConfirmDialog } from "../../stores/alerts" +import { tGlobal } from "../i18n" import { runtimeEnv } from "../runtime-env" export interface RemoteWindowOpenPayload { @@ -34,6 +36,28 @@ export async function openRemoteServerWindow( } if (runtimeEnv.host === "tauri") { + const requiresLocalCertificate = + proxySessionId !== undefined && (entryUrl ?? profile.baseUrl).startsWith("https://") + + if (requiresLocalCertificate) { + const needsInstall = await invoke("needs_local_certificate_install") + if (needsInstall) { + const accepted = await showConfirmDialog( + tGlobal("folderSelection.servers.certificateInstall.confirmMessage"), + { + title: tGlobal("folderSelection.servers.certificateInstall.title"), + variant: "warning", + confirmLabel: tGlobal("folderSelection.servers.certificateInstall.confirmLabel"), + cancelLabel: tGlobal("folderSelection.servers.certificateInstall.cancelLabel"), + }, + ) + + if (!accepted) { + throw new Error(tGlobal("folderSelection.servers.certificateInstall.cancelled")) + } + } + } + await invoke("open_remote_window", { payload }) return }