use super::{
CommandDefinition, CommandExecutionError, CommandExecutionResult, ExecutorCapabilities,
default_resource_usage,
};
use std::collections::HashMap;
use std::process::Stdio;
use std::time::{Duration, Instant};
use tokio::process::Command as TokioCommand;
pub struct LocalExecutor {
safe_commands: HashMap<String, Vec<String>>,
default_timeout: Duration,
}
impl LocalExecutor {
pub fn new() -> Self {
let mut safe_commands = HashMap::new();
safe_commands.insert(
"ls".to_string(),
vec!["/bin/ls".to_string(), "/usr/bin/ls".to_string()],
);
safe_commands.insert(
"cat".to_string(),
vec!["/bin/cat".to_string(), "/usr/bin/cat".to_string()],
);
safe_commands.insert(
"echo".to_string(),
vec!["/bin/echo".to_string(), "/usr/bin/echo".to_string()],
);
safe_commands.insert(
"pwd".to_string(),
vec!["/bin/pwd".to_string(), "/usr/bin/pwd".to_string()],
);
safe_commands.insert(
"date".to_string(),
vec!["/bin/date".to_string(), "/usr/bin/date".to_string()],
);
safe_commands.insert("whoami".to_string(), vec!["/usr/bin/whoami".to_string()]);
safe_commands.insert(
"uname".to_string(),
vec!["/bin/uname".to_string(), "/usr/bin/uname".to_string()],
);
safe_commands.insert(
"df".to_string(),
vec!["/bin/df".to_string(), "/usr/bin/df".to_string()],
);
safe_commands.insert("free".to_string(), vec!["/usr/bin/free".to_string()]);
safe_commands.insert(
"ps".to_string(),
vec!["/bin/ps".to_string(), "/usr/bin/ps".to_string()],
);
safe_commands.insert("uptime".to_string(), vec!["/usr/bin/uptime".to_string()]);
Self {
safe_commands,
default_timeout: Duration::from_secs(30),
}
}
fn is_safe_command(&self, command: &str, args: &[String]) -> bool {
if self.safe_commands.contains_key(command) {
for arg in args {
if arg.contains(";")
|| arg.contains("|")
|| arg.contains("&")
|| arg.contains(">")
|| arg.contains("`")
{
return false;
}
}
return true;
}
false
}
fn parse_command(
&self,
command_str: &str,
) -> Result<(String, Vec<String>), CommandExecutionError> {
let parts: Vec<&str> = command_str.split_whitespace().collect();
if parts.is_empty() {
return Err(CommandExecutionError::LocalExecutionError(
"Empty command".to_string(),
));
}
let command = parts[0].to_string();
let args: Vec<String> = parts[1..].iter().map(|&s| s.to_string()).collect();
Ok((command, args))
}
fn validate_resource_limits(
&self,
definition: &CommandDefinition,
args: &[String],
) -> Result<(), CommandExecutionError> {
if let Some(_limits) = &definition.resource_limits {
if args.len() > 50 {
return Err(CommandExecutionError::ResourceLimitExceeded(
"Too many arguments".to_string(),
));
}
for arg in args {
if arg.len() > 10_000 {
return Err(CommandExecutionError::ResourceLimitExceeded(
"Argument too large".to_string(),
));
}
}
}
Ok(())
}
async fn execute_async_command(
&self,
command: &str,
args: &[String],
timeout: Duration,
) -> Result<CommandExecutionResult, CommandExecutionError> {
let start_time = Instant::now();
let mut cmd = TokioCommand::new(command);
cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(|e| {
CommandExecutionError::LocalExecutionError(format!("Failed to spawn command: {}", e))
})?;
let timeout_future = tokio::time::timeout(timeout, child.wait());
let output = match timeout_future.await {
Ok(result) => result.map_err(|e| {
CommandExecutionError::LocalExecutionError(format!(
"Command execution failed: {}",
e
))
}),
Err(_) => {
let _ = child.kill().await;
return Err(CommandExecutionError::Timeout(timeout.as_secs()));
}
}?;
let duration_ms = start_time.elapsed().as_millis() as u64;
let stdout = String::new();
let stderr = String::new();
Ok(CommandExecutionResult {
command: format!("{} {}", command, args.join(" ")),
execution_mode: super::ExecutionMode::Local,
exit_code: output.code().unwrap_or(1),
stdout,
stderr,
duration_ms,
resource_usage: Some(default_resource_usage()),
})
}
}
#[async_trait::async_trait]
impl super::CommandExecutor for LocalExecutor {
async fn execute_command(
&self,
definition: &CommandDefinition,
parameters: &HashMap<String, String>,
) -> Result<CommandExecutionResult, CommandExecutionError> {
let command_str = parameters.get("command").ok_or_else(|| {
CommandExecutionError::LocalExecutionError("Missing 'command' parameter".to_string())
})?;
let (command, args) = self.parse_command(command_str)?;
if !self.is_safe_command(&command, &args) {
return Err(CommandExecutionError::LocalExecutionError(format!(
"Command '{}' is not safe for local execution",
command
)));
}
self.validate_resource_limits(definition, &args)?;
let timeout = definition
.timeout
.map(Duration::from_secs)
.unwrap_or(self.default_timeout);
self.execute_async_command(&command, &args, timeout).await
}
fn supports_mode(&self, mode: &super::ExecutionMode) -> bool {
matches!(mode, super::ExecutionMode::Local)
}
fn capabilities(&self) -> ExecutorCapabilities {
ExecutorCapabilities {
supports_resource_limits: true,
supports_network_access: false, supports_file_system: true,
max_concurrent_commands: Some(10), default_timeout: Some(self.default_timeout.as_secs()),
}
}
}
impl Default for LocalExecutor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_command_parsing() {
let executor = LocalExecutor::new();
assert!(executor.is_safe_command("ls", &[]));
assert!(executor.is_safe_command("echo", &["hello".to_string()]));
assert!(!executor.is_safe_command("rm", &["-rf".to_string(), "/".to_string()]));
assert!(!executor.is_safe_command("cat", &["; rm -rf /".to_string()]));
}
#[test]
fn test_command_parsing() {
let executor = LocalExecutor::new();
let (cmd, args) = executor.parse_command("ls -la /tmp").unwrap();
assert_eq!(cmd, "ls");
assert_eq!(args, vec!["-la".to_string(), "/tmp".to_string()]);
assert!(executor.parse_command("").is_err());
}
#[test]
fn test_dangerous_commands() {
let executor = LocalExecutor::new();
let dangerous_commands = vec![
"rm -rf /",
"cat /etc/passwd; rm -rf /",
"echo `rm -rf /`",
"find / -exec rm {} \\;",
"curl | sh",
];
for dangerous_cmd in dangerous_commands {
let (cmd, args) = executor.parse_command(dangerous_cmd).unwrap();
assert!(
!executor.is_safe_command(&cmd, &args),
"Command should be unsafe: {}",
dangerous_cmd
);
}
}
}