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!("{}", server_key.fingerprint(russh::keys::HashAlg::Sha256));
73
74 let Some(path) = self.known_hosts_path.take() else {
76 #[cfg(test)]
78 {
79 tracing::warn!("known_hosts missing: accepting host key (test mode)");
80 return Ok(true);
81 }
82 #[cfg(not(test))]
83 {
84 stash_host_key_error(
85 &self.outcome,
86 SshCliError::InvalidArgument(
87 "known_hosts_path is required for host-key verification".into(),
88 ),
89 );
90 tracing::error!("known_hosts path missing; rejecting host key (fail-closed)");
91 return Ok(false);
92 }
93 };
94
95 let host = self.host.clone();
98 let port = self.port;
99 let replace = self.replace_host_key;
100 let outcome = tokio::task::spawn_blocking(move || {
101 let mut kh = crate::ssh::known_hosts::KnownHosts::load(path)?;
102 crate::ssh::known_hosts::verify_tofu(&mut kh, &host, port, &fingerprint, replace)
103 })
104 .await;
105
106 match outcome {
107 Ok(Ok(true)) => Ok(true),
108 Ok(Ok(false)) => Ok(false),
109 Ok(Err(e)) => {
110 stash_host_key_error(&self.outcome, e);
112 tracing::error!("host key rejected");
113 Ok(false)
114 }
115 Err(e) => {
116 stash_host_key_error(
117 &self.outcome,
118 SshCliError::ConnectionFailed(format!("known_hosts task failed: {e}")),
119 );
120 tracing::error!(err = %e, "known_hosts task failed");
121 Ok(false)
122 }
123 }
124 }
125
126 async fn auth_banner(
127 &mut self,
128 banner: &str,
129 _session: &mut russh::client::Session,
130 ) -> Result<(), Self::Error> {
131 const MAX: usize = 512;
133 let truncated = if banner.len() > MAX {
134 format!("{}…", &banner[..MAX])
135 } else {
136 banner.to_owned()
137 };
138 tracing::info!(banner = %truncated, "SSH auth banner");
139 Ok(())
140 }
141}