regent_sdk/hosts/handlers/mod.rs
1//! Host connection handlers
2//!
3//! This module provides the connection handling infrastructure for Regent SDK.
4//! It includes handlers for different connection methods (SSH, localhost) and
5//! the traits that define the interface for host operations.
6
7pub mod localhost;
8pub mod ssh2;
9
10use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13use crate::error::RegentError;
14use crate::hosts::handlers::localhost::WhichUser;
15use crate::hosts::handlers::ssh2::Ssh2Auth;
16use crate::secrets::SecretProvider;
17use crate::secrets::SecretReference;
18use crate::{LocalHostHandler, Ssh2HostHandler};
19use crate::{command::CommandResult, hosts::privilege::Privilege};
20
21// Intermediary representation of a WhichUser
22// WhichUser holds secrets, TargetUser holds references to secrets
23
24/// Defines the type of user for a connection.
25///
26/// - `CurrentUser`: Use the currently authenticated user
27/// - `User`: Use a specific user identified by a secret reference
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub enum TargetUserKind {
30 /// Use the currently authenticated user for the connection.
31 CurrentUser,
32 /// Use a specific user, identified by a secret reference.
33 User(SecretReference),
34}
35
36/// Specifies the target user for a host connection.
37///
38/// This struct is used during the connection setup phase and holds references
39/// to secrets rather than the secrets themselves. The actual secrets are
40/// retrieved from the secret provider when the connection is established.
41///
42/// # Example
43///
44/// ```no_run
45/// use regent_sdk::hosts::handlers::{ConnectionMethod, TargetUser};
46///
47/// // Connect as the current user
48/// let target = TargetUser::current_user();
49///
50/// // Connect as a specific user (secret will be retrieved from provider)
51/// let target = TargetUser::user("admin_credentials", Some("files".to_string()));
52/// ```
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55#[serde(rename_all = "PascalCase")]
56pub struct TargetUser {
57 /// The kind of user to connect as.
58 pub user_kind: TargetUserKind,
59}
60
61impl TargetUser {
62 /// Create a target user for the current user.
63 ///
64 /// # Example
65 ///
66 /// ```no_run
67 /// use regent_sdk::hosts::handlers::TargetUser;
68 ///
69 /// let target = TargetUser::current_user();
70 /// ```
71 pub fn current_user() -> Self {
72 Self {
73 user_kind: TargetUserKind::CurrentUser,
74 }
75 }
76
77 /// Create a target user for a specific user.
78 ///
79 /// # Arguments
80 ///
81 /// * `sec_ref` - Reference to the secret containing credentials
82 /// * `provider` - Optional name of the secret provider to use
83 ///
84 /// # Example
85 ///
86 /// ```no_run
87 /// use regent_sdk::hosts::handlers::TargetUser;
88 ///
89 /// let target = TargetUser::user("admin_password", Some("files".to_string()));
90 /// ```
91 pub fn user(sec_ref: &str, provider: Option<String>) -> Self {
92 Self {
93 user_kind: TargetUserKind::User(SecretReference::from(sec_ref, provider)),
94 }
95 }
96}
97
98/// Defines the method used to connect to a host.
99///
100/// # Variants
101///
102/// - `Localhost`: Connect to the local machine
103/// - `Ssh2`: Connect via SSH protocol
104///
105/// # Example
106///
107/// ```no_run
108/// use regent_sdk::hosts::handlers::{ConnectionMethod, TargetUser, Ssh2Auth};
109///
110/// // Localhost connection
111/// let method = ConnectionMethod::Localhost(TargetUser::current_user());
112///
113/// // SSH connection
114/// let method = ConnectionMethod::Ssh2(Ssh2Auth::username_password("creds", Some("files")));
115/// ```
116#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(rename_all = "PascalCase")]
118pub enum ConnectionMethod {
119 /// Connect to the local machine.
120 Localhost(TargetUser),
121 /// Connect via SSH protocol.
122 Ssh2(Ssh2Auth),
123}
124
125/// Trait defining the interface for host connection handlers.
126///
127/// All host handlers must implement this trait to provide the basic operations
128/// needed for managing connections and executing commands on remote hosts.
129///
130/// # Methods
131///
132/// - `connect`: Establish a connection to the host
133/// - `is_connected`: Check if currently connected
134/// - `disconnect`: Close the connection
135/// - `is_this_command_available`: Check if a command exists on the host
136/// - `run_command`: Execute a command on the host
137/// - `run_windows_command`: Execute a Windows command (available when the `windows` feature is enabled)
138/// - `get_file`: Retrieve a file from the host
139///
140/// # Example
141///
142/// Implementers of this trait can be used interchangeably through the [`Handler`] enum.
143pub trait HostHandler: Sized {
144 /// Establish a connection to the specified endpoint.
145 ///
146 /// # Arguments
147 ///
148 /// * `endpoint` - The host address to connect to (e.g., "192.168.1.100:22")
149 ///
150 /// # Returns
151 ///
152 /// `Ok(())` if connection was successful, or a [`RegentError`] if it failed.
153 async fn connect(
154 &mut self,
155 endpoint: &str,
156 // secret_provider: &Option<SecretProvider>,
157 ) -> Result<(), RegentError>;
158
159 /// Check if the handler is currently connected to a host.
160 ///
161 /// # Returns
162 ///
163 /// `true` if connected, `false` otherwise.
164 async fn is_connected(&mut self) -> bool;
165
166 /// Disconnect from the host.
167 ///
168 /// # Returns
169 ///
170 /// `Ok(())` if disconnection was successful, or a [`RegentError`] if it failed.
171 async fn disconnect(&mut self) -> Result<(), RegentError>;
172
173 /// Check if a specific command is available on the host.
174 ///
175 /// # Arguments
176 ///
177 /// * `command` - The command name to check
178 /// * `privilege` - The privilege level to check with
179 ///
180 /// # Returns
181 ///
182 /// `Ok(true)` if the command exists, `Ok(false)` if it doesn't,
183 /// or a [`RegentError`] if the check failed.
184 async fn is_this_command_available(
185 &mut self,
186 command: &str,
187 privilege: &Privilege,
188 ) -> Result<bool, RegentError>;
189
190 /// Execute a command on the host.
191 ///
192 /// # Arguments
193 ///
194 /// * `command` - The command to execute
195 /// * `privilege` - The privilege level to use
196 ///
197 /// # Returns
198 ///
199 /// A [`CommandResult`] containing the exit code, stdout, and stderr,
200 /// or a [`RegentError`] if execution failed.
201 async fn run_command(
202 &mut self,
203 command: &str,
204 privilege: &Privilege,
205 ) -> Result<CommandResult, RegentError>;
206
207 /// Execute a Windows command on the host.
208 ///
209 /// This method is specifically for Windows command execution.
210 ///
211 /// # Arguments
212 ///
213 /// * `command` - The Windows command to execute
214 ///
215 /// # Returns
216 ///
217 /// A [`CommandResult`] or a [`RegentError`] if execution failed.
218 #[cfg(feature = "windows")]
219 async fn run_windows_command(&mut self, command: &str) -> Result<CommandResult, RegentError>;
220
221 /// Retrieve the contents of a file from the host.
222 ///
223 /// # Arguments
224 ///
225 /// * `path` - The path to the file to retrieve
226 ///
227 /// # Returns
228 ///
229 /// The file contents as a byte vector, or a [`RegentError`] if retrieval failed.
230 async fn get_file(&mut self, path: PathBuf) -> Result<Vec<u8>, RegentError>;
231}
232
233/// Enum that can hold any type of host handler.
234///
235/// This enum provides a unified interface for working with different types
236/// of host handlers (local or SSH) through the [`HostHandler`] trait.
237///
238/// # Variants
239///
240/// - `LocalHost`: Handler for local machine connections
241/// - `Ssh2`: Handler for SSH connections
242///
243/// # Example
244///
245/// ```no_run
246/// use regent_sdk::{Handler, LocalHostHandler, Ssh2HostHandler, hosts::handlers::localhost::WhichUser};
247///
248/// // Create a local host handler
249/// let local = Handler::localhost(LocalHostHandler::from(WhichUser::CurrentUser));
250///
251/// // Create an SSH handler (requires connection details)
252/// // let ssh = Handler::ss2(Ssh2HostHandler::from(...).unwrap());
253/// ```
254// #[derive(Clone, Debug)]
255pub enum Handler {
256 /// Handler for local machine connections.
257 LocalHost(LocalHostHandler),
258 /// Handler for SSH connections.
259 Ssh2(Ssh2HostHandler),
260}
261
262impl Clone for Handler {
263 fn clone(&self) -> Self {
264 match self {
265 Handler::LocalHost(h) => Handler::LocalHost(h.clone()),
266 Handler::Ssh2(h) => Handler::Ssh2(h.clone()),
267 }
268 }
269}
270
271impl Handler {
272 /// Create a [`Handler`] from a [`LocalHostHandler`].
273 ///
274 /// # Arguments
275 ///
276 /// * `localhost_handler` - The local host handler to wrap
277 ///
278 /// # Example
279 ///
280 /// ```no_run
281 /// use regent_sdk::{Handler, LocalHostHandler, hosts::handlers::localhost::WhichUser};
282 ///
283 /// let handler = Handler::localhost(LocalHostHandler::from(WhichUser::CurrentUser));
284 /// ```
285 pub fn localhost(localhost_handler: LocalHostHandler) -> Self {
286 Handler::LocalHost(localhost_handler)
287 }
288
289 /// Create a [`Handler`] from a [`Ssh2HostHandler`].
290 ///
291 /// # Arguments
292 ///
293 /// * `ss2_handler` - The SSH host handler to wrap
294 ///
295 /// # Example
296 ///
297 /// ```no_run
298 /// use regent_sdk::{Handler, Ssh2HostHandler, hosts::handlers::ssh2::Ssh2AuthMethod};
299 ///
300 /// // Assuming we have valid auth details
301 /// // let ssh_handler = Ssh2HostHandler::from(Ssh2AuthMethod::Key(...)).unwrap();
302 /// // let handler = Handler::ss2(ssh_handler);
303 /// ```
304 pub fn ss2(ss2_handler: Ssh2HostHandler) -> Self {
305 Handler::Ssh2(ss2_handler)
306 }
307}
308
309impl HostHandler for Handler {
310 async fn connect(
311 &mut self,
312 endpoint: &str,
313 // secret_provider: &Option<SecretProvider>,
314 ) -> Result<(), RegentError> {
315 match self {
316 Handler::LocalHost(handler) => handler.connect(endpoint).await,
317 Handler::Ssh2(handler) => handler.connect(endpoint).await,
318 }
319 }
320
321 async fn is_connected(&mut self) -> bool {
322 match self {
323 Handler::LocalHost(handler) => handler.is_connected().await,
324 Handler::Ssh2(handler) => handler.is_connected().await,
325 }
326 }
327
328 async fn disconnect(&mut self) -> Result<(), RegentError> {
329 match self {
330 Handler::LocalHost(handler) => handler.disconnect().await,
331 Handler::Ssh2(handler) => handler.disconnect().await,
332 }
333 }
334
335 async fn is_this_command_available(
336 &mut self,
337 command: &str,
338 privilege: &Privilege,
339 ) -> Result<bool, RegentError> {
340 match self {
341 Handler::LocalHost(handler) => {
342 handler.is_this_command_available(command, privilege).await
343 }
344 Handler::Ssh2(handler) => handler.is_this_command_available(command, privilege).await,
345 }
346 }
347
348 async fn run_command(
349 &mut self,
350 command: &str,
351 privilege: &Privilege,
352 ) -> Result<CommandResult, RegentError> {
353 match self {
354 Handler::LocalHost(handler) => handler.run_command(command, privilege).await,
355 Handler::Ssh2(handler) => handler.run_command(command, privilege).await,
356 }
357 }
358
359 #[cfg(feature = "windows")]
360 async fn run_windows_command(&mut self, command: &str) -> Result<CommandResult, RegentError> {
361 match self {
362 Handler::LocalHost(handler) => handler.run_windows_command(command).await,
363 Handler::Ssh2(handler) => handler.run_windows_command(command).await,
364 }
365 }
366
367 async fn get_file(&mut self, path: PathBuf) -> Result<Vec<u8>, RegentError> {
368 match self {
369 Handler::LocalHost(handler) => handler.get_file(path).await,
370 Handler::Ssh2(handler) => handler.get_file(path).await,
371 }
372 }
373}
374
375// #[derive(Debug, Clone, Serialize, Deserialize)]
376// pub enum ConnectionDetails {
377// // LocalHost(WhichUser),
378// // Ssh2(NewSsh2ConnectionDetails),
379// }
380
381// TODO : add some syntax checks
382pub fn final_command(cmd: &str, privilege: &Privilege, user: &WhichUser) -> String {
383 match user {
384 WhichUser::CurrentUser => match privilege {
385 Privilege::None => format!("{} 2>&1", cmd),
386 Privilege::WithSudo => format!("sudo {} 2>&1", cmd),
387 Privilege::WithSudoRs => format!("sudo-rs {} 2>&1", cmd),
388 },
389 WhichUser::UsernamePassword(credentials) => match privilege {
390 Privilege::None => format!(
391 "echo {} | su - {} -c \"{}\" 2>&1", // echo <otherpwd> | su - otheruser -c "my command line"
392 credentials.password(),
393 credentials.username(),
394 cmd
395 ),
396 Privilege::WithSudo => format!(
397 "echo {} | sudo -S -u {} {} 2>&1",
398 credentials.password(),
399 credentials.username(),
400 cmd
401 ),
402 Privilege::WithSudoRs => format!(
403 "echo {} | sudo-rs -S -u {} {} 2>&1",
404 credentials.password(),
405 credentials.username(),
406 cmd
407 ),
408 },
409 }
410
411 // match privilege {
412 // Privilege::None => {
413 // let final_cmd = format!("{} 2>&1", cmd);
414 // return final_cmd;
415 // }
416 // // Privilege::WithSuAsUser(credentials) => {
417 // // let final_cmd = format!("echo {} | su - {} -c {} 2>&1", credentials.password(), credentials.username(), cmd);
418 // // return final_cmd;
419 // // }
420 // Privilege::WithSudo => {
421 // let final_cmd = format!("sudo {} 2>&1", cmd);
422 // return final_cmd;
423 // }
424 // // Privilege::WithSudoAsUser(credentials) => {
425 // // let final_cmd = format!("echo {} | sudo -S -u {} {} 2>&1", credentials.password(), credentials.username(), cmd);
426 // // return final_cmd;
427 // // }
428 // Privilege::WithSudoRs => {
429 // let final_cmd = format!("sudo-rs {} 2>&1", cmd);
430 // return final_cmd;
431 // }
432 // // Privilege::WithSudoRsAsUser(credentials) => {
433 // // let final_cmd = format!("echo {} | sudo-rs -u {} {} 2>&1", credentials.password(), credentials.username(), cmd);
434 // // return final_cmd;
435 // // }
436 // }
437}