use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::time::Duration;
use remotefs::{RemoteError, RemoteErrorType, RemoteResult};
use ssh2_config::{DefaultAlgorithms, HostParams, ParseRule, SshConfig};
use super::SshOpts;
pub struct Config {
pub params: HostParams,
pub host: String,
pub resolved_host: String,
pub address: String,
pub username: String,
pub connection_timeout: Duration,
pub connection_attempts: usize,
}
impl Config {
fn from_params(params: HostParams, opts: &SshOpts) -> Self {
Config {
host: opts.host.to_string(),
resolved_host: Self::resolve_host(¶ms, opts),
address: Self::resolve_address(¶ms, opts),
username: Self::resolve_username(¶ms, opts),
connection_timeout: Self::resolve_connection_timeout(¶ms, opts),
connection_attempts: Self::resolve_connection_attempts(¶ms),
params,
}
}
fn parse(p: &Path, host: &str, rules: ParseRule) -> RemoteResult<HostParams> {
trace!("Parsing configuration at {}", p.display());
let mut reader = BufReader::new(File::open(p).map_err(|e| {
RemoteError::new_ex(
RemoteErrorType::IoError,
format!("Could not open configuration file: {e}"),
)
})?);
SshConfig::default()
.parse(&mut reader, rules)
.map_err(|e| {
RemoteError::new_ex(
RemoteErrorType::IoError,
format!("Could not parse configuration file: {e}"),
)
})
.map(|x| x.query(host))
}
fn resolve_host(params: &HostParams, opts: &SshOpts) -> String {
match params.host_name.as_deref() {
Some(h) => h.to_string(),
None => opts.host.to_string(),
}
}
fn resolve_address(params: &HostParams, opts: &SshOpts) -> String {
let host = Self::resolve_host(params, opts);
let port = match opts.port {
None => params.port.unwrap_or(22),
Some(p) => p,
};
format!("{host}:{port}")
}
fn resolve_username(params: &HostParams, opts: &SshOpts) -> String {
match opts.username.as_ref() {
Some(u) => u.to_string(),
None => params.user.as_deref().unwrap_or("").to_string(),
}
}
fn resolve_connection_timeout(params: &HostParams, opts: &SshOpts) -> Duration {
match opts.connection_timeout {
Some(t) => t,
None => params
.connect_timeout
.unwrap_or_else(|| Duration::from_secs(30)),
}
}
fn resolve_connection_attempts(params: &HostParams) -> usize {
params.connection_attempts.unwrap_or(1)
}
}
impl TryFrom<&SshOpts> for Config {
type Error = RemoteError;
fn try_from(opts: &SshOpts) -> Result<Self, Self::Error> {
if let Some(p) = opts.config_file.as_deref() {
let params = Self::parse(p, opts.host.as_str(), opts.parse_rules)?;
Ok(Self::from_params(params, opts))
} else {
let params = HostParams::new(&DefaultAlgorithms::default());
Ok(Self::from_params(params, opts))
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::{assert_eq, assert_ne};
use super::*;
use crate::mock::ssh as ssh_mock;
#[test]
fn should_init_config_from_default_ssh_opts() {
let opts = SshOpts::new("192.168.1.1");
let config = Config::try_from(&opts).ok().unwrap();
assert_eq!(config.connection_attempts, 1);
assert_eq!(config.connection_timeout, Duration::from_secs(30));
assert_eq!(config.address.as_str(), "192.168.1.1:22");
assert_eq!(config.host.as_str(), "192.168.1.1");
assert!(config.username.is_empty());
assert_eq!(
config.params,
HostParams::new(&DefaultAlgorithms::default())
);
}
#[test]
fn should_init_config_from_custom_opts() {
let opts = SshOpts::new("192.168.1.1")
.connection_timeout(Duration::from_secs(10))
.port(2222)
.username("omar");
let config = Config::try_from(&opts).ok().unwrap();
assert_eq!(config.connection_attempts, 1);
assert_eq!(config.connection_timeout, Duration::from_secs(10));
assert_eq!(config.host.as_str(), "192.168.1.1");
assert_eq!(config.address.as_str(), "192.168.1.1:2222");
assert_eq!(config.username.as_str(), "omar");
assert_eq!(
config.params,
HostParams::new(&DefaultAlgorithms::default())
);
}
#[test]
fn should_init_config_from_file() {
let config_file = ssh_mock::create_ssh_config(22);
let opts = SshOpts::new("sftp").config_file(config_file.path(), ParseRule::STRICT);
let config = Config::try_from(&opts).ok().unwrap();
assert_eq!(config.connection_attempts, 3);
assert_eq!(config.connection_timeout, Duration::from_secs(60));
assert_eq!(config.host.as_str(), "sftp");
assert_eq!(config.resolved_host.as_str(), "127.0.0.1");
assert_eq!(config.address.as_str(), "127.0.0.1:22");
assert_eq!(config.username.as_str(), "sftp");
assert_ne!(
config.params,
HostParams::new(&DefaultAlgorithms::default())
);
}
#[test]
fn should_init_config_from_file_with_override() {
let config_file = ssh_mock::create_ssh_config(22);
let opts = SshOpts::new("sftp")
.config_file(config_file.path(), ParseRule::STRICT)
.connection_timeout(Duration::from_secs(10))
.port(22)
.username("omar");
let config = Config::try_from(&opts).ok().unwrap();
assert_eq!(config.connection_attempts, 3);
assert_eq!(config.connection_timeout, Duration::from_secs(10));
assert_eq!(config.host.as_str(), "sftp");
assert_eq!(config.resolved_host.as_str(), "127.0.0.1");
assert_eq!(config.address.as_str(), "127.0.0.1:22");
assert_eq!(config.username.as_str(), "omar");
assert_ne!(
config.params,
HostParams::new(&DefaultAlgorithms::default())
);
}
}