diff options
| author | akbash <akbash@herdr.dev> | 2026-07-06 18:21:31 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-06 18:21:31 +0300 |
| commit | d9230f517a88be0eb88c9af39f7a4212644c9a3d (patch) | |
| tree | 745973b12bad8557ef4c3e06fe6ef5d07fc8fc7e /src | |
| parent | e98c49658caf054f2cfdc0f52831f29d59ed6fba (diff) | |
fix: clarify remote ssh auth failures (#1084)
refs #1034
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 8 | ||||
| -rw-r--r-- | src/remote.rs | 75 |
2 files changed, 82 insertions, 1 deletions
diff --git a/src/main.rs b/src/main.rs index 3b4bed1c..deb332b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -661,7 +661,13 @@ fn main() -> io::Result<()> { } if let Some(remote_launch) = remote_launch { - return remote::run_remote(remote_launch); + let remote_target = remote_launch.target.clone(); + if let Err(err) = remote::run_remote(remote_launch) { + eprintln!("error: {err}"); + remote::print_remote_error_hint(&err, &remote_target); + std::process::exit(1); + } + return Ok(()); } let loaded_config = config::Config::load(); diff --git a/src/remote.rs b/src/remote.rs index edfb9269..ce4ca91b 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -146,3 +146,78 @@ pub(crate) fn run_remote_client_bridge() -> std::io::Result<()> { "remote client bridge is not supported on Windows yet", )) } + +pub(crate) fn print_remote_error_hint(err: &std::io::Error, target: &str) { + if is_remote_auth_error(err) { + eprintln!( + "hint: verify SSH access first with `{}`.", + ssh_check_command(target) + ); + eprintln!( + "hint: if your SSH key has a passphrase, load it into ssh-agent with `ssh-add` before running `herdr --remote`." + ); + } +} + +fn is_remote_auth_error(err: &std::io::Error) -> bool { + let message = err.to_string(); + message.contains("Permission denied") + && (message.contains("(publickey") + || message.contains("(keyboard-interactive") + || message.contains("(password")) +} + +fn ssh_check_command(target: &str) -> String { + format!("ssh {}", shell_quote(target)) +} + +fn shell_quote(value: &str) -> String { + if !value.is_empty() + && value.chars().all(|ch| { + ch.is_ascii_alphanumeric() + || matches!( + ch, + '@' | '%' | '_' | '+' | '=' | ':' | ',' | '.' | '/' | '-' + ) + }) + { + return value.to_string(); + } + + format!("'{}'", value.replace('\'', "'\\''")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn remote_auth_error_matches_ssh_auth_denied() { + let err = std::io::Error::other( + "remote platform detection failed: user@host: Permission denied (publickey).", + ); + + assert!(is_remote_auth_error(&err)); + } + + #[test] + fn remote_auth_error_matches_keyboard_interactive_denied() { + let err = std::io::Error::other( + "remote server status failed: user@host: Permission denied (keyboard-interactive).", + ); + + assert!(is_remote_auth_error(&err)); + } + + #[test] + fn remote_auth_error_ignores_non_auth_errors() { + let err = std::io::Error::other("remote platform detection failed: unsupported platform"); + + assert!(!is_remote_auth_error(&err)); + } + + #[test] + fn ssh_check_command_quotes_remote_target() { + assert_eq!(ssh_check_command("host name"), "ssh 'host name'"); + } +} |
