fedimint_tor_dirmgr/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg, 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#![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)] // This can reasonably be done for explicitness
38#![allow(clippy::uninlined_format_args)]
39#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
40#![allow(clippy::result_large_err)] // temporary workaround for arti#587
41#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
42//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
43
44// This clippy lint produces a false positive on `use strum`, below.
45// Attempting to apply the lint to just the use statement fails to suppress
46// this lint and instead produces another lint about a useless clippy attribute.
47#![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
111/// Re-export of `strum` crate for use by an internal macro
112use strum;
113
114/// A Result as returned by this crate.
115pub type Result<T> = std::result::Result<T, Error>;
116
117/// Storage manager used by [`DirMgr`] and
118/// [`BridgeDescMgr`](bridgedesc::BridgeDescMgr)
119///
120/// Internally, this wraps up a sqlite database.
121///
122/// This is a handle, which is cheap to clone; clones share state.
123#[derive(Clone)]
124pub struct DirMgrStore<R: Runtime> {
125    /// The actual store
126    pub(crate) store: Arc<Mutex<crate::DynStore>>,
127
128    /// Be parameterized by Runtime even though we don't use it right now
129    pub(crate) runtime: PhantomData<R>,
130}
131
132impl<R: Runtime> DirMgrStore<R> {
133    /// Open the storage, according to the specified configuration
134    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/// Trait for DirMgr implementations
143#[async_trait]
144pub trait DirProvider: NetDirProvider {
145    /// Try to change our configuration to `new_config`.
146    ///
147    /// Actual behavior will depend on the value of `how`.
148    fn reconfigure(
149        &self,
150        new_config: &DirMgrConfig,
151        how: tor_config::Reconfigure,
152    ) -> std::result::Result<(), tor_config::ReconfigureError>;
153
154    /// Bootstrap a `DirProvider` that hasn't been bootstrapped yet.
155    async fn bootstrap(&self) -> Result<()>;
156
157    /// Return a stream of [`DirBootstrapStatus`] events to tell us about changes
158    /// in the latest directory's bootstrap status.
159    ///
160    /// Note that this stream can be lossy: the caller will not necessarily
161    /// observe every event on the stream
162    fn bootstrap_events(&self) -> BoxStream<'static, DirBootstrapStatus>;
163
164    /// Return a [`TaskHandle`] that can be used to manage the download process.
165    fn download_task_handle(&self) -> Option<TaskHandle> {
166        None
167    }
168}
169
170// NOTE(eta): We can't implement this for Arc<DirMgr<R>> due to trait coherence rules, so instead
171//            there's a blanket impl for Arc<T> in tor-netdir.
172impl<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            // We have a directory, so we'd like to give it out for its
202            // parameters.
203            //
204            // We do this even if the directory is expired, since parameters
205            // don't really expire on any plausible timescale.
206            netdir
207        } else {
208            // We have no directory, so we'll give out the default parameters as
209            // modified by the provided override_net_params configuration.
210            //
211            self.default_parameters
212                .lock()
213                .expect("Poisoned lock")
214                .clone()
215        }
216        // TODO(nickm): If we felt extremely clever, we could add a third case
217        // where, if we have a pending directory with a validated consensus, we
218        // give out that consensus's network parameters even if we _don't_ yet
219        // have a full directory.  That's significant refactoring, though, for
220        // an unclear amount of benefit.
221    }
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
247/// A directory manager to download, fetch, and cache a Tor directory.
248///
249/// A DirMgr can operate in three modes:
250///   * In **offline** mode, it only reads from the cache, and can
251///     only read once.
252///   * In **read-only** mode, it reads from the cache, but checks
253///     whether it can acquire an associated lock file.  If it can, then
254///     it enters read-write mode.  If not, it checks the cache
255///     periodically for new information.
256///   * In **read-write** mode, it knows that no other process will be
257///     writing to the cache, and it takes responsibility for fetching
258///     data from the network and updating the directory with new
259///     directory information.
260pub struct DirMgr<R: Runtime> {
261    /// Configuration information: where to find directories, how to
262    /// validate them, and so on.
263    config: tor_config::MutCfg<DirMgrConfig>,
264    /// Handle to our sqlite cache.
265    // TODO(nickm): I'd like to use an rwlock, but that's not feasible, since
266    // rusqlite::Connection isn't Sync.
267    // TODO is needed?
268    store: Arc<Mutex<DynStore>>,
269    /// Our latest sufficiently bootstrapped directory, if we have one.
270    ///
271    /// We use the RwLock so that we can give this out to a bunch of other
272    /// users, and replace it once a new directory is bootstrapped.
273    // TODO(eta): Eurgh! This is so many Arcs! (especially considering this
274    //            gets wrapped in an Arc)
275    netdir: Arc<SharedMutArc<NetDir>>,
276
277    /// A set of network parameters to hand out when we have no directory.
278    default_parameters: Mutex<Arc<NetParameters>>,
279
280    /// A publisher handle that we notify whenever the consensus changes.
281    events: event::FlagPublisher<DirEvent>,
282
283    /// A publisher handle that we notify whenever our bootstrapping status
284    /// changes.
285    send_status: Mutex<watch::Sender<event::DirBootstrapStatus>>,
286
287    /// A receiver handle that gets notified whenever our bootstrapping status
288    /// changes.
289    ///
290    /// We don't need to keep this drained, since `postage::watch` already knows
291    /// to discard unread events.
292    receive_status: DirBootstrapEvents,
293
294    /// A circuit manager, if this DirMgr supports downloading.
295    circmgr: Option<Arc<CircMgr<R>>>,
296
297    /// Our asynchronous runtime.
298    runtime: R,
299
300    /// Whether or not we're operating in offline mode.
301    offline: bool,
302
303    /// If we're not in offline mode, stores whether or not the `DirMgr` has attempted
304    /// to bootstrap yet or not.
305    ///
306    /// This exists in order to prevent starting two concurrent bootstrap tasks.
307    ///
308    /// (In offline mode, this does nothing.)
309    bootstrap_started: AtomicBool,
310
311    /// A filter that gets applied to directory objects before we use them.
312    #[cfg(feature = "dirfilter")]
313    filter: crate::filter::FilterConfig,
314
315    /// A task schedule that can be used if we're bootstrapping.  If this is
316    /// None, then there's currently a scheduled task in progress.
317    task_schedule: Mutex<Option<TaskSchedule<R>>>,
318
319    /// A task handle that we return to anybody who needs to manage our download process.
320    task_handle: TaskHandle,
321}
322
323/// The possible origins of a document.
324///
325/// Used (for example) to report where we got a document from if it fails to
326/// parse.
327#[derive(Debug, Clone)]
328#[non_exhaustive]
329pub enum DocSource {
330    /// We loaded the document from our cache.
331    LocalCache,
332    /// We fetched the document from a server.
333    DirServer {
334        /// Information about the server we fetched the document from.
335        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    /// Try to load the directory from disk, without launching any
351    /// kind of update process.
352    ///
353    /// This function runs in **offline** mode: it will give an error
354    /// if the result is not up-to-date, or not fully downloaded.
355    ///
356    /// In general, you shouldn't use this function in a long-running
357    /// program; it's only suitable for command-line or batch tools.
358    // TODO: I wish this function didn't have to be async or take a runtime.
359    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        // TODO: add some way to return a directory that isn't up-to-date
364        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    /// Return a current netdir, either loading it or bootstrapping it
376    /// as needed.
377    ///
378    /// Like load_once, but will try to bootstrap (or wait for another
379    /// process to bootstrap) if we don't have an up-to-date
380    /// bootstrapped directory.
381    ///
382    /// In general, you shouldn't use this function in a long-running
383    /// program; it's only suitable for command-line or batch tools.
384    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    /// Create a new `DirMgr` in online mode, but don't bootstrap it yet.
397    ///
398    /// The `DirMgr` can be bootstrapped later with `bootstrap`.
399    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    /// Bootstrap a `DirMgr` created in online mode that hasn't been bootstrapped yet.
415    ///
416    /// This function will not return until the directory is bootstrapped enough to build circuits.
417    /// It will also launch a background task that fetches any missing information, and that
418    /// replaces the directory when a new one is available.
419    ///
420    /// This function is intended to be used together with `create_unbootstrapped`. There is no
421    /// need to call this function otherwise.
422    ///
423    /// If bootstrapping has already successfully taken place, returns early with success.
424    ///
425    /// # Errors
426    ///
427    /// Returns an error if bootstrapping fails. If the error is [`Error::CantAdvanceState`],
428    /// it may be possible to successfully bootstrap later on by calling this function again.
429    ///
430    /// # Panics
431    ///
432    /// Panics if the `DirMgr` passed to this function was not created in online mode, such as
433    /// via `load_once`.
434    pub async fn bootstrap(self: &Arc<Self>) -> Result<()> {
435        if self.offline {
436            return Err(Error::OfflineMode);
437        }
438
439        // The semantics of this are "attempt to replace a 'false' value with 'true'.
440        // If the value in bootstrap_started was not 'false' when the attempt was made, returns
441        // `Err`; this means another bootstrap attempt is in progress or has completed, so we
442        // return early.
443
444        // NOTE(eta): could potentially weaken the `Ordering` here in future
445        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        // Use a RAII guard to reset `bootstrap_started` to `false` if we return early without
455        // completing bootstrap.
456        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        // Try to load from the cache.
472        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        // Whether we loaded or not, we now start downloading.
486        let dirmgr_weak = Arc::downgrade(self);
487        self.runtime
488            .spawn(async move {
489                // Use an RAII guard to make sure that when this task exits, the
490                // TaskSchedule object is put back.
491                //
492                // TODO(nick): Putting the schedule back isn't actually useful
493                // if the task exits _after_ we've bootstrapped for the first
494                // time, because of how bootstrap_started works.
495                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                // Don't warn when these are Error::ManagerDropped: that
502                // means that the DirMgr has been shut down.
503                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                    // Disarm the RAII guard, since we succeeded.  Now bootstrap_started will remain true.
528                    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    /// Returns `true` if a bootstrap attempt is in progress, or successfully completed.
540    pub fn bootstrap_started(&self) -> bool {
541        self.bootstrap_started.load(Ordering::SeqCst)
542    }
543
544    /// Return a new directory manager from a given configuration,
545    /// bootstrapping from the network as necessary.
546    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    /// Try forever to either lock the storage (and thereby become the
560    /// owner), or to reload the database.
561    ///
562    /// If we have begin to have a bootstrapped directory, send a
563    /// message using `on_complete`.
564    ///
565    /// If we eventually become the owner, return Ok().
566    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                    // We now own the lock!  (Maybe we owned it before; the
585                    // upgrade_to_readwrite() function is idempotent.)  We can
586                    // do our own bootstrapping.
587                    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            // We don't own the lock.  Somebody else owns the cache.  They
604            // should be updating it.  Wait a bit, then try again.
605            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            // TODO: instead of loading the whole thing we should have a
612            // database entry that says when the last update was, or use
613            // our state functions.
614            {
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                    // Successfully loaded a bootstrapped directory.
619                    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    /// Try to fetch our directory info and keep it updated, indefinitely.
632    ///
633    /// If we have begin to have a bootstrapped directory, send a
634    /// message using `on_complete`.
635    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                // TODO(nickm): instead of getting this every time we loop, it
664                // might be a good idea to refresh it with each attempt, at
665                // least at the point of checking the number of attempts.
666                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                // we ran out of attempts.
721                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                // Report success, if appropriate.
728                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    /// Get a reference to the circuit manager, if we have one.
748    fn circmgr(&self) -> Result<Arc<CircMgr<R>>> {
749        self.circmgr.clone().ok_or(Error::NoDownloadSupport)
750    }
751
752    /// Try to change our configuration to `new_config`.
753    ///
754    /// Actual behavior will depend on the value of `how`.
755    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        // We don't support changing these: doing so basically would require us
762        // to abort all our in-progress downloads, since they might be based on
763        // no-longer-viable information.
764        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            // (It's okay to ignore the error, since it just means that there
791            // was no current netdir.)
792            self.events.publish(DirEvent::NewConsensus);
793        }
794
795        Ok(())
796    }
797
798    /// Return a stream of [`DirBootstrapStatus`] events to tell us about changes
799    /// in the latest directory's bootstrap status.
800    ///
801    /// Note that this stream can be lossy: the caller will not necessarily
802    /// observe every event on the stream
803    pub fn bootstrap_events(&self) -> event::DirBootstrapEvents {
804        self.receive_status.clone()
805    }
806
807    /// Replace the latest status with `progress` and broadcast to anybody
808    /// watching via a [`DirBootstrapEvents`] stream.
809    fn update_progress(&self, attempt_id: AttemptId, progress: DirProgress) {
810        // TODO(nickm): can I kill off this lock by having something else own the sender?
811        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    /// Update our status tracker to note that some number of errors has
818    /// occurred.
819    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    /// Update our status tracker to note that we've needed to reset our download attempt.
830    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    /// Try to make this a directory manager with read-write access to its
838    /// storage.
839    ///
840    /// Return true if we got the lock, or if we already had it.
841    ///
842    /// Return false if another process has the lock
843    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    /// Return a reference to the store, if it is currently read-write.
851    #[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        // A race-condition is possible here, but I believe it's harmless.
859        if rw {
860            Some(&self.store)
861        } else {
862            None
863        }
864    }
865
866    /// Construct a DirMgr from a DirMgrConfig.
867    ///
868    /// If `offline` is set, opens the SQLite store read-only and sets the offline flag in the
869    /// returned manager.
870    #[allow(clippy::unnecessary_wraps)] // API compat and future-proofing
871    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        // We create these early so the client code can access task_handle before bootstrap() returns.
892        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    /// Load the latest non-pending non-expired directory from the
915    /// cache, if it is newer than the one we have.
916    ///
917    /// Return false if there is no such consensus.
918    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    /// Return a new asynchronous stream that will receive notification
935    /// whenever the consensus has changed.
936    ///
937    /// Multiple events may be batched up into a single item: each time
938    /// this stream yields an event, all you can assume is that the event has
939    /// occurred at least once.
940    pub fn events(&self) -> impl futures::Stream<Item = DirEvent> {
941        self.events.subscribe()
942    }
943
944    /// Try to load the text of a single document described by `doc` from
945    /// storage.
946    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    /// Load the text for a collection of documents.
968    ///
969    /// If many of the documents have the same type, this can be more
970    /// efficient than calling [`text`](Self::text).
971    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    /// Given a request we sent and the response we got from a
985    /// directory server, see whether we should expand that response
986    /// into "something larger".
987    ///
988    /// Currently, this handles expanding consensus diffs, and nothing
989    /// else.  We do it at this stage of our downloading operation
990    /// because it requires access to the store.
991    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    /// If `state` has netdir changes to apply, apply them to our netdir.
1019    #[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                    // Check the new netdir is sufficient, if we have a circmgr.
1032                    // (Unwraps are fine because the `Option` is `Some` until we take it.)
1033                    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                        // Done inside a block to not hold a long-lived copy of the NetDir.
1043                        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                        // Now that a consensus is usable, older consensuses may
1070                        // need to expire.
1071                        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/// A degree of readiness for a given directory state object.
1093#[derive(Debug, Copy, Clone)]
1094enum Readiness {
1095    /// There is no more information to download.
1096    Complete,
1097    /// There is more information to download, but we don't need to
1098    Usable,
1099}
1100
1101/// Try to upgrade a weak reference to a DirMgr, and give an error on
1102/// failure.
1103fn upgrade_weak_ref<T>(weak: &Weak<T>) -> Result<Arc<T>> {
1104    Weak::upgrade(weak).ok_or(Error::ManagerDropped)
1105}
1106
1107/// Given a time `now`, and an amount of tolerated clock skew `tolerance`,
1108/// return the age of the oldest consensus that we should request at that time.
1109pub(crate) fn default_consensus_cutoff(
1110    now: SystemTime,
1111    tolerance: &DirTolerance,
1112) -> Result<SystemTime> {
1113    /// We _always_ allow at least this much age in our consensuses, to account
1114    /// for the fact that consensuses have some lifetime.
1115    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    // We now round cutoff to the next hour, so that we aren't leaking our exact
1119    // time to the directory cache.
1120    //
1121    // With the time crate, it's easier to calculate the "next hour" by rounding
1122    // _down_ then adding an hour; rounding up would sometimes require changing
1123    // the date too.
1124    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    // @@ begin test lint list maintained by maint/add_warning @@
1137    #![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    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
1149    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            // Seed the storage with a bunch of junk.
1190            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            // Try to get it with text().
1247            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            // Now try texts()
1263            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            // Try with an empty store.
1306            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            // Add a fake consensus record.
1328            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            // Now try again.
1343            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            // Try an authcert.
1382            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            // Try a bunch of mds.
1396            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            // Try a bunch of rds.
1403            #[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            // Try a simple request: nothing should happen.
1428            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            // Try a consensus response that doesn't look like a diff in
1438            // response to a query that doesn't ask for one.
1439            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            // Now stick some metadata and a string into the storage so that
1457            // we can ask for a diff.
1458            {
1459                let mut store = mgr.store.lock().unwrap();
1460                let d_in = [0x99; 32]; // This one, we can fake.
1461                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            // Try expanding something that isn't a consensus, even if we'd like
1477            // one.
1478            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            // Finally, try "expanding" a diff (by applying it and checking the digest.
1492            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            // If the digest is wrong, that should get rejected.
1503            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}