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:
Ian Jackson 2022-08-25 18:08:46 +00:00
commit cbb30b5e75
11 changed files with 755 additions and 66 deletions

2
Cargo.lock generated
View File

@ -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",

View File

@ -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 }

View File

@ -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]

View File

@ -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",
],
);
}
}

View File

@ -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,

View File

@ -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"]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(_))));
}

View File

@ -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 = [ [] ]"#);
}
}