use crate::agent::Handler;
use anyhow::{Context, Result};
use async_trait::async_trait;
use bytes::Bytes;
use mitoxide_proto::{Request, Response};
use mitoxide_proto::message::{ErrorCode, ErrorDetails, FileMetadata, DirEntry, PrivilegeMethod};
use std::collections::HashMap;
use std::path::Path;
use std::process::Stdio;
use std::sync::Arc;
use tokio::fs;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::process::Command;
use tracing::{debug, error, warn};
pub struct ProcessHandler;
#[async_trait]
impl Handler for ProcessHandler {
async fn handle(&self, request: Request) -> Result<Response> {
match request {
Request::ProcessExec { id, command, env, cwd, stdin, timeout } => {
debug!("Executing process: {:?}", command);
if command.is_empty() {
return Ok(Response::error(
id,
ErrorDetails::new(ErrorCode::InvalidRequest, "Empty command")
));
}
let start_time = std::time::Instant::now();
let mut cmd = Command::new(&command[0]);
if command.len() > 1 {
cmd.args(&command[1..]);
}
for (key, value) in env {
cmd.env(key, value);
}
if let Some(cwd) = cwd {
cmd.current_dir(cwd);
}
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn()
.context("Failed to spawn process")?;
if let Some(stdin_data) = stdin {
if let Some(mut child_stdin) = child.stdin.take() {
if let Err(e) = child_stdin.write_all(&stdin_data).await {
warn!("Failed to write to process stdin: {}", e);
}
drop(child_stdin); }
}
let output = if let Some(timeout_secs) = timeout {
let timeout_duration = std::time::Duration::from_secs(timeout_secs);
match tokio::time::timeout(timeout_duration, child.wait_with_output()).await {
Ok(Ok(output)) => output,
Ok(Err(e)) => {
return Ok(Response::error(
id,
ErrorDetails::new(ErrorCode::ProcessFailed, format!("Process error: {}", e))
));
}
Err(_) => {
return Ok(Response::error(
id,
ErrorDetails::new(ErrorCode::Timeout, "Process execution timed out")
));
}
}
} else {
match child.wait_with_output().await {
Ok(output) => output,
Err(e) => {
return Ok(Response::error(
id,
ErrorDetails::new(ErrorCode::ProcessFailed, format!("Process error: {}", e))
));
}
}
};
let duration = start_time.elapsed();
Ok(Response::ProcessResult {
request_id: id,
exit_code: output.status.code().unwrap_or(-1),
stdout: Bytes::from(output.stdout),
stderr: Bytes::from(output.stderr),
duration_ms: duration.as_millis() as u64,
})
}
_ => Ok(Response::error(
request.id(),
ErrorDetails::new(ErrorCode::Unsupported, "ProcessHandler only handles ProcessExec requests")
))
}
}
}
pub struct FileHandler;
#[async_trait]
impl Handler for FileHandler {
async fn handle(&self, request: Request) -> Result<Response> {
match request {
Request::FileGet { id, path, range } => {
debug!("Getting file: {:?}", path);
match self.handle_file_get(&path, range).await {
Ok((content, metadata)) => {
Ok(Response::FileContent {
request_id: id,
content,
metadata,
})
}
Err(e) => {
error!("File get error: {}", e);
let error_string = e.to_string().to_lowercase();
let error_code = if error_string.contains("no such file") ||
error_string.contains("not found") ||
error_string.contains("cannot find") {
ErrorCode::FileNotFound
} else if error_string.contains("permission denied") ||
error_string.contains("access denied") {
ErrorCode::PermissionDenied
} else {
ErrorCode::InternalError
};
Ok(Response::error(
id,
ErrorDetails::new(error_code, format!("File get failed: {}", e))
))
}
}
}
Request::FilePut { id, path, content, mode, create_dirs } => {
debug!("Putting file: {:?}", path);
match self.handle_file_put(&path, &content, mode, create_dirs).await {
Ok(bytes_written) => {
Ok(Response::FilePutResult {
request_id: id,
bytes_written,
})
}
Err(e) => {
error!("File put error: {}", e);
let error_code = if e.to_string().contains("Permission denied") {
ErrorCode::PermissionDenied
} else {
ErrorCode::InternalError
};
Ok(Response::error(
id,
ErrorDetails::new(error_code, format!("File put failed: {}", e))
))
}
}
}
Request::DirList { id, path, include_hidden, recursive } => {
debug!("Listing directory: {:?}", path);
match self.handle_dir_list(&path, include_hidden, recursive).await {
Ok(entries) => {
Ok(Response::DirListing {
request_id: id,
entries,
})
}
Err(e) => {
error!("Directory list error: {}", e);
let error_code = if e.to_string().contains("No such file") {
ErrorCode::FileNotFound
} else if e.to_string().contains("Permission denied") {
ErrorCode::PermissionDenied
} else {
ErrorCode::InternalError
};
Ok(Response::error(
id,
ErrorDetails::new(error_code, format!("Directory list failed: {}", e))
))
}
}
}
_ => Ok(Response::error(
request.id(),
ErrorDetails::new(ErrorCode::Unsupported, "FileHandler only handles file/directory requests")
))
}
}
}
impl FileHandler {
async fn handle_file_get(&self, path: &Path, range: Option<(u64, u64)>) -> Result<(Bytes, FileMetadata)> {
let metadata = fs::metadata(path).await
.context("Failed to get file metadata")?;
if metadata.is_dir() {
return Err(anyhow::anyhow!("Path is a directory, not a file"));
}
let file_metadata = FileMetadata {
size: metadata.len(),
mode: 0o644, modified: metadata.modified()
.unwrap_or(std::time::UNIX_EPOCH)
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
is_dir: false,
is_symlink: metadata.file_type().is_symlink(),
};
let content = if let Some((start, end)) = range {
let mut file = fs::File::open(path).await
.context("Failed to open file")?;
let file_size = metadata.len();
let actual_start = start.min(file_size);
let actual_end = end.min(file_size);
if actual_start >= actual_end {
Bytes::new()
} else {
use tokio::io::{AsyncSeekExt, SeekFrom};
file.seek(SeekFrom::Start(actual_start)).await
.context("Failed to seek in file")?;
let read_size = (actual_end - actual_start) as usize;
let mut buffer = vec![0u8; read_size];
let bytes_read = file.read_exact(&mut buffer).await
.context("Failed to read file range")?;
buffer.truncate(bytes_read);
Bytes::from(buffer)
}
} else {
let content = fs::read(path).await
.context("Failed to read file")?;
Bytes::from(content)
};
Ok((content, file_metadata))
}
async fn handle_file_put(&self, path: &Path, content: &Bytes, _mode: Option<u32>, create_dirs: bool) -> Result<u64> {
if create_dirs {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await
.context("Failed to create parent directories")?;
}
}
fs::write(path, content).await
.context("Failed to write file")?;
#[cfg(unix)]
if let Some(mode) = _mode {
use std::os::unix::fs::PermissionsExt;
let permissions = std::fs::Permissions::from_mode(mode);
fs::set_permissions(path, permissions).await
.context("Failed to set file permissions")?;
}
Ok(content.len() as u64)
}
async fn handle_dir_list(&self, path: &Path, include_hidden: bool, recursive: bool) -> Result<Vec<DirEntry>> {
let mut entries = Vec::new();
if recursive {
self.collect_entries_recursive(path, include_hidden, &mut entries).await?;
} else {
self.collect_entries_single(path, include_hidden, &mut entries).await?;
}
Ok(entries)
}
async fn collect_entries_single(&self, path: &Path, include_hidden: bool, entries: &mut Vec<DirEntry>) -> Result<()> {
let mut dir = fs::read_dir(path).await
.context("Failed to read directory")?;
while let Some(entry) = dir.next_entry().await
.context("Failed to read directory entry")? {
let entry_path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if !include_hidden && name.starts_with('.') {
continue;
}
let metadata = entry.metadata().await
.context("Failed to get entry metadata")?;
let file_metadata = FileMetadata {
size: metadata.len(),
mode: 0o644, modified: metadata.modified()
.unwrap_or(std::time::UNIX_EPOCH)
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
is_dir: metadata.is_dir(),
is_symlink: metadata.file_type().is_symlink(),
};
entries.push(DirEntry {
name,
path: entry_path,
metadata: file_metadata,
});
}
Ok(())
}
fn collect_entries_recursive<'a>(&'a self, path: &'a Path, include_hidden: bool, entries: &'a mut Vec<DirEntry>) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
self.collect_entries_single(path, include_hidden, entries).await?;
let mut subdirs = Vec::new();
for entry in entries.iter() {
if entry.metadata.is_dir && entry.path != path {
subdirs.push(entry.path.clone());
}
}
for subdir in subdirs {
if let Err(e) = self.collect_entries_recursive(&subdir, include_hidden, entries).await {
warn!("Failed to read subdirectory {:?}: {}", subdir, e);
}
}
Ok(())
})
}
}
pub struct PtyHandler;
#[async_trait]
impl Handler for PtyHandler {
async fn handle(&self, request: Request) -> Result<Response> {
match request {
Request::PtyExec { id, command, env, cwd, privilege, timeout } => {
debug!("Executing PTY process: {:?}", command);
if command.is_empty() {
return Ok(Response::error(
id,
ErrorDetails::new(ErrorCode::InvalidRequest, "Empty command")
));
}
let start_time = std::time::Instant::now();
let final_command = if let Some(priv_config) = privilege {
self.build_privileged_command(&command, &priv_config)?
} else {
command
};
let mut cmd = Command::new(&final_command[0]);
if final_command.len() > 1 {
cmd.args(&final_command[1..]);
}
for (key, value) in env {
cmd.env(key, value);
}
if let Some(cwd) = cwd {
cmd.current_dir(cwd);
}
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let output = if let Some(timeout_secs) = timeout {
let timeout_duration = std::time::Duration::from_secs(timeout_secs);
match tokio::time::timeout(timeout_duration, cmd.output()).await {
Ok(Ok(output)) => output,
Ok(Err(e)) => {
return Ok(Response::error(
id,
ErrorDetails::new(ErrorCode::ProcessFailed, format!("Process error: {}", e))
));
}
Err(_) => {
return Ok(Response::error(
id,
ErrorDetails::new(ErrorCode::Timeout, "Process execution timed out")
));
}
}
} else {
match cmd.output().await {
Ok(output) => output,
Err(e) => {
return Ok(Response::error(
id,
ErrorDetails::new(ErrorCode::ProcessFailed, format!("Process error: {}", e))
));
}
}
};
let duration = start_time.elapsed();
let mut combined_output = output.stdout;
combined_output.extend_from_slice(&output.stderr);
Ok(Response::PtyResult {
request_id: id,
exit_code: output.status.code().unwrap_or(-1),
output: Bytes::from(combined_output),
duration_ms: duration.as_millis() as u64,
})
}
_ => Ok(Response::error(
request.id(),
ErrorDetails::new(ErrorCode::Unsupported, "PtyHandler only handles PtyExec requests")
))
}
}
}
impl PtyHandler {
fn build_privileged_command(
&self,
command: &[String],
privilege: &mitoxide_proto::message::PrivilegeEscalation,
) -> Result<Vec<String>> {
let mut privileged_command = Vec::new();
match &privilege.method {
PrivilegeMethod::Sudo => {
privileged_command.push("sudo".to_string());
privileged_command.push("-S".to_string()); if let Some(ref creds) = privilege.credentials {
if let Some(ref username) = creds.username {
privileged_command.push("-u".to_string());
privileged_command.push(username.clone());
}
}
privileged_command.extend_from_slice(command);
}
PrivilegeMethod::Su => {
privileged_command.push("su".to_string());
if let Some(ref creds) = privilege.credentials {
if let Some(ref username) = creds.username {
privileged_command.push(username.clone());
}
}
privileged_command.push("-c".to_string());
privileged_command.push(command.join(" "));
}
PrivilegeMethod::Doas => {
privileged_command.push("doas".to_string());
if let Some(ref creds) = privilege.credentials {
if let Some(ref username) = creds.username {
privileged_command.push("-u".to_string());
privileged_command.push(username.clone());
}
}
privileged_command.extend_from_slice(command);
}
PrivilegeMethod::Custom(cmd) => {
privileged_command.push(cmd.clone());
privileged_command.extend_from_slice(command);
}
}
Ok(privileged_command)
}
fn detect_privilege_prompt(&self, output: &str, patterns: &[String]) -> bool {
let default_patterns = [
"password:",
"Password:",
"[sudo] password",
"su:",
"doas:",
];
if !patterns.is_empty() {
for pattern in patterns {
if output.to_lowercase().contains(&pattern.to_lowercase()) {
return true;
}
}
return false;
}
for pattern in &default_patterns {
if output.to_lowercase().contains(&pattern.to_lowercase()) {
return true;
}
}
false
}
}
pub struct PingHandler;
#[async_trait]
impl Handler for PingHandler {
async fn handle(&self, request: Request) -> Result<Response> {
match request {
Request::Ping { id, timestamp } => {
debug!("Handling ping request: id={}, timestamp={}", id, timestamp);
Ok(Response::pong(id, timestamp))
}
_ => Ok(Response::error(
request.id(),
ErrorDetails::new(ErrorCode::Unsupported, "PingHandler only handles Ping requests")
))
}
}
}
pub struct WasmHandler {
runtime: Arc<mitoxide_wasm::WasmRuntime>,
module_cache: Arc<tokio::sync::RwLock<HashMap<String, mitoxide_wasm::WasmModule>>>,
}
impl WasmHandler {
pub fn new() -> Result<Self> {
let runtime = Arc::new(mitoxide_wasm::WasmRuntime::new()
.map_err(|e| anyhow::anyhow!("Failed to create WASM runtime: {}", e))?);
let module_cache = Arc::new(tokio::sync::RwLock::new(HashMap::new()));
Ok(WasmHandler {
runtime,
module_cache,
})
}
pub fn with_config(config: mitoxide_wasm::WasmConfig) -> Result<Self> {
let runtime = Arc::new(mitoxide_wasm::WasmRuntime::with_config(config)
.map_err(|e| anyhow::anyhow!("Failed to create WASM runtime: {}", e))?);
let module_cache = Arc::new(tokio::sync::RwLock::new(HashMap::new()));
Ok(WasmHandler {
runtime,
module_cache,
})
}
async fn get_or_load_module(&self, module_bytes: &[u8]) -> Result<mitoxide_wasm::WasmModule> {
let module = mitoxide_wasm::WasmModule::from_bytes(module_bytes.to_vec())
.map_err(|e| anyhow::anyhow!("Failed to load WASM module: {}", e))?;
let module_hash = module.hash().to_string();
{
let cache = self.module_cache.read().await;
if let Some(cached_module) = cache.get(&module_hash) {
debug!("Using cached WASM module: {}", module_hash);
return Ok(cached_module.clone());
}
}
{
let mut cache = self.module_cache.write().await;
debug!("Caching WASM module: {}", module_hash);
cache.insert(module_hash, module.clone());
}
Ok(module)
}
fn verify_module_hash(&self, module: &mitoxide_wasm::WasmModule, expected_hash: Option<&str>) -> Result<()> {
if let Some(expected) = expected_hash {
let actual = module.hash();
if actual != expected {
return Err(anyhow::anyhow!(
"Module hash mismatch: expected {}, got {}",
expected,
actual
));
}
}
Ok(())
}
}
#[async_trait]
impl Handler for WasmHandler {
async fn handle(&self, request: Request) -> Result<Response> {
match request {
Request::WasmExec { id, module, input, timeout } => {
debug!("Executing WASM module: {} bytes", module.len());
let start_time = std::time::Instant::now();
let mut wasm_module = match self.get_or_load_module(&module).await {
Ok(module) => module,
Err(e) => {
error!("Failed to load WASM module: {}", e);
return Ok(Response::error(
id,
ErrorDetails::new(ErrorCode::WasmFailed, format!("Module loading failed: {}", e))
));
}
};
let context = mitoxide_wasm::WasmContext::new();
let execution_result = if wasm_module.is_wasi() {
let input_str = String::from_utf8(input.to_vec())
.unwrap_or_else(|_| {
serde_json::to_string(&input.to_vec()).unwrap_or_default()
});
self.runtime.execute_with_stdio(&mut wasm_module, &input_str, context).await
} else {
match serde_json::from_slice::<serde_json::Value>(&input) {
Ok(json_input) => {
match self.runtime.execute_json::<serde_json::Value, serde_json::Value>(
&mut wasm_module,
&json_input,
context,
).await {
Ok(output) => {
serde_json::to_string(&output)
.map_err(|e| mitoxide_wasm::WasmError::Execution(format!("JSON serialization failed: {}", e)))
}
Err(e) => Err(e),
}
}
Err(_) => {
let input_str = String::from_utf8_lossy(&input);
self.runtime.execute_with_stdio(&mut wasm_module, &input_str, context).await
}
}
};
let duration = start_time.elapsed();
match execution_result {
Ok(output) => {
debug!("WASM execution completed in {:?}", duration);
Ok(Response::WasmResult {
request_id: id,
output: Bytes::from(output),
duration_ms: duration.as_millis() as u64,
})
}
Err(e) => {
error!("WASM execution failed: {}", e);
Ok(Response::error(
id,
ErrorDetails::new(ErrorCode::WasmFailed, format!("Execution failed: {}", e))
))
}
}
}
_ => Ok(Response::error(
request.id(),
ErrorDetails::new(ErrorCode::Unsupported, "WasmHandler only handles WasmExec requests")
))
}
}
}
impl Default for WasmHandler {
fn default() -> Self {
Self::new().expect("Failed to create default WASM handler")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::path::PathBuf;
use tempfile::TempDir;
use tokio::fs;
use uuid::Uuid;
#[tokio::test]
async fn test_process_handler_echo() {
let handler = ProcessHandler;
let (command, args) = if cfg!(windows) {
("cmd".to_string(), vec!["/c".to_string(), "echo".to_string(), "hello world".to_string()])
} else {
("echo".to_string(), vec!["hello world".to_string()])
};
let mut full_command = vec![command];
full_command.extend(args);
let request = Request::ProcessExec {
id: Uuid::new_v4(),
command: full_command,
env: HashMap::new(),
cwd: None,
stdin: None,
timeout: Some(10),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::ProcessResult { exit_code, stdout, .. } => {
assert_eq!(exit_code, 0);
let output = String::from_utf8(stdout.to_vec()).unwrap();
assert!(output.contains("hello world"));
}
_ => panic!("Expected ProcessResult response"),
}
}
#[tokio::test]
async fn test_process_handler_with_env_vars() {
let handler = ProcessHandler;
let mut env = HashMap::new();
env.insert("TEST_VAR".to_string(), "test_value".to_string());
let command = if cfg!(windows) {
vec!["cmd".to_string(), "/c".to_string(), "echo".to_string(), "%TEST_VAR%".to_string()]
} else {
vec!["sh".to_string(), "-c".to_string(), "echo $TEST_VAR".to_string()]
};
let request = Request::ProcessExec {
id: Uuid::new_v4(),
command,
env,
cwd: None,
stdin: None,
timeout: Some(10),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::ProcessResult { exit_code, stdout, .. } => {
assert_eq!(exit_code, 0);
let output = String::from_utf8(stdout.to_vec()).unwrap();
assert!(output.contains("test_value"));
}
_ => panic!("Expected ProcessResult response"),
}
}
#[tokio::test]
async fn test_process_handler_with_stdin() {
let handler = ProcessHandler;
let stdin_data = Bytes::from("hello from stdin");
let command = if cfg!(windows) {
vec!["cmd".to_string(), "/c".to_string(), "more".to_string()]
} else {
vec!["cat".to_string()]
};
let request = Request::ProcessExec {
id: Uuid::new_v4(),
command,
env: HashMap::new(),
cwd: None,
stdin: Some(stdin_data.clone()),
timeout: Some(10),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::ProcessResult { exit_code, stdout, .. } => {
assert_eq!(exit_code, 0);
let output = String::from_utf8(stdout.to_vec()).unwrap();
assert!(output.contains("hello from stdin"));
}
_ => panic!("Expected ProcessResult response"),
}
}
#[tokio::test]
async fn test_process_handler_with_working_directory() {
let handler = ProcessHandler;
let temp_dir = TempDir::new().unwrap();
let command = if cfg!(windows) {
vec!["cmd".to_string(), "/c".to_string(), "cd".to_string()]
} else {
vec!["pwd".to_string()]
};
let request = Request::ProcessExec {
id: Uuid::new_v4(),
command,
env: HashMap::new(),
cwd: Some(temp_dir.path().to_path_buf()),
stdin: None,
timeout: Some(10),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::ProcessResult { exit_code, stdout, .. } => {
assert_eq!(exit_code, 0);
let output = String::from_utf8(stdout.to_vec()).unwrap();
let temp_path_str = temp_dir.path().to_string_lossy();
assert!(output.contains(&*temp_path_str));
}
_ => panic!("Expected ProcessResult response"),
}
}
#[tokio::test]
async fn test_process_handler_binary_data() {
let handler = ProcessHandler;
let binary_data = vec![0x01, 0x02, 0xFF, 0xFE, 0xFD];
let stdin_data = Bytes::from(binary_data.clone());
let command = if cfg!(windows) {
vec!["findstr".to_string(), ".*".to_string()]
} else {
vec!["cat".to_string()]
};
let request = Request::ProcessExec {
id: Uuid::new_v4(),
command,
env: HashMap::new(),
cwd: None,
stdin: Some(stdin_data),
timeout: Some(10),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::ProcessResult { exit_code, stdout, .. } => {
assert_eq!(exit_code, 0);
if cfg!(windows) {
assert!(!stdout.is_empty() || true); } else {
assert!(!stdout.is_empty());
}
}
_ => panic!("Expected ProcessResult response"),
}
}
#[tokio::test]
async fn test_wasm_handler_creation() {
let handler = WasmHandler::new();
assert!(handler.is_ok());
}
#[tokio::test]
async fn test_wasm_handler_simple_execution() {
let handler = WasmHandler::new().unwrap();
let wasm_bytes = mitoxide_wasm::test_utils::test_modules::minimal_wasm();
let input_data = Bytes::from(r#"{"message": "hello"}"#);
let request = Request::WasmExec {
id: Uuid::new_v4(),
module: Bytes::from(wasm_bytes.to_vec()),
input: input_data,
timeout: Some(10),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::WasmResult { output, duration_ms, .. } => {
assert!(duration_ms > 0);
assert!(output.len() >= 0);
}
Response::Error { error, .. } => {
assert!(error.code == ErrorCode::WasmFailed);
}
_ => panic!("Expected WasmResult or Error response"),
}
}
#[tokio::test]
async fn test_wasm_handler_with_function_module() {
let handler = WasmHandler::new().unwrap();
let wasm_bytes = mitoxide_wasm::test_utils::test_modules::simple_function_wasm();
let input_data = Bytes::from(r#"{"a": 5, "b": 3}"#);
let request = Request::WasmExec {
id: Uuid::new_v4(),
module: Bytes::from(wasm_bytes.to_vec()),
input: input_data,
timeout: Some(10),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::WasmResult { .. } => {
}
Response::Error { error, .. } => {
assert!(error.code == ErrorCode::WasmFailed);
}
_ => panic!("Expected WasmResult or Error response"),
}
}
#[tokio::test]
async fn test_wasm_handler_invalid_module() {
let handler = WasmHandler::new().unwrap();
let invalid_wasm = vec![0xFF, 0xFF, 0xFF, 0xFF];
let input_data = Bytes::from("test input");
let request = Request::WasmExec {
id: Uuid::new_v4(),
module: Bytes::from(invalid_wasm),
input: input_data,
timeout: Some(10),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::Error { error, .. } => {
assert!(error.code == ErrorCode::WasmFailed);
assert!(error.message.contains("Module loading failed"));
}
_ => panic!("Expected Error response for invalid WASM"),
}
}
#[tokio::test]
async fn test_wasm_handler_module_caching() {
let handler = WasmHandler::new().unwrap();
let wasm_bytes = mitoxide_wasm::test_utils::test_modules::minimal_wasm();
let input_data = Bytes::from("test");
for _ in 0..2 {
let request = Request::WasmExec {
id: Uuid::new_v4(),
module: Bytes::from(wasm_bytes.to_vec()),
input: input_data.clone(),
timeout: Some(10),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::WasmResult { .. } | Response::Error { .. } => {
}
_ => panic!("Expected WasmResult or Error response"),
}
}
}
#[tokio::test]
async fn test_wasm_handler_unsupported_request() {
let handler = WasmHandler::new().unwrap();
let request = Request::Ping {
id: Uuid::new_v4(),
timestamp: 12345,
};
let response = handler.handle(request).await.unwrap();
match response {
Response::Error { error, .. } => {
assert!(error.code == ErrorCode::Unsupported);
}
_ => panic!("Expected Error response for unsupported request"),
}
}
#[tokio::test]
async fn test_process_handler_timeout() {
let handler = ProcessHandler;
let command = if cfg!(windows) {
vec!["ping".to_string(), "-n".to_string(), "10".to_string(), "127.0.0.1".to_string()]
} else {
vec!["sleep".to_string(), "5".to_string()]
};
let request = Request::ProcessExec {
id: Uuid::new_v4(),
command,
env: HashMap::new(),
cwd: None,
stdin: None,
timeout: Some(1), };
let response = handler.handle(request).await.unwrap();
match response {
Response::Error { error, .. } => {
assert_eq!(error.code, ErrorCode::Timeout);
let message_lower = error.message.to_lowercase();
assert!(message_lower.contains("timeout") || message_lower.contains("timed out"),
"Error message should contain timeout: {}", error.message);
}
Response::ProcessResult { .. } => {
println!("Command completed before timeout - this is acceptable");
}
_ => panic!("Expected Error or ProcessResult response"),
}
}
#[tokio::test]
async fn test_process_handler_stderr_capture() {
let handler = ProcessHandler;
let command = if cfg!(windows) {
vec!["cmd".to_string(), "/c".to_string(), "echo error message 1>&2".to_string()]
} else {
vec!["sh".to_string(), "-c".to_string(), "echo 'error message' >&2".to_string()]
};
let request = Request::ProcessExec {
id: Uuid::new_v4(),
command,
env: HashMap::new(),
cwd: None,
stdin: None,
timeout: Some(10),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::ProcessResult { exit_code, stderr, .. } => {
assert_eq!(exit_code, 0);
let error_output = String::from_utf8(stderr.to_vec()).unwrap();
assert!(error_output.contains("error message"));
}
_ => panic!("Expected ProcessResult response"),
}
}
#[tokio::test]
async fn test_process_handler_empty_command() {
let handler = ProcessHandler;
let request = Request::ProcessExec {
id: Uuid::new_v4(),
command: vec![],
env: HashMap::new(),
cwd: None,
stdin: None,
timeout: None,
};
let response = handler.handle(request).await.unwrap();
match response {
Response::Error { error, .. } => {
assert_eq!(error.code, ErrorCode::InvalidRequest);
}
_ => panic!("Expected Error response"),
}
}
#[tokio::test]
async fn test_file_handler_put_get() {
let handler = FileHandler;
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
let content = Bytes::from("Hello, world!");
let put_request = Request::FilePut {
id: Uuid::new_v4(),
path: file_path.clone(),
content: content.clone(),
mode: Some(0o644),
create_dirs: true,
};
let put_response = handler.handle(put_request).await.unwrap();
match put_response {
Response::FilePutResult { bytes_written, .. } => {
assert_eq!(bytes_written, content.len() as u64);
}
_ => panic!("Expected FilePutResult response"),
}
let get_request = Request::FileGet {
id: Uuid::new_v4(),
path: file_path,
range: None,
};
let get_response = handler.handle(get_request).await.unwrap();
match get_response {
Response::FileContent { content: retrieved_content, metadata, .. } => {
assert_eq!(retrieved_content, content);
assert!(!metadata.is_dir);
assert_eq!(metadata.size, content.len() as u64);
}
_ => panic!("Expected FileContent response"),
}
}
#[tokio::test]
async fn test_file_handler_get_nonexistent() {
let handler = FileHandler;
let request = Request::FileGet {
id: Uuid::new_v4(),
path: PathBuf::from("/nonexistent/file.txt"),
range: None,
};
let response = handler.handle(request).await.unwrap();
match response {
Response::Error { error, .. } => {
println!("Error message: {}", error.message);
assert!(matches!(error.code, ErrorCode::FileNotFound | ErrorCode::InternalError));
}
_ => panic!("Expected Error response"),
}
}
#[tokio::test]
async fn test_file_handler_dir_list() {
let handler = FileHandler;
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("file1.txt");
let file2 = temp_dir.path().join("file2.txt");
let hidden_file = temp_dir.path().join(".hidden");
fs::write(&file1, "content1").await.unwrap();
fs::write(&file2, "content2").await.unwrap();
fs::write(&hidden_file, "hidden").await.unwrap();
let request = Request::DirList {
id: Uuid::new_v4(),
path: temp_dir.path().to_path_buf(),
include_hidden: false,
recursive: false,
};
let response = handler.handle(request).await.unwrap();
match response {
Response::DirListing { entries, .. } => {
assert_eq!(entries.len(), 2); let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
assert!(names.contains(&&"file1.txt".to_string()));
assert!(names.contains(&&"file2.txt".to_string()));
assert!(!names.contains(&&".hidden".to_string()));
}
_ => panic!("Expected DirListing response"),
}
let request_with_hidden = Request::DirList {
id: Uuid::new_v4(),
path: temp_dir.path().to_path_buf(),
include_hidden: true,
recursive: false,
};
let response_with_hidden = handler.handle(request_with_hidden).await.unwrap();
match response_with_hidden {
Response::DirListing { entries, .. } => {
assert_eq!(entries.len(), 3); let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
assert!(names.contains(&&".hidden".to_string()));
}
_ => panic!("Expected DirListing response"),
}
}
#[tokio::test]
async fn test_file_handler_recursive_dir_list() {
let handler = FileHandler;
let temp_dir = TempDir::new().unwrap();
let subdir = temp_dir.path().join("subdir");
fs::create_dir(&subdir).await.unwrap();
let file1 = temp_dir.path().join("file1.txt");
let file2 = subdir.join("file2.txt");
let file3 = subdir.join("file3.txt");
fs::write(&file1, "content1").await.unwrap();
fs::write(&file2, "content2").await.unwrap();
fs::write(&file3, "content3").await.unwrap();
let request = Request::DirList {
id: Uuid::new_v4(),
path: temp_dir.path().to_path_buf(),
include_hidden: false,
recursive: true,
};
let response = handler.handle(request).await.unwrap();
match response {
Response::DirListing { entries, .. } => {
assert!(entries.len() >= 4); let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
assert!(names.contains(&&"file1.txt".to_string()));
assert!(names.contains(&&"subdir".to_string()));
assert!(names.contains(&&"file2.txt".to_string()));
assert!(names.contains(&&"file3.txt".to_string()));
}
_ => panic!("Expected DirListing response"),
}
}
#[tokio::test]
async fn test_file_handler_range_get() {
let handler = FileHandler;
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
let content = "Hello, world! This is a test file with some content.";
fs::write(&file_path, content).await.unwrap();
let request = Request::FileGet {
id: Uuid::new_v4(),
path: file_path,
range: Some((7, 12)),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::FileContent { content: retrieved_content, .. } => {
let content_str = String::from_utf8(retrieved_content.to_vec()).unwrap();
assert_eq!(content_str, "world");
}
_ => panic!("Expected FileContent response"),
}
}
#[tokio::test]
async fn test_file_handler_create_dirs() {
let handler = FileHandler;
let temp_dir = TempDir::new().unwrap();
let nested_path = temp_dir.path().join("nested").join("dirs").join("test.txt");
let content = Bytes::from("test content");
let request = Request::FilePut {
id: Uuid::new_v4(),
path: nested_path.clone(),
content: content.clone(),
mode: Some(0o644),
create_dirs: true,
};
let response = handler.handle(request).await.unwrap();
match response {
Response::FilePutResult { bytes_written, .. } => {
assert_eq!(bytes_written, content.len() as u64);
}
_ => panic!("Expected FilePutResult response"),
}
assert!(nested_path.exists());
let read_content = fs::read(&nested_path).await.unwrap();
assert_eq!(read_content, content.to_vec());
}
#[tokio::test]
async fn test_file_handler_large_file() {
let handler = FileHandler;
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("large.txt");
let large_content = vec![b'A'; 1024 * 1024];
let content = Bytes::from(large_content.clone());
let put_request = Request::FilePut {
id: Uuid::new_v4(),
path: file_path.clone(),
content: content.clone(),
mode: Some(0o644),
create_dirs: false,
};
let put_response = handler.handle(put_request).await.unwrap();
match put_response {
Response::FilePutResult { bytes_written, .. } => {
assert_eq!(bytes_written, content.len() as u64);
}
_ => panic!("Expected FilePutResult response"),
}
let get_request = Request::FileGet {
id: Uuid::new_v4(),
path: file_path,
range: None,
};
let get_response = handler.handle(get_request).await.unwrap();
match get_response {
Response::FileContent { content: retrieved_content, metadata, .. } => {
assert_eq!(retrieved_content.len(), large_content.len());
assert_eq!(metadata.size, large_content.len() as u64);
assert_eq!(retrieved_content.to_vec(), large_content);
}
_ => panic!("Expected FileContent response"),
}
}
#[tokio::test]
async fn test_file_handler_permissions() {
let handler = FileHandler;
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test_perms.txt");
let content = Bytes::from("test content");
let request = Request::FilePut {
id: Uuid::new_v4(),
path: file_path.clone(),
content: content.clone(),
mode: Some(0o755),
create_dirs: false,
};
let response = handler.handle(request).await.unwrap();
match response {
Response::FilePutResult { bytes_written, .. } => {
assert_eq!(bytes_written, content.len() as u64);
}
_ => panic!("Expected FilePutResult response"),
}
assert!(file_path.exists());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(&file_path).unwrap();
let mode = metadata.permissions().mode() & 0o777;
assert_eq!(mode, 0o755);
}
}
#[tokio::test]
async fn test_file_handler_directory_as_file_error() {
let handler = FileHandler;
let temp_dir = TempDir::new().unwrap();
let request = Request::FileGet {
id: Uuid::new_v4(),
path: temp_dir.path().to_path_buf(),
range: None,
};
let response = handler.handle(request).await.unwrap();
match response {
Response::Error { error, .. } => {
assert_eq!(error.code, ErrorCode::InternalError);
assert!(error.message.contains("directory"));
}
_ => panic!("Expected Error response"),
}
}
#[tokio::test]
async fn test_file_handler_put_without_create_dirs() {
let handler = FileHandler;
let temp_dir = TempDir::new().unwrap();
let nested_path = temp_dir.path().join("nonexistent").join("test.txt");
let content = Bytes::from("test content");
let request = Request::FilePut {
id: Uuid::new_v4(),
path: nested_path,
content,
mode: Some(0o644),
create_dirs: false,
};
let response = handler.handle(request).await.unwrap();
match response {
Response::Error { error, .. } => {
assert!(matches!(error.code, ErrorCode::InternalError | ErrorCode::FileNotFound));
}
_ => panic!("Expected Error response"),
}
}
#[tokio::test]
async fn test_ping_handler() {
let handler = PingHandler;
let request_id = Uuid::new_v4();
let timestamp = 12345;
let request = Request::Ping {
id: request_id,
timestamp,
};
let response = handler.handle(request).await.unwrap();
match response {
Response::Pong { request_id: resp_id, timestamp: resp_timestamp, .. } => {
assert_eq!(resp_id, request_id);
assert_eq!(resp_timestamp, timestamp);
}
_ => panic!("Expected Pong response"),
}
}
#[tokio::test]
async fn test_pty_handler_basic_command() {
let handler = PtyHandler;
let command = if cfg!(windows) {
vec!["cmd".to_string(), "/c".to_string(), "echo".to_string(), "hello pty".to_string()]
} else {
vec!["echo".to_string(), "hello pty".to_string()]
};
let request = Request::PtyExec {
id: Uuid::new_v4(),
command,
env: HashMap::new(),
cwd: None,
privilege: None,
timeout: Some(10),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::PtyResult { exit_code, output, .. } => {
assert_eq!(exit_code, 0);
let output_str = String::from_utf8(output.to_vec()).unwrap();
assert!(output_str.contains("hello pty"));
}
_ => panic!("Expected PtyResult response"),
}
}
#[tokio::test]
async fn test_pty_handler_sudo_command() {
let handler = PtyHandler;
use mitoxide_proto::message::{PrivilegeEscalation, PrivilegeMethod, Credentials};
let privilege = PrivilegeEscalation {
method: PrivilegeMethod::Sudo,
credentials: Some(Credentials {
username: Some("root".to_string()),
password: Some("password".to_string()),
}),
prompt_patterns: vec!["[sudo] password".to_string()],
};
let command = vec!["whoami".to_string()];
let request = Request::PtyExec {
id: Uuid::new_v4(),
command,
env: HashMap::new(),
cwd: None,
privilege: Some(privilege),
timeout: Some(10),
};
let response = handler.handle(request).await.unwrap();
match response {
Response::PtyResult { .. } => {
}
Response::Error { error, .. } => {
assert!(matches!(error.code, ErrorCode::ProcessFailed | ErrorCode::PrivilegeEscalationFailed));
}
_ => panic!("Expected PtyResult or Error response"),
}
}
#[tokio::test]
async fn test_pty_handler_prompt_detection() {
let handler = PtyHandler;
assert!(handler.detect_privilege_prompt("Password:", &[]));
assert!(handler.detect_privilege_prompt("[sudo] password for user:", &[]));
assert!(handler.detect_privilege_prompt("su: password", &[]));
assert!(!handler.detect_privilege_prompt("normal output", &[]));
let custom_patterns = vec!["Enter passphrase:".to_string()];
assert!(handler.detect_privilege_prompt("Enter passphrase: ", &custom_patterns));
assert!(!handler.detect_privilege_prompt("Password:", &custom_patterns));
}
#[tokio::test]
async fn test_pty_handler_build_privileged_command() {
let handler = PtyHandler;
use mitoxide_proto::message::{PrivilegeEscalation, PrivilegeMethod, Credentials};
let command = vec!["ls".to_string(), "-la".to_string()];
let sudo_privilege = PrivilegeEscalation {
method: PrivilegeMethod::Sudo,
credentials: Some(Credentials {
username: Some("root".to_string()),
password: None,
}),
prompt_patterns: vec![],
};
let sudo_command = handler.build_privileged_command(&command, &sudo_privilege).unwrap();
assert_eq!(sudo_command[0], "sudo");
assert_eq!(sudo_command[1], "-S");
assert_eq!(sudo_command[2], "-u");
assert_eq!(sudo_command[3], "root");
assert_eq!(sudo_command[4], "ls");
assert_eq!(sudo_command[5], "-la");
let su_privilege = PrivilegeEscalation {
method: PrivilegeMethod::Su,
credentials: Some(Credentials {
username: Some("root".to_string()),
password: None,
}),
prompt_patterns: vec![],
};
let su_command = handler.build_privileged_command(&command, &su_privilege).unwrap();
assert_eq!(su_command[0], "su");
assert_eq!(su_command[1], "root");
assert_eq!(su_command[2], "-c");
assert_eq!(su_command[3], "ls -la");
let doas_privilege = PrivilegeEscalation {
method: PrivilegeMethod::Doas,
credentials: Some(Credentials {
username: Some("root".to_string()),
password: None,
}),
prompt_patterns: vec![],
};
let doas_command = handler.build_privileged_command(&command, &doas_privilege).unwrap();
assert_eq!(doas_command[0], "doas");
assert_eq!(doas_command[1], "-u");
assert_eq!(doas_command[2], "root");
assert_eq!(doas_command[3], "ls");
assert_eq!(doas_command[4], "-la");
}
#[tokio::test]
async fn test_pty_handler_empty_command() {
let handler = PtyHandler;
let request = Request::PtyExec {
id: Uuid::new_v4(),
command: vec![],
env: HashMap::new(),
cwd: None,
privilege: None,
timeout: None,
};
let response = handler.handle(request).await.unwrap();
match response {
Response::Error { error, .. } => {
assert_eq!(error.code, ErrorCode::InvalidRequest);
}
_ => panic!("Expected Error response"),
}
}
#[tokio::test]
async fn test_handler_wrong_request_type() {
let ping_handler = PingHandler;
let command = if cfg!(windows) {
vec!["cmd".to_string(), "/c".to_string(), "echo".to_string()]
} else {
vec!["echo".to_string()]
};
let process_request = Request::ProcessExec {
id: Uuid::new_v4(),
command,
env: HashMap::new(),
cwd: None,
stdin: None,
timeout: None,
};
let response = ping_handler.handle(process_request).await.unwrap();
match response {
Response::Error { error, .. } => {
assert_eq!(error.code, ErrorCode::Unsupported);
}
_ => panic!("Expected Error response"),
}
}
}