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