diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 790d5602..e6d9980b 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -194,6 +194,7 @@ pub async fn expand_ssh_connection_params( expanded_params.ssh_password = ssh_conn.password.clone(); expanded_params.ssh_key_file = ssh_conn.key_file.clone(); expanded_params.ssh_key_passphrase = ssh_conn.key_passphrase.clone(); + expanded_params.ssh_allow_passphrase_prompt = ssh_conn.allow_passphrase_prompt; } } @@ -258,6 +259,7 @@ pub fn resolve_connection_params(params: &ConnectionParams) -> Result(app: &AppHandle) -> Result<(), S Some(key_file.clone()) }, key_passphrase: None, + allow_passphrase_prompt: None, save_in_keychain: conn.params.save_in_keychain, }; @@ -1139,6 +1142,7 @@ pub async fn save_ssh_connection( } else { ssh.key_passphrase.clone() }, + allow_passphrase_prompt: ssh.allow_passphrase_prompt, save_in_keychain: ssh.save_in_keychain, }; @@ -1206,6 +1210,7 @@ pub async fn update_ssh_connection( } else { ssh.key_passphrase.clone() }, + allow_passphrase_prompt: ssh.allow_passphrase_prompt, save_in_keychain: ssh.save_in_keychain, }; @@ -1302,6 +1307,7 @@ pub async fn test_ssh_connection( resolved_password.as_deref(), ssh.key_file.as_deref(), resolved_passphrase.as_deref(), + ssh.allow_passphrase_prompt.unwrap_or(false), ) } @@ -1695,6 +1701,7 @@ mod tests { password: password.map(|p| p.to_string()), key_file: None, key_passphrase: None, + allow_passphrase_prompt: None, save_in_keychain: Some(save_in_keychain), } } diff --git a/src-tauri/src/export_import_tests.rs b/src-tauri/src/export_import_tests.rs index 13bc7621..73f26ca2 100644 --- a/src-tauri/src/export_import_tests.rs +++ b/src-tauri/src/export_import_tests.rs @@ -52,6 +52,7 @@ mod tests { password: Some("ssh_password".to_string()), key_file: None, key_passphrase: None, + allow_passphrase_prompt: None, save_in_keychain: Some(true), }], }; diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 6261201a..abfdd15d 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -80,6 +80,8 @@ pub struct SshConnection { pub key_file: Option, #[serde(skip_serializing_if = "Option::is_none")] pub key_passphrase: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_passphrase_prompt: Option, pub save_in_keychain: Option, } @@ -95,6 +97,8 @@ pub struct SshConnectionInput { pub key_file: Option, #[serde(skip_serializing_if = "Option::is_none")] pub key_passphrase: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_passphrase_prompt: Option, pub save_in_keychain: Option, } @@ -110,6 +114,8 @@ pub struct SshTestParams { #[serde(skip_serializing_if = "Option::is_none")] pub key_passphrase: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub allow_passphrase_prompt: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub connection_id: Option, } @@ -141,6 +147,8 @@ pub struct ConnectionParams { pub ssh_key_file: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ssh_key_passphrase: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ssh_allow_passphrase_prompt: Option, pub save_in_keychain: Option, // Connection ID for stable pooling (not persisted, set at runtime) #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src-tauri/src/ssh_tunnel.rs b/src-tauri/src/ssh_tunnel.rs index d9f9492a..af7c938e 100644 --- a/src-tauri/src/ssh_tunnel.rs +++ b/src-tauri/src/ssh_tunnel.rs @@ -95,13 +95,14 @@ impl SshTunnel { ssh_password: Option<&str>, ssh_key_file: Option<&str>, ssh_key_passphrase: Option<&str>, + ssh_allow_passphrase_prompt: bool, remote_host: &str, remote_port: u16, ) -> Result { let use_system_ssh = should_use_system_ssh(ssh_password); println!( - "[SSH Tunnel] New Request: Host={}, Port={}, User={}, UseSystemSSH={}", - ssh_host, ssh_port, ssh_user, use_system_ssh + "[SSH Tunnel] New Request: Host={}, Port={}, User={}, UseSystemSSH={}, AllowPrompt={}", + ssh_host, ssh_port, ssh_user, use_system_ssh, ssh_allow_passphrase_prompt ); let local_port = { @@ -120,6 +121,7 @@ impl SshTunnel { ssh_port, ssh_user, ssh_key_file, + ssh_allow_passphrase_prompt, remote_host, remote_port, local_port, @@ -152,6 +154,7 @@ impl SshTunnel { ssh_port: u16, ssh_user: &str, ssh_key_file: Option<&str>, + ssh_allow_passphrase_prompt: bool, remote_host: &str, remote_port: u16, local_port: u16, @@ -188,18 +191,27 @@ impl SshTunnel { args.push("-o".to_string()); args.push("StrictHostKeyChecking=accept-new".to_string()); args.push("-o".to_string()); - args.push("BatchMode=yes".to_string()); + if ssh_allow_passphrase_prompt { + args.push("BatchMode=no".to_string()); + } else { + args.push("BatchMode=yes".to_string()); + } args.push(destination); println!("[SSH Tunnel] Executing: ssh {:?}", args); - let mut child = Command::new("ssh") + let mut command = Command::new("ssh"); + command .args(&args) .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|e| { + .stderr(Stdio::piped()); + + if ssh_allow_passphrase_prompt { + command.env("SSH_ASKPASS_REQUIRE", "force"); + } + + let mut child = command.spawn().map_err(|e| { let err = format!( "Failed to launch system ssh: {}. Ensure 'ssh' is in PATH.", e @@ -526,15 +538,22 @@ pub fn test_ssh_connection( ssh_password: Option<&str>, ssh_key_file: Option<&str>, ssh_key_passphrase: Option<&str>, + ssh_allow_passphrase_prompt: bool, ) -> Result { let use_system_ssh = should_use_system_ssh(ssh_password); println!( - "[SSH Test] Testing connection to {}:{} as {} (UseSystemSSH={})", - ssh_host, ssh_port, ssh_user, use_system_ssh + "[SSH Test] Testing connection to {}:{} as {} (UseSystemSSH={}, AllowPrompt={})", + ssh_host, ssh_port, ssh_user, use_system_ssh, ssh_allow_passphrase_prompt ); if use_system_ssh { - test_ssh_connection_system(ssh_host, ssh_port, ssh_user, ssh_key_file) + test_ssh_connection_system( + ssh_host, + ssh_port, + ssh_user, + ssh_key_file, + ssh_allow_passphrase_prompt, + ) } else { test_ssh_connection_russh( ssh_host, @@ -553,6 +572,7 @@ fn test_ssh_connection_system( ssh_port: u16, ssh_user: &str, ssh_key_file: Option<&str>, + ssh_allow_passphrase_prompt: bool, ) -> Result { println!("[SSH Test] Using system SSH (supports ~/.ssh/config)"); @@ -563,7 +583,11 @@ fn test_ssh_connection_system( let mut args = Vec::with_capacity(12); args.extend([ "-o", - "BatchMode=yes", + if ssh_allow_passphrase_prompt { + "BatchMode=no" + } else { + "BatchMode=yes" + }, "-o", "ConnectTimeout=10", "-o", @@ -585,7 +609,14 @@ fn test_ssh_connection_system( println!("[SSH Test] Executing: ssh {:?}", args); - let output = Command::new("ssh").args(&args).output().map_err(|e| { + let mut command = Command::new("ssh"); + command.args(&args); + + if ssh_allow_passphrase_prompt { + command.env("SSH_ASKPASS_REQUIRE", "force"); + } + + let output = command.output().map_err(|e| { format!( "Failed to execute ssh command: {}. Ensure 'ssh' is in PATH.", e diff --git a/src/components/modals/NewConnectionModal.tsx b/src/components/modals/NewConnectionModal.tsx index 4790d098..d1de710a 100644 --- a/src/components/modals/NewConnectionModal.tsx +++ b/src/components/modals/NewConnectionModal.tsx @@ -59,6 +59,7 @@ interface ConnectionParams { ssh_password?: string; ssh_key_file?: string; ssh_key_passphrase?: string; + ssh_allow_passphrase_prompt?: boolean; save_in_keychain?: boolean; } @@ -1432,6 +1433,23 @@ export const NewConnectionModal = ({ type="password" placeholder={t("newConnection.sshKeyPassphrasePlaceholder")} /> +
+ + updateField("ssh_allow_passphrase_prompt", e.target.checked) + } + className="accent-blue-500 w-3.5 h-3.5 rounded cursor-pointer" + /> + +
)} diff --git a/src/components/modals/SshConnectionsModal.tsx b/src/components/modals/SshConnectionsModal.tsx index 3baabaf2..a6420812 100644 --- a/src/components/modals/SshConnectionsModal.tsx +++ b/src/components/modals/SshConnectionsModal.tsx @@ -217,6 +217,7 @@ export function SshConnectionsModal({ password: formData.password, key_file: formData.key_file, key_passphrase: formData.key_passphrase, + allow_passphrase_prompt: formData.allow_passphrase_prompt, save_in_keychain: formData.save_in_keychain, }; @@ -559,6 +560,24 @@ export function SshConnectionsModal({ +
+ { + updateField("allow_passphrase_prompt", e.target.checked); + }} + className="accent-blue-500 w-4 h-4 rounded cursor-pointer" + /> + +
+ {/* Test Button and Status */}