use std::io::Read;
use std::io::{stdin, stdout, Write};
use std::io::{Error as IoError, ErrorKind};
use std::net::TcpStream;
use anyhow::Result;
use russh::keys::PrivateKeyWithHashAlg;
use russh::{client, ChannelMsg};
use std::{str, sync::Arc};
use tracing::debug;
use russh::client::Config;
use russh::keys::*;
#[allow(unused_imports)]
use russh::*;
use ssh2::Session;
use std::process::Command;
use tokio::io::AsyncWriteExt;
use tokio::net::ToSocketAddrs;
pub fn run_ssh_command_ts(
hostname: &str,
command: Vec<String>,
interactive: bool,
tty: bool,
username: Option<&str>,
) -> Result<String, IoError> {
debug!(
"Running SSH command: '{:?}' on {hostname} as {:?} with interactive={interactive} and tty={tty}",
command, username
);
let mut ssh_cmd = Command::new("ssh");
ssh_cmd.arg("-o").arg("StrictHostKeyChecking=no");
ssh_cmd.arg("-o").arg("UserKnownHostsFile=/dev/null");
if let Some(u) = username {
ssh_cmd.arg(format!("{u}@{hostname}"));
} else {
ssh_cmd.arg(hostname);
}
if interactive || tty {
ssh_cmd.arg("-t");
}
ssh_cmd.args(command);
let output = ssh_cmd
.output()
.map_err(|err| IoError::new(ErrorKind::Other, format!("Failed to spawn ssh: {err}")))?;
if !output.status.success() {
return Err(IoError::new(
ErrorKind::Other,
format!(
"SSH command failed with status: {:?}\nStderr:\n{}",
output.status.code(),
String::from_utf8_lossy(&output.stderr),
),
));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
struct Client {}
impl client::Handler for Client {
type Error = russh::Error;
async fn check_server_key(
&mut self,
_server_public_key: &ssh_key::PublicKey,
) -> Result<bool, Self::Error> {
Ok(true)
}
}
pub async fn exec_ssh_command<A: ToSocketAddrs>(
host: A,
username: &str,
private_key: &str,
command: &str,
) -> anyhow::Result<String>
where
A: ToSocketAddrs + std::fmt::Debug,
{
let config = Config::default();
let config = Arc::new(config);
debug!("Connecting to SSH server");
let handler = Client {};
let mut session = client::connect(config, host, handler).await?;
let raw_key = russh::keys::decode_secret_key(private_key, None)?;
let key_with_hash = PrivateKeyWithHashAlg::new(Arc::new(raw_key), Some(HashAlg::Sha256));
match session
.authenticate_publickey(username, key_with_hash)
.await?
{
russh::client::AuthResult::Success => {
debug!("Authenticated with SSH server");
}
russh::client::AuthResult::Failure {
remaining_methods: _remaining_methods,
partial_success,
} => {
return Err(anyhow::anyhow!(
"Public-key authentication failed: partial_success={partial_success:?}",
));
}
}
let mut channel = session.channel_open_session().await?;
channel
.request_pty(true, "xterm", 80, 24, 0, 0, &[])
.await?;
channel.request_shell(true).await?;
let marker = "CMD_DONE_1234"; let script = format!(
"{cmd}\necho {marker}\nexit\n",
cmd = command,
marker = marker
);
let mut writer = channel.make_writer();
writer.write_all(script.as_bytes()).await?;
channel.eof().await?;
let mut output = String::new();
let mut buffer = String::new();
let mut collecting = false;
while let Some(msg) = channel.wait().await {
match msg {
ChannelMsg::Data { data } => {
if let Ok(text) = std::str::from_utf8(&data) {
buffer.push_str(text);
while let Some(newline_idx) = buffer.find('\n') {
let raw_line = buffer[..newline_idx].to_string();
let line = raw_line.replace('\r', "");
debug!("Line: '{}'", line.replace('\n', "\\n"));
buffer.drain(..=newline_idx);
if line.contains(command) {
collecting = true;
continue;
}
if line.contains(marker) {
collecting = false;
continue;
}
if collecting {
output.push_str(&line);
output.push('\n');
}
}
}
}
ChannelMsg::Eof | ChannelMsg::Close => {
break;
}
_ => {}
}
}
channel.close().await?;
debug!("Output: {}", output);
Ok(output)
}
pub async fn exec_ssh_command_async_ssh2tokio(
host: &str,
username: &str,
private_key: &str,
command: &str,
) -> Result<String, async_ssh2_tokio::Error> {
use async_ssh2_tokio::client::{AuthMethod, Client, ServerCheckMethod};
let auth_method = AuthMethod::with_key(private_key, None);
let client = Client::connect(
(host, 22), username,
auth_method,
ServerCheckMethod::NoCheck,
)
.await?;
let result = client.execute(command).await?;
client.disconnect().await?;
Ok(result.stdout)
}
pub fn open_ssh_shell(
host: &str,
username: &str,
private_key: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let tcp = TcpStream::connect((host, 22))?;
let mut session = Session::new()?;
session.set_tcp_stream(tcp);
session.handshake()?;
session.userauth_pubkey_memory(username, None, private_key, None)?;
if !session.authenticated() {
return Err("SSH authentication failed".into());
}
let mut channel = session.channel_session()?;
channel.request_pty("xterm", None, None)?;
channel.shell()?;
let mut stdin_clone = stdin();
let mut stdout_clone = stdout();
let mut remote_out = channel.stream(0);
let mut buf = [0u8; 1024];
loop {
if let Ok(size) = remote_out.read(&mut buf) {
if size == 0 {
break;
}
stdout_clone.write_all(&buf[..size])?;
stdout_clone.flush()?;
}
if let Ok(size) = stdin_clone.read(&mut buf) {
if size > 0 {
channel.write_all(&buf[..size])?;
}
}
}
channel.wait_close()?;
Ok(())
}