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