1use std::{fmt::Formatter, time::Duration};
12
13use derive_builder::Builder;
14use getset::{CopyGetters, Getters};
15use serde::{
16 Deserialize, Deserializer, Serialize,
17 de::{MapAccess, SeqAccess, Visitor, value::MapAccessDeserializer},
18};
19use tor_checkable::timed::TimerangeBound;
20use tor_config::{ConfigBuildError, define_list_builder_accessors, impl_standard_builder};
21use tor_netdoc::doc::netstatus::Lifetime;
22use tracing::warn;
23
24use crate::{
25 authority::{AuthorityContacts, AuthorityContactsBuilder, LegacyAuthority},
26 fallback::{FallbackDirBuilder, FallbackList, FallbackListBuilder},
27 retry::{DownloadSchedule, DownloadScheduleBuilder},
28};
29
30#[derive(Debug, Clone, Builder, Eq, PartialEq, Getters)]
39#[builder(build_fn(validate = "Self::validate", error = "ConfigBuildError"))]
40#[builder(derive(Debug, Serialize, Deserialize))]
41#[non_exhaustive]
42pub struct NetworkConfig {
43 #[builder(sub_builder, setter(custom))]
55 #[getset(get = "pub")]
56 fallback_caches: FallbackList,
57
58 #[builder(sub_builder)]
69 #[builder_field_attr(serde(default, deserialize_with = "authority_compat"))]
70 #[getset(get = "pub")]
71 authorities: AuthorityContacts,
72}
73
74impl_standard_builder! { NetworkConfig }
75
76define_list_builder_accessors! {
77 struct NetworkConfigBuilder {
78 pub fallback_caches: [FallbackDirBuilder],
79 }
80}
81
82fn authority_compat<'de, D>(deserializer: D) -> Result<AuthorityContactsBuilder, D::Error>
153where
154 D: Deserializer<'de>,
155{
156 struct LegacyOrProp330;
157
158 impl<'de> Visitor<'de> for LegacyOrProp330 {
159 type Value = AuthorityContactsBuilder;
160
161 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
162 formatter.write_str("legacy or prop330")
163 }
164
165 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
167 where
168 A: SeqAccess<'de>,
169 {
170 warn!("using deprecated (before arti 1.6.0) authority configuration syntax");
171 let mut builder = AuthorityContacts::builder();
172 while let Some(legacy_authority) = seq.next_element::<LegacyAuthority>()? {
173 builder.v3idents().push(legacy_authority.v3ident);
174 }
175
176 Ok(builder)
177 }
178
179 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
182 where
183 A: MapAccess<'de>,
184 {
185 Deserialize::deserialize(MapAccessDeserializer::new(map))
186 }
187 }
188
189 deserializer.deserialize_any(LegacyOrProp330)
190}
191
192impl NetworkConfigBuilder {
193 fn validate(&self) -> std::result::Result<(), ConfigBuildError> {
195 if self.authorities.opt_v3idents().is_some() && self.opt_fallback_caches().is_none() {
196 return Err(ConfigBuildError::Inconsistent {
197 fields: vec!["authorities".to_owned(), "fallbacks".to_owned()],
198 problem: "Non-default authorities are use, but the fallback list is not overridden"
199 .to_owned(),
200 });
201 }
202
203 Ok(())
204 }
205}
206
207#[derive(Debug, Clone, Builder, Eq, PartialEq, Getters, CopyGetters)]
213#[builder(build_fn(error = "ConfigBuildError"))]
214#[builder(derive(Debug, Serialize, Deserialize))]
215#[non_exhaustive]
216pub struct DownloadScheduleConfig {
217 #[builder(
219 sub_builder,
220 field(build = "self.retry_bootstrap.build_retry_bootstrap()?")
221 )]
222 #[builder_field_attr(serde(default))]
223 #[getset(get_copy = "pub")]
224 retry_bootstrap: DownloadSchedule,
225
226 #[builder(sub_builder)]
228 #[builder_field_attr(serde(default))]
229 #[getset(get_copy = "pub")]
230 retry_consensus: DownloadSchedule,
231
232 #[builder(sub_builder)]
234 #[builder_field_attr(serde(default))]
235 #[getset(get_copy = "pub")]
236 retry_certs: DownloadSchedule,
237
238 #[builder(
240 sub_builder,
241 field(build = "self.retry_microdescs.build_retry_microdescs()?")
242 )]
243 #[builder_field_attr(serde(default))]
244 #[getset(get_copy = "pub")]
245 retry_microdescs: DownloadSchedule,
246}
247
248impl_standard_builder! { DownloadScheduleConfig }
249
250#[derive(Debug, Clone, Builder, Eq, PartialEq, Getters, CopyGetters)]
258#[builder(derive(Debug, Serialize, Deserialize))]
259#[builder(build_fn(error = "ConfigBuildError"))]
260#[non_exhaustive]
261pub struct DirTolerance {
262 #[builder(default = "Duration::from_secs(24 * 60 * 60)")]
268 #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
269 #[getset(get_copy = "pub")]
270 pre_valid_tolerance: Duration,
271
272 #[builder(default = "Duration::from_secs(3 * 24 * 60 * 60)")]
284 #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
285 #[getset(get_copy = "pub")]
286 post_valid_tolerance: Duration,
287}
288
289impl_standard_builder! { DirTolerance }
290
291impl DirTolerance {
292 pub fn extend_tolerance<B>(&self, timebound: TimerangeBound<B>) -> TimerangeBound<B> {
295 timebound
296 .extend_tolerance(self.post_valid_tolerance)
297 .extend_pre_tolerance(self.pre_valid_tolerance)
298 }
299
300 pub fn extend_lifetime(&self, lifetime: &Lifetime) -> Lifetime {
303 Lifetime::new(
304 lifetime.valid_after() - self.pre_valid_tolerance,
305 lifetime.fresh_until(),
306 lifetime.valid_until() + self.post_valid_tolerance,
307 )
308 .expect("Logic error when constructing lifetime")
309 }
310}
311
312#[cfg(test)]
313mod test {
314 #![allow(clippy::bool_assert_comparison)]
316 #![allow(clippy::clone_on_copy)]
317 #![allow(clippy::dbg_macro)]
318 #![allow(clippy::mixed_attributes_style)]
319 #![allow(clippy::print_stderr)]
320 #![allow(clippy::print_stdout)]
321 #![allow(clippy::single_char_pattern)]
322 #![allow(clippy::unwrap_used)]
323 #![allow(clippy::unchecked_duration_subtraction)]
324 #![allow(clippy::useless_vec)]
325 #![allow(clippy::needless_pass_by_value)]
326 #![allow(clippy::unnecessary_wraps)]
328
329 use crate::fallback::FallbackDir;
330
331 use super::*;
332
333 #[test]
334 fn build_network() {
335 let dflt = NetworkConfig::default();
336
337 let mut bld = NetworkConfig::builder();
339 let cfg = bld.build().unwrap();
340 assert_eq!(
341 cfg.authorities.v3idents().len(),
342 dflt.authorities.v3idents().len()
343 );
344 assert_eq!(cfg.fallback_caches.len(), dflt.fallback_caches.len());
345
346 bld.authorities
349 .set_v3idents(vec![[b'?'; 20].into(), [b'!'; 20].into()]);
350 assert!(bld.build().is_err());
351
352 bld.set_fallback_caches(vec![{
353 let mut bld = FallbackDir::builder();
354 bld.rsa_identity([b'x'; 20].into())
355 .ed_identity([b'y'; 32].into());
356 bld.orports().push("127.0.0.1:99".parse().unwrap());
357 bld.orports().push("[::]:99".parse().unwrap());
358 bld
359 }]);
360 let cfg = bld.build().unwrap();
361 assert_eq!(cfg.authorities.v3idents().len(), 2);
362 assert_eq!(cfg.fallback_caches.len(), 1);
363 }
364
365 #[test]
366 fn deserialize_compat() {
367 let mut netcfg_legacy: NetworkConfigBuilder = toml::from_str(
370 "
371 authorities = [
372 { name = \"test000a\", v3ident = \"911F7C74212214823DDBDE3044B5B1AF3EFB98A0\" },
373 { name = \"test001a\", v3ident = \"46C4A4492D103A8C5CA544AC653B51C7B9AC8692\" },
374 { name = \"test002a\", v3ident = \"28D4680EA9C3660D1028FC40BACAC1319414581E\" },
375 { name = \"test003a\", v3ident = \"3817C9EB7E41C957594D0D9BCD6C7D7D718479C2\" },
376 ]",
377 )
378 .unwrap();
379
380 let mut netcfg_prop330: NetworkConfigBuilder = toml::from_str(
381 "
382 [authorities]
383 v3idents = [
384 \"911F7C74212214823DDBDE3044B5B1AF3EFB98A0\",
385 \"46C4A4492D103A8C5CA544AC653B51C7B9AC8692\",
386 \"28D4680EA9C3660D1028FC40BACAC1319414581E\",
387 \"3817C9EB7E41C957594D0D9BCD6C7D7D718479C2\",
388 ]",
389 )
390 .unwrap();
391
392 assert_eq!(netcfg_legacy.authorities.v3idents().len(), 4);
393 assert_eq!(
394 netcfg_legacy.authorities.v3idents(),
395 netcfg_prop330.authorities.v3idents(),
396 );
397 }
398}