1mod scanner;
25
26static DOMAIN_REGEX: &str =
27 r"^(([a-zA-Z_\-]{1,63}\.)*?)*?([a-zA-Z_\-]{1,63})(\.[a-zA-Z_\-]{1,63})?$";
28
29#[derive(Debug, PartialEq, Clone)]
38pub enum ToCheck {
39 HostnameAndPort(String, u16),
41
42 #[cfg(feature = "http")]
43 #[allow(rustdoc::bare_urls)]
44 HttpOrHttpsUrl(hyper::Uri),
46}
47
48impl std::fmt::Display for ToCheck {
49 #[cfg(feature = "http")]
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 Self::HostnameAndPort(domain, port) => format!("{domain}:{port}").fmt(f),
53 Self::HttpOrHttpsUrl(uri) => uri.fmt(f),
54 }
55 }
56
57 #[cfg(not(feature = "http"))]
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 Self::HostnameAndPort(domain, port) => format!("{}:{}", domain, port).fmt(f),
61 }
62 }
63}
64
65impl ToCheck {
66 fn from_host_and_port(domain_and_port: &str) -> Result<Self, String> {
67 let parts: Vec<String> = domain_and_port.split(':').map(String::from).collect();
68 if parts.len() != 2 {
69 return Err(format!(
70 "'{domain_and_port}' doesn't match <hostname>:<port> pattern"
71 ));
72 }
73
74 let port: u16 = parts[1]
76 .parse()
77 .map_err(|err| format!("'{domain_and_port}', port error: {err}"))?;
78
79 if port == 0 {
80 return Err("dynamic port number (0) can't be used here".into());
81 }
82
83 let hostname = parts[0].clone();
85 let regex = regex::Regex::new(DOMAIN_REGEX).unwrap();
86 let ip: Result<std::net::IpAddr, _> = hostname.parse();
87
88 if !regex.is_match(&hostname) && ip.is_err() {
89 return Err(format!("'{hostname}' is not a valid hostname"));
90 }
91 Ok(Self::HostnameAndPort(hostname, port))
92 }
93
94 #[cfg(feature = "http")]
95 fn from_http_url(http_url: &str) -> Result<Self, String> {
96 Ok(Self::HttpOrHttpsUrl(
97 http_url.parse::<hyper::Uri>().map_err(|e| e.to_string())?,
98 ))
99 }
100
101 #[cfg(not(feature = "http"))]
102 fn from_http_url(_uri: &str) -> Result<Self, String> {
103 panic!("Not compiled with 'http' feature")
104 }
105}
106
107impl std::str::FromStr for ToCheck {
108 type Err = String;
109
110 #[cfg(feature = "http")]
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 if s.starts_with("http://") || s.starts_with("https://") {
113 Self::from_http_url(s)
114 } else {
115 Self::from_host_and_port(s)
116 }
117 }
118
119 #[cfg(not(feature = "http"))]
120 fn from_str(s: &str) -> Result<Self, Self::Err> {
121 Self::from_host_and_port(s)
122 }
123}
124
125pub async fn wait_for_them(
139 hosts_ports_or_http_urls: &[ToCheck],
140 timeout: Option<u64>,
141 start_time: Option<std::time::Instant>,
142 silent: bool,
143) -> Vec<Option<u64>> {
144 let start_time = start_time.unwrap_or_else(std::time::Instant::now);
145 let futures = if silent {
146 scanner::wait_silent(hosts_ports_or_http_urls, timeout, start_time)
147 } else {
148 scanner::wait(hosts_ports_or_http_urls, timeout, start_time)
149 };
150
151 futures::future::join_all(futures).await
152}