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