1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
4use std::{fmt::Display, iter};
5
6use either::Either;
7use itertools::Itertools as _;
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
24#[serde(try_from = "CustomizableListen", into = "CustomizableListen")]
25pub struct Listen(CustomizableListen);
26
27impl Listen {
28 pub fn new_none() -> Listen {
30 CustomizableListen::Disabled
31 .try_into()
32 .expect("'disabled' should be valid")
33 }
34
35 pub fn new_localhost(port: u16) -> Listen {
39 CustomizableListen::One(ListenItem::Port(port))
40 .try_into()
41 .expect("a standalone port (including 0) should be valid")
42 }
43
44 pub fn new_localhost_optional(port: Option<u16>) -> Listen {
48 Self::new_localhost(port.unwrap_or_default())
49 }
50
51 pub fn is_empty(&self) -> bool {
53 self.ip_addrs_internal().count() == 0
54 }
55
56 pub fn using_auto(&self) -> bool {
60 self.0.items().any(ListenItem::is_auto)
61 }
62
63 pub fn using_port_zero(&self) -> bool {
67 self.0.items().any(ListenItem::is_port_zero)
68 }
69
70 pub fn ip_addrs(
83 &self,
84 ) -> Result<impl Iterator<Item = impl Iterator<Item = SocketAddr> + '_> + '_, ListenUnsupported>
85 {
86 Ok(self.ip_addrs_internal())
87 }
88
89 fn ip_addrs_internal(
93 &self,
94 ) -> impl Iterator<Item = impl Iterator<Item = SocketAddr> + '_> + '_ {
95 let ips = [Ipv6Addr::LOCALHOST.into(), Ipv4Addr::LOCALHOST.into()];
97 self.0.items().map(move |item| item.iter(ips))
98 }
99
100 #[deprecated(since = "0.38.0")]
107 pub fn localhost_port_legacy(&self) -> Result<Option<u16>, ListenUnsupported> {
108 Ok(match self.to_singleton_legacy()? {
109 None => None,
110 Some(ListenItem::Port(port)) => Some(*port),
111 Some(ListenItem::Auto) => return Err(ListenUnsupported {}),
112 Some(ListenItem::AutoPort(_)) => return Err(ListenUnsupported {}),
113 Some(ListenItem::General(_)) => return Err(ListenUnsupported {}),
114 })
115 }
116
117 pub fn single_address_legacy(&self) -> Result<Option<SocketAddr>, ListenUnsupported> {
130 Ok(match self.to_singleton_legacy()? {
131 None => None,
132 Some(ListenItem::Port(port)) => Some((Ipv4Addr::LOCALHOST, *port).into()),
133 Some(ListenItem::Auto) => Some((Ipv4Addr::LOCALHOST, 0).into()),
134 Some(ListenItem::AutoPort(addr)) => Some((*addr, 0).into()),
135 Some(ListenItem::General(addr)) => Some(*addr),
136 })
137 }
138
139 fn to_singleton_legacy(&self) -> Result<Option<&ListenItem>, ListenUnsupported> {
145 use CustomizableListen as CL;
146 match &self.0 {
147 CL::Disabled => Ok(None),
148 CL::One(li) => Ok(Some(li)),
149 CL::List(lst) => match lst.as_slice() {
150 [] => Ok(None),
151 [li] => Ok(Some(li)),
152 [_, _, ..] => Err(ListenUnsupported {}),
153 },
154 }
155 }
156
157 pub fn is_loopback_only(&self) -> bool {
162 self.ip_addrs_internal()
163 .flatten()
164 .all(|a| a.ip().is_loopback())
165 }
166
167 #[deprecated(since = "0.37.0", note = "please use `is_loopback_only` instead")]
171 pub fn is_localhost_only(&self) -> bool {
172 self.is_loopback_only()
173 }
174}
175
176impl Default for Listen {
177 fn default() -> Self {
178 Self::new_none()
179 }
180}
181
182impl Display for Listen {
183 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
184 let mut sep = "";
185 for item in self.0.items() {
186 match item {
187 ListenItem::Port(_) => {
188 write!(f, "{sep}localhost {item}")?;
189 sep = ", ";
190 }
191 _other => {
192 write!(f, "{sep}{item}")?;
193 sep = ", ";
194 }
195 }
196 }
197 Ok(())
198 }
199}
200
201impl TryFrom<CustomizableListen> for Listen {
202 type Error = InvalidListen;
203
204 fn try_from(l: CustomizableListen) -> Result<Self, Self::Error> {
205 match &l {
206 CustomizableListen::Disabled | CustomizableListen::One(ListenItem::Port(0)) => {
207 Ok(Self(CustomizableListen::Disabled))
209 }
210 CustomizableListen::One(li) => {
211 if li.is_port_zero() {
212 tracing::warn!(
213 "Configured to listen on port zero via {li}. \
214 This is deprecated. Instead, replace '0' with 'auto'."
215 );
216 }
217 Ok(Self(l))
218 }
219 CustomizableListen::List(list) => {
220 if list.iter().any(|item| matches!(item, ListenItem::Port(0))) {
222 return Err(InvalidListen::ZeroPortInList);
223 }
224 if let Some(zero_li) = list.iter().find(|li| li.is_port_zero()) {
226 tracing::warn!(
227 "Configured to listen on port zero via {zero_li}. \
228 This is deprecated. Instead, replace '0' with 'auto'."
229 );
230 }
231 Ok(Self(l))
232 }
233 }
234 }
235}
236
237impl From<Listen> for CustomizableListen {
238 fn from(l: Listen) -> Self {
239 l.0
240 }
241}
242
243#[derive(thiserror::Error, Debug, Clone)]
245#[non_exhaustive]
246#[error("Unsupported listening configuration")]
247pub struct ListenUnsupported {}
248
249#[derive(thiserror::Error, Debug, Clone)]
251#[non_exhaustive]
252enum InvalidListen {
253 #[error("Invalid listen specification: zero (for no port) not permitted in list")]
255 ZeroPortInList,
256}
257
258#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
266#[serde(try_from = "ListenSerde", into = "ListenSerde")]
267enum CustomizableListen {
268 Disabled,
270 One(ListenItem),
272 List(Vec<ListenItem>),
274}
275
276impl CustomizableListen {
277 fn items(&self) -> impl Iterator<Item = &ListenItem> {
279 match self {
280 Self::Disabled => Either::Right(std::slice::Iter::default()),
281 Self::One(one) => Either::Left(iter::once(one)),
282 Self::List(many) => Either::Right(many.iter()),
283 }
284 }
285}
286
287#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq)]
302#[allow(clippy::exhaustive_enums)]
305enum ListenItem {
306 Port(u16),
308
309 Auto,
311
312 AutoPort(IpAddr),
314
315 General(SocketAddr),
317}
318
319impl ListenItem {
320 fn iter<'a>(
325 &'a self,
326 ips_for_port: impl IntoIterator<Item = IpAddr> + 'a,
327 ) -> impl Iterator<Item = SocketAddr> + 'a {
328 use ListenItem as LI;
329 let with_ips = |portnum| {
330 Either::Left({
331 ips_for_port
332 .into_iter()
333 .map(move |ip| SocketAddr::new(ip, portnum))
334 })
335 };
336
337 match self {
338 &LI::Port(port) => with_ips(port),
339 LI::Auto => with_ips(0),
340 LI::AutoPort(addr) => Either::Right(iter::once((*addr, 0).into())),
341 LI::General(addr) => Either::Right(iter::once(*addr)),
342 }
343 }
344
345 fn is_auto(&self) -> bool {
347 use ListenItem as LI;
348 match self {
349 LI::Port(_) => false,
350 LI::Auto => true,
351 LI::AutoPort(_) => true,
352 LI::General(_) => false,
353 }
354 }
355
356 fn is_port_zero(&self) -> bool {
358 use ListenItem as LI;
359
360 match self {
361 LI::Port(port) => *port == 0,
362 LI::Auto => false,
363 LI::AutoPort(_) => false,
364 LI::General(addr) => addr.port() == 0,
365 }
366 }
367}
368
369impl Display for ListenItem {
370 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
371 match self {
372 ListenItem::Port(port) => write!(f, "port {}", port)?,
373 ListenItem::Auto => write!(f, "auto")?,
374 ListenItem::AutoPort(addr) => write!(f, "{addr}:auto")?,
375 ListenItem::General(addr) => write!(f, "{}", addr)?,
376 }
377 Ok(())
378 }
379}
380#[derive(Serialize, Deserialize)]
382#[serde(untagged)]
383#[serde(expecting = "value was not a bool, `u16` integer, string, or list of integers/strings")]
385enum ListenSerde {
386 Bool(bool),
388
389 One(ListenItemSerde),
391
392 List(Vec<ListenItemSerde>),
394}
395
396#[derive(Serialize, Deserialize)]
398#[serde(untagged)]
399#[serde(expecting = "item was not a `u16` integer or string")]
401enum ListenItemSerde {
402 Port(u16),
404
405 String(String),
407}
408
409impl From<CustomizableListen> for ListenSerde {
410 fn from(l: CustomizableListen) -> Self {
411 match l {
412 CustomizableListen::Disabled => ListenSerde::Bool(false),
413 CustomizableListen::One(item) => ListenSerde::One(item.into()),
414 CustomizableListen::List(list) => match list.as_slice() {
415 [] => ListenSerde::List(Vec::new()),
416 [one] => ListenSerde::List(vec![one.clone().into()]),
417 list => ListenSerde::List(list.iter().cloned().map(Into::into).collect()),
418 },
419 }
420 }
421}
422impl From<ListenItem> for ListenItemSerde {
423 fn from(i: ListenItem) -> Self {
424 use ListenItem as LI;
425 match i {
426 LI::Port(port) => Self::Port(port),
427 LI::Auto => Self::String("auto".to_string()),
428 LI::AutoPort(addr) => Self::String(format!("{addr}:auto")),
429 LI::General(addr) => Self::String(addr.to_string()),
430 }
431 }
432}
433
434#[derive(thiserror::Error, Debug, Clone)]
436#[non_exhaustive]
437enum InvalidCustomizableListen {
438 #[error("Invalid listen specification: need actual addr/port, or `false`; not `true`")]
440 InvalidBool,
441
442 #[error("Invalid listen specification: failed to parse string: {0}")]
444 InvalidString(#[from] std::net::AddrParseError),
445}
446impl TryFrom<ListenSerde> for CustomizableListen {
447 type Error = InvalidCustomizableListen;
448
449 fn try_from(l: ListenSerde) -> Result<CustomizableListen, Self::Error> {
450 use ListenSerde as LS;
451 Ok(match l {
452 LS::Bool(false) => CustomizableListen::Disabled,
454 LS::Bool(true) => return Err(InvalidCustomizableListen::InvalidBool),
455 LS::One(ListenItemSerde::String(s)) if s.is_empty() => CustomizableListen::List(vec![]),
457 LS::One(i) => CustomizableListen::One(i.try_into()?),
458 LS::List(l) => {
459 CustomizableListen::List(l.into_iter().map(|i| i.try_into()).try_collect()?)
460 }
461 })
462 }
463}
464impl ListenItemSerde {}
465impl TryFrom<ListenItemSerde> for ListenItem {
466 type Error = InvalidCustomizableListen;
467
468 fn try_from(i: ListenItemSerde) -> Result<ListenItem, Self::Error> {
469 use ListenItem as LI;
470 use ListenItemSerde as LIS;
471 Ok(match i {
472 LIS::String(a) => {
473 if a == "auto" {
474 LI::Auto
475 } else if let Some(ip_addr) = a.strip_suffix(":auto") {
476 LI::AutoPort(ip_addr.parse()?)
477 } else {
478 LI::General(a.parse()?)
479 }
480 }
481 LIS::Port(p) => LI::Port(p),
482 })
483 }
484}
485
486#[cfg(test)]
487mod test {
488 #![allow(clippy::bool_assert_comparison)]
490 #![allow(clippy::clone_on_copy)]
491 #![allow(clippy::dbg_macro)]
492 #![allow(clippy::mixed_attributes_style)]
493 #![allow(clippy::print_stderr)]
494 #![allow(clippy::print_stdout)]
495 #![allow(clippy::single_char_pattern)]
496 #![allow(clippy::unwrap_used)]
497 #![allow(clippy::unchecked_time_subtraction)]
498 #![allow(clippy::useless_vec)]
499 #![allow(clippy::needless_pass_by_value)]
500 use super::*;
502
503 #[derive(Debug, Default, Deserialize, Serialize)]
504 struct TestConfigFile {
505 #[serde(default)]
506 listen: Option<Listen>,
507 }
508
509 #[test]
510 fn listen_parse() {
511 use ListenItem as LI;
512
513 let localhost6 = |p| SocketAddr::new(Ipv6Addr::LOCALHOST.into(), p);
514 let localhost4 = |p| SocketAddr::new(Ipv4Addr::LOCALHOST.into(), p);
515 let unspec6 = |p| SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), p);
516
517 #[allow(clippy::needless_pass_by_value)] fn chk(
519 exp_i: Vec<ListenItem>,
520 exp_addrs: Result<Vec<Vec<SocketAddr>>, ()>,
521 exp_lpd: Result<Option<u16>, ()>,
522 s: &str,
523 ) {
524 let tc: TestConfigFile = toml::from_str(s).expect(s);
525 let ll = tc.listen.unwrap();
526 eprintln!("s={:?} ll={:?}", &s, &ll);
527 assert_eq!(ll.0.items().cloned().collect::<Vec<_>>(), exp_i);
528 assert_eq!(
529 ll.ip_addrs()
530 .map(|a| a.map(|l| l.collect_vec()).collect_vec())
531 .map_err(|_| ()),
532 exp_addrs
533 );
534 #[allow(deprecated)]
535 {
536 assert_eq!(ll.localhost_port_legacy().map_err(|_| ()), exp_lpd);
537 }
538 }
539
540 let chk_err = |exp, s: &str| {
541 let got: Result<TestConfigFile, _> = toml::from_str(s);
542 let got = got.expect_err(s).to_string();
543 assert!(got.contains(exp), "s={:?} got={:?} exp={:?}", s, got, exp);
544 };
545
546 let chk_none = |s: &str| {
547 chk(vec![], Ok(vec![]), Ok(None), &format!("listen = {}", s));
548 chk_err(
549 "", &format!("listen = [ {} ]", s),
551 );
552 };
553
554 let chk_1 = |v: ListenItem, addrs: Vec<Vec<SocketAddr>>, port, s| {
555 chk(
556 vec![v.clone()],
557 Ok(addrs.clone()),
558 port,
559 &format!("listen = {}", s),
560 );
561 chk(
562 vec![v.clone()],
563 Ok(addrs.clone()),
564 port,
565 &format!("listen = [ {} ]", s),
566 );
567 chk(
568 vec![v, LI::Port(23.try_into().unwrap())],
569 Ok([addrs, vec![vec![localhost6(23), localhost4(23)]]]
570 .into_iter()
571 .flatten()
572 .collect()),
573 Err(()),
574 &format!("listen = [ {}, 23 ]", s),
575 );
576 };
577
578 chk_none(r#""""#);
579 chk_none(r#"0"#);
580 chk_none(r#"false"#);
581 chk(vec![], Ok(vec![]), Ok(None), r#"listen = []"#);
582
583 chk_1(
584 LI::Port(42.try_into().unwrap()),
585 vec![vec![localhost6(42), localhost4(42)]],
586 Ok(Some(42)),
587 "42",
588 );
589 chk_1(
590 LI::General(unspec6(56)),
591 vec![vec![unspec6(56)]],
592 Err(()),
593 r#""[::]:56""#,
594 );
595
596 let chk_err_1 = |e, el, s| {
597 chk_err(e, &format!("listen = {}", s));
598 chk_err(el, &format!("listen = [ {} ]", s));
599 chk_err(el, &format!("listen = [ 23, {}, 77 ]", s));
600 };
601
602 chk_err_1(
603 "need actual addr/port",
604 "value was not a bool, `u16` integer, string, or list of integers/strings",
605 "true",
606 );
607 chk_err(
608 "value was not a bool, `u16` integer, string, or list of integers/strings",
609 r#"listen = [ [] ]"#,
610 );
611
612 chk_1(
613 LI::Auto,
614 vec![vec![localhost6(0), localhost4(0)]],
615 Err(()),
616 r#" "auto" "#,
617 );
618
619 chk_1(
620 LI::AutoPort("1.2.3.4".parse().unwrap()),
621 vec![vec!["1.2.3.4:0".parse().unwrap()]],
622 Err(()),
623 r#" "1.2.3.4:auto" "#,
624 );
625 }
626
627 #[test]
628 fn more_parsing_checks() {
629 let config: TestConfigFile = toml::from_str(r#"listen = false"#).unwrap();
630 assert!(config.listen.unwrap().is_empty());
631
632 let config: TestConfigFile = toml::from_str(r#"listen = 0"#).unwrap();
633 assert!(config.listen.unwrap().is_empty());
634
635 let config: TestConfigFile = toml::from_str(r#"listen = 1"#).unwrap();
636 #[rustfmt::skip]
637 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 2);
638
639 let config: TestConfigFile = toml::from_str(r#"listen = """#).unwrap();
640 assert!(config.listen.unwrap().is_empty());
641
642 let config: TestConfigFile = toml::from_str(r#"listen = "127.0.0.1:8080""#).unwrap();
643 #[rustfmt::skip]
644 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 1);
645
646 let config: TestConfigFile = toml::from_str(r#"listen = ["127.0.0.1:8080"]"#).unwrap();
647 #[rustfmt::skip]
648 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 1);
649
650 let config: TestConfigFile = toml::from_str(r#"listen = "127.0.0.1:0""#).unwrap();
651 #[rustfmt::skip]
652 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 1);
653
654 let config: TestConfigFile = toml::from_str(r#"listen = ["127.0.0.1:0"]"#).unwrap();
655 #[rustfmt::skip]
656 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 1);
657
658 let config: TestConfigFile = toml::from_str(r#"listen = [1]"#).unwrap();
659 #[rustfmt::skip]
660 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 2);
661
662 let config: TestConfigFile = toml::from_str(r#"listen = [1, 2]"#).unwrap();
663 #[rustfmt::skip]
664 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 4);
665
666 let config: TestConfigFile = toml::from_str(r#"listen = ["127.0.0.1:8080", 2]"#).unwrap();
667 #[rustfmt::skip]
668 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 3);
669
670 assert!(toml::from_str::<TestConfigFile>(r#"listen = [false]"#).is_err());
671 assert!(toml::from_str::<TestConfigFile>(r#"listen = true"#).is_err());
672 assert!(toml::from_str::<TestConfigFile>(r#"listen = [true]"#).is_err());
673 assert!(toml::from_str::<TestConfigFile>(r#"listen = [0]"#).is_err());
674 assert!(toml::from_str::<TestConfigFile>(r#"listen = ["127.0.0.1:8080", 0]"#).is_err());
675 assert!(toml::from_str::<TestConfigFile>(r#"listen = ["foo"]"#).is_err());
676 assert!(toml::from_str::<TestConfigFile>(r#"listen = ["127.0.0.1:8080", "foo"]"#).is_err());
677 assert!(toml::from_str::<TestConfigFile>(r#"listen = [""]"#).is_err());
678 assert!(toml::from_str::<TestConfigFile>(r#"listen = ["127.0.0.1:8080", ""]"#).is_err());
679 }
680
681 #[test]
682 fn constructor() {
683 let l = Listen::new_none();
684 assert!(l.is_empty());
685 assert_eq!(l.ip_addrs().unwrap().flatten().count(), 0);
686
687 let l = Listen::new_localhost(1234);
688 assert!(!l.is_empty());
689 assert_eq!(l.ip_addrs().unwrap().flatten().count(), 2);
690
691 let l = Listen::new_localhost(0);
692 assert!(l.is_empty());
693 assert_eq!(l.ip_addrs().unwrap().flatten().count(), 0);
694
695 let l = Listen::new_localhost_optional(Some(1234));
696 assert!(!l.is_empty());
697 assert_eq!(l.ip_addrs().unwrap().flatten().count(), 2);
698
699 let l = Listen::new_localhost_optional(Some(0));
700 assert!(l.is_empty());
701 assert_eq!(l.ip_addrs().unwrap().flatten().count(), 0);
702
703 let l = Listen::new_localhost_optional(None);
704 assert!(l.is_empty());
705 assert_eq!(l.ip_addrs().unwrap().flatten().count(), 0);
706 }
707
708 #[test]
709 fn display_listen() {
710 let empty = Listen::new_none();
711 assert_eq!(empty.to_string(), "");
712
713 let one_port = Listen::new_localhost(1234);
714 assert_eq!(one_port.to_string(), "localhost port 1234");
715
716 let multi_port = Listen(CustomizableListen::List(vec![
717 ListenItem::Port(1111.try_into().unwrap()),
718 ListenItem::Port(2222.try_into().unwrap()),
719 ]));
720 assert_eq!(
721 multi_port.to_string(),
722 "localhost port 1111, localhost port 2222"
723 );
724
725 let multi_addr = Listen(CustomizableListen::List(vec![
726 ListenItem::Port(1234.try_into().unwrap()),
727 ListenItem::General("1.2.3.4:5678".parse().unwrap()),
728 ]));
729 assert_eq!(multi_addr.to_string(), "localhost port 1234, 1.2.3.4:5678");
730
731 let multi_addr = Listen(CustomizableListen::List(vec![
732 ListenItem::Auto,
733 ListenItem::AutoPort("1.2.3.4".parse().unwrap()),
734 ]));
735 assert_eq!(multi_addr.to_string(), "auto, 1.2.3.4:auto");
736 }
737
738 #[test]
739 fn is_localhost() {
740 fn localhost_only(s: &str) -> bool {
741 let tc: TestConfigFile = toml::from_str(s).expect(s);
742 tc.listen.unwrap().is_loopback_only()
743 }
744
745 assert_eq!(localhost_only(r#"listen = [ ]"#), true);
746 assert_eq!(localhost_only(r#"listen = [ 3 ]"#), true);
747 assert_eq!(localhost_only(r#"listen = [ 3, 10 ]"#), true);
748 assert_eq!(localhost_only(r#"listen = [ "127.0.0.1:9050" ]"#), true);
749 assert_eq!(localhost_only(r#"listen = [ "[::1]:9050" ]"#), true);
750 assert_eq!(
751 localhost_only(r#"listen = [ "[::1]:9050", "192.168.0.1:1234" ]"#),
752 false
753 );
754 assert_eq!(localhost_only(r#"listen = [ "192.168.0.1:1234" ]"#), false);
755 }
756}