ssh_cli/ssh/
client_handler.rs1#![forbid(unsafe_code)]
4use std::path::PathBuf;
11use std::sync::{Arc, Mutex};
12
13use crate::errors::SshCliError;
14use crate::ssh::connection::ConnectionConfig;
15
16pub type HostKeyOutcome = Arc<Mutex<Option<SshCliError>>>;
22
23#[must_use]
25pub fn new_host_key_outcome() -> HostKeyOutcome {
26 Arc::new(Mutex::new(None))
27}
28
29pub fn stash_host_key_error(outcome: &HostKeyOutcome, err: SshCliError) {
31 if let Ok(mut g) = outcome.lock() {
32 *g = Some(err);
33 }
34}
35
36#[must_use]
38pub fn take_host_key_error(outcome: &HostKeyOutcome) -> Option<SshCliError> {
39 outcome.lock().ok().and_then(|mut g| g.take())
40}
41
42pub struct ClientHandler {
44 host: String,
45 port: u16,
46 known_hosts_path: Option<PathBuf>,
47 replace_host_key: bool,
48 outcome: HostKeyOutcome,
49}
50
51impl ClientHandler {
52 #[must_use]
54 pub fn new(cfg: &ConnectionConfig, outcome: HostKeyOutcome) -> Self {
55 Self {
56 host: cfg.host.as_str().to_owned(),
57 port: cfg.port.get(),
58 known_hosts_path: cfg.known_hosts_path.clone(),
59 replace_host_key: cfg.replace_host_key,
60 outcome,
61 }
62 }
63}
64
65impl russh::client::Handler for ClientHandler {
66 type Error = russh::Error;
67
68 async fn check_server_key(
69 &mut self,
70 server_key: &russh::keys::ssh_key::PublicKey,
71 ) -> Result<bool, Self::Error> {
72 let fingerprint = format!(
73 "{}",
74 server_key.fingerprint(russh::keys::HashAlg::Sha256)
75 );
76
77 let Some(path) = self.known_hosts_path.take() else {
79 #[cfg(test)]
81 {
82 tracing::warn!("known_hosts missing: accepting host key (test mode)");
83 return Ok(true);
84 }
85 #[cfg(not(test))]
86 {
87 stash_host_key_error(
88 &self.outcome,
89 SshCliError::InvalidArgument(
90 "known_hosts_path is required for host-key verification".into(),
91 ),
92 );
93 tracing::error!("known_hosts path missing; rejecting host key (fail-closed)");
94 return Ok(false);
95 }
96 };
97
98 let host = self.host.clone();
101 let port = self.port;
102 let replace = self.replace_host_key;
103 let outcome = tokio::task::spawn_blocking(move || {
104 let mut kh = crate::ssh::known_hosts::KnownHosts::load(path)?;
105 crate::ssh::known_hosts::verify_tofu(&mut kh, &host, port, &fingerprint, replace)
106 })
107 .await;
108
109 match outcome {
110 Ok(Ok(true)) => Ok(true),
111 Ok(Ok(false)) => Ok(false),
112 Ok(Err(e)) => {
113 stash_host_key_error(&self.outcome, e);
115 tracing::error!("host key rejected");
116 Ok(false)
117 }
118 Err(e) => {
119 stash_host_key_error(
120 &self.outcome,
121 SshCliError::ConnectionFailed(format!("known_hosts task failed: {e}")),
122 );
123 tracing::error!(err = %e, "known_hosts task failed");
124 Ok(false)
125 }
126 }
127 }
128
129 async fn auth_banner(
130 &mut self,
131 banner: &str,
132 _session: &mut russh::client::Session,
133 ) -> Result<(), Self::Error> {
134 const MAX: usize = 512;
136 let truncated = if banner.len() > MAX {
137 format!("{}…", &banner[..MAX])
138 } else {
139 banner.to_owned()
140 };
141 tracing::info!(banner = %truncated, "SSH auth banner");
142 Ok(())
143 }
144}