1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
4use std::{fmt::Display, iter, num::NonZeroU16};
5
6use either::Either;
7use itertools::Itertools as _;
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
20#[serde(try_from = "ListenSerde", into = "ListenSerde")]
21#[derive(Default)]
22pub struct Listen(Vec<ListenItem>);
23
24impl Listen {
25 pub fn new_none() -> Listen {
27 Listen(vec![])
28 }
29
30 pub fn new_localhost(port: u16) -> Listen {
34 Listen(
35 port.try_into()
36 .ok()
37 .map(ListenItem::Localhost)
38 .into_iter()
39 .collect_vec(),
40 )
41 }
42
43 pub fn new_localhost_optional(port: Option<u16>) -> Listen {
47 Self::new_localhost(port.unwrap_or_default())
48 }
49
50 pub fn is_empty(&self) -> bool {
52 self.0.is_empty()
53 }
54
55 pub fn ip_addrs(
68 &self,
69 ) -> Result<impl Iterator<Item = impl Iterator<Item = SocketAddr> + '_> + '_, ListenUnsupported>
70 {
71 Ok(self.0.iter().map(|i| i.iter()))
72 }
73
74 pub fn localhost_port_legacy(&self) -> Result<Option<u16>, ListenUnsupported> {
81 use ListenItem as LI;
82 Ok(match &*self.0 {
83 [] => None,
84 [LI::Localhost(port)] => Some((*port).into()),
85 _ => return Err(ListenUnsupported {}),
86 })
87 }
88
89 pub fn single_address_legacy(&self) -> Result<Option<SocketAddr>, ListenUnsupported> {
102 use ListenItem as LI;
103 Ok(match &*self.0 {
104 [] => None,
105 [LI::Localhost(port)] => Some((Ipv4Addr::LOCALHOST, u16::from(*port)).into()),
106 [LI::General(sa)] => Some(*sa),
107 _ => return Err(ListenUnsupported {}),
108 })
109 }
110
111 pub fn is_localhost_only(&self) -> bool {
113 self.0.iter().all(ListenItem::is_localhost)
114 }
115}
116
117impl Display for Listen {
118 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
119 let mut sep = "";
120 for a in &self.0 {
121 write!(f, "{sep}{a}")?;
122 sep = ", ";
123 }
124 Ok(())
125 }
126}
127#[derive(thiserror::Error, Debug, Clone)]
129#[non_exhaustive]
130#[error("Unsupported listening configuration")]
131pub struct ListenUnsupported {}
132
133#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
140#[serde(untagged)]
141enum ListenItem {
142 Localhost(NonZeroU16),
144
145 General(SocketAddr),
147}
148
149impl ListenItem {
150 fn iter(&self) -> impl Iterator<Item = SocketAddr> + '_ {
152 use ListenItem as LI;
153 match self {
154 &LI::Localhost(port) => Either::Left({
155 let port = port.into();
156 let addrs: [IpAddr; 2] = [Ipv6Addr::LOCALHOST.into(), Ipv4Addr::LOCALHOST.into()];
157 addrs.into_iter().map(move |ip| SocketAddr::new(ip, port))
158 }),
159 LI::General(addr) => Either::Right(iter::once(addr).cloned()),
160 }
161 }
162
163 fn is_localhost(&self) -> bool {
165 use ListenItem as LI;
166 match self {
167 LI::Localhost(_) => true,
168 LI::General(addr) => addr.ip().is_loopback(),
169 }
170 }
171}
172
173impl Display for ListenItem {
174 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
175 match self {
176 ListenItem::Localhost(port) => write!(f, "localhost port {}", port)?,
177 ListenItem::General(addr) => write!(f, "{}", addr)?,
178 }
179 Ok(())
180 }
181}
182#[derive(Serialize, Deserialize)]
184#[serde(untagged)]
185enum ListenSerde {
186 Bool(bool),
188
189 One(ListenItemSerde),
191
192 List(Vec<ListenItemSerde>),
194}
195
196#[derive(Serialize, Deserialize)]
198#[serde(untagged)]
199enum ListenItemSerde {
200 Port(u16),
204
205 String(String),
209}
210
211#[allow(clippy::fallible_impl_from)]
214impl From<Listen> for ListenSerde {
215 fn from(l: Listen) -> ListenSerde {
216 let l = l.0;
217 match l.len() {
218 0 => ListenSerde::Bool(false),
219 1 => ListenSerde::One(l.into_iter().next().expect("len=1 but no next").into()),
220 _ => ListenSerde::List(l.into_iter().map(Into::into).collect()),
221 }
222 }
223}
224impl From<ListenItem> for ListenItemSerde {
225 fn from(i: ListenItem) -> ListenItemSerde {
226 use ListenItem as LI;
227 use ListenItemSerde as LIS;
228 match i {
229 LI::Localhost(port) => LIS::Port(port.into()),
230 LI::General(addr) => LIS::String(addr.to_string()),
231 }
232 }
233}
234
235#[derive(thiserror::Error, Debug, Clone)]
237#[non_exhaustive]
238pub enum InvalidListen {
239 #[error("Invalid listen specification: need actual addr/port, or `false`; not `true`")]
241 InvalidBool,
242
243 #[error("Invalid listen specification: failed to parse string: {0}")]
245 InvalidString(#[from] std::net::AddrParseError),
246
247 #[error("Invalid listen specification: zero (for no port) not permitted in list")]
249 ZeroPortInList,
250}
251impl TryFrom<ListenSerde> for Listen {
252 type Error = InvalidListen;
253
254 fn try_from(l: ListenSerde) -> Result<Listen, Self::Error> {
255 use ListenSerde as LS;
256 Ok(Listen(match l {
257 LS::Bool(false) => vec![],
258 LS::Bool(true) => return Err(InvalidListen::InvalidBool),
259 LS::One(i) if i.means_none() => vec![],
260 LS::One(i) => vec![i.try_into()?],
261 LS::List(l) => l.into_iter().map(|i| i.try_into()).try_collect()?,
262 }))
263 }
264}
265impl ListenItemSerde {
266 fn means_none(&self) -> bool {
270 use ListenItemSerde as LIS;
271 match self {
272 &LIS::Port(port) => port == 0,
273 LIS::String(s) => s.is_empty(),
274 }
275 }
276}
277impl TryFrom<ListenItemSerde> for ListenItem {
278 type Error = InvalidListen;
279
280 fn try_from(i: ListenItemSerde) -> Result<ListenItem, Self::Error> {
281 use ListenItem as LI;
282 use ListenItemSerde as LIS;
283 Ok(match i {
284 LIS::String(s) => LI::General(s.parse()?),
285 LIS::Port(p) => LI::Localhost(p.try_into().map_err(|_| InvalidListen::ZeroPortInList)?),
286 })
287 }
288}
289
290#[cfg(test)]
291mod test {
292 #![allow(clippy::bool_assert_comparison)]
294 #![allow(clippy::clone_on_copy)]
295 #![allow(clippy::dbg_macro)]
296 #![allow(clippy::mixed_attributes_style)]
297 #![allow(clippy::print_stderr)]
298 #![allow(clippy::print_stdout)]
299 #![allow(clippy::single_char_pattern)]
300 #![allow(clippy::unwrap_used)]
301 #![allow(clippy::unchecked_duration_subtraction)]
302 #![allow(clippy::useless_vec)]
303 #![allow(clippy::needless_pass_by_value)]
304 use super::*;
306
307 #[derive(Debug, Default, Deserialize, Serialize)]
308 struct TestConfigFile {
309 #[serde(default)]
310 listen: Option<Listen>,
311 }
312
313 #[test]
314 fn listen_parse() {
315 use ListenItem as LI;
316
317 let localhost6 = |p| SocketAddr::new(Ipv6Addr::LOCALHOST.into(), p);
318 let localhost4 = |p| SocketAddr::new(Ipv4Addr::LOCALHOST.into(), p);
319 let unspec6 = |p| SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), p);
320
321 #[allow(clippy::needless_pass_by_value)] fn chk(
323 exp_i: Vec<ListenItem>,
324 exp_addrs: Result<Vec<Vec<SocketAddr>>, ()>,
325 exp_lpd: Result<Option<u16>, ()>,
326 s: &str,
327 ) {
328 let tc: TestConfigFile = toml::from_str(s).expect(s);
329 let ll = tc.listen.unwrap();
330 eprintln!("s={:?} ll={:?}", &s, &ll);
331 assert_eq!(ll, Listen(exp_i));
332 assert_eq!(
333 ll.ip_addrs()
334 .map(|a| a.map(|l| l.collect_vec()).collect_vec())
335 .map_err(|_| ()),
336 exp_addrs
337 );
338 assert_eq!(ll.localhost_port_legacy().map_err(|_| ()), exp_lpd);
339 }
340
341 let chk_err = |exp, s: &str| {
342 let got: Result<TestConfigFile, _> = toml::from_str(s);
343 let got = got.expect_err(s).to_string();
344 assert!(got.contains(exp), "s={:?} got={:?} exp={:?}", s, got, exp);
345 };
346
347 let chk_none = |s: &str| {
348 chk(vec![], Ok(vec![]), Ok(None), &format!("listen = {}", s));
349 chk_err(
350 "", &format!("listen = [ {} ]", s),
352 );
353 };
354
355 let chk_1 = |v: ListenItem, addrs: Vec<Vec<SocketAddr>>, port, s| {
356 chk(
357 vec![v.clone()],
358 Ok(addrs.clone()),
359 port,
360 &format!("listen = {}", s),
361 );
362 chk(
363 vec![v.clone()],
364 Ok(addrs.clone()),
365 port,
366 &format!("listen = [ {} ]", s),
367 );
368 chk(
369 vec![v, LI::Localhost(23.try_into().unwrap())],
370 Ok([addrs, vec![vec![localhost6(23), localhost4(23)]]]
371 .into_iter()
372 .flatten()
373 .collect()),
374 Err(()),
375 &format!("listen = [ {}, 23 ]", s),
376 );
377 };
378
379 chk_none(r#""""#);
380 chk_none(r#"0"#);
381 chk_none(r#"false"#);
382 chk(vec![], Ok(vec![]), Ok(None), r#"listen = []"#);
383
384 chk_1(
385 LI::Localhost(42.try_into().unwrap()),
386 vec![vec![localhost6(42), localhost4(42)]],
387 Ok(Some(42)),
388 "42",
389 );
390 chk_1(
391 LI::General(unspec6(56)),
392 vec![vec![unspec6(56)]],
393 Err(()),
394 r#""[::]:56""#,
395 );
396
397 let chk_err_1 = |e, el, s| {
398 chk_err(e, &format!("listen = {}", s));
399 chk_err(el, &format!("listen = [ {} ]", s));
400 chk_err(el, &format!("listen = [ 23, {}, 77 ]", s));
401 };
402
403 chk_err_1("need actual addr/port", "did not match any variant", "true");
404 chk_err("did not match any variant", r#"listen = [ [] ]"#);
405 }
406
407 #[test]
408 fn display_listen() {
409 let empty = Listen::new_none();
410 assert_eq!(empty.to_string(), "");
411
412 let one_port = Listen::new_localhost(1234);
413 assert_eq!(one_port.to_string(), "localhost port 1234");
414
415 let multi_port = Listen(vec![
416 ListenItem::Localhost(1111.try_into().unwrap()),
417 ListenItem::Localhost(2222.try_into().unwrap()),
418 ]);
419 assert_eq!(
420 multi_port.to_string(),
421 "localhost port 1111, localhost port 2222"
422 );
423
424 let multi_addr = Listen(vec![
425 ListenItem::Localhost(1234.try_into().unwrap()),
426 ListenItem::General("1.2.3.4:5678".parse().unwrap()),
427 ]);
428 assert_eq!(multi_addr.to_string(), "localhost port 1234, 1.2.3.4:5678");
429 }
430
431 #[test]
432 fn is_localhost() {
433 fn localhost_only(s: &str) -> bool {
434 let tc: TestConfigFile = toml::from_str(s).expect(s);
435 tc.listen.unwrap().is_localhost_only()
436 }
437
438 assert_eq!(localhost_only(r#"listen = [ ]"#), true);
439 assert_eq!(localhost_only(r#"listen = [ 3 ]"#), true);
440 assert_eq!(localhost_only(r#"listen = [ 3, 10 ]"#), true);
441 assert_eq!(localhost_only(r#"listen = [ "127.0.0.1:9050" ]"#), true);
442 assert_eq!(localhost_only(r#"listen = [ "[::1]:9050" ]"#), true);
443 assert_eq!(
444 localhost_only(r#"listen = [ "[::1]:9050", "192.168.0.1:1234" ]"#),
445 false
446 );
447 assert_eq!(localhost_only(r#"listen = [ "192.168.0.1:1234" ]"#), false);
448 }
449}