1#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum SshDiagnosis {
12 PermissionDenied,
14 ConnectionRefused,
16 Unreachable,
18 HostKeyChanged,
20 Other,
23}
24
25pub fn classify_ssh_error(stderr: &str) -> SshDiagnosis {
31 if stderr.contains("REMOTE HOST IDENTIFICATION HAS CHANGED")
32 || stderr.contains("Host key verification failed")
33 {
34 return SshDiagnosis::HostKeyChanged;
35 }
36 if stderr.contains("Connection refused") {
37 return SshDiagnosis::ConnectionRefused;
38 }
39 if stderr.contains("timed out")
40 || stderr.contains("No route to host")
41 || stderr.contains("Could not resolve hostname")
42 || stderr.contains("Network is unreachable")
43 {
44 return SshDiagnosis::Unreachable;
45 }
46 if stderr.contains("Permission denied") || stderr.contains("publickey") {
47 return SshDiagnosis::PermissionDenied;
48 }
49 SshDiagnosis::Other
50}
51
52pub fn diagnosis_message(d: SshDiagnosis, target: &str, port: u16) -> String {
55 match d {
56 SshDiagnosis::PermissionDenied => format!(
57 "can't authenticate to `{target}` — key auth isn't set up on this host. \
58 Fix it with `git-ark host setup-key {target} [--port {port}]` (generates + \
59 copies a key), or `ssh-copy-id -p {port} {target}` yourself, then re-run \
60 `git-ark host add`."
61 ),
62 SshDiagnosis::ConnectionRefused => format!(
63 "nothing is listening on `{target}:{port}` — is the host up and is sshd on \
64 that port?"
65 ),
66 SshDiagnosis::Unreachable => {
67 format!("`{target}` is unreachable — check the hostname/network.")
68 }
69 SshDiagnosis::HostKeyChanged => format!(
70 "`{target}`'s SSH host key changed — if expected, run `ssh-keygen -R \
71 '[host]:{port}'`; if not, investigate before proceeding."
72 ),
73 SshDiagnosis::Other => String::new(),
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn classifies_permission_denied() {
83 assert_eq!(
84 classify_ssh_error("ark@nas: Permission denied (publickey)."),
85 SshDiagnosis::PermissionDenied
86 );
87 }
88
89 #[test]
90 fn classifies_connection_refused() {
91 assert_eq!(
92 classify_ssh_error("ssh: connect to host x port 22: Connection refused"),
93 SshDiagnosis::ConnectionRefused
94 );
95 }
96
97 #[test]
98 fn classifies_unreachable() {
99 assert_eq!(
100 classify_ssh_error("ssh: connect to host x port 22: Operation timed out"),
101 SshDiagnosis::Unreachable
102 );
103 assert_eq!(
104 classify_ssh_error("ssh: connect to host x port 22: No route to host"),
105 SshDiagnosis::Unreachable
106 );
107 assert_eq!(
108 classify_ssh_error("ssh: Could not resolve hostname x: nodename nor servname provided"),
109 SshDiagnosis::Unreachable
110 );
111 }
112
113 #[test]
114 fn classifies_host_key_changed() {
115 assert_eq!(
116 classify_ssh_error(
117 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n\
118 @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\n\
119 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
120 ),
121 SshDiagnosis::HostKeyChanged
122 );
123 assert_eq!(
124 classify_ssh_error("Host key verification failed."),
125 SshDiagnosis::HostKeyChanged
126 );
127 }
128
129 #[test]
130 fn classifies_other_for_unrecognized_stderr() {
131 assert_eq!(
132 classify_ssh_error("some completely unrelated error message"),
133 SshDiagnosis::Other
134 );
135 }
136
137 #[test]
138 fn diagnosis_message_permission_denied_names_setup_key_and_target() {
139 let msg = diagnosis_message(SshDiagnosis::PermissionDenied, "ark@nas", 2222);
140 assert!(msg.contains("setup-key"));
141 assert!(msg.contains("ark@nas"));
142 }
143
144 #[test]
145 fn diagnosis_message_other_is_empty() {
146 assert_eq!(diagnosis_message(SshDiagnosis::Other, "ark@nas", 22), "");
147 }
148}