use std::error::Error;
use std::io;
use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
use anyhow::format_err;
use log::debug;
use ssh2::{Channel, Session};
use telnet::{Event, Telnet};
use regex::Regex;
pub trait Connection {
type ConnectionHandler;
fn connect<A: ToSocketAddrs>(addr: A, username: Option<&str>, password: Option<&str>) -> Result<Self::ConnectionHandler, Box<dyn Error>>;
fn read_ignore(&mut self, prompt_end: &Regex);
fn execute_raw(&mut self, command: &str, prompt_end: &Regex) -> io::Result<()>;
}
pub struct TelnetConnection {
conn: Telnet,
}
impl Connection for TelnetConnection {
type ConnectionHandler = TelnetConnection;
fn connect<A: ToSocketAddrs>(addr: A, username: Option<&str>, password: Option<&str>) -> Result<TelnetConnection, Box<dyn Error>> {
let mut conn = TelnetConnection {
conn: Telnet::connect(addr, 1024)?,
};
if let Some(username) = username {
conn.execute_raw(username, &Regex::new("[Pp]assword")?)?;
if let Some(password) = password {
conn.execute_raw(password, &Regex::new("^([Uu]sername)")?)?;
}
}
Ok(conn)
}
fn read_ignore(&mut self, prompt_end: &Regex) {
loop {
let event = self.conn.read_timeout(Duration::from_secs(1));
if let Ok(Event::TimedOut) = event {
break;
}
if let Err(e) = event {
debug!("Error discarded: {}", e);
break;
}
if let Ok(Event::Data(data)) = event {
let datastr = String::from_utf8_lossy(&data);
debug!("Discarded data {}", datastr);
if prompt_end.is_match(datastr.trim_end()) {
debug!("Found prompt. Ready for next command");
break;
}
} else {
debug!("Discarded event: {:?}", event);
}
}
}
fn execute_raw(&mut self, command: &str, prompt_end: &Regex) -> io::Result<()> {
self.conn.write(command.as_bytes())?;
self.conn.write(b"\n")?;
self.read_ignore(prompt_end);
Ok(())
}
}
pub struct SSHConnection {
#[allow(dead_code)]
sess: Session,
channel: Channel,
}
impl SSHConnection {
fn establish_connection<A: ToSocketAddrs>(addr: A, timeout: Option<Duration>) -> Result<Session, Box<dyn Error>> {
let tcp = match timeout {
None => TcpStream::connect(addr)?,
Some(timeout) => {
let mut result = None;
for addr in addr.to_socket_addrs()? {
result = Some(TcpStream::connect_timeout(&addr, timeout));
match result {
Some(Ok(_)) => break,
_ => continue,
}
}
match result {
None => Err(format_err!("No socket address was supplied in addr"))?,
Some(result) => result?
}
}
};
let mut sess = Session::new()?;
sess.set_timeout(60000);
sess.set_tcp_stream(tcp);
sess.handshake()?;
Ok(sess)
}
fn make_channel_session(session: Session) -> Result<SSHConnection, Box<dyn Error>>{
let mut channel = session.channel_session()?;
channel.request_pty("rustmiko", None, None)?;
channel.shell()?;
Ok(SSHConnection {
sess: session,
channel,
})
}
pub fn connect_agentauth<A: ToSocketAddrs>(addr: A, username: &str, timeout: Option<Duration>) -> Result<SSHConnection, Box<dyn Error>> {
let sess = Self::establish_connection(addr, timeout)?;
sess.userauth_agent(username)?;
if !sess.authenticated() {
return Err("Couldn't authenticate properly against SSH Server using SSH Agent.".into());
}
Self::make_channel_session(sess)
}
}
impl Connection for SSHConnection {
type ConnectionHandler = SSHConnection;
fn connect<A: ToSocketAddrs>(addr: A, username: Option<&str>, password: Option<&str>) -> Result<SSHConnection, Box<dyn Error>> {
if username.is_none() || password.is_none() {
return Err("Can't connect to SSH without username and password".into());
}
let sess = Self::establish_connection(addr, None)?;
let username = username.unwrap();
let password = password.unwrap();
sess.userauth_password(username, password)?;
if !sess.authenticated() {
return Err("Couldn't authenticate properly against SSH Server using password auth".into());
}
Self::make_channel_session(sess)
}
fn read_ignore(&mut self, prompt_end: &Regex) {
debug!("Reading...");
loop {
let mut buf = [0u8; 1024];
let size = match self.channel.read(&mut buf) {
Ok(s) => s,
Err(ref e) if e.kind() == io::ErrorKind::TimedOut => {
debug!("Timed out... Assuming no data");
break;
}
Err(e) => {
debug!("Ignored error: {}", e);
break;
},
};
let str = String::from_utf8_lossy(&buf[..size]);
debug!("Ignored \"{}\"", str);
if prompt_end.is_match(&str) {
debug!("Found prompt. Ready for next command");
break;
}
}
}
fn execute_raw(&mut self, command: &str, prompt_end: &Regex) -> io::Result<()> {
debug!("Wrote: {}", command);
self.channel.write_all(command.as_bytes())?;
self.channel.write_all(b"\n")?;
self.read_ignore(prompt_end);
Ok(())
}
}