Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ pub async fn expand_ssh_connection_params<R: Runtime>(
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;
}
}

Expand Down Expand Up @@ -258,6 +259,7 @@ pub fn resolve_connection_params(params: &ConnectionParams) -> Result<Connection
params.ssh_password.as_deref(),
params.ssh_key_file.as_deref(),
params.ssh_key_passphrase.as_deref(),
params.ssh_allow_passphrase_prompt.unwrap_or(false),
remote_host,
remote_port,
)
Expand Down Expand Up @@ -972,6 +974,7 @@ async fn migrate_ssh_connections<R: Runtime>(app: &AppHandle<R>) -> Result<(), S
Some(key_file.clone())
},
key_passphrase: None,
allow_passphrase_prompt: None,
save_in_keychain: conn.params.save_in_keychain,
};

Expand Down Expand Up @@ -1139,6 +1142,7 @@ pub async fn save_ssh_connection<R: Runtime>(
} else {
ssh.key_passphrase.clone()
},
allow_passphrase_prompt: ssh.allow_passphrase_prompt,
save_in_keychain: ssh.save_in_keychain,
};

Expand Down Expand Up @@ -1206,6 +1210,7 @@ pub async fn update_ssh_connection<R: Runtime>(
} else {
ssh.key_passphrase.clone()
},
allow_passphrase_prompt: ssh.allow_passphrase_prompt,
save_in_keychain: ssh.save_in_keychain,
};

Expand Down Expand Up @@ -1302,6 +1307,7 @@ pub async fn test_ssh_connection<R: Runtime>(
resolved_password.as_deref(),
ssh.key_file.as_deref(),
resolved_passphrase.as_deref(),
ssh.allow_passphrase_prompt.unwrap_or(false),
)
}

Expand Down Expand Up @@ -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),
}
}
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/export_import_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}],
};
Expand Down
8 changes: 8 additions & 0 deletions src-tauri/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ pub struct SshConnection {
pub key_file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_passphrase: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_passphrase_prompt: Option<bool>,
pub save_in_keychain: Option<bool>,
}

Expand All @@ -95,6 +97,8 @@ pub struct SshConnectionInput {
pub key_file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_passphrase: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_passphrase_prompt: Option<bool>,
pub save_in_keychain: Option<bool>,
}

Expand All @@ -110,6 +114,8 @@ pub struct SshTestParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub key_passphrase: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_passphrase_prompt: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub connection_id: Option<String>,
}

