1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
mod args;
mod attach;
#[cfg(unix)]
mod host_unix;
mod process;
mod restart_policy;
mod saved;
pub(crate) use args::*;
pub(crate) use attach::*;
#[cfg(unix)]
pub(crate) use host_unix::run_remote_client_bridge;
pub(crate) use saved::*;
#[cfg(windows)]
pub(crate) fn run_remote_client_bridge() -> std::io::Result<()> {
Err(std::io::Error::other(
"remote Windows hosts are not supported yet",
))
}
pub(crate) fn print_saved_ssh_error_hint(err: &std::io::Error, target: &str) {
if is_remote_host_key_error(err) {
eprintln!(
"hint: saved machines use strict host-key checking; add the host key to the configured known_hosts file, then retry."
);
} else {
print_remote_error_hint(err, target);
}
}
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_host_key_error(err: &std::io::Error) -> bool {
let message = err.to_string().to_ascii_lowercase();
message.contains("host key verification failed")
|| message.contains("remote host identification has changed")
}
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_host_key_error_matches_ssh_diagnostics() {
for message in [
"Host key verification failed.",
"REMOTE HOST IDENTIFICATION HAS CHANGED!",
] {
assert!(is_remote_host_key_error(&std::io::Error::other(message)));
}
assert!(!is_remote_host_key_error(&std::io::Error::other(
"server closed connection"
)));
}
#[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'");
}
}
|