1const AUTHZ_HEADER: &str = "bWlvbjovTXVsdGlfSS9PX05ldHdvcmsv";
16
17mod control;
18mod dump_eeprom;
19mod dump_memory;
20mod setup;
21mod signal_get;
22mod status;
23mod update;
24
25pub use control::*;
26pub use dump_eeprom::*;
27pub use dump_memory::*;
28pub use setup::*;
29pub use signal_get::*;
30pub use status::*;
31pub use update::*;
32
33use crate::{
34 errors::{NetworkError, NetworkParseError},
35 mion::proto::cgis::MionCGIErrors,
36};
37use bytes::Bytes;
38use form_urlencoded::byte_serialize;
39use reqwest::{Body, Client, Method, Response, Version};
40use std::{fmt::Display, ops::Deref, time::Duration};
41use tokio::time::timeout;
42use tracing::{Instrument, error_span, field::valuable, warn};
43
44pub const DEFAULT_HTTP_TIMEOUT: Duration = Duration::from_secs(5);
48
49async fn do_simple_request<BodyTy>(
65 client: &Client,
66 method: Method,
67 url: String,
68 body: Option<BodyTy>,
69 req_timeout: Option<Duration>,
70) -> Result<String, NetworkError>
71where
72 BodyTy: Into<Body>,
73{
74 let span = error_span!(
75 "cat_dev::mion::cgis::do_simple_request",
76 http.method = %method,
77 http.url = %url,
78 );
79
80 async {
81 let mut req = client
82 .request(method, url)
83 .version(Version::HTTP_11)
84 .header("authorization", format!("Basic {AUTHZ_HEADER}"))
85 .header("content-type", "application/x-www-form-urlencoded")
86 .header("user-agent", concat!("cat-dev/", env!("CARGO_PKG_VERSION")));
87 if let Some(body) = body {
88 req = req.body(body);
89 }
90
91 let timeout_time = req_timeout.unwrap_or(DEFAULT_HTTP_TIMEOUT);
92 let response_body = timeout(
93 timeout_time,
94 assert_status_and_read_body(200, req.send().await.map_err(NetworkError::HTTP)?),
95 )
96 .await
97 .map_err(|_| NetworkError::Timeout(timeout_time))??;
98
99 Ok(String::from_utf8(response_body.into()).map_err(NetworkParseError::Utf8Expected)?)
100 }
101 .instrument(span)
102 .await
103}
104
105fn encode_url_parameters(parameters: &[(impl Deref<Target = str>, impl Display)]) -> String {
106 let mut buff = String::new();
107 let mut first = true;
108
109 for (param_key, param_value) in parameters {
110 if !first {
111 buff.push('&');
112 }
113 first = false;
114 buff.extend(byte_serialize(param_key.as_bytes()));
115 buff.push('=');
116 buff.extend(byte_serialize(format!("{param_value}").as_bytes()));
117 }
118
119 buff
120}
121
122async fn assert_status_and_read_body(
128 needed_status: u16,
129 response: Response,
130) -> Result<Bytes, NetworkError> {
131 let status = response.status().as_u16();
132 let body_result = response.bytes().await.map_err(NetworkError::HTTP);
133 if status != needed_status {
134 if let Ok(body) = body_result {
135 return Err(MionCGIErrors::UnexpectedStatusCode(status, body).into());
136 }
137
138 return Err(MionCGIErrors::UnexpectedStatusCodeNoBody(status).into());
139 }
140
141 body_result
142}
143
144fn parse_result_from_body(body: &str, operation_name: &str) -> Result<bool, MionCGIErrors> {
149 let start_tag_location = body
150 .find("<body>")
151 .map(|num| num + 6)
152 .or(
153 body.find("<HTML>").map(|num| num + 6),
156 )
157 .ok_or_else(|| MionCGIErrors::HtmlResponseMissingBody(body.to_owned()))?;
158 let body_without_start_tag = body.split_at(start_tag_location).1;
159 let end_tag_location = body_without_start_tag
160 .find("</body>")
161 .ok_or_else(|| MionCGIErrors::HtmlResponseMissingBody(body.to_owned()))?;
162 let just_inner_body = body_without_start_tag.split_at(end_tag_location).0;
163 let without_newlines_or_extra_tags = just_inner_body
164 .replace('\n', "")
165 .replace("<CENTER>", "")
166 .replace("</CENTER>", "");
167
168 let mut was_successful = false;
169 let mut returned_result_code = "";
170 let mut log_lines = Vec::with_capacity(0);
171 let mut extra_lines = Vec::with_capacity(0);
172 for line in without_newlines_or_extra_tags
173 .split("<br>")
174 .fold(Vec::new(), |mut accum, item| {
175 accum.extend(item.split("<br/>"));
176 accum
177 }) {
178 let trimmed_line = line.trim();
179 if trimmed_line.is_empty() {
180 continue;
181 }
182
183 if let Some(result_code) = trimmed_line.strip_prefix("RESULT:") {
184 returned_result_code = result_code;
185 if result_code == "OK" {
186 was_successful = true;
187 }
188 } else if trimmed_line.starts_with("INFO:")
189 || trimmed_line.starts_with("ERROR:")
190 || trimmed_line.starts_with("WARN:")
191 {
192 log_lines.push(trimmed_line);
193 } else {
194 extra_lines.push(trimmed_line);
195 }
196 }
197
198 if !was_successful {
199 warn!(
200 log_lines = valuable(&log_lines),
201 extra_lines = valuable(&extra_lines),
202 %operation_name,
203 result_code = %returned_result_code,
204 "got an error from a result status page",
205 );
206 }
207
208 Ok(was_successful)
209}
210
211#[cfg(test)]
212mod unit_tests {
213 use super::*;
214
215 #[test]
216 pub fn can_parse_response_from_power_on_v2_on_14_80() {
217 assert!(
223 parse_result_from_body(
224 "<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>",
225 "power_on_v2"
226 ).expect("Failed to parse result from body!"),
227 "Expected successful responsee."
228 );
229 }
230}