Skip to main content

cat_dev/mion/cgis/
setup.rs

1//! APIs for parsing the data from `/setup.cgi`, an HTTP interface usually
2//! only meant for humans.
3
4use 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
15/// Get the setup parameters from a particular MION IP.
16///
17/// ## Errors
18///
19/// If we cannot make a network request to the `setup.cgi` page, or get some
20/// sort of error condition back.
21pub 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
28/// Get the setup parameters from a particular MION IP, but with an existing
29/// HTTP client.
30///
31/// ## Errors
32///
33/// If we cannot make a network request to the `setup.cgi` page, or get some
34/// sort of error condition back.
35pub 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
52/// Parse the actual setup parameters from an HTML body.
53///
54/// This is seperated into it's own method for testability.
55///
56/// ## Errors
57///
58/// - If we can't parse the response as the HTML format MIONs should respond
59///   with.
60/// - If the type of inputs were incorrect.
61fn 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("&nbsp;", "")
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	/// A real world response to the MION setup page.
287	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">&nbsp;
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   &nbsp;&nbsp;&nbsp;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">&nbsp;&nbsp;<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}