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

            
3
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
4
use std::{fmt::Display, iter, num::NonZeroU16};
5

            
6
use either::Either;
7
use itertools::Itertools as _;
8
use 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
98
#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
20
#[serde(try_from = "ListenSerde", into = "ListenSerde")]
21
#[derive(Default)]
22
pub struct Listen(Vec<ListenItem>);
23

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

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

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

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

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

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

            
89
    /// Get a single address to listen on
90
    ///
91
    /// Returns `None` if listening is configured to be disabled.
92
    ///
93
    /// If the configuration is "listen on a single port",
94
    /// treats this as a request to listening on IPv4 only.
95
    /// Use of this function implies a bug:
96
    /// lack of proper support for the current internet protocol IPv6.
97
    /// It should only be used if an underlying library or facility is likewise buggy.
98
    ///
99
    /// Fails, giving an unsupported error, if the configuration
100
    /// isn't just "listen on a single port on one address family".
101
71
    pub fn single_address_legacy(&self) -> Result<Option<SocketAddr>, ListenUnsupported> {
102
        use ListenItem as LI;
103
71
        Ok(match &*self.0 {
104
71
            [] => None,
105
71
            [LI::Localhost(port)] => Some((Ipv4Addr::LOCALHOST, u16::from(*port)).into()),
106
            [LI::General(sa)] => Some(*sa),
107
            _ => return Err(ListenUnsupported {}),
108
        })
109
71
    }
110

            
111
    /// Return true if this `Listen` only configures listening on localhost.
112
14
    pub fn is_localhost_only(&self) -> bool {
113
14
        self.0.iter().all(ListenItem::is_localhost)
114
14
    }
115
}
116

            
117
impl Display for Listen {
118
8
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
119
8
        let mut sep = "";
120
18
        for a in &self.0 {
121
10
            write!(f, "{sep}{a}")?;
122
10
            sep = ", ";
123
        }
124
8
        Ok(())
125
8
    }
126
}
127
/// [`Listen`] configuration specified something not supported by application code
128
#[derive(thiserror::Error, Debug, Clone)]
129
#[non_exhaustive]
130
#[error("Unsupported listening configuration")]
131
pub struct ListenUnsupported {}
132

            
133
/// One item in the `Listen`
134
///
135
/// We distinguish `Localhost`,
136
/// rather than just storing two `net:SocketAddr`,
137
/// so that we can handle localhost (which means two address families) specially
138
/// in order to implement `localhost_port_legacy()`.
139
#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
140
#[serde(untagged)]
141
enum ListenItem {
142
    /// One port, both IPv6 and IPv4
143
    Localhost(NonZeroU16),
144

            
145
    /// Any other single socket address
146
    General(SocketAddr),
147
}
148

            
149
impl ListenItem {
150
    /// Return the `SocketAddr`s implied by this item
151
16
    fn iter(&self) -> impl Iterator<Item = SocketAddr> + '_ {
152
        use ListenItem as LI;
153
16
        match self {
154
10
            &LI::Localhost(port) => Either::Left({
155
10
                let port = port.into();
156
10
                let addrs: [IpAddr; 2] = [Ipv6Addr::LOCALHOST.into(), Ipv4Addr::LOCALHOST.into()];
157
25
                addrs.into_iter().map(move |ip| SocketAddr::new(ip, port))
158
10
            }),
159
6
            LI::General(addr) => Either::Right(iter::once(addr).cloned()),
160
        }
161
16
    }
162

            
163
    /// Return true if this is a localhost address.
164
16
    fn is_localhost(&self) -> bool {
165
        use ListenItem as LI;
166
16
        match self {
167
6
            LI::Localhost(_) => true,
168
10
            LI::General(addr) => addr.ip().is_loopback(),
169
        }
170
16
    }
171
}
172

            
173
impl Display for ListenItem {
174
10
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
175
10
        match self {
176
8
            ListenItem::Localhost(port) => write!(f, "localhost port {}", port)?,
177
2
            ListenItem::General(addr) => write!(f, "{}", addr)?,
178
        }
179
10
        Ok(())
180
10
    }
