use async_trait::async_trait;
#[derive(Clone)]
pub struct SshTarget {
pub host: String,
pub port: u16,
pub user: String,
pub private_key: Option<String>,
pub password: Option<String>,
}
impl std::fmt::Debug for SshTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SshTarget")
.field("host", &self.host)
.field("port", &self.port)
.field("user", &self.user)
.field(
"private_key",
&self.private_key.as_ref().map(|_| "[REDACTED]"),
)
.field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
.finish()
}
}
#[derive(Debug, Clone, Default)]
pub struct SshOutput {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
#[async_trait]
pub trait SshHandler: Send + Sync {
async fn exec(
&self,
target: &SshTarget,
command: &str,
) -> std::result::Result<SshOutput, String>;
async fn shell(&self, target: &SshTarget) -> std::result::Result<SshOutput, String> {
self.exec(target, "").await
}
async fn upload(
&self,
target: &SshTarget,
remote_path: &str,
content: &[u8],
mode: u32,
) -> std::result::Result<(), String>;
async fn download(
&self,
target: &SshTarget,
remote_path: &str,
) -> std::result::Result<Vec<u8>, String>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_debug_redacts_credentials() {
let target = SshTarget {
host: "example.com".to_string(),
port: 22,
user: "admin".to_string(),
private_key: Some("-----BEGIN OPENSSH PRIVATE KEY-----".to_string()),
password: Some("super_secret".to_string()),
};
let debug = format!("{:?}", target); assert!(!debug.contains("super_secret"), "password leaked: {debug}");
assert!(
!debug.contains("BEGIN OPENSSH PRIVATE KEY"),
"key leaked: {debug}"
);
assert!(debug.contains("[REDACTED]"), "REDACTED missing: {debug}");
assert!(
debug.contains("example.com"),
"host should be visible: {debug}"
);
}
}