1
//! Configuration for ports and addresses to listen on.
2

            
3
use std::{fmt::Display, iter, net, num::NonZeroU16};
4

            
5
use either::Either;
6
use itertools::Itertools as _;
7
use serde::{Deserialize, Serialize};
8

            
9
/// Specification of (possibly) something to listen on (eg, a port, or some addresses/ports)
10
///
11
/// Can represent, at least:
12
///  * "do not listen"
13
///  * Listen on the following port on localhost (IPv6 and IPv4)
14
///  * Listen on precisely the following address and port
15
///  * Listen on several addresses/ports
16
///
17
/// Currently only IP (v6 and v4) is supported.
18
96
#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
19
#[serde(try_from = "ListenSerde", into = "ListenSerde")]
20
#[derive(Default)]
21
pub struct Listen(Vec<ListenItem>);
22

            
23
impl Listen {
24
    /// Create a new `Listen` specifying no addresses (no listening)
25
682
    pub fn new_none() -> Listen {
26
682
        Listen(vec![])
27
682
    }
28

            
29
    /// Create a new `Listen` specifying listening on a port on localhost
30
    ///
31
    /// Special case: if `port` is zero, specifies no listening.
32
11086
    pub fn new_localhost(port: u16) -> Listen {
33
11086
        Listen(
34
11086
            port.try_into()
35
11086
                .ok()
36
11086
                .map(ListenItem::Localhost)
37
11086
                .into_iter()
38
11086
                .collect_vec(),
39
11086
        )
40
11086
    }
41

            
42
    /// Create a new `Listen`, possibly specifying listening on a port on localhost
43
    ///
44
    /// Special case: if `port` is `Some(0)`, also specifies no listening.
45
2720
    pub fn new_localhost_optional(port: Option<u16>) -> Listen {
46
2720
        Self::new_localhost(port.unwrap_or_default())
47
2720
    }
48

            
49
    /// Return true if no listening addresses have been configured
50
    pub fn is_empty(&self) -> bool {
51
        self.0.is_empty()
52
    }
53

            
54
    /// List the network socket addresses to listen on
55
    ///
56
    /// Each returned item is a list of `SocketAddr`,
57
    /// of which *at least one* must be successfully bound.
58
    /// It is OK if the others (up to all but one of them)
59
    /// fail with `EAFNOSUPPORT` ("Address family not supported").
60
    /// This allows handling of support, or non-support,
61
    /// for particular address families, eg IPv6 vs IPv4 localhost.
62
    /// Other errors (eg, `EADDRINUSE`) should always be treated as serious problems.
63
    ///
64
    /// Fails if the listen spec involves listening on things other than IP addresses.
65
    /// (Currently that is not possible.)
66
20
    pub fn ip_addrs(
67
20
        &self,
68
20
    ) -> Result<
69
20
        impl Iterator<Item = impl Iterator<Item = net::SocketAddr> + '_> + '_,
70
20
        ListenUnsupported,
71
20
    > {
72
28
        Ok(self.0.iter().map(|i| i.iter()))
73
20
    }
74

            
75
    /// Get the localhost port to listen on
76
    ///
77
    /// Returns `None` if listening is configured to be disabled.
78
    ///
79
    /// Fails, giving an unsupported error, if the configuration
80
    /// isn't just "listen on a single localhost port in all address families"
81
20
    pub fn localhost_port_legacy(&self) -> Result<Option<u16>, ListenUnsupported> {
82
        use ListenItem as LI;
83
20
        Ok(match &*self.0 {
84
20
            [] => None,
85
4
            [LI::Localhost(port)] => Some((*port).into()),
86
8
            _ => return Err(ListenUnsupported {}),
87
        })
88
20
    }
89

            
90
    /// Return true if this `Listen` only configures listening on localhost.
91
14
    pub fn is_localhost_only(&self) -> bool {
92
14
        self.0.iter().all(ListenItem::is_localhost)
93
14
    }
94
}
95

            
96
impl Display for Listen {
97
8
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
98
8
        let mut sep = "";
99
18
        for a in &self.0 {
100
10
            write!(f, "{sep}{a}")?;
101
10
            sep = ", ";
102
        }
103
8
        Ok(())
104
8
    }
