use derive_deftly::Deftly;
use tor_config_path::CfgPath;
#[cfg(feature = "onion-service-service")]
use crate::onion_proxy::{
OnionServiceProxyConfigBuilder, OnionServiceProxyConfigMap, OnionServiceProxyConfigMapBuilder,
};
#[cfg(feature = "rpc")]
semipublic_use! {
use crate::rpc::{
RpcConfig, RpcConfigBuilder,
listener::{RpcListenerSetConfig, RpcListenerSetConfigBuilder},
};
}
use arti_client::TorClientConfig;
#[cfg(feature = "onion-service-service")]
use tor_config::define_list_builder_accessors;
use tor_config::derive::prelude::*;
pub(crate) use tor_config::{ConfigBuildError, Listen};
use crate::{LoggingConfig, LoggingConfigBuilder};
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
pub(crate) const ARTI_EXAMPLE_CONFIG: &str = concat!(include_str!("./arti-example-config.toml"));
#[cfg(test)]
const OLDEST_SUPPORTED_CONFIG: &str = concat!(include_str!("./oldest-supported-config.toml"),);
#[cfg(not(feature = "rpc"))]
type RpcConfig = ();
#[cfg(not(feature = "onion-service-service"))]
type OnionServiceProxyConfigMap = ();
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
#[derive_deftly(TorConfig)]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct ApplicationConfig {
#[deftly(tor_config(default))]
pub(crate) watch_configuration: bool,
#[deftly(tor_config(default))]
pub(crate) permit_debugging: bool,
#[deftly(tor_config(default))]
pub(crate) allow_running_as_root: bool,
}
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
#[derive_deftly(TorConfig)]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct ProxyConfig {
#[deftly(tor_config(default = "Listen::new_localhost(9150)"))]
pub(crate) socks_listen: Listen,
#[deftly(tor_config(default = "Listen::new_none()"))]
pub(crate) dns_listen: Listen,
}
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
#[derive_deftly(TorConfig)]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct ArtiStorageConfig {
#[deftly(tor_config(setter(into), default = "default_port_info_file()"))]
pub(crate) port_info_file: CfgPath,
}
fn default_port_info_file() -> CfgPath {
CfgPath::new("${ARTI_LOCAL_DATA}/public/port_info.json".to_owned())
}
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
#[derive_deftly(TorConfig)]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
#[non_exhaustive]
pub(crate) struct SystemConfig {
#[deftly(tor_config(setter(into), default = "default_max_files()"))]
pub(crate) max_files: u64,
}
fn default_max_files() -> u64 {
16384
}
#[derive(Debug, Deftly, Clone, Eq, PartialEq)]
#[derive_deftly(TorConfig)]
#[deftly(tor_config(post_build = "Self::post_build"))]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct ArtiConfig {
#[deftly(tor_config(sub_builder))]
application: ApplicationConfig,
#[deftly(tor_config(sub_builder))]
proxy: ProxyConfig,
#[deftly(tor_config(sub_builder))]
logging: LoggingConfig,
#[deftly(tor_config(sub_builder))]
pub(crate) metrics: MetricsConfig,
#[deftly(tor_config(
sub_builder,
cfg = r#" feature = "rpc" "#,
cfg_desc = "with RPC support"
))]
pub(crate) rpc: RpcConfig,
#[deftly(tor_config(sub_builder))]
pub(crate) system: SystemConfig,
#[deftly(tor_config(sub_builder))]
pub(crate) storage: ArtiStorageConfig,
#[deftly(tor_config(
setter(skip),
sub_builder,
cfg = r#" feature = "onion-service-service" "#,
cfg_reject,
cfg_desc = "with onion service support"
))]
pub(crate) onion_services: OnionServiceProxyConfigMap,
}
impl ArtiConfigBuilder {
#[allow(clippy::unnecessary_wraps)]
fn post_build(config: ArtiConfig) -> Result<ArtiConfig, ConfigBuildError> {
#[cfg_attr(not(feature = "onion-service-service"), allow(unused_mut))]
let mut config = config;
#[cfg(feature = "onion-service-service")]
for svc in config.onion_services.values_mut() {
*svc.svc_cfg
.restricted_discovery_mut()
.watch_configuration_mut() = config.application.watch_configuration;
}
Ok(config)
}
}
impl tor_config::load::TopLevel for ArtiConfig {
type Builder = ArtiConfigBuilder;
const DEPRECATED_KEYS: &'static [&'static str] = &["proxy.socks_port", "proxy.dns_port"];
}
#[cfg(feature = "onion-service-service")]
define_list_builder_accessors! {
struct ArtiConfigBuilder {
pub(crate) onion_services: [OnionServiceProxyConfigBuilder],
}
}
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
pub(crate) type ArtiCombinedConfig = (ArtiConfig, TorClientConfig);
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
#[derive_deftly(TorConfig)]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct MetricsConfig {
#[deftly(tor_config(sub_builder))]
pub(crate) prometheus: PrometheusConfig,
}
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
#[derive_deftly(TorConfig)]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct PrometheusConfig {
#[deftly(tor_config(default))]
pub(crate) listen: Listen,
}
impl ArtiConfig {
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
pub(crate) fn application(&self) -> &ApplicationConfig {
&self.application
}
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
pub(crate) fn logging(&self) -> &LoggingConfig {
&self.logging
}
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
pub(crate) fn proxy(&self) -> &ProxyConfig {
&self.proxy
}
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
pub(crate) fn storage(&self) -> &ArtiStorageConfig {
&self.storage
}
#[cfg(feature = "rpc")]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
pub(crate) fn rpc(&self) -> &RpcConfig {
&self.rpc
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::iter_overeager_cloned)]
#![cfg_attr(not(feature = "pt-client"), allow(dead_code))]
use arti_client::config::TorClientConfigBuilder;
use arti_client::config::dir;
use itertools::{EitherOrBoth, Itertools, chain};
use regex::Regex;
use std::collections::HashSet;
use std::fmt::Write as _;
use std::iter;
use std::time::Duration;
use tor_config::load::{ConfigResolveError, ResolutionResults};
use tor_config_path::CfgPath;
#[allow(unused_imports)] use tor_error::ErrorReport as _;
#[cfg(feature = "restricted-discovery")]
use {
arti_client::HsClientDescEncKey,
std::str::FromStr as _,
tor_hsservice::config::restricted_discovery::{
DirectoryKeyProviderBuilder, HsClientNickname,
},
};
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()
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
enum InExample {
Absent,
Present,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
enum WhichExample {
Old,
New,
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
struct ConfigException {
key: String,
in_old_example: InExample,
in_new_example: InExample,
in_code: Option<bool>,
}
impl ConfigException {
fn in_example(&self, which: WhichExample) -> InExample {
use WhichExample::*;
match which {
Old => self.in_old_example,
New => self.in_new_example,
}
}
}
const ALL_RELEVANT_FEATURES_ENABLED: bool = cfg!(all(
feature = "bridge-client",
feature = "pt-client",
feature = "onion-service-client",
feature = "rpc",
));
fn declared_config_exceptions() -> Vec<ConfigException> {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
enum InCode {
Ignored,
FeatureDependent,
Recognized,
}
use InCode::*;
struct InOld;
struct InNew;
let mut out = vec![];
let mut declare_exceptions = |in_old_example: Option<InOld>,
in_new_example: Option<InNew>,
in_code: InCode,
keys: &[&str]| {
let in_code = match in_code {
Ignored => Some(false),
Recognized => Some(true),
FeatureDependent if ALL_RELEVANT_FEATURES_ENABLED => Some(true),
FeatureDependent => None,
};
#[allow(clippy::needless_pass_by_value)] fn in_example<T>(spec: Option<T>) -> InExample {
match spec {
None => InExample::Absent,
Some(_) => InExample::Present,
}
}
let in_old_example = in_example(in_old_example);
let in_new_example = in_example(in_new_example);
out.extend(keys.iter().cloned().map(|key| ConfigException {
key: key.to_owned(),
in_old_example,
in_new_example,
in_code,
}));
};
declare_exceptions(
None,
Some(InNew),
Recognized,
&[
"application.allow_running_as_root",
"bridges",
"logging.time_granularity",
"path_rules.long_lived_ports",
"use_obsolete_software",
"circuit_timing.disused_circuit_timeout",
"storage.port_info_file",
],
);
declare_exceptions(
None,
None,
Recognized,
&[
"tor_network.authorities",
"tor_network.fallback_caches",
],
);
declare_exceptions(
None,
None,
Recognized,
&[
"logging.opentelemetry",
],
);
declare_exceptions(
Some(InOld),
Some(InNew),
if cfg!(target_family = "windows") {
Ignored
} else {
Recognized
},
&[
"storage.permissions.trust_group",
"storage.permissions.trust_user",
],
);
declare_exceptions(
None,
None, FeatureDependent,
&[
"bridges.transports", ],
);
declare_exceptions(
None,
Some(InNew),
FeatureDependent,
&[
"storage.keystore",
],
);
declare_exceptions(
None,
None, FeatureDependent,
&[
"logging.tokio_console",
"logging.tokio_console.enabled",
],
);
declare_exceptions(
None,
None, Recognized,
&[
"system.memory",
"system.memory.max",
"system.memory.low_water",
],
);
declare_exceptions(
None,
Some(InNew), Recognized,
&["metrics"],
);
declare_exceptions(
None,
None, Recognized,
&[
"metrics.prometheus",
"metrics.prometheus.listen",
],
);
declare_exceptions(
None,
Some(InNew),
FeatureDependent,
&[
],
);
declare_exceptions(
None,
Some(InNew),
FeatureDependent,
&[
"address_filter.allow_onion_addrs",
"circuit_timing.hs_desc_fetch_attempts",
"circuit_timing.hs_intro_rend_attempts",
],
);
declare_exceptions(
None,
None, FeatureDependent,
&[
"rpc",
"rpc.rpc_listen",
],
);
declare_exceptions(
None,
None,
FeatureDependent,
&[
"onion_services",
],
);
declare_exceptions(
None,
Some(InNew),
FeatureDependent,
&[
"vanguards",
"vanguards.mode",
],
);
declare_exceptions(
None,
None,
FeatureDependent,
&[
"storage.keystore.ctor",
"storage.keystore.ctor.services",
"storage.keystore.ctor.clients",
],
);
out.sort();
let dupes = out.iter().map(|exc| &exc.key).duplicates().collect_vec();
assert!(
dupes.is_empty(),
"duplicate exceptions in configuration {dupes:?}"
);
eprintln!(
"declared config exceptions for this configuration:\n{:#?}",
&out
);
out
}
#[test]
fn default_config() {
use InExample::*;
let empty_config = tor_config::ConfigurationSources::new_empty()
.load()
.unwrap();
let empty_config: ArtiCombinedConfig = tor_config::resolve(empty_config).unwrap();
let default = (ArtiConfig::default(), TorClientConfig::default());
let exceptions = declared_config_exceptions();
#[allow(clippy::needless_pass_by_value)] fn analyse_joined_info(
which: WhichExample,
uncommented: bool,
eob: EitherOrBoth<&String, &ConfigException>,
) -> Result<(), (String, String)> {
use EitherOrBoth::*;
let (key, err) = match eob {
Left(found) => (found, "found in example but not processed".into()),
Both(found, exc) => {
let but = match (exc.in_example(which), exc.in_code, uncommented) {
(Absent, _, _) => "but exception entry expected key to be absent",
(_, _, false) => "when processing still-commented-out file!",
(_, Some(true), _) => {
"but an exception entry says it should have been recognised"
}
(Present, Some(false), true) => return Ok(()), (Present, None, true) => return Ok(()), };
(
found,
format!("parser reported unrecognised config key, {but}"),
)
}
Right(exc) => {
let trouble = match (exc.in_example(which), exc.in_code, uncommented) {
(Absent, _, _) => return Ok(()), (_, _, false) => return Ok(()), (_, Some(true), _) => return Ok(()), (Present, Some(false), true) => {
"expected an 'unknown config key' report but didn't see one"
}
(Present, None, true) => return Ok(()), };
(&exc.key, trouble.into())
}
};
Err((key.clone(), err))
}
let parses_to_defaults = |example: &str, which: WhichExample, uncommented: bool| {
let cfg = {
let mut sources = tor_config::ConfigurationSources::new_empty();
sources.push_source(
tor_config::ConfigurationSource::from_verbatim(example.to_string()),
tor_config::sources::MustRead::MustRead,
);
sources.load().unwrap()
};
let results: ResolutionResults<ArtiCombinedConfig> =
tor_config::resolve_return_results(cfg).unwrap();
assert_eq!(&results.value, &default, "{which:?} {uncommented:?}");
assert_eq!(&results.value, &empty_config, "{which:?} {uncommented:?}");
let unrecognized = results
.unrecognized
.iter()
.map(|k| k.to_string())
.collect_vec();
eprintln!(
"parsing of {which:?} uncommented={uncommented:?}, unrecognized={unrecognized:#?}"
);
let reports =
Itertools::merge_join_by(unrecognized.iter(), exceptions.iter(), |u, e| {
u.as_str().cmp(&e.key)
})
.filter_map(|eob| analyse_joined_info(which, uncommented, eob).err())
.collect_vec();
if !reports.is_empty() {
let reports = reports.iter().fold(String::new(), |mut out, (k, s)| {
writeln!(out, " {}: {}", s, k).unwrap();
out
});
panic!(
r"
mismatch: results of parsing example files (& vs declared exceptions):
example config file {which:?}, uncommented={uncommented:?}
{reports}
"
);
}
results.value
};
let _ = parses_to_defaults(ARTI_EXAMPLE_CONFIG, WhichExample::New, false);
let _ = parses_to_defaults(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, false);
let built_default = (
ArtiConfigBuilder::default().build().unwrap(),
TorClientConfigBuilder::default().build().unwrap(),
);
let parsed = parses_to_defaults(
&uncomment_example_settings(ARTI_EXAMPLE_CONFIG),
WhichExample::New,
true,
);
let parsed_old = parses_to_defaults(
&uncomment_example_settings(OLDEST_SUPPORTED_CONFIG),
WhichExample::Old,
true,
);
assert_eq!(&parsed, &built_default);
assert_eq!(&parsed_old, &built_default);
assert_eq!(&default, &built_default);
}
fn exhaustive_1(example_file: &str, which: WhichExample, deprecated: &[String]) {
use InExample::*;
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();
let example = serde_json::to_value(example).unwrap();
let exhausts = [
serde_json::to_value(TorClientConfig::builder()).unwrap(),
serde_json::to_value(ArtiConfig::builder()).unwrap(),
];
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, derive_more::Display)]
enum ProblemKind {
#[display("recognised by serialisation, but missing from example config file")]
MissingFromExample,
#[display("expected that example config file should contain have this as a table")]
ExpectedTableInExample,
#[display(
"declared exception says this key should be recognised but not in file, but that doesn't seem to be the case"
)]
UnusedException,
}
#[derive(Default, Debug)]
struct Walk {
current_path: Vec<String>,
problems: Vec<(String, ProblemKind)>,
}
impl Walk {
fn bad(&mut self, kind: ProblemKind) {
self.problems.push((self.current_path.join("."), kind));
}
fn walk<const E: usize>(
&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(ProblemKind::MissingFromExample);
return;
};
let tables = exhausts.map(|e| e?.as_object());
let table_keys = tables
.iter()
.flat_map(|t| t.map(|t| t.keys().cloned()).into_iter().flatten())
.collect::<BTreeSet<String>>();
for key in table_keys {
let example = if let Some(e) = example.as_object() {
e
} else {
self.bad(ProblemKind::ExpectedTableInExample);
continue;
};
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;
#[derive(Debug, Copy, Clone)]
struct DefinitelyRecognized;
let expect_missing = declared_config_exceptions()
.iter()
.filter_map(|exc| {
let definitely = match (exc.in_example(which), exc.in_code) {
(Present, _) => return None, (_, Some(false)) => return None, (Absent, Some(true)) => Some(DefinitelyRecognized),
(Absent, None) => None, };
Some((exc.key.clone(), definitely))
})
.collect_vec();
dbg!(&expect_missing);
let expect_missing: Vec<(String, Option<DefinitelyRecognized>)> = expect_missing
.iter()
.cloned()
.filter({
let original: HashSet<_> = expect_missing.iter().map(|(k, _)| k.clone()).collect();
move |(found, _)| {
!found
.match_indices('.')
.any(|(doti, _)| original.contains(&found[0..doti]))
}
})
.collect_vec();
dbg!(&expect_missing);
for (exp, definitely) in expect_missing {
let was = problems.len();
problems.retain(|(path, _)| path != &exp);
if problems.len() == was && definitely.is_some() {
problems.push((exp, ProblemKind::UnusedException));
}
}
let problems = problems
.into_iter()
.filter(|(key, _kind)| !deprecated.iter().any(|dep| key == dep))
.map(|(path, m)| format!(" config key {:?}: {}", path, m))
.collect_vec();
assert!(
problems.is_empty(),
"example config {which:?} 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().cloned().collect_vec();
exhaustive_1(ARTI_EXAMPLE_CONFIG, WhichExample::New, &deprecated);
exhaustive_1(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, &deprecated);
}
#[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<dyn StdError> = Box::new(err);
let err = tor_error::Report(err).to_string();
assert!(
err.contains(exp),
"wrong message, got {:?}, exp {:?}",
err,
exp,
);
}
#[test]
fn bridges() {
let filter_examples = |#[allow(unused_mut)] mut examples: ExampleSectionLines| -> _ {
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
};
let resolve_examples = |examples: &ExampleSectionLines| {
#[cfg(all(feature = "bridge-client", not(feature = "pt-client")))]
{
let err = examples.resolve::<TorClientConfig>().unwrap_err();
expect_err_contains(err, "support disabled in cargo features");
}
let examples = filter_examples(examples.clone());
#[cfg(feature = "bridge-client")]
{
examples.resolve::<TorClientConfig>().unwrap()
}
#[cfg(not(feature = "bridge-client"))]
{
let err = examples.resolve::<TorClientConfig>().unwrap_err();
expect_err_contains(err, "support disabled in cargo features");
((),)
}
};
let mut examples = ExampleSectionLines::from_section("bridges");
examples.narrow((r#"^# For example:"#, true), NARROW_NONE);
let compare = {
let mut examples = examples.clone();
examples.narrow((r#"^# bridges = '''"#, true), (r#"^# '''"#, true));
examples.uncomment();
let parsed = resolve_examples(&examples);
examples.lines.remove(0);
examples.lines.remove(examples.lines.len() - 1);
examples.expect_lines(3);
#[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
};
{
examples.narrow((r#"^# bridges = \["#, true), (r#"^# \]"#, true));
examples.uncomment();
let parsed = resolve_examples(&examples);
assert_eq!(&parsed, &compare);
}
}
#[test]
fn transports() {
let mut file =
ExampleSectionLines::from_markers("# An example managed pluggable transport", "[");
file.lines.retain(|line| line.starts_with("# "));
file.uncomment();
let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
let cfg_got = result.unwrap();
#[cfg(feature = "pt-client")]
{
use arti_client::config::{BridgesConfig, pt::TransportConfig};
use tor_config_path::CfgPath;
let bridges_got: &BridgesConfig = cfg_got.0.as_ref();
let mut bld = BridgesConfig::builder();
{
let mut b = TransportConfig::builder();
b.protocols(vec!["obfs4".parse().unwrap(), "obfs5".parse().unwrap()]);
b.path(CfgPath::new("/usr/bin/obfsproxy".to_string()));
b.arguments(vec!["-obfs4".to_string(), "-obfs5".to_string()]);
b.run_on_startup(true);
bld.transports().push(b);
}
{
let mut b = TransportConfig::builder();
b.protocols(vec!["obfs4".parse().unwrap()]);
b.proxy_addr("127.0.0.1:31337".parse().unwrap());
bld.transports().push(b);
}
let bridges_expected = bld.build().unwrap();
assert_eq!(&bridges_expected, bridges_got);
}
}
#[test]
fn memquota() {
let mut file = ExampleSectionLines::from_section("system");
file.lines.retain(|line| line.starts_with("# memory."));
file.uncomment();
let result = file.resolve_return_results::<(TorClientConfig, ArtiConfig)>();
let result = result.unwrap();
assert_eq!(result.unrecognized, []);
assert_eq!(result.deprecated, []);
let inner: &tor_memquota::testing::ConfigInner =
result.value.0.system_memory().inner().unwrap();
let defaulted_low = tor_memquota::Config::builder()
.max(*inner.max)
.build()
.unwrap();
let inner_defaulted_low = defaulted_low.inner().unwrap();
assert_eq!(inner, inner_defaulted_low);
}
#[test]
fn metrics() {
let mut file = ExampleSectionLines::from_section("metrics");
file.lines
.retain(|line| line.starts_with("# prometheus."));
file.uncomment();
let result = file
.resolve_return_results::<(TorClientConfig, ArtiConfig)>()
.unwrap();
assert_eq!(result.unrecognized, []);
assert_eq!(result.deprecated, []);
assert_eq!(
result
.value
.1
.metrics
.prometheus
.listen
.single_address_legacy()
.unwrap(),
Some("127.0.0.1:9035".parse().unwrap()),
);
}
#[test]
fn onion_services() {
let mut file = ExampleSectionLines::from_markers("##### ONION SERVICES", "##### RPC");
file.lines.retain(|line| line.starts_with("# "));
file.uncomment();
let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
#[cfg(feature = "onion-service-service")]
{
let svc_expected = {
use tor_hsrproxy::config::*;
let mut b = OnionServiceProxyConfigBuilder::default();
b.service().nickname("allium-cepa".parse().unwrap());
b.proxy().proxy_ports().push(ProxyRule::new(
ProxyPattern::one_port(80).unwrap(),
ProxyAction::Forward(
Encapsulation::Simple,
TargetAddr::Inet("127.0.0.1:10080".parse().unwrap()),
),
));
b.proxy().proxy_ports().push(ProxyRule::new(
ProxyPattern::one_port(22).unwrap(),
ProxyAction::DestroyCircuit,
));
b.proxy().proxy_ports().push(ProxyRule::new(
ProxyPattern::one_port(265).unwrap(),
ProxyAction::IgnoreStream,
));
b.proxy().proxy_ports().push(ProxyRule::new(
ProxyPattern::one_port(443).unwrap(),
ProxyAction::RejectStream,
));
b.proxy().proxy_ports().push(ProxyRule::new(
ProxyPattern::all_ports(),
ProxyAction::DestroyCircuit,
));
#[cfg(feature = "restricted-discovery")]
{
const ALICE_KEY: &str =
"descriptor:x25519:PU63REQUH4PP464E2Y7AVQ35HBB5DXDH5XEUVUNP3KCPNOXZGIBA";
const BOB_KEY: &str =
"descriptor:x25519:b5zqgtpermmuda6vc63lhjuf5ihpokjmuk26ly2xksf7vg52aesq";
for (nickname, key) in [("alice", ALICE_KEY), ("bob", BOB_KEY)] {
b.service()
.restricted_discovery()
.enabled(true)
.static_keys()
.access()
.push((
HsClientNickname::from_str(nickname).unwrap(),
HsClientDescEncKey::from_str(key).unwrap(),
));
}
let mut dir = DirectoryKeyProviderBuilder::default();
dir.path(CfgPath::new(
"/var/lib/tor/hidden_service/authorized_clients".to_string(),
));
b.service()
.restricted_discovery()
.key_dirs()
.access()
.push(dir);
}
b.build().unwrap()
};
cfg_if::cfg_if! {
if #[cfg(feature = "restricted-discovery")] {
let cfg = result.unwrap();
let services = cfg.1.onion_services;
assert_eq!(services.len(), 1);
let svc = services.values().next().unwrap();
assert_eq!(svc, &svc_expected);
} else {
expect_err_contains(
result.unwrap_err(),
"restricted_discovery.enabled=true, but restricted-discovery feature not enabled"
);
}
}
}
#[cfg(not(feature = "onion-service-service"))]
{
expect_err_contains(result.unwrap_err(), "not built with onion service support");
}
}
#[cfg(feature = "rpc")]
#[test]
fn rpc_defaults() {
let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
file.lines
.retain(|line| line.starts_with("# ") && !line.starts_with("# "));
file.uncomment();
let parsed = file
.resolve_return_results::<(TorClientConfig, ArtiConfig)>()
.unwrap();
assert!(parsed.unrecognized.is_empty());
assert!(parsed.deprecated.is_empty());
let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
let rpc_default = RpcConfig::default();
assert_eq!(rpc_parsed, &rpc_default);
}
#[cfg(feature = "rpc")]
#[test]
fn rpc_full() {
use crate::rpc::listener::{ConnectPointOptionsBuilder, RpcListenerSetConfigBuilder};
let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
file.lines
.retain(|line| line.starts_with("# ") && !line.contains("file ="));
file.uncomment();
let parsed = file
.resolve_return_results::<(TorClientConfig, ArtiConfig)>()
.unwrap();
let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
let expected = {
let mut bld_opts = ConnectPointOptionsBuilder::default();
bld_opts.enable(false);
let mut bld_set = RpcListenerSetConfigBuilder::default();
bld_set.dir(CfgPath::new("${HOME}/.my_connect_files/".to_string()));
bld_set.listener_options().enable(true);
bld_set
.file_options()
.insert("bad_file.json".to_string(), bld_opts);
let mut bld = RpcConfigBuilder::default();
bld.listen().insert("label".to_string(), bld_set);
bld.build().unwrap()
};
assert_eq!(&expected, rpc_parsed);
}
#[derive(Debug, Clone)]
struct ExampleSectionLines {
section: String,
lines: Vec<String>,
}
type NarrowInstruction<'s> = (&'s str, bool);
const NARROW_NONE: NarrowInstruction<'static> = ("?<none>", false);
impl ExampleSectionLines {
fn from_section(section: &str) -> Self {
Self::from_markers(format!("[{section}]"), "[")
}
fn from_markers<S, E>(start: S, end: E) -> Self
where
S: AsRef<str>,
E: AsRef<str>,
{
let (start, end) = (start.as_ref(), end.as_ref());
let mut lines = ARTI_EXAMPLE_CONFIG
.lines()
.skip_while(|line| !line.starts_with(start))
.peekable();
let section = lines
.next_if(|l0| l0.starts_with('['))
.map(|section| section.to_owned())
.unwrap_or_default();
let lines = lines
.take_while(|line| !line.starts_with(end))
.map(|l| l.to_owned())
.collect_vec();
Self { section, lines }
}
fn narrow(&mut self, start: NarrowInstruction, end: NarrowInstruction) {
let find_index = |(re, include), start_pos, exactly_one: bool, adjust: [isize; 2]| {
if (re, include) == NARROW_NONE {
return None;
}
let re = Regex::new(re).expect(re);
let i = self
.lines
.iter()
.enumerate()
.skip(start_pos)
.filter(|(_, l)| re.is_match(l))
.map(|(i, _)| i);
let i = if exactly_one {
i.clone().exactly_one().unwrap_or_else(|_| {
panic!("RE={:?} I={:#?} L={:#?}", re, i.collect_vec(), &self.lines)
})
} else {
i.clone().next()?
};
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, 0, true, [1, 0]).unwrap_or(0);
let end = find_index(end, start + 1, false, [0, 1]).unwrap_or(self.lines.len());
eprintln!("{:?} {:?}", start, end);
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) {
self.strip_prefix("#");
}
fn strip_prefix(&mut self, prefix: &str) {
for l in &mut self.lines {
if !l.starts_with('[') {
*l = l.strip_prefix(prefix).expect(l).to_string();
}
}
}
fn build_string(&self) -> String {
chain!(iter::once(&self.section), self.lines.iter(),).join("\n")
}
fn parse(&self) -> tor_config::ConfigurationTree {
let s = self.build_string();
eprintln!("parsing\n --\n{}\n --", &s);
let mut sources = tor_config::ConfigurationSources::new_empty();
sources.push_source(
tor_config::ConfigurationSource::from_verbatim(s.clone()),
tor_config::sources::MustRead::MustRead,
);
sources.load().expect(&s)
}
fn resolve<R: tor_config::load::Resolvable>(&self) -> Result<R, ConfigResolveError> {
tor_config::load::resolve(self.parse())
}
fn resolve_return_results<R: tor_config::load::Resolvable>(
&self,
) -> Result<ResolutionResults<R>, ConfigResolveError> {
tor_config::load::resolve_return_results(self.parse())
}
}
#[test]
fn builder() {
use tor_config_path::CfgPath;
let sec = std::time::Duration::from_secs(1);
let mut authorities = dir::AuthorityContacts::builder();
authorities.v3idents().push([22; 20].into());
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().authorities() = authorities;
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);
}
fn ports_listen(
f: &str,
get_listen: &dyn Fn(&ArtiConfig) -> &Listen,
bld_get_listen: &dyn Fn(&ArtiConfigBuilder) -> &Option<Listen>,
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 chk = |cfg: &ArtiConfigBuilder, expected: &Listen| {
dbg!(bld_get_listen(cfg));
let cfg = cfg.build().unwrap();
assert_eq!(get_listen(&cfg), expected);
};
let check_setters = |port, expected: &_| {
let cfg = ArtiConfig::builder();
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_listen(&mut cfg, dbg!(listen));
chk(&cfg, expected);
}
};
{
let expected = Listen::new_localhost(100);
let cfg = from_toml(&format!("proxy.{}_listen = 100", f));
assert_eq!(bld_get_listen(&cfg), &Some(Listen::new_localhost(100)));
chk(&cfg, &expected);
check_setters(Some(100), &expected);
}
{
let expected = Listen::new_none();
let cfg = from_toml(&format!("proxy.{}_listen = 0", f));
chk(&cfg, &expected);
check_setters(None, &expected);
}
}
#[test]
fn ports_listen_socks() {
ports_listen(
"socks",
&|cfg| &cfg.proxy.socks_listen,
&|bld| &bld.proxy.socks_listen,
&|bld, arg| bld.proxy.socks_listen(arg),
);
}
#[test]
fn ports_listen_dns() {
ports_listen(
"dns",
&|cfg| &cfg.proxy.dns_listen,
&|bld| &bld.proxy.dns_listen,
&|bld, arg| bld.proxy.dns_listen(arg),
);
}
}