#![forbid(unsafe_code)]
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use crate::errors::SshCliError;
use crate::ssh::connection::ConnectionConfig;
pub type HostKeyOutcome = Arc<Mutex<Option<SshCliError>>>;
#[must_use]
pub fn new_host_key_outcome() -> HostKeyOutcome {
Arc::new(Mutex::new(None))
}
pub fn stash_host_key_error(outcome: &HostKeyOutcome, err: SshCliError) {
if let Ok(mut g) = outcome.lock() {
*g = Some(err);
}
}
#[must_use]
pub fn take_host_key_error(outcome: &HostKeyOutcome) -> Option<SshCliError> {
outcome.lock().ok().and_then(|mut g| g.take())
}
pub struct ClientHandler {
host: String,
port: u16,
known_hosts_path: Option<PathBuf>,
replace_host_key: bool,
outcome: HostKeyOutcome,
}
impl ClientHandler {
#[must_use]
pub fn new(cfg: &ConnectionConfig, outcome: HostKeyOutcome) -> Self {
Self {
host: cfg.host.as_str().to_owned(),
port: cfg.port.get(),
known_hosts_path: cfg.known_hosts_path.clone(),
replace_host_key: cfg.replace_host_key,
outcome,
}
}
}
impl russh::client::Handler for ClientHandler {
type Error = russh::Error;
async fn check_server_key(
&mut self,
server_key: &russh::keys::ssh_key::PublicKey,
) -> Result<bool, Self::Error> {
let fingerprint = format!(
"{}",
server_key.fingerprint(russh::keys::HashAlg::Sha256)
);
let Some(path) = self.known_hosts_path.take() else {
#[cfg(test)]
{
tracing::warn!("known_hosts missing: accepting host key (test mode)");
return Ok(true);
}
#[cfg(not(test))]
{
stash_host_key_error(
&self.outcome,
SshCliError::InvalidArgument(
"known_hosts_path is required for host-key verification".into(),
),
);
tracing::error!("known_hosts path missing; rejecting host key (fail-closed)");
return Ok(false);
}
};
let host = self.host.clone();
let port = self.port;
let replace = self.replace_host_key;
let outcome = tokio::task::spawn_blocking(move || {
let mut kh = crate::ssh::known_hosts::KnownHosts::load(path)?;
crate::ssh::known_hosts::verify_tofu(&mut kh, &host, port, &fingerprint, replace)
})
.await;
match outcome {
Ok(Ok(true)) => Ok(true),
Ok(Ok(false)) => Ok(false),
Ok(Err(e)) => {
stash_host_key_error(&self.outcome, e);
tracing::error!("host key rejected");
Ok(false)
}
Err(e) => {
stash_host_key_error(
&self.outcome,
SshCliError::ConnectionFailed(format!("known_hosts task failed: {e}")),
);
tracing::error!(err = %e, "known_hosts task failed");
Ok(false)
}
}
}
async fn auth_banner(
&mut self,
banner: &str,
_session: &mut russh::client::Session,
) -> Result<(), Self::Error> {
const MAX: usize = 512;
let truncated = if banner.len() > MAX {
format!("{}…", &banner[..MAX])
} else {
banner.to_owned()
};
tracing::info!(banner = %truncated, "SSH auth banner");
Ok(())
}
}