105
}
106
/// [`Listen`] configuration specified something not supported by application code
107
#[derive(thiserror::Error, Debug, Clone)]
108
#[non_exhaustive]
109
#[error("Unsupported listening configuration")]
110
pub struct ListenUnsupported {}
111

            
112
/// One item in the `Listen`
113
///
114
/// We distinguish `Localhost`,
115
/// rather than just storing two `net:SocketAddr`,
116
/// so that we can handle localhost (which means two address families) specially
117
/// in order to implement `localhost_port_legacy()`.
118
#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
119
#[serde(untagged)]
120
enum ListenItem {
121
    /// One port, both IPv6 and IPv4
122
    Localhost(NonZeroU16),
123

            
124
    /// Any other single socket address
125
    General(net::SocketAddr),
126
}
127

            
128
impl ListenItem {
129
    /// Return the `SocketAddr`s implied by this item
130
16
    fn iter(&self) -> impl Iterator<Item = net::SocketAddr> + '_ {
131
        use net::{IpAddr, Ipv4Addr, Ipv6Addr};
132
        use ListenItem as LI;
133
16
        match self {
134
10
            &LI::Localhost(port) => Either::Left({
135
10
                let port = port.into();
136
10
                let addrs: [IpAddr; 2] = [Ipv6Addr::LOCALHOST.into(), Ipv4Addr::LOCALHOST.into()];
137
10
                addrs
138
10
                    .into_iter()
139
25
                    .map(move |ip| net::SocketAddr::new(ip, port))
140
10
            }),
141
6
            LI::General(addr) => Either::Right(iter::once(addr).cloned()),
142
        }
143
16
    }
144

            
145
    /// Return true if this is a localhost address.
146
16
    fn is_localhost(&self) -> bool {
147
        use ListenItem as LI;
148
16
        match self {
149
6
            LI::Localhost(_) => true,
150
10
            LI::General(addr) => addr.ip().is_loopback(),
151
        }
152
16
    }
153
}
154

            
155
impl Display for ListenItem {
156
10
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
157
10
        match self {
158
8
            ListenItem::Localhost(port) => write!(f, "localhost port {}", port)?,
159
2
            ListenItem::General(addr) => write!(f, "{}", addr)?,
160
        }
161
10
        Ok(())
162
10
    }
