1use crate::{
5 errors::CatBridgeError,
6 mion::{
7 cgis::do_simple_request,
8 proto::cgis::{MionCGIErrors, SetupParameters},
9 },
10};
11use mac_address::MacAddress;
12use reqwest::{Body, Client, Method};
13use std::net::Ipv4Addr;
14
15pub async fn get_setup_parameters(mion_ip: Ipv4Addr) -> Result<SetupParameters, CatBridgeError> {
22 get_setup_parameters_with_raw_client(&Client::default(), mion_ip).await
23}
24
25pub async fn get_setup_parameters_with_raw_client(
33 client: &Client,
34 mion_ip: Ipv4Addr,
35) -> Result<SetupParameters, CatBridgeError> {
36 let body_as_string = do_simple_request::<Body>(
37 client,
38 Method::GET,
39 format!("http://{mion_ip}/setup.cgi"),
40 None,
41 None,
42 )
43 .await?;
44
45 parse_response_body_from_setup(&body_as_string)
46}
47
48fn parse_response_body_from_setup(body_as_string: &str) -> Result<SetupParameters, CatBridgeError> {
58 Ok(SetupParameters::new(
59 find_input_from_body(&InputType::Text, "id_5", body_as_string)?
60 .parse::<Ipv4Addr>()
61 .map_err(MionCGIErrors::HtmlResponseIpExpectedButNotThere)?,
62 find_input_from_body(&InputType::Text, "id_6", body_as_string)?
63 .parse::<Ipv4Addr>()
64 .map_err(MionCGIErrors::HtmlResponseIpExpectedButNotThere)?,
65 find_input_from_body(&InputType::Text, "id_7", body_as_string)?
66 .parse::<Ipv4Addr>()
67 .map_err(MionCGIErrors::HtmlResponseIpExpectedButNotThere)?,
68 find_input_from_body(&InputType::BooleanRadio, "id_4", body_as_string)?
69 .parse::<bool>()
70 .expect("impossible, this function only returns Ok(true) or Ok(false)"),
71 find_input_from_body(&InputType::BooleanRadio, "id_8", body_as_string)?
72 .parse::<bool>()
73 .expect("impossible, this function only returns Ok(true) or Ok(false)"),
74 find_input_from_body(&InputType::Text, "id_9", body_as_string)?
75 .parse::<Ipv4Addr>()
76 .map_err(MionCGIErrors::HtmlResponseIpExpectedButNotThere)?,
77 find_input_from_body(&InputType::Text, "id_10", body_as_string)?
78 .parse::<Ipv4Addr>()
79 .map_err(MionCGIErrors::HtmlResponseIpExpectedButNotThere)?,
80 find_input_from_body(&InputType::BooleanRadio, "id_11", body_as_string)?
81 .parse::<bool>()
82 .expect("impossible, this function only returns Ok(true) or Ok(false)"),
83 find_input_from_body(&InputType::Text, "id_12", body_as_string)?
84 .parse::<Ipv4Addr>()
85 .map_err(MionCGIErrors::HtmlResponseIpExpectedButNotThere)?,
86 find_input_from_body(&InputType::SelectedResultValue, "id_26", body_as_string)?
87 .parse::<u32>()
88 .map_err(MionCGIErrors::HtmlResponseNumberExpectedButNotThere)?
89 .try_into()?,
90 find_input_from_body(&InputType::Text, "id_27", body_as_string)?
91 .parse::<u8>()
92 .map_err(MionCGIErrors::HtmlResponseNumberExpectedButNotThere)?,
93 find_input_from_body(&InputType::Text, "id_13", body_as_string)?
94 .parse::<u16>()
95 .map_err(MionCGIErrors::HtmlResponseNumberExpectedButNotThere)?,
96 find_input_from_body(&InputType::Text, "id_14", body_as_string)?
97 .parse::<u16>()
98 .map_err(MionCGIErrors::HtmlResponseNumberExpectedButNotThere)?,
99 find_input_from_body(&InputType::Text, "id_15", body_as_string)?
100 .parse::<u16>()
101 .map_err(MionCGIErrors::HtmlResponseNumberExpectedButNotThere)?,
102 find_input_from_body(&InputType::Text, "id_16", body_as_string)?
103 .parse::<u16>()
104 .map_err(MionCGIErrors::HtmlResponseNumberExpectedButNotThere)?,
105 find_input_from_body(&InputType::Text, "id_17", body_as_string)?
106 .parse::<u16>()
107 .map_err(MionCGIErrors::HtmlResponseNumberExpectedButNotThere)?,
108 find_input_from_body(&InputType::BooleanRadio, "id_33", body_as_string)?
109 .parse::<bool>()
110 .expect("impossible, this function only returns Ok(true) or Ok(false)"),
111 find_input_from_body(&InputType::BooleanRadio, "id_32", body_as_string)?
112 .parse::<bool>()
113 .expect("impossible, this function only returns Ok(true) or Ok(false)"),
114 find_input_from_body(&InputType::Text, "id_28", body_as_string)?,
115 find_input_from_body(&InputType::Text, "id_29", body_as_string)?,
116 find_input_from_body(&InputType::Text, "id_30", body_as_string)?,
117 find_input_from_body(&InputType::Text, "id_31", body_as_string)?,
118 find_input_from_body(&InputType::TextArea, "id_2", body_as_string)?,
119 find_input_from_body(
120 &InputType::TableItemWithPrefix("MAC Address"),
121 "",
122 body_as_string,
123 )?
124 .parse::<MacAddress>()
125 .map_err(MionCGIErrors::HtmlResponseMacExpectedButNotThere)?,
126 ))
127}
128
129enum InputType<'inner> {
130 Text,
131 TextArea,
132 BooleanRadio,
133 SelectedResultValue,
134 TableItemWithPrefix(&'inner str),
135}
136
137fn find_input_from_body(
138 input_typ: &InputType<'_>,
139 input_id: &str,
140 html_response: &str,
141) -> Result<String, MionCGIErrors> {
142 match input_typ {
143 InputType::Text => {
144 let string_to_find = format!("<input type=\"text\" name=\"{input_id}\"");
145 let pos = get_html_value(input_id, html_response, html_response, &string_to_find)?;
146
147 let starts_at_input = &html_response[pos..];
148 let value_start = get_html_value(input_id, html_response, starts_at_input, "value=\"")?;
149 let next_value = &starts_at_input[value_start + 7..];
150 let ending_value = get_html_value(input_id, html_response, next_value, "\">")?;
151 let final_value = &next_value[..ending_value];
152
153 Ok(final_value.to_owned())
154 }
155 InputType::TextArea => {
156 let pos = get_html_value(
157 input_id,
158 html_response,
159 html_response,
160 &format!("<textarea name=\"{input_id}\""),
161 )?;
162 let starts_at_input = &html_response[pos..];
163 let end_tag_pos = get_html_value(input_id, html_response, starts_at_input, ">")?;
164 let minus_start_tag = &starts_at_input[end_tag_pos + 1..];
165 let closing_tag =
166 get_html_value(input_id, html_response, minus_start_tag, "</textarea>")?;
167 let value = &minus_start_tag[..closing_tag];
168
169 Ok(value.to_owned())
170 }
171 InputType::BooleanRadio => {
172 if html_response.contains(&format!(
173 "<input type=\"radio\" name=\"{input_id}\" value=\"1\" checked>"
174 )) {
175 Ok("true".to_owned())
176 } else if html_response.contains(&format!(
177 "<input type=\"radio\" name=\"{input_id}\" value=\"0\" checked>"
178 )) {
179 Ok("false".to_owned())
180 } else {
181 Err(MionCGIErrors::HtmlResponseNoRadioChecked(
182 html_response.to_owned(),
183 ))
184 }
185 }
186 InputType::SelectedResultValue => {
187 let start_input_pos = get_html_value(
188 input_id,
189 html_response,
190 html_response,
191 &format!("<select name=\"{input_id}\">"),
192 )?;
193 let start_at_select_value = &html_response[start_input_pos + 15 + input_id.len()..];
194 let select_end_pos =
195 get_html_value(input_id, html_response, start_at_select_value, "</select>")?;
196 let selects = &start_at_select_value[..select_end_pos];
197 let end_input_pos = get_html_value(input_id, html_response, selects, "\" selected>")?;
198 let ends_at_selected_value = &selects[..end_input_pos];
199 let start_option_location = rget_html_value(
200 input_id,
201 html_response,
202 ends_at_selected_value,
203 "<option value=\"",
204 )?;
205
206 let value_as_string = &ends_at_selected_value[start_option_location + 15..];
207 Ok(value_as_string.to_owned())
208 }
209 InputType::TableItemWithPrefix(table_prefix) => {
210 for table_item in get_all_table_items(html_response) {
211 let stripped_beginning = table_item
212 .replace(" ", "")
213 .replace("<br>", "")
214 .replace("<br/>", "")
215 .replace("<br />", "");
216 let trimmed = stripped_beginning.trim();
217 if !trimmed.starts_with(table_prefix) {
218 continue;
219 }
220 let value = trimmed[table_prefix.len()..].trim();
221
222 return Ok(value.to_owned());
223 }
224
225 Err(MionCGIErrors::HtmlResponseNoTableItemWithPrefix(
226 html_response.to_owned(),
227 table_prefix.to_owned().to_owned(),
228 ))
229 }
230 }
231}
232
233fn get_all_table_items(body: &str) -> Vec<&str> {
234 let mut table_items = Vec::new();
235
236 let mut response = body;
237 loop {
238 let Some(next_table_start) = response.find("<td") else {
239 break;
240 };
241 let minus_start_of_td = &response[next_table_start + 3..];
242 let Some(tag_end) = minus_start_of_td.find('>') else {
243 break;
244 };
245 let minus_opening_tag = &minus_start_of_td[tag_end + 1..];
246
247 let Some(end_tag_start) = minus_opening_tag.find("</td>") else {
248 break;
249 };
250 let minus_ending_tag = &minus_opening_tag[..end_tag_start];
251 table_items.push(minus_ending_tag);
252
253 response = &response[end_tag_start + 5..];
254 }
255
256 table_items
257}
258
259fn get_html_value(
260 input_id: &str,
261 html_response: &str,
262 haystack: &str,
263 needle: &str,
264) -> Result<usize, MionCGIErrors> {
265 haystack.find(needle).ok_or_else(|| {
266 MionCGIErrors::HtmlResponseMissingTaggedInput(input_id.to_owned(), html_response.to_owned())
267 })
268}
269fn rget_html_value(
270 input_id: &str,
271 html_response: &str,
272 haystack: &str,
273 needle: &str,
274) -> Result<usize, MionCGIErrors> {
275 haystack.rfind(needle).ok_or_else(|| {
276 MionCGIErrors::HtmlResponseMissingTaggedInput(input_id.to_owned(), html_response.to_owned())
277 })
278}
279
280#[cfg(test)]
281mod unit_tests {
282 use super::*;
283 use crate::mion::proto::cgis::CatDevBankSize;
284
285 const REAL_WORLD_SETUP_RESPONSE: &str = r##"<HTML>
287<head>
288<meta http-equiv="Content-Type" content="text/html; charset=ASCII">
289<title>MION Board Parameter Settings</title>
290</head>
291<body bgcolor="#FFFFFF" text="#000000">
292<div align="center">
293<h1><u>MION Board Parameter Settings</u></h1>
294<form method="POST" name="param_form" action="setup.cgi">
295<table border=1 cellspacing=1 cellpadding=8>
296 <tr>
297 <td rowspan=9><font size="+1">Network</font></td>
298 <td>IP Address<br><input type="text" name="id_5" maxlength=15 size=25 value="192.168.0.1"></td>
299 </tr>
300 <tr>
301 <td>Subnet Mask<br><input type="text" name="id_6" maxlength=15 size=25 value="255.255.255.0"></td>
302 </tr>
303 <tr>
304 <td>Default Gateway<br><input type="text" name="id_7" maxlength=15 size=25 value="0.0.0.0"></td>
305 </tr>
306 <tr>
307 <td>
308 DHCP<br>
309 <input type="radio" name="id_4" value="1" checked>ON
310 <input type="radio" name="id_4" value="0">OFF
311 </td>
312 </tr>
313 <tr>
314 <td>
315 DNS<br>
316 <input type="radio" name="id_8" value="1">ON
317 <input type="radio" name="id_8" value="0" checked>OFF
318 </td>
319 </tr>
320 <tr>
321 <td>Preferred DNS Server Address<br><input type="text" name="id_9" maxlength=15 size=25 value="0.0.0.0"></td>
322 </tr>
323 <tr>
324 <td>Alternate DNS Server Address<br><input type="text" name="id_10" maxlength=15 size=25 value="0.0.0.0"></td>
325 </tr>
326 <tr>
327 <td>
328 Jumbo Frame<br>
329 <input type="radio" name="id_11" value="1" checked>ON
330 <input type="radio" name="id_11" value="0">OFF
331 </td>
332 </tr>
333 <tr>
334 <td>
335 Host PC IP Address<br>
336 <input type="text" name="id_12" maxlength=15 size=25 value="0.0.0.0">
337 </td>
338 </tr>
339 <tr>
340 <td rowspan=2><font size="+1">SATA</font></td>
341 <td>
342 Internal HDD Bank Size<br>
343 <select name="id_26">
344 <option value="4294967295">
345 <option value="0" selected>25GB
346 <option value="1">5GB
347 <option value="2">9GB
348 <option value="3">12GB
349 <option value="4">14GB
350 <option value="5">16GB
351 <option value="6">18GB
352 <option value="7">21GB
353 </select>
354 </td>
355 </tr>
356 <tr>
357 <td>HDD Bank No.<br><input type="text" name="id_27" size=25 value="0"></td>
358 </tr>
359 <tr>
360 <td><font size="+1">ATAPI Emulator</font></td>
361 <td>Port No. for ATAPI Emulator<br><input type="text" name="id_13" maxlength=5 size=25 value="7974"></td>
362 </tr>
363 <tr>
364 <td><font size="+1">SDIO Printf/Control</font></td>
365 <td>Port No. for SDIO Printf/Control<br><input type="text" name="id_14" maxlength=5 size=25 value="7975"></td>
366 </tr>
367 <tr>
368 <td><font size="+1">SDIO Block Data</font></td>
369 <td>Port No. for SDIO Block Data<br><input type="text" name="id_15" maxlength=5 size=25 value="7976"></td>
370 </tr>
371 <tr>
372 <td><font size="+1">EXI</font></td>
373 <td>Port No. for EXI<br><input type="text" name="id_16" maxlength=5 size=25 value="7977"></td>
374 </tr>
375 <tr>
376 <td><font size="+1">Parameter Space</font></td>
377 <td>Port No. for Parameter Space<br><input type="text" name="id_17" maxlength=5 size=25 value="7978"></td>
378 </tr>
379 <tr>
380 <td><font size="+1">Drive Setting</font></td>
381 <td>
382 Drive Timing Emulation<br>
383 <input type="radio" name="id_33" value="1">enable
384 <input type="radio" name="id_33" value="0" checked>disable
385 </td>
386 </tr>
387 <tr>
388 <td><font size="+1">Operational Mode</font></td>
389 <td>
390 Operational Mode<br>
391 <input type="radio" name="id_32" value="0" checked>CAT-DEV Mode
392 <input type="radio" name="id_32" value="1">H-Reader Mode
393 </td>
394 </tr>
395 <tr>
396 <td rowspan=4><font size="+1">Drive Information</font></td>
397 <td>Product Revision<br><input type="text" name="id_28" maxlength=4 size=25 value="0000">(HEX)</td>
398 </tr>
399 <tr>
400 <td>Vendor Code<br><input type="text" name="id_29" maxlength=2 size=25 value="02">(HEX)</td>
401 </tr>
402 <tr>
403 <td>Device Code<br><input type="text" name="id_30" maxlength=2 size=25 value="06">(HEX)</td>
404 </tr>
405 <tr>
406 <td> Release Date<br><input type="text" name="id_31" maxlength=8 size=25 value="20100430">(HEX)</td>
407 </tr>
408 <tr>
409 <td rowspan=2><font size="+1">Device Unique</font></td>
410 <td>Machine Name<br><textarea name="id_2" cols=50 rows=3>00-25-5C-BA-5A-00</textarea></td>
411 </tr>
412 <tr>
413 <td>
414 MAC Address<br>
415 00-25-5C-BA-5A-00
416 </td>
417 </tr>
418 <tr>
419 <td colspan=2>
420 <div align="center">
421 <input type="hidden" name="op" value="0">
422 <input type="submit" value="Write"> <input type="reset" value="Form Reset">
423 </div>
424 </td>
425 </tr>
426</table>
427</form>
428<form method="POST" name="param_read" action="setup.cgi">
429 <input type="hidden" name="op" value="1">
430 <div align="center">
431 <input type="submit" value="Read">
432 </div>
433</form>
434</div>
435<br>
436<hr>
437<a href="../">Homepage</a>
438</body>
439</HTML>"##;
440
441 #[test]
442 pub fn can_parse_setup_cgis() {
443 let result = parse_response_body_from_setup(REAL_WORLD_SETUP_RESPONSE);
444 assert!(
445 result.is_ok(),
446 "Failed to parse response body from `setup.cgi`:\n {:?}",
447 result,
448 );
449 let parameters = result.expect("impossible probably, past assert.");
450
451 assert!(
452 parameters.static_ip_address().is_none(),
453 "Expected static ip address to be empty, is using DHCP",
454 );
455 assert_eq!(
456 parameters.raw_static_ip_address(),
457 "192.168.0.1"
458 .parse::<Ipv4Addr>()
459 .expect("Failed to parse test IP Address"),
460 );
461 assert_eq!(
462 parameters.subnet_mask(),
463 "255.255.255.0"
464 .parse::<Ipv4Addr>()
465 .expect("Failed to parse test IP Address"),
466 );
467 assert_eq!(
468 parameters.default_gateway(),
469 "0.0.0.0"
470 .parse::<Ipv4Addr>()
471 .expect("Failed to parse test IP Address"),
472 );
473
474 assert!(parameters.using_dhcp());
475 assert!(!parameters.using_self_managed_dns());
476
477 assert!(
478 parameters.primary_dns().is_none(),
479 "Expected Primary DNS to be empty, is not using self-managed DNS",
480 );
481 assert!(
482 parameters.secondary_dns().is_none(),
483 "Expected Primary DNS to be empty, is not using self-managed DNS",
484 );
485 assert_eq!(
486 parameters.raw_primary_dns(),
487 "0.0.0.0"
488 .parse::<Ipv4Addr>()
489 .expect("Failed to parse static IP Address for testing")
490 );
491 assert_eq!(
492 parameters.raw_secondary_dns(),
493 "0.0.0.0"
494 .parse::<Ipv4Addr>()
495 .expect("Failed to parse static IP Address for testing")
496 );
497
498 assert!(parameters.jumbo_frame());
499
500 assert_eq!(
501 parameters.host_pc_ip_address(),
502 "0.0.0.0"
503 .parse::<Ipv4Addr>()
504 .expect("Failed to parse static IP Address for testing")
505 );
506 assert_eq!(parameters.hdd_bank_size(), CatDevBankSize::TwentyFiveGbs);
507 assert_eq!(parameters.hdd_bank_no(), 0);
508
509 assert_eq!(parameters.atapi_emulator_port(), 7974);
510 assert_eq!(parameters.sdio_printf_port(), 7975);
511 assert_eq!(parameters.sdio_block_port(), 7976);
512 assert_eq!(parameters.exi_port(), 7977);
513 assert_eq!(parameters.parameter_space_port(), 7978);
514
515 assert!(!parameters.drive_timing_emulation_enabled());
516
517 assert!(parameters.is_cat_dev_mode());
518 assert!(!parameters.is_h_reader_mode());
519
520 assert_eq!(parameters.drive_product_revision(), "0000");
521 assert_eq!(parameters.drive_vendor_code(), "02");
522 assert_eq!(parameters.drive_device_code(), "06");
523 assert_eq!(parameters.drive_release_date(), "20100430");
524
525 assert_eq!(parameters.device_name(), "00-25-5C-BA-5A-00");
526 assert_eq!(
527 parameters.mac_address(),
528 "00-25-5C-BA-5A-00"
529 .parse::<MacAddress>()
530 .expect("Failed to parse static MacAddress for tests"),
531 );
532 }
533}