181
}
182
/// How we (de) serialize a [`Listen`]
183
#[derive(Serialize, Deserialize)]
184
#[serde(untagged)]
185
enum ListenSerde {
186
    /// for `listen = false` (in TOML syntax)
187
    Bool(bool),
188

            
189
    /// A bare item
190
    One(ListenItemSerde),
191

            
192
    /// An item in a list
193
    List(Vec<ListenItemSerde>),
194
}
195

            
196
/// One item in the list of a list-ish `Listen`, or the plain value
197
#[derive(Serialize, Deserialize)]
198
#[serde(untagged)]
199
enum ListenItemSerde {
200
    /// An integer.
201
    ///
202
    /// When appearing "loose" (in ListenSerde::One), `0` is parsed as none.
203
    Port(u16),
204

            
205
    /// An string which will be parsed as an address and port
206
    ///
207
    /// When appearing "loose" (in ListenSerde::One), `""` is parsed as none.
208
    String(String),
209
}
210

            
211
// This implementation isn't fallible, but clippy thinks it is because of the unwrap.
212
// The unwrap is just there because we can't pattern-match on a Vec
213
#[allow(clippy::fallible_impl_from)]
214
impl 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
}
224
impl 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
/// Listen configuration is invalid
236
#[derive(thiserror::Error, Debug, Clone)]
237
#[non_exhaustive]
238
pub enum InvalidListen {
239
    /// Bool was `true` but that's not an address.
240
    #[error("Invalid listen specification: need actual addr/port, or `false`; not `true`")]
241
    InvalidBool,
242

            
243
    /// Specified listen was a string but couldn't parse to a [`SocketAddr`].
244
    #[error("Invalid listen specification: failed to parse string: {0}")]
245
    InvalidString(#[from] std::net::AddrParseError),
246

            
247
    /// Specified listen was a list containing a zero integer
248
    #[error("Invalid listen specification: zero (for no port) not permitted in list")]
249
    ZeroPortInList,
250
}
251
impl TryFrom<ListenSerde> for Listen {
252
    type Error = InvalidListen;
253

            
254
2099
    fn try_from(l: ListenSerde) -> Result<Listen, Self::Error> {
255
        use ListenSerde as LS;
256
2067
        Ok(Listen(match l {
257
2
            LS::Bool(false) => vec![],
258
2
            LS::Bool(true) => return Err(InvalidListen::InvalidBool),
259
2067
            LS::One(i) if i.means_none() => vec![],
260
1282
            LS::One(i) => vec![i.try_into()?],
261
46
            LS::List(l) => l.into_iter().map(|i| i.try_into()).try_collect()?,
262
        }))
263
2099
    }
264
}
265
impl ListenItemSerde {
266
    /// Is this item actually a sentinel, meaning "don't listen, disable this thing"?
267
    ///
268
    /// Allowed only bare, not in a list.
269
2067
    fn means_none(&self) -> bool {
270
        use ListenItemSerde as LIS;
271
2067
        match self {
272
2063
            &LIS::Port(port) => port == 0,
273
4
            LIS::String(s) => s.is_empty(),
274
        }
275
2067
    }
276
}
277
impl TryFrom<ListenItemSerde> for ListenItem {
278
    type Error = InvalidListen;
279

            
280
1314
    fn try_from(i: ListenItemSerde) -> Result<ListenItem, Self::Error> {
281
        use ListenItem as LI;
282
        use ListenItemSerde as LIS;
283
1314
        Ok(match i {
284
18
            LIS::String(s) => LI::General(s.parse()?),
285
1297
            LIS::Port(p) => LI::Localhost(p.try_into().map_err(|_| InvalidListen::ZeroPortInList)?),
286
        })
287
1314
    }
288
}
289

            
290
#[cfg(test)]
291
mod test {
292
    // @@ begin test lint list maintained by maint/add_warning @@
293
    #![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
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
305
    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)] // we do this for consistency
322
        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
                "", /* any error will do */
351
                &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
}