You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
distant/src/cli/commands/manager/handlers.rs

454 lines
15 KiB
Rust

use crate::config::ClientLaunchConfig;
use async_trait::async_trait;
use distant_core::net::client::{Client, ClientConfig, ReconnectStrategy, UntypedClient};
use distant_core::net::common::authentication::msg::*;
use distant_core::net::common::authentication::{
AuthHandler, Authenticator, DynAuthHandler, ProxyAuthHandler, SingleAuthHandler,
StaticKeyAuthMethodHandler,
};
use distant_core::net::common::{Destination, Map, SecretKey32};
use distant_core::net::manager::{ConnectHandler, LaunchHandler};
use log::*;
use std::{
io,
net::{IpAddr, SocketAddr},
path::PathBuf,
process::Stdio,
time::Duration,
};
use tokio::{
io::{AsyncBufReadExt, BufReader},
process::{Child, Command},
sync::Mutex,
};
#[inline]
fn missing(label: &str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, format!("Missing {label}"))
}
#[inline]
fn invalid(label: &str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, format!("Invalid {label}"))
}
/// Supports launching locally through the manager as defined by `manager://...`
pub struct ManagerLaunchHandler {
servers: Mutex<Vec<Child>>,
}
impl ManagerLaunchHandler {
pub fn new() -> Self {
Self {
servers: Mutex::new(Vec::new()),
}
}
}
#[async_trait]
impl LaunchHandler for ManagerLaunchHandler {
async fn launch(
&self,
destination: &Destination,
options: &Map,
_authenticator: &mut dyn Authenticator,
) -> io::Result<Destination> {
debug!("Handling launch of {destination} with options '{options}'");
let config = ClientLaunchConfig::from(options.clone());
// Get the path to the distant binary, ensuring it exists and is executable
let program = which::which(match config.distant.bin {
Some(bin) => PathBuf::from(bin),
None => std::env::current_exe().unwrap_or_else(|_| {
PathBuf::from(if cfg!(windows) {
"distant.exe"
} else {
"distant"
})
}),
})
.map_err(|x| io::Error::new(io::ErrorKind::NotFound, x))?;
// Build our command to run
let mut args = vec![
String::from("server"),
String::from("listen"),
String::from("--host"),
config
.distant
.bind_server
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| String::from("any")),
];
if let Some(port) = destination.port {
args.push("--port".to_string());
args.push(port.to_string());
}
// Add any options arguments to the command
if let Some(options_args) = config.distant.args {
// NOTE: Split arguments based on whether we are running on windows or unix
args.extend(if cfg!(windows) {
winsplit::split(&options_args)
} else {
shell_words::split(&options_args)
.map_err(|x| io::Error::new(io::ErrorKind::InvalidInput, x))?
});
}
// Spawn it and wait to get the communicated destination
// NOTE: Server will persist until this handler is dropped
let mut command = Command::new(program);
command
.kill_on_drop(true)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
debug!("Launching local to manager by spawning command: {command:?}");
let mut child = command.spawn()?;
let mut stdout = BufReader::new(child.stdout.take().unwrap());
let mut line = String::new();
loop {
match stdout.read_line(&mut line).await {
Ok(n) if n > 0 => {
if let Ok(destination) = line[..n].trim().parse::<Destination>() {
// Store a reference to the server so we can terminate them
// when this handler is dropped
self.servers.lock().await.push(child);
break Ok(destination);
} else {
line.clear();
}
}
// If we reach the point of no more data, then fail with EOF
Ok(_) => {
// Ensure that the server is terminated
child.kill().await?;
break Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Missing output destination",
));
}
// If we fail to read a line, we assume that the child has completed
// and we missed it, so capture the stderr to report issues
Err(x) => {
let output = child.wait_with_output().await?;
break Err(io::Error::new(
io::ErrorKind::Other,
String::from_utf8(output.stderr).unwrap_or_else(|_| x.to_string()),
));
}
}
}
}
}
/// Supports launching remotely via SSH as defined by `ssh://...`
#[cfg(any(feature = "libssh", feature = "ssh2"))]
pub struct SshLaunchHandler;
#[cfg(any(feature = "libssh", feature = "ssh2"))]
#[async_trait]
impl LaunchHandler for SshLaunchHandler {
async fn launch(
&self,
destination: &Destination,
options: &Map,
authenticator: &mut dyn Authenticator,
) -> io::Result<Destination> {
debug!("Handling launch of {destination} with options '{options}'");
let config = ClientLaunchConfig::from(options.clone());
use distant_ssh2::DistantLaunchOpts;
let mut ssh = load_ssh(destination, options)?;
let handler = AuthClientSshAuthHandler::new(authenticator);
let _ = ssh.authenticate(handler).await?;
let opts = {
let opts = DistantLaunchOpts::default();
DistantLaunchOpts {
binary: config.distant.bin.unwrap_or(opts.binary),
args: config.distant.args.unwrap_or(opts.args),
use_login_shell: !config.distant.no_shell,
timeout: match options.get("timeout") {
Some(s) => std::time::Duration::from_millis(
s.parse::<u64>().map_err(|_| invalid("timeout"))?,
),
None => opts.timeout,
},
}
};
debug!("Launching via ssh: {opts:?}");
ssh.launch(opts).await?.try_to_destination()
}
}
/// Supports connecting to a remote distant TCP server as defined by `distant://...`
pub struct DistantConnectHandler;
impl DistantConnectHandler {
async fn try_connect(
ips: Vec<IpAddr>,
port: u16,
mut auth_handler: impl AuthHandler,
) -> io::Result<UntypedClient> {
// Try each IP address with the same port to see if one works
let mut err = None;
for ip in ips {
let addr = SocketAddr::new(ip, port);
debug!("Attempting to connect to distant server @ {}", addr);
match Client::tcp(addr)
.auth_handler(DynAuthHandler::from(&mut auth_handler))
.config(ClientConfig {
reconnect_strategy: ReconnectStrategy::ExponentialBackoff {
base: Duration::from_secs(1),
factor: 2.0,
max_duration: Some(Duration::from_secs(10)),
max_retries: None,
timeout: None,
},
..Default::default()
})
.connect_timeout(Duration::from_secs(180))
.connect_untyped()
.await
{
Ok(client) => return Ok(client),
Err(x) => err = Some(x),
}
}
// If all failed, return the last error we got
Err(err.expect("Err set above"))
}
}
#[async_trait]
impl ConnectHandler for DistantConnectHandler {
async fn connect(
&self,
destination: &Destination,
options: &Map,
authenticator: &mut dyn Authenticator,
) -> io::Result<UntypedClient> {
debug!("Handling connect of {destination} with options '{options}'");
let host = destination.host.to_string();
let port = destination.port.ok_or_else(|| missing("port"))?;
debug!("Looking up host {host} @ port {port}");
let mut candidate_ips = tokio::net::lookup_host(format!("{host}:{port}"))
.await
.map_err(|x| {
io::Error::new(
x.kind(),
format!("{host} needs to be resolvable outside of ssh: {x}"),
)
})?
.into_iter()
.map(|addr| addr.ip())
.collect::<Vec<IpAddr>>();
candidate_ips.sort_unstable();
candidate_ips.dedup();
if candidate_ips.is_empty() {
return Err(io::Error::new(
io::ErrorKind::AddrNotAvailable,
format!("Unable to resolve {host}:{port}"),
));
}
// For legacy reasons, we need to support a static key being provided
// via part of the destination OR an option, and attempt to use it
// during authentication if it is provided
if let Some(key) = destination
.password
.as_deref()
.or_else(|| options.get("key").map(|s| s.as_str()))
{
let key = key.parse::<SecretKey32>().map_err(|_| invalid("key"))?;
Self::try_connect(
candidate_ips,
port,
SingleAuthHandler::new(StaticKeyAuthMethodHandler::simple(key)),
)
.await
} else {
Self::try_connect(candidate_ips, port, ProxyAuthHandler::new(authenticator)).await
}
}
}
/// Supports connecting to a remote SSH server as defined by `ssh://...`
#[cfg(any(feature = "libssh", feature = "ssh2"))]
pub struct SshConnectHandler;
#[cfg(any(feature = "libssh", feature = "ssh2"))]
#[async_trait]
impl ConnectHandler for SshConnectHandler {
async fn connect(
&self,
destination: &Destination,
options: &Map,
authenticator: &mut dyn Authenticator,
) -> io::Result<UntypedClient> {
debug!("Handling connect of {destination} with options '{options}'");
let mut ssh = load_ssh(destination, options)?;
let handler = AuthClientSshAuthHandler::new(authenticator);
let _ = ssh.authenticate(handler).await?;
Ok(ssh.into_distant_client().await?.into_untyped_client())
}
}
#[cfg(any(feature = "libssh", feature = "ssh2"))]
struct AuthClientSshAuthHandler<'a>(Mutex<&'a mut dyn Authenticator>);
#[cfg(any(feature = "libssh", feature = "ssh2"))]
impl<'a> AuthClientSshAuthHandler<'a> {
pub fn new(authenticator: &'a mut dyn Authenticator) -> Self {
Self(Mutex::new(authenticator))
}
}
#[cfg(any(feature = "libssh", feature = "ssh2"))]
#[async_trait]
impl<'a> distant_ssh2::SshAuthHandler for AuthClientSshAuthHandler<'a> {
async fn on_authenticate(&self, event: distant_ssh2::SshAuthEvent) -> io::Result<Vec<String>> {
use std::collections::HashMap;
let mut options = HashMap::new();
let mut questions = Vec::new();
for prompt in event.prompts {
let mut options = HashMap::new();
options.insert("echo".to_string(), prompt.echo.to_string());
questions.push(Question {
label: "ssh-prompt".to_string(),
text: prompt.prompt,
options,
});
}
options.insert("instructions".to_string(), event.instructions);
options.insert("username".to_string(), event.username);
Ok(self
.0
.lock()
.await
.challenge(Challenge { questions, options })
.await?
.answers)
}
async fn on_verify_host(&self, host: &str) -> io::Result<bool> {
Ok(self
.0
.lock()
.await
.verify(Verification {
kind: VerificationKind::Host,
text: host.to_string(),
})
.await?
.valid)
}
async fn on_banner(&self, text: &str) {
if let Err(x) = self
.0
.lock()
.await
.info(Info {
text: text.to_string(),
})
.await
{
error!("ssh on_banner failed: {}", x);
}
}
async fn on_error(&self, text: &str) {
if let Err(x) = self
.0
.lock()
.await
.error(Error {
kind: ErrorKind::Fatal,
text: text.to_string(),
})
.await
{
error!("ssh on_error failed: {}", x);
}
}
}
#[cfg(any(feature = "libssh", feature = "ssh2"))]
fn load_ssh(destination: &Destination, options: &Map) -> io::Result<distant_ssh2::Ssh> {
trace!("load_ssh({destination}, {options})");
use distant_ssh2::{Ssh, SshOpts};
let host = destination.host.to_string();
let opts = SshOpts {
backend: match options
.get("backend")
.or_else(|| options.get("ssh.backend"))
{
Some(s) => s.parse().map_err(|_| invalid("backend"))?,
None => Default::default(),
},
identity_files: options
.get("identity_files")
.or_else(|| options.get("ssh.identity_files"))
.map(|s| s.split(',').map(|s| PathBuf::from(s.trim())).collect())
.unwrap_or_default(),
identities_only: match options
.get("identities_only")
.or_else(|| options.get("ssh.identities_only"))
{
Some(s) => Some(s.parse().map_err(|_| invalid("identities_only"))?),
None => None,
},
port: destination.port,
proxy_command: options
.get("proxy_command")
.or_else(|| options.get("ssh.proxy_command"))
.cloned(),
user: destination.username.clone(),
user_known_hosts_files: options
.get("user_known_hosts_files")
.or_else(|| options.get("ssh.user_known_hosts_files"))
.map(|s| s.split(',').map(|s| PathBuf::from(s.trim())).collect())
.unwrap_or_default(),
verbose: match options
.get("verbose")
.or_else(|| options.get("ssh.verbose"))
{
Some(s) => s.parse().map_err(|_| invalid("verbose"))?,
None => false,
},
..Default::default()
};
debug!("Connecting to {host} via ssh with {opts:?}");
Ssh::connect(host, opts)
}