use crate::*;
use ssh2::{Channel, FileStat, OpenFlags, OpenType, Session};
use std::{
env, fs, io,
io::{Read, Write},
net::{TcpStream, ToSocketAddrs},
path::{Path, PathBuf},
time::{Duration, Instant},
};
type HostAddr = String;
type HostAddrRef<'a> = &'a str;
type User = String;
type UserRef<'a> = &'a str;
type Port = u16;
#[inline(always)]
fn ssh_timeout_secs() -> u32 {
env::var("RUC_SSH_TIMEOUT")
.ok()
.and_then(|t| info!(t.parse::<u32>(), t).ok())
.unwrap_or(20)
.min(300)
}
fn drain_channel(
sess: &Session,
channel: &mut Channel,
) -> Result<(Vec<u8>, Vec<u8>)> {
fn drain_stream(
r: &mut impl Read,
dst: &mut Vec<u8>,
buf: &mut [u8],
) -> Result<bool> {
let mut progressed = false;
loop {
match r.read(buf) {
Ok(0) => return Ok(progressed), Ok(n) => {
dst.extend_from_slice(&buf[..n]);
progressed = true;
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
return Ok(progressed);
}
Err(e) => return Err(eg!(e)),
}
}
}
let idle_timeout = Duration::from_secs(ssh_timeout_secs() as u64);
let mut deadline = Instant::now() + idle_timeout;
let mut stdout = vec![];
let mut stderr = vec![];
let mut buf = [0u8; 16 * 1024];
sess.set_blocking(false);
let ret = loop {
let out_progress = match drain_stream(channel, &mut stdout, &mut buf) {
Ok(p) => p,
Err(e) => break Err(e),
};
let err_progress =
match drain_stream(&mut channel.stderr(), &mut stderr, &mut buf) {
Ok(p) => p,
Err(e) => break Err(e),
};
if out_progress || err_progress {
deadline = Instant::now() + idle_timeout;
} else {
if channel.eof() {
break Ok((stdout, stderr));
}
if deadline < Instant::now() {
break Err(eg!("channel-drain timeout(no data incoming)"));
}
sleep_ms!(1);
}
};
sess.set_blocking(true);
ret.c(d!())
}
#[derive(Debug)]
pub struct RemoteHost<'a> {
pub addr: HostAddrRef<'a>,
pub user: UserRef<'a>,
pub port: Port,
pub local_sk: &'a Path,
}
impl RemoteHost<'_> {
fn id(&self) -> String {
format!(
"{}|{}|{}|{}",
self.addr,
self.user,
self.port,
self.local_sk.to_str().unwrap_or_default()
)
}
fn gen_session(&self) -> Result<Session> {
let timeout = ssh_timeout_secs();
let mut sess = Session::new().c(d!())?;
let endpoint = format!("{}:{}", &self.addr, self.port);
let mut tcp = Err(eg!("no address resolved from `{}`", &endpoint));
for addr in endpoint.to_socket_addrs().c(d!(&endpoint))? {
match TcpStream::connect_timeout(
&addr,
Duration::from_secs(timeout as u64),
) {
Ok(s) => {
tcp = Ok(s);
break;
}
Err(e) => tcp = Err(eg!("{}: {}", addr, e)),
}
}
sess.set_tcp_stream(tcp.c(d!(&endpoint))?);
sess.set_timeout(timeout * 1000);
sess.set_blocking(true);
sess.handshake().c(d!()).and_then(|_| {
let p = PathBuf::from(self.local_sk);
sess.userauth_pubkey_file(self.user, None, p.as_path(), None)
.c(d!())
})?;
Ok(sess)
}
pub fn exec_cmd(&self, cmd: &str) -> Result<Vec<u8>> {
let sess = self.gen_session().c(d!())?;
let mut channel = sess.channel_session().c(d!())?;
channel.exec(cmd).c(d!())?;
channel.send_eof().c(d!())?;
let (stdout, stderr) = drain_channel(&sess, &mut channel).c(d!())?;
channel.wait_eof().c(d!())?;
channel.close().c(d!())?;
channel.wait_close().c(d!())?;
let stderr = String::from_utf8_lossy(&stderr);
match channel.exit_status() {
Ok(code) => {
if 0 == code {
Ok(stdout)
} else {
Err(eg!(
"STDOUT: {}; STDERR: [{}] {stderr}",
String::from_utf8_lossy(&stdout),
self.id(),
))
}
}
Err(e) => {
info!(Err(eg!(
"STDOUT: {}; STDERR: [{}] {stderr}\n{}",
String::from_utf8_lossy(&stdout),
self.id(),
e,
)))
}
}
}
pub fn exec_exit_code(&self, cmd: &str) -> Result<i32> {
let sess = self.gen_session().c(d!())?;
let mut channel = sess.channel_session().c(d!())?;
channel.exec(cmd).c(d!())?;
channel.send_eof().c(d!())?;
drain_channel(&sess, &mut channel).c(d!())?;
channel.wait_eof().c(d!())?;
channel.close().c(d!())?;
channel.wait_close().c(d!())?;
channel.exit_status().c(d!(self.id()))
}
pub fn file_stat<P: AsRef<Path>>(&self, path: P) -> Result<FileStat> {
let sess = self.gen_session().c(d!())?;
let sftp = sess.sftp().c(d!())?;
sftp.stat(path.as_ref()).c(d!(self.id()))
}
pub fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
let sess = self.gen_session().c(d!())?;
let sftp = sess.sftp().c(d!())?;
let mut file = sftp.open(path.as_ref()).c(d!(self.id()))?;
let mut buf = Vec::new();
file.read_to_end(&mut buf).c(d!())?;
Ok(buf)
}
pub fn replace_file<P: AsRef<Path>>(
&self,
remote_path: P,
contents: &[u8],
) -> Result<()> {
let sess = self.gen_session().c(d!(self.id()))?;
let sftp = sess.sftp().c(d!())?;
let mut remote_file = sftp
.open_mode(
remote_path.as_ref(),
OpenFlags::CREATE | OpenFlags::WRITE | OpenFlags::TRUNCATE,
0o644,
OpenType::File,
)
.c(d!(self.id()))?;
remote_file.write_all(contents).c(d!(self.id()))?;
remote_file.fsync().c(d!())
}
pub fn append_file<P: AsRef<Path>>(
&self,
remote_path: P,
contents: &[u8],
) -> Result<()> {
let sess = self.gen_session().c(d!(self.id()))?;
let sftp = sess.sftp().c(d!())?;
let mut remote_file = sftp
.open_mode(
remote_path.as_ref(),
OpenFlags::CREATE | OpenFlags::WRITE | OpenFlags::APPEND,
0o644,
OpenType::File,
)
.c(d!())?;
remote_file.write_all(contents).c(d!(self.id()))?;
remote_file.fsync().c(d!())
}
#[inline(always)]
pub fn put_file<LP: AsRef<Path>, RP: AsRef<Path>>(
&self,
local_path: LP,
remote_path: RP,
) -> Result<()> {
self.scp(local_path, remote_path, true).c(d!())
}
#[inline(always)]
pub fn get_file<RP: AsRef<Path>, LP: AsRef<Path>>(
&self,
remote_path: RP,
local_path: LP,
) -> Result<()> {
self.scp(local_path, remote_path, false).c(d!())
}
pub fn scp<LP: AsRef<Path>, RP: AsRef<Path>>(
&self,
local_path: LP,
remote_path: RP,
direction_is_out: bool,
) -> Result<()> {
if direction_is_out {
fs::read(local_path.as_ref()).c(d!()).and_then(|contents| {
self.replace_file(remote_path, &contents).c(d!())
})
} else {
self.read_file(remote_path)
.c(d!())
.and_then(|contents| fs::write(local_path, contents).c(d!()))
}
}
}
#[derive(Debug)]
pub struct RemoteHostOwned {
pub addr: HostAddr,
pub user: User,
pub port: Port,
pub local_sk: PathBuf,
}
impl RemoteHostOwned {
#[inline(always)]
pub fn new_default(addr: HostAddr, remote_user: User) -> Result<Self> {
let home = env::var("HOME").c(d!())?;
let rsa_key_path = PathBuf::from(format!("{}/.ssh/id_rsa", &home));
let ed25519_key_path = PathBuf::from(home + "/.ssh/id_ed25519");
let local_sk;
if ed25519_key_path.exists() {
local_sk = ed25519_key_path;
} else if rsa_key_path.exists() {
local_sk = rsa_key_path;
} else {
return Err(eg!(
"Private key not found, neither RSA nor ED25519."
));
};
Ok(Self {
addr,
user: remote_user,
port: 22,
local_sk,
})
}
}
impl<'a> From<&'a RemoteHostOwned> for RemoteHost<'a> {
fn from(o: &'a RemoteHostOwned) -> RemoteHost<'a> {
Self {
addr: o.addr.as_str(),
user: o.user.as_str(),
port: o.port,
local_sk: o.local_sk.as_path(),
}
}
}