This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
pub mod config_store;
|
||||
pub mod local_terminal;
|
||||
pub mod pty;
|
||||
pub mod ssh_runtime;
|
||||
pub mod wallet_vault;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SshCommand {
|
||||
pub host: String,
|
||||
pub user: Option<String>,
|
||||
pub port: u16,
|
||||
pub command: String,
|
||||
pub timeout: Duration,
|
||||
}
|
||||
|
||||
impl SshCommand {
|
||||
pub fn new(host: impl Into<String>, command: impl Into<String>) -> Self {
|
||||
Self {
|
||||
host: host.into(),
|
||||
user: None,
|
||||
port: 22,
|
||||
command: command.into(),
|
||||
timeout: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user(mut self, user: impl Into<String>) -> Self {
|
||||
self.user = Some(user.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn destination(&self) -> String {
|
||||
match &self.user {
|
||||
Some(user) if !user.trim().is_empty() => {
|
||||
format!("{}@{}", user.trim(), self.host.trim())
|
||||
}
|
||||
_ => self.host.trim().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), SshError> {
|
||||
if self.host.trim().is_empty() {
|
||||
return Err(SshError::Invalid("ssh host is required".to_string()));
|
||||
}
|
||||
if self.command.trim().is_empty() {
|
||||
return Err(SshError::Invalid("ssh command is required".to_string()));
|
||||
}
|
||||
if self.port == 0 {
|
||||
return Err(SshError::Invalid("ssh port is required".to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct SshCommandOutput {
|
||||
pub exit_code: i32,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SshError {
|
||||
Invalid(String),
|
||||
Io(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SshClient;
|
||||
|
||||
impl SshClient {
|
||||
pub fn build_args(&self, request: &SshCommand) -> Result<Vec<String>, SshError> {
|
||||
request.validate()?;
|
||||
Ok(vec![
|
||||
"-o".to_string(),
|
||||
"BatchMode=yes".to_string(),
|
||||
"-o".to_string(),
|
||||
format!("ConnectTimeout={}", request.timeout.as_secs().max(1)),
|
||||
"-p".to_string(),
|
||||
request.port.to_string(),
|
||||
request.destination(),
|
||||
request.command.clone(),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn run(&self, request: SshCommand) -> Result<SshCommandOutput, SshError> {
|
||||
let args = self.build_args(&request)?;
|
||||
let output = Command::new("ssh")
|
||||
.args(args)
|
||||
.output()
|
||||
.map_err(|err| SshError::Io(err.to_string()))?;
|
||||
Ok(SshCommandOutput {
|
||||
exit_code: output.status.code().unwrap_or(-1),
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ssh_command_builds_batch_mode_args() {
|
||||
let request = SshCommand::new("example.com", "uptime")
|
||||
.user("alice")
|
||||
.port(2222)
|
||||
.timeout(Duration::from_secs(3));
|
||||
let args = SshClient::default().build_args(&request).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=3",
|
||||
"-p",
|
||||
"2222",
|
||||
"[email protected]",
|
||||
"uptime"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_command_rejects_missing_host_or_command() {
|
||||
assert_eq!(
|
||||
SshCommand::new(" ", "uptime").validate().unwrap_err(),
|
||||
SshError::Invalid("ssh host is required".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
SshCommand::new("example.com", " ").validate().unwrap_err(),
|
||||
SshError::Invalid("ssh command is required".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_command_destination_omits_blank_user() {
|
||||
let request = SshCommand::new("host.local", "true").user(" ");
|
||||
assert_eq!(request.destination(), "host.local");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user