//! Configuration for the Arti command line application // // (This 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::resolve_alternative_specs; #[cfg(feature = "rpc")] use tor_config::CfgPath; pub(crate) use tor_config::{impl_standard_builder, ConfigBuildError, Listen}; use crate::{LoggingConfig, LoggingConfigBuilder}; /// Example file demonstrating our our configuration and the default options. /// /// The options in this example file are all commented out; /// the actual defaults are done via builder attributes in all the Rust config structs. pub const ARTI_EXAMPLE_CONFIG: &str = concat!(include_str!("./arti-example-config.toml"),); /// Test case file for the oldest version of the config we still support. /// /// (When updating, copy `arti-example-config.toml` from the earliest version we want to /// be compatible with.) // // Probably, in the long run, we will want to make this architecture more general: we'll want // to have a larger number of examples to test, and we won't want to write a separate constant // for each. Probably in that case, we'll want a directory of test examples, and we'll want to // traverse the whole directory. // // Compare C tor, look at conf_examples and conf_failures - each of the subdirectories there is // an example configuration situation that we wanted to validate. // // NB here in Arti the OLDEST_SUPPORTED_CONFIG and the ARTI_EXAMPLE_CONFIG are tested // somewhat differently: we test that the current example is *exhaustive*, not just // parsable. #[cfg(test)] const OLDEST_SUPPORTED_CONFIG: &str = concat!(include_str!("./oldest-supported-config.toml"),); /// Structure to hold our application configuration options #[derive(Debug, Clone, Builder, Eq, PartialEq)] #[builder(build_fn(error = "ConfigBuildError"))] #[builder(derive(Debug, Serialize, Deserialize))] pub struct ApplicationConfig { /// If true, we should watch our configuration files for changes, and reload /// our configuration when they change. /// /// Note that this feature may behave in unexpected ways if the path to the /// directory holding our configuration files changes its identity (because /// an intermediate symlink is changed, because the directory is removed and /// recreated, or for some other reason). #[builder(default)] pub(crate) watch_configuration: bool, /// If true, we should allow other applications not owned by the system /// administrator to monitor the Arti application and inspect its memory. /// /// Otherwise, we take various steps (including disabling core dumps) to /// make it harder for other programs to view our internal state. /// /// This option has no effect when arti is built without the `harden` /// feature. When `harden` is not enabled, debugger attachment is permitted /// whether this option is set or not. #[builder(default)] pub(crate) permit_debugging: bool, /// If true, then we do not exit when we are running as `root`. /// /// This has no effect on Windows. #[builder(default)] pub(crate) allow_running_as_root: bool, } impl_standard_builder! { ApplicationConfig } /// Resolves values from `$field_listen` and `$field_port` (compat) into a `Listen` /// /// For `dns` and `proxy`. /// /// Handles defaulting, and normalization, 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 { /// 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>", 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 listen on (at localhost) for incoming DNS connections. /// /// 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>", build = "()") )] #[builder_setter_attr(deprecated)] pub(crate) dns_port: (), } impl_standard_builder! { ProxyConfig } /// Configuration for system resources used by Tor. /// /// You cannot change this section on a running Arti client. #[derive(Debug, Clone, Builder, Eq, PartialEq)] #[builder(build_fn(error = "ConfigBuildError"))] #[builder(derive(Debug, Serialize, Deserialize))] #[non_exhaustive] pub struct SystemConfig { /// Maximum number of file descriptors we should launch with #[builder(setter(into), default = "default_max_files()")] pub(crate) max_files: u64, } impl_standard_builder! { SystemConfig } /// Return the default maximum number of file descriptors to launch with. fn default_max_files() -> u64 { 16384 } /// Configuration for Arti's RPC subsystem. /// /// You cannot change this section on a running Arti client. #[derive(Debug, Clone, Builder, Eq, PartialEq)] #[builder(build_fn(error = "ConfigBuildError"))] #[builder(derive(Debug, Serialize, Deserialize))] #[non_exhaustive] pub struct RpcConfig { /// Location to listen for incoming RPC connections. #[cfg(feature = "rpc")] #[builder(default = "default_rpc_path()")] pub(crate) rpc_listen: Option, } /// Return the default value for our configuration path. #[cfg(feature = "rpc")] #[allow(clippy::unnecessary_wraps)] fn default_rpc_path() -> Option { let s = if cfg!(target_os = "windows") { r"\\.\pipe\arti\SOCKET" } else { "~/.local/run/arti/SOCKET" }; Some(CfgPath::new(s.to_string())) } /// Structure to hold Arti's configuration options, whether from a /// configuration file or the command line. // /// These options are declared in a public crate outside of `arti` so that other /// applications can parse and use them, if desired. If you're only embedding /// arti via `arti-client`, and you don't want to use Arti's configuration /// format, use [`arti_client::TorClientConfig`] instead. /// /// By default, Arti will run using the default Tor network, store state and /// cache information to a per-user set of directories shared by all /// that user's applications, and run a SOCKS client on a local port. /// /// NOTE: These are NOT the final options or their final layout. Expect NO /// stability here. #[derive(Debug, Builder, Clone, Eq, PartialEq)] #[builder(derive(Serialize, Deserialize, Debug))] #[builder(build_fn(error = "ConfigBuildError"))] pub struct ArtiConfig { /// Configuration for application behavior. #[builder(sub_builder)] #[builder_field_attr(serde(default))] application: ApplicationConfig, /// Configuration for proxy listeners #[builder(sub_builder)] #[builder_field_attr(serde(default))] proxy: ProxyConfig, /// Logging configuration #[builder(sub_builder)] #[builder_field_attr(serde(default))] logging: LoggingConfig, /// Configuration for RPC subsystem #[cfg(feature = "rpc")] #[builder(sub_builder)] #[builder_field_attr(serde(default))] rpc: RpcConfig, /// Information on system resources used by Arti. #[builder(sub_builder)] #[builder_field_attr(serde(default))] pub(crate) system: SystemConfig, } 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 /// /// Used primarily as a type parameter on calls to [`tor_config::resolve`] pub type ArtiCombinedConfig = (ArtiConfig, TorClientConfig); impl ArtiConfig { /// Return the [`ApplicationConfig`] for this configuration. pub fn application(&self) -> &ApplicationConfig { &self.application } /// Return the [`LoggingConfig`] for this configuration. pub fn logging(&self) -> &LoggingConfig { &self.logging } /// Return the [`ProxyConfig`] for this configuration. pub fn proxy(&self) -> &ProxyConfig { &self.proxy } /// Return the [`RpcConfig`] for this configuration. #[cfg(feature = "rpc")] pub fn rpc(&self) -> &RpcConfig { &self.rpc } } #[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::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::unchecked_duration_subtraction)] //! // Saves adding many individual #[cfg], or a sub-module #![cfg_attr(not(feature = "pt-client"), allow(dead_code))] use arti_client::config::dir; use arti_client::config::TorClientConfigBuilder; use itertools::{chain, Itertools}; use regex::Regex; use std::collections::HashSet; use std::iter; use std::time::Duration; use tor_config::load::{ConfigResolveError, ResolutionResults}; use super::*; fn uncomment_example_settings(template: &str) -> String { let re = Regex::new(r#"(?m)^\#([^ \n])"#).unwrap(); re.replace_all(template, |cap: ®ex::Captures<'_>| -> _ { cap.get(1).unwrap().as_str().to_string() }) .into() } #[test] fn default_config() { // See comment for OLDEST_SUPPORTED_CONFIG for likely future evolution let empty_config = config::Config::builder().build().unwrap(); let empty_config: ArtiCombinedConfig = tor_config::resolve(empty_config).unwrap(); let default = (ArtiConfig::default(), TorClientConfig::default()); let parses_to_defaults = |example: &str, known_unrecognized_options: &[&str]| { let cfg = config::Config::builder() .add_source(config::File::from_str(example, config::FileFormat::Toml)) .build() .unwrap(); // This tests that the example settings do not *contradict* the defaults. // // 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 results: ResolutionResults = tor_config::resolve_return_results(cfg).unwrap(); assert_eq!(&results.value, &default); assert_eq!(&results.value, &empty_config); // We serialize the DisfavouredKey entries to strings to compare them against // `known_unrecognized_options`. let unrecognized = results .unrecognized .iter() .map(|k| k.to_string()) .collect_vec(); assert_eq!(&unrecognized, &known_unrecognized_options); results.value }; #[allow(unused_mut)] let mut known_unrecognized_options_all = vec![]; #[allow(unused_mut)] let mut known_unrecognized_options_new = vec![]; #[cfg(target_family = "windows")] known_unrecognized_options_all.extend([ "storage.permissions.trust_group", "storage.permissions.trust_user", ]); // Additional cfg blocks will need to be added whenever we add features // which have example config, since if the feature isn't enabled, // those keys are ignored (unrecognized). // The unrecognized options in new are those that are only new, plus those in all known_unrecognized_options_new.extend(known_unrecognized_options_all.clone()); let unrecognized_sections = |options| { let options: &[&str] = options; options .iter() .cloned() .filter(|o| !o.contains(".")) .collect_vec() }; let _ = parses_to_defaults( ARTI_EXAMPLE_CONFIG, &unrecognized_sections(&known_unrecognized_options_new), ); let _ = parses_to_defaults( OLDEST_SUPPORTED_CONFIG, &unrecognized_sections(&known_unrecognized_options_all), ); let built_default = ( ArtiConfigBuilder::default().build().unwrap(), TorClientConfigBuilder::default().build().unwrap(), ); let parsed = parses_to_defaults( &uncomment_example_settings(ARTI_EXAMPLE_CONFIG), &known_unrecognized_options_new, ); let parsed_old = parses_to_defaults( &uncomment_example_settings(OLDEST_SUPPORTED_CONFIG), &known_unrecognized_options_all, ); assert_eq!(&parsed, &built_default); assert_eq!(&parsed_old, &built_default); assert_eq!(&default, &built_default); } #[test] fn builder() { use tor_config::CfgPath; let sec = std::time::Duration::from_secs(1); let auth = dir::Authority::builder() .name("Fred") .v3ident([22; 20].into()) .clone(); let mut fallback = dir::FallbackDir::builder(); fallback .rsa_identity([23; 20].into()) .ed_identity([99; 32].into()) .orports() .push("127.0.0.7:7".parse().unwrap()); let mut bld = ArtiConfig::builder(); let mut bld_tor = TorClientConfig::builder(); bld.proxy().socks_listen(Listen::new_localhost(9999)); bld.logging().console("warn"); bld_tor.tor_network().set_authorities(vec![auth]); bld_tor.tor_network().set_fallback_caches(vec![fallback]); bld_tor .storage() .cache_dir(CfgPath::new("/var/tmp/foo".to_owned())) .state_dir(CfgPath::new("/var/tmp/bar".to_owned())); bld_tor.download_schedule().retry_certs().attempts(10); bld_tor.download_schedule().retry_certs().initial_delay(sec); bld_tor.download_schedule().retry_certs().parallelism(3); bld_tor.download_schedule().retry_microdescs().attempts(30); bld_tor .download_schedule() .retry_microdescs() .initial_delay(10 * sec); bld_tor .download_schedule() .retry_microdescs() .parallelism(9); bld_tor .override_net_params() .insert("wombats-per-quokka".to_owned(), 7); bld_tor .path_rules() .ipv4_subnet_family_prefix(20) .ipv6_subnet_family_prefix(48); bld_tor.preemptive_circuits().disable_at_threshold(12); bld_tor .preemptive_circuits() .set_initial_predicted_ports(vec![80, 443]); bld_tor .preemptive_circuits() .prediction_lifetime(Duration::from_secs(3600)) .min_exit_circs_for_port(2); bld_tor .circuit_timing() .max_dirtiness(90 * sec) .request_timeout(10 * sec) .request_max_retries(22) .request_loyalty(3600 * sec); bld_tor.address_filter().allow_local_addrs(true); let val = bld.build().unwrap(); assert_ne!(val, ArtiConfig::default()); } #[test] fn articonfig_application() { let config = ArtiConfig::default(); let application = config.application(); assert_eq!(&config.application, application); } #[test] fn articonfig_logging() { let config = ArtiConfig::default(); let logging = config.logging(); assert_eq!(&config.logging, logging); } #[test] fn articonfig_proxy() { let config = ArtiConfig::default(); let proxy = config.proxy(); 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>, bld_get_listen: &dyn Fn(&ArtiConfigBuilder) -> &Option, setter_port: &dyn Fn(&mut ArtiConfigBuilder, Option) -> &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), ); } /// Config keys which would be recognised by the parser, but are missing from the examples /// /// Used by `exhaustive_1`. const CONFIG_KEYS_EXPECT_NO_EXAMPLE: &[&str] = &[ // TODO: Provide a test case that parses the `[bridges.transports]` example. // See and bullet points 2 and 3 in the doc for `exhaustive_1`, below. // https://gitlab.torproject.org/tpo/core/arti/-/issues/674 #[cfg(feature = "pt-client")] "bridges.transports", "tor_network.authorities", "tor_network.fallback_caches", #[cfg(feature = "rpc")] "rpc", #[cfg(feature = "rpc")] "rpc.rpc_listen", ]; /// Config file exhaustiveness and default checking /// /// `example_file` is a putative configuration file text. /// It is expected to contain "example lines", /// which are lines in start with `#` *not followed by whitespace*. /// /// This function checks that: /// /// Positive check on the example lines that are present. /// * `example_file`, when example lines are uncommented, can be parsed. /// * The example values are the same as the default values. /// /// Check for missing examples: /// * Every key `in `TorClientConfig` or `ArtiConfig` has a corresponding example value. /// * Except: entries in union(`expect_missing` `CONFIG_KEYS_EXPECT_NO_EXAMPLE`) /// do *not* have an example value. /// /// It handles straightforward cases, where the example line is in a `[section]` /// and is something like `#key = value`. /// /// It does not handle more complex keys, eg those listed in `CONFIG_KEYS_EXPECT_NO_EXAMPLE`, /// and which don't appear in "example lines" starting with just `#`: /// /// For complex config keys, it may not be sufficient to simply write the default value in /// the example files (along with perhaps some other information). In that case, /// 1. Write a bespoke example (with lines starting `# `) in the config file. /// 2. Write a bespoke test, to test the parsing of the bespoke example. /// This will probably involve using `ExampleSectionLines` and may be quite ad-hoc. /// The test function bridges(), below, is a complex worked example. /// 3. Either add a trivial example for the affected key(s) (starting with just `#`) /// or add the affected key(s) to the manual overrides `CONFIG_KEYS_EXPECT_NO_EXAMPLE`. fn exhaustive_1(example_file: &str, expect_missing: &[&str]) { use serde_json::Value as JsValue; use std::collections::BTreeSet; let example = uncomment_example_settings(example_file); let example: toml::Value = toml::from_str(&example).unwrap(); // dbg!(&example); let example = serde_json::to_value(example).unwrap(); // dbg!(&example); // "Exhaustive" taxonomy of the recognized configuration keys // // We use the JSON serialization of the default builders, because Rust's toml // implementation likes to omit more things, that we want to see. // // I'm not sure this is quite perfect but it is pretty good, // and has found a number of un-exampled config keys. let exhausts = [ serde_json::to_value(TorClientConfig::builder()).unwrap(), serde_json::to_value(ArtiConfig::builder()).unwrap(), ]; #[derive(Default, Debug)] struct Walk { current_path: Vec, problems: Vec<(String, String)>, } impl Walk { /// Records a problem fn bad(&mut self, m: &str) { self.problems .push((self.current_path.join("."), m.to_string())); } /// Recurses, looking for problems /// /// Visited for every node in either or both of the starting `exhausts`. /// /// `E` is the number of elements in `exhausts`, ie the number of different /// top-level config types that Arti uses. Ie, 2. fn walk( &mut self, example: Option<&JsValue>, exhausts: [Option<&JsValue>; E], ) { assert! { exhausts.into_iter().any(|e| e.is_some()) } let example = if let Some(e) = example { e } else { self.bad("missing from example"); return; }; let tables = exhausts.map(|e| e?.as_object()); // Union of the keys of both exhausts' tables (insofar as they *are* tables) let table_keys = tables .iter() .flat_map(|t| t.map(|t| t.keys().cloned()).into_iter().flatten()) .collect::>(); for key in table_keys { let example = if let Some(e) = example.as_object() { e } else { // At least one of the exhausts was a nonempty table, // but the corresponding example node isn't a table. self.bad("expected table in example"); continue; }; // Descend the same key in all the places. self.current_path.push(key.clone()); self.walk(example.get(&key), tables.map(|t| t?.get(&key))); self.current_path.pop().unwrap(); } } } let exhausts = exhausts.iter().map(Some).collect_vec().try_into().unwrap(); let mut walk = Walk::default(); walk.walk::<2>(Some(&example), exhausts); let mut problems = walk.problems; // When adding things here, check that `arti-example-config.toml` // actually has something about these particular config keys. dbg!(&expect_missing); let expect_missing: Vec<&str> = CONFIG_KEYS_EXPECT_NO_EXAMPLE .iter() .cloned() .chain(expect_missing.iter().cloned()) .collect_vec(); // Things might appear in expect_missing for different reasons, and sometimes // at different levels. For example, `bridges.transports` is expected to be // missing because we document that a different way in the example; but // `bridges` is expected to be missing from the OLDEST_SUPPORTED_CONFIG, // because that config predates bridge support. // // When this happens, we need to remove `bridges.transports` in favour of // the over-arching `bridges`. let expect_missing = expect_missing .iter() .cloned() .filter({ let original: HashSet<_> = expect_missing.iter().cloned().collect(); move |found| { !found .match_indices('.') .any(|(doti, _)| original.contains(&found[0..doti])) } }) .collect_vec(); dbg!(&expect_missing); for exp in expect_missing { let was = problems.len(); problems.retain(|(path, _)| path != exp); if problems.len() == was { problems.push(( exp.into(), "expected to be missing but found in default".into(), )); } } let problems = problems .into_iter() .map(|(path, m)| format!(" config key {:?}: {}", path, m)) .collect_vec(); // If this assert fails, it might be because in `fn exhaustive`, below, // a newly-defined config item has not been added to the list for OLDEST_SUPPORTED_CONFIG. assert! { problems.is_empty(), "example config exhaustiveness check failed: {}\n-----8<-----\n{}\n-----8<-----\n", problems.join("\n"), example_file} } #[test] fn exhaustive() { 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(); // Check that: // - The primary example config file has good examples for everything // - Except for deprecated config keys // - (And, except for those that we never expect: CONFIG_KEYS_EXPECT_NO_EXAMPLE.) exhaustive_1(ARTI_EXAMPLE_CONFIG, &deprecated); // Check that: // - That oldest supported example config file has good examples for everything // - Except for keys that we have introduced since that file was written // - (And, except for those that we never expect: CONFIG_KEYS_EXPECT_NO_EXAMPLE.) exhaustive_1( OLDEST_SUPPORTED_CONFIG, // add *new*, not present in old file, settings here &[ "application.allow_running_as_root", "bridges", "proxy.socks_listen", "proxy.dns_listen", ], ); } /// Check that the `Report` of `err` contains the string `exp`, and otherwise panic #[cfg_attr(feature = "pt-client", allow(dead_code))] fn expect_err_contains(err: ConfigResolveError, exp: &str) { use std::error::Error as StdError; let err: Box = Box::new(err); let err = tor_error::Report(err).to_string(); assert!( err.contains(exp), "wrong message, got {:?}, exp {:?}", err, exp, ); } #[test] fn bridges() { // We make assumptions about the contents of `arti-example-config.toml` ! // // 1. There are nontrivial, non-default examples of `bridges.bridges`. // 2. These are in the `[bridges]` section, after a line `# For example:` // 3. There's precisely one ``` example, with conventional TOML formatting. // 4. There's precisely one [ ] example, with conventional TOML formatting. // 5. Both these examples specify the same set of bridges. // 6. There are three bridges. // 7. Lines starting with a digit or `[` are direct bridges; others are PT. // // Below, we annotate with `[1]` etc. where these assumptions are made. // Filter examples that we don't want to test in this configuration let filter_examples = |#[allow(unused_mut)] mut examples: ExampleSectionLines| -> _ { // [7], filter out the PTs if cfg!(all(feature = "bridge-client", not(feature = "pt-client"))) { let looks_like_addr = |l: &str| l.starts_with(|c: char| c.is_ascii_digit() || c == '['); examples.lines.retain(|l| looks_like_addr(l)); } examples }; // Tests that one example parses, and returns what it parsed. // If bridge support is completely disabled, checks that this configuration // is rejected, as it should be, and returns a dummy value `((),)` // (so that the rest of the test has something to "compare that we parsed it the same"). let resolve_examples = |examples: &ExampleSectionLines| { // [7], check that the PT bridge is properly rejected #[cfg(all(feature = "bridge-client", not(feature = "pt-client")))] { let err = examples.resolve::().unwrap_err(); expect_err_contains(err, "support disabled in cargo features"); } let examples = filter_examples(examples.clone()); #[cfg(feature = "bridge-client")] { examples.resolve::().unwrap() } #[cfg(not(feature = "bridge-client"))] { let err = examples.resolve::().unwrap_err(); expect_err_contains(err, "support disabled in cargo features"); // Use ((),) as the dummy unit value because () gives clippy conniptions ((),) } }; // [1], [2], narrow to just the nontrivial, non-default, examples let mut examples = ExampleSectionLines::new("bridges"); examples.narrow((r#"^# For example:"#, true), NARROW_NONE); let compare = { // [3], narrow to the multi-line string let mut examples = examples.clone(); examples.narrow((r#"^# bridges = '''"#, true), (r#"^# '''"#, true)); examples.uncomment(); let parsed = resolve_examples(&examples); // Now we fish out the lines ourselves as a double-check // We must strip off the bridges = ''' and ''' lines. examples.lines.remove(0); examples.lines.remove(examples.lines.len() - 1); // [6], check we got the number of examples we expected examples.expect_lines(3); // If we have the bridge API, try parsing each line and using the API to insert it #[cfg(feature = "bridge-client")] { let examples = filter_examples(examples); let mut built = TorClientConfig::builder(); for l in &examples.lines { built.bridges().bridges().push(l.trim().parse().expect(l)); } let built = built.build().unwrap(); assert_eq!(&parsed, &built); } parsed }; // [4], [5], narrow to the [ ] section, parse again, and compare { examples.narrow((r#"^# bridges = \["#, true), (r#"^# \]"#, true)); examples.uncomment(); let parsed = resolve_examples(&examples); assert_eq!(&parsed, &compare); } } /// Helper for fishing out parts of the config file and uncommenting them /// /// This can be used to find part of the config file by ad-hoc regexp matching, /// uncomment it, and parse it. This is useful as part of a test to check /// that we can parse more complex config. #[derive(Debug, Clone)] struct ExampleSectionLines { section: String, lines: Vec, } type NarrowInstruction<'s> = (&'s str, bool); const NARROW_NONE: NarrowInstruction<'static> = ("?", false); impl ExampleSectionLines { fn new(section: &str) -> Self { let section = format!("[{}]", section); let mut first = Some(()); let lines = ARTI_EXAMPLE_CONFIG .lines() .skip_while(|l| l != §ion) .take_while(|l| first.take().is_some() || !l.starts_with("[")) .map(|l| l.to_string()) .collect_vec(); ExampleSectionLines { section, lines } } fn narrow(&mut self, start: NarrowInstruction, end: NarrowInstruction) { let find_index = |(re, include), adjust: [isize; 2]| { if (re, include) == NARROW_NONE { return None; } let re = Regex::new(re).expect(re); let i = self .lines .iter() .enumerate() .filter(|(_, l)| re.is_match(l)) .map(|(i, _)| i); let i = i.clone().exactly_one().unwrap_or_else(|_| { panic!("RE={:?} I={:#?} L={:#?}", re, i.collect_vec(), &self.lines) }); let adjust = adjust[usize::from(include)]; let i = (i as isize + adjust) as usize; Some(i) }; eprint!("narrow {:?} {:?}: ", start, end); let start = find_index(start, [1, 0]).unwrap_or(0); let end = find_index(end, [0, 1]).unwrap_or(self.lines.len()); eprintln!("{:?} {:?}", start, end); // don't tolerate empty assert!(start < end, "empty, from {:#?}", &self.lines); self.lines = self.lines.drain(..).take(end).skip(start).collect_vec(); } fn expect_lines(&self, n: usize) { assert_eq!(self.lines.len(), n); } fn uncomment(&mut self) { for l in &mut self.lines { *l = l.strip_prefix('#').expect(l).to_string(); } } fn parse(&self) -> config::Config { let s: String = chain!(iter::once(&self.section), self.lines.iter(),).join("\n"); eprintln!("parsing\n --\n{}\n --", &s); let c: toml::Value = toml::from_str(&s).expect(&s); config::Config::try_from(&c).expect(&s) } fn resolve(&self) -> Result { tor_config::load::resolve(self.parse()) } } }