Merge branch 'port-listen' into 'main'
Rename *_port to *_listen and provide ListenConfig type for API See merge request tpo/core/arti!602
This commit is contained in:
commit
cbb30b5e75
|
@ -96,6 +96,7 @@ dependencies = [
|
|||
"libc",
|
||||
"notify",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"regex",
|
||||
"rlimit",
|
||||
"safelog",
|
||||
|
@ -3553,6 +3554,7 @@ dependencies = [
|
|||
"directories",
|
||||
"dirs",
|
||||
"educe",
|
||||
"either",
|
||||
"fs-mistrust",
|
||||
"itertools",
|
||||
"once_cell",
|
||||
|
|
|
@ -46,9 +46,11 @@ derive_builder = { version = "0.11", package = "derive_builder_fork_arti" }
|
|||
educe = "0.4.6"
|
||||
fs-mistrust = { path = "../fs-mistrust", version = "0.4.0" }
|
||||
futures = "0.3.14"
|
||||
itertools = "0.10.1"
|
||||
libc = "0.2"
|
||||
notify = "4.0"
|
||||
once_cell = { version = "1", optional = true }
|
||||
paste = "1"
|
||||
rlimit = "0.8.3"
|
||||
safelog = { path = "../safelog", version = "0.1.0" }
|
||||
secmem-proc = { version = "0.1.1", optional = true }
|
||||
|
@ -69,7 +71,7 @@ visibility = { version = "0.0.1", optional = true }
|
|||
itertools = "0.10.1"
|
||||
regex = { version = "1", default-features = false, features = ["std"] }
|
||||
serde_json = "1.0.50"
|
||||
toml = "0.5"
|
||||
toml = "0.5.6"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = { version = "0.2", default-features = false }
|
||||
|
|
|
@ -32,10 +32,10 @@
|
|||
# listen on localhost.
|
||||
#
|
||||
# Note that only one process can listen on a given port at a time.
|
||||
#socks_port = 9150
|
||||
#socks_listen = 9150
|
||||
|
||||
# Port to use to listen for DNS requests. 0 means disabled.
|
||||
#dns_port = 0
|
||||
#dns_listen = 0
|
||||
|
||||
# Configure logging
|
||||
[logging]
|
||||
|
|
|
@ -2,11 +2,14 @@
|
|||
//
|
||||
// (Thia module is called `cfg` to avoid name clash with the `config` crate, which we use.)
|
||||
|
||||
use paste::paste;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use arti_client::TorClientConfig;
|
||||
use tor_config::{impl_standard_builder, ConfigBuildError};
|
||||
use tor_config::resolve_alternative_specs;
|
||||
pub(crate) use tor_config::{impl_standard_builder, ConfigBuildError, Listen};
|
||||
|
||||
use crate::{LoggingConfig, LoggingConfigBuilder};
|
||||
|
||||
|
@ -70,18 +73,70 @@ pub struct ApplicationConfig {
|
|||
}
|
||||
impl_standard_builder! { ApplicationConfig }
|
||||
|
||||
/// Resolves values from `$field_listen` and `$field_port` (compat) into a `Listen`
|
||||
///
|
||||
/// For `dns` and `proxy`.
|
||||
///
|
||||
/// Handles defaulting, and normalisation, using `resolve_alternative_specs`
|
||||
/// and `Listen::new_localhost_option`.
|
||||
///
|
||||
/// Broken out into a macro so as to avoid having to state the field name four times,
|
||||
/// which is a recipe for programming slips.
|
||||
macro_rules! resolve_listen_port {
|
||||
{ $self:expr, $field:ident, $def_port:expr } => { paste!{
|
||||
resolve_alternative_specs(
|
||||
[
|
||||
(
|
||||
concat!(stringify!($field), "_listen"),
|
||||
$self.[<$field _listen>].clone(),
|
||||
),
|
||||
(
|
||||
concat!(stringify!($field), "_port"),
|
||||
$self.[<$field _port>].map(Listen::new_localhost_optional),
|
||||
),
|
||||
],
|
||||
|| Listen::new_localhost($def_port),
|
||||
)?
|
||||
} }
|
||||
}
|
||||
|
||||
/// Configuration for one or more proxy listeners.
|
||||
#[derive(Debug, Clone, Builder, Eq, PartialEq)]
|
||||
#[builder(build_fn(error = "ConfigBuildError"))]
|
||||
#[builder(derive(Debug, Serialize, Deserialize))]
|
||||
#[allow(clippy::option_option)] // Builder port fields: Some(None) = specified to disable
|
||||
pub struct ProxyConfig {
|
||||
/// Port to listen on (at localhost) for incoming SOCKS
|
||||
/// connections.
|
||||
#[builder(field(build = r#"tor_config::resolve_option(&self.socks_port, || Some(9150))"#))]
|
||||
pub(crate) socks_port: Option<u16>,
|
||||
/// Addresses to listen on for incoming SOCKS connections.
|
||||
#[builder(field(build = r#"resolve_listen_port!(self, socks, 9150)"#))]
|
||||
pub(crate) socks_listen: Listen,
|
||||
|
||||
/// Port to listen on (at localhost) for incoming SOCKS connections.
|
||||
///
|
||||
/// This field is deprecated, and will, eventually, be removed.
|
||||
/// Use `socks_listen` instead, which accepts the same values,
|
||||
/// but which will also be able to support more flexible listening in the future.
|
||||
#[builder(
|
||||
setter(strip_option),
|
||||
field(type = "Option<Option<u16>>", build = "()")
|
||||
)]
|
||||
#[builder_setter_attr(deprecated)]
|
||||
pub(crate) socks_port: (),
|
||||
|
||||
/// Addresses to listen on for incoming DNS connections.
|
||||
#[builder(field(build = r#"resolve_listen_port!(self, dns, 0)"#))]
|
||||
pub(crate) dns_listen: Listen,
|
||||
|
||||
/// Port to lisen on (at localhost) for incoming DNS connections.
|
||||
#[builder(field(build = r#"tor_config::resolve_option(&self.dns_port, || None)"#))]
|
||||
pub(crate) dns_port: Option<u16>,
|
||||
///
|
||||
/// This field is deprecated, and will, eventually, be removed.
|
||||
/// Use `dns_listen` instead, which accepts the same values,
|
||||
/// but which will also be able to support more flexible listening in the future.
|
||||
#[builder(
|
||||
setter(strip_option),
|
||||
field(type = "Option<Option<u16>>", build = "()")
|
||||
)]
|
||||
#[builder_setter_attr(deprecated)]
|
||||
pub(crate) dns_port: (),
|
||||
}
|
||||
impl_standard_builder! { ProxyConfig }
|
||||
|
||||
|
@ -146,6 +201,7 @@ impl_standard_builder! { ArtiConfig }
|
|||
|
||||
impl tor_config::load::TopLevel for ArtiConfig {
|
||||
type Builder = ArtiConfigBuilder;
|
||||
const DEPRECATED_KEYS: &'static [&'static str] = &["proxy.socks_port", "proxy.dns_port"];
|
||||
}
|
||||
|
||||
/// Convenience alias for the config for a whole `arti` program
|
||||
|
@ -176,8 +232,11 @@ mod test {
|
|||
|
||||
use arti_client::config::dir;
|
||||
use arti_client::config::TorClientConfigBuilder;
|
||||
use itertools::{chain, Itertools};
|
||||
use regex::Regex;
|
||||
use std::iter;
|
||||
use std::time::Duration;
|
||||
use tor_config::load::ResolutionResults;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -208,14 +267,14 @@ mod test {
|
|||
// Also we should ideally test that every setting from the config appears here in
|
||||
// the file. Possibly that could be done with some kind of stunt Deserializer,
|
||||
// but it's not trivial.
|
||||
let (parsed, unrecognized): (ArtiCombinedConfig, _) =
|
||||
tor_config::resolve_return_unrecognized(cfg).unwrap();
|
||||
let results: ResolutionResults<ArtiCombinedConfig> =
|
||||
tor_config::resolve_return_results(cfg).unwrap();
|
||||
|
||||
assert_eq!(&parsed, &default);
|
||||
assert_eq!(&parsed, &empty_config);
|
||||
assert_eq!(&results.value, &default);
|
||||
assert_eq!(&results.value, &empty_config);
|
||||
|
||||
assert_eq!(unrecognized, &[]);
|
||||
parsed
|
||||
assert_eq!(results.unrecognized, &[]);
|
||||
results.value
|
||||
};
|
||||
|
||||
let _ = parses_to_defaults(ARTI_EXAMPLE_CONFIG);
|
||||
|
@ -252,7 +311,7 @@ mod test {
|
|||
let mut bld = ArtiConfig::builder();
|
||||
let mut bld_tor = TorClientConfig::builder();
|
||||
|
||||
bld.proxy().socks_port(Some(9999));
|
||||
bld.proxy().socks_listen(Listen::new_localhost(9999));
|
||||
bld.logging().console("warn");
|
||||
|
||||
bld_tor.tor_network().set_authorities(vec![auth]);
|
||||
|
@ -325,9 +384,122 @@ mod test {
|
|||
assert_eq!(&config.proxy, proxy);
|
||||
}
|
||||
|
||||
/// Comprehensive tests for the various `socks_port` and `dns_port`
|
||||
///
|
||||
/// The "this isn't set at all, just use the default" cases are tested elsewhere.
|
||||
fn compat_ports_listen(
|
||||
f: &str,
|
||||
get_listen: &dyn Fn(&ArtiConfig) -> &Listen,
|
||||
bld_get_port: &dyn Fn(&ArtiConfigBuilder) -> &Option<Option<u16>>,
|
||||
bld_get_listen: &dyn Fn(&ArtiConfigBuilder) -> &Option<Listen>,
|
||||
setter_port: &dyn Fn(&mut ArtiConfigBuilder, Option<u16>) -> &mut ProxyConfigBuilder,
|
||||
setter_listen: &dyn Fn(&mut ArtiConfigBuilder, Listen) -> &mut ProxyConfigBuilder,
|
||||
) {
|
||||
let from_toml = |s: &str| -> ArtiConfigBuilder {
|
||||
let cfg: toml::Value = toml::from_str(dbg!(s)).unwrap();
|
||||
let cfg: ArtiConfigBuilder = cfg.try_into().unwrap();
|
||||
cfg
|
||||
};
|
||||
|
||||
let conflicting_cfgs = [
|
||||
format!("proxy.{}_port = 0 \n proxy.{}_listen = 200", f, f),
|
||||
format!("proxy.{}_port = 100 \n proxy.{}_listen = 0", f, f),
|
||||
format!("proxy.{}_port = 100 \n proxy.{}_listen = 200", f, f),
|
||||
];
|
||||
|
||||
let chk = |cfg: &ArtiConfigBuilder, expected: &Listen| {
|
||||
dbg!(bld_get_listen(cfg), bld_get_port(cfg));
|
||||
let cfg = cfg.build().unwrap();
|
||||
assert_eq!(get_listen(&cfg), expected);
|
||||
};
|
||||
|
||||
let check_setters = |port, expected: &_| {
|
||||
for cfg in chain!(
|
||||
iter::once(ArtiConfig::builder()),
|
||||
conflicting_cfgs.iter().map(|cfg| from_toml(cfg)),
|
||||
) {
|
||||
for listen in match port {
|
||||
None => vec![Listen::new_none(), Listen::new_localhost(0)],
|
||||
Some(port) => vec![Listen::new_localhost(port)],
|
||||
} {
|
||||
let mut cfg = cfg.clone();
|
||||
setter_port(&mut cfg, dbg!(port));
|
||||
setter_listen(&mut cfg, dbg!(listen));
|
||||
chk(&cfg, expected);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
let expected = Listen::new_localhost(100);
|
||||
|
||||
let cfg = from_toml(&format!("proxy.{}_port = 100", f));
|
||||
assert_eq!(bld_get_port(&cfg), &Some(Some(100)));
|
||||
chk(&cfg, &expected);
|
||||
|
||||
let cfg = from_toml(&format!("proxy.{}_listen = 100", f));
|
||||
assert_eq!(bld_get_listen(&cfg), &Some(Listen::new_localhost(100)));
|
||||
chk(&cfg, &expected);
|
||||
|
||||
let cfg = from_toml(&format!(
|
||||
"proxy.{}_port = 100\n proxy.{}_listen = 100",
|
||||
f, f
|
||||
));
|
||||
chk(&cfg, &expected);
|
||||
|
||||
check_setters(Some(100), &expected);
|
||||
}
|
||||
|
||||
{
|
||||
let expected = Listen::new_none();
|
||||
|
||||
let cfg = from_toml(&format!("proxy.{}_port = 0", f));
|
||||
chk(&cfg, &expected);
|
||||
|
||||
let cfg = from_toml(&format!("proxy.{}_listen = 0", f));
|
||||
chk(&cfg, &expected);
|
||||
|
||||
let cfg = from_toml(&format!("proxy.{}_port = 0 \n proxy.{}_listen = 0", f, f));
|
||||
chk(&cfg, &expected);
|
||||
|
||||
check_setters(None, &expected);
|
||||
}
|
||||
|
||||
for cfg in &conflicting_cfgs {
|
||||
let cfg = from_toml(cfg);
|
||||
let err = dbg!(cfg.build()).unwrap_err();
|
||||
assert!(err.to_string().contains("specifying different values"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn ports_listen_socks() {
|
||||
compat_ports_listen(
|
||||
"socks",
|
||||
&|cfg| &cfg.proxy.socks_listen,
|
||||
&|bld| &bld.proxy.socks_port,
|
||||
&|bld| &bld.proxy.socks_listen,
|
||||
&|bld, arg| bld.proxy.socks_port(arg),
|
||||
&|bld, arg| bld.proxy.socks_listen(arg),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn compat_ports_listen_dns() {
|
||||
compat_ports_listen(
|
||||
"dns",
|
||||
&|cfg| &cfg.proxy.dns_listen,
|
||||
&|bld| &bld.proxy.dns_port,
|
||||
&|bld| &bld.proxy.dns_listen,
|
||||
&|bld, arg| bld.proxy.dns_port(arg),
|
||||
&|bld, arg| bld.proxy.dns_listen(arg),
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::dbg_macro)]
|
||||
fn exhaustive_1(example_file: &str, expect_missing: &[&str]) {
|
||||
use itertools::Itertools;
|
||||
use serde_json::Value as JsValue;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
|
@ -446,16 +618,26 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn exhaustive() {
|
||||
exhaustive_1(
|
||||
ARTI_EXAMPLE_CONFIG,
|
||||
// add *old*, obsoleted settings here
|
||||
&[],
|
||||
let mut deprecated = vec![];
|
||||
<(ArtiConfig, TorClientConfig) as tor_config::load::Resolvable>::enumerate_deprecated_keys(
|
||||
&mut |l| {
|
||||
for k in l {
|
||||
deprecated.push(k.to_string());
|
||||
}
|
||||
},
|
||||
);
|
||||
let deprecated = deprecated.iter().map(|s| &**s).collect_vec();
|
||||
|
||||
exhaustive_1(ARTI_EXAMPLE_CONFIG, &deprecated);
|
||||
|
||||
exhaustive_1(
|
||||
OLDEST_SUPPORTED_CONFIG,
|
||||
// add *new*, not present in old file, settings here
|
||||
&["application.allow_running_as_root"],
|
||||
&[
|
||||
"application.allow_running_as_root",
|
||||
"proxy.socks_listen",
|
||||
"proxy.dns_listen",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -509,14 +509,17 @@ where
|
|||
if let Some(proxy_matches) = matches.subcommand_matches("proxy") {
|
||||
let socks_port = match (
|
||||
proxy_matches.value_of("socks-port"),
|
||||
config.proxy().socks_port,
|
||||
config.proxy().socks_listen.localhost_port_legacy()?,
|
||||
) {
|
||||
(Some(p), _) => p.parse().expect("Invalid port specified"),
|
||||
(None, Some(s)) => s,
|
||||
(None, None) => 0,
|
||||
};
|
||||
|
||||
let dns_port = match (proxy_matches.value_of("dns-port"), config.proxy().dns_port) {
|
||||
let dns_port = match (
|
||||
proxy_matches.value_of("dns-port"),
|
||||
config.proxy().dns_listen.localhost_port_legacy()?,
|
||||
) {
|
||||
(Some(p), _) => p.parse().expect("Invalid port specified"),
|
||||
(None, Some(s)) => s,
|
||||
(None, None) => 0,
|
||||
|
|
|
@ -32,7 +32,7 @@ users = "0.11"
|
|||
anyhow = "1.0.23"
|
||||
serde_json = "1.0.50"
|
||||
tempfile = "3"
|
||||
toml = "0.5"
|
||||
toml = "0.5.6"
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
|
|
@ -21,6 +21,7 @@ config = { version = "0.13", default-features = false, features = ["toml"] }
|
|||
derive_builder = { version = "0.11.2", package = "derive_builder_fork_arti" }
|
||||
directories = { version = "4", optional = true }
|
||||
educe = "0.4.6"
|
||||
either = "1"
|
||||
fs-mistrust = { path = "../fs-mistrust", version = "0.4.0" }
|
||||
itertools = "0.10.1"
|
||||
once_cell = "1"
|
||||
|
@ -31,7 +32,7 @@ serde_ignored = "0.1.3"
|
|||
shellexpand = { version = "2.1.2", optional = true }
|
||||
strum = { version = "0.24", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
toml = "0.5"
|
||||
toml = "0.5.6"
|
||||
tor-basic-utils = { path = "../tor-basic-utils", version = "0.3.3" }
|
||||
tor-error = { path = "../tor-error", version = "0.3.2" }
|
||||
tracing = "0.1.18"
|
||||
|
@ -43,7 +44,6 @@ dirs = "4.0.0"
|
|||
rmp-serde = "1"
|
||||
serde_json = "1.0.50"
|
||||
tempfile = "3"
|
||||
toml = "0.5"
|
||||
tracing-test = "0.2"
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
|
|
@ -4,3 +4,7 @@ ADDED: resolve_option_general
|
|||
ADDED: sources::FoundConfigFiles
|
||||
BREAKING: ConfigurationSources takes ConfigurationSource for files, not Paths
|
||||
BREAKING: ConfigurationSources::from_cmdline wants an iterator of defaults
|
||||
BREAKING: load::resolve_ignore_unrecognized renamed resolve_ignore_warnings
|
||||
BREAKING: load::resolve_return_unrecognized replaced with resolve_return_results
|
||||
BREAKING: load::UnrecognizedKey renamed to DisfavouredKey
|
||||
ADDED: Support for tracking deprecated config keys
|
||||
|
|
|
@ -106,7 +106,8 @@ pub use cmdline::CmdLine;
|
|||
pub use config as config_crate;
|
||||
pub use educe;
|
||||
pub use err::{ConfigBuildError, ReconfigureError};
|
||||
pub use load::{resolve, resolve_ignore_unrecognized, resolve_return_unrecognized};
|
||||
pub use itertools::Itertools;
|
||||
pub use load::{resolve, resolve_ignore_warnings, resolve_return_results};
|
||||
pub use misc::*;
|
||||
pub use mut_cfg::MutCfg;
|
||||
pub use paste::paste;
|
||||
|
@ -231,6 +232,83 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Helper for resolving a config item which can be specified in multiple ways
|
||||
///
|
||||
/// Useable when a single configuration item can be specified
|
||||
/// via multiple (alternative) input fields;
|
||||
/// Each input field which is actually present
|
||||
/// should be converted to the common output type,
|
||||
/// and then passed to this function,
|
||||
/// which will handle consistency checks and defaulting.
|
||||
///
|
||||
/// A common use case is deprecated field name/types.
|
||||
/// In that case, the deprecated field names should be added to the appropriate
|
||||
/// [`load::TopLevel::DEPRECATED_KEYS`].
|
||||
///
|
||||
/// `specified` should be an array (or other iterator) of `(key, Option<value>)`
|
||||
/// where `key` is the field name and
|
||||
/// `value` is that field from the builder,
|
||||
/// converted to the common output type `V`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use derive_builder::Builder;
|
||||
/// use serde::{Deserialize, Serialize};
|
||||
/// use tor_config::{impl_standard_builder, ConfigBuildError, Listen, resolve_alternative_specs};
|
||||
///
|
||||
/// #[derive(Debug, Clone, Builder, Eq, PartialEq)]
|
||||
/// #[builder(build_fn(error = "ConfigBuildError"))]
|
||||
/// #[builder(derive(Debug, Serialize, Deserialize))]
|
||||
/// #[allow(clippy::option_option)]
|
||||
/// pub struct ProxyConfig {
|
||||
/// /// Addresses to listen on for incoming SOCKS connections.
|
||||
/// #[builder(field(build = r#"self.resolve_socks_port()?"#))]
|
||||
/// pub(crate) socks_listen: Listen,
|
||||
///
|
||||
/// /// Port to listen on (at localhost) for incoming SOCKS
|
||||
/// /// connections.
|
||||
/// #[builder(setter(strip_option), field(type = "Option<Option<u16>>", build = "()"))]
|
||||
/// pub(crate) socks_port: (),
|
||||
/// }
|
||||
/// impl_standard_builder! { ProxyConfig }
|
||||
///
|
||||
/// impl ProxyConfigBuilder {
|
||||
/// fn resolve_socks_port(&self) -> Result<Listen, ConfigBuildError> {
|
||||
/// resolve_alternative_specs(
|
||||
/// [
|
||||
/// ("socks_listen", self.socks_listen.clone()),
|
||||
/// ("socks_port", self.socks_port.map(Listen::new_localhost_optional)),
|
||||
/// ],
|
||||
/// || Listen::new_localhost(9150),
|
||||
/// )
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
//
|
||||
// Testing: this is tested quit exhaustively in the context of the listen/port handling, in
|
||||
// crates/arti/src/cfg.rs.
|
||||
pub fn resolve_alternative_specs<V, K>(
|
||||
specified: impl IntoIterator<Item = (K, Option<V>)>,
|
||||
default: impl FnOnce() -> V,
|
||||
) -> Result<V, ConfigBuildError>
|
||||
where
|
||||
K: Into<String>,
|
||||
V: Eq,
|
||||
{
|
||||
Ok(specified
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| Some((k, v?)))
|
||||
.dedup_by(|(_, v1), (_, v2)| v1 == v2)
|
||||
.at_most_one()
|
||||
.map_err(|several| ConfigBuildError::Inconsistent {
|
||||
fields: several.into_iter().map(|(k, _v)| k.into()).collect_vec(),
|
||||
problem: "conflicting fields, specifying different values".into(),
|
||||
})?
|
||||
.map(|(_k, v)| v)
|
||||
.unwrap_or_else(default))
|
||||
}
|
||||
|
||||
/// Defines standard impls for a struct with a `Builder`, incl `Default`
|
||||
///
|
||||
/// **Use this.** Do not `#[derive(Builder, Default)]`. That latter approach would produce
|
||||
|
|
|
@ -157,6 +157,11 @@ pub trait Resolvable: Sized {
|
|||
// because that would somehow involve creating `Self` from `ResolveContext`
|
||||
// but `ResolveContext` is completely opaque outside this module.
|
||||
fn resolve(input: &mut ResolveContext) -> Result<Self, ConfigResolveError>;
|
||||
|
||||
/// Return a list of deprecated config keys, as "."-separated strings
|
||||
fn enumerate_deprecated_keys<F>(f: &mut F)
|
||||
where
|
||||
F: FnMut(&'static [&'static str]);
|
||||
}
|
||||
|
||||
/// Top-level configuration struct, made from a deserializable builder
|
||||
|
@ -175,6 +180,9 @@ pub trait TopLevel {
|
|||
///
|
||||
/// Should satisfy `&'_ Self::Builder: Builder<Built=Self>`
|
||||
type Builder: DeserializeOwned;
|
||||
|
||||
/// Deprecated config keys, as "."-separates strings
|
||||
const DEPRECATED_KEYS: &'static [&'static str] = &[];
|
||||
}
|
||||
|
||||
/// `impl Resolvable for (A,B..) where A: Resolvable, B: Resolvable ...`
|
||||
|
@ -198,6 +206,10 @@ macro_rules! define_for_tuples {
|
|||
fn resolve(cfg: &mut ResolveContext) -> Result<Self, ConfigResolveError> {
|
||||
Ok(( $( $A::resolve(cfg)?, )* ))
|
||||
}
|
||||
fn enumerate_deprecated_keys<NF>(f: &mut NF)
|
||||
where NF: FnMut(&'static [&'static str]) {
|
||||
$( $A::enumerate_deprecated_keys(f); )*
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -233,7 +245,7 @@ enum UnrecognizedKeys {
|
|||
/// The keys which remain unrecognized by any consumer
|
||||
///
|
||||
/// If this is empty, we do not (need to) do any further tracking.
|
||||
These(BTreeSet<UnrecognizedKey>),
|
||||
These(BTreeSet<DisfavouredKey>),
|
||||
}
|
||||
use UnrecognizedKeys as UK;
|
||||
|
||||
|
@ -247,7 +259,7 @@ impl UnrecognizedKeys {
|
|||
}
|
||||
|
||||
/// Update in place, intersecting with `other`
|
||||
fn intersect_with(&mut self, other: BTreeSet<UnrecognizedKey>) {
|
||||
fn intersect_with(&mut self, other: BTreeSet<DisfavouredKey>) {
|
||||
match self {
|
||||
UK::AllKeys => *self = UK::These(other),
|
||||
UK::These(self_) => {
|
||||
|
@ -258,16 +270,16 @@ impl UnrecognizedKeys {
|
|||
}
|
||||
}
|
||||
|
||||
/// Key in config file(s) unrecognized by all Resolvables we obtained
|
||||
/// Key in config file(s) which is disfavoured (unrecognized or deprecated)
|
||||
///
|
||||
/// `Display`s in an approximation to TOML format.
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub struct UnrecognizedKey {
|
||||
pub struct DisfavouredKey {
|
||||
/// Can be empty only before returned from this module
|
||||
path: Vec<PathEntry>,
|
||||
}
|
||||
|
||||
/// Element of an UnrecognizedKey
|
||||
/// Element of an DisfavouredKey
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
|
||||
enum PathEntry {
|
||||
/// Array index
|
||||
|
@ -284,23 +296,38 @@ enum PathEntry {
|
|||
/// Inner function used by all the `resolve_*` family
|
||||
fn resolve_inner<T>(
|
||||
input: config::Config,
|
||||
want_unrecognized: bool,
|
||||
) -> Result<(T, Vec<UnrecognizedKey>), ConfigResolveError>
|
||||
want_disfavoured: bool,
|
||||
) -> Result<ResolutionResults<T>, ConfigResolveError>
|
||||
where
|
||||
T: Resolvable,
|
||||
{
|
||||
let mut deprecated = BTreeSet::new();
|
||||
|
||||
if want_disfavoured {
|
||||
T::enumerate_deprecated_keys(&mut |l: &[&str]| {
|
||||
for key in l {
|
||||
match input.get(key) {
|
||||
Err(_) => {}
|
||||
Ok(serde::de::IgnoredAny) => {
|
||||
deprecated.insert(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut lc = ResolveContext {
|
||||
input,
|
||||
unrecognized: if want_unrecognized {
|
||||
unrecognized: if want_disfavoured {
|
||||
UK::AllKeys
|
||||
} else {
|
||||
UK::These(BTreeSet::new())
|
||||
},
|
||||
};
|
||||
|
||||
let val = Resolvable::resolve(&mut lc)?;
|
||||
let value = Resolvable::resolve(&mut lc)?;
|
||||
|
||||
let ign = match lc.unrecognized {
|
||||
let unrecognized = match lc.unrecognized {
|
||||
UK::AllKeys => panic!("all unrecognized, as if we had processed nothing"),
|
||||
UK::These(ign) => ign,
|
||||
}
|
||||
|
@ -308,7 +335,22 @@ where
|
|||
.filter(|ip| !ip.path.is_empty())
|
||||
.collect_vec();
|
||||
|
||||
Ok((val, ign))
|
||||
let deprecated = deprecated
|
||||
.into_iter()
|
||||
.map(|key| {
|
||||
let path = key
|
||||
.split('.')
|
||||
.map(|e| PathEntry::MapEntry(e.into()))
|
||||
.collect_vec();
|
||||
DisfavouredKey { path }
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
Ok(ResolutionResults {
|
||||
value,
|
||||
unrecognized,
|
||||
deprecated,
|
||||
})
|
||||
}
|
||||
|
||||
/// Deserialize and build overall configuration from config sources
|
||||
|
@ -327,29 +369,50 @@ pub fn resolve<T>(input: config::Config) -> Result<T, ConfigResolveError>
|
|||
where
|
||||
T: Resolvable,
|
||||
{
|
||||
let (val, ign) = resolve_inner(input, true)?;
|
||||
for ign in ign {
|
||||
let ResolutionResults {
|
||||
value,
|
||||
unrecognized,
|
||||
deprecated,
|
||||
} = resolve_inner(input, true)?;
|
||||
for depr in deprecated {
|
||||
warn!("deprecated configuration key: {}", &depr);
|
||||
}
|
||||
for ign in unrecognized {
|
||||
warn!("unrecognized configuration key: {}", &ign);
|
||||
}
|
||||
Ok(val)
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Deserialize and build overall configuration, reporting unrecognized keys in the return value
|
||||
pub fn resolve_return_unrecognized<T>(
|
||||
pub fn resolve_return_results<T>(
|
||||
input: config::Config,
|
||||
) -> Result<(T, Vec<UnrecognizedKey>), ConfigResolveError>
|
||||
) -> Result<ResolutionResults<T>, ConfigResolveError>
|
||||
where
|
||||
T: Resolvable,
|
||||
{
|
||||
resolve_inner(input, true)
|
||||
}
|
||||
|
||||
/// Results of a successful `resolve_return_disfavoured`
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct ResolutionResults<T> {
|
||||
/// The configuration, successfully parsed
|
||||
pub value: T,
|
||||
|
||||
/// Any config keys which were found in the input, but not recognized (and so, ignored)
|
||||
pub unrecognized: Vec<DisfavouredKey>,
|
||||
|
||||
/// Any config keys which were found, but have been declared deprecated
|
||||
pub deprecated: Vec<DisfavouredKey>,
|
||||
}
|
||||
|
||||
/// Deserialize and build overall configuration, silently ignoring unrecognized config keys
|
||||
pub fn resolve_ignore_unrecognized<T>(input: config::Config) -> Result<T, ConfigResolveError>
|
||||
pub fn resolve_ignore_warnings<T>(input: config::Config) -> Result<T, ConfigResolveError>
|
||||
where
|
||||
T: Resolvable,
|
||||
{
|
||||
Ok(resolve_inner(input, false)?.0)
|
||||
Ok(resolve_inner(input, false)?.value)
|
||||
}
|
||||
|
||||
impl<T> Resolvable for T
|
||||
|
@ -386,10 +449,17 @@ where
|
|||
let built = builder.build()?;
|
||||
Ok(built)
|
||||
}
|
||||
|
||||
fn enumerate_deprecated_keys<NF>(f: &mut NF)
|
||||
where
|
||||
NF: FnMut(&'static [&'static str]),
|
||||
{
|
||||
f(T::DEPRECATED_KEYS);
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns a [`serde_ignored::Path`] (which is borrowed) into an owned `UnrecognizedKey`
|
||||
fn copy_path(mut path: &serde_ignored::Path) -> UnrecognizedKey {
|
||||
/// Turns a [`serde_ignored::Path`] (which is borrowed) into an owned `DisfavouredKey`
|
||||
fn copy_path(mut path: &serde_ignored::Path) -> DisfavouredKey {
|
||||
use serde_ignored::Path as SiP;
|
||||
use PathEntry as PE;
|
||||
|
||||
|
@ -407,7 +477,7 @@ fn copy_path(mut path: &serde_ignored::Path) -> UnrecognizedKey {
|
|||
path = new_path;
|
||||
}
|
||||
descend.reverse();
|
||||
UnrecognizedKey { path: descend }
|
||||
DisfavouredKey { path: descend }
|
||||
}
|
||||
|
||||
/// Computes the intersection, resolving ignorances at different depths
|
||||
|
@ -428,9 +498,9 @@ fn copy_path(mut path: &serde_ignored::Path) -> UnrecognizedKey {
|
|||
/// If the inputs are not minimal, the output may not be either
|
||||
/// (although `serde_ignored` gives us minimal sets, so that case is not important).
|
||||
fn intersect_unrecognized_lists(
|
||||
al: BTreeSet<UnrecognizedKey>,
|
||||
bl: BTreeSet<UnrecognizedKey>,
|
||||
) -> BTreeSet<UnrecognizedKey> {
|
||||
al: BTreeSet<DisfavouredKey>,
|
||||
bl: BTreeSet<DisfavouredKey>,
|
||||
) -> BTreeSet<DisfavouredKey> {
|
||||
//eprintln!("INTERSECT:");
|
||||
//for ai in &al { eprintln!("A: {}", ai); }
|
||||
//for bi in &bl { eprintln!("B: {}", bi); }
|
||||
|
@ -535,7 +605,7 @@ fn intersect_unrecognized_lists(
|
|||
output
|
||||
}
|
||||
|
||||
impl Display for UnrecognizedKey {
|
||||
impl Display for DisfavouredKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use PathEntry as PE;
|
||||
if self.path.is_empty() {
|
||||
|
@ -581,9 +651,9 @@ mod test {
|
|||
use derive_builder::Builder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn parse_test_set(l: &[&str]) -> BTreeSet<UnrecognizedKey> {
|
||||
fn parse_test_set(l: &[&str]) -> BTreeSet<DisfavouredKey> {
|
||||
l.iter()
|
||||
.map(|s| UnrecognizedKey {
|
||||
.map(|s| DisfavouredKey {
|
||||
path: s
|
||||
.split('.')
|
||||
.map(|s| PathEntry::MapEntry(s.into()))
|
||||
|
@ -645,7 +715,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_display_key() {
|
||||
let chk = |exp, path: &[PathEntry]| {
|
||||
assert_eq! { UnrecognizedKey { path: path.into() }.to_string(), exp };
|
||||
assert_eq! { DisfavouredKey { path: path.into() }.to_string(), exp };
|
||||
};
|
||||
let me = |s: &str| PathEntry::MapEntry(s.into());
|
||||
use PathEntry::ArrayIndex as AI;
|
||||
|
@ -677,10 +747,14 @@ mod test {
|
|||
struct TestConfigB {
|
||||
#[builder(default)]
|
||||
b: String,
|
||||
|
||||
#[builder(default)]
|
||||
old: bool,
|
||||
}
|
||||
impl_standard_builder! { TestConfigB }
|
||||
impl TopLevel for TestConfigB {
|
||||
type Builder = TestConfigBBuilder;
|
||||
const DEPRECATED_KEYS: &'static [&'static str] = &["old"];
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -688,6 +762,7 @@ mod test {
|
|||
let test_data = r#"
|
||||
wombat = 42
|
||||
a = "hi"
|
||||
old = true
|
||||
"#;
|
||||
let source = config::File::from_str(test_data, config::FileFormat::Toml);
|
||||
|
||||
|
@ -696,16 +771,22 @@ mod test {
|
|||
.build()
|
||||
.unwrap();
|
||||
|
||||
let _: (TestConfigA, TestConfigB) = resolve_ignore_unrecognized(cfg.clone()).unwrap();
|
||||
let _: (TestConfigA, TestConfigB) = resolve_ignore_warnings(cfg.clone()).unwrap();
|
||||
|
||||
let ((a, b), ign): ((TestConfigA, TestConfigB), _) =
|
||||
resolve_return_unrecognized(cfg).unwrap();
|
||||
let resolved: ResolutionResults<(TestConfigA, TestConfigB)> =
|
||||
resolve_return_results(cfg).unwrap();
|
||||
let (a, b) = resolved.value;
|
||||
|
||||
let ign = ign.into_iter().map(|ik| ik.to_string()).collect_vec();
|
||||
let mk_strings =
|
||||
|l: Vec<DisfavouredKey>| l.into_iter().map(|ik| ik.to_string()).collect_vec();
|
||||
|
||||
let ign = mk_strings(resolved.unrecognized);
|
||||
let depr = mk_strings(resolved.deprecated);
|
||||
|
||||
assert_eq! { &a, &TestConfigA { a: "hi".into() } };
|
||||
assert_eq! { &b, &TestConfigB { b: "".into() } };
|
||||
assert_eq! { &b, &TestConfigB { b: "".into(), old: true } };
|
||||
assert_eq! { ign, &["wombat"] };
|
||||
assert_eq! { depr, &["old"] };
|
||||
|
||||
let _ = TestConfigA::builder();
|
||||
let _ = TestConfigB::builder();
|
||||
|
@ -742,15 +823,15 @@ mod test {
|
|||
.unwrap();
|
||||
{
|
||||
// First try "A", then "C".
|
||||
let res1: Result<((TestConfigA, TestConfigC), Vec<UnrecognizedKey>), _> =
|
||||
resolve_return_unrecognized(cfg.clone());
|
||||
let res1: Result<ResolutionResults<(TestConfigA, TestConfigC)>, _> =
|
||||
resolve_return_results(cfg.clone());
|
||||
assert!(res1.is_err());
|
||||
assert!(matches!(res1, Err(ConfigResolveError::Deserialize(_))));
|
||||
}
|
||||
{
|
||||
// Now the other order: first try "C", then "A".
|
||||
let res2: Result<((TestConfigC, TestConfigA), Vec<UnrecognizedKey>), _> =
|
||||
resolve_return_unrecognized(cfg.clone());
|
||||
let res2: Result<ResolutionResults<(TestConfigC, TestConfigA)>, _> =
|
||||
resolve_return_results(cfg.clone());
|
||||
assert!(res2.is_err());
|
||||
assert!(matches!(res2, Err(ConfigResolveError::Deserialize(_))));
|
||||
}
|
||||
|
|
|
@ -4,8 +4,13 @@
|
|||
//! and layers, but which don't depend on specific elements of the Tor system.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::iter;
|
||||
use std::net;
|
||||
use std::num::NonZeroU16;
|
||||
|
||||
use educe::Educe;
|
||||
use either::Either;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumString, IntoStaticStr};
|
||||
|
||||
|
@ -74,14 +79,251 @@ impl TryFrom<PaddingLevelSerde> for PaddingLevel {
|
|||
}
|
||||
}
|
||||
|
||||
/// Specification of (possibly) something to listen on (eg, a port, or some addresses/ports)
|
||||
///
|
||||
/// Can represent, at least:
|
||||
/// * "do not listen"
|
||||
/// * Listen on the following port on localhost (IPv6 and IPv4)
|
||||
/// * Listen on precisely the following address and port
|
||||
/// * Listen on several addresses/ports
|
||||
///
|
||||
/// Currently only IP (v6 and v4) is supported.
|
||||
#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "ListenSerde", into = "ListenSerde")]
|
||||
#[derive(Educe)]
|
||||
#[educe(Default)]
|
||||
pub struct Listen(Vec<ListenItem>);
|
||||
|
||||
impl Listen {
|
||||
/// Create a new `Listen` specifying no addresses (no listening)
|
||||
pub fn new_none() -> Listen {
|
||||
Listen(vec![])
|
||||
}
|
||||
|
||||
/// Create a new `Listen` specifying listening on a port on localhost
|
||||
///
|
||||
/// Special case: if `port` is zero, specifies no listening.
|
||||
pub fn new_localhost(port: u16) -> Listen {
|
||||
Listen(
|
||||
port.try_into()
|
||||
.ok()
|
||||
.map(ListenItem::Localhost)
|
||||
.into_iter()
|
||||
.collect_vec(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a new `Listen`, possibly specifying listening on a port on localhost
|
||||
///
|
||||
/// Special case: if `port` is `Some(0)`, also specifies no listening.
|
||||
pub fn new_localhost_optional(port: Option<u16>) -> Listen {
|
||||
Self::new_localhost(port.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// List the network socket addresses to listen on
|
||||
///
|
||||
/// Each returned item is a list of `SocketAddr`,
|
||||
/// of which *at least one* must be successfully bound.
|
||||
/// It is OK if the others (up to all but one of them)
|
||||
/// fail with `EAFNOSUPPORT` ("Address family not supported").
|
||||
/// This allows handling of support, or non-support,
|
||||
/// for particular address faimilies, eg IPv6 vs IPv4 localhost.
|
||||
/// Other errors (eg, `EADDRINUSE`) should always be treated as serious problems.
|
||||
///
|
||||
/// Fails if the listen spec involves listening on things other than IP addresses.
|
||||
/// (Currently that is not possible.)
|
||||
pub fn ip_addrs(
|
||||
&self,
|
||||
) -> Result<
|
||||
impl Iterator<Item = impl Iterator<Item = net::SocketAddr> + '_> + '_,
|
||||
ListenUnsupported,
|
||||
> {
|
||||
Ok(self.0.iter().map(|i| i.iter()))
|
||||
}
|
||||
|
||||
/// Get the localhost port to listen on
|
||||
///
|
||||
/// Returns `None` if listening is configured to be disabled.
|
||||
///
|
||||
/// Fails, giving an unsupported error, if the configuration
|
||||
/// isn't just "listen on a single localhost port".
|
||||
pub fn localhost_port_legacy(&self) -> Result<Option<u16>, ListenUnsupported> {
|
||||
use ListenItem as LI;
|
||||
Ok(match &*self.0 {
|
||||
[] => None,
|
||||
[LI::Localhost(port)] => Some((*port).into()),
|
||||
_ => return Err(ListenUnsupported {}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Listen`] configuration specified something not supported by application code
|
||||
#[derive(thiserror::Error, Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
#[error("Unsupported listening configration")]
|
||||
pub struct ListenUnsupported {}
|
||||
|
||||
/// One item in the `Listen`
|
||||
///
|
||||
/// We distinguish `Localhost`,
|
||||
/// rather than just storing two `net:SocketAddr`,
|
||||
/// so that we can handle localhost (which means to address families) specially
|
||||
/// in order to implement `localhost_port_deprecated()`.
|
||||
#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ListenItem {
|
||||
/// One port, both IPv6 and IPv4
|
||||
Localhost(NonZeroU16),
|
||||
|
||||
/// Any other single socket address
|
||||
General(net::SocketAddr),
|
||||
}
|
||||
|
||||
impl ListenItem {
|
||||
/// Return the `SocketAddr`s implied by this item
|
||||
fn iter(&self) -> impl Iterator<Item = net::SocketAddr> + '_ {
|
||||
use net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use ListenItem as LI;
|
||||
match self {
|
||||
&LI::Localhost(port) => Either::Left({
|
||||
let port = port.into();
|
||||
let addrs: [IpAddr; 2] = [Ipv6Addr::LOCALHOST.into(), Ipv4Addr::LOCALHOST.into()];
|
||||
addrs
|
||||
.into_iter()
|
||||
.map(move |ip| net::SocketAddr::new(ip, port))
|
||||
}),
|
||||
LI::General(addr) => Either::Right(iter::once(addr).cloned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How we (de) serialize a [`Listen`]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ListenSerde {
|
||||
/// for `listen = false` (in TOML syntax)
|
||||
Bool(bool),
|
||||
|
||||
/// A bare item
|
||||
One(ListenItemSerde),
|
||||
|
||||
/// An item in a list
|
||||
List(Vec<ListenItemSerde>),
|
||||
}
|
||||
|
||||
/// One item in the list of a list-ish `Listen`, or the plain value
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ListenItemSerde {
|
||||
/// An integer.
|
||||
///
|
||||
/// When appearing "loose" (in ListenSerde::One), `0` is parsed as none.
|
||||
Port(u16),
|
||||
|
||||
/// An string which will be parsed as an address and port
|
||||
///
|
||||
/// When appearing "loose" (in ListenSerde::One), `""` is parsed as none.
|
||||
String(String),
|
||||
}
|
||||
|
||||
// This implementation isn't fallible, but clippy thinks it is because of the unwrap.
|
||||
// The unwrap is just there because we can't pattern-match on a Vec
|
||||
#[allow(clippy::fallible_impl_from)]
|
||||
impl From<Listen> for ListenSerde {
|
||||
fn from(l: Listen) -> ListenSerde {
|
||||
let l = l.0;
|
||||
match l.len() {
|
||||
0 => ListenSerde::Bool(false),
|
||||
1 => ListenSerde::One(l.into_iter().next().expect("len=1 but no next").into()),
|
||||
_ => ListenSerde::List(l.into_iter().map(Into::into).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<ListenItem> for ListenItemSerde {
|
||||
fn from(i: ListenItem) -> ListenItemSerde {
|
||||
use ListenItem as LI;
|
||||
use ListenItemSerde as LIS;
|
||||
match i {
|
||||
LI::Localhost(port) => LIS::Port(port.into()),
|
||||
LI::General(addr) => LIS::String(addr.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Listen configuration is invalid
|
||||
#[derive(thiserror::Error, Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum InvalidListen {
|
||||
/// Bool was `true` but that's not an address.
|
||||
#[error("Invalid listen specification: need actual addr/port, or `false`; not `true`")]
|
||||
InvalidBool,
|
||||
|
||||
/// Specified listen was a string but couldn't parse to a [`net::SocketAddr`].
|
||||
#[error("Invalid listen specification: failed to parse string: {0}")]
|
||||
InvalidString(#[from] net::AddrParseError),
|
||||
|
||||
/// Specified listen was a list containing a zero integer
|
||||
#[error("Invalid listen specification: zero (for no port) not permitted in list")]
|
||||
ZeroPortInList,
|
||||
}
|
||||
impl TryFrom<ListenSerde> for Listen {
|
||||
type Error = InvalidListen;
|
||||
|
||||
fn try_from(l: ListenSerde) -> Result<Listen, Self::Error> {
|
||||
use ListenSerde as LS;
|
||||
Ok(Listen(match l {
|
||||
LS::Bool(false) => vec![],
|
||||
LS::Bool(true) => return Err(InvalidListen::InvalidBool),
|
||||
LS::One(i) if i.means_none() => vec![],
|
||||
LS::One(i) => vec![i.try_into()?],
|
||||
LS::List(l) => l.into_iter().map(|i| i.try_into()).try_collect()?,
|
||||
}))
|
||||
}
|
||||
}
|
||||
impl ListenItemSerde {
|
||||
/// Is this item actually a sentinel, meaning "don't listen, disable this thing"?
|
||||
///
|
||||
/// Allowed only bare, not in a list.
|
||||
fn means_none(&self) -> bool {
|
||||
use ListenItemSerde as LIS;
|
||||
match self {
|
||||
&LIS::Port(port) => port == 0,
|
||||
LIS::String(s) => s.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TryFrom<ListenItemSerde> for ListenItem {
|
||||
type Error = InvalidListen;
|
||||
|
||||
fn try_from(i: ListenItemSerde) -> Result<ListenItem, Self::Error> {
|
||||
use ListenItem as LI;
|
||||
use ListenItemSerde as LIS;
|
||||
Ok(match i {
|
||||
LIS::String(s) => LI::General(s.parse()?),
|
||||
LIS::Port(p) => LI::Localhost(p.try_into().map_err(|_| InvalidListen::ZeroPortInList)?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// @@ begin test lint list maintained by maint/add_warning @@
|
||||
#![allow(clippy::bool_assert_comparison)]
|
||||
#![allow(clippy::clone_on_copy)]
|
||||
#![allow(clippy::dbg_macro)]
|
||||
#![allow(clippy::print_stderr)]
|
||||
#![allow(clippy::print_stdout)]
|
||||
#![allow(clippy::unwrap_used)]
|
||||
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct TestConfigFile {
|
||||
#[serde(default)]
|
||||
padding: PaddingLevel,
|
||||
|
||||
#[serde(default)]
|
||||
listen: Option<Listen>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -109,4 +351,99 @@ mod test {
|
|||
chk_e(r#"padding = "unknown""#);
|
||||
chk_e(r#"padding = "Normal""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_parse() {
|
||||
use net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use ListenItem as LI;
|
||||
|
||||
let localhost6 = |p| SocketAddr::new(Ipv6Addr::LOCALHOST.into(), p);
|
||||
let localhost4 = |p| SocketAddr::new(Ipv4Addr::LOCALHOST.into(), p);
|
||||
let unspec6 = |p| SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), p);
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)] // we do this for consistency
|
||||
fn chk(
|
||||
exp_i: Vec<ListenItem>,
|
||||
exp_addrs: Result<Vec<Vec<SocketAddr>>, ()>,
|
||||
exp_lpd: Result<Option<u16>, ()>,
|
||||
s: &str,
|
||||
) {
|
||||
let tc: TestConfigFile = toml::from_str(s).expect(s);
|
||||
let ll = tc.listen.unwrap();
|
||||
eprintln!("s={:?} ll={:?}", &s, &ll);
|
||||
assert_eq!(ll, Listen(exp_i));
|
||||
assert_eq!(
|
||||
ll.ip_addrs()
|
||||
.map(|a| a.map(|l| l.collect_vec()).collect_vec())
|
||||
.map_err(|_| ()),
|
||||
exp_addrs
|
||||
);
|
||||
assert_eq!(ll.localhost_port_legacy().map_err(|_| ()), exp_lpd);
|
||||
}
|
||||
|
||||
let chk_err = |exp, s: &str| {
|
||||
let got: Result<TestConfigFile, _> = toml::from_str(s);
|
||||
let got = got.expect_err(s).to_string();
|
||||
assert!(got.contains(exp), "s={:?} got={:?} exp={:?}", s, got, exp);
|
||||
};
|
||||
|
||||
let chk_none = |s: &str| {
|
||||
chk(vec![], Ok(vec![]), Ok(None), &format!("listen = {}", s));
|
||||
chk_err(
|
||||
"", /* any error will do */
|
||||
&format!("listen = [ {} ]", s),
|
||||
);
|
||||
};
|
||||
|
||||
let chk_1 = |v: ListenItem, addrs: Vec<Vec<SocketAddr>>, port, s| {
|
||||
chk(
|
||||
vec![v.clone()],
|
||||
Ok(addrs.clone()),
|
||||
port,
|
||||
&format!("listen = {}", s),
|
||||
);
|
||||
chk(
|
||||
vec![v.clone()],
|
||||
Ok(addrs.clone()),
|
||||
port,
|
||||
&format!("listen = [ {} ]", s),
|
||||
);
|
||||
chk(
|
||||
vec![v, LI::Localhost(23.try_into().unwrap())],
|
||||
Ok([addrs, vec![vec![localhost6(23), localhost4(23)]]]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect()),
|
||||
Err(()),
|
||||
&format!("listen = [ {}, 23 ]", s),
|
||||
);
|
||||
};
|
||||
|
||||
chk_none(r#""""#);
|
||||
chk_none(r#"0"#);
|
||||
chk_none(r#"false"#);
|
||||
chk(vec![], Ok(vec![]), Ok(None), r#"listen = []"#);
|
||||
|
||||
chk_1(
|
||||
LI::Localhost(42.try_into().unwrap()),
|
||||
vec![vec![localhost6(42), localhost4(42)]],
|
||||
Ok(Some(42)),
|
||||
"42",
|
||||
);
|
||||
chk_1(
|
||||
LI::General(unspec6(56)),
|
||||
vec![vec![unspec6(56)]],
|
||||
Err(()),
|
||||
r#""[::]:56""#,
|
||||
);
|
||||
|
||||
let chk_err_1 = |e, el, s| {
|
||||
chk_err(e, &format!("listen = {}", s));
|
||||
chk_err(el, &format!("listen = [ {} ]", s));
|
||||
chk_err(el, &format!("listen = [ 23, {}, 77 ]", s));
|
||||
};
|
||||
|
||||
chk_err_1("need actual addr/port", "did not match any variant", "true");
|
||||
chk_err("did not match any variant", r#"listen = [ [] ]"#);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue