Skip to main content

product_os_command_control/
commands.rs

1//! Command execution module
2//!
3//! Provides structures and functions for executing commands and queries
4//! on remote Product OS nodes.
5
6use std::prelude::v1::*;
7
8use std::collections::BTreeMap;
9use product_os_request::{ProductOSClient, ProductOSResponse};
10
11use crate::registry::Node;
12
13
14/// Command structure for executing commands on remote nodes.
15///
16/// This struct holds all the necessary information to execute a command
17/// on a remote Product OS node, including authentication and target details.
18///
19/// # Examples
20///
21/// ```no_run
22/// # use product_os_command_control::commands::Command;
23/// # use product_os_request::{ProductOSRequestClient, Uri};
24/// # use std::str::FromStr;
25/// let command = Command {
26///     requester: ProductOSRequestClient::new(),
27///     node_url: Uri::from_str("https://localhost:8443").unwrap(),
28///     verify_key: vec![1, 2, 3, 4],
29///     module: "status".to_string(),
30///     instruction: "ping".to_string(),
31///     data: None,
32/// };
33/// ```
34pub struct Command {
35    /// HTTP client for making requests
36    pub requester: product_os_request::ProductOSRequestClient,
37    /// Target node URL
38    pub node_url: product_os_request::Uri,
39    /// Verification key for authentication
40    pub verify_key: Vec<u8>,
41    /// Module name to execute
42    pub module: String,
43    /// Instruction/command name
44    pub instruction: String,
45    /// Optional data payload
46    pub data: Option<serde_json::Value>
47}
48
49impl Command {
50    /// Execute this command on the target node.
51    ///
52    /// Sends the command to the remote node and returns the response.
53    ///
54    /// # Errors
55    ///
56    /// Returns an error if the request fails or the node cannot be reached.
57    pub async fn command(&self) -> Result<ProductOSResponse<product_os_request::BodyBytes>, product_os_request::ProductOSRequestError> {
58        command(&self.requester, self.node_url.to_owned(), self.verify_key.to_owned(), self.module.as_str(), self.instruction.as_str(), self.data.to_owned()).await
59    }
60}
61
62
63
64/// Ask structure for querying remote nodes.
65///
66/// Similar to Command but for general HTTP requests to remote nodes.
67///
68/// # Examples
69///
70/// ```no_run
71/// # use product_os_command_control::commands::Ask;
72/// # use product_os_request::{ProductOSRequestClient, Uri, Method};
73/// # use std::str::FromStr;
74/// # use std::collections::BTreeMap;
75/// let ask = Ask {
76///     requester: ProductOSRequestClient::new(),
77///     node_url: Uri::from_str("https://localhost:8443").unwrap(),
78///     verify_key: vec![1, 2, 3, 4],
79///     path: "/api/status".to_string(),
80///     data: None,
81///     headers: BTreeMap::new(),
82///     params: BTreeMap::new(),
83///     method: Method::GET,
84/// };
85/// ```
86pub struct Ask {
87    /// HTTP client for making requests
88    pub requester: product_os_request::ProductOSRequestClient,
89    /// Target node URL
90    pub node_url: product_os_request::Uri,
91    /// Verification key for authentication
92    pub verify_key: Vec<u8>,
93    /// Request path
94    pub path: String,
95    /// Optional request data
96    pub data: Option<serde_json::Value>,
97    /// HTTP headers
98    pub headers: BTreeMap<String, String>,
99    /// Query parameters
100    pub params: BTreeMap<String, String>,
101    /// HTTP method
102    pub method: product_os_request::Method
103}
104
105impl Ask {
106    /// Execute this query on the target node.
107    ///
108    /// Sends the HTTP request to the remote node and returns the response.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if the request fails or the node cannot be reached.
113    pub async fn ask(&self) -> Result<ProductOSResponse<product_os_request::BodyBytes>, product_os_request::ProductOSRequestError> {
114        ask(&self.requester, &self.node_url, self.verify_key.as_slice(), self.path.as_str(), &self.data, &self.headers, &self.params, &self.method).await
115    }
116}
117
118
119
120/// Execute a command on a remote node.
121///
122/// Sends a command to the specified node URL with authentication.
123///
124/// # Arguments
125///
126/// * `requester` - HTTP client for making the request
127/// * `uri` - Target node URI
128/// * `verify_key` - Authentication key
129/// * `module` - Module name
130/// * `instruction` - Instruction/command name
131/// * `data` - Optional JSON data payload
132///
133/// # Errors
134///
135/// Returns an error if the request fails or authentication is rejected.
136pub async fn command(requester: &product_os_request::ProductOSRequestClient, uri: product_os_request::Uri, verify_key: Vec<u8>,
137                     module: &str, instruction: &str, data: Option<serde_json::Value>) -> Result<ProductOSResponse<product_os_request::BodyBytes>, product_os_request::ProductOSRequestError> {
138    let endpoint = String::from(uri.to_string().trim_end_matches("/")) +
139        "/command/" + module + "/" + instruction;
140
141    let mut request = requester.new_request(product_os_request::Method::POST, endpoint.as_str());
142
143    match &data {
144        None => {}
145        Some(data) => {
146            requester.set_body_json(&mut request, data.to_owned()).await;
147        }
148    }
149
150    request.add_header("x-product-os-verify",
151                       product_os_security::create_auth_request(None, false, data,
152                                                                   None, &[], Some(verify_key.as_slice())).as_str(),
153                       true);
154
155    match requester.request(request).await {
156        Ok(response) => {
157            tracing::trace!("Successfully sent {:?}/{:?} command to server {:?}", module, instruction, uri);
158            Ok(response)
159        },
160        Err(e) => {
161            tracing::error!("Error encountered {:?} from {}", e, uri);
162            Err(product_os_request::ProductOSRequestError::Error(format!("No matching node found: {:?}", e)))
163        }
164    }
165}
166
167/// Execute a query on a specific node.
168///
169/// Helper function that wraps `ask` with node-specific details.
170///
171/// # Arguments
172///
173/// * `requester` - HTTP client
174/// * `node` - Target node
175/// * `verify_key` - Authentication key
176/// * `path` - Request path
177/// * `data` - Optional JSON data
178/// * `headers` - HTTP headers
179/// * `params` - Query parameters
180/// * `method` - HTTP method
181///
182/// # Errors
183///
184/// Returns an error if the request fails.
185pub async fn ask_node(requester: &product_os_request::ProductOSRequestClient, node: &Node, verify_key: &[u8],
186       path: &str, data: &Option<serde_json::Value>, headers: &BTreeMap<String, String>,
187       params: &BTreeMap<String, String>, method: &product_os_request::Method) -> Result<ProductOSResponse<product_os_request::BodyBytes>, product_os_request::ProductOSRequestError> {
188    #[allow(deprecated)]
189    let uri = node.get_address();
190
191    ask(requester, &uri, verify_key, path, data, headers, params, method).await
192}
193
194/// Send an HTTP request to a remote node.
195///
196/// General-purpose function for making authenticated requests to Product OS nodes.
197///
198/// # Arguments
199///
200/// * `requester` - HTTP client
201/// * `url` - Target URL
202/// * `verify_key` - Authentication key
203/// * `path` - Request path
204/// * `data` - Optional JSON data
205/// * `headers` - HTTP headers
206/// * `params` - Query parameters
207/// * `method` - HTTP method
208///
209/// # Errors
210///
211/// Returns an error if the request fails or the path is invalid.
212pub async fn ask(requester: &product_os_request::ProductOSRequestClient, url: &product_os_request::Uri, verify_key: &[u8],
213                 path: &str, data: &Option<serde_json::Value>, headers: &BTreeMap<String, String>,
214                 params: &BTreeMap<String, String>, method: &product_os_request::Method) -> Result<ProductOSResponse<product_os_request::BodyBytes>, product_os_request::ProductOSRequestError> {
215    let path_clean = clean_path(path, false);
216    let endpoint = String::from(url.to_string().trim_end_matches("/")) +
217        path_clean.as_str();
218
219    let mut request = requester.new_request(method.to_owned(), endpoint.as_str());
220
221    request.add_headers(headers.to_owned(), false);
222    request.add_params(params.to_owned());
223
224    match &data {
225        None => {}
226        Some(data) => {
227            requester.set_body_json(&mut request, data.to_owned()).await;
228        }
229    }
230
231    request.add_header("x-product-os-verify",
232                       product_os_security::create_auth_request(None, false, data.to_owned(),
233                                                                   None, &[], Some(verify_key)).as_str(),
234                       true);
235
236    match requester.request(request).await {
237        Ok(response) => {
238            tracing::trace!("Successfully sent ask {:?} command to server {:?}", path_clean, url);
239            Ok(response)
240        },
241        Err(e) => {
242            tracing::error!("Error encountered {:?} from {}", e, url);
243            Err(product_os_request::ProductOSRequestError::Error(format!("No matching node found: {:?}", e)))
244        }
245    }
246}
247
248/// Sanitises a URL path by collapsing double slashes, removing path traversal
249/// sequences, and stripping trailing slashes.
250///
251/// Ensures the path starts with `/`. If `add_back_slash` is true, ensures it ends with `/`.
252pub(crate) fn clean_path(input: &str, add_back_slash: bool) -> String {
253    let mut path = input.to_string();
254
255    if path != "/" {
256        while path.contains("//") {
257            path = path.replace("//", "/");
258        }
259        path = path.trim_end_matches('/').to_string();
260        if path.is_empty() {
261            path = "/".to_string();
262        }
263    }
264
265    if !path.starts_with('/') {
266        path = format!("/{}", path);
267    }
268
269    if add_back_slash && !path.ends_with('/') {
270        path.push('/');
271    }
272
273    path.replace("..", "")
274}
275
276#[cfg(test)]
277mod tests {
278    use super::clean_path;
279
280    #[test]
281    fn test_clean_path_normal() {
282        assert_eq!(clean_path("/api/test", false), "/api/test");
283    }
284
285    #[test]
286    fn test_clean_path_root() {
287        assert_eq!(clean_path("/", false), "/");
288    }
289
290    #[test]
291    fn test_clean_path_double_slashes() {
292        let result = clean_path("//api//test", false);
293        assert!(!result.contains("//"));
294    }
295
296    #[test]
297    fn test_clean_path_trailing_slash() {
298        assert_eq!(clean_path("/api/test/", false), "/api/test");
299    }
300
301    #[test]
302    fn test_clean_path_no_leading_slash() {
303        let result = clean_path("api/test", false);
304        assert!(result.starts_with('/'));
305    }
306
307    #[test]
308    fn test_clean_path_traversal() {
309        let result = clean_path("/../../../etc/passwd", false);
310        assert!(!result.contains(".."));
311    }
312
313    #[test]
314    fn test_clean_path_add_back_slash() {
315        let result = clean_path("/api/test", true);
316        assert!(result.ends_with('/'));
317    }
318
319    #[test]
320    fn test_clean_path_add_back_slash_already_present() {
321        let result = clean_path("/", true);
322        assert_eq!(result, "/");
323    }
324
325    #[test]
326    fn test_clean_path_empty_string() {
327        let result = clean_path("", false);
328        assert!(result.starts_with('/'));
329    }
330}