cat_dev/mion/cgis/
mod.rs

1//! CGI's that are available to interact with the MION on.
2//!
3//! These various CGI web pages you can interact with normally on the web.
4
5/// HTTP Basic authorization header.
6///
7/// This gets passed as the header:
8/// `Authorization: Basic bWlvbjovTXVsdGlfSS9PX05ldHdvcmsv`
9///
10/// Given this is http basic auth, you can decode this string as:
11/// `mion:/Multi_I/O_Network/`
12///
13/// Which means the username is: `mion`, and the password is:
14/// `/Multi_I/O_Network/`.
15const AUTHZ_HEADER: &str = "bWlvbjovTXVsdGlfSS9PX05ldHdvcmsv";
16
17mod control;
18mod dump_eeprom;
19mod dump_memory;
20mod errors;
21mod setup;
22mod signal_get;
23mod status;
24mod update;
25
26pub use control::*;
27pub use dump_eeprom::*;
28pub use dump_memory::*;
29pub use errors::*;
30pub use setup::*;
31pub use signal_get::*;
32pub use status::*;
33pub use update::*;
34
35use crate::{
36	errors::{NetworkError, NetworkParseError},
37	mion::proto::cgis::MIONCGIErrors,
38};
39use bytes::Bytes;
40use reqwest::{Body, Client, Method, Response, Version};
41use tracing::{field::valuable, warn};
42
43/// Perform a request that attempts to remove all the logic for the 'simple'
44/// request cases.
45///
46/// Most MION HTTP Requests follow a simple format:
47///
48/// - Use HTTP Version 1.1.
49/// - Authenticate with the common authorization header.
50/// - Content Type is: `application/x-www-form-urlencoded`.
51/// - Only Response Status Code that is successful is 200.
52/// - There is a response body, and it is a UTF-8 String.
53///
54/// If your request meets all these requirements, then congrats! You can use
55/// this method to implement it. Cutting down on the logic you have to
56/// implement by a ton! Otherwise, you'll have to implement bits, and pieces
57/// yourself.
58async fn do_simple_request<BodyTy>(
59	client: &Client,
60	method: Method,
61	url: String,
62	body: Option<BodyTy>,
63) -> Result<String, NetworkError>
64where
65	BodyTy: Into<Body>,
66{
67	let mut req = client
68		.request(method, url)
69		.version(Version::HTTP_11)
70		.header("authorization", format!("Basic {AUTHZ_HEADER}"))
71		.header("content-type", "application/x-www-form-urlencoded")
72		.header("user-agent", concat!("cat-dev/", env!("CARGO_PKG_VERSION")));
73	if let Some(body) = body {
74		req = req.body(body);
75	}
76	let response_body =
77		assert_status_and_read_body(200, req.send().await.map_err(NetworkError::HTTP)?).await?;
78
79	Ok(String::from_utf8(response_body.into()).map_err(NetworkParseError::Utf8Expected)?)
80}
81
82/// Assert that a response status code is a specific code, and get the body.
83///
84/// ## Errors
85///
86/// If the status code did not match the expected result.
87async fn assert_status_and_read_body(
88	needed_status: u16,
89	response: Response,
90) -> Result<Bytes, NetworkError> {
91	let status = response.status().as_u16();
92	let body_result = response.bytes().await.map_err(NetworkError::HTTP);
93	if status != needed_status {
94		if let Ok(body) = body_result {
95			return Err(MIONCGIErrors::UnexpectedStatusCode(status, body).into());
96		}
97
98		return Err(MIONCGIErrors::UnexpectedStatusCodeNoBody(status).into());
99	}
100
101	body_result
102}
103
104/// Parse out a result status from an HTML page.
105///
106/// If your page ends up rendering up results like: `RESULT:OK` when things are
107/// successful, you can use this method to get that result information.
108fn parse_result_from_body(body: &str, operation_name: &str) -> Result<bool, MIONCGIErrors> {
109	let start_tag_location = body
110		.find("<body>")
111		.map(|num| num + 6)
112		.or(
113			// Some pages don't have a start body tag, because MION's HTML stack
114			// is absolute trash.
115			body.find("<HTML>").map(|num| num + 6),
116		)
117		.ok_or_else(|| MIONCGIErrors::HtmlResponseMissingBody(body.to_owned()))?;
118	let body_without_start_tag = body.split_at(start_tag_location).1;
119	let end_tag_location = body_without_start_tag
120		.find("</body>")
121		.ok_or_else(|| MIONCGIErrors::HtmlResponseMissingBody(body.to_owned()))?;
122	let just_inner_body = body_without_start_tag.split_at(end_tag_location).0;
123	let without_newlines_or_extra_tags = just_inner_body
124		.replace('\n', "")
125		.replace("<CENTER>", "")
126		.replace("</CENTER>", "");
127
128	let mut was_successful = false;
129	let mut returned_result_code = "";
130	let mut log_lines = Vec::with_capacity(0);
131	let mut extra_lines = Vec::with_capacity(0);
132	for line in without_newlines_or_extra_tags
133		.split("<br>")
134		.fold(Vec::new(), |mut accum, item| {
135			accum.extend(item.split("<br/>"));
136			accum
137		}) {
138		let trimmed_line = line.trim();
139		if trimmed_line.is_empty() {
140			continue;
141		}
142
143		if let Some(result_code) = trimmed_line.strip_prefix("RESULT:") {
144			returned_result_code = result_code;
145			if result_code == "OK" {
146				was_successful = true;
147			}
148		} else if trimmed_line.starts_with("INFO:")
149			|| trimmed_line.starts_with("ERROR:")
150			|| trimmed_line.starts_with("WARN:")
151		{
152			log_lines.push(trimmed_line);
153		} else {
154			extra_lines.push(trimmed_line);
155		}
156	}
157
158	if !was_successful {
159		warn!(
160			log_lines = valuable(&log_lines),
161			extra_lines = valuable(&extra_lines),
162			%operation_name,
163			result_code = %returned_result_code,
164			"got an error from a result status page",
165		);
166	}
167
168	Ok(was_successful)
169}
170
171#[cfg(test)]
172mod unit_tests {
173	use super::*;
174
175	#[test]
176	pub fn can_parse_response_from_power_on_v2_on_14_80() {
177		// Yes. This is a real HTML response, returned from a real MION running,
178		// real, official firmware.
179		//
180		// Yes it is invalid HTML. Yes it is a mess. Yes it is not always returned
181		// based off the parameters passed.
182		assert!(
183			parse_result_from_body(
184				"<HTML>\r\nINFO: Enabling CATDEV mode...<br>\nINFO: no change in parameters<br>\nINFO: Enabling HSATA keep alive<br/>\nRESULT:OK<br>cafe powered on successfully, nAtt=0<br>\n</body>\n</HTML>",
185				"power_on_v2"
186			).expect("Failed to parse result from body!"),
187			"Expected successful responsee."
188		);
189	}
190}