use std::process::Stdio;
use tokio::io::BufReader;
use anyhow::{Context as _, Result};
use indicatif::MultiProgress;
use tokio::io::AsyncBufReadExt;
use tracing::debug;
use crate::client::Parameters;
use crate::config::{Configuration, Configuration_Optional};
use crate::{cli::styles::maybe_strip_color, protocol::control::ConnectionType};
use crate::util::process::ProcessWrapper;
fn ssh_cli_args(
connection_type: ConnectionType,
ssh_hostname: &str,
config: &Configuration_Optional,
remote_trace: bool,
) -> Vec<String> {
let mut args = Vec::new();
let defaults = Configuration::system_default();
args.push(
match connection_type {
ConnectionType::Ipv4 => "-4",
ConnectionType::Ipv6 => "-6",
}
.to_owned(),
);
if let Some(username) = &config.remote_user {
if !username.is_empty() {
args.push("-l".to_owned());
args.push(username.clone());
}
}
args.extend_from_slice(config.ssh_options.as_ref().unwrap_or(&defaults.ssh_options));
args.push(ssh_hostname.to_owned());
if remote_trace {
args.push("RUST_LOG=qcp=trace".to_owned());
}
if config.ssh_subsystem.unwrap_or(false) {
args.extend_from_slice(&["-s".to_owned(), "qcp".to_owned()]);
} else {
let remote_qcp_binary = config
.remote_qcp_binary
.as_ref()
.unwrap_or(&defaults.remote_qcp_binary)
.clone();
args.push(remote_qcp_binary);
args.push("--server".to_owned());
}
args
}
pub(crate) fn create(
display: &MultiProgress,
working_config: &Configuration_Optional,
parameters: &Parameters,
ssh_hostname: &str,
connection_type: ConnectionType,
) -> Result<ProcessWrapper> {
let defaults = Configuration::system_default();
let mut server = tokio::process::Command::new(
working_config
.ssh
.as_deref()
.unwrap_or_else(|| &defaults.ssh),
);
let _ = server
.args(ssh_cli_args(
connection_type,
ssh_hostname,
working_config,
parameters.remote_trace,
))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.kill_on_drop(true);
if !parameters.quiet {
let _ = server.stderr(Stdio::piped());
} debug!("spawning command: {:?}", server);
let mut wrapper = ProcessWrapper::spawn(server)
.context("Could not launch control connection to remote server")?;
if !parameters.quiet {
let stderr = wrapper.stderr();
let Some(stderr) = stderr else {
anyhow::bail!("could not get stderr of remote process");
};
let cloned = display.clone();
let _reader = tokio::spawn(async move {
let mut reader = BufReader::new(stderr).lines();
while let Ok(Some(line)) = reader.next_line().await {
let line = maybe_strip_color(&line);
cloned.suspend(|| eprintln!("{line}"));
}
});
}
Ok(wrapper)
}
#[cfg(test)]
#[cfg(unix)]
#[cfg_attr(coverage_nightly, coverage(off))]
pub(crate) fn create_fake<B>(data: &B) -> ProcessWrapper
where
B: AsRef<[u8]> + Send + 'static,
{
use std::fmt::Write;
let mut encoded = String::new();
for byte in data.as_ref() {
write!(encoded, "\\0{byte:o}").expect("failed to write to encoded string");
}
eprintln!("Fake client will echo -e {encoded}");
let mut process = tokio::process::Command::new("echo");
let _ = process.args(["-en", &encoded]);
ProcessWrapper::spawn(process).expect("failed to start fake ssh client")
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod test {
use super::{Configuration_Optional, ConnectionType, ssh_cli_args};
fn vec_contains(v: &[String], s: &str) -> bool {
v.iter().any(|x| x == s)
}
fn vec_subslice<T: PartialEq>(mut haystack: &[T], needle: &[T]) -> bool {
if needle.is_empty() {
return true;
}
while !haystack.is_empty() {
if haystack.starts_with(needle) {
return true;
}
haystack = &haystack[1..];
}
false
}
fn vec_subslice_strings(haystack: &[String], needle1: &[&str]) -> bool {
let needle = needle1.iter().map(|s| String::from(*s)).collect::<Vec<_>>();
vec_subslice(haystack, &needle)
}
#[test]
fn connection_type() {
let cfg = Configuration_Optional::default();
let args = ssh_cli_args(ConnectionType::Ipv4, "", &cfg, false);
assert!(vec_contains(&args, "-4"));
assert!(!vec_contains(&args, "-6"));
let args = ssh_cli_args(ConnectionType::Ipv6, "", &cfg, false);
assert!(vec_contains(&args, "-6"));
assert!(!vec_contains(&args, "-4"));
}
#[test]
fn username() {
let args = ssh_cli_args(
ConnectionType::Ipv4,
"",
&Configuration_Optional::default(),
false,
);
assert!(!vec_contains(&args, "-l"));
let cfg1 = Configuration_Optional {
remote_user: Some("xyzy".to_owned()),
..Default::default()
};
let args = ssh_cli_args(ConnectionType::Ipv4, "", &cfg1, false);
assert!(vec_subslice_strings(&args, &["-l", "xyzy"]));
}
#[test]
fn hostname_ssh_opts() {
let xopts = ["--abc", "def", "--ghi", "jkl"];
let cfg1 = Configuration_Optional {
ssh_options: Some(xopts.map(String::from).to_vec()),
..Default::default()
};
let args = ssh_cli_args(ConnectionType::Ipv4, "my_host", &cfg1, false);
assert!(vec_contains(&args, "my_host"));
assert!(vec_subslice_strings(&args, &xopts));
}
#[test]
fn subsystem_mode() {
let host = "myserver";
let args = ssh_cli_args(
ConnectionType::Ipv4,
host,
&Configuration_Optional::default(),
false,
);
assert!(vec_subslice_strings(&args, &["qcp", "--server"]));
let cfg1 = Configuration_Optional {
ssh_subsystem: Some(true),
..Default::default()
};
let args1 = ssh_cli_args(ConnectionType::Ipv4, host, &cfg1, false);
assert!(vec_subslice_strings(&args1, &["-s", "qcp"]));
}
#[test]
fn remote_qcp_binary_override() {
let cfg1 = Configuration_Optional {
remote_qcp_binary: Some("/opt/qcp/bin/qcp".to_owned()),
..Default::default()
};
let args = ssh_cli_args(ConnectionType::Ipv4, "my_host", &cfg1, false);
assert!(vec_subslice_strings(
&args,
&["/opt/qcp/bin/qcp", "--server"]
));
let cfg2 = Configuration_Optional {
remote_qcp_binary: Some("/opt/qcp/bin/qcp".to_owned()),
ssh_subsystem: Some(true),
..Default::default()
};
let args2 = ssh_cli_args(ConnectionType::Ipv4, "my_host", &cfg2, false);
assert!(vec_subslice_strings(&args2, &["-s", "qcp"]));
assert!(!vec_contains(&args2, "/opt/qcp/bin/qcp"));
}
}