use crate::connection::connection::Connection;
use crate::connection::command::CommandResult;
use crate::connection::factory::ConnectionFactory;
use crate::playbooks::context::PlaybookContext;
use crate::connection::local::LocalFactory;
use crate::tasks::*;
use crate::inventory::hosts::Host;
use crate::Inventory;
use crate::handle::response::Response;
use crate::connection::command::Forward;
use crate::connection::local::convert_out;
use std::process::Command;
use std::sync::{Arc,Mutex,RwLock};
use ssh2::Session;
use std::io::{Read,Write};
use std::net::TcpStream;
use std::path::Path;
use std::time::Duration;
use std::net::ToSocketAddrs;
use std::fs::File;
pub struct SshFactory {
local_factory: LocalFactory,
localhost: Arc<RwLock<Host>>,
forward_agent: bool,
login_password: Option<String>
}
impl SshFactory {
pub fn new(inventory: &Arc<RwLock<Inventory>>, forward_agent: bool, login_password: Option<String>) -> Self {
Self {
localhost : inventory.read().expect("inventory read").get_host(&String::from("localhost")),
local_factory: LocalFactory::new(inventory),
forward_agent,
login_password
}
}
}
impl ConnectionFactory for SshFactory {
fn get_local_connection(&self, context: &Arc<RwLock<PlaybookContext>>) -> Result<Arc<Mutex<dyn Connection>>, String> {
return Ok(self.local_factory.get_connection(context, &self.localhost)?);
}
fn get_connection(&self, context: &Arc<RwLock<PlaybookContext>>, host:&Arc<RwLock<Host>>) -> Result<Arc<Mutex<dyn Connection>>, String> {
let ctx = context.read().expect("context read");
let hostname1 = host.read().expect("host read").name.clone();
if hostname1.eq("localhost") {
let conn : Arc<Mutex<dyn Connection>> = self.local_factory.get_connection(context, &self.localhost)?;
return Ok(conn);
}
{
let cache = ctx.connection_cache.read().unwrap();
if cache.has_connection(host) {
let conn = cache.get_connection(host);
return Ok(conn);
}
}
let (hostname2, user, port) = ctx.get_ssh_connection_details(host);
if hostname2.eq("localhost") {
let conn : Arc<Mutex<dyn Connection>> = self.local_factory.get_connection(context, &self.localhost)?;
return Ok(conn);
}
let mut conn = SshConnection::new(Arc::clone(&host), &user, port, self.forward_agent, self.login_password.clone());
return match conn.connect() {
Ok(_) => {
let conn2 : Arc<Mutex<dyn Connection>> = Arc::new(Mutex::new(conn));
ctx.connection_cache.write().expect("connection cache write").add_connection(
&Arc::clone(&host), &Arc::clone(&conn2));
Ok(conn2)
},
Err(x) => { Err(x) }
}
}
}
pub struct SshConnection {
pub host: Arc<RwLock<Host>>,
pub username: String,
pub port: i64,
pub session: Option<Session>,
pub forward_agent: bool,
pub login_password: Option<String>
}
impl SshConnection {
pub fn new(host: Arc<RwLock<Host>>, username: &String, port: i64, forward_agent: bool, login_password: Option<String>) -> Self {
Self { host: Arc::clone(&host), username: username.clone(), port, session: None, forward_agent, login_password }
}
}
impl Connection for SshConnection {
fn whoami(&self) -> Result<String,String> {
return Ok(self.username.clone());
}
fn connect(&mut self) -> Result<(), String> {
if self.session.is_some() {
return Ok(());
}
let session = match Session::new() { Ok(x) => x, Err(_y) => { return Err(String::from("failed to attach to session")); } };
let mut agent = match session.agent() { Ok(x) => x, Err(_y) => { return Err(String::from("failed to acquire SSH-agent")); } };
match agent.connect() { Ok(_x) => {}, Err(_y) => { return Err(String::from("failed to connect to SSH-agent")) }}
let seconds = Duration::from_secs(10);
assert!(!self.host.read().expect("host read").name.eq("localhost"));
let connect_str = format!("{host}:{port}", host=self.host.read().expect("host read").name, port=self.port.to_string());
let addrs_iter = connect_str.as_str().to_socket_addrs();
let mut addrs_iter2 = match addrs_iter { Err(_x) => { return Err(String::from("unable to resolve")); }, Ok(y) => y };
let addr = addrs_iter2.next();
if ! addr.is_some() { return Err(String::from("unable to resolve(2)")); }
let tcp = match TcpStream::connect_timeout(&addr.unwrap(), seconds) { Ok(x) => x, _ => {
return Err(format!("SSH connection attempt failed for {}:{}", self.host.read().expect("host read").name, self.port)); } };
let mut sess = match Session::new() { Ok(x) => x, _ => { return Err(String::from("SSH session failed")); } };
sess.set_tcp_stream(tcp);
match sess.handshake() { Ok(_) => {}, _ => { return Err(String::from("SSH handshake failed")); } } ;
if self.login_password.is_some() {
match sess.userauth_password(&self.username.clone(), self.login_password.clone().unwrap().as_str()) {
Ok(_) => {},
Err(x) => {
return Err(format!("SSH password authentication failed for user {}: {}", self.username, x));
}
}
} else {
match sess.userauth_agent(&self.username) {
Ok(_) => {},
Err(x) => {
return Err(format!("SSH agent authentication failed for user {}: {}", self.username, x));
}
};
}
if !(sess.authenticated()) { return Err("failed to authenticate".to_string()); };
self.session = Some(sess);
let uname_result = self.run_command_low_level(&String::from("uname -a"));
match uname_result {
Ok((_rc,out)) => {
{
match self.host.write().unwrap().set_os_info(&out.clone()) {
Ok(_x) => {},
Err(_y) => return Err(format!("failed to set OS info"))
}
}
},
Err((rc,out)) => return Err(format!("uname -a command failed: rc={}, out={}", rc,out))
}
return Ok(());
}
fn run_command(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, cmd: &String, forward: Forward) -> Result<Arc<TaskResponse>,Arc<TaskResponse>> {
let result = match forward {
Forward::Yes => match self.forward_agent {
false => self.run_command_low_level(cmd),
true => self.run_command_with_ssh_a(cmd)
},
Forward::No => self.run_command_low_level(cmd)
};
match result {
Ok((rc,s)) => {
return Ok(response.command_ok(request, &Arc::new(Some(CommandResult { cmd: cmd.clone(), out: s.clone(), rc: rc }))));
},
Err((rc,s)) => {
return Err(response.command_failed(request, &Arc::new(Some(CommandResult { cmd: cmd.clone(), out: s.clone(), rc: rc }))));
}
}
}
fn write_data(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, data: &String, remote_path: &String) -> Result<(),Arc<TaskResponse>> {
let session = self.session.as_ref().expect("session not established");
let sftp_result = session.sftp();
let sftp = match sftp_result {
Ok(x) => x,
Err(y) => { return Err(response.is_failed(request, &format!("sftp connection failed: {y}"))); }
};
let sftp_path = Path::new(&remote_path);
let fh_result = sftp.create(sftp_path);
let mut fh = match fh_result {
Ok(x) => x,
Err(y) => { return Err(response.is_failed(request, &format!("sftp open failed: {y}"))) }
};
let bytes = data.as_bytes();
match fh.write_all(bytes) {
Ok(_x) => {},
Err(y) => { return Err(response.is_failed(request, &format!("sftp write failed: {y}"))); }
}
return Ok(());
}
fn copy_file(&self, response: &Arc<Response>, request: &Arc<TaskRequest>, src: &Path, remote_path: &String) -> Result<(), Arc<TaskResponse>> {
let src_open_result = File::open(src);
let mut src = match src_open_result {
Ok(x) => x,
Err(y) => { return Err(response.is_failed(request, &format!("failed to open source file: {y}"))); }
};
let session = self.session.as_ref().expect("session not established");
let sftp_result = session.sftp();
let sftp = match sftp_result {
Ok(x) => x,
Err(y) => { return Err(response.is_failed(request, &format!("sftp connection failed: {y}"))); }
};
let sftp_path = Path::new(&remote_path);
let fh_result = sftp.create(sftp_path);
let mut fh = match fh_result {
Ok(x) => x,
Err(y) => { return Err(response.is_failed(request, &format!("sftp write failed (1): {y}"))) }
};
let chunk_size = 64536;
loop {
let mut chunk = Vec::with_capacity(chunk_size);
let mut taken = std::io::Read::by_ref(&mut src).take(chunk_size as u64);
let take_result = taken.read_to_end(&mut chunk);
let n = match take_result {
Ok(x) => x,
Err(y) => { return Err(response.is_failed(request, &format!("failed during file transfer: {y}"))); }
};
if n == 0 { break; }
match fh.write(&chunk) {
Err(y) => { return Err(response.is_failed(request, &format!("sftp write failed: {y}"))); }
_ => {},
}
}
return Ok(());
}
}
impl SshConnection {
fn trim_newlines(&self, s: &mut String) {
if s.ends_with('\n') {
s.pop();
if s.ends_with('\r') {
s.pop();
}
}
}
fn run_command_low_level(&self, cmd: &String) -> Result<(i32,String),(i32,String)> {
let session = self.session.as_ref().unwrap();
let mut channel = match session.channel_session() {
Ok(x) => x,
Err(y) => { return Err((500, format!("channel session failed: {:?}", y))); }
};
let actual_cmd = format!("{} 2>&1", cmd);
match channel.exec(&actual_cmd) { Ok(_x) => {}, Err(y) => { return Err((500,y.to_string())) } };
let mut s = String::new();
match channel.read_to_string(&mut s) { Ok(_x) => {}, Err(y) => { return Err((500,y.to_string())) } };
let _w = channel.wait_close();
let exit_status = match channel.exit_status() { Ok(x) => x, Err(y) => { return Err((500,y.to_string())) } };
self.trim_newlines(&mut s);
return Ok((exit_status, s.clone()));
}
fn run_command_with_ssh_a(&self, cmd: &String) -> Result<(i32,String),(i32,String)> {
let mut base = Command::new("ssh");
let hostname = &self.host.read().unwrap().name;
let port = format!("{}", self.port);
let cmd2 = format!("{} 2>&1", cmd);
let command = base.arg(hostname).arg("-p").arg(port).arg("-l").arg(self.username.clone()).arg("-A").arg(cmd2);
match command.output() {
Ok(x) => {
match x.status.code() {
Some(rc) => {
let mut out = convert_out(&x.stdout,&x.stderr);
self.trim_newlines(&mut out);
return Ok((rc, out.clone()))
},
None => {
return Ok((418, String::from("")))
}
}
},
Err(_x) => {
return Err((404, String::from("")))
}
};
}
}