1use crate::agent::Handler;
4use anyhow::{Context, Result};
5use async_trait::async_trait;
6use bytes::Bytes;
7use mitoxide_proto::{Request, Response};
8use mitoxide_proto::message::{ErrorCode, ErrorDetails, FileMetadata, DirEntry, PrivilegeMethod};
9use std::collections::HashMap;
10use std::path::Path;
11use std::process::Stdio;
12use std::sync::Arc;
13use tokio::fs;
14use tokio::io::{AsyncReadExt, AsyncWriteExt};
15use tokio::process::Command;
16use tracing::{debug, error, warn};
17
18pub struct ProcessHandler;
20
21#[async_trait]
22impl Handler for ProcessHandler {
23 async fn handle(&self, request: Request) -> Result<Response> {
24 match request {
25 Request::ProcessExec { id, command, env, cwd, stdin, timeout } => {
26 debug!("Executing process: {:?}", command);
27
28 if command.is_empty() {
29 return Ok(Response::error(
30 id,
31 ErrorDetails::new(ErrorCode::InvalidRequest, "Empty command")
32 ));
33 }
34
35 let start_time = std::time::Instant::now();
36
37 let mut cmd = Command::new(&command[0]);
39 if command.len() > 1 {
40 cmd.args(&command[1..]);
41 }
42
43 for (key, value) in env {
45 cmd.env(key, value);
46 }
47
48 if let Some(cwd) = cwd {
50 cmd.current_dir(cwd);
51 }
52
53 cmd.stdin(Stdio::piped())
55 .stdout(Stdio::piped())
56 .stderr(Stdio::piped());
57
58 let mut child = cmd.spawn()
60 .context("Failed to spawn process")?;
61
62 if let Some(stdin_data) = stdin {
64 if let Some(mut child_stdin) = child.stdin.take() {
65 if let Err(e) = child_stdin.write_all(&stdin_data).await {
66 warn!("Failed to write to process stdin: {}", e);
67 }
68 drop(child_stdin); }
70 }
71
72 let output = if let Some(timeout_secs) = timeout {
74 let timeout_duration = std::time::Duration::from_secs(timeout_secs);
75
76 match tokio::time::timeout(timeout_duration, child.wait_with_output()).await {
77 Ok(Ok(output)) => output,
78 Ok(Err(e)) => {
79 return Ok(Response::error(
80 id,
81 ErrorDetails::new(ErrorCode::ProcessFailed, format!("Process error: {}", e))
82 ));
83 }
84 Err(_) => {
85 return Ok(Response::error(
88 id,
89 ErrorDetails::new(ErrorCode::Timeout, "Process execution timed out")
90 ));
91 }
92 }
93 } else {
94 match child.wait_with_output().await {
95 Ok(output) => output,
96 Err(e) => {
97 return Ok(Response::error(
98 id,
99 ErrorDetails::new(ErrorCode::ProcessFailed, format!("Process error: {}", e))
100 ));
101 }
102 }
103 };
104
105 let duration = start_time.elapsed();
106
107 Ok(Response::ProcessResult {
108 request_id: id,
109 exit_code: output.status.code().unwrap_or(-1),
110 stdout: Bytes::from(output.stdout),
111 stderr: Bytes::from(output.stderr),
112 duration_ms: duration.as_millis() as u64,
113 })
114 }
115 _ => Ok(Response::error(
116 request.id(),
117 ErrorDetails::new(ErrorCode::Unsupported, "ProcessHandler only handles ProcessExec requests")
118 ))
119 }
120 }
121}
122
123pub struct FileHandler;
125
126#[async_trait]
127impl Handler for FileHandler {
128 async fn handle(&self, request: Request) -> Result<Response> {
129 match request {
130 Request::FileGet { id, path, range } => {
131 debug!("Getting file: {:?}", path);
132
133 match self.handle_file_get(&path, range).await {
134 Ok((content, metadata)) => {
135 Ok(Response::FileContent {
136 request_id: id,
137 content,
138 metadata,
139 })
140 }
141 Err(e) => {
142 error!("File get error: {}", e);
143 let error_string = e.to_string().to_lowercase();
144 let error_code = if error_string.contains("no such file") ||
145 error_string.contains("not found") ||
146 error_string.contains("cannot find") {
147 ErrorCode::FileNotFound
148 } else if error_string.contains("permission denied") ||
149 error_string.contains("access denied") {
150 ErrorCode::PermissionDenied
151 } else {
152 ErrorCode::InternalError
153 };
154
155 Ok(Response::error(
156 id,
157 ErrorDetails::new(error_code, format!("File get failed: {}", e))
158 ))
159 }
160 }
161 }
162
163 Request::FilePut { id, path, content, mode, create_dirs } => {
164 debug!("Putting file: {:?}", path);
165
166 match self.handle_file_put(&path, &content, mode, create_dirs).await {
167 Ok(bytes_written) => {
168 Ok(Response::FilePutResult {
169 request_id: id,
170 bytes_written,
171 })
172 }
173 Err(e) => {
174 error!("File put error: {}", e);
175 let error_code = if e.to_string().contains("Permission denied") {
176 ErrorCode::PermissionDenied
177 } else {
178 ErrorCode::InternalError
179 };
180
181 Ok(Response::error(
182 id,
183 ErrorDetails::new(error_code, format!("File put failed: {}", e))
184 ))
185 }
186 }
187 }
188
189 Request::DirList { id, path, include_hidden, recursive } => {
190 debug!("Listing directory: {:?}", path);
191
192 match self.handle_dir_list(&path, include_hidden, recursive).await {
193 Ok(entries) => {
194 Ok(Response::DirListing {
195 request_id: id,
196 entries,
197 })
198 }
199 Err(e) => {
200 error!("Directory list error: {}", e);
201 let error_code = if e.to_string().contains("No such file") {
202 ErrorCode::FileNotFound
203 } else if e.to_string().contains("Permission denied") {
204 ErrorCode::PermissionDenied
205 } else {
206 ErrorCode::InternalError
207 };
208
209 Ok(Response::error(
210 id,
211 ErrorDetails::new(error_code, format!("Directory list failed: {}", e))
212 ))
213 }
214 }
215 }
216
217 _ => Ok(Response::error(
218 request.id(),
219 ErrorDetails::new(ErrorCode::Unsupported, "FileHandler only handles file/directory requests")
220 ))
221 }
222 }
223}
224
225impl FileHandler {
226 async fn handle_file_get(&self, path: &Path, range: Option<(u64, u64)>) -> Result<(Bytes, FileMetadata)> {
228 let metadata = fs::metadata(path).await
229 .context("Failed to get file metadata")?;
230
231 if metadata.is_dir() {
232 return Err(anyhow::anyhow!("Path is a directory, not a file"));
233 }
234
235 let file_metadata = FileMetadata {
236 size: metadata.len(),
237 mode: 0o644, modified: metadata.modified()
239 .unwrap_or(std::time::UNIX_EPOCH)
240 .duration_since(std::time::UNIX_EPOCH)
241 .unwrap_or_default()
242 .as_secs(),
243 is_dir: false,
244 is_symlink: metadata.file_type().is_symlink(),
245 };
246
247 let content = if let Some((start, end)) = range {
248 let mut file = fs::File::open(path).await
250 .context("Failed to open file")?;
251
252 let file_size = metadata.len();
253 let actual_start = start.min(file_size);
254 let actual_end = end.min(file_size);
255
256 if actual_start >= actual_end {
257 Bytes::new()
258 } else {
259 use tokio::io::{AsyncSeekExt, SeekFrom};
260 file.seek(SeekFrom::Start(actual_start)).await
261 .context("Failed to seek in file")?;
262
263 let read_size = (actual_end - actual_start) as usize;
264 let mut buffer = vec![0u8; read_size];
265 let bytes_read = file.read_exact(&mut buffer).await
266 .context("Failed to read file range")?;
267
268 buffer.truncate(bytes_read);
269 Bytes::from(buffer)
270 }
271 } else {
272 let content = fs::read(path).await
274 .context("Failed to read file")?;
275 Bytes::from(content)
276 };
277
278 Ok((content, file_metadata))
279 }
280
281 async fn handle_file_put(&self, path: &Path, content: &Bytes, _mode: Option<u32>, create_dirs: bool) -> Result<u64> {
283 if create_dirs {
285 if let Some(parent) = path.parent() {
286 fs::create_dir_all(parent).await
287 .context("Failed to create parent directories")?;
288 }
289 }
290
291 fs::write(path, content).await
293 .context("Failed to write file")?;
294
295 #[cfg(unix)]
297 if let Some(mode) = _mode {
298 use std::os::unix::fs::PermissionsExt;
299 let permissions = std::fs::Permissions::from_mode(mode);
300 fs::set_permissions(path, permissions).await
301 .context("Failed to set file permissions")?;
302 }
303
304 Ok(content.len() as u64)
305 }
306
307 async fn handle_dir_list(&self, path: &Path, include_hidden: bool, recursive: bool) -> Result<Vec<DirEntry>> {
309 let mut entries = Vec::new();
310
311 if recursive {
312 self.collect_entries_recursive(path, include_hidden, &mut entries).await?;
313 } else {
314 self.collect_entries_single(path, include_hidden, &mut entries).await?;
315 }
316
317 Ok(entries)
318 }
319
320 async fn collect_entries_single(&self, path: &Path, include_hidden: bool, entries: &mut Vec<DirEntry>) -> Result<()> {
322 let mut dir = fs::read_dir(path).await
323 .context("Failed to read directory")?;
324
325 while let Some(entry) = dir.next_entry().await
326 .context("Failed to read directory entry")? {
327
328 let entry_path = entry.path();
329 let name = entry.file_name().to_string_lossy().to_string();
330
331 if !include_hidden && name.starts_with('.') {
333 continue;
334 }
335
336 let metadata = entry.metadata().await
337 .context("Failed to get entry metadata")?;
338
339 let file_metadata = FileMetadata {
340 size: metadata.len(),
341 mode: 0o644, modified: metadata.modified()
343 .unwrap_or(std::time::UNIX_EPOCH)
344 .duration_since(std::time::UNIX_EPOCH)
345 .unwrap_or_default()
346 .as_secs(),
347 is_dir: metadata.is_dir(),
348 is_symlink: metadata.file_type().is_symlink(),
349 };
350
351 entries.push(DirEntry {
352 name,
353 path: entry_path,
354 metadata: file_metadata,
355 });
356 }
357
358 Ok(())
359 }
360
361 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>> {
363 Box::pin(async move {
364 self.collect_entries_single(path, include_hidden, entries).await?;
365
366 let mut subdirs = Vec::new();
368 for entry in entries.iter() {
369 if entry.metadata.is_dir && entry.path != path {
370 subdirs.push(entry.path.clone());
371 }
372 }
373
374 for subdir in subdirs {
376 if let Err(e) = self.collect_entries_recursive(&subdir, include_hidden, entries).await {
377 warn!("Failed to read subdirectory {:?}: {}", subdir, e);
378 }
380 }
381
382 Ok(())
383 })
384 }
385}
386
387pub struct PtyHandler;
389
390#[async_trait]
391impl Handler for PtyHandler {
392 async fn handle(&self, request: Request) -> Result<Response> {
393 match request {
394 Request::PtyExec { id, command, env, cwd, privilege, timeout } => {
395 debug!("Executing PTY process: {:?}", command);
396
397 if command.is_empty() {
398 return Ok(Response::error(
399 id,
400 ErrorDetails::new(ErrorCode::InvalidRequest, "Empty command")
401 ));
402 }
403
404 let start_time = std::time::Instant::now();
405
406 let final_command = if let Some(priv_config) = privilege {
408 self.build_privileged_command(&command, &priv_config)?
409 } else {
410 command
411 };
412
413 let mut cmd = Command::new(&final_command[0]);
416 if final_command.len() > 1 {
417 cmd.args(&final_command[1..]);
418 }
419
420 for (key, value) in env {
422 cmd.env(key, value);
423 }
424
425 if let Some(cwd) = cwd {
427 cmd.current_dir(cwd);
428 }
429
430 cmd.stdin(Stdio::piped())
432 .stdout(Stdio::piped())
433 .stderr(Stdio::piped());
434
435 let output = if let Some(timeout_secs) = timeout {
437 let timeout_duration = std::time::Duration::from_secs(timeout_secs);
438
439 match tokio::time::timeout(timeout_duration, cmd.output()).await {
440 Ok(Ok(output)) => output,
441 Ok(Err(e)) => {
442 return Ok(Response::error(
443 id,
444 ErrorDetails::new(ErrorCode::ProcessFailed, format!("Process error: {}", e))
445 ));
446 }
447 Err(_) => {
448 return Ok(Response::error(
449 id,
450 ErrorDetails::new(ErrorCode::Timeout, "Process execution timed out")
451 ));
452 }
453 }
454 } else {
455 match cmd.output().await {
456 Ok(output) => output,
457 Err(e) => {
458 return Ok(Response::error(
459 id,
460 ErrorDetails::new(ErrorCode::ProcessFailed, format!("Process error: {}", e))
461 ));
462 }
463 }
464 };
465
466 let duration = start_time.elapsed();
467
468 let mut combined_output = output.stdout;
470 combined_output.extend_from_slice(&output.stderr);
471
472 Ok(Response::PtyResult {
473 request_id: id,
474 exit_code: output.status.code().unwrap_or(-1),
475 output: Bytes::from(combined_output),
476 duration_ms: duration.as_millis() as u64,
477 })
478 }
479 _ => Ok(Response::error(
480 request.id(),
481 ErrorDetails::new(ErrorCode::Unsupported, "PtyHandler only handles PtyExec requests")
482 ))
483 }
484 }
485}
486
487impl PtyHandler {
488 fn build_privileged_command(
490 &self,
491 command: &[String],
492 privilege: &mitoxide_proto::message::PrivilegeEscalation,
493 ) -> Result<Vec<String>> {
494 let mut privileged_command = Vec::new();
495
496 match &privilege.method {
497 PrivilegeMethod::Sudo => {
498 privileged_command.push("sudo".to_string());
499 privileged_command.push("-S".to_string()); if let Some(ref creds) = privilege.credentials {
501 if let Some(ref username) = creds.username {
502 privileged_command.push("-u".to_string());
503 privileged_command.push(username.clone());
504 }
505 }
506 privileged_command.extend_from_slice(command);
507 }
508 PrivilegeMethod::Su => {
509 privileged_command.push("su".to_string());
510 if let Some(ref creds) = privilege.credentials {
511 if let Some(ref username) = creds.username {
512 privileged_command.push(username.clone());
513 }
514 }
515 privileged_command.push("-c".to_string());
516 privileged_command.push(command.join(" "));
517 }
518 PrivilegeMethod::Doas => {
519 privileged_command.push("doas".to_string());
520 if let Some(ref creds) = privilege.credentials {
521 if let Some(ref username) = creds.username {
522 privileged_command.push("-u".to_string());
523 privileged_command.push(username.clone());
524 }
525 }
526 privileged_command.extend_from_slice(command);
527 }
528 PrivilegeMethod::Custom(cmd) => {
529 privileged_command.push(cmd.clone());
530 privileged_command.extend_from_slice(command);
531 }
532 }
533
534 Ok(privileged_command)
535 }
536
537 fn detect_privilege_prompt(&self, output: &str, patterns: &[String]) -> bool {
539 let default_patterns = [
540 "password:",
541 "Password:",
542 "[sudo] password",
543 "su:",
544 "doas:",
545 ];
546
547 if !patterns.is_empty() {
549 for pattern in patterns {
550 if output.to_lowercase().contains(&pattern.to_lowercase()) {
551 return true;
552 }
553 }
554 return false;
555 }
556
557 for pattern in &default_patterns {
559 if output.to_lowercase().contains(&pattern.to_lowercase()) {
560 return true;
561 }
562 }
563
564 false
565 }
566}
567
568pub struct PingHandler;
570
571#[async_trait]
572impl Handler for PingHandler {
573 async fn handle(&self, request: Request) -> Result<Response> {
574 match request {
575 Request::Ping { id, timestamp } => {
576 debug!("Handling ping request: id={}, timestamp={}", id, timestamp);
577 Ok(Response::pong(id, timestamp))
578 }
579 _ => Ok(Response::error(
580 request.id(),
581 ErrorDetails::new(ErrorCode::Unsupported, "PingHandler only handles Ping requests")
582 ))
583 }
584 }
585}
586
587pub struct WasmHandler {
589 runtime: Arc<mitoxide_wasm::WasmRuntime>,
591 module_cache: Arc<tokio::sync::RwLock<HashMap<String, mitoxide_wasm::WasmModule>>>,
593}
594
595impl WasmHandler {
596 pub fn new() -> Result<Self> {
598 let runtime = Arc::new(mitoxide_wasm::WasmRuntime::new()
599 .map_err(|e| anyhow::anyhow!("Failed to create WASM runtime: {}", e))?);
600
601 let module_cache = Arc::new(tokio::sync::RwLock::new(HashMap::new()));
602
603 Ok(WasmHandler {
604 runtime,
605 module_cache,
606 })
607 }
608
609 pub fn with_config(config: mitoxide_wasm::WasmConfig) -> Result<Self> {
611 let runtime = Arc::new(mitoxide_wasm::WasmRuntime::with_config(config)
612 .map_err(|e| anyhow::anyhow!("Failed to create WASM runtime: {}", e))?);
613
614 let module_cache = Arc::new(tokio::sync::RwLock::new(HashMap::new()));
615
616 Ok(WasmHandler {
617 runtime,
618 module_cache,
619 })
620 }
621
622 async fn get_or_load_module(&self, module_bytes: &[u8]) -> Result<mitoxide_wasm::WasmModule> {
624 let module = mitoxide_wasm::WasmModule::from_bytes(module_bytes.to_vec())
626 .map_err(|e| anyhow::anyhow!("Failed to load WASM module: {}", e))?;
627
628 let module_hash = module.hash().to_string();
629
630 {
632 let cache = self.module_cache.read().await;
633 if let Some(cached_module) = cache.get(&module_hash) {
634 debug!("Using cached WASM module: {}", module_hash);
635 return Ok(cached_module.clone());
636 }
637 }
638
639 {
641 let mut cache = self.module_cache.write().await;
642 debug!("Caching WASM module: {}", module_hash);
643 cache.insert(module_hash, module.clone());
644 }
645
646 Ok(module)
647 }
648
649 fn verify_module_hash(&self, module: &mitoxide_wasm::WasmModule, expected_hash: Option<&str>) -> Result<()> {
651 if let Some(expected) = expected_hash {
652 let actual = module.hash();
653 if actual != expected {
654 return Err(anyhow::anyhow!(
655 "Module hash mismatch: expected {}, got {}",
656 expected,
657 actual
658 ));
659 }
660 }
661 Ok(())
662 }
663}
664
665#[async_trait]
666impl Handler for WasmHandler {
667 async fn handle(&self, request: Request) -> Result<Response> {
668 match request {
669 Request::WasmExec { id, module, input, timeout } => {
670 debug!("Executing WASM module: {} bytes", module.len());
671
672 let start_time = std::time::Instant::now();
673
674 let mut wasm_module = match self.get_or_load_module(&module).await {
676 Ok(module) => module,
677 Err(e) => {
678 error!("Failed to load WASM module: {}", e);
679 return Ok(Response::error(
680 id,
681 ErrorDetails::new(ErrorCode::WasmFailed, format!("Module loading failed: {}", e))
682 ));
683 }
684 };
685
686 let context = mitoxide_wasm::WasmContext::new();
688
689 let execution_result = if wasm_module.is_wasi() {
691 let input_str = String::from_utf8(input.to_vec())
693 .unwrap_or_else(|_| {
694 serde_json::to_string(&input.to_vec()).unwrap_or_default()
696 });
697
698 self.runtime.execute_with_stdio(&mut wasm_module, &input_str, context).await
699 } else {
700 match serde_json::from_slice::<serde_json::Value>(&input) {
702 Ok(json_input) => {
703 match self.runtime.execute_json::<serde_json::Value, serde_json::Value>(
704 &mut wasm_module,
705 &json_input,
706 context,
707 ).await {
708 Ok(output) => {
709 serde_json::to_string(&output)
710 .map_err(|e| mitoxide_wasm::WasmError::Execution(format!("JSON serialization failed: {}", e)))
711 }
712 Err(e) => Err(e),
713 }
714 }
715 Err(_) => {
716 let input_str = String::from_utf8_lossy(&input);
718 self.runtime.execute_with_stdio(&mut wasm_module, &input_str, context).await
719 }
720 }
721 };
722
723 let duration = start_time.elapsed();
724
725 match execution_result {
726 Ok(output) => {
727 debug!("WASM execution completed in {:?}", duration);
728 Ok(Response::WasmResult {
729 request_id: id,
730 output: Bytes::from(output),
731 duration_ms: duration.as_millis() as u64,
732 })
733 }
734 Err(e) => {
735 error!("WASM execution failed: {}", e);
736 Ok(Response::error(
737 id,
738 ErrorDetails::new(ErrorCode::WasmFailed, format!("Execution failed: {}", e))
739 ))
740 }
741 }
742 }
743 _ => Ok(Response::error(
744 request.id(),
745 ErrorDetails::new(ErrorCode::Unsupported, "WasmHandler only handles WasmExec requests")
746 ))
747 }
748 }
749}
750
751impl Default for WasmHandler {
752 fn default() -> Self {
753 Self::new().expect("Failed to create default WASM handler")
754 }
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760 use std::collections::HashMap;
761 use std::path::PathBuf;
762 use tempfile::TempDir;
763 use tokio::fs;
764 use uuid::Uuid;
765
766 #[tokio::test]
767 async fn test_process_handler_echo() {
768 let handler = ProcessHandler;
769
770 let (command, args) = if cfg!(windows) {
772 ("cmd".to_string(), vec!["/c".to_string(), "echo".to_string(), "hello world".to_string()])
773 } else {
774 ("echo".to_string(), vec!["hello world".to_string()])
775 };
776
777 let mut full_command = vec![command];
778 full_command.extend(args);
779
780 let request = Request::ProcessExec {
781 id: Uuid::new_v4(),
782 command: full_command,
783 env: HashMap::new(),
784 cwd: None,
785 stdin: None,
786 timeout: Some(10),
787 };
788
789 let response = handler.handle(request).await.unwrap();
790
791 match response {
792 Response::ProcessResult { exit_code, stdout, .. } => {
793 assert_eq!(exit_code, 0);
794 let output = String::from_utf8(stdout.to_vec()).unwrap();
795 assert!(output.contains("hello world"));
796 }
797 _ => panic!("Expected ProcessResult response"),
798 }
799 }
800
801 #[tokio::test]
802 async fn test_process_handler_with_env_vars() {
803 let handler = ProcessHandler;
804
805 let mut env = HashMap::new();
806 env.insert("TEST_VAR".to_string(), "test_value".to_string());
807
808 let command = if cfg!(windows) {
810 vec!["cmd".to_string(), "/c".to_string(), "echo".to_string(), "%TEST_VAR%".to_string()]
811 } else {
812 vec!["sh".to_string(), "-c".to_string(), "echo $TEST_VAR".to_string()]
813 };
814
815 let request = Request::ProcessExec {
816 id: Uuid::new_v4(),
817 command,
818 env,
819 cwd: None,
820 stdin: None,
821 timeout: Some(10),
822 };
823
824 let response = handler.handle(request).await.unwrap();
825
826 match response {
827 Response::ProcessResult { exit_code, stdout, .. } => {
828 assert_eq!(exit_code, 0);
829 let output = String::from_utf8(stdout.to_vec()).unwrap();
830 assert!(output.contains("test_value"));
831 }
832 _ => panic!("Expected ProcessResult response"),
833 }
834 }
835
836 #[tokio::test]
837 async fn test_process_handler_with_stdin() {
838 let handler = ProcessHandler;
839
840 let stdin_data = Bytes::from("hello from stdin");
841
842 let command = if cfg!(windows) {
844 vec!["cmd".to_string(), "/c".to_string(), "more".to_string()]
846 } else {
847 vec!["cat".to_string()]
848 };
849
850 let request = Request::ProcessExec {
851 id: Uuid::new_v4(),
852 command,
853 env: HashMap::new(),
854 cwd: None,
855 stdin: Some(stdin_data.clone()),
856 timeout: Some(10),
857 };
858
859 let response = handler.handle(request).await.unwrap();
860
861 match response {
862 Response::ProcessResult { exit_code, stdout, .. } => {
863 assert_eq!(exit_code, 0);
864 let output = String::from_utf8(stdout.to_vec()).unwrap();
865 assert!(output.contains("hello from stdin"));
866 }
867 _ => panic!("Expected ProcessResult response"),
868 }
869 }
870
871 #[tokio::test]
872 async fn test_process_handler_with_working_directory() {
873 let handler = ProcessHandler;
874 let temp_dir = TempDir::new().unwrap();
875
876 let command = if cfg!(windows) {
878 vec!["cmd".to_string(), "/c".to_string(), "cd".to_string()]
879 } else {
880 vec!["pwd".to_string()]
881 };
882
883 let request = Request::ProcessExec {
884 id: Uuid::new_v4(),
885 command,
886 env: HashMap::new(),
887 cwd: Some(temp_dir.path().to_path_buf()),
888 stdin: None,
889 timeout: Some(10),
890 };
891
892 let response = handler.handle(request).await.unwrap();
893
894 match response {
895 Response::ProcessResult { exit_code, stdout, .. } => {
896 assert_eq!(exit_code, 0);
897 let output = String::from_utf8(stdout.to_vec()).unwrap();
898 let temp_path_str = temp_dir.path().to_string_lossy();
899 assert!(output.contains(&*temp_path_str));
900 }
901 _ => panic!("Expected ProcessResult response"),
902 }
903 }
904
905 #[tokio::test]
906 async fn test_process_handler_binary_data() {
907 let handler = ProcessHandler;
908
909 let binary_data = vec![0x01, 0x02, 0xFF, 0xFE, 0xFD];
911 let stdin_data = Bytes::from(binary_data.clone());
912
913 let command = if cfg!(windows) {
915 vec!["findstr".to_string(), ".*".to_string()]
917 } else {
918 vec!["cat".to_string()]
919 };
920
921 let request = Request::ProcessExec {
922 id: Uuid::new_v4(),
923 command,
924 env: HashMap::new(),
925 cwd: None,
926 stdin: Some(stdin_data),
927 timeout: Some(10),
928 };
929
930 let response = handler.handle(request).await.unwrap();
931
932 match response {
933 Response::ProcessResult { exit_code, stdout, .. } => {
934 assert_eq!(exit_code, 0);
935 if cfg!(windows) {
937 assert!(!stdout.is_empty() || true); } else {
940 assert!(!stdout.is_empty());
942 }
943 }
944 _ => panic!("Expected ProcessResult response"),
945 }
946 }
947
948 #[tokio::test]
949 async fn test_wasm_handler_creation() {
950 let handler = WasmHandler::new();
951 assert!(handler.is_ok());
952 }
953
954 #[tokio::test]
955 async fn test_wasm_handler_simple_execution() {
956 let handler = WasmHandler::new().unwrap();
957
958 let wasm_bytes = mitoxide_wasm::test_utils::test_modules::minimal_wasm();
960 let input_data = Bytes::from(r#"{"message": "hello"}"#);
961
962 let request = Request::WasmExec {
963 id: Uuid::new_v4(),
964 module: Bytes::from(wasm_bytes.to_vec()),
965 input: input_data,
966 timeout: Some(10),
967 };
968
969 let response = handler.handle(request).await.unwrap();
970
971 match response {
972 Response::WasmResult { output, duration_ms, .. } => {
973 assert!(duration_ms > 0);
975 assert!(output.len() >= 0);
977 }
978 Response::Error { error, .. } => {
979 assert!(error.code == ErrorCode::WasmFailed);
981 }
982 _ => panic!("Expected WasmResult or Error response"),
983 }
984 }
985
986 #[tokio::test]
987 async fn test_wasm_handler_with_function_module() {
988 let handler = WasmHandler::new().unwrap();
989
990 let wasm_bytes = mitoxide_wasm::test_utils::test_modules::simple_function_wasm();
992 let input_data = Bytes::from(r#"{"a": 5, "b": 3}"#);
993
994 let request = Request::WasmExec {
995 id: Uuid::new_v4(),
996 module: Bytes::from(wasm_bytes.to_vec()),
997 input: input_data,
998 timeout: Some(10),
999 };
1000
1001 let response = handler.handle(request).await.unwrap();
1002
1003 match response {
1006 Response::WasmResult { .. } => {
1007 }
1009 Response::Error { error, .. } => {
1010 assert!(error.code == ErrorCode::WasmFailed);
1012 }
1013 _ => panic!("Expected WasmResult or Error response"),
1014 }
1015 }
1016
1017 #[tokio::test]
1018 async fn test_wasm_handler_invalid_module() {
1019 let handler = WasmHandler::new().unwrap();
1020
1021 let invalid_wasm = vec![0xFF, 0xFF, 0xFF, 0xFF];
1023 let input_data = Bytes::from("test input");
1024
1025 let request = Request::WasmExec {
1026 id: Uuid::new_v4(),
1027 module: Bytes::from(invalid_wasm),
1028 input: input_data,
1029 timeout: Some(10),
1030 };
1031
1032 let response = handler.handle(request).await.unwrap();
1033
1034 match response {
1035 Response::Error { error, .. } => {
1036 assert!(error.code == ErrorCode::WasmFailed);
1037 assert!(error.message.contains("Module loading failed"));
1038 }
1039 _ => panic!("Expected Error response for invalid WASM"),
1040 }
1041 }
1042
1043 #[tokio::test]
1044 async fn test_wasm_handler_module_caching() {
1045 let handler = WasmHandler::new().unwrap();
1046
1047 let wasm_bytes = mitoxide_wasm::test_utils::test_modules::minimal_wasm();
1048 let input_data = Bytes::from("test");
1049
1050 for _ in 0..2 {
1052 let request = Request::WasmExec {
1053 id: Uuid::new_v4(),
1054 module: Bytes::from(wasm_bytes.to_vec()),
1055 input: input_data.clone(),
1056 timeout: Some(10),
1057 };
1058
1059 let response = handler.handle(request).await.unwrap();
1060
1061 match response {
1063 Response::WasmResult { .. } | Response::Error { .. } => {
1064 }
1066 _ => panic!("Expected WasmResult or Error response"),
1067 }
1068 }
1069 }
1070
1071 #[tokio::test]
1072 async fn test_wasm_handler_unsupported_request() {
1073 let handler = WasmHandler::new().unwrap();
1074
1075 let request = Request::Ping {
1076 id: Uuid::new_v4(),
1077 timestamp: 12345,
1078 };
1079
1080 let response = handler.handle(request).await.unwrap();
1081
1082 match response {
1083 Response::Error { error, .. } => {
1084 assert!(error.code == ErrorCode::Unsupported);
1085 }
1086 _ => panic!("Expected Error response for unsupported request"),
1087 }
1088 }
1089
1090 #[tokio::test]
1091 async fn test_process_handler_timeout() {
1092 let handler = ProcessHandler;
1093
1094 let command = if cfg!(windows) {
1096 vec!["ping".to_string(), "-n".to_string(), "10".to_string(), "127.0.0.1".to_string()]
1098 } else {
1099 vec!["sleep".to_string(), "5".to_string()]
1100 };
1101
1102 let request = Request::ProcessExec {
1103 id: Uuid::new_v4(),
1104 command,
1105 env: HashMap::new(),
1106 cwd: None,
1107 stdin: None,
1108 timeout: Some(1), };
1110
1111 let response = handler.handle(request).await.unwrap();
1112
1113 match response {
1114 Response::Error { error, .. } => {
1115 assert_eq!(error.code, ErrorCode::Timeout);
1116 let message_lower = error.message.to_lowercase();
1118 assert!(message_lower.contains("timeout") || message_lower.contains("timed out"),
1119 "Error message should contain timeout: {}", error.message);
1120 }
1121 Response::ProcessResult { .. } => {
1122 println!("Command completed before timeout - this is acceptable");
1125 }
1126 _ => panic!("Expected Error or ProcessResult response"),
1127 }
1128 }
1129
1130 #[tokio::test]
1131 async fn test_process_handler_stderr_capture() {
1132 let handler = ProcessHandler;
1133
1134 let command = if cfg!(windows) {
1136 vec!["cmd".to_string(), "/c".to_string(), "echo error message 1>&2".to_string()]
1137 } else {
1138 vec!["sh".to_string(), "-c".to_string(), "echo 'error message' >&2".to_string()]
1139 };
1140
1141 let request = Request::ProcessExec {
1142 id: Uuid::new_v4(),
1143 command,
1144 env: HashMap::new(),
1145 cwd: None,
1146 stdin: None,
1147 timeout: Some(10),
1148 };
1149
1150 let response = handler.handle(request).await.unwrap();
1151
1152 match response {
1153 Response::ProcessResult { exit_code, stderr, .. } => {
1154 assert_eq!(exit_code, 0);
1155 let error_output = String::from_utf8(stderr.to_vec()).unwrap();
1156 assert!(error_output.contains("error message"));
1157 }
1158 _ => panic!("Expected ProcessResult response"),
1159 }
1160 }
1161
1162 #[tokio::test]
1163 async fn test_process_handler_empty_command() {
1164 let handler = ProcessHandler;
1165 let request = Request::ProcessExec {
1166 id: Uuid::new_v4(),
1167 command: vec![],
1168 env: HashMap::new(),
1169 cwd: None,
1170 stdin: None,
1171 timeout: None,
1172 };
1173
1174 let response = handler.handle(request).await.unwrap();
1175
1176 match response {
1177 Response::Error { error, .. } => {
1178 assert_eq!(error.code, ErrorCode::InvalidRequest);
1179 }
1180 _ => panic!("Expected Error response"),
1181 }
1182 }
1183
1184 #[tokio::test]
1185 async fn test_file_handler_put_get() {
1186 let handler = FileHandler;
1187 let temp_dir = TempDir::new().unwrap();
1188 let file_path = temp_dir.path().join("test.txt");
1189 let content = Bytes::from("Hello, world!");
1190
1191 let put_request = Request::FilePut {
1193 id: Uuid::new_v4(),
1194 path: file_path.clone(),
1195 content: content.clone(),
1196 mode: Some(0o644),
1197 create_dirs: true,
1198 };
1199
1200 let put_response = handler.handle(put_request).await.unwrap();
1201 match put_response {
1202 Response::FilePutResult { bytes_written, .. } => {
1203 assert_eq!(bytes_written, content.len() as u64);
1204 }
1205 _ => panic!("Expected FilePutResult response"),
1206 }
1207
1208 let get_request = Request::FileGet {
1210 id: Uuid::new_v4(),
1211 path: file_path,
1212 range: None,
1213 };
1214
1215 let get_response = handler.handle(get_request).await.unwrap();
1216 match get_response {
1217 Response::FileContent { content: retrieved_content, metadata, .. } => {
1218 assert_eq!(retrieved_content, content);
1219 assert!(!metadata.is_dir);
1220 assert_eq!(metadata.size, content.len() as u64);
1221 }
1222 _ => panic!("Expected FileContent response"),
1223 }
1224 }
1225
1226 #[tokio::test]
1227 async fn test_file_handler_get_nonexistent() {
1228 let handler = FileHandler;
1229 let request = Request::FileGet {
1230 id: Uuid::new_v4(),
1231 path: PathBuf::from("/nonexistent/file.txt"),
1232 range: None,
1233 };
1234
1235 let response = handler.handle(request).await.unwrap();
1236 match response {
1237 Response::Error { error, .. } => {
1238 println!("Error message: {}", error.message);
1240 assert!(matches!(error.code, ErrorCode::FileNotFound | ErrorCode::InternalError));
1242 }
1243 _ => panic!("Expected Error response"),
1244 }
1245 }
1246
1247 #[tokio::test]
1248 async fn test_file_handler_dir_list() {
1249 let handler = FileHandler;
1250 let temp_dir = TempDir::new().unwrap();
1251
1252 let file1 = temp_dir.path().join("file1.txt");
1254 let file2 = temp_dir.path().join("file2.txt");
1255 let hidden_file = temp_dir.path().join(".hidden");
1256
1257 fs::write(&file1, "content1").await.unwrap();
1258 fs::write(&file2, "content2").await.unwrap();
1259 fs::write(&hidden_file, "hidden").await.unwrap();
1260
1261 let request = Request::DirList {
1263 id: Uuid::new_v4(),
1264 path: temp_dir.path().to_path_buf(),
1265 include_hidden: false,
1266 recursive: false,
1267 };
1268
1269 let response = handler.handle(request).await.unwrap();
1270 match response {
1271 Response::DirListing { entries, .. } => {
1272 assert_eq!(entries.len(), 2); let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
1274 assert!(names.contains(&&"file1.txt".to_string()));
1275 assert!(names.contains(&&"file2.txt".to_string()));
1276 assert!(!names.contains(&&".hidden".to_string()));
1277 }
1278 _ => panic!("Expected DirListing response"),
1279 }
1280
1281 let request_with_hidden = Request::DirList {
1283 id: Uuid::new_v4(),
1284 path: temp_dir.path().to_path_buf(),
1285 include_hidden: true,
1286 recursive: false,
1287 };
1288
1289 let response_with_hidden = handler.handle(request_with_hidden).await.unwrap();
1290 match response_with_hidden {
1291 Response::DirListing { entries, .. } => {
1292 assert_eq!(entries.len(), 3); let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
1294 assert!(names.contains(&&".hidden".to_string()));
1295 }
1296 _ => panic!("Expected DirListing response"),
1297 }
1298 }
1299
1300 #[tokio::test]
1301 async fn test_file_handler_recursive_dir_list() {
1302 let handler = FileHandler;
1303 let temp_dir = TempDir::new().unwrap();
1304
1305 let subdir = temp_dir.path().join("subdir");
1307 fs::create_dir(&subdir).await.unwrap();
1308
1309 let file1 = temp_dir.path().join("file1.txt");
1310 let file2 = subdir.join("file2.txt");
1311 let file3 = subdir.join("file3.txt");
1312
1313 fs::write(&file1, "content1").await.unwrap();
1314 fs::write(&file2, "content2").await.unwrap();
1315 fs::write(&file3, "content3").await.unwrap();
1316
1317 let request = Request::DirList {
1319 id: Uuid::new_v4(),
1320 path: temp_dir.path().to_path_buf(),
1321 include_hidden: false,
1322 recursive: true,
1323 };
1324
1325 let response = handler.handle(request).await.unwrap();
1326 match response {
1327 Response::DirListing { entries, .. } => {
1328 assert!(entries.len() >= 4); let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
1331 assert!(names.contains(&&"file1.txt".to_string()));
1332 assert!(names.contains(&&"subdir".to_string()));
1333 assert!(names.contains(&&"file2.txt".to_string()));
1334 assert!(names.contains(&&"file3.txt".to_string()));
1335 }
1336 _ => panic!("Expected DirListing response"),
1337 }
1338 }
1339
1340 #[tokio::test]
1341 async fn test_file_handler_range_get() {
1342 let handler = FileHandler;
1343 let temp_dir = TempDir::new().unwrap();
1344 let file_path = temp_dir.path().join("test.txt");
1345 let content = "Hello, world! This is a test file with some content.";
1346
1347 fs::write(&file_path, content).await.unwrap();
1349
1350 let request = Request::FileGet {
1352 id: Uuid::new_v4(),
1353 path: file_path,
1354 range: Some((7, 12)),
1355 };
1356
1357 let response = handler.handle(request).await.unwrap();
1358 match response {
1359 Response::FileContent { content: retrieved_content, .. } => {
1360 let content_str = String::from_utf8(retrieved_content.to_vec()).unwrap();
1361 assert_eq!(content_str, "world");
1362 }
1363 _ => panic!("Expected FileContent response"),
1364 }
1365 }
1366
1367 #[tokio::test]
1368 async fn test_file_handler_create_dirs() {
1369 let handler = FileHandler;
1370 let temp_dir = TempDir::new().unwrap();
1371 let nested_path = temp_dir.path().join("nested").join("dirs").join("test.txt");
1372 let content = Bytes::from("test content");
1373
1374 let request = Request::FilePut {
1376 id: Uuid::new_v4(),
1377 path: nested_path.clone(),
1378 content: content.clone(),
1379 mode: Some(0o644),
1380 create_dirs: true,
1381 };
1382
1383 let response = handler.handle(request).await.unwrap();
1384 match response {
1385 Response::FilePutResult { bytes_written, .. } => {
1386 assert_eq!(bytes_written, content.len() as u64);
1387 }
1388 _ => panic!("Expected FilePutResult response"),
1389 }
1390
1391 assert!(nested_path.exists());
1393 let read_content = fs::read(&nested_path).await.unwrap();
1394 assert_eq!(read_content, content.to_vec());
1395 }
1396
1397 #[tokio::test]
1398 async fn test_file_handler_large_file() {
1399 let handler = FileHandler;
1400 let temp_dir = TempDir::new().unwrap();
1401 let file_path = temp_dir.path().join("large.txt");
1402
1403 let large_content = vec![b'A'; 1024 * 1024];
1405 let content = Bytes::from(large_content.clone());
1406
1407 let put_request = Request::FilePut {
1409 id: Uuid::new_v4(),
1410 path: file_path.clone(),
1411 content: content.clone(),
1412 mode: Some(0o644),
1413 create_dirs: false,
1414 };
1415
1416 let put_response = handler.handle(put_request).await.unwrap();
1417 match put_response {
1418 Response::FilePutResult { bytes_written, .. } => {
1419 assert_eq!(bytes_written, content.len() as u64);
1420 }
1421 _ => panic!("Expected FilePutResult response"),
1422 }
1423
1424 let get_request = Request::FileGet {
1426 id: Uuid::new_v4(),
1427 path: file_path,
1428 range: None,
1429 };
1430
1431 let get_response = handler.handle(get_request).await.unwrap();
1432 match get_response {
1433 Response::FileContent { content: retrieved_content, metadata, .. } => {
1434 assert_eq!(retrieved_content.len(), large_content.len());
1435 assert_eq!(metadata.size, large_content.len() as u64);
1436 assert_eq!(retrieved_content.to_vec(), large_content);
1437 }
1438 _ => panic!("Expected FileContent response"),
1439 }
1440 }
1441
1442 #[tokio::test]
1443 async fn test_file_handler_permissions() {
1444 let handler = FileHandler;
1445 let temp_dir = TempDir::new().unwrap();
1446 let file_path = temp_dir.path().join("test_perms.txt");
1447 let content = Bytes::from("test content");
1448
1449 let request = Request::FilePut {
1451 id: Uuid::new_v4(),
1452 path: file_path.clone(),
1453 content: content.clone(),
1454 mode: Some(0o755),
1455 create_dirs: false,
1456 };
1457
1458 let response = handler.handle(request).await.unwrap();
1459 match response {
1460 Response::FilePutResult { bytes_written, .. } => {
1461 assert_eq!(bytes_written, content.len() as u64);
1462 }
1463 _ => panic!("Expected FilePutResult response"),
1464 }
1465
1466 assert!(file_path.exists());
1468
1469 #[cfg(unix)]
1472 {
1473 use std::os::unix::fs::PermissionsExt;
1474 let metadata = std::fs::metadata(&file_path).unwrap();
1475 let mode = metadata.permissions().mode() & 0o777;
1476 assert_eq!(mode, 0o755);
1477 }
1478 }
1479
1480 #[tokio::test]
1481 async fn test_file_handler_directory_as_file_error() {
1482 let handler = FileHandler;
1483 let temp_dir = TempDir::new().unwrap();
1484
1485 let request = Request::FileGet {
1487 id: Uuid::new_v4(),
1488 path: temp_dir.path().to_path_buf(),
1489 range: None,
1490 };
1491
1492 let response = handler.handle(request).await.unwrap();
1493 match response {
1494 Response::Error { error, .. } => {
1495 assert_eq!(error.code, ErrorCode::InternalError);
1496 assert!(error.message.contains("directory"));
1497 }
1498 _ => panic!("Expected Error response"),
1499 }
1500 }
1501
1502 #[tokio::test]
1503 async fn test_file_handler_put_without_create_dirs() {
1504 let handler = FileHandler;
1505 let temp_dir = TempDir::new().unwrap();
1506 let nested_path = temp_dir.path().join("nonexistent").join("test.txt");
1507 let content = Bytes::from("test content");
1508
1509 let request = Request::FilePut {
1511 id: Uuid::new_v4(),
1512 path: nested_path,
1513 content,
1514 mode: Some(0o644),
1515 create_dirs: false,
1516 };
1517
1518 let response = handler.handle(request).await.unwrap();
1519 match response {
1520 Response::Error { error, .. } => {
1521 assert!(matches!(error.code, ErrorCode::InternalError | ErrorCode::FileNotFound));
1523 }
1524 _ => panic!("Expected Error response"),
1525 }
1526 }
1527
1528 #[tokio::test]
1529 async fn test_ping_handler() {
1530 let handler = PingHandler;
1531 let request_id = Uuid::new_v4();
1532 let timestamp = 12345;
1533
1534 let request = Request::Ping {
1535 id: request_id,
1536 timestamp,
1537 };
1538
1539 let response = handler.handle(request).await.unwrap();
1540 match response {
1541 Response::Pong { request_id: resp_id, timestamp: resp_timestamp, .. } => {
1542 assert_eq!(resp_id, request_id);
1543 assert_eq!(resp_timestamp, timestamp);
1544 }
1545 _ => panic!("Expected Pong response"),
1546 }
1547 }
1548
1549 #[tokio::test]
1550 async fn test_pty_handler_basic_command() {
1551 let handler = PtyHandler;
1552
1553 let command = if cfg!(windows) {
1555 vec!["cmd".to_string(), "/c".to_string(), "echo".to_string(), "hello pty".to_string()]
1556 } else {
1557 vec!["echo".to_string(), "hello pty".to_string()]
1558 };
1559
1560 let request = Request::PtyExec {
1561 id: Uuid::new_v4(),
1562 command,
1563 env: HashMap::new(),
1564 cwd: None,
1565 privilege: None,
1566 timeout: Some(10),
1567 };
1568
1569 let response = handler.handle(request).await.unwrap();
1570
1571 match response {
1572 Response::PtyResult { exit_code, output, .. } => {
1573 assert_eq!(exit_code, 0);
1574 let output_str = String::from_utf8(output.to_vec()).unwrap();
1575 assert!(output_str.contains("hello pty"));
1576 }
1577 _ => panic!("Expected PtyResult response"),
1578 }
1579 }
1580
1581 #[tokio::test]
1582 async fn test_pty_handler_sudo_command() {
1583 let handler = PtyHandler;
1584
1585 use mitoxide_proto::message::{PrivilegeEscalation, PrivilegeMethod, Credentials};
1586
1587 let privilege = PrivilegeEscalation {
1588 method: PrivilegeMethod::Sudo,
1589 credentials: Some(Credentials {
1590 username: Some("root".to_string()),
1591 password: Some("password".to_string()),
1592 }),
1593 prompt_patterns: vec!["[sudo] password".to_string()],
1594 };
1595
1596 let command = vec!["whoami".to_string()];
1598
1599 let request = Request::PtyExec {
1600 id: Uuid::new_v4(),
1601 command,
1602 env: HashMap::new(),
1603 cwd: None,
1604 privilege: Some(privilege),
1605 timeout: Some(10),
1606 };
1607
1608 let response = handler.handle(request).await.unwrap();
1609
1610 match response {
1613 Response::PtyResult { .. } => {
1614 }
1616 Response::Error { error, .. } => {
1617 assert!(matches!(error.code, ErrorCode::ProcessFailed | ErrorCode::PrivilegeEscalationFailed));
1619 }
1620 _ => panic!("Expected PtyResult or Error response"),
1621 }
1622 }
1623
1624 #[tokio::test]
1625 async fn test_pty_handler_prompt_detection() {
1626 let handler = PtyHandler;
1627
1628 assert!(handler.detect_privilege_prompt("Password:", &[]));
1630 assert!(handler.detect_privilege_prompt("[sudo] password for user:", &[]));
1631 assert!(handler.detect_privilege_prompt("su: password", &[]));
1632 assert!(!handler.detect_privilege_prompt("normal output", &[]));
1633
1634 let custom_patterns = vec!["Enter passphrase:".to_string()];
1636 assert!(handler.detect_privilege_prompt("Enter passphrase: ", &custom_patterns));
1637 assert!(!handler.detect_privilege_prompt("Password:", &custom_patterns));
1638 }
1639
1640 #[tokio::test]
1641 async fn test_pty_handler_build_privileged_command() {
1642 let handler = PtyHandler;
1643
1644 use mitoxide_proto::message::{PrivilegeEscalation, PrivilegeMethod, Credentials};
1645
1646 let command = vec!["ls".to_string(), "-la".to_string()];
1647
1648 let sudo_privilege = PrivilegeEscalation {
1650 method: PrivilegeMethod::Sudo,
1651 credentials: Some(Credentials {
1652 username: Some("root".to_string()),
1653 password: None,
1654 }),
1655 prompt_patterns: vec![],
1656 };
1657
1658 let sudo_command = handler.build_privileged_command(&command, &sudo_privilege).unwrap();
1659 assert_eq!(sudo_command[0], "sudo");
1660 assert_eq!(sudo_command[1], "-S");
1661 assert_eq!(sudo_command[2], "-u");
1662 assert_eq!(sudo_command[3], "root");
1663 assert_eq!(sudo_command[4], "ls");
1664 assert_eq!(sudo_command[5], "-la");
1665
1666 let su_privilege = PrivilegeEscalation {
1668 method: PrivilegeMethod::Su,
1669 credentials: Some(Credentials {
1670 username: Some("root".to_string()),
1671 password: None,
1672 }),
1673 prompt_patterns: vec![],
1674 };
1675
1676 let su_command = handler.build_privileged_command(&command, &su_privilege).unwrap();
1677 assert_eq!(su_command[0], "su");
1678 assert_eq!(su_command[1], "root");
1679 assert_eq!(su_command[2], "-c");
1680 assert_eq!(su_command[3], "ls -la");
1681
1682 let doas_privilege = PrivilegeEscalation {
1684 method: PrivilegeMethod::Doas,
1685 credentials: Some(Credentials {
1686 username: Some("root".to_string()),
1687 password: None,
1688 }),
1689 prompt_patterns: vec![],
1690 };
1691
1692 let doas_command = handler.build_privileged_command(&command, &doas_privilege).unwrap();
1693 assert_eq!(doas_command[0], "doas");
1694 assert_eq!(doas_command[1], "-u");
1695 assert_eq!(doas_command[2], "root");
1696 assert_eq!(doas_command[3], "ls");
1697 assert_eq!(doas_command[4], "-la");
1698 }
1699
1700 #[tokio::test]
1701 async fn test_pty_handler_empty_command() {
1702 let handler = PtyHandler;
1703
1704 let request = Request::PtyExec {
1705 id: Uuid::new_v4(),
1706 command: vec![],
1707 env: HashMap::new(),
1708 cwd: None,
1709 privilege: None,
1710 timeout: None,
1711 };
1712
1713 let response = handler.handle(request).await.unwrap();
1714
1715 match response {
1716 Response::Error { error, .. } => {
1717 assert_eq!(error.code, ErrorCode::InvalidRequest);
1718 }
1719 _ => panic!("Expected Error response"),
1720 }
1721 }
1722
1723 #[tokio::test]
1724 async fn test_handler_wrong_request_type() {
1725 let ping_handler = PingHandler;
1726
1727 let command = if cfg!(windows) {
1729 vec!["cmd".to_string(), "/c".to_string(), "echo".to_string()]
1730 } else {
1731 vec!["echo".to_string()]
1732 };
1733
1734 let process_request = Request::ProcessExec {
1735 id: Uuid::new_v4(),
1736 command,
1737 env: HashMap::new(),
1738 cwd: None,
1739 stdin: None,
1740 timeout: None,
1741 };
1742
1743 let response = ping_handler.handle(process_request).await.unwrap();
1744 match response {
1745 Response::Error { error, .. } => {
1746 assert_eq!(error.code, ErrorCode::Unsupported);
1747 }
1748 _ => panic!("Expected Error response"),
1749 }
1750 }
1751}