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 #![allow(clippy::string_slice)] use super::*;
503
504 #[derive(Debug, Default, Deserialize, Serialize)]
505 struct TestConfigFile {
506 #[serde(default)]
507 listen: Option<Listen>,
508 }
509
510 #[test]
511 fn listen_parse() {
512 use ListenItem as LI;
513
514 let localhost6 = |p| SocketAddr::new(Ipv6Addr::LOCALHOST.into(), p);
515 let localhost4 = |p| SocketAddr::new(Ipv4Addr::LOCALHOST.into(), p);
516 let unspec6 = |p| SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), p);
517
518 #[allow(clippy::needless_pass_by_value)] fn chk(
520 exp_i: Vec<ListenItem>,
521 exp_addrs: Result<Vec<Vec<SocketAddr>>, ()>,
522 exp_lpd: Result<Option<u16>, ()>,
523 s: &str,
524 ) {
525 let tc: TestConfigFile = toml::from_str(s).expect(s);
526 let ll = tc.listen.unwrap();
527 eprintln!("s={:?} ll={:?}", &s, &ll);
528 assert_eq!(ll.0.items().cloned().collect::<Vec<_>>(), exp_i);
529 assert_eq!(
530 ll.ip_addrs()
531 .map(|a| a.map(|l| l.collect_vec()).collect_vec())
532 .map_err(|_| ()),
533 exp_addrs
534 );
535 #[allow(deprecated)]
536 {
537 assert_eq!(ll.localhost_port_legacy().map_err(|_| ()), exp_lpd);
538 }
539 }
540
541 let chk_err = |exp, s: &str| {
542 let got: Result<TestConfigFile, _> = toml::from_str(s);
543 let got = got.expect_err(s).to_string();
544 assert!(got.contains(exp), "s={:?} got={:?} exp={:?}", s, got, exp);
545 };
546
547 let chk_none = |s: &str| {
548 chk(vec![], Ok(vec![]), Ok(None), &format!("listen = {}", s));
549 chk_err(
550 "", &format!("listen = [ {} ]", s),
552 );
553 };
554
555 let chk_1 = |v: ListenItem, addrs: Vec<Vec<SocketAddr>>, port, s| {
556 chk(
557 vec![v.clone()],
558 Ok(addrs.clone()),
559 port,
560 &format!("listen = {}", s),
561 );
562 chk(
563 vec![v.clone()],
564 Ok(addrs.clone()),
565 port,
566 &format!("listen = [ {} ]", s),
567 );
568 chk(
569 vec![v, LI::Port(23.try_into().unwrap())],
570 Ok([addrs, vec![vec![localhost6(23), localhost4(23)]]]
571 .into_iter()
572 .flatten()
573 .collect()),
574 Err(()),
575 &format!("listen = [ {}, 23 ]", s),
576 );
577 };
578
579 chk_none(r#""""#);
580 chk_none(r#"0"#);
581 chk_none(r#"false"#);
582 chk(vec![], Ok(vec![]), Ok(None), r#"listen = []"#);
583
584 chk_1(
585 LI::Port(42.try_into().unwrap()),
586 vec![vec![localhost6(42), localhost4(42)]],
587 Ok(Some(42)),
588 "42",
589 );
590 chk_1(
591 LI::General(unspec6(56)),
592 vec![vec![unspec6(56)]],
593 Err(()),
594 r#""[::]:56""#,
595 );
596
597 let chk_err_1 = |e, el, s| {
598 chk_err(e, &format!("listen = {}", s));
599 chk_err(el, &format!("listen = [ {} ]", s));
600 chk_err(el, &format!("listen = [ 23, {}, 77 ]", s));
601 };
602
603 chk_err_1(
604 "need actual addr/port",
605 "value was not a bool, `u16` integer, string, or list of integers/strings",
606 "true",
607 );
608 chk_err(
609 "value was not a bool, `u16` integer, string, or list of integers/strings",
610 r#"listen = [ [] ]"#,
611 );
612
613 chk_1(
614 LI::Auto,
615 vec![vec![localhost6(0), localhost4(0)]],
616 Err(()),
617 r#" "auto" "#,
618 );
619
620 chk_1(
621 LI::AutoPort("1.2.3.4".parse().unwrap()),
622 vec![vec!["1.2.3.4:0".parse().unwrap()]],
623 Err(()),
624 r#" "1.2.3.4:auto" "#,
625 );
626 }
627
628 #[test]
629 fn more_parsing_checks() {
630 let config: TestConfigFile = toml::from_str(r#"listen = false"#).unwrap();
631 assert!(config.listen.unwrap().is_empty());
632
633 let config: TestConfigFile = toml::from_str(r#"listen = 0"#).unwrap();
634 assert!(config.listen.unwrap().is_empty());
635
636 let config: TestConfigFile = toml::from_str(r#"listen = 1"#).unwrap();
637 #[rustfmt::skip]
638 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 2);
639
640 let config: TestConfigFile = toml::from_str(r#"listen = """#).unwrap();
641 assert!(config.listen.unwrap().is_empty());
642
643 let config: TestConfigFile = toml::from_str(r#"listen = "127.0.0.1:8080""#).unwrap();
644 #[rustfmt::skip]
645 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 1);
646
647 let config: TestConfigFile = toml::from_str(r#"listen = ["127.0.0.1:8080"]"#).unwrap();
648 #[rustfmt::skip]
649 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 1);
650
651 let config: TestConfigFile = toml::from_str(r#"listen = "127.0.0.1:0""#).unwrap();
652 #[rustfmt::skip]
653 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 1);
654
655 let config: TestConfigFile = toml::from_str(r#"listen = ["127.0.0.1:0"]"#).unwrap();
656 #[rustfmt::skip]
657 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 1);
658
659 let config: TestConfigFile = toml::from_str(r#"listen = [1]"#).unwrap();
660 #[rustfmt::skip]
661 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 2);
662
663 let config: TestConfigFile = toml::from_str(r#"listen = [1, 2]"#).unwrap();
664 #[rustfmt::skip]
665 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 4);
666
667 let config: TestConfigFile = toml::from_str(r#"listen = ["127.0.0.1:8080", 2]"#).unwrap();
668 #[rustfmt::skip]
669 assert_eq!(config.listen.unwrap().ip_addrs().unwrap().flatten().count(), 3);
670
671 assert!(toml::from_str::<TestConfigFile>(r#"listen = [false]"#).is_err());
672 assert!(toml::from_str::<TestConfigFile>(r#"listen = true"#).is_err());
673 assert!(toml::from_str::<TestConfigFile>(r#"listen = [true]"#).is_err());
674 assert!(toml::from_str::<TestConfigFile>(r#"listen = [0]"#).is_err());
675 assert!(toml::from_str::<TestConfigFile>(r#"listen = ["127.0.0.1:8080", 0]"#).is_err());
676 assert!(toml::from_str::<TestConfigFile>(r#"listen = ["foo"]"#).is_err());
677 assert!(toml::from_str::<TestConfigFile>(r#"listen = ["127.0.0.1:8080", "foo"]"#).is_err());
678 assert!(toml::from_str::<TestConfigFile>(r#"listen = [""]"#).is_err());
679 assert!(toml::from_str::<TestConfigFile>(r#"listen = ["127.0.0.1:8080", ""]"#).is_err());
680 }
681
682 #[test]
683 fn constructor() {
684 let l = Listen::new_none();
685 assert!(l.is_empty());
686 assert_eq!(l.ip_addrs().unwrap().flatten().count(), 0);
687
688 let l = Listen::new_localhost(1234);
689 assert!(!l.is_empty());
690 assert_eq!(l.ip_addrs().unwrap().flatten().count(), 2);
691
692 let l = Listen::new_localhost(0);
693 assert!(l.is_empty());
694 assert_eq!(l.ip_addrs().unwrap().flatten().count(), 0);
695
696 let l = Listen::new_localhost_optional(Some(1234));
697 assert!(!l.is_empty());
698 assert_eq!(l.ip_addrs().unwrap().flatten().count(), 2);
699
700 let l = Listen::new_localhost_optional(Some(0));
701 assert!(l.is_empty());
702 assert_eq!(l.ip_addrs().unwrap().flatten().count(), 0);
703
704 let l = Listen::new_localhost_optional(None);
705 assert!(l.is_empty());
706 assert_eq!(l.ip_addrs().unwrap().flatten().count(), 0);
707 }
708
709 #[test]
710 fn display_listen() {
711 let empty = Listen::new_none();
712 assert_eq!(empty.to_string(), "");
713
714 let one_port = Listen::new_localhost(1234);
715 assert_eq!(one_port.to_string(), "localhost port 1234");
716
717 let multi_port = Listen(CustomizableListen::List(vec![
718 ListenItem::Port(1111.try_into().unwrap()),
719 ListenItem::Port(2222.try_into().unwrap()),
720 ]));
721 assert_eq!(
722 multi_port.to_string(),
723 "localhost port 1111, localhost port 2222"
724 );
725
726 let multi_addr = Listen(CustomizableListen::List(vec![
727 ListenItem::Port(1234.try_into().unwrap()),
728 ListenItem::General("1.2.3.4:5678".parse().unwrap()),
729 ]));
730 assert_eq!(multi_addr.to_string(), "localhost port 1234, 1.2.3.4:5678");
731
732 let multi_addr = Listen(CustomizableListen::List(vec![
733 ListenItem::Auto,
734 ListenItem::AutoPort("1.2.3.4".parse().unwrap()),
735 ]));
736 assert_eq!(multi_addr.to_string(), "auto, 1.2.3.4:auto");
737 }
738
739 #[test]
740 fn is_localhost() {
741 fn localhost_only(s: &str) -> bool {
742 let tc: TestConfigFile = toml::from_str(s).expect(s);
743 tc.listen.unwrap().is_loopback_only()
744 }
745
746 assert_eq!(localhost_only(r#"listen = [ ]"#), true);
747 assert_eq!(localhost_only(r#"listen = [ 3 ]"#), true);
748 assert_eq!(localhost_only(r#"listen = [ 3, 10 ]"#), true);
749 assert_eq!(localhost_only(r#"listen = [ "127.0.0.1:9050" ]"#), true);
750 assert_eq!(localhost_only(r#"listen = [ "[::1]:9050" ]"#), true);
751 assert_eq!(
752 localhost_only(r#"listen = [ "[::1]:9050", "192.168.0.1:1234" ]"#),
753 false
754 );
755 assert_eq!(localhost_only(r#"listen = [ "192.168.0.1:1234" ]"#), false);
756 }
757}