Skip to main content

tor_config/
listen.rs

1//! Configuration for ports and addresses to listen on.
2
3use 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/// Specification of (possibly) something to listen on (eg, a port, or some addresses/ports)
11///
12/// Can represent, at least:
13///  * "do not listen"
14///  * Listen on the following port on localhost (IPv6 and IPv4)
15///  * Listen on precisely the following address and port
16///  * Listen on several addresses/ports
17///
18/// Currently only IP (v6 and v4) is supported.
19//
20// NOTE: If you're adding or changing functionality for this type,
21// make sure that all existing users of this type (for example all config options in arti and
22// arti-relay which use this) want that functionality.
23#[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    /// Create a new `Listen` specifying no addresses (no listening)
29    pub fn new_none() -> Listen {
30        CustomizableListen::Disabled
31            .try_into()
32            .expect("'disabled' should be valid")
33    }
34
35    /// Create a new `Listen` specifying listening on a port on localhost
36    ///
37    /// Special case: if `port` is zero, specifies no listening.
38    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    /// Create a new `Listen`, possibly specifying listening on a port on localhost
45    ///
46    /// Special case: if `port` is `Some(0)`, also specifies no listening.
47    pub fn new_localhost_optional(port: Option<u16>) -> Listen {
48        Self::new_localhost(port.unwrap_or_default())
49    }
50
51    /// Return true if no listening addresses have been configured
52    pub fn is_empty(&self) -> bool {
53        self.ip_addrs_internal().count() == 0
54    }
55
56    /// Return true if there are any "auto" addresses in this Listen.
57    ///
58    /// See also [`using_port_zero()`](Self::using_port_zero).
59    pub fn using_auto(&self) -> bool {
60        self.0.items().any(ListenItem::is_auto)
61    }
62
63    /// Return true if there are any port-zero addresses in this Listen.
64    ///
65    /// See also [`using_auto()`](Self::using_auto).
66    pub fn using_port_zero(&self) -> bool {
67        self.0.items().any(ListenItem::is_port_zero)
68    }
69
70    /// List the network socket addresses to listen on
71    ///
72    /// Each returned item is a list of `SocketAddr`,
73    /// of which *at least one* must be successfully bound.
74    /// It is OK if the others (up to all but one of them)
75    /// fail with `EAFNOSUPPORT` ("Address family not supported").
76    /// This allows handling of support, or non-support,
77    /// for particular address families, eg IPv6 vs IPv4 localhost.
78    /// Other errors (eg, `EADDRINUSE`) should always be treated as serious problems.
79    ///
80    /// Fails if the listen spec involves listening on things other than IP addresses.
81    /// (Currently that is not possible.)
82    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    /// List the network socket addresses to listen on.
90    ///
91    /// See [`Self::ip_addrs`], which wraps this result in an `Ok`.
92    fn ip_addrs_internal(
93        &self,
94    ) -> impl Iterator<Item = impl Iterator<Item = SocketAddr> + '_> + '_ {
95        // We interpret standalone ports to be localhost addresses.
96        let ips = [Ipv6Addr::LOCALHOST.into(), Ipv4Addr::LOCALHOST.into()];
97        self.0.items().map(move |item| item.iter(ips))
98    }
99
100    /// Get the localhost port to listen on
101    ///
102    /// Returns `None` if listening is configured to be disabled.
103    ///
104    /// Fails, giving an unsupported error, if the configuration
105    /// isn't just "listen on a single localhost port in all address families"
106    #[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    /// Get a single address to listen on
118    ///
119    /// Returns `None` if listening is configured to be disabled.
120    ///
121    /// If the configuration is "listen on a single port",
122    /// treats this as a request to listening on IPv4 only.
123    /// Use of this function implies a bug:
124    /// lack of proper support for the current internet protocol IPv6.
125    /// It should only be used if an underlying library or facility is likewise buggy.
126    ///
127    /// Fails, giving an unsupported error, if the configuration
128    /// isn't just "listen on a single port on one address family".
129    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    /// Helper: return a ListenItem if this Listen has exactly one.
140    ///
141    /// Return None if there are multiple items, or an error if there are multiple items.
142    ///
143    /// (Note that all users of this function are, or should be, deprecated.)
144    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    /// Return true if this `Listen` only configures listening on loopback addresses (`127.0.0.0/8`
158    /// and `::1`).
159    ///
160    /// Returns true if there are no addresses configured.
161    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.
168    /// Use [`Self::is_loopback_only`] instead,
169    /// which behaves the same but has the correct method name.
170    #[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                // A non-list standalone 0 means "none".
208                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                // We don't support a standalone 0 port in a list item.
221                if list.iter().any(|item| matches!(item, ListenItem::Port(0))) {
222                    return Err(InvalidListen::ZeroPortInList);
223                }
224                // We warn on e.g. "127.0.0.1:0"
225                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/// [`Listen`] configuration specified something not supported by application code
244#[derive(thiserror::Error, Debug, Clone)]
245#[non_exhaustive]
246#[error("Unsupported listening configuration")]
247pub struct ListenUnsupported {}
248
249/// Listen configuration is invalid
250#[derive(thiserror::Error, Debug, Clone)]
251#[non_exhaustive]
252enum InvalidListen {
253    /// Specified listen was a list containing a zero integer
254    #[error("Invalid listen specification: zero (for no port) not permitted in list")]
255    ZeroPortInList,
256}
257
258/// A general structure for configuring listening ports.
259///
260/// This is meant to provide some basic parsing without being too opinionated.
261/// If you have further requirements, you should wrap this in a new type.
262/// For example if `CustomizableListen` supports keywords or flags in the future such as "auto",
263/// any config options that don't want to support them should use a wrapper type that handles them
264/// and returns an error.
265#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
266#[serde(try_from = "ListenSerde", into = "ListenSerde")]
267enum CustomizableListen {
268    /// Explicitly disabled with `false`.
269    Disabled,
270    /// A single item not in a list.
271    One(ListenItem),
272    /// A list of items.
273    List(Vec<ListenItem>),
274}
275
276impl CustomizableListen {
277    /// All configured listen options.
278    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/// One item in the [`CustomizableListen`].
288///
289/// This type defines a common format for parsing.
290/// We don't assign any particular meaning to the variants.
291/// For example a standalone port doesn't imply anything about what IP address should be used.
292/// Similarly, a port of 0 doesn't have any inherent meaning.
293/// For example a port of 0 might mean "don't listen" (when network addresses are optional)
294/// or might mean "raise an error" (when network addresses are required).
295/// It's up to the user of this type to assign meaning to the values given.
296///
297/// We distinguish a standalone port,
298/// rather than just storing two `net:SocketAddr`,
299/// so that we can handle localhost (which means two address families) specially
300/// in order to implement `localhost_port_legacy()`.
301#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq)]
302// If we add new variants, it *is* a breaking change.
303// We want a compile-time error, not a runtime error.
304#[allow(clippy::exhaustive_enums)]
305enum ListenItem {
306    /// One port, both IPv6 and IPv4
307    Port(u16),
308
309    /// IPv6 and/or IPv4, arbitrarily chosen ports.
310    Auto,
311
312    /// Specific address, arbitrarily chosen port.
313    AutoPort(IpAddr),
314
315    /// Any other single socket address
316    General(SocketAddr),
317}
318
319impl ListenItem {
320    /// Return the `SocketAddr`s implied by this item
321    ///
322    /// If the item is a standalone port, then the returned iterator will return a socket address
323    /// using that port for each IP address in `ips_for_port`.
324    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    /// Return true if this ListenItem is "addr:auto" or "auto"
346    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    /// Return true if this ListenItem is using an explicit (deprecated) port value of 0.
357    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/// How we (de) serialize a [`Listen`]
381#[derive(Serialize, Deserialize)]
382#[serde(untagged)]
383// the default error message from serde's "untagged" is useless for users, so we provide our own
384#[serde(expecting = "value was not a bool, `u16` integer, string, or list of integers/strings")]
385enum ListenSerde {
386    /// for `listen = false` (in TOML syntax)
387    Bool(bool),
388
389    /// A bare item
390    One(ListenItemSerde),
391
392    /// An item in a list
393    List(Vec<ListenItemSerde>),
394}
395
396/// One item in the list of a list-ish `Listen`, or the plain value
397#[derive(Serialize, Deserialize)]
398#[serde(untagged)]
399// the default error message from serde's "untagged" is useless for users, so we provide our own
400#[serde(expecting = "item was not a `u16` integer or string")]
401enum ListenItemSerde {
402    /// An integer.
403    Port(u16),
404
405    /// A string.
406    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/// Listen configuration is invalid
435#[derive(thiserror::Error, Debug, Clone)]
436#[non_exhaustive]
437enum InvalidCustomizableListen {
438    /// Bool was `true` but that's not an address.
439    #[error("Invalid listen specification: need actual addr/port, or `false`; not `true`")]
440    InvalidBool,
441
442    /// Specified listen was a string but couldn't parse to a [`SocketAddr`].
443    #[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            // A false value not in a list is interpreted as "none".
453            LS::Bool(false) => CustomizableListen::Disabled,
454            LS::Bool(true) => return Err(InvalidCustomizableListen::InvalidBool),
455            // An empty string not in a list is interpreted as "none".
456            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    // @@ begin test lint list maintained by maint/add_warning @@
489    #![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    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
501    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)] // we do this for consistency
518        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                "", /* any error will do */
550                &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}