1#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![warn(missing_docs)]
7#![warn(noop_method_call)]
8#![warn(unreachable_pub)]
9#![warn(clippy::all)]
10#![deny(clippy::await_holding_lock)]
11#![deny(clippy::cargo_common_metadata)]
12#![deny(clippy::cast_lossless)]
13#![deny(clippy::checked_conversions)]
14#![warn(clippy::cognitive_complexity)]
15#![deny(clippy::debug_assert_with_mut_call)]
16#![deny(clippy::exhaustive_enums)]
17#![deny(clippy::exhaustive_structs)]
18#![deny(clippy::expl_impl_clone_on_copy)]
19#![deny(clippy::fallible_impl_from)]
20#![deny(clippy::implicit_clone)]
21#![deny(clippy::large_stack_arrays)]
22#![warn(clippy::manual_ok_or)]
23#![deny(clippy::missing_docs_in_private_items)]
24#![warn(clippy::needless_borrow)]
25#![warn(clippy::needless_pass_by_value)]
26#![warn(clippy::option_option)]
27#![deny(clippy::print_stderr)]
28#![deny(clippy::print_stdout)]
29#![warn(clippy::rc_buffer)]
30#![deny(clippy::ref_option_ref)]
31#![warn(clippy::semicolon_if_nothing_returned)]
32#![warn(clippy::trait_duplication_in_bounds)]
33#![deny(clippy::unchecked_duration_subtraction)]
34#![deny(clippy::unnecessary_wraps)]
35#![warn(clippy::unseparated_literal_suffix)]
36#![deny(clippy::unwrap_used)]
37#![allow(clippy::let_unit_value)] #![allow(clippy::uninlined_format_args)]
39#![allow(clippy::significant_drop_in_scrutinee)] #![allow(clippy::result_large_err)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::single_component_path_imports)]
48
49pub mod authority;
50mod bootstrap;
51pub mod config;
52mod docid;
53mod docmeta;
54mod err;
55mod event;
56mod retry;
57mod shared_ref;
58mod state;
59mod storage;
60
61#[cfg(feature = "bridge-client")]
62pub mod bridgedesc;
63#[cfg(feature = "dirfilter")]
64pub mod filter;
65
66use crate::docid::{CacheUsage, ClientRequest, DocQuery};
67use crate::err::BootstrapAction;
68#[cfg(not(feature = "experimental-api"))]
69use crate::shared_ref::SharedMutArc;
70#[cfg(feature = "experimental-api")]
71pub use crate::shared_ref::SharedMutArc;
72use crate::storage::{DynStore, Store};
73use bootstrap::AttemptId;
74use event::DirProgress;
75use postage::watch;
76pub use retry::{DownloadSchedule, DownloadScheduleBuilder};
77use scopeguard::ScopeGuard;
78use tor_circmgr::CircMgr;
79use tor_dirclient::SourceInfo;
80use tor_error::{info_report, into_internal, warn_report};
81use tor_netdir::params::NetParameters;
82use tor_netdir::{DirEvent, MdReceiver, NetDir, NetDirProvider};
83
84use async_trait::async_trait;
85use futures::{stream::BoxStream, task::SpawnExt};
86use tor_async_utils::oneshot;
87use tor_rtcompat::scheduler::{TaskHandle, TaskSchedule};
88use tor_rtcompat::Runtime;
89use tracing::{debug, info, trace, warn};
90
91use std::marker::PhantomData;
92use std::sync::atomic::{AtomicBool, Ordering};
93use std::sync::{Arc, Mutex};
94use std::time::Duration;
95use std::{collections::HashMap, sync::Weak};
96use std::{fmt::Debug, time::SystemTime};
97
98use crate::state::{DirState, NetDirChange};
99pub use authority::{Authority, AuthorityBuilder};
100pub use config::{
101 DirMgrConfig, DirTolerance, DirToleranceBuilder, DownloadScheduleConfig,
102 DownloadScheduleConfigBuilder, NetworkConfig, NetworkConfigBuilder,
103};
104pub use docid::DocId;
105pub use err::Error;
106pub use event::{DirBlockage, DirBootstrapEvents, DirBootstrapStatus};
107pub use storage::DocumentText;
108pub use tor_guardmgr::fallback::{FallbackDir, FallbackDirBuilder};
109pub use tor_netdir::Timeliness;
110
111use strum;
113
114pub type Result<T> = std::result::Result<T, Error>;
116
117#[derive(Clone)]
124pub struct DirMgrStore<R: Runtime> {
125 pub(crate) store: Arc<Mutex<crate::DynStore>>,
127
128 pub(crate) runtime: PhantomData<R>,
130}
131
132impl<R: Runtime> DirMgrStore<R> {
133 pub fn new(config: &DirMgrConfig, runtime: R, offline: bool) -> Result<Self> {
135 let store = Arc::new(Mutex::new(config.open_store(offline)?));
136 drop(runtime);
137 let runtime = PhantomData;
138 Ok(DirMgrStore { store, runtime })
139 }
140}
141
142#[async_trait]
144pub trait DirProvider: NetDirProvider {
145 fn reconfigure(
149 &self,
150 new_config: &DirMgrConfig,
151 how: tor_config::Reconfigure,
152 ) -> std::result::Result<(), tor_config::ReconfigureError>;
153
154 async fn bootstrap(&self) -> Result<()>;
156
157 fn bootstrap_events(&self) -> BoxStream<'static, DirBootstrapStatus>;
163
164 fn download_task_handle(&self) -> Option<TaskHandle> {
166 None
167 }
168}
169
170impl<R: Runtime> NetDirProvider for DirMgr<R> {
173 fn netdir(&self, timeliness: Timeliness) -> tor_netdir::Result<Arc<NetDir>> {
174 use tor_netdir::Error as NetDirError;
175 let netdir = self.netdir.get().ok_or(NetDirError::NoInfo)?;
176 let lifetime = match timeliness {
177 Timeliness::Strict => netdir.lifetime().clone(),
178 Timeliness::Timely => self
179 .config
180 .get()
181 .tolerance
182 .extend_lifetime(netdir.lifetime()),
183 Timeliness::Unchecked => return Ok(netdir),
184 };
185 let now = SystemTime::now();
186 if lifetime.valid_after() > now {
187 Err(NetDirError::DirNotYetValid)
188 } else if lifetime.valid_until() < now {
189 Err(NetDirError::DirExpired)
190 } else {
191 Ok(netdir)
192 }
193 }
194
195 fn events(&self) -> BoxStream<'static, DirEvent> {
196 Box::pin(self.events.subscribe())
197 }
198
199 fn params(&self) -> Arc<dyn AsRef<tor_netdir::params::NetParameters>> {
200 if let Some(netdir) = self.netdir.get() {
201 netdir
207 } else {
208 self.default_parameters
212 .lock()
213 .expect("Poisoned lock")
214 .clone()
215 }
216 }
222}
223
224#[async_trait]
225impl<R: Runtime> DirProvider for Arc<DirMgr<R>> {
226 fn reconfigure(
227 &self,
228 new_config: &DirMgrConfig,
229 how: tor_config::Reconfigure,
230 ) -> std::result::Result<(), tor_config::ReconfigureError> {
231 DirMgr::reconfigure(self, new_config, how)
232 }
233
234 async fn bootstrap(&self) -> Result<()> {
235 DirMgr::bootstrap(self).await
236 }
237
238 fn bootstrap_events(&self) -> BoxStream<'static, DirBootstrapStatus> {
239 Box::pin(DirMgr::bootstrap_events(self))
240 }
241
242 fn download_task_handle(&self) -> Option<TaskHandle> {
243 Some(self.task_handle.clone())
244 }
245}
246
247pub struct DirMgr<R: Runtime> {
261 config: tor_config::MutCfg<DirMgrConfig>,
264 store: Arc<Mutex<DynStore>>,
269 netdir: Arc<SharedMutArc<NetDir>>,
276
277 default_parameters: Mutex<Arc<NetParameters>>,
279
280 events: event::FlagPublisher<DirEvent>,
282
283 send_status: Mutex<watch::Sender<event::DirBootstrapStatus>>,
286
287 receive_status: DirBootstrapEvents,
293
294 circmgr: Option<Arc<CircMgr<R>>>,
296
297 runtime: R,
299
300 offline: bool,
302
303 bootstrap_started: AtomicBool,
310
311 #[cfg(feature = "dirfilter")]
313 filter: crate::filter::FilterConfig,
314
315 task_schedule: Mutex<Option<TaskSchedule<R>>>,
318
319 task_handle: TaskHandle,
321}
322
323#[derive(Debug, Clone)]
328#[non_exhaustive]
329pub enum DocSource {
330 LocalCache,
332 DirServer {
334 source: Option<SourceInfo>,
336 },
337}
338
339impl std::fmt::Display for DocSource {
340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341 match self {
342 DocSource::LocalCache => write!(f, "local cache"),
343 DocSource::DirServer { source: None } => write!(f, "directory server"),
344 DocSource::DirServer { source: Some(info) } => write!(f, "directory server {}", info),
345 }
346 }
347}
348
349impl<R: Runtime> DirMgr<R> {
350 pub async fn load_once(runtime: R, config: DirMgrConfig) -> Result<Arc<NetDir>> {
360 let store = DirMgrStore::new(&config, runtime.clone(), true)?;
361 let dirmgr = Arc::new(Self::from_config(config, runtime, store, None, true)?);
362
363 let attempt = AttemptId::next();
365 trace!(%attempt, "Trying to load a full directory from cache");
366 let outcome = dirmgr.load_directory(attempt).await;
367 trace!(%attempt, "Load result: {outcome:?}");
368 let _success = outcome?;
369
370 dirmgr
371 .netdir(Timeliness::Timely)
372 .map_err(|_| Error::DirectoryNotPresent)
373 }
374
375 pub async fn load_or_bootstrap_once(
385 config: DirMgrConfig,
386 runtime: R,
387 store: DirMgrStore<R>,
388 circmgr: Arc<CircMgr<R>>,
389 ) -> Result<Arc<NetDir>> {
390 let dirmgr = DirMgr::bootstrap_from_config(config, runtime, store, circmgr).await?;
391 dirmgr
392 .timely_netdir()
393 .map_err(|_| Error::DirectoryNotPresent)
394 }
395
396 pub fn create_unbootstrapped(
400 config: DirMgrConfig,
401 runtime: R,
402 store: DirMgrStore<R>,
403 circmgr: Arc<CircMgr<R>>,
404 ) -> Result<Arc<Self>> {
405 Ok(Arc::new(DirMgr::from_config(
406 config,
407 runtime,
408 store,
409 Some(circmgr),
410 false,
411 )?))
412 }
413
414 pub async fn bootstrap(self: &Arc<Self>) -> Result<()> {
435 if self.offline {
436 return Err(Error::OfflineMode);
437 }
438
439 if self
446 .bootstrap_started
447 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
448 .is_err()
449 {
450 debug!("Attempted to bootstrap twice; ignoring.");
451 return Ok(());
452 }
453
454 let reset_bootstrap_started = scopeguard::guard(&self.bootstrap_started, |v| {
457 v.store(false, Ordering::SeqCst);
458 });
459
460 let schedule = {
461 let sched = self.task_schedule.lock().expect("poisoned lock").take();
462 match sched {
463 Some(sched) => sched,
464 None => {
465 debug!("Attempted to bootstrap twice; ignoring.");
466 return Ok(());
467 }
468 }
469 };
470
471 let attempt_id = AttemptId::next();
473 trace!(attempt=%attempt_id, "Starting to bootstrap directory");
474 let have_directory = self.load_directory(attempt_id).await?;
475
476 let (mut sender, receiver) = if have_directory {
477 info!("Loaded a good directory from cache.");
478 (None, None)
479 } else {
480 info!("Didn't get usable directory from cache.");
481 let (sender, receiver) = oneshot::channel();
482 (Some(sender), Some(receiver))
483 };
484
485 let dirmgr_weak = Arc::downgrade(self);
487 self.runtime
488 .spawn(async move {
489 let mut schedule = scopeguard::guard(schedule, |schedule| {
496 if let Some(dm) = Weak::upgrade(&dirmgr_weak) {
497 *dm.task_schedule.lock().expect("poisoned lock") = Some(schedule);
498 }
499 });
500
501 if let Err(e) =
504 Self::reload_until_owner(&dirmgr_weak, &mut schedule, attempt_id, &mut sender)
505 .await
506 {
507 match e {
508 Error::ManagerDropped => {}
509 _ => warn_report!(e, "Unrecovered error while waiting for bootstrap",),
510 }
511 } else if let Err(e) =
512 Self::download_forever(dirmgr_weak.clone(), &mut schedule, attempt_id, sender)
513 .await
514 {
515 match e {
516 Error::ManagerDropped => {}
517 _ => warn_report!(e, "Unrecovered error while downloading"),
518 }
519 }
520 })
521 .map_err(|e| Error::from_spawn("directory updater task", e))?;
522
523 if let Some(receiver) = receiver {
524 match receiver.await {
525 Ok(()) => {
526 info!("We have enough information to build circuits.");
527 let _ = ScopeGuard::into_inner(reset_bootstrap_started);
529 }
530 Err(_) => {
531 warn!("Bootstrapping task exited before finishing.");
532 return Err(Error::CantAdvanceState);
533 }
534 }
535 }
536 Ok(())
537 }
538
539 pub fn bootstrap_started(&self) -> bool {
541 self.bootstrap_started.load(Ordering::SeqCst)
542 }
543
544 pub async fn bootstrap_from_config(
547 config: DirMgrConfig,
548 runtime: R,
549 store: DirMgrStore<R>,
550 circmgr: Arc<CircMgr<R>>,
551 ) -> Result<Arc<Self>> {
552 let dirmgr = Self::create_unbootstrapped(config, runtime, store, circmgr)?;
553
554 dirmgr.bootstrap().await?;
555
556 Ok(dirmgr)
557 }
558
559 async fn reload_until_owner(
567 weak: &Weak<Self>,
568 schedule: &mut TaskSchedule<R>,
569 attempt_id: AttemptId,
570 on_complete: &mut Option<oneshot::Sender<()>>,
571 ) -> Result<()> {
572 let mut logged = false;
573 let mut bootstrapped;
574 {
575 let dirmgr = upgrade_weak_ref(weak)?;
576 bootstrapped = dirmgr.netdir.get().is_some();
577 }
578
579 loop {
580 {
581 let dirmgr = upgrade_weak_ref(weak)?;
582 trace!("Trying to take ownership of the directory cache lock");
583 if dirmgr.try_upgrade_to_readwrite()? {
584 if logged {
588 info!("The previous owning process has given up the lock. We are now in charge of managing the directory.");
589 }
590 return Ok(());
591 }
592 }
593
594 if !logged {
595 logged = true;
596 if bootstrapped {
597 info!("Another process is managing the directory. We'll use its cache.");
598 } else {
599 info!("Another process is bootstrapping the directory. Waiting till it finishes or exits.");
600 }
601 }
602
603 let pause = if bootstrapped {
606 std::time::Duration::new(120, 0)
607 } else {
608 std::time::Duration::new(5, 0)
609 };
610 schedule.sleep(pause).await?;
611 {
615 let dirmgr = upgrade_weak_ref(weak)?;
616 trace!("Trying to load from the directory cache");
617 if dirmgr.load_directory(attempt_id).await? {
618 if let Some(send_done) = on_complete.take() {
620 let _ = send_done.send(());
621 }
622 if !bootstrapped {
623 info!("The directory is now bootstrapped.");
624 }
625 bootstrapped = true;
626 }
627 }
628 }
629 }
630
631 async fn download_forever(
636 weak: Weak<Self>,
637 schedule: &mut TaskSchedule<R>,
638 mut attempt_id: AttemptId,
639 mut on_complete: Option<oneshot::Sender<()>>,
640 ) -> Result<()> {
641 let mut state: Box<dyn DirState> = {
642 let dirmgr = upgrade_weak_ref(&weak)?;
643 Box::new(state::GetConsensusState::new(
644 dirmgr.runtime.clone(),
645 dirmgr.config.get(),
646 CacheUsage::CacheOkay,
647 Some(dirmgr.netdir.clone()),
648 #[cfg(feature = "dirfilter")]
649 dirmgr
650 .filter
651 .clone()
652 .unwrap_or_else(|| Arc::new(crate::filter::NilFilter)),
653 ))
654 };
655
656 trace!("Entering download loop.");
657
658 loop {
659 let mut usable = false;
660
661 let retry_config = {
662 let dirmgr = upgrade_weak_ref(&weak)?;
663 dirmgr.config.get().schedule.retry_bootstrap
667 };
668 let mut retry_delay = retry_config.schedule();
669
670 'retry_attempt: for try_num in retry_config.attempts() {
671 trace!(attempt=%attempt_id, ?try_num, "Trying to download a directory.");
672 let outcome = bootstrap::download(
673 Weak::clone(&weak),
674 &mut state,
675 schedule,
676 attempt_id,
677 &mut on_complete,
678 )
679 .await;
680 trace!(attempt=%attempt_id, ?try_num, ?outcome, "Download is over.");
681
682 if let Err(err) = outcome {
683 if state.is_ready(Readiness::Usable) {
684 usable = true;
685 info_report!(err, "Unable to completely download a directory. (Nevertheless, the directory is usable, so we'll pause for now)");
686 break 'retry_attempt;
687 }
688
689 match err.bootstrap_action() {
690 BootstrapAction::Nonfatal => {
691 return Err(into_internal!(
692 "Nonfatal error should not have propagated here"
693 )(err)
694 .into());
695 }
696 BootstrapAction::Reset => {}
697 BootstrapAction::Fatal => return Err(err),
698 }
699
700 let delay = retry_delay.next_delay(&mut rand::thread_rng());
701 warn_report!(
702 err,
703 "Unable to download a usable directory. (We will restart in {})",
704 humantime::format_duration(delay),
705 );
706 {
707 let dirmgr = upgrade_weak_ref(&weak)?;
708 dirmgr.note_reset(attempt_id);
709 }
710 schedule.sleep(delay).await?;
711 state = state.reset();
712 } else {
713 info!(attempt=%attempt_id, "Directory is complete.");
714 usable = true;
715 break 'retry_attempt;
716 }
717 }
718
719 if !usable {
720 warn!(
722 "We failed {} times to bootstrap a directory. We're going to give up.",
723 retry_config.n_attempts()
724 );
725 return Err(Error::CantAdvanceState);
726 } else {
727 if let Some(send_done) = on_complete.take() {
729 let _ = send_done.send(());
730 }
731 }
732
733 let reset_at = state.reset_time();
734 match reset_at {
735 Some(t) => {
736 trace!("Sleeping until {}", time::OffsetDateTime::from(t));
737 schedule.sleep_until_wallclock(t).await?;
738 }
739 None => return Ok(()),
740 }
741 attempt_id = bootstrap::AttemptId::next();
742 trace!(attempt=%attempt_id, "Beginning new attempt to bootstrap directory");
743 state = state.reset();
744 }
745 }
746
747 fn circmgr(&self) -> Result<Arc<CircMgr<R>>> {
749 self.circmgr.clone().ok_or(Error::NoDownloadSupport)
750 }
751
752 pub fn reconfigure(
756 &self,
757 new_config: &DirMgrConfig,
758 how: tor_config::Reconfigure,
759 ) -> std::result::Result<(), tor_config::ReconfigureError> {
760 let config = self.config.get();
761 if new_config.cache_dir != config.cache_dir {
765 how.cannot_change("storage.cache_dir")?;
766 }
767 if new_config.authorities() != config.authorities() {
768 how.cannot_change("network.authorities")?;
769 }
770
771 if how == tor_config::Reconfigure::CheckAllOrNothing {
772 return Ok(());
773 }
774
775 let params_changed = new_config.override_net_params != config.override_net_params;
776
777 self.config
778 .map_and_replace(|cfg| cfg.update_from_config(new_config));
779
780 if params_changed {
781 let _ignore_err = self.netdir.mutate(|netdir| {
782 netdir.replace_overridden_parameters(&new_config.override_net_params);
783 Ok(())
784 });
785 {
786 let mut params = self.default_parameters.lock().expect("lock failed");
787 *params = Arc::new(NetParameters::from_map(&new_config.override_net_params));
788 }
789
790 self.events.publish(DirEvent::NewConsensus);
793 }
794
795 Ok(())
796 }
797
798 pub fn bootstrap_events(&self) -> event::DirBootstrapEvents {
804 self.receive_status.clone()
805 }
806
807 fn update_progress(&self, attempt_id: AttemptId, progress: DirProgress) {
810 let mut sender = self.send_status.lock().expect("poisoned lock");
812 let mut status = sender.borrow_mut();
813
814 status.update_progress(attempt_id, progress);
815 }
816
817 fn note_errors(&self, attempt_id: AttemptId, n_errors: usize) {
820 if n_errors == 0 {
821 return;
822 }
823 let mut sender = self.send_status.lock().expect("poisoned lock");
824 let mut status = sender.borrow_mut();
825
826 status.note_errors(attempt_id, n_errors);
827 }
828
829 fn note_reset(&self, attempt_id: AttemptId) {
831 let mut sender = self.send_status.lock().expect("poisoned lock");
832 let mut status = sender.borrow_mut();
833
834 status.note_reset(attempt_id);
835 }
836
837 fn try_upgrade_to_readwrite(&self) -> Result<bool> {
844 self.store
845 .lock()
846 .expect("Directory storage lock poisoned")
847 .upgrade_to_readwrite()
848 }
849
850 #[cfg(test)]
852 fn store_if_rw(&self) -> Option<&Mutex<DynStore>> {
853 let rw = !self
854 .store
855 .lock()
856 .expect("Directory storage lock poisoned")
857 .is_readonly();
858 if rw {
860 Some(&self.store)
861 } else {
862 None
863 }
864 }
865
866 #[allow(clippy::unnecessary_wraps)] fn from_config(
872 config: DirMgrConfig,
873 runtime: R,
874 store: DirMgrStore<R>,
875 circmgr: Option<Arc<CircMgr<R>>>,
876 offline: bool,
877 ) -> Result<Self> {
878 let netdir = Arc::new(SharedMutArc::new());
879 let events = event::FlagPublisher::new();
880 let default_parameters = NetParameters::from_map(&config.override_net_params);
881 let default_parameters = Mutex::new(Arc::new(default_parameters));
882
883 let (send_status, receive_status) = postage::watch::channel();
884 let send_status = Mutex::new(send_status);
885 let receive_status = DirBootstrapEvents {
886 inner: receive_status,
887 };
888 #[cfg(feature = "dirfilter")]
889 let filter = config.extensions.filter.clone();
890
891 let (task_schedule, task_handle) = TaskSchedule::new(runtime.clone());
893 let task_schedule = Mutex::new(Some(task_schedule));
894
895 Ok(DirMgr {
896 config: config.into(),
897 store: store.store,
898 netdir,
899 default_parameters,
900 events,
901 send_status,
902 receive_status,
903 circmgr,
904 runtime,
905 offline,
906 bootstrap_started: AtomicBool::new(false),
907 #[cfg(feature = "dirfilter")]
908 filter,
909 task_schedule,
910 task_handle,
911 })
912 }
913
914 async fn load_directory(self: &Arc<Self>, attempt_id: AttemptId) -> Result<bool> {
919 let state = state::GetConsensusState::new(
920 self.runtime.clone(),
921 self.config.get(),
922 CacheUsage::CacheOnly,
923 None,
924 #[cfg(feature = "dirfilter")]
925 self.filter
926 .clone()
927 .unwrap_or_else(|| Arc::new(crate::filter::NilFilter)),
928 );
929 let _ = bootstrap::load(Arc::clone(self), Box::new(state), attempt_id).await?;
930
931 Ok(self.netdir.get().is_some())
932 }
933
934 pub fn events(&self) -> impl futures::Stream<Item = DirEvent> {
941 self.events.subscribe()
942 }
943
944 pub fn text(&self, doc: &DocId) -> Result<Option<DocumentText>> {
947 use itertools::Itertools;
948 let mut result = HashMap::new();
949 let query: DocQuery = (*doc).into();
950 let store = self.store.lock().expect("store lock poisoned");
951 query.load_from_store_into(&mut result, &**store)?;
952 let item = result.into_iter().at_most_one().map_err(|_| {
953 Error::CacheCorruption("Found more than one entry in storage for given docid")
954 })?;
955 if let Some((docid, doctext)) = item {
956 if &docid != doc {
957 return Err(Error::CacheCorruption(
958 "Item from storage had incorrect docid.",
959 ));
960 }
961 Ok(Some(doctext))
962 } else {
963 Ok(None)
964 }
965 }
966
967 pub fn texts<T>(&self, docs: T) -> Result<HashMap<DocId, DocumentText>>
972 where
973 T: IntoIterator<Item = DocId>,
974 {
975 let partitioned = docid::partition_by_type(docs);
976 let mut result = HashMap::new();
977 let store = self.store.lock().expect("store lock poisoned");
978 for (_, query) in partitioned.into_iter() {
979 query.load_from_store_into(&mut result, &**store)?;
980 }
981 Ok(result)
982 }
983
984 fn expand_response_text(&self, req: &ClientRequest, text: String) -> Result<String> {
992 if let ClientRequest::Consensus(req) = req {
993 if tor_consdiff::looks_like_diff(&text) {
994 if let Some(old_d) = req.old_consensus_digests().next() {
995 let db_val = {
996 let s = self.store.lock().expect("Directory storage lock poisoned");
997 s.consensus_by_sha3_digest_of_signed_part(old_d)?
998 };
999 if let Some((old_consensus, meta)) = db_val {
1000 info!("Applying a consensus diff");
1001 let new_consensus = tor_consdiff::apply_diff(
1002 old_consensus.as_str()?,
1003 &text,
1004 Some(*meta.sha3_256_of_signed()),
1005 )?;
1006 new_consensus.check_digest()?;
1007 return Ok(new_consensus.to_string());
1008 }
1009 }
1010 return Err(Error::Unwanted(
1011 "Received a consensus diff we did not ask for",
1012 ));
1013 }
1014 }
1015 Ok(text)
1016 }
1017
1018 #[allow(clippy::cognitive_complexity)]
1020 fn apply_netdir_changes(
1021 self: &Arc<Self>,
1022 state: &mut Box<dyn DirState>,
1023 store: &mut dyn Store,
1024 ) -> Result<()> {
1025 if let Some(change) = state.get_netdir_change() {
1026 match change {
1027 NetDirChange::AttemptReplace {
1028 netdir,
1029 consensus_meta,
1030 } => {
1031 if let Some(ref cm) = self.circmgr {
1034 if !cm
1035 .netdir_is_sufficient(netdir.as_ref().expect("AttemptReplace had None"))
1036 {
1037 debug!("Got a new NetDir, but it doesn't have enough guards yet.");
1038 return Ok(());
1039 }
1040 }
1041 let is_stale = {
1042 self.netdir
1044 .get()
1045 .map(|x| {
1046 x.lifetime().valid_after()
1047 > netdir
1048 .as_ref()
1049 .expect("AttemptReplace had None")
1050 .lifetime()
1051 .valid_after()
1052 })
1053 .unwrap_or(false)
1054 };
1055 if is_stale {
1056 warn!("Got a new NetDir, but it's older than the one we currently have!");
1057 return Err(Error::NetDirOlder);
1058 }
1059 let cfg = self.config.get();
1060 let mut netdir = netdir.take().expect("AttemptReplace had None");
1061 netdir.replace_overridden_parameters(&cfg.override_net_params);
1062 self.netdir.replace(netdir);
1063 self.events.publish(DirEvent::NewConsensus);
1064 self.events.publish(DirEvent::NewDescriptors);
1065
1066 info!("Marked consensus usable.");
1067 if !store.is_readonly() {
1068 store.mark_consensus_usable(consensus_meta)?;
1069 store.expire_all(&crate::storage::EXPIRATION_DEFAULTS)?;
1072 }
1073 Ok(())
1074 }
1075 NetDirChange::AddMicrodescs(mds) => {
1076 self.netdir.mutate(|netdir| {
1077 for md in mds.drain(..) {
1078 netdir.add_microdesc(md);
1079 }
1080 Ok(())
1081 })?;
1082 self.events.publish(DirEvent::NewDescriptors);
1083 Ok(())
1084 }
1085 }
1086 } else {
1087 Ok(())
1088 }
1089 }
1090}
1091
1092#[derive(Debug, Copy, Clone)]
1094enum Readiness {
1095 Complete,
1097 Usable,
1099}
1100
1101fn upgrade_weak_ref<T>(weak: &Weak<T>) -> Result<Arc<T>> {
1104 Weak::upgrade(weak).ok_or(Error::ManagerDropped)
1105}
1106
1107pub(crate) fn default_consensus_cutoff(
1110 now: SystemTime,
1111 tolerance: &DirTolerance,
1112) -> Result<SystemTime> {
1113 const MIN_AGE_TO_ALLOW: Duration = Duration::from_secs(3 * 3600);
1116 let allow_skew = std::cmp::max(MIN_AGE_TO_ALLOW, tolerance.post_valid_tolerance);
1117 let cutoff = time::OffsetDateTime::from(now - allow_skew);
1118 let (h, _m, _s) = cutoff.to_hms();
1125 let cutoff = cutoff.replace_time(
1126 time::Time::from_hms(h, 0, 0)
1127 .map_err(tor_error::into_internal!("Failed clock calculation"))?,
1128 );
1129 let cutoff = cutoff + Duration::from_secs(3600);
1130
1131 Ok(cutoff.into())
1132}
1133
1134#[cfg(test)]
1135mod test {
1136 #![allow(clippy::bool_assert_comparison)]
1138 #![allow(clippy::clone_on_copy)]
1139 #![allow(clippy::dbg_macro)]
1140 #![allow(clippy::mixed_attributes_style)]
1141 #![allow(clippy::print_stderr)]
1142 #![allow(clippy::print_stdout)]
1143 #![allow(clippy::single_char_pattern)]
1144 #![allow(clippy::unwrap_used)]
1145 #![allow(clippy::unchecked_duration_subtraction)]
1146 #![allow(clippy::useless_vec)]
1147 #![allow(clippy::needless_pass_by_value)]
1148 use super::*;
1150 use crate::docmeta::{AuthCertMeta, ConsensusMeta};
1151 use std::time::Duration;
1152 use tempfile::TempDir;
1153 use tor_basic_utils::test_rng::testing_rng;
1154 use tor_netdoc::doc::netstatus::ConsensusFlavor;
1155 use tor_netdoc::doc::{authcert::AuthCertKeyIds, netstatus::Lifetime};
1156 use tor_rtcompat::SleepProvider;
1157
1158 pub(crate) fn new_mgr<R: Runtime>(runtime: R) -> (TempDir, DirMgr<R>) {
1159 let dir = TempDir::new().unwrap();
1160 let config = DirMgrConfig {
1161 cache_dir: dir.path().into(),
1162 ..Default::default()
1163 };
1164 let store = DirMgrStore::new(&config, runtime.clone(), false).unwrap();
1165 let dirmgr = DirMgr::from_config(config, runtime, store, None, false).unwrap();
1166
1167 (dir, dirmgr)
1168 }
1169
1170 #[test]
1171 fn failing_accessors() {
1172 tor_rtcompat::test_with_one_runtime!(|rt| async {
1173 let (_tempdir, mgr) = new_mgr(rt);
1174
1175 assert!(mgr.circmgr().is_err());
1176 assert!(mgr.netdir(Timeliness::Unchecked).is_err());
1177 });
1178 }
1179
1180 #[test]
1181 fn load_and_store_internals() {
1182 tor_rtcompat::test_with_one_runtime!(|rt| async {
1183 let now = rt.wallclock();
1184 let tomorrow = now + Duration::from_secs(86400);
1185 let later = tomorrow + Duration::from_secs(86400);
1186
1187 let (_tempdir, mgr) = new_mgr(rt);
1188
1189 let d1 = [5_u8; 32];
1191 let d2 = [7; 32];
1192 let d3 = [42; 32];
1193 let d4 = [99; 20];
1194 let d5 = [12; 20];
1195 let certid1 = AuthCertKeyIds {
1196 id_fingerprint: d4.into(),
1197 sk_fingerprint: d5.into(),
1198 };
1199 let certid2 = AuthCertKeyIds {
1200 id_fingerprint: d5.into(),
1201 sk_fingerprint: d4.into(),
1202 };
1203
1204 {
1205 let mut store = mgr.store.lock().unwrap();
1206
1207 store
1208 .store_microdescs(
1209 &[
1210 ("Fake micro 1", &d1),
1211 ("Fake micro 2", &d2),
1212 ("Fake micro 3", &d3),
1213 ],
1214 now,
1215 )
1216 .unwrap();
1217
1218 #[cfg(feature = "routerdesc")]
1219 store
1220 .store_routerdescs(&[("Fake rd1", now, &d4), ("Fake rd2", now, &d5)])
1221 .unwrap();
1222
1223 store
1224 .store_authcerts(&[
1225 (
1226 AuthCertMeta::new(certid1, now, tomorrow),
1227 "Fake certificate one",
1228 ),
1229 (
1230 AuthCertMeta::new(certid2, now, tomorrow),
1231 "Fake certificate two",
1232 ),
1233 ])
1234 .unwrap();
1235
1236 let cmeta = ConsensusMeta::new(
1237 Lifetime::new(now, tomorrow, later).unwrap(),
1238 [102; 32],
1239 [103; 32],
1240 );
1241 store
1242 .store_consensus(&cmeta, ConsensusFlavor::Microdesc, false, "Fake consensus!")
1243 .unwrap();
1244 }
1245
1246 let t1 = mgr.text(&DocId::Microdesc(d1)).unwrap().unwrap();
1248 assert_eq!(t1.as_str(), Ok("Fake micro 1"));
1249
1250 let t2 = mgr
1251 .text(&DocId::LatestConsensus {
1252 flavor: ConsensusFlavor::Microdesc,
1253 cache_usage: CacheUsage::CacheOkay,
1254 })
1255 .unwrap()
1256 .unwrap();
1257 assert_eq!(t2.as_str(), Ok("Fake consensus!"));
1258
1259 let t3 = mgr.text(&DocId::Microdesc([255; 32])).unwrap();
1260 assert!(t3.is_none());
1261
1262 let d_bogus = DocId::Microdesc([255; 32]);
1264 let res = mgr
1265 .texts(vec![
1266 DocId::Microdesc(d2),
1267 DocId::Microdesc(d3),
1268 d_bogus,
1269 DocId::AuthCert(certid2),
1270 #[cfg(feature = "routerdesc")]
1271 DocId::RouterDesc(d5),
1272 ])
1273 .unwrap();
1274 assert_eq!(
1275 res.get(&DocId::Microdesc(d2)).unwrap().as_str(),
1276 Ok("Fake micro 2")
1277 );
1278 assert_eq!(
1279 res.get(&DocId::Microdesc(d3)).unwrap().as_str(),
1280 Ok("Fake micro 3")
1281 );
1282 assert!(!res.contains_key(&d_bogus));
1283 assert_eq!(
1284 res.get(&DocId::AuthCert(certid2)).unwrap().as_str(),
1285 Ok("Fake certificate two")
1286 );
1287 #[cfg(feature = "routerdesc")]
1288 assert_eq!(
1289 res.get(&DocId::RouterDesc(d5)).unwrap().as_str(),
1290 Ok("Fake rd2")
1291 );
1292 });
1293 }
1294
1295 #[test]
1296 fn make_consensus_request() {
1297 tor_rtcompat::test_with_one_runtime!(|rt| async {
1298 let now = rt.wallclock();
1299 let tomorrow = now + Duration::from_secs(86400);
1300 let later = tomorrow + Duration::from_secs(86400);
1301
1302 let (_tempdir, mgr) = new_mgr(rt);
1303 let config = DirMgrConfig::default();
1304
1305 let req = {
1307 let store = mgr.store.lock().unwrap();
1308 bootstrap::make_consensus_request(
1309 now,
1310 ConsensusFlavor::Microdesc,
1311 &**store,
1312 &config,
1313 )
1314 .unwrap()
1315 };
1316 let tolerance = DirTolerance::default().post_valid_tolerance;
1317 match req {
1318 ClientRequest::Consensus(r) => {
1319 assert_eq!(r.old_consensus_digests().count(), 0);
1320 let date = r.last_consensus_date().unwrap();
1321 assert!(date >= now - tolerance);
1322 assert!(date <= now - tolerance + Duration::from_secs(3600));
1323 }
1324 _ => panic!("Wrong request type"),
1325 }
1326
1327 let d_prev = [42; 32];
1329 {
1330 let mut store = mgr.store.lock().unwrap();
1331
1332 let cmeta = ConsensusMeta::new(
1333 Lifetime::new(now, tomorrow, later).unwrap(),
1334 d_prev,
1335 [103; 32],
1336 );
1337 store
1338 .store_consensus(&cmeta, ConsensusFlavor::Microdesc, false, "Fake consensus!")
1339 .unwrap();
1340 }
1341
1342 let req = {
1344 let store = mgr.store.lock().unwrap();
1345 bootstrap::make_consensus_request(
1346 now,
1347 ConsensusFlavor::Microdesc,
1348 &**store,
1349 &config,
1350 )
1351 .unwrap()
1352 };
1353 match req {
1354 ClientRequest::Consensus(r) => {
1355 let ds: Vec<_> = r.old_consensus_digests().collect();
1356 assert_eq!(ds.len(), 1);
1357 assert_eq!(ds[0], &d_prev);
1358 assert_eq!(r.last_consensus_date(), Some(now));
1359 }
1360 _ => panic!("Wrong request type"),
1361 }
1362 });
1363 }
1364
1365 #[test]
1366 fn make_other_requests() {
1367 tor_rtcompat::test_with_one_runtime!(|rt| async {
1368 use rand::Rng;
1369 let (_tempdir, mgr) = new_mgr(rt);
1370
1371 let certid1 = AuthCertKeyIds {
1372 id_fingerprint: [99; 20].into(),
1373 sk_fingerprint: [100; 20].into(),
1374 };
1375 let mut rng = testing_rng();
1376 #[cfg(feature = "routerdesc")]
1377 let rd_ids: Vec<DocId> = (0..1000).map(|_| DocId::RouterDesc(rng.gen())).collect();
1378 let md_ids: Vec<DocId> = (0..1000).map(|_| DocId::Microdesc(rng.gen())).collect();
1379 let config = DirMgrConfig::default();
1380
1381 let query = DocId::AuthCert(certid1);
1383 let store = mgr.store.lock().unwrap();
1384 let reqs =
1385 bootstrap::make_requests_for_documents(&mgr.runtime, &[query], &**store, &config)
1386 .unwrap();
1387 assert_eq!(reqs.len(), 1);
1388 let req = &reqs[0];
1389 if let ClientRequest::AuthCert(r) = req {
1390 assert_eq!(r.keys().next(), Some(&certid1));
1391 } else {
1392 panic!();
1393 }
1394
1395 let reqs =
1397 bootstrap::make_requests_for_documents(&mgr.runtime, &md_ids, &**store, &config)
1398 .unwrap();
1399 assert_eq!(reqs.len(), 2);
1400 assert!(matches!(reqs[0], ClientRequest::Microdescs(_)));
1401
1402 #[cfg(feature = "routerdesc")]
1404 {
1405 let reqs = bootstrap::make_requests_for_documents(
1406 &mgr.runtime,
1407 &rd_ids,
1408 &**store,
1409 &config,
1410 )
1411 .unwrap();
1412 assert_eq!(reqs.len(), 2);
1413 assert!(matches!(reqs[0], ClientRequest::RouterDescs(_)));
1414 }
1415 });
1416 }
1417
1418 #[test]
1419 fn expand_response() {
1420 tor_rtcompat::test_with_one_runtime!(|rt| async {
1421 let now = rt.wallclock();
1422 let day = Duration::from_secs(86400);
1423 let config = DirMgrConfig::default();
1424
1425 let (_tempdir, mgr) = new_mgr(rt);
1426
1427 let q = DocId::Microdesc([99; 32]);
1429 let r = {
1430 let store = mgr.store.lock().unwrap();
1431 bootstrap::make_requests_for_documents(&mgr.runtime, &[q], &**store, &config)
1432 .unwrap()
1433 };
1434 let expanded = mgr.expand_response_text(&r[0], "ABC".to_string());
1435 assert_eq!(&expanded.unwrap(), "ABC");
1436
1437 let latest_id = DocId::LatestConsensus {
1440 flavor: ConsensusFlavor::Microdesc,
1441 cache_usage: CacheUsage::CacheOkay,
1442 };
1443 let r = {
1444 let store = mgr.store.lock().unwrap();
1445 bootstrap::make_requests_for_documents(
1446 &mgr.runtime,
1447 &[latest_id],
1448 &**store,
1449 &config,
1450 )
1451 .unwrap()
1452 };
1453 let expanded = mgr.expand_response_text(&r[0], "DEF".to_string());
1454 assert_eq!(&expanded.unwrap(), "DEF");
1455
1456 {
1459 let mut store = mgr.store.lock().unwrap();
1460 let d_in = [0x99; 32]; let cmeta = ConsensusMeta::new(
1462 Lifetime::new(now, now + day, now + 2 * day).unwrap(),
1463 d_in,
1464 d_in,
1465 );
1466 store
1467 .store_consensus(
1468 &cmeta,
1469 ConsensusFlavor::Microdesc,
1470 false,
1471 "line 1\nline2\nline 3\n",
1472 )
1473 .unwrap();
1474 }
1475
1476 let r = {
1479 let store = mgr.store.lock().unwrap();
1480 bootstrap::make_requests_for_documents(
1481 &mgr.runtime,
1482 &[latest_id],
1483 &**store,
1484 &config,
1485 )
1486 .unwrap()
1487 };
1488 let expanded = mgr.expand_response_text(&r[0], "hello".to_string());
1489 assert_eq!(&expanded.unwrap(), "hello");
1490
1491 let diff = "network-status-diff-version 1
1493hash 9999999999999999999999999999999999999999999999999999999999999999 8382374ca766873eb0d2530643191c6eaa2c5e04afa554cbac349b5d0592d300
14942c
1495replacement line
1496.
1497".to_string();
1498 let expanded = mgr.expand_response_text(&r[0], diff);
1499
1500 assert_eq!(expanded.unwrap(), "line 1\nreplacement line\nline 3\n");
1501
1502 let diff = "network-status-diff-version 1
1504hash 9999999999999999999999999999999999999999999999999999999999999999 9999999999999999999999999999999999999999999999999999999999999999
15052c
1506replacement line
1507.
1508".to_string();
1509 let expanded = mgr.expand_response_text(&r[0], diff);
1510 assert!(expanded.is_err());
1511 });
1512 }
1513}