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