#![forbid(unsafe_code)]
use crate::domain::{secret_nonempty, KeyPath, SshHost, SshPort, SshUser, TimeoutMs};
use crate::errors::{SshCliError, SshCliResult};
use crate::tls::TlsConnectOptions;
use secrecy::SecretString;
use std::path::PathBuf;
#[derive(Clone)]
pub struct ConnectionConfig {
pub host: SshHost,
pub port: SshPort,
pub username: SshUser,
pub password: SecretString,
pub key_path: Option<KeyPath>,
pub key_passphrase: Option<SecretString>,
pub timeout_ms: TimeoutMs,
pub known_hosts_path: Option<PathBuf>,
pub replace_host_key: bool,
pub tls: Option<TlsConnectOptions>,
pub use_agent: bool,
pub agent_socket: Option<PathBuf>,
}
impl std::fmt::Debug for ConnectionConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConnectionConfig")
.field("host", &self.host.as_str())
.field("port", &self.port.get())
.field("username", &self.username.as_str())
.field("password", &"<redacted>")
.field("key_path", &self.key_path.as_ref().map(|k| k.as_path()))
.field(
"key_passphrase",
&self.key_passphrase.as_ref().map(|_| "<redacted>"),
)
.field("timeout_ms", &self.timeout_ms.get())
.field("known_hosts_path", &self.known_hosts_path)
.field("replace_host_key", &self.replace_host_key)
.field("tls", &self.tls)
.field("use_agent", &self.use_agent)
.field("agent_socket", &self.agent_socket)
.finish()
}
}
impl ConnectionConfig {
pub fn validate(&self) -> SshCliResult<()> {
let has_password = secret_nonempty(&self.password);
let has_key = self.key_path.is_some();
let has_agent = self.use_agent;
if !has_password && !has_key && !has_agent {
return Err(SshCliError::InvalidArgument(
"auth requires password, key_path, or --use-agent".to_string(),
));
}
if has_agent {
#[cfg(unix)]
if self.agent_socket.is_none() {
return Err(SshCliError::InvalidArgument(
"use_agent requires --agent-socket PATH (or agent_socket in XDG VPS record); \
env SSH_AUTH_SOCK is not used as product store"
.into(),
));
}
}
Ok(())
}
#[must_use]
pub fn resolved_agent_socket(&self) -> Option<PathBuf> {
if !self.use_agent {
return None;
}
if let Some(p) = &self.agent_socket {
return Some(p.clone());
}
#[cfg(windows)]
{
return Some(PathBuf::from(crate::constants::WINDOWS_SSH_AGENT_PIPE));
}
#[cfg(not(windows))]
{
None
}
}
}