add /ssh-exec command

This commit is contained in:
2026-04-25 18:14:20 +03:00
parent 27f2c8db49
commit dda56f1694
4 changed files with 829 additions and 19 deletions

View File

@@ -1,4 +1,11 @@
use russh::client::Handler;
use std::{fmt::Debug, sync::Arc, time::Duration};
use anyhow::{Context as _, Result as AnyhowResult};
use russh::{
client::{AuthResult, Handle, Handler},
keys::PrivateKeyWithHashAlg,
};
use tokio::net::ToSocketAddrs;
pub(crate) struct ClientHandler {}
@@ -12,3 +19,121 @@ impl Handler for ClientHandler {
Ok(true)
}
}
pub(crate) async fn connect_ssh<A: ToSocketAddrs + Debug, K: AsRef<[u8]>, P: Into<String>>(
connect_timeout: Duration,
auth_timeout: Duration,
addr: A,
username: &str,
key: Option<K>,
password: Option<P>,
) -> AnyhowResult<Handle<ClientHandler>> {
let ssh_config = Arc::new(russh::client::Config {
inactivity_timeout: None,
..Default::default()
});
log::info!(
"Connecting to SSH {:?} {} key & {} password",
&addr,
if key.is_some() { "with" } else { "without" },
if password.is_some() {
"with"
} else {
"without"
}
);
let mut connection = tokio::time::timeout(
connect_timeout,
russh::client::connect(ssh_config, addr, crate::ssh::ClientHandler {}),
)
.await
.context("Connection to host timed out")?
.context("Cannot connect to host")?;
let key = match key {
Some(key) => {
let privkey = russh::keys::PrivateKey::from_openssh(key).context("Invalid SSH key")?;
Some(PrivateKeyWithHashAlg::new(
Arc::new(privkey),
connection.best_supported_rsa_hash().await?.flatten(),
))
}
None => None,
};
let mut auth_success = false;
if !auth_success {
if let Some(key) = key {
log::info!("Trying to authenticate using key");
match tokio::time::timeout(
auth_timeout,
connection.authenticate_publickey(username, key),
)
.await
{
Ok(Ok(AuthResult::Success)) => {
auth_success = true;
}
Ok(Ok(AuthResult::Failure {
remaining_methods,
partial_success,
})) => {
if partial_success || !remaining_methods.contains(&russh::MethodKind::Password)
{
if partial_success {
anyhow::bail!("SSH auth failed: multi-factor auth is not supported");
} else {
anyhow::bail!("SSH auth failed: no auth methods left");
}
}
log::warn!("SSH public key rejected");
}
Ok(Err(e)) => {
log::warn!("SSH public key auth failed: {e}");
}
Err(_) => {
log::warn!("SSH public key auth timed out");
}
}
} else {
log::info!("No key provided");
}
}
if !auth_success {
if let Some(password) = password {
log::info!("Trying to authenticate using password");
match tokio::time::timeout(
auth_timeout,
connection.authenticate_password(username, password),
)
.await
{
Ok(Ok(AuthResult::Success)) => {
auth_success = true;
}
Ok(Ok(AuthResult::Failure {
remaining_methods: _,
partial_success,
})) => {
if partial_success {
anyhow::bail!("SSH auth failed: multi-factor auth is not supported");
} else {
anyhow::bail!("SSH auth failed: no auth methods left");
}
}
Ok(Err(e)) => {
log::warn!("SSH password auth failed: {e}");
}
Err(_) => {
log::warn!("SSH password auth timed out");
}
}
} else {
log::info!("No password provided");
}
}
if !auth_success {
anyhow::bail!("SSH auth failed: no auth methods left");
}
Ok(connection)
}