cat_dev/mion/cgis/control.rs
1//! API's for interacting with `/mion/control.cgi`, an HTTP interface for
2//! turning the device on & off.
3
4use crate::{
5 errors::{APIError, CatBridgeError, NetworkError},
6 mion::{
7 cgis::{do_simple_request, encode_url_parameters, parse_result_from_body},
8 proto::cgis::{ControlOperation, MionCGIErrors, SetParameter},
9 },
10};
11use fnv::FnvHashMap;
12use local_ip_address::local_ip;
13use reqwest::{Client, Method};
14use std::{fmt::Display, net::Ipv4Addr, ops::Deref};
15use tracing::warn;
16
17/// Perform a `get_info` request given a host, and a name.
18///
19/// ## Errors
20///
21/// - If we cannot encode the parameters as a form url encoded.
22/// - If we cannot make the HTTP request.
23/// - If the server does not respond with a 200.
24/// - If we cannot read the body from HTTP.
25/// - If we cannot parse the HTML response.
26pub async fn get_info(
27 mion_ip: Ipv4Addr,
28 name: &str,
29) -> Result<FnvHashMap<String, String>, NetworkError> {
30 get_info_with_raw_client(&Client::default(), mion_ip, name).await
31}
32
33/// Perform a get info request, but with an already existing HTTP client.
34///
35/// ## Errors
36///
37/// - If we cannot encode the parameters as a form url encoded.
38/// - If we cannot make the HTTP request.
39/// - If the server does not respond with a 200.
40/// - If we cannot read the body from HTTP.
41/// - If we cannot parse the HTML response.
42pub async fn get_info_with_raw_client(
43 client: &Client,
44 mion_ip: Ipv4Addr,
45 name: &str,
46) -> Result<FnvHashMap<String, String>, NetworkError> {
47 let body_as_string = do_raw_control_request(
48 client,
49 mion_ip,
50 &[
51 ("operation", Into::<&str>::into(ControlOperation::GetInfo)),
52 (
53 "host",
54 &format!("{}", local_ip().map_err(NetworkError::LocalIp)?),
55 ),
56 ("shutdown", "1"),
57 ("name", name),
58 ],
59 )
60 .await?;
61
62 Ok(extract_body_tags(
63 &body_as_string,
64 ControlOperation::GetInfo.into(),
65 )?)
66}
67
68/// Perform a `set_param` request given a host, and the parameter to set.
69///
70/// ## Errors
71///
72/// - If we cannot encode the parameters as a form url encoded.
73/// - If we cannot make the HTTP request.
74/// - If the server does not respond with a 200.
75/// - If we cannot read the body from HTTP.
76/// - If we cannot parse the HTML response.
77pub async fn set_param(
78 mion_ip: Ipv4Addr,
79 parameter_to_set: SetParameter,
80) -> Result<bool, NetworkError> {
81 set_param_with_raw_client(&Client::default(), mion_ip, parameter_to_set).await
82}
83
84/// Set a parameter on the MION, but with an already existing HTTP Client.
85///
86/// ## Errors
87///
88/// - If we cannot encode the parameters as a form url encoded.
89/// - If we cannot make the HTTP request.
90/// - If the server does not respond with a 200.
91/// - If we cannot read the body from HTTP.
92/// - If we cannot parse the HTML response.
93pub async fn set_param_with_raw_client(
94 client: &Client,
95 mion_ip: Ipv4Addr,
96 parameter_to_set: SetParameter,
97) -> Result<bool, NetworkError> {
98 let body_as_string = do_raw_control_request(
99 client,
100 mion_ip,
101 &[
102 (
103 "operation".to_owned(),
104 Into::<&str>::into(ControlOperation::SetParam).to_owned(),
105 ),
106 (
107 format!("{parameter_to_set}"),
108 parameter_to_set.get_value_as_string(),
109 ),
110 ],
111 )
112 .await?;
113
114 Ok(parse_result_from_body(
115 &body_as_string,
116 Into::<&str>::into(ControlOperation::SetParam),
117 )?)
118}
119
120/// Initiate a power-on request to turn on the CAT-DEV machine.
121///
122/// Note: This request just starts the actual power on, by the time you get to
123/// powering on the device, if you are using things like emulation you should
124/// already be connected to the SDIO ports, and be listening for ATAPI
125/// requests.
126///
127/// This is an older power on API and ***you should always prefer
128/// `power_on_v2` if it is possible to use.***
129///
130/// ## Errors
131///
132/// - If we cannot encode the parameters as a form url encoded.
133/// - If we cannot make the HTTP request.
134/// - If the server does not respond with a 200.
135/// - If we cannot read the body from HTTP.
136/// - If we cannot parse the HTML response.
137pub async fn power_on(mion_ip: Ipv4Addr) -> Result<bool, NetworkError> {
138 power_on_with_raw_client(&Client::default(), mion_ip).await
139}
140
141/// Initiate a power-on request to turn on the CAT-DEV machine.
142///
143/// Note: This request just starts the actual power on, by the time you get to
144/// powering on the device, if you are using things like emulation you should
145/// already be connected to the SDIO ports, and be listening for ATAPI
146/// requests.
147///
148/// This is an older power on API and ***you should always prefer
149/// `power_on_v2` if it is possible to use.***
150///
151/// ## Errors
152///
153/// - If we cannot encode the parameters as a form url encoded.
154/// - If we cannot make the HTTP request.
155/// - If the server does not respond with a 200.
156/// - If we cannot read the body from HTTP.
157/// - If we cannot parse the HTML response.
158pub async fn power_on_with_raw_client(
159 client: &Client,
160 mion_ip: Ipv4Addr,
161) -> Result<bool, NetworkError> {
162 let body_as_string = do_raw_control_request(
163 client,
164 mion_ip,
165 &[(
166 "operation",
167 Into::<&str>::into(ControlOperation::PowerOn).to_owned(),
168 )],
169 )
170 .await?;
171
172 Ok(parse_result_from_body(
173 &body_as_string,
174 Into::<&str>::into(ControlOperation::PowerOnV2),
175 )?)
176}
177
178/// Initiate a power-on request to turn on the CAT-DEV machine.
179///
180/// Note: This request just starts the actual power on, by the time you get to
181/// powering on the device, if you are using things like emulation you should
182/// already be connected to the SDIO ports, and be listening for ATAPI
183/// requests.
184///
185/// This can also override some pre-existing configurations, e.g. if you send
186/// a POST with emulation set to off even if PCFS is set to on, the cat-dev
187/// will techincally still boot.
188///
189/// ***Power ON V2 is only available on MIONs running at least 00.14.77 in
190/// terms of firmware.***
191///
192/// ## Errors
193///
194/// - If we cannot encode the parameters as a form url encoded.
195/// - If we cannot make the HTTP request.
196/// - If the server does not respond with a 200.
197/// - If we cannot read the body from HTTP.
198/// - If we cannot parse the HTML response.
199pub async fn power_on_v2(
200 mion_ip: Ipv4Addr,
201 host_ip: Option<Ipv4Addr>,
202 atapi_port: Option<u16>,
203 pcfs_port: Option<u16>,
204 emulate_fs: bool,
205) -> Result<bool, CatBridgeError> {
206 power_on_v2_with_raw_client(
207 &Client::default(),
208 mion_ip,
209 host_ip,
210 atapi_port,
211 pcfs_port,
212 emulate_fs,
213 )
214 .await
215}
216
217/// Initiate a power-on request to turn on the CAT-DEV machine.
218///
219/// Note: This request just starts the actual power on, by the time you get to
220/// powering on the device, if you are using things like emulation you should
221/// already be connected to the SDIO ports, and be listening for ATAPI
222/// requests.
223///
224/// This can also override some pre-existing configurations, e.g. if you send
225/// a POST with emulation set to off even if PCFS is set to on, the cat-dev
226/// will techincally still boot.
227///
228/// ***Power ON V2 is only available on MIONs running at least 00.14.77 in
229/// terms of firmware.***
230///
231/// ## Errors
232///
233/// - If we cannot encode the parameters as a form url encoded.
234/// - If we cannot make the HTTP request.
235/// - If the server does not respond with a 200.
236/// - If we cannot read the body from HTTP.
237/// - If we cannot parse the HTML response.
238pub async fn power_on_v2_with_raw_client(
239 client: &Client,
240 mion_ip: Ipv4Addr,
241 host_ip: Option<Ipv4Addr>,
242 atapi_port: Option<u16>,
243 pcfs_port: Option<u16>,
244 emulate_fs: bool,
245) -> Result<bool, CatBridgeError> {
246 let host_ip_as_str = if let Some(ip) = host_ip {
247 format!("{ip}")
248 } else {
249 let ip = local_ip().map_err(|_| APIError::NoHostIpFound)?;
250 format!("{ip}")
251 };
252
253 let mut parameters = vec![
254 (
255 "operation",
256 Into::<&str>::into(ControlOperation::PowerOnV2).to_owned(),
257 ),
258 (
259 "emulation",
260 if emulate_fs {
261 "on".to_owned()
262 } else {
263 "off".to_owned()
264 },
265 ),
266 ("host", host_ip_as_str),
267 ];
268 if let Some(port) = atapi_port {
269 parameters.push(("atapi_port", format!("{port}")));
270 }
271 if let Some(port) = pcfs_port {
272 parameters.push(("pcfs_port", format!("{port}")));
273 }
274
275 let body_as_string = do_raw_control_request(client, mion_ip, ¶meters).await?;
276 Ok(parse_result_from_body(
277 &body_as_string,
278 Into::<&str>::into(ControlOperation::PowerOnV2),
279 )?)
280}
281
282/// Perform a raw operation on the MION board's `control.cgi` page.
283///
284/// *note: you probably want to call one of the actual methods, as this is
285/// basically just a thin wrapper around an HTTP Post Request. Not doing much
286/// else more. A lot of it requires that you set things up correctly.*
287///
288/// ## Errors
289///
290/// - If we cannot make an HTTP request to the MION Request.
291/// - If we fail to encode your parameters into a request body.
292pub async fn do_raw_control_request(
293 client: &Client,
294 mion_ip: Ipv4Addr,
295 url_parameters: &[(impl Deref<Target = str>, impl Display)],
296) -> Result<String, NetworkError> {
297 do_simple_request::<String>(
298 client,
299 Method::POST,
300 format!("http://{mion_ip}/mion/control.cgi"),
301 Some(encode_url_parameters(url_parameters)),
302 None,
303 )
304 .await
305}
306
307/// Extract tags from body request.
308///
309/// "tags" are values separated by `<br>`, and separated by `:`.
310///
311/// ## Errors
312///
313/// - If we cannot find the start `<body>` tag.
314/// - If we cannot find the end `</body>` tag.
315fn extract_body_tags(
316 body: &str,
317 operation_name: &str,
318) -> Result<FnvHashMap<String, String>, MionCGIErrors> {
319 let start_tag_location = body
320 .find("<body>")
321 .map(|num| num + 6)
322 .ok_or_else(|| MionCGIErrors::HtmlResponseMissingBody(body.to_owned()))?;
323 let body_without_start_tag = body.split_at(start_tag_location).1;
324 let end_tag_location = body_without_start_tag
325 .find("</body>")
326 .ok_or_else(|| MionCGIErrors::HtmlResponseMissingBody(body.to_owned()))?;
327 let just_inner_body = body_without_start_tag.split_at(end_tag_location).0;
328
329 let without_newlines = just_inner_body.replace('\n', "");
330 let fields = without_newlines
331 .split("<br>")
332 .filter_map(|line| {
333 // Remove all empty lines, and all log lines.
334 if line.is_empty()
335 || line.trim().is_empty()
336 || line.starts_with("INFO:")
337 || line.starts_with("WARN:")
338 || line.starts_with("ERROR:")
339 {
340 None
341 } else if let Some(location) = line.find(':') {
342 let (key, value) = line.split_at(location);
343 Some((key.to_owned(), value.trim_start_matches(':').to_owned()))
344 } else {
345 warn!(%operation_name, "Unparsable line from body on mion/control.cgi: {line}");
346 None
347 }
348 })
349 .collect::<FnvHashMap<String, String>>();
350
351 Ok(fields)
352}