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