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, &parameters).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}