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}