mitoxide_proto/
message.rs

1//! Message types and enums
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use bytes::Bytes;
7use uuid::Uuid;
8
9/// Top-level message wrapper
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub enum Message {
12    /// Request message
13    Request(Request),
14    /// Response message
15    Response(Response),
16}
17
18impl Message {
19    /// Create a request message
20    pub fn request(req: Request) -> Self {
21        Self::Request(req)
22    }
23    
24    /// Create a response message
25    pub fn response(resp: Response) -> Self {
26        Self::Response(resp)
27    }
28    
29    /// Get the request ID if this is a request
30    pub fn request_id(&self) -> Option<Uuid> {
31        match self {
32            Self::Request(req) => Some(req.id()),
33            Self::Response(resp) => Some(resp.request_id()),
34        }
35    }
36}
37
38/// Request message types
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub enum Request {
41    /// Process execution request
42    ProcessExec {
43        /// Request ID for correlation
44        id: Uuid,
45        /// Command to execute
46        command: Vec<String>,
47        /// Environment variables
48        env: HashMap<String, String>,
49        /// Working directory
50        cwd: Option<PathBuf>,
51        /// Standard input data
52        stdin: Option<Bytes>,
53        /// Timeout in seconds
54        timeout: Option<u64>,
55    },
56    
57    /// File get operation
58    FileGet {
59        /// Request ID for correlation
60        id: Uuid,
61        /// Path to file
62        path: PathBuf,
63        /// Optional byte range (start, end)
64        range: Option<(u64, u64)>,
65    },
66    
67    /// File put operation
68    FilePut {
69        /// Request ID for correlation
70        id: Uuid,
71        /// Path to file
72        path: PathBuf,
73        /// File content
74        content: Bytes,
75        /// File mode (permissions)
76        mode: Option<u32>,
77        /// Create parent directories
78        create_dirs: bool,
79    },
80    
81    /// Directory listing
82    DirList {
83        /// Request ID for correlation
84        id: Uuid,
85        /// Directory path
86        path: PathBuf,
87        /// Include hidden files
88        include_hidden: bool,
89        /// Recursive listing
90        recursive: bool,
91    },
92    
93    /// WASM module execution
94    WasmExec {
95        /// Request ID for correlation
96        id: Uuid,
97        /// WASM module bytecode
98        module: Bytes,
99        /// JSON input data
100        input: Bytes,
101        /// Execution timeout in seconds
102        timeout: Option<u64>,
103    },
104    
105    /// JSON RPC call
106    JsonCall {
107        /// Request ID for correlation
108        id: Uuid,
109        /// Method name
110        method: String,
111        /// JSON parameters
112        params: Bytes,
113    },
114    
115    /// Ping request for health checking
116    Ping {
117        /// Request ID for correlation
118        id: Uuid,
119        /// Timestamp
120        timestamp: u64,
121    },
122    
123    /// PTY process execution with privilege escalation
124    PtyExec {
125        /// Request ID for correlation
126        id: Uuid,
127        /// Command to execute
128        command: Vec<String>,
129        /// Environment variables
130        env: HashMap<String, String>,
131        /// Working directory
132        cwd: Option<PathBuf>,
133        /// Privilege escalation method
134        privilege: Option<PrivilegeEscalation>,
135        /// Execution timeout in seconds
136        timeout: Option<u64>,
137    },
138}
139
140impl Request {
141    /// Get the request ID
142    pub fn id(&self) -> Uuid {
143        match self {
144            Self::ProcessExec { id, .. } => *id,
145            Self::FileGet { id, .. } => *id,
146            Self::FilePut { id, .. } => *id,
147            Self::DirList { id, .. } => *id,
148            Self::WasmExec { id, .. } => *id,
149            Self::JsonCall { id, .. } => *id,
150            Self::Ping { id, .. } => *id,
151            Self::PtyExec { id, .. } => *id,
152        }
153    }
154    
155    /// Create a process execution request
156    pub fn process_exec(
157        command: Vec<String>,
158        env: HashMap<String, String>,
159        cwd: Option<PathBuf>,
160        stdin: Option<Bytes>,
161        timeout: Option<u64>,
162    ) -> Self {
163        Self::ProcessExec {
164            id: Uuid::new_v4(),
165            command,
166            env,
167            cwd,
168            stdin,
169            timeout,
170        }
171    }
172    
173    /// Create a file get request
174    pub fn file_get(path: PathBuf, range: Option<(u64, u64)>) -> Self {
175        Self::FileGet {
176            id: Uuid::new_v4(),
177            path,
178            range,
179        }
180    }
181    
182    /// Create a file put request
183    pub fn file_put(path: PathBuf, content: Bytes, mode: Option<u32>, create_dirs: bool) -> Self {
184        Self::FilePut {
185            id: Uuid::new_v4(),
186            path,
187            content,
188            mode,
189            create_dirs,
190        }
191    }
192    
193    /// Create a ping request
194    pub fn ping() -> Self {
195        Self::Ping {
196            id: Uuid::new_v4(),
197            timestamp: std::time::SystemTime::now()
198                .duration_since(std::time::UNIX_EPOCH)
199                .unwrap_or_default()
200                .as_secs(),
201        }
202    }
203}
204
205/// Response message types
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub enum Response {
208    /// Process execution result
209    ProcessResult {
210        /// Request ID this responds to
211        request_id: Uuid,
212        /// Exit code
213        exit_code: i32,
214        /// Standard output
215        stdout: Bytes,
216        /// Standard error
217        stderr: Bytes,
218        /// Execution duration in milliseconds
219        duration_ms: u64,
220    },
221    
222    /// File get result
223    FileContent {
224        /// Request ID this responds to
225        request_id: Uuid,
226        /// File content
227        content: Bytes,
228        /// File metadata
229        metadata: FileMetadata,
230    },
231    
232    /// File put result
233    FilePutResult {
234        /// Request ID this responds to
235        request_id: Uuid,
236        /// Bytes written
237        bytes_written: u64,
238    },
239    
240    /// Directory listing result
241    DirListing {
242        /// Request ID this responds to
243        request_id: Uuid,
244        /// Directory entries
245        entries: Vec<DirEntry>,
246    },
247    
248    /// WASM execution result
249    WasmResult {
250        /// Request ID this responds to
251        request_id: Uuid,
252        /// JSON output data
253        output: Bytes,
254        /// Execution duration in milliseconds
255        duration_ms: u64,
256    },
257    
258    /// JSON RPC result
259    JsonResult {
260        /// Request ID this responds to
261        request_id: Uuid,
262        /// JSON result
263        result: Bytes,
264    },
265    
266    /// Pong response
267    Pong {
268        /// Request ID this responds to
269        request_id: Uuid,
270        /// Original timestamp
271        timestamp: u64,
272        /// Response timestamp
273        response_timestamp: u64,
274    },
275    
276    /// PTY process execution result
277    PtyResult {
278        /// Request ID this responds to
279        request_id: Uuid,
280        /// Exit code
281        exit_code: i32,
282        /// Combined stdout/stderr output
283        output: Bytes,
284        /// Execution duration in milliseconds
285        duration_ms: u64,
286    },
287    
288    /// Error response
289    Error {
290        /// Request ID this responds to
291        request_id: Uuid,
292        /// Error details
293        error: ErrorDetails,
294    },
295}
296
297impl Response {
298    /// Get the request ID this response corresponds to
299    pub fn request_id(&self) -> Uuid {
300        match self {
301            Self::ProcessResult { request_id, .. } => *request_id,
302            Self::FileContent { request_id, .. } => *request_id,
303            Self::FilePutResult { request_id, .. } => *request_id,
304            Self::DirListing { request_id, .. } => *request_id,
305            Self::WasmResult { request_id, .. } => *request_id,
306            Self::JsonResult { request_id, .. } => *request_id,
307            Self::Pong { request_id, .. } => *request_id,
308            Self::PtyResult { request_id, .. } => *request_id,
309            Self::Error { request_id, .. } => *request_id,
310        }
311    }
312    
313    /// Create an error response
314    pub fn error(request_id: Uuid, error: ErrorDetails) -> Self {
315        Self::Error { request_id, error }
316    }
317    
318    /// Create a pong response
319    pub fn pong(request_id: Uuid, timestamp: u64) -> Self {
320        Self::Pong {
321            request_id,
322            timestamp,
323            response_timestamp: std::time::SystemTime::now()
324                .duration_since(std::time::UNIX_EPOCH)
325                .unwrap_or_default()
326                .as_secs(),
327        }
328    }
329}
330
331/// File metadata information
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct FileMetadata {
334    /// File size in bytes
335    pub size: u64,
336    /// File permissions mode
337    pub mode: u32,
338    /// Last modified timestamp (Unix epoch)
339    pub modified: u64,
340    /// Whether this is a directory
341    pub is_dir: bool,
342    /// Whether this is a symlink
343    pub is_symlink: bool,
344}
345
346/// Directory entry information
347#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct DirEntry {
349    /// Entry name
350    pub name: String,
351    /// Full path
352    pub path: PathBuf,
353    /// File metadata
354    pub metadata: FileMetadata,
355}
356
357/// Error details for error responses
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct ErrorDetails {
360    /// Error code
361    pub code: ErrorCode,
362    /// Human-readable error message
363    pub message: String,
364    /// Additional context data
365    pub context: HashMap<String, String>,
366}
367
368/// Privilege escalation configuration
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct PrivilegeEscalation {
371    /// Escalation method
372    pub method: PrivilegeMethod,
373    /// Credentials for escalation
374    pub credentials: Option<Credentials>,
375    /// Custom prompt patterns to detect
376    pub prompt_patterns: Vec<String>,
377}
378
379/// Privilege escalation methods
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub enum PrivilegeMethod {
382    /// Use sudo
383    Sudo,
384    /// Use su
385    Su,
386    /// Use doas
387    Doas,
388    /// Custom command
389    Custom(String),
390}
391
392/// Credentials for privilege escalation
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct Credentials {
395    /// Username (for su)
396    pub username: Option<String>,
397    /// Password
398    pub password: Option<String>,
399}
400
401/// Error codes for different types of errors
402#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
403pub enum ErrorCode {
404    /// Invalid request format
405    InvalidRequest,
406    /// File not found
407    FileNotFound,
408    /// Permission denied
409    PermissionDenied,
410    /// Process execution failed
411    ProcessFailed,
412    /// WASM execution failed
413    WasmFailed,
414    /// Timeout occurred
415    Timeout,
416    /// Internal server error
417    InternalError,
418    /// Unsupported operation
419    Unsupported,
420    /// Resource exhausted
421    ResourceExhausted,
422    /// Privilege escalation failed
423    PrivilegeEscalationFailed,
424}
425
426impl ErrorDetails {
427    /// Create a new error details
428    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
429        Self {
430            code,
431            message: message.into(),
432            context: HashMap::new(),
433        }
434    }
435    
436    /// Add context to the error
437    pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
438        self.context.insert(key.into(), value.into());
439        self
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    use proptest::prelude::*;
447    
448    #[test]
449    fn test_request_creation() {
450        let req = Request::process_exec(
451            vec!["echo".to_string(), "hello".to_string()],
452            HashMap::new(),
453            None,
454            None,
455            Some(30),
456        );
457        
458        match req {
459            Request::ProcessExec { command, timeout, .. } => {
460                assert_eq!(command, vec!["echo", "hello"]);
461                assert_eq!(timeout, Some(30));
462            }
463            _ => panic!("Expected ProcessExec request"),
464        }
465    }
466    
467    #[test]
468    fn test_response_creation() {
469        let request_id = Uuid::new_v4();
470        let resp = Response::error(
471            request_id,
472            ErrorDetails::new(ErrorCode::FileNotFound, "File not found"),
473        );
474        
475        match resp {
476            Response::Error { request_id: resp_id, error } => {
477                assert_eq!(resp_id, request_id);
478                assert_eq!(error.code, ErrorCode::FileNotFound);
479                assert_eq!(error.message, "File not found");
480            }
481            _ => panic!("Expected Error response"),
482        }
483    }
484    
485    #[test]
486    fn test_message_request_id() {
487        let req = Request::ping();
488        let req_id = req.id();
489        let msg = Message::request(req);
490        
491        assert_eq!(msg.request_id(), Some(req_id));
492    }
493    
494    #[test]
495    fn test_error_details_with_context() {
496        let error = ErrorDetails::new(ErrorCode::ProcessFailed, "Command failed")
497            .with_context("command", "ls")
498            .with_context("exit_code", "1");
499        
500        assert_eq!(error.code, ErrorCode::ProcessFailed);
501        assert_eq!(error.message, "Command failed");
502        assert_eq!(error.context.get("command"), Some(&"ls".to_string()));
503        assert_eq!(error.context.get("exit_code"), Some(&"1".to_string()));
504    }
505    
506    #[test]
507    fn test_message_serialization() {
508        let req = Request::ping();
509        let msg = Message::request(req);
510        
511        let serialized = rmp_serde::to_vec(&msg).unwrap();
512        let deserialized: Message = rmp_serde::from_slice(&serialized).unwrap();
513        
514        assert_eq!(msg.request_id(), deserialized.request_id());
515    }
516    
517    proptest! {
518        #[test]
519        fn test_request_id_consistency(
520            command in prop::collection::vec("[a-zA-Z0-9]+", 1..5),
521            timeout in prop::option::of(1u64..3600)
522        ) {
523            let req = Request::process_exec(
524                command,
525                HashMap::new(),
526                None,
527                None,
528                timeout,
529            );
530            
531            let id1 = req.id();
532            let id2 = req.id();
533            prop_assert_eq!(id1, id2);
534        }
535    }
536}