163
}
164
/// How we (de) serialize a [`Listen`]
165
#[derive(Serialize, Deserialize)]
166
#[serde(untagged)]
167
enum ListenSerde {
168
    /// for `listen = false` (in TOML syntax)
169
    Bool(bool),
170

            
171
    /// A bare item
172
    One(ListenItemSerde),
173

            
174
    /// An item in a list
175
    List(Vec<ListenItemSerde>),
176
}
177

            
178
/// One item in the list of a list-ish `Listen`, or the plain value
179
#[derive(Serialize, Deserialize)]
180
#[serde(untagged)]
181
enum ListenItemSerde {
182
    /// An integer.
183
    ///
184
    /// When appearing "loose" (in ListenSerde::One), `0` is parsed as none.
185
    Port(u16),
186

            
187
    /// An string which will be parsed as an address and port
188
    ///
189
    /// When appearing "loose" (in ListenSerde::One), `""` is parsed as none.
190
    String(String),
191
}
192

            
193
// This implementation isn't fallible, but clippy thinks it is because of the unwrap.
194
// The unwrap is just there because we can't pattern-match on a Vec
195
#[allow(clippy::fallible_impl_from)]
196
impl From<Listen> for ListenSerde {
197
    fn from(l: Listen) -> ListenSerde {
198
        let l = l.0;
199
        match l.len() {
200
            0 => ListenSerde::Bool(false),
201
            1 => ListenSerde::One(l.into_iter().next().expect("len=1 but no next").into()),
202
            _ => ListenSerde::List(l.into_iter().map(Into::into).collect()),
203
        }
204
    }
205
}
206
impl From<ListenItem> for ListenItemSerde {
207
    fn from(i: ListenItem) -> ListenItemSerde {
208
        use ListenItem as LI;
209
        use ListenItemSerde as LIS;
210
        match i {
211
            LI::Localhost(port) => LIS::Port(port.into()),
212
            LI::General(addr) => LIS::String(addr.to_string()),
213
        }
214
    }
215
}
216

            
217
/// Listen configuration is invalid
218
#[derive(thiserror::Error, Debug, Clone)]
219
#[non_exhaustive]
220
pub enum InvalidListen {
221
    /// Bool was `true` but that's not an address.
222
    #[error("Invalid listen specification: need actual addr/port, or `false`; not `true`")]
223
    InvalidBool,
224

            
225
    /// Specified listen was a string but couldn't parse to a [`net::SocketAddr`].
226
    #[error("Invalid listen specification: failed to parse string: {0}")]
227
    InvalidString(#[from] net::AddrParseError),
228

            
229
    /// Specified listen was a list containing a zero integer
230
    #[error("Invalid listen specification: zero (for no port) not permitted in list")]
231
    ZeroPortInList,
232
}
233
impl TryFrom<ListenSerde> for Listen {
234
    type Error = InvalidListen;
235

            
236
1944
    fn try_from(l: ListenSerde) -> Result<Listen, Self::Error> {
237
        use ListenSerde as LS;
238
1912
        Ok(Listen(match l {
239
2
            LS::Bool(false) => vec![],
240
2
            LS::Bool(true) => return Err(InvalidListen::InvalidBool),
241
1912
            LS::One(i) if i.means_none() => vec![],
242
1160
            LS::One(i) => vec![i.try_into()?],
243
46
            LS::List(l) => l.into_iter().map(|i| i.try_into()).try_collect()?,
244
        }))
245
1944
    }
246
}
247
impl ListenItemSerde {
248
    /// Is this item actually a sentinel, meaning "don't listen, disable this thing"?
249
    ///
250
    /// Allowed only bare, not in a list.
251
1912
    fn means_none(&self) -> bool {
252
        use ListenItemSerde as LIS;
253
1912
        match self {
254
1908
            &LIS::Port(port) => port == 0,
255
4
            LIS::String(s) => s.is_empty(),
256
        }
257
1912
    }
258
}
259
impl TryFrom<ListenItemSerde> for ListenItem {
260
    type Error = InvalidListen;
261

            
262
1192
    fn try_from(i: ListenItemSerde) -> Result<ListenItem, Self::Error> {
263
        use ListenItem as LI;
264
        use ListenItemSerde as LIS;
265
1192
        Ok(match i {
266
18
            LIS::String(s) => LI::General(s.parse()?),
267
1175
            LIS::Port(p) => LI::Localhost(p.try_into().map_err(|_| InvalidListen::ZeroPortInList)?),
268
        })
269
1192
    }
270
}
271

            
272
#[cfg(test)]
273
mod test {
274
    // @@ begin test lint list maintained by maint/add_warning @@
275
    #![allow(clippy::bool_assert_comparison)]
276
    #![allow(clippy::clone_on_copy)]
277
    #![allow(clippy::dbg_macro)]
278
    #![allow(clippy::mixed_attributes_style)]
279
    #![allow(clippy::print_stderr)]
280
    #![allow(clippy::print_stdout)]
281
    #![allow(clippy::single_char_pattern)]
282
    #![allow(clippy::unwrap_used)]
283
    #![allow(clippy::unchecked_duration_subtraction)]
284
    #![allow(clippy::useless_vec)]
285
    #![allow(clippy::needless_pass_by_value)]
286
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
287
    use super::*;
288

            
289
    #[derive(Debug, Default, Deserialize, Serialize)]
290
    struct TestConfigFile {
291
        #[serde(default)]
292
        listen: Option<Listen>,
293
    }
294

            
295
    #[test]
296
    fn listen_parse() {
297
        use net::{Ipv4Addr, Ipv6Addr, SocketAddr};
298
        use ListenItem as LI;
299

            
300
        let localhost6 = |p| SocketAddr::new(Ipv6Addr::LOCALHOST.into(), p);
301
        let localhost4 = |p| SocketAddr::new(Ipv4Addr::LOCALHOST.into(), p);
302
        let unspec6 = |p| SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), p);
303

            
304
        #[allow(clippy::needless_pass_by_value)] // we do this for consistency
305
        fn chk(
306
            exp_i: Vec<ListenItem>,
307
            exp_addrs: Result<Vec<Vec<SocketAddr>>, ()>,
308
            exp_lpd: Result<Option<u16>, ()>,
309
            s: &str,
310
        ) {
311
            let tc: TestConfigFile = toml::from_str(s).expect(s);
312
            let ll = tc.listen.unwrap();
313
            eprintln!("s={:?} ll={:?}", &s, &ll);
314
            assert_eq!(ll, Listen(exp_i));
315
            assert_eq!(
316
                ll.ip_addrs()
317
                    .map(|a| a.map(|l| l.collect_vec()).collect_vec())
318
                    .map_err(|_| ()),
319
                exp_addrs
320
            );
321
            assert_eq!(ll.localhost_port_legacy().map_err(|_| ()), exp_lpd);
322
        }
323

            
324
        let chk_err = |exp, s: &str| {
325
            let got: Result<TestConfigFile, _> = toml::from_str(s);
326
            let got = got.expect_err(s).to_string();
327
            assert!(got.contains(exp), "s={:?} got={:?} exp={:?}", s, got, exp);
328
        };
329

            
330
        let chk_none = |s: &str| {
331
            chk(vec![], Ok(vec![]), Ok(None), &format!("listen = {}", s));
332
            chk_err(
333
                "", /* any error will do */
334
                &format!("listen = [ {} ]", s),
335
            );
336
        };
337

            
338
        let chk_1 = |v: ListenItem, addrs: Vec<Vec<SocketAddr>>, port, s| {
339
            chk(
340
                vec![v.clone()],
341
                Ok(addrs.clone()),
342
                port,
343
                &format!("listen = {}", s),
344
            );
345
            chk(
346
                vec![v.clone()],
347
                Ok(addrs.clone()),
348
                port,
349
                &format!("listen = [ {} ]", s),
350
            );
351
            chk(
352
                vec![v, LI::Localhost(23.try_into().unwrap())],
353
                Ok([addrs, vec![vec![localhost6(23), localhost4(23)]]]
354
                    .into_iter()
355
                    .flatten()
356
                    .collect()),
357
                Err(()),
358
                &format!("listen = [ {}, 23 ]", s),
359
            );
360
        };
361

            
362
        chk_none(r#""""#);
363
        chk_none(r#"0"#);
364
        chk_none(r#"false"#);
365
        chk(vec![], Ok(vec![]), Ok(None), r#"listen = []"#);
366

            
367
        chk_1(
368
            LI::Localhost(42.try_into().unwrap()),
369
            vec![vec![localhost6(42), localhost4(42)]],
370
            Ok(Some(42)),
371
            "42",
372
        );
373
        chk_1(
374
            LI::General(unspec6(56)),
375
            vec![vec![unspec6(56)]],
376
            Err(()),
377
            r#""[::]:56""#,
378
        );
379

            
380
        let chk_err_1 = |e, el, s| {
381
            chk_err(e, &format!("listen = {}", s));
382
            chk_err(el, &format!("listen = [ {} ]", s));
383
            chk_err(el, &format!("listen = [ 23, {}, 77 ]", s));
384
        };
385

            
386
        chk_err_1("need actual addr/port", "did not match any variant", "true");
387
        chk_err("did not match any variant", r#"listen = [ [] ]"#);
388
    }
389

            
390
    #[test]
391
    fn display_listen() {
392
        let empty = Listen::new_none();
393
        assert_eq!(empty.to_string(), "");
394

            
395
        let one_port = Listen::new_localhost(1234);
396
        assert_eq!(one_port.to_string(), "localhost port 1234");
397

            
398
        let multi_port = Listen(vec![
399
            ListenItem::Localhost(1111.try_into().unwrap()),
400
            ListenItem::Localhost(2222.try_into().unwrap()),
401
        ]);
402
        assert_eq!(
403
            multi_port.to_string(),
404
            "localhost port 1111, localhost port 2222"
405
        );
406

            
407
        let multi_addr = Listen(vec![
408
            ListenItem::Localhost(1234.try_into().unwrap()),
409
            ListenItem::General("1.2.3.4:5678".parse().unwrap()),
410
        ]);
411
        assert_eq!(multi_addr.to_string(), "localhost port 1234, 1.2.3.4:5678");
412
    }
413

            
414
    #[test]
415
    fn is_localhost() {
416
        fn localhost_only(s: &str) -> bool {
417
            let tc: TestConfigFile = toml::from_str(s).expect(s);
418
            tc.listen.unwrap().is_localhost_only()
419
        }
420

            
421
        assert_eq!(localhost_only(r#"listen = [ ]"#), true);
422
        assert_eq!(localhost_only(r#"listen = [ 3 ]"#), true);
423
        assert_eq!(localhost_only(r#"listen = [ 3, 10 ]"#), true);
424
        assert_eq!(localhost_only(r#"listen = [ "127.0.0.1:9050" ]"#), true);
425
        assert_eq!(localhost_only(r#"listen = [ "[::1]:9050" ]"#), true);
426
        assert_eq!(
427
            localhost_only(r#"listen = [ "[::1]:9050", "192.168.0.1:1234" ]"#),
428
            false
429
        );
430
        assert_eq!(localhost_only(r#"listen = [  "192.168.0.1:1234" ]"#), false);
431
    }
432
}