Skip to main content

tor_dirmgr/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3// @@ begin lint list maintained by maint/add_warning @@
4#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6#![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#![allow(clippy::cognitive_complexity)] // See arti#2556
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_time_subtraction)]
34#![deny(clippy::unnecessary_wraps)]
35#![warn(clippy::unseparated_literal_suffix)]
36#![deny(clippy::unwrap_used)]
37#![deny(clippy::mod_module_files)]
38#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39#![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43#![allow(clippy::needless_lifetimes)] // See arti#1765
44#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45#![allow(clippy::collapsible_if)] // See arti#2342
46#![deny(clippy::unused_async)]
47#![deny(clippy::string_slice)] // See arti#2571
48//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
49
50// This clippy lint produces a false positive on `use strum`, below.
51// Attempting to apply the lint to just the use statement fails to suppress
52// this lint and instead produces another lint about a useless clippy attribute.
53#![allow(clippy::single_component_path_imports)]
54
55mod bootstrap;
56pub mod config;
57mod docid;
58mod docmeta;
59mod err;
60mod event;
61mod shared_ref;
62mod state;
63mod storage;
64
65#[cfg(feature = "dir-plugin")]
66mod as_plugin;
67#[cfg(feature = "bridge-client")]
68pub mod bridgedesc;
69#[cfg(feature = "dirfilter")]
70pub mod filter;
71
72use crate::docid::{CacheUsage, ClientRequest, DocQuery};
73use crate::err::BootstrapAction;
74#[cfg(not(feature = "experimental-api"))]
75use crate::shared_ref::SharedMutArc;
76#[cfg(feature = "experimental-api")]
77pub use crate::shared_ref::SharedMutArc;
78use crate::storage::{DynStore, Store};
79use bootstrap::AttemptId;
80use event::DirProgress;
81use postage::watch;
82use scopeguard::ScopeGuard;
83use tor_circmgr::CircMgr;
84use tor_dirclient::SourceInfo;
85use tor_dircommon::config::DirTolerance;
86use tor_error::{info_report, into_internal, warn_report};
87use tor_netdir::params::NetParameters;
88use tor_netdir::{DirEvent, MdReceiver, NetDir, NetDirProvider};
89
90use async_trait::async_trait;
91use futures::stream::BoxStream;
92use oneshot_fused_workaround as oneshot;
93use tor_netdoc::doc::netstatus::ProtoStatuses;
94use tor_rtcompat::scheduler::{TaskHandle, TaskSchedule};
95use tor_rtcompat::{Runtime, SpawnExt};
96use tracing::{debug, info, instrument, trace, warn};
97use web_time_compat::SystemTimeExt;
98
99use std::marker::PhantomData;
100use std::sync::atomic::{AtomicBool, Ordering};
101use std::sync::{Arc, Mutex};
102use std::time::Duration;
103use std::{collections::HashMap, sync::Weak};
104use std::{fmt::Debug, time::SystemTime};
105
106use crate::state::{DirState, NetDirChange};
107pub use config::DirMgrConfig;
108pub use docid::DocId;
109pub use err::Error;
110pub use event::{DirBlockage, DirBootstrapEvents, DirBootstrapStatus};
111pub use storage::DocumentText;
112pub use tor_dircommon::fallback::{FallbackDir, FallbackDirBuilder};
113pub use tor_netdir::Timeliness;
114
115#[cfg(feature = "dir-plugin")]
116pub use as_plugin::DirPlugin;
117
118/// Re-export of `strum` crate for use by an internal macro
119use strum;
120
121/// A Result as returned by this crate.
122pub type Result<T> = std::result::Result<T, Error>;
123
124/// Storage manager used by [`DirMgr`] and
125/// [`BridgeDescMgr`](bridgedesc::BridgeDescMgr)
126///
127/// Internally, this wraps up a sqlite database.
128///
129/// This is a handle, which is cheap to clone; clones share state.
130#[derive(Clone)]
131pub struct DirMgrStore<R: Runtime> {
132    /// The actual store
133    pub(crate) store: Arc<Mutex<crate::DynStore>>,
134
135    /// Be parameterized by Runtime even though we don't use it right now
136    pub(crate) runtime: PhantomData<R>,
137}
138
139impl<R: Runtime> DirMgrStore<R> {
140    /// Open the storage, according to the specified configuration
141    pub fn new(config: &DirMgrConfig, runtime: R, offline: bool) -> Result<Self> {
142        let store = Arc::new(Mutex::new(config.open_store(offline)?));
143        drop(runtime);
144        let runtime = PhantomData;
145        Ok(DirMgrStore { store, runtime })
146    }
147}
148
149/// Trait for DirMgr implementations
150#[async_trait]
151pub trait DirProvider: NetDirProvider {
152    /// Try to change our configuration to `new_config`.
153    ///
154    /// Actual behavior will depend on the value of `how`.
155    fn reconfigure(
156        &self,
157        new_config: &DirMgrConfig,
158        how: tor_config::Reconfigure,
159    ) -> std::result::Result<(), tor_config::ReconfigureError>;
160
161    /// Bootstrap a `DirProvider` that hasn't been bootstrapped yet.
162    async fn bootstrap(&self) -> Result<()>;
163
164    /// Return a stream of [`DirBootstrapStatus`] events to tell us about changes
165    /// in the latest directory's bootstrap status.
166    ///
167    /// Note that this stream can be lossy: the caller will not necessarily
168    /// observe every event on the stream
169    fn bootstrap_events(&self) -> BoxStream<'static, DirBootstrapStatus>;
170
171    /// Return a [`TaskHandle`] that can be used to manage the download process.
172    fn download_task_handle(&self) -> Option<TaskHandle> {
173        None
174    }
175}
176
177// NOTE(eta): We can't implement this for Arc<DirMgr<R>> due to trait coherence rules, so instead
178//            there's a blanket impl for Arc<T> in tor-netdir.
179impl<R: Runtime> NetDirProvider for DirMgr<R> {
180    fn netdir(&self, timeliness: Timeliness) -> tor_netdir::Result<Arc<NetDir>> {
181        use tor_netdir::Error as NetDirError;
182        let netdir = self.netdir.get().ok_or(NetDirError::NoInfo)?;
183        let lifetime = match timeliness {
184            Timeliness::Strict => netdir.lifetime().clone(),
185            Timeliness::Timely => self
186                .config
187                .get()
188                .tolerance
189                .extend_lifetime(netdir.lifetime()),
190            Timeliness::Unchecked => return Ok(netdir),
191        };
192        // TODO #2384 -- we have a runtime here; we should use it.
193        let now = SystemTime::get();
194        if lifetime.valid_after() > now {
195            Err(NetDirError::DirNotYetValid)
196        } else if lifetime.valid_until() < now {
197            Err(NetDirError::DirExpired)
198        } else {
199            Ok(netdir)
200        }
201    }
202
203    fn events(&self) -> BoxStream<'static, DirEvent> {
204        Box::pin(self.events.subscribe())
205    }
206
207    fn params(&self) -> Arc<dyn AsRef<tor_netdir::params::NetParameters>> {
208        if let Some(netdir) = self.netdir.get() {
209            // We have a directory, so we'd like to give it out for its
210            // parameters.
211            //
212            // We do this even if the directory is expired, since parameters
213            // don't really expire on any plausible timescale.
214            netdir
215        } else {
216            // We have no directory, so we'll give out the default parameters as
217            // modified by the provided override_net_params configuration.
218            //
219            self.default_parameters
220                .lock()
221                .expect("Poisoned lock")
222                .clone()
223        }
224        // TODO(nickm): If we felt extremely clever, we could add a third case
225        // where, if we have a pending directory with a validated consensus, we
226        // give out that consensus's network parameters even if we _don't_ yet
227        // have a full directory.  That's significant refactoring, though, for
228        // an unclear amount of benefit.
229    }
230
231    fn protocol_statuses(&self) -> Option<(SystemTime, Arc<ProtoStatuses>)> {
232        self.protocols.lock().expect("Poisoned lock").clone()
233    }
234}
235
236#[async_trait]
237impl<R: Runtime> DirProvider for Arc<DirMgr<R>> {
238    fn reconfigure(
239        &self,
240        new_config: &DirMgrConfig,
241        how: tor_config::Reconfigure,
242    ) -> std::result::Result<(), tor_config::ReconfigureError> {
243        DirMgr::reconfigure(self, new_config, how)
244    }
245
246    #[instrument(level = "trace", skip_all)]
247    async fn bootstrap(&self) -> Result<()> {
248        DirMgr::bootstrap(self).await
249    }
250
251    fn bootstrap_events(&self) -> BoxStream<'static, DirBootstrapStatus> {
252        Box::pin(DirMgr::bootstrap_events(self))
253    }
254
255    fn download_task_handle(&self) -> Option<TaskHandle> {
256        Some(self.task_handle.clone())
257    }
258}
259
260/// A directory manager to download, fetch, and cache a Tor directory.
261///
262/// A DirMgr can operate in three modes:
263///   * In **offline** mode, it only reads from the cache, and can
264///     only read once.
265///   * In **read-only** mode, it reads from the cache, but checks
266///     whether it can acquire an associated lock file.  If it can, then
267///     it enters read-write mode.  If not, it checks the cache
268///     periodically for new information.
269///   * In **read-write** mode, it knows that no other process will be
270///     writing to the cache, and it takes responsibility for fetching
271///     data from the network and updating the directory with new
272///     directory information.
273pub struct DirMgr<R: Runtime> {
274    /// Configuration information: where to find directories, how to
275    /// validate them, and so on.
276    config: tor_config::MutCfg<DirMgrConfig>,
277    /// Handle to our sqlite cache.
278    // TODO(nickm): I'd like to use an rwlock, but that's not feasible, since
279    // rusqlite::Connection isn't Sync.
280    // TODO is needed?
281    store: Arc<Mutex<DynStore>>,
282    /// Our latest sufficiently bootstrapped directory, if we have one.
283    ///
284    /// We use the RwLock so that we can give this out to a bunch of other
285    /// users, and replace it once a new directory is bootstrapped.
286    // TODO(eta): Eurgh! This is so many Arcs! (especially considering this
287    //            gets wrapped in an Arc)
288    netdir: Arc<SharedMutArc<NetDir>>,
289
290    /// Our latest set of recommended protocols.
291    protocols: Mutex<Option<(SystemTime, Arc<ProtoStatuses>)>>,
292
293    /// A set of network parameters to hand out when we have no directory.
294    default_parameters: Mutex<Arc<NetParameters>>,
295
296    /// A publisher handle that we notify whenever the consensus changes.
297    events: event::FlagPublisher<DirEvent>,
298
299    /// A publisher handle that we notify whenever our bootstrapping status
300    /// changes.
301    send_status: Mutex<watch::Sender<event::DirBootstrapStatus>>,
302
303    /// A receiver handle that gets notified whenever our bootstrapping status
304    /// changes.
305    ///
306    /// We don't need to keep this drained, since `postage::watch` already knows
307    /// to discard unread events.
308    receive_status: DirBootstrapEvents,
309
310    /// A circuit manager, if this DirMgr supports downloading.
311    circmgr: Option<Arc<CircMgr<R>>>,
312
313    /// Our asynchronous runtime.
314    runtime: R,
315
316    /// Whether or not we're operating in offline mode.
317    offline: bool,
318
319    /// If we're not in offline mode, stores whether or not the `DirMgr` has attempted
320    /// to bootstrap yet or not.
321    ///
322    /// This exists in order to prevent starting two concurrent bootstrap tasks.
323    ///
324    /// (In offline mode, this does nothing.)
325    bootstrap_started: AtomicBool,
326
327    /// A filter that gets applied to directory objects before we use them.
328    #[cfg(feature = "dirfilter")]
329    filter: crate::filter::FilterConfig,
330
331    /// A task schedule that can be used if we're bootstrapping.  If this is
332    /// None, then there's currently a scheduled task in progress.
333    task_schedule: Mutex<Option<TaskSchedule<R>>>,
334
335    /// A task handle that we return to anybody who needs to manage our download process.
336    task_handle: TaskHandle,
337}
338
339/// The possible origins of a document.
340///
341/// Used (for example) to report where we got a document from if it fails to
342/// parse.
343#[derive(Debug, Clone)]
344#[non_exhaustive]
345pub enum DocSource {
346    /// We loaded the document from our cache.
347    LocalCache,
348    /// We fetched the document from a server.
349    DirServer {
350        /// Information about the server we fetched the document from.
351        source: Option<SourceInfo>,
352    },
353}
354
355impl std::fmt::Display for DocSource {
356    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357        match self {
358            DocSource::LocalCache => write!(f, "local cache"),
359            DocSource::DirServer { source: None } => write!(f, "directory server"),
360            DocSource::DirServer { source: Some(info) } => write!(f, "directory server {}", info),
361        }
362    }
363}
364
365impl<R: Runtime> DirMgr<R> {
366    /// Try to load the directory from disk, without launching any
367    /// kind of update process.
368    ///
369    /// This function runs in **offline** mode: it will give an error
370    /// if the result is not up-to-date, or not fully downloaded.
371    ///
372    /// In general, you shouldn't use this function in a long-running
373    /// program; it's only suitable for command-line or batch tools.
374    // TODO: I wish this function didn't have to be async or take a runtime.
375    pub fn load_once(runtime: R, config: DirMgrConfig) -> Result<Arc<NetDir>> {
376        let store = DirMgrStore::new(&config, runtime.clone(), true)?;
377        let dirmgr = Arc::new(Self::from_config(config, runtime, store, None, true)?);
378
379        // TODO: add some way to return a directory that isn't up-to-date
380        let attempt = AttemptId::next();
381        trace!(%attempt, "Trying to load a full directory from cache");
382        let outcome = dirmgr.load_directory(attempt);
383        trace!(%attempt, "Load result: {outcome:?}");
384        let _success = outcome?;
385
386        dirmgr
387            .netdir(Timeliness::Timely)
388            .map_err(|_| Error::DirectoryNotPresent)
389    }
390
391    /// Return a current netdir, either loading it or bootstrapping it
392    /// as needed.
393    ///
394    /// Like load_once, but will try to bootstrap (or wait for another
395    /// process to bootstrap) if we don't have an up-to-date
396    /// bootstrapped directory.
397    ///
398    /// In general, you shouldn't use this function in a long-running
399    /// program; it's only suitable for command-line or batch tools.
400    pub async fn load_or_bootstrap_once(
401        config: DirMgrConfig,
402        runtime: R,
403        store: DirMgrStore<R>,
404        circmgr: Arc<CircMgr<R>>,
405    ) -> Result<Arc<NetDir>> {
406        let dirmgr = DirMgr::bootstrap_from_config(config, runtime, store, circmgr).await?;
407        dirmgr
408            .timely_netdir()
409            .map_err(|_| Error::DirectoryNotPresent)
410    }
411
412    /// Create a new `DirMgr` in online mode, but don't bootstrap it yet.
413    ///
414    /// The `DirMgr` can be bootstrapped later with `bootstrap`.
415    pub fn create_unbootstrapped(
416        config: DirMgrConfig,
417        runtime: R,
418        store: DirMgrStore<R>,
419        circmgr: Arc<CircMgr<R>>,
420    ) -> Result<Arc<Self>> {
421        Ok(Arc::new(DirMgr::from_config(
422            config,
423            runtime,
424            store,
425            Some(circmgr),
426            false,
427        )?))
428    }
429
430    /// Bootstrap a `DirMgr` created in online mode that hasn't been bootstrapped yet.
431    ///
432    /// This function will not return until the directory is bootstrapped enough to build circuits.
433    /// It will also launch a background task that fetches any missing information, and that
434    /// replaces the directory when a new one is available.
435    ///
436    /// This function is intended to be used together with `create_unbootstrapped`. There is no
437    /// need to call this function otherwise.
438    ///
439    /// If bootstrapping has already successfully taken place, returns early with success.
440    ///
441    /// # Errors
442    ///
443    /// Returns an error if bootstrapping fails. If the error is [`Error::CantAdvanceState`],
444    /// it may be possible to successfully bootstrap later on by calling this function again.
445    ///
446    /// # Panics
447    ///
448    /// Panics if the `DirMgr` passed to this function was not created in online mode, such as
449    /// via `load_once`.
450    #[instrument(level = "trace", skip_all)]
451    pub async fn bootstrap(self: &Arc<Self>) -> Result<()> {
452        if self.offline {
453            return Err(Error::OfflineMode);
454        }
455
456        // The semantics of this are "attempt to replace a 'false' value with 'true'.
457        // If the value in bootstrap_started was not 'false' when the attempt was made, returns
458        // `Err`; this means another bootstrap attempt is in progress or has completed, so we
459        // return early.
460
461        // NOTE(eta): could potentially weaken the `Ordering` here in future
462        if self
463            .bootstrap_started
464            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
465            .is_err()
466        {
467            debug!("Attempted to bootstrap twice; ignoring.");
468            return Ok(());
469        }
470
471        // Use a RAII guard to reset `bootstrap_started` to `false` if we return early without
472        // completing bootstrap.
473        let reset_bootstrap_started = scopeguard::guard(&self.bootstrap_started, |v| {
474            v.store(false, Ordering::SeqCst);
475        });
476
477        let schedule = {
478            let sched = self.task_schedule.lock().expect("poisoned lock").take();
479            match sched {
480                Some(sched) => sched,
481                None => {
482                    debug!("Attempted to bootstrap twice; ignoring.");
483                    return Ok(());
484                }
485            }
486        };
487
488        // Try to load from the cache.
489        let attempt_id = AttemptId::next();
490        trace!(attempt=%attempt_id, "Starting to bootstrap directory");
491        let have_directory = self.load_directory(attempt_id)?;
492
493        let (mut sender, receiver) = if have_directory {
494            info!("Loaded a good directory from cache.");
495            (None, None)
496        } else {
497            info!("Didn't get usable directory from cache.");
498            let (sender, receiver) = oneshot::channel();
499            (Some(sender), Some(receiver))
500        };
501
502        // Whether we loaded or not, we now start downloading.
503        let dirmgr_weak = Arc::downgrade(self);
504        self.runtime
505            .spawn(async move {
506                // Use an RAII guard to make sure that when this task exits, the
507                // TaskSchedule object is put back.
508                //
509                // TODO(nick): Putting the schedule back isn't actually useful
510                // if the task exits _after_ we've bootstrapped for the first
511                // time, because of how bootstrap_started works.
512                let mut schedule = scopeguard::guard(schedule, |schedule| {
513                    if let Some(dm) = Weak::upgrade(&dirmgr_weak) {
514                        *dm.task_schedule.lock().expect("poisoned lock") = Some(schedule);
515                    }
516                });
517
518                // Don't warn when these are Error::ManagerDropped: that
519                // means that the DirMgr has been shut down.
520                if let Err(e) =
521                    Self::reload_until_owner(&dirmgr_weak, &mut schedule, attempt_id, &mut sender)
522                        .await
523                {
524                    match e {
525                        Error::ManagerDropped => {}
526                        _ => warn_report!(e, "Unrecovered error while waiting for bootstrap",),
527                    }
528                } else if let Err(e) =
529                    Self::download_forever(dirmgr_weak.clone(), &mut schedule, attempt_id, sender)
530                        .await
531                {
532                    match e {
533                        Error::ManagerDropped => {}
534                        _ => warn_report!(e, "Unrecovered error while downloading"),
535                    }
536                }
537            })
538            .map_err(|e| Error::from_spawn("directory updater task", e))?;
539
540        if let Some(receiver) = receiver {
541            match receiver.await {
542                Ok(()) => {
543                    info!("We have enough information to build circuits.");
544                    // Disarm the RAII guard, since we succeeded.  Now bootstrap_started will remain true.
545                    let _ = ScopeGuard::into_inner(reset_bootstrap_started);
546                }
547                Err(_) => {
548                    warn!("Bootstrapping task exited before finishing.");
549                    return Err(Error::CantAdvanceState);
550                }
551            }
552        }
553        Ok(())
554    }
555
556    /// Returns `true` if a bootstrap attempt is in progress, or successfully completed.
557    pub fn bootstrap_started(&self) -> bool {
558        self.bootstrap_started.load(Ordering::SeqCst)
559    }
560
561    /// Return a new directory manager from a given configuration,
562    /// bootstrapping from the network as necessary.
563    #[instrument(level = "trace", skip_all)]
564    pub async fn bootstrap_from_config(
565        config: DirMgrConfig,
566        runtime: R,
567        store: DirMgrStore<R>,
568        circmgr: Arc<CircMgr<R>>,
569    ) -> Result<Arc<Self>> {
570        let dirmgr = Self::create_unbootstrapped(config, runtime, store, circmgr)?;
571
572        dirmgr.bootstrap().await?;
573
574        Ok(dirmgr)
575    }
576
577    /// Try forever to either lock the storage (and thereby become the
578    /// owner), or to reload the database.
579    ///
580    /// If we have begin to have a bootstrapped directory, send a
581    /// message using `on_complete`.
582    ///
583    /// If we eventually become the owner, return Ok().
584    async fn reload_until_owner(
585        weak: &Weak<Self>,
586        schedule: &mut TaskSchedule<R>,
587        attempt_id: AttemptId,
588        on_complete: &mut Option<oneshot::Sender<()>>,
589    ) -> Result<()> {
590        let mut logged = false;
591        let mut bootstrapped;
592        {
593            let dirmgr = upgrade_weak_ref(weak)?;
594            bootstrapped = dirmgr.netdir.get().is_some();
595        }
596
597        loop {
598            {
599                let dirmgr = upgrade_weak_ref(weak)?;
600                trace!("Trying to take ownership of the directory cache lock");
601                if dirmgr.try_upgrade_to_readwrite()? {
602                    // We now own the lock!  (Maybe we owned it before; the
603                    // upgrade_to_readwrite() function is idempotent.)  We can
604                    // do our own bootstrapping.
605                    if logged {
606                        info!(
607                            "The previous owning process has given up the lock. We are now in charge of managing the directory."
608                        );
609                    }
610                    return Ok(());
611                }
612            }
613
614            if !logged {
615                logged = true;
616                if bootstrapped {
617                    info!("Another process is managing the directory. We'll use its cache.");
618                } else {
619                    info!(
620                        "Another process is bootstrapping the directory. Waiting till it finishes or exits."
621                    );
622                }
623            }
624
625            // We don't own the lock.  Somebody else owns the cache.  They
626            // should be updating it.  Wait a bit, then try again.
627            let pause = if bootstrapped {
628                std::time::Duration::new(120, 0)
629            } else {
630                std::time::Duration::new(5, 0)
631            };
632            schedule.sleep(pause).await?;
633            // TODO: instead of loading the whole thing we should have a
634            // database entry that says when the last update was, or use
635            // our state functions.
636            {
637                let dirmgr = upgrade_weak_ref(weak)?;
638                trace!("Trying to load from the directory cache");
639                if dirmgr.load_directory(attempt_id)? {
640                    // Successfully loaded a bootstrapped directory.
641                    if let Some(send_done) = on_complete.take() {
642                        let _ = send_done.send(());
643                    }
644                    if !bootstrapped {
645                        info!("The directory is now bootstrapped.");
646                    }
647                    bootstrapped = true;
648                }
649            }
650        }
651    }
652
653    /// Try to fetch our directory info and keep it updated, indefinitely.
654    ///
655    /// If we have begin to have a bootstrapped directory, send a
656    /// message using `on_complete`.
657    #[instrument(level = "trace", skip_all)]
658    async fn download_forever(
659        weak: Weak<Self>,
660        schedule: &mut TaskSchedule<R>,
661        mut attempt_id: AttemptId,
662        mut on_complete: Option<oneshot::Sender<()>>,
663    ) -> Result<()> {
664        let mut state: Box<dyn DirState> = {
665            let dirmgr = upgrade_weak_ref(&weak)?;
666            Box::new(state::GetConsensusState::new(
667                dirmgr.runtime.clone(),
668                dirmgr.config.get(),
669                CacheUsage::CacheOkay,
670                Some(dirmgr.netdir.clone()),
671                #[cfg(feature = "dirfilter")]
672                dirmgr
673                    .filter
674                    .clone()
675                    .unwrap_or_else(|| Arc::new(crate::filter::NilFilter)),
676            ))
677        };
678
679        trace!("Entering download loop.");
680
681        loop {
682            let mut usable = false;
683
684            let retry_config = {
685                let dirmgr = upgrade_weak_ref(&weak)?;
686                // TODO(nickm): instead of getting this every time we loop, it
687                // might be a good idea to refresh it with each attempt, at
688                // least at the point of checking the number of attempts.
689                dirmgr.config.get().schedule.retry_bootstrap()
690            };
691            let mut retry_delay = retry_config.schedule();
692
693            'retry_attempt: for try_num in retry_config.attempts() {
694                trace!(attempt=%attempt_id, ?try_num, "Trying to download a directory.");
695                let outcome = bootstrap::download(
696                    Weak::clone(&weak),
697                    &mut state,
698                    schedule,
699                    attempt_id,
700                    &mut on_complete,
701                )
702                .await;
703                trace!(attempt=%attempt_id, ?try_num, ?outcome, "Download is over.");
704
705                if let Err(err) = outcome {
706                    if state.is_ready(Readiness::Usable) {
707                        usable = true;
708                        info_report!(
709                            err,
710                            "Unable to completely download a directory. (Nevertheless, the directory is usable, so we'll pause for now)"
711                        );
712                        break 'retry_attempt;
713                    }
714
715                    match err.bootstrap_action() {
716                        BootstrapAction::Nonfatal => {
717                            return Err(into_internal!(
718                                "Nonfatal error should not have propagated here"
719                            )(err)
720                            .into());
721                        }
722                        BootstrapAction::Reset => {}
723                        BootstrapAction::Fatal => return Err(err),
724                    }
725
726                    let delay = retry_delay.next_delay(&mut rand::rng());
727                    warn_report!(
728                        err,
729                        "Unable to download a usable directory. (We will restart in {})",
730                        humantime::format_duration(delay),
731                    );
732                    {
733                        let dirmgr = upgrade_weak_ref(&weak)?;
734                        dirmgr.note_reset(attempt_id);
735                    }
736                    schedule.sleep(delay).await?;
737                    state = state.reset();
738                } else {
739                    info!(attempt=%attempt_id, "Directory is complete.");
740                    usable = true;
741                    break 'retry_attempt;
742                }
743            }
744
745            if !usable {
746                // we ran out of attempts.
747                warn!(
748                    "We failed {} times to bootstrap a directory. We're going to give up.",
749                    retry_config.n_attempts()
750                );
751                return Err(Error::CantAdvanceState);
752            } else {
753                // Report success, if appropriate.
754                if let Some(send_done) = on_complete.take() {
755                    let _ = send_done.send(());
756                }
757            }
758
759            let reset_at = state.reset_time();
760            match reset_at {
761                Some(t) => {
762                    trace!("Sleeping until {}", time::OffsetDateTime::from(t));
763                    schedule.sleep_until_wallclock(t).await?;
764                }
765                None => return Ok(()),
766            }
767            attempt_id = bootstrap::AttemptId::next();
768            trace!(attempt=%attempt_id, "Beginning new attempt to bootstrap directory");
769            state = state.reset();
770        }
771    }
772
773    /// Get a reference to the circuit manager, if we have one.
774    fn circmgr(&self) -> Result<Arc<CircMgr<R>>> {
775        self.circmgr.clone().ok_or(Error::NoDownloadSupport)
776    }
777
778    /// Try to change our configuration to `new_config`.
779    ///
780    /// Actual behavior will depend on the value of `how`.
781    pub fn reconfigure(
782        &self,
783        new_config: &DirMgrConfig,
784        how: tor_config::Reconfigure,
785    ) -> std::result::Result<(), tor_config::ReconfigureError> {
786        let config = self.config.get();
787        // We don't support changing these: doing so basically would require us
788        // to abort all our in-progress downloads, since they might be based on
789        // no-longer-viable information.
790        // NOTE: keep this in sync with the behaviour of `DirMgrConfig::update_from_config`
791        if new_config.cache_dir != config.cache_dir {
792            how.cannot_change("storage.cache_dir")?;
793        }
794        if new_config.cache_trust != config.cache_trust {
795            how.cannot_change("storage.permissions")?;
796        }
797        if new_config.authorities() != config.authorities() {
798            how.cannot_change("network.authorities")?;
799        }
800
801        if how == tor_config::Reconfigure::CheckAllOrNothing {
802            return Ok(());
803        }
804
805        let params_changed = new_config.override_net_params != config.override_net_params;
806
807        self.config
808            .map_and_replace(|cfg| cfg.update_from_config(new_config));
809
810        if params_changed {
811            let _ignore_err = self.netdir.mutate(|netdir| {
812                netdir.replace_overridden_parameters(&new_config.override_net_params);
813                Ok(())
814            });
815            {
816                let mut params = self.default_parameters.lock().expect("lock failed");
817                *params = Arc::new(NetParameters::from_map(&new_config.override_net_params));
818            }
819
820            // (It's okay to ignore the error, since it just means that there
821            // was no current netdir.)
822            self.events.publish(DirEvent::NewConsensus);
823        }
824
825        Ok(())
826    }
827
828    /// Return a stream of [`DirBootstrapStatus`] events to tell us about changes
829    /// in the latest directory's bootstrap status.
830    ///
831    /// Note that this stream can be lossy: the caller will not necessarily
832    /// observe every event on the stream
833    pub fn bootstrap_events(&self) -> event::DirBootstrapEvents {
834        self.receive_status.clone()
835    }
836
837    /// Replace the latest status with `progress` and broadcast to anybody
838    /// watching via a [`DirBootstrapEvents`] stream.
839    fn update_progress(&self, attempt_id: AttemptId, progress: DirProgress) {
840        // TODO(nickm): can I kill off this lock by having something else own the sender?
841        let mut sender = self.send_status.lock().expect("poisoned lock");
842        let mut status = sender.borrow_mut();
843
844        status.update_progress(attempt_id, progress);
845    }
846
847    /// Update our status tracker to note that some number of errors has
848    /// occurred.
849    fn note_errors(&self, attempt_id: AttemptId, n_errors: usize) {
850        if n_errors == 0 {
851            return;
852        }
853        let mut sender = self.send_status.lock().expect("poisoned lock");
854        let mut status = sender.borrow_mut();
855
856        status.note_errors(attempt_id, n_errors);
857    }
858
859    /// Update our status tracker to note that we've needed to reset our download attempt.
860    fn note_reset(&self, attempt_id: AttemptId) {
861        let mut sender = self.send_status.lock().expect("poisoned lock");
862        let mut status = sender.borrow_mut();
863
864        status.note_reset(attempt_id);
865    }
866
867    /// Try to make this a directory manager with read-write access to its
868    /// storage.
869    ///
870    /// Return true if we got the lock, or if we already had it.
871    ///
872    /// Return false if another process has the lock
873    fn try_upgrade_to_readwrite(&self) -> Result<bool> {
874        self.store
875            .lock()
876            .expect("Directory storage lock poisoned")
877            .upgrade_to_readwrite()
878    }
879
880    /// Return a reference to the store, if it is currently read-write.
881    #[cfg(test)]
882    fn store_if_rw(&self) -> Option<&Mutex<DynStore>> {
883        let rw = !self
884            .store
885            .lock()
886            .expect("Directory storage lock poisoned")
887            .is_readonly();
888        // A race-condition is possible here, but I believe it's harmless.
889        if rw { Some(&self.store) } else { None }
890    }
891
892    /// Construct a DirMgr from a DirMgrConfig.
893    ///
894    /// If `offline` is set, opens the SQLite store read-only and sets the offline flag in the
895    /// returned manager.
896    #[allow(clippy::unnecessary_wraps)] // API compat and future-proofing
897    fn from_config(
898        config: DirMgrConfig,
899        runtime: R,
900        store: DirMgrStore<R>,
901        circmgr: Option<Arc<CircMgr<R>>>,
902        offline: bool,
903    ) -> Result<Self> {
904        let netdir = Arc::new(SharedMutArc::new());
905        let events = event::FlagPublisher::new();
906        let default_parameters = NetParameters::from_map(&config.override_net_params);
907        let default_parameters = Mutex::new(Arc::new(default_parameters));
908
909        let (send_status, receive_status) = postage::watch::channel();
910        let send_status = Mutex::new(send_status);
911        let receive_status = DirBootstrapEvents {
912            inner: receive_status,
913        };
914        #[cfg(feature = "dirfilter")]
915        let filter = config.extensions.filter.clone();
916
917        // We create these early so the client code can access task_handle before bootstrap() returns.
918        let (task_schedule, task_handle) = TaskSchedule::new(runtime.clone());
919        let task_schedule = Mutex::new(Some(task_schedule));
920
921        // We load the cached protocol recommendations unconditionally: the caller needs them even
922        // if it does not try to load the reset of the cache.
923        let protocols = {
924            let store = store.store.lock().expect("lock poisoned");
925            store
926                .cached_protocol_recommendations()?
927                .map(|(t, p)| (t, Arc::new(p)))
928        };
929
930        Ok(DirMgr {
931            config: config.into(),
932            store: store.store,
933            netdir,
934            protocols: Mutex::new(protocols),
935            default_parameters,
936            events,
937            send_status,
938            receive_status,
939            circmgr,
940            runtime,
941            offline,
942            bootstrap_started: AtomicBool::new(false),
943            #[cfg(feature = "dirfilter")]
944            filter,
945            task_schedule,
946            task_handle,
947        })
948    }
949
950    /// Load the latest non-pending non-expired directory from the
951    /// cache, if it is newer than the one we have.
952    ///
953    /// Return false if there is no such consensus.
954    fn load_directory(self: &Arc<Self>, attempt_id: AttemptId) -> Result<bool> {
955        let state = state::GetConsensusState::new(
956            self.runtime.clone(),
957            self.config.get(),
958            CacheUsage::CacheOnly,
959            None,
960            #[cfg(feature = "dirfilter")]
961            self.filter
962                .clone()
963                .unwrap_or_else(|| Arc::new(crate::filter::NilFilter)),
964        );
965        let _ = bootstrap::load(self, Box::new(state), attempt_id)?;
966
967        Ok(self.netdir.get().is_some())
968    }
969
970    /// Return a new asynchronous stream that will receive notification
971    /// whenever the consensus has changed.
972    ///
973    /// Multiple events may be batched up into a single item: each time
974    /// this stream yields an event, all you can assume is that the event has
975    /// occurred at least once.
976    pub fn events(&self) -> impl futures::Stream<Item = DirEvent> + use<R> {
977        self.events.subscribe()
978    }
979
980    /// Try to load the text of a single document described by `doc` from
981    /// storage.
982    pub fn text(&self, doc: &DocId) -> Result<Option<DocumentText>> {
983        use itertools::Itertools;
984        let mut result = HashMap::new();
985        let query: DocQuery = (*doc).into();
986        let store = self.store.lock().expect("store lock poisoned");
987        query.load_from_store_into(&mut result, &**store)?;
988        let item = result.into_iter().at_most_one().map_err(|_| {
989            Error::CacheCorruption("Found more than one entry in storage for given docid")
990        })?;
991        if let Some((docid, doctext)) = item {
992            if &docid != doc {
993                return Err(Error::CacheCorruption(
994                    "Item from storage had incorrect docid.",
995                ));
996            }
997            Ok(Some(doctext))
998        } else {
999            Ok(None)
1000        }
1001    }
1002
1003    /// Load the text for a collection of documents.
1004    ///
1005    /// If many of the documents have the same type, this can be more
1006    /// efficient than calling [`text`](Self::text).
1007    pub fn texts<T>(&self, docs: T) -> Result<HashMap<DocId, DocumentText>>
1008    where
1009        T: IntoIterator<Item = DocId>,
1010    {
1011        let partitioned = docid::partition_by_type(docs);
1012        let mut result = HashMap::new();
1013        let store = self.store.lock().expect("store lock poisoned");
1014        for (_, query) in partitioned.into_iter() {
1015            query.load_from_store_into(&mut result, &**store)?;
1016        }
1017        Ok(result)
1018    }
1019
1020    /// Given a request we sent and the response we got from a
1021    /// directory server, see whether we should expand that response
1022    /// into "something larger".
1023    ///
1024    /// Currently, this handles expanding consensus diffs, and nothing
1025    /// else.  We do it at this stage of our downloading operation
1026    /// because it requires access to the store.
1027    fn expand_response_text(&self, req: &ClientRequest, text: String) -> Result<String> {
1028        if let ClientRequest::Consensus(req) = req {
1029            if tor_consdiff::looks_like_diff(&text) {
1030                if let Some(old_d) = req.old_consensus_digests().next() {
1031                    let db_val = {
1032                        let s = self.store.lock().expect("Directory storage lock poisoned");
1033                        s.consensus_by_sha3_digest_of_signed_part(old_d)?
1034                    };
1035                    if let Some((old_consensus, meta)) = db_val {
1036                        info!("Applying a consensus diff");
1037                        let new_consensus = tor_consdiff::apply_diff(
1038                            old_consensus.as_str()?,
1039                            &text,
1040                            Some(*meta.sha3_256_of_signed()),
1041                        )?;
1042                        new_consensus.check_digest()?;
1043                        return Ok(new_consensus.to_string());
1044                    }
1045                }
1046                return Err(Error::Unwanted(
1047                    "Received a consensus diff we did not ask for",
1048                ));
1049            }
1050        }
1051        Ok(text)
1052    }
1053
1054    /// If `state` has netdir changes to apply, apply them to our netdir.
1055    fn apply_netdir_changes(
1056        self: &Arc<Self>,
1057        state: &mut Box<dyn DirState>,
1058        store: &mut dyn Store,
1059    ) -> Result<()> {
1060        if let Some(change) = state.get_netdir_change() {
1061            match change {
1062                NetDirChange::AttemptReplace {
1063                    netdir,
1064                    consensus_meta,
1065                } => {
1066                    // Check the new netdir is sufficient, if we have a circmgr.
1067                    // (Unwraps are fine because the `Option` is `Some` until we take it.)
1068                    if let Some(ref cm) = self.circmgr {
1069                        if !cm
1070                            .netdir_is_sufficient(netdir.as_ref().expect("AttemptReplace had None"))
1071                        {
1072                            debug!("Got a new NetDir, but it doesn't have enough guards yet.");
1073                            return Ok(());
1074                        }
1075                    }
1076                    let is_stale = {
1077                        // Done inside a block to not hold a long-lived copy of the NetDir.
1078                        self.netdir
1079                            .get()
1080                            .map(|x| {
1081                                x.lifetime().valid_after()
1082                                    > netdir
1083                                        .as_ref()
1084                                        .expect("AttemptReplace had None")
1085                                        .lifetime()
1086                                        .valid_after()
1087                            })
1088                            .unwrap_or(false)
1089                    };
1090                    if is_stale {
1091                        warn!("Got a new NetDir, but it's older than the one we currently have!");
1092                        return Err(Error::NetDirOlder);
1093                    }
1094                    let cfg = self.config.get();
1095                    let mut netdir = netdir.take().expect("AttemptReplace had None");
1096                    netdir.replace_overridden_parameters(&cfg.override_net_params);
1097                    self.netdir.replace(netdir);
1098                    self.events.publish(DirEvent::NewConsensus);
1099                    self.events.publish(DirEvent::NewDescriptors);
1100
1101                    info!("Marked consensus usable.");
1102                    if !store.is_readonly() {
1103                        store.mark_consensus_usable(consensus_meta)?;
1104                        // Now that a consensus is usable, older consensuses may
1105                        // need to expire.
1106                        store.expire_all(&crate::storage::EXPIRATION_DEFAULTS)?;
1107                    }
1108                    Ok(())
1109                }
1110                NetDirChange::AddMicrodescs(mds) => {
1111                    self.netdir.mutate(|netdir| {
1112                        for md in mds.drain(..) {
1113                            netdir.add_microdesc(md);
1114                        }
1115                        Ok(())
1116                    })?;
1117                    self.events.publish(DirEvent::NewDescriptors);
1118                    Ok(())
1119                }
1120                NetDirChange::SetRequiredProtocol { timestamp, protos } => {
1121                    if !store.is_readonly() {
1122                        store.update_protocol_recommendations(timestamp, protos.as_ref())?;
1123                    }
1124                    let mut pr = self.protocols.lock().expect("Poisoned lock");
1125                    *pr = Some((timestamp, protos));
1126                    self.events.publish(DirEvent::NewProtocolRecommendation);
1127                    Ok(())
1128                }
1129            }
1130        } else {
1131            Ok(())
1132        }
1133    }
1134
1135    /// Experimental; temporary: Return a directory plugin to be used while tor-dirserver is a work
1136    /// in progress.
1137    #[cfg(feature = "dir-plugin")]
1138    pub fn get_plugin(&self) -> as_plugin::DirPlugin {
1139        as_plugin::DirPlugin {
1140            store: Arc::clone(&self.store),
1141        }
1142    }
1143}
1144
1145/// A degree of readiness for a given directory state object.
1146#[derive(Debug, Copy, Clone)]
1147enum Readiness {
1148    /// There is no more information to download.
1149    Complete,
1150    /// There is more information to download, but we don't need to
1151    Usable,
1152}
1153
1154/// Try to upgrade a weak reference to a DirMgr, and give an error on
1155/// failure.
1156fn upgrade_weak_ref<T>(weak: &Weak<T>) -> Result<Arc<T>> {
1157    Weak::upgrade(weak).ok_or(Error::ManagerDropped)
1158}
1159
1160/// Given a time `now`, and an amount of tolerated clock skew `tolerance`,
1161/// return the age of the oldest consensus that we should request at that time.
1162pub(crate) fn default_consensus_cutoff(
1163    now: SystemTime,
1164    tolerance: &DirTolerance,
1165) -> Result<SystemTime> {
1166    /// We _always_ allow at least this much age in our consensuses, to account
1167    /// for the fact that consensuses have some lifetime.
1168    const MIN_AGE_TO_ALLOW: Duration = Duration::from_secs(3 * 3600);
1169    let allow_skew = std::cmp::max(MIN_AGE_TO_ALLOW, tolerance.post_valid_tolerance());
1170    let cutoff = time::OffsetDateTime::from(now - allow_skew);
1171    // We now round cutoff to the next hour, so that we aren't leaking our exact
1172    // time to the directory cache.
1173    //
1174    // With the time crate, it's easier to calculate the "next hour" by rounding
1175    // _down_ then adding an hour; rounding up would sometimes require changing
1176    // the date too.
1177    let (h, _m, _s) = cutoff.to_hms();
1178    let cutoff = cutoff.replace_time(
1179        time::Time::from_hms(h, 0, 0)
1180            .map_err(tor_error::into_internal!("Failed clock calculation"))?,
1181    );
1182    let cutoff = cutoff + Duration::from_secs(3600);
1183
1184    Ok(cutoff.into())
1185}
1186
1187/// Return a list of the protocols [supported](tor_protover::doc_supported) by this crate
1188/// when running as a client.
1189pub fn supported_client_protocols() -> tor_protover::Protocols {
1190    use tor_protover::named::*;
1191    // WARNING: REMOVING ELEMENTS FROM THIS LIST CAN BE DANGEROUS!
1192    // SEE [`tor_protover::doc_changing`]
1193    [
1194        //
1195        DIRCACHE_CONSDIFF,
1196    ]
1197    .into_iter()
1198    .collect()
1199}
1200
1201#[cfg(test)]
1202mod test {
1203    // @@ begin test lint list maintained by maint/add_warning @@
1204    #![allow(clippy::bool_assert_comparison)]
1205    #![allow(clippy::clone_on_copy)]
1206    #![allow(clippy::dbg_macro)]
1207    #![allow(clippy::mixed_attributes_style)]
1208    #![allow(clippy::print_stderr)]
1209    #![allow(clippy::print_stdout)]
1210    #![allow(clippy::single_char_pattern)]
1211    #![allow(clippy::unwrap_used)]
1212    #![allow(clippy::unchecked_time_subtraction)]
1213    #![allow(clippy::useless_vec)]
1214    #![allow(clippy::needless_pass_by_value)]
1215    #![allow(clippy::string_slice)] // See arti#2571
1216    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
1217    use super::*;
1218    use crate::docmeta::{AuthCertMeta, ConsensusMeta};
1219    use std::time::Duration;
1220    use tempfile::TempDir;
1221    use tor_basic_utils::test_rng::testing_rng;
1222    use tor_netdoc::doc::netstatus::ConsensusFlavor;
1223    use tor_netdoc::doc::{authcert::AuthCertKeyIds, netstatus::Lifetime};
1224    use tor_rtcompat::SleepProvider;
1225
1226    #[test]
1227    fn protocols() {
1228        let pr = supported_client_protocols();
1229        let expected = "DirCache=2".parse().unwrap();
1230        assert_eq!(pr, expected);
1231    }
1232
1233    pub(crate) fn new_mgr<R: Runtime>(runtime: R) -> (TempDir, DirMgr<R>) {
1234        let dir = TempDir::new().unwrap();
1235        let config = DirMgrConfig {
1236            cache_dir: dir.path().into(),
1237            ..Default::default()
1238        };
1239        let store = DirMgrStore::new(&config, runtime.clone(), false).unwrap();
1240        let dirmgr = DirMgr::from_config(config, runtime, store, None, false).unwrap();
1241
1242        (dir, dirmgr)
1243    }
1244
1245    #[test]
1246    fn failing_accessors() {
1247        tor_rtcompat::test_with_one_runtime!(|rt| async {
1248            let (_tempdir, mgr) = new_mgr(rt);
1249
1250            assert!(mgr.circmgr().is_err());
1251            assert!(mgr.netdir(Timeliness::Unchecked).is_err());
1252        });
1253    }
1254
1255    #[test]
1256    fn load_and_store_internals() {
1257        tor_rtcompat::test_with_one_runtime!(|rt| async {
1258            let now = rt.wallclock();
1259            let tomorrow = now + Duration::from_secs(86400);
1260            let later = tomorrow + Duration::from_secs(86400);
1261
1262            let (_tempdir, mgr) = new_mgr(rt);
1263
1264            // Seed the storage with a bunch of junk.
1265            let d1 = [5_u8; 32];
1266            let d2 = [7; 32];
1267            let d3 = [42; 32];
1268            let d4 = [99; 20];
1269            let d5 = [12; 20];
1270            let certid1 = AuthCertKeyIds {
1271                id_fingerprint: d4.into(),
1272                sk_fingerprint: d5.into(),
1273            };
1274            let certid2 = AuthCertKeyIds {
1275                id_fingerprint: d5.into(),
1276                sk_fingerprint: d4.into(),
1277            };
1278
1279            {
1280                let mut store = mgr.store.lock().unwrap();
1281
1282                store
1283                    .store_microdescs(
1284                        &[
1285                            ("Fake micro 1", &d1),
1286                            ("Fake micro 2", &d2),
1287                            ("Fake micro 3", &d3),
1288                        ],
1289                        now,
1290                    )
1291                    .unwrap();
1292
1293                #[cfg(feature = "routerdesc")]
1294                store
1295                    .store_routerdescs(&[("Fake rd1", now, &d4), ("Fake rd2", now, &d5)])
1296                    .unwrap();
1297
1298                store
1299                    .store_authcerts(&[
1300                        (
1301                            AuthCertMeta::new(certid1, now, tomorrow),
1302                            "Fake certificate one",
1303                        ),
1304                        (
1305                            AuthCertMeta::new(certid2, now, tomorrow),
1306                            "Fake certificate two",
1307                        ),
1308                    ])
1309                    .unwrap();
1310
1311                let cmeta = ConsensusMeta::new(
1312                    Lifetime::new(now, tomorrow, later).unwrap(),
1313                    [102; 32],
1314                    [103; 32],
1315                );
1316                store
1317                    .store_consensus(&cmeta, ConsensusFlavor::Microdesc, false, "Fake consensus!")
1318                    .unwrap();
1319            }
1320
1321            // Try to get it with text().
1322            let t1 = mgr.text(&DocId::Microdesc(d1)).unwrap().unwrap();
1323            assert_eq!(t1.as_str(), Ok("Fake micro 1"));
1324
1325            let t2 = mgr
1326                .text(&DocId::LatestConsensus {
1327                    flavor: ConsensusFlavor::Microdesc,
1328                    cache_usage: CacheUsage::CacheOkay,
1329                })
1330                .unwrap()
1331                .unwrap();
1332            assert_eq!(t2.as_str(), Ok("Fake consensus!"));
1333
1334            let t3 = mgr.text(&DocId::Microdesc([255; 32])).unwrap();
1335            assert!(t3.is_none());
1336
1337            // Now try texts()
1338            let d_bogus = DocId::Microdesc([255; 32]);
1339            let res = mgr
1340                .texts(vec![
1341                    DocId::Microdesc(d2),
1342                    DocId::Microdesc(d3),
1343                    d_bogus,
1344                    DocId::AuthCert(certid2),
1345                    #[cfg(feature = "routerdesc")]
1346                    DocId::RouterDesc(d5),
1347                ])
1348                .unwrap();
1349            assert_eq!(
1350                res.get(&DocId::Microdesc(d2)).unwrap().as_str(),
1351                Ok("Fake micro 2")
1352            );
1353            assert_eq!(
1354                res.get(&DocId::Microdesc(d3)).unwrap().as_str(),
1355                Ok("Fake micro 3")
1356            );
1357            assert!(!res.contains_key(&d_bogus));
1358            assert_eq!(
1359                res.get(&DocId::AuthCert(certid2)).unwrap().as_str(),
1360                Ok("Fake certificate two")
1361            );
1362            #[cfg(feature = "routerdesc")]
1363            assert_eq!(
1364                res.get(&DocId::RouterDesc(d5)).unwrap().as_str(),
1365                Ok("Fake rd2")
1366            );
1367        });
1368    }
1369
1370    #[test]
1371    fn make_consensus_request() {
1372        tor_rtcompat::test_with_one_runtime!(|rt| async {
1373            let now = rt.wallclock();
1374            let tomorrow = now + Duration::from_secs(86400);
1375            let later = tomorrow + Duration::from_secs(86400);
1376
1377            let (_tempdir, mgr) = new_mgr(rt);
1378            let config = DirMgrConfig::default();
1379
1380            // Try with an empty store.
1381            let req = {
1382                let store = mgr.store.lock().unwrap();
1383                bootstrap::make_consensus_request(
1384                    now,
1385                    ConsensusFlavor::Microdesc,
1386                    &**store,
1387                    &config,
1388                )
1389                .unwrap()
1390            };
1391            let tolerance = DirTolerance::default().post_valid_tolerance();
1392            match req {
1393                ClientRequest::Consensus(r) => {
1394                    assert_eq!(r.old_consensus_digests().count(), 0);
1395                    let date = r.last_consensus_date().unwrap();
1396                    assert!(date >= now - tolerance);
1397                    assert!(date <= now - tolerance + Duration::from_secs(3600));
1398                }
1399                _ => panic!("Wrong request type"),
1400            }
1401
1402            // Add a fake consensus record.
1403            let d_prev = [42; 32];
1404            {
1405                let mut store = mgr.store.lock().unwrap();
1406
1407                let cmeta = ConsensusMeta::new(
1408                    Lifetime::new(now, tomorrow, later).unwrap(),
1409                    d_prev,
1410                    [103; 32],
1411                );
1412                store
1413                    .store_consensus(&cmeta, ConsensusFlavor::Microdesc, false, "Fake consensus!")
1414                    .unwrap();
1415            }
1416
1417            // Now try again.
1418            let req = {
1419                let store = mgr.store.lock().unwrap();
1420                bootstrap::make_consensus_request(
1421                    now,
1422                    ConsensusFlavor::Microdesc,
1423                    &**store,
1424                    &config,
1425                )
1426                .unwrap()
1427            };
1428            match req {
1429                ClientRequest::Consensus(r) => {
1430                    let ds: Vec<_> = r.old_consensus_digests().collect();
1431                    assert_eq!(ds.len(), 1);
1432                    assert_eq!(ds[0], &d_prev);
1433                    assert_eq!(r.last_consensus_date(), Some(now));
1434                }
1435                _ => panic!("Wrong request type"),
1436            }
1437        });
1438    }
1439
1440    #[test]
1441    fn make_other_requests() {
1442        tor_rtcompat::test_with_one_runtime!(|rt| async {
1443            use rand::RngExt;
1444            let (_tempdir, mgr) = new_mgr(rt);
1445
1446            let certid1 = AuthCertKeyIds {
1447                id_fingerprint: [99; 20].into(),
1448                sk_fingerprint: [100; 20].into(),
1449            };
1450            let mut rng = testing_rng();
1451            #[cfg(feature = "routerdesc")]
1452            let rd_ids: Vec<DocId> = (0..1000).map(|_| DocId::RouterDesc(rng.random())).collect();
1453            let md_ids: Vec<DocId> = (0..1000).map(|_| DocId::Microdesc(rng.random())).collect();
1454            let config = DirMgrConfig::default();
1455
1456            // Try an authcert.
1457            let query = DocId::AuthCert(certid1);
1458            let store = mgr.store.lock().unwrap();
1459            let reqs =
1460                bootstrap::make_requests_for_documents(&mgr.runtime, &[query], &**store, &config)
1461                    .unwrap();
1462            assert_eq!(reqs.len(), 1);
1463            let req = &reqs[0];
1464            if let ClientRequest::AuthCert(r) = req {
1465                assert_eq!(r.keys().next(), Some(&certid1));
1466            } else {
1467                panic!();
1468            }
1469
1470            // Try a bunch of mds.
1471            let reqs =
1472                bootstrap::make_requests_for_documents(&mgr.runtime, &md_ids, &**store, &config)
1473                    .unwrap();
1474            assert_eq!(reqs.len(), 2);
1475            assert!(matches!(reqs[0], ClientRequest::Microdescs(_)));
1476
1477            // Try a bunch of rds.
1478            #[cfg(feature = "routerdesc")]
1479            {
1480                let reqs = bootstrap::make_requests_for_documents(
1481                    &mgr.runtime,
1482                    &rd_ids,
1483                    &**store,
1484                    &config,
1485                )
1486                .unwrap();
1487                assert_eq!(reqs.len(), 2);
1488                assert!(matches!(reqs[0], ClientRequest::RouterDescs(_)));
1489            }
1490        });
1491    }
1492
1493    #[test]
1494    fn expand_response() {
1495        tor_rtcompat::test_with_one_runtime!(|rt| async {
1496            let now = rt.wallclock();
1497            let day = Duration::from_secs(86400);
1498            let config = DirMgrConfig::default();
1499
1500            let (_tempdir, mgr) = new_mgr(rt);
1501
1502            // Try a simple request: nothing should happen.
1503            let q = DocId::Microdesc([99; 32]);
1504            let r = {
1505                let store = mgr.store.lock().unwrap();
1506                bootstrap::make_requests_for_documents(&mgr.runtime, &[q], &**store, &config)
1507                    .unwrap()
1508            };
1509            let expanded = mgr.expand_response_text(&r[0], "ABC".to_string());
1510            assert_eq!(&expanded.unwrap(), "ABC");
1511
1512            // Try a consensus response that doesn't look like a diff in
1513            // response to a query that doesn't ask for one.
1514            let latest_id = DocId::LatestConsensus {
1515                flavor: ConsensusFlavor::Microdesc,
1516                cache_usage: CacheUsage::CacheOkay,
1517            };
1518            let r = {
1519                let store = mgr.store.lock().unwrap();
1520                bootstrap::make_requests_for_documents(
1521                    &mgr.runtime,
1522                    &[latest_id],
1523                    &**store,
1524                    &config,
1525                )
1526                .unwrap()
1527            };
1528            let expanded = mgr.expand_response_text(&r[0], "DEF".to_string());
1529            assert_eq!(&expanded.unwrap(), "DEF");
1530
1531            // Now stick some metadata and a string into the storage so that
1532            // we can ask for a diff.
1533            {
1534                let mut store = mgr.store.lock().unwrap();
1535                let d_in = [0x99; 32]; // This one, we can fake.
1536                let cmeta = ConsensusMeta::new(
1537                    Lifetime::new(now, now + day, now + 2 * day).unwrap(),
1538                    d_in,
1539                    d_in,
1540                );
1541                store
1542                    .store_consensus(
1543                        &cmeta,
1544                        ConsensusFlavor::Microdesc,
1545                        false,
1546                        "line 1\nline2\nline 3\n",
1547                    )
1548                    .unwrap();
1549            }
1550
1551            // Try expanding something that isn't a consensus, even if we'd like
1552            // one.
1553            let r = {
1554                let store = mgr.store.lock().unwrap();
1555                bootstrap::make_requests_for_documents(
1556                    &mgr.runtime,
1557                    &[latest_id],
1558                    &**store,
1559                    &config,
1560                )
1561                .unwrap()
1562            };
1563            let expanded = mgr.expand_response_text(&r[0], "hello".to_string());
1564            assert_eq!(&expanded.unwrap(), "hello");
1565
1566            // Finally, try "expanding" a diff (by applying it and checking the digest.
1567            let diff = "network-status-diff-version 1
1568hash 9999999999999999999999999999999999999999999999999999999999999999 8382374ca766873eb0d2530643191c6eaa2c5e04afa554cbac349b5d0592d300
15692c
1570replacement line
1571.
1572".to_string();
1573            let expanded = mgr.expand_response_text(&r[0], diff);
1574
1575            assert_eq!(expanded.unwrap(), "line 1\nreplacement line\nline 3\n");
1576
1577            // If the digest is wrong, that should get rejected.
1578            let diff = "network-status-diff-version 1
1579hash 9999999999999999999999999999999999999999999999999999999999999999 9999999999999999999999999999999999999999999999999999999999999999
15802c
1581replacement line
1582.
1583".to_string();
1584            let expanded = mgr.expand_response_text(&r[0], diff);
1585            assert!(expanded.is_err());
1586        });
1587    }
1588}