Expand Down Expand Up @@ -141,6 +147,8 @@ pub struct ConnectionParams {
pub ssh_key_file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ssh_key_passphrase: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ssh_allow_passphrase_prompt: Option<bool>,
pub save_in_keychain: Option<bool>,
// Connection ID for stable pooling (not persisted, set at runtime)
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
55 changes: 43 additions & 12 deletions src-tauri/src/ssh_tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, String> {
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 = {
Expand All @@ -120,6 +121,7 @@ impl SshTunnel {
ssh_port,
ssh_user,
ssh_key_file,
ssh_allow_passphrase_prompt,
remote_host,
remote_port,
local_port,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String, String> {
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,
Expand All @@ -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<String, String> {
println!("[SSH Test] Using system SSH (supports ~/.ssh/config)");

Expand All @@ -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",
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions src/components/modals/NewConnectionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -1432,6 +1433,23 @@ export const NewConnectionModal = ({
type="password"
placeholder={t("newConnection.sshKeyPassphrasePlaceholder")}
/>
<div className="flex items-center gap-2 mt-1">
<input
type="checkbox"
id="ssh-prompt-toggle"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Duplicate element ID ssh-prompt-toggle. The same ID is also used in SshConnectionsModal.tsx. If both modals exist in the DOM simultaneously, clicking the <label htmlFor="ssh-prompt-toggle"> will target the wrong checkbox, and it violates HTML uniqueness constraints. Consider prefixing the ID with a modal-specific identifier (e.g., new-conn-ssh-prompt-toggle).

checked={!!formData.ssh_allow_passphrase_prompt}
onChange={(e) =>
updateField("ssh_allow_passphrase_prompt", e.target.checked)
}
className="accent-blue-500 w-3.5 h-3.5 rounded cursor-pointer"
/>
<label
htmlFor="ssh-prompt-toggle"
className="text-xs font-medium text-secondary cursor-pointer select-none"
>
{t("newConnection.allowSshPrompt")}
</label>
</div>
</div>
)}
</div>
Expand Down
19 changes: 19 additions & 0 deletions src/components/modals/SshConnectionsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -559,6 +560,24 @@ export function SshConnectionsModal({
</label>
</div>

<div className="flex items-center gap-2">
<input
type="checkbox"
id="ssh-prompt-toggle"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Duplicate element ID ssh-prompt-toggle. The same ID is also used in NewConnectionModal.tsx. If both modals exist in the DOM simultaneously, clicking the <label htmlFor="ssh-prompt-toggle"> will target the wrong checkbox, and it violates HTML uniqueness constraints. Consider prefixing the ID with a modal-specific identifier (e.g., ssh-conn-prompt-toggle).

checked={!!formData.allow_passphrase_prompt}
onChange={(e) => {
updateField("allow_passphrase_prompt", e.target.checked);
}}
className="accent-blue-500 w-4 h-4 rounded cursor-pointer"
/>
<label
htmlFor="ssh-prompt-toggle"
className="text-sm font-medium text-secondary cursor-pointer select-none"
>
{t("newConnection.allowSshPrompt")}
</label>
</div>

{/* Test Button and Status */}
<div className="pt-4 border-t border-strong">
<button
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@
"sshKeyFilePlaceholder": "/pfad/zu/id_rsa",
"sshKeyPassphrase": "Passphrase für SSH-Schlüssel (optional)",
"sshKeyPassphrasePlaceholder": "Passphrase eingeben, falls der Schlüssel verschlüsselt ist",
"allowSshPrompt": "SSH-Passwort/PIN-Eingabeaufforderung erlauben",
"saveKeychain": "Passwörter im Keychain speichern",
"testConnection": "Verbindung testen",
"save": "Speichern",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@
"sshKeyFilePlaceholder": "/path/to/id_rsa",
"sshKeyPassphrase": "SSH Key Passphrase (Optional)",
"sshKeyPassphrasePlaceholder": "Enter key passphrase if encrypted",
"allowSshPrompt": "Allow SSH password/PIN prompt",
"saveKeychain": "Save passwords in Keychain",
"testConnection": "Test Connection",
"save": "Save",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@
"sshKeyFilePlaceholder": "/ruta/a/id_rsa",
"sshKeyPassphrase": "Frase de Paso de Clave SSH (Opcional)",
"sshKeyPassphrasePlaceholder": "Ingresa la frase de paso si la clave está cifrada",
"allowSshPrompt": "Permitir solicitud de contraseña/PIN de SSH",
"saveKeychain": "Guardar contraseñas en el Llavero",
"testConnection": "Probar Conexión",
"save": "Guardar",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@
"sshKeyFilePlaceholder": "/chemin/vers/id_rsa",
"sshKeyPassphrase": "Phrase secrète de clé SSH (optionnel)",
"sshKeyPassphrasePlaceholder": "Saisissez la phrase secrète si la clé est chiffrée",
"allowSshPrompt": "Autoriser l'invite de mot de passe/PIN SSH",
"saveKeychain": "Enregistrer les mots de passe dans le trousseau",
"testConnection": "Tester la connexion",
"save": "Enregistrer",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@
"sshKeyFilePlaceholder": "/percorso/id_rsa",
"sshKeyPassphrase": "Passphrase Chiave SSH (Opzionale)",
"sshKeyPassphrasePlaceholder": "Inserisci passphrase se la chiave è cifrata",
"allowSshPrompt": "Consenti prompt password/PIN SSH",
"saveKeychain": "Salva password nel Portachiavi",
"testConnection": "Testa Connessione",
"save": "Salva",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,7 @@
"sshKeyFilePlaceholder": "/path/to/id_rsa",
"sshKeyPassphrase": "SSH 鍵のパスフレーズ (任意)",
"sshKeyPassphrasePlaceholder": "鍵が暗号化されている場合はパスフレーズを入力",
"allowSshPrompt": "SSHパスワード/PINプロンプトを許可する",
"saveKeychain": "パスワードをキーチェーンに保存",
"testConnection": "接続テスト",
"save": "保存",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@
"sshKeyFilePlaceholder": "/path/to/id_rsa",
"sshKeyPassphrase": "Пароль SSH-ключа (необязательно)",
"sshKeyPassphrasePlaceholder": "Введите пароль, если ключ зашифрован",
"allowSshPrompt": "Разрешить запрос пароля/PIN-кода SSH",
"saveKeychain": "Сохранять пароли в Keychain",
"testConnection": "Проверить подключение",
"save": "Сохранить",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@
"sshKeyFilePlaceholder": "/path/to/id_rsa",
"sshKeyPassphrase": "SSH 密钥密码(可选)",
"sshKeyPassphrasePlaceholder": "如果加密请输入密钥密码",
"allowSshPrompt": "允许 SSH 密码/PIN 提示",
"saveKeychain": "在密钥链中保存密码",
"testConnection": "测试连接",
"save": "保存",
Expand Down
1 change: 1 addition & 0 deletions src/utils/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface ConnectionParams {
ssh_password?: string;
ssh_key_file?: string;
ssh_key_passphrase?: string;
ssh_allow_passphrase_prompt?: boolean;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/utils/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface ConnectionParams {
ssh_password?: string;
ssh_key_file?: string;
ssh_key_passphrase?: string;
ssh_allow_passphrase_prompt?: boolean;
save_in_keychain?: boolean;
}

Expand Down
1 change: 1 addition & 0 deletions src/utils/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface SshConnection {
password?: string;
key_file?: string;
key_passphrase?: string;
allow_passphrase_prompt?: boolean;
save_in_keychain?: boolean;
}

Expand Down