1use std::collections::{HashMap, HashSet};
14use std::fmt::Debug;
15use std::mem;
16use std::sync::{Arc, Mutex};
17use std::time::{Duration, SystemTime};
18use time::OffsetDateTime;
19use tor_basic_utils::RngExt as _;
20use tor_dircommon::retry::DownloadSchedule;
21use tor_error::{internal, warn_report};
22use tor_netdir::{MdReceiver, NetDir, PartialNetDir};
23use tor_netdoc::doc::authcert::UncheckedAuthCert;
24use tor_netdoc::doc::netstatus::{Lifetime, ProtoStatuses};
25use tracing::{debug, warn};
26
27use crate::event::DirProgress;
28
29use crate::storage::DynStore;
30use crate::{
31 CacheUsage, ClientRequest, DirMgrConfig, DocId, DocumentText, Error, Readiness, Result,
32 docmeta::{AuthCertMeta, ConsensusMeta},
33 event,
34};
35use crate::{DocSource, SharedMutArc};
36use tor_checkable::{ExternallySigned, SelfSigned, Timebound};
37#[cfg(feature = "geoip")]
38use tor_geoip::GeoipDb;
39use tor_llcrypto::pk::rsa::RsaIdentity;
40use tor_netdoc::doc::{
41 microdesc::{MdDigest, MicrodescAndHash},
42 netstatus::MdConsensus,
43};
44use tor_netdoc::{
45 AllowAnnotations,
46 doc::{
47 authcert::{AuthCert, AuthCertKeyIds},
48 microdesc::MicrodescReader,
49 netstatus::{ConsensusFlavor, UnvalidatedMdConsensus},
50 },
51};
52use tor_rtcompat::Runtime;
53
54#[derive(Debug)]
56pub(crate) enum NetDirChange<'a> {
57 AttemptReplace {
62 netdir: &'a mut Option<NetDir>,
67 consensus_meta: &'a ConsensusMeta,
69 },
70 AddMicrodescs(&'a mut Vec<MicrodescAndHash>),
72 SetRequiredProtocol {
74 timestamp: SystemTime,
76 protos: Arc<ProtoStatuses>,
78 },
79}
80
81pub(crate) trait DirState: Send {
96 fn describe(&self) -> String;
98 fn missing_docs(&self) -> Vec<DocId>;
107 fn is_ready(&self, ready: Readiness) -> bool;
109 fn get_netdir_change(&mut self) -> Option<NetDirChange<'_>> {
112 None
113 }
114 fn can_advance(&self) -> bool;
117 fn add_from_cache(
125 &mut self,
126 docs: HashMap<DocId, DocumentText>,
127 changed: &mut bool,
128 ) -> Result<()>;
129
130 fn add_from_download(
143 &mut self,
144 text: &str,
145 request: &ClientRequest,
146 source: DocSource,
147 storage: Option<&Mutex<DynStore>>,
148 changed: &mut bool,
149 ) -> Result<()>;
150 fn bootstrap_progress(&self) -> event::DirProgress;
152 fn dl_config(&self) -> DownloadSchedule;
154 fn advance(self: Box<Self>) -> Box<dyn DirState>;
156 fn reset_time(&self) -> Option<SystemTime>;
159 fn reset(self: Box<Self>) -> Box<dyn DirState>;
161}
162
163pub(crate) trait PreviousNetDir: Send + Sync + 'static + Debug {
165 fn get_netdir(&self) -> Option<Arc<NetDir>>;
167}
168
169impl PreviousNetDir for SharedMutArc<NetDir> {
170 fn get_netdir(&self) -> Option<Arc<NetDir>> {
171 self.get()
172 }
173}
174
175#[derive(Clone, Debug)]
177pub(crate) struct GetConsensusState<R: Runtime> {
178 cache_usage: CacheUsage,
180
181 after: Option<SystemTime>,
188
189 next: Option<GetCertsState<R>>,
193
194 authority_ids: Vec<RsaIdentity>,
199
200 rt: R,
202 config: Arc<DirMgrConfig>,
205 prev_netdir: Option<Arc<dyn PreviousNetDir>>,
207
208 #[cfg(feature = "dirfilter")]
210 filter: Arc<dyn crate::filter::DirFilter>,
211}
212
213impl<R: Runtime> GetConsensusState<R> {
214 pub(crate) fn new(
218 rt: R,
219 config: Arc<DirMgrConfig>,
220 cache_usage: CacheUsage,
221 prev_netdir: Option<Arc<dyn PreviousNetDir>>,
222 #[cfg(feature = "dirfilter")] filter: Arc<dyn crate::filter::DirFilter>,
223 ) -> Self {
224 let authority_ids = config.authorities().v3idents().clone();
225 let after = prev_netdir
226 .as_ref()
227 .and_then(|x| x.get_netdir())
228 .map(|nd| nd.lifetime().valid_after());
229
230 GetConsensusState {
231 cache_usage,
232 after,
233 next: None,
234 authority_ids,
235 rt,
236 config,
237 prev_netdir,
238 #[cfg(feature = "dirfilter")]
239 filter,
240 }
241 }
242}
243
244impl<R: Runtime> DirState for GetConsensusState<R> {
245 fn describe(&self) -> String {
246 if self.next.is_some() {
247 "About to fetch certificates."
248 } else {
249 match self.cache_usage {
250 CacheUsage::CacheOnly => "Looking for a cached consensus.",
251 CacheUsage::CacheOkay => "Looking for a consensus.",
252 CacheUsage::MustDownload => "Downloading a consensus.",
253 }
254 }
255 .to_string()
256 }
257 fn missing_docs(&self) -> Vec<DocId> {
258 if self.can_advance() {
259 return Vec::new();
260 }
261 let flavor = ConsensusFlavor::Microdesc;
262 vec![DocId::LatestConsensus {
263 flavor,
264 cache_usage: self.cache_usage,
265 }]
266 }
267 fn is_ready(&self, _ready: Readiness) -> bool {
268 false
269 }
270 fn can_advance(&self) -> bool {
271 self.next.is_some()
272 }
273 fn bootstrap_progress(&self) -> DirProgress {
274 if let Some(next) = &self.next {
275 next.bootstrap_progress()
276 } else {
277 DirProgress::NoConsensus { after: self.after }
278 }
279 }
280 fn dl_config(&self) -> DownloadSchedule {
281 self.config.schedule.retry_consensus()
282 }
283 fn add_from_cache(
284 &mut self,
285 docs: HashMap<DocId, DocumentText>,
286 changed: &mut bool,
287 ) -> Result<()> {
288 let text = match docs.into_iter().next() {
289 None => return Ok(()),
290 Some((
291 DocId::LatestConsensus {
292 flavor: ConsensusFlavor::Microdesc,
293 ..
294 },
295 text,
296 )) => text,
297 _ => return Err(Error::CacheCorruption("Not an md consensus")),
298 };
299
300 let source = DocSource::LocalCache;
301
302 self.add_consensus_text(
303 source,
304 text.as_str().map_err(Error::BadUtf8InCache)?,
305 None,
306 changed,
307 )?;
308 Ok(())
309 }
310 fn add_from_download(
311 &mut self,
312 text: &str,
313 request: &ClientRequest,
314 source: DocSource,
315 storage: Option<&Mutex<DynStore>>,
316 changed: &mut bool,
317 ) -> Result<()> {
318 let requested_newer_than = match request {
319 ClientRequest::Consensus(r) => r.last_consensus_date(),
320 _ => None,
321 };
322 let meta = self.add_consensus_text(source, text, requested_newer_than, changed)?;
323
324 if let Some(store) = storage {
325 let mut w = store.lock().expect("Directory storage lock poisoned");
326 w.store_consensus(meta, ConsensusFlavor::Microdesc, true, text)?;
327 }
328 Ok(())
329 }
330 fn advance(self: Box<Self>) -> Box<dyn DirState> {
331 match self.next {
332 Some(next) => Box::new(next),
333 None => self,
334 }
335 }
336 fn reset_time(&self) -> Option<SystemTime> {
337 None
338 }
339 fn reset(self: Box<Self>) -> Box<dyn DirState> {
340 self
341 }
342}
343
344impl<R: Runtime> GetConsensusState<R> {
345 fn add_consensus_text(
354 &mut self,
355 source: DocSource,
356 text: &str,
357 cutoff: Option<SystemTime>,
358 changed: &mut bool,
359 ) -> Result<&ConsensusMeta> {
360 let (consensus_meta, unvalidated) = {
362 let (signedval, remainder, parsed) =
363 MdConsensus::parse(text).map_err(|e| Error::from_netdoc(source.clone(), e))?;
364 #[cfg(feature = "dirfilter")]
365 let parsed = self.filter.filter_consensus(parsed)?;
366 let parsed = self.config.tolerance.extend_tolerance(parsed);
367 let now = self.rt.wallclock();
368 let timely = parsed.check_valid_at(&now)?;
369 if let Some(cutoff) = cutoff {
370 if timely.peek_lifetime().valid_after() < cutoff {
371 return Err(Error::Unwanted("consensus was older than requested"));
372 }
373 }
374 let meta = ConsensusMeta::from_unvalidated(signedval, remainder, &timely);
375 (meta, timely)
376 };
377
378 let unvalidated = unvalidated.set_n_authorities(self.authority_ids.len());
381
382 let id_refs: Vec<_> = self.authority_ids.iter().collect();
383 if !unvalidated.authorities_are_correct(&id_refs[..]) {
384 return Err(Error::UnrecognizedAuthorities);
385 }
386 *changed = true;
388
389 let desired_certs = unvalidated
393 .signing_cert_ids()
394 .filter(|m| self.recognizes_authority(&m.id_fingerprint))
395 .collect();
396
397 self.next = Some(GetCertsState {
398 cache_usage: self.cache_usage,
399 consensus_source: source,
400 consensus: GetCertsConsensus::Unvalidated(unvalidated),
401 consensus_meta,
402 missing_certs: desired_certs,
403 certs: Vec::new(),
404 rt: self.rt.clone(),
405 config: self.config.clone(),
406 prev_netdir: self.prev_netdir.take(),
407 protocol_statuses: None,
408 #[cfg(feature = "dirfilter")]
409 filter: self.filter.clone(),
410 });
411
412 #[allow(clippy::unwrap_used)]
414 Ok(&self.next.as_ref().unwrap().consensus_meta)
415 }
416
417 fn recognizes_authority(&self, id: &RsaIdentity) -> bool {
419 self.authority_ids.iter().any(|auth| auth == id)
420 }
421}
422
423#[derive(Clone, Debug)]
427enum GetCertsConsensus {
428 Unvalidated(UnvalidatedMdConsensus),
430 Validated(MdConsensus),
432 Failed,
434}
435
436#[derive(Clone, Debug)]
445struct GetCertsState<R: Runtime> {
446 cache_usage: CacheUsage,
448 consensus_source: DocSource,
450 consensus: GetCertsConsensus,
453 consensus_meta: ConsensusMeta,
455 missing_certs: HashSet<AuthCertKeyIds>,
458 certs: Vec<AuthCert>,
460
461 rt: R,
463 config: Arc<DirMgrConfig>,
466 prev_netdir: Option<Arc<dyn PreviousNetDir>>,
468
469 protocol_statuses: Option<(SystemTime, Arc<ProtoStatuses>)>,
471
472 #[cfg(feature = "dirfilter")]
474 filter: Arc<dyn crate::filter::DirFilter>,
475}
476
477impl<R: Runtime> GetCertsState<R> {
478 fn check_parsed_certificate<'s>(
484 &self,
485 parsed: tor_netdoc::Result<UncheckedAuthCert>,
486 source: &DocSource,
487 within: &'s str,
488 ) -> Result<(AuthCert, &'s str)> {
489 let parsed = parsed.map_err(|e| Error::from_netdoc(source.clone(), e))?;
490 let cert_text = parsed
491 .within(within)
492 .expect("Certificate was not in input as expected");
493 let wellsigned = parsed.check_signature()?;
494 let now = self.rt.wallclock();
495 let timely_cert = self
496 .config
497 .tolerance
498 .extend_tolerance(wellsigned)
499 .check_valid_at(&now)?;
500 Ok((timely_cert, cert_text))
501 }
502
503 fn try_checking_sigs(&mut self) -> Result<()> {
511 use GetCertsConsensus as C;
512 let mut consensus = C::Failed;
515 std::mem::swap(&mut consensus, &mut self.consensus);
516
517 let unvalidated = match consensus {
518 C::Unvalidated(uv) if uv.key_is_correct(&self.certs[..]).is_ok() => uv,
519 _ => {
520 self.consensus = consensus;
522 return Ok(());
523 }
524 };
525
526 let (new_consensus, outcome) = match unvalidated.check_signature(&self.certs[..]) {
527 Ok(validated) => (C::Validated(validated), Ok(())),
528 Err(cause) => (
529 C::Failed,
530 Err(Error::ConsensusInvalid {
531 source: self.consensus_source.clone(),
532 cause,
533 }),
534 ),
535 };
536 self.consensus = new_consensus;
537
538 if let GetCertsConsensus::Validated(v) = &self.consensus {
541 if self.protocol_statuses.is_none() {
542 let protoset: &Arc<ProtoStatuses> = v.protocol_statuses();
543 self.protocol_statuses = Some((
544 self.consensus_meta.lifetime().valid_after(),
545 Arc::clone(protoset),
546 ));
547 }
548 }
549
550 outcome
551 }
552}
553
554impl<R: Runtime> DirState for GetCertsState<R> {
555 fn describe(&self) -> String {
556 use GetCertsConsensus as C;
557 match &self.consensus {
558 C::Unvalidated(_) => {
559 let total = self.certs.len() + self.missing_certs.len();
560 format!(
561 "Downloading certificates for consensus (we are missing {}/{}).",
562 self.missing_certs.len(),
563 total
564 )
565 }
566 C::Validated(_) => "Validated consensus; about to get microdescriptors".to_string(),
567 C::Failed => "Failed to validate consensus".to_string(),
568 }
569 }
570 fn missing_docs(&self) -> Vec<DocId> {
571 self.missing_certs
572 .iter()
573 .map(|id| DocId::AuthCert(*id))
574 .collect()
575 }
576 fn is_ready(&self, _ready: Readiness) -> bool {
577 false
578 }
579 fn can_advance(&self) -> bool {
580 matches!(self.consensus, GetCertsConsensus::Validated(_))
581 }
582 fn bootstrap_progress(&self) -> DirProgress {
583 let n_certs = self.certs.len();
584 let n_missing_certs = self.missing_certs.len();
585 let total_certs = n_missing_certs + n_certs;
586 DirProgress::FetchingCerts {
587 lifetime: self.consensus_meta.lifetime().clone(),
588 usable_lifetime: self
589 .config
590 .tolerance
591 .extend_lifetime(self.consensus_meta.lifetime()),
592
593 n_certs: (n_certs as u16, total_certs as u16),
594 }
595 }
596 fn dl_config(&self) -> DownloadSchedule {
597 self.config.schedule.retry_certs()
598 }
599 fn add_from_cache(
600 &mut self,
601 docs: HashMap<DocId, DocumentText>,
602 changed: &mut bool,
603 ) -> Result<()> {
604 let source = DocSource::LocalCache;
607 let mut nonfatal_error = None;
608 for id in &self.missing_docs() {
609 if let Some(cert) = docs.get(id) {
610 let text = cert.as_str().map_err(Error::BadUtf8InCache)?;
611 let parsed = AuthCert::parse(text);
612 match self.check_parsed_certificate(parsed, &source, text) {
613 Ok((cert, _text)) => {
614 self.missing_certs.remove(&cert.key_ids());
615 self.certs.push(cert);
616 *changed = true;
617 }
618 Err(e) => {
619 nonfatal_error.get_or_insert(e);
620 }
621 }
622 }
623 }
624 if *changed {
625 self.try_checking_sigs()?;
626 }
627 opt_err_to_result(nonfatal_error)
628 }
629 fn add_from_download(
630 &mut self,
631 text: &str,
632 request: &ClientRequest,
633 source: DocSource,
634 storage: Option<&Mutex<DynStore>>,
635 changed: &mut bool,
636 ) -> Result<()> {
637 let asked_for: HashSet<_> = match request {
638 ClientRequest::AuthCert(a) => a.keys().collect(),
639 _ => return Err(internal!("expected an AuthCert request").into()),
640 };
641
642 let mut nonfatal_error = None;
643 let mut newcerts = Vec::new();
644 for cert in
645 AuthCert::parse_multiple(text).map_err(|e| Error::from_netdoc(source.clone(), e))?
646 {
647 match self.check_parsed_certificate(cert, &source, text) {
648 Ok((cert, cert_text)) => {
649 newcerts.push((cert, cert_text));
650 }
651 Err(e) => {
652 warn_report!(e, "Problem with certificate received from {}", &source);
653 nonfatal_error.get_or_insert(e);
654 }
655 }
656 }
657
658 let len_orig = newcerts.len();
660 newcerts.retain(|(cert, _)| asked_for.contains(&cert.key_ids()));
661 if newcerts.len() != len_orig {
662 warn!(
663 "Discarding certificates from {} that we didn't ask for.",
664 source
665 );
666 nonfatal_error.get_or_insert(Error::Unwanted("Certificate we didn't request"));
667 }
668
669 if newcerts.is_empty() {
671 return opt_err_to_result(nonfatal_error);
672 }
673
674 if let Some(store) = storage {
675 let v: Vec<_> = newcerts[..]
677 .iter()
678 .map(|(cert, s)| (AuthCertMeta::from_authcert(cert), *s))
679 .collect();
680 let mut w = store.lock().expect("Directory storage lock poisoned");
681 w.store_authcerts(&v[..])?;
682 }
683
684 for (cert, _) in newcerts {
687 let ids = cert.key_ids();
688 if self.missing_certs.contains(&ids) {
689 self.missing_certs.remove(&ids);
690 self.certs.push(cert);
691 *changed = true;
692 }
693 }
694
695 if *changed {
696 self.try_checking_sigs()?;
697 }
698 opt_err_to_result(nonfatal_error)
699 }
700
701 fn advance(self: Box<Self>) -> Box<dyn DirState> {
702 use GetCertsConsensus::*;
703 match self.consensus {
704 Validated(validated) => Box::new(GetMicrodescsState::new(
705 self.cache_usage,
706 validated,
707 self.consensus_meta,
708 self.rt,
709 self.config,
710 self.prev_netdir,
711 #[cfg(feature = "dirfilter")]
712 self.filter,
713 )),
714 _ => self,
715 }
716 }
717
718 fn get_netdir_change(&mut self) -> Option<NetDirChange<'_>> {
719 self.protocol_statuses.as_ref().map(|(timestamp, protos)| {
720 NetDirChange::SetRequiredProtocol {
721 timestamp: *timestamp,
722 protos: Arc::clone(protos),
723 }
724 })
725 }
726
727 fn reset_time(&self) -> Option<SystemTime> {
728 Some(
729 self.consensus_meta.lifetime().valid_until()
730 + self.config.tolerance.post_valid_tolerance(),
731 )
732 }
733 fn reset(self: Box<Self>) -> Box<dyn DirState> {
734 let cache_usage = if self.cache_usage == CacheUsage::CacheOnly {
735 CacheUsage::CacheOnly
737 } else {
738 CacheUsage::MustDownload
743 };
744
745 Box::new(GetConsensusState::new(
746 self.rt,
747 self.config,
748 cache_usage,
749 self.prev_netdir,
750 #[cfg(feature = "dirfilter")]
751 self.filter,
752 ))
753 }
754}
755
756#[derive(Debug, Clone)]
758struct GetMicrodescsState<R: Runtime> {
759 cache_usage: CacheUsage,
761 n_microdescs: usize,
763 partial: PendingNetDir,
765 meta: ConsensusMeta,
767 newly_listed: Vec<MdDigest>,
770 reset_time: SystemTime,
774
775 rt: R,
777 config: Arc<DirMgrConfig>,
780 prev_netdir: Option<Arc<dyn PreviousNetDir>>,
782
783 #[cfg(feature = "dirfilter")]
785 filter: Arc<dyn crate::filter::DirFilter>,
786}
787
788#[derive(Debug, Clone)]
791enum PendingNetDir {
792 Partial(PartialNetDir),
794 Yielding {
800 netdir: Option<NetDir>,
803 collected_microdescs: Vec<MicrodescAndHash>,
805 missing_microdescs: HashSet<MdDigest>,
812 replace_dir_time: SystemTime,
815 },
816 Dummy,
818}
819
820impl MdReceiver for PendingNetDir {
821 fn missing_microdescs(&self) -> Box<dyn Iterator<Item = &MdDigest> + '_> {
822 match self {
823 PendingNetDir::Partial(partial) => partial.missing_microdescs(),
824 PendingNetDir::Yielding {
825 netdir,
826 missing_microdescs,
827 ..
828 } => {
829 if let Some(nd) = netdir.as_ref() {
830 nd.missing_microdescs()
831 } else {
832 Box::new(missing_microdescs.iter())
833 }
834 }
835 PendingNetDir::Dummy => unreachable!(),
836 }
837 }
838
839 fn add_microdesc(&mut self, md: MicrodescAndHash) -> bool {
840 match self {
841 PendingNetDir::Partial(partial) => partial.add_microdesc(md),
842 PendingNetDir::Yielding {
843 netdir,
844 missing_microdescs,
845 collected_microdescs,
846 ..
847 } => {
848 let wanted = missing_microdescs.remove(md.digest());
849 if let Some(nd) = netdir.as_mut() {
850 let nd_wanted = nd.add_microdesc(md);
851 debug_assert_eq!(wanted, nd_wanted);
853 nd_wanted
854 } else {
855 collected_microdescs.push(md);
856 wanted
857 }
858 }
859 PendingNetDir::Dummy => unreachable!(),
860 }
861 }
862
863 fn n_missing(&self) -> usize {
864 match self {
865 PendingNetDir::Partial(partial) => partial.n_missing(),
866 PendingNetDir::Yielding {
867 netdir,
868 missing_microdescs,
869 ..
870 } => {
871 if let Some(nd) = netdir.as_ref() {
872 debug_assert_eq!(nd.n_missing(), missing_microdescs.len());
874 nd.n_missing()
875 } else {
876 missing_microdescs.len()
877 }
878 }
879 PendingNetDir::Dummy => unreachable!(),
880 }
881 }
882}
883
884impl PendingNetDir {
885 fn upgrade_if_necessary(&mut self) {
887 if matches!(self, PendingNetDir::Partial(..)) {
888 match mem::replace(self, PendingNetDir::Dummy) {
889 PendingNetDir::Partial(p) => match p.unwrap_if_sufficient() {
890 Ok(nd) => {
891 let missing: HashSet<_> = nd.missing_microdescs().copied().collect();
892 let replace_dir_time = pick_download_time(nd.lifetime());
893 debug!(
894 "Consensus now usable, with {} microdescriptors missing. \
895 The current consensus is fresh until {}, and valid until {}. \
896 I've picked {} as the earliest time to replace it.",
897 missing.len(),
898 OffsetDateTime::from(nd.lifetime().fresh_until()),
899 OffsetDateTime::from(nd.lifetime().valid_until()),
900 OffsetDateTime::from(replace_dir_time)
901 );
902 *self = PendingNetDir::Yielding {
903 netdir: Some(nd),
904 collected_microdescs: vec![],
905 missing_microdescs: missing,
906 replace_dir_time,
907 };
908 }
909 Err(p) => {
910 *self = PendingNetDir::Partial(p);
911 }
912 },
913 _ => unreachable!(),
914 }
915 }
916 assert!(!matches!(self, PendingNetDir::Dummy));
917 }
918}
919
920impl<R: Runtime> GetMicrodescsState<R> {
921 fn new(
924 cache_usage: CacheUsage,
925 consensus: MdConsensus,
926 meta: ConsensusMeta,
927 rt: R,
928 config: Arc<DirMgrConfig>,
929 prev_netdir: Option<Arc<dyn PreviousNetDir>>,
930 #[cfg(feature = "dirfilter")] filter: Arc<dyn crate::filter::DirFilter>,
931 ) -> Self {
932 let reset_time =
933 consensus.lifetime().valid_until() + config.tolerance.post_valid_tolerance();
934 let n_microdescs = consensus.relays().len();
935
936 let params = &config.override_net_params;
937 #[cfg(not(feature = "geoip"))]
938 let mut partial_dir = PartialNetDir::new(consensus, Some(params));
939 #[cfg(feature = "geoip")]
941 let mut partial_dir =
942 PartialNetDir::new_with_geoip(consensus, Some(params), &GeoipDb::new_embedded());
943
944 if let Some(old_dir) = prev_netdir.as_ref().and_then(|x| x.get_netdir()) {
945 partial_dir.fill_from_previous_netdir(old_dir);
946 }
947
948 let mut partial = PendingNetDir::Partial(partial_dir);
951 partial.upgrade_if_necessary();
952
953 GetMicrodescsState {
954 cache_usage,
955 n_microdescs,
956 partial,
957 meta,
958 newly_listed: Vec::new(),
959 reset_time,
960 rt,
961 config,
962 prev_netdir,
963
964 #[cfg(feature = "dirfilter")]
965 filter,
966 }
967 }
968
969 fn register_microdescs<I>(&mut self, mds: I, _source: &DocSource, changed: &mut bool)
971 where
972 I: IntoIterator<Item = MicrodescAndHash>,
973 {
974 #[cfg(feature = "dirfilter")]
975 let mds: Vec<MicrodescAndHash> = mds
976 .into_iter()
977 .filter_map(|m| self.filter.filter_md(m).ok())
978 .collect();
979 let is_partial = matches!(self.partial, PendingNetDir::Partial(..));
980 for md in mds {
981 if is_partial {
982 self.newly_listed.push(*md.digest());
983 }
984 self.partial.add_microdesc(md);
985 *changed = true;
986 }
987 self.partial.upgrade_if_necessary();
988 }
989}
990
991impl<R: Runtime> DirState for GetMicrodescsState<R> {
992 fn describe(&self) -> String {
993 format!(
994 "Downloading microdescriptors (we are missing {}).",
995 self.partial.n_missing()
996 )
997 }
998 fn missing_docs(&self) -> Vec<DocId> {
999 self.partial
1000 .missing_microdescs()
1001 .map(|d| DocId::Microdesc(*d))
1002 .collect()
1003 }
1004 fn get_netdir_change(&mut self) -> Option<NetDirChange<'_>> {
1005 match self.partial {
1006 PendingNetDir::Yielding {
1007 ref mut netdir,
1008 ref mut collected_microdescs,
1009 ..
1010 } => {
1011 if netdir.is_some() {
1012 Some(NetDirChange::AttemptReplace {
1013 netdir,
1014 consensus_meta: &self.meta,
1015 })
1016 } else {
1017 collected_microdescs
1018 .is_empty()
1019 .then_some(NetDirChange::AddMicrodescs(collected_microdescs))
1020 }
1021 }
1022 _ => None,
1023 }
1024 }
1025 fn is_ready(&self, ready: Readiness) -> bool {
1026 match ready {
1027 Readiness::Complete => self.partial.n_missing() == 0,
1028 Readiness::Usable => {
1029 matches!(self.partial, PendingNetDir::Yielding { ref netdir, .. } if netdir.is_none())
1032 }
1033 }
1034 }
1035 fn can_advance(&self) -> bool {
1036 false
1037 }
1038 fn bootstrap_progress(&self) -> DirProgress {
1039 let n_present = self.n_microdescs - self.partial.n_missing();
1040 DirProgress::Validated {
1041 lifetime: self.meta.lifetime().clone(),
1042 usable_lifetime: self.config.tolerance.extend_lifetime(self.meta.lifetime()),
1043 n_mds: (n_present as u32, self.n_microdescs as u32),
1044 usable: self.is_ready(Readiness::Usable),
1045 }
1046 }
1047 fn dl_config(&self) -> DownloadSchedule {
1048 self.config.schedule.retry_microdescs()
1049 }
1050 fn add_from_cache(
1051 &mut self,
1052 docs: HashMap<DocId, DocumentText>,
1053 changed: &mut bool,
1054 ) -> Result<()> {
1055 let mut microdescs = Vec::new();
1056 for (id, text) in docs {
1057 if let DocId::Microdesc(digest) = id {
1058 if let Ok(md) =
1059 MicrodescAndHash::parse(text.as_str().map_err(Error::BadUtf8InCache)?)
1060 {
1061 if md.digest() == &digest {
1062 microdescs.push(md);
1063 continue;
1064 }
1065 }
1066 warn!("Found a mismatched microdescriptor in cache; ignoring");
1067 }
1068 }
1069
1070 self.register_microdescs(microdescs, &DocSource::LocalCache, changed);
1071 Ok(())
1072 }
1073
1074 fn add_from_download(
1075 &mut self,
1076 text: &str,
1077 request: &ClientRequest,
1078 source: DocSource,
1079 storage: Option<&Mutex<DynStore>>,
1080 changed: &mut bool,
1081 ) -> Result<()> {
1082 let requested: HashSet<_> = if let ClientRequest::Microdescs(req) = request {
1083 req.digests().collect()
1084 } else {
1085 return Err(internal!("expected a microdesc request").into());
1086 };
1087 let mut new_mds = Vec::new();
1088 let mut nonfatal_err = None;
1089
1090 for anno in MicrodescReader::new(text, &AllowAnnotations::AnnotationsNotAllowed)
1091 .map_err(|e| Error::from_netdoc(source.clone(), e))?
1092 {
1093 let anno = match anno {
1094 Err(e) => {
1095 nonfatal_err.get_or_insert_with(|| Error::from_netdoc(source.clone(), e));
1096 continue;
1097 }
1098 Ok(a) => a,
1099 };
1100 let txt = anno
1101 .within(text)
1102 .expect("microdesc not from within text as expected");
1103 let md = anno.into_microdesc();
1104 if !requested.contains(md.digest()) {
1105 warn!(
1106 "Received microdescriptor from {} we did not ask for: {:?}",
1107 source,
1108 md.digest()
1109 );
1110 nonfatal_err.get_or_insert(Error::Unwanted("un-requested microdescriptor"));
1111 continue;
1112 }
1113 new_mds.push((txt, md));
1114 }
1115
1116 let mark_listed = self.meta.lifetime().valid_after();
1117 if let Some(store) = storage {
1118 let mut s = store
1119 .lock()
1120 .expect("Directory storage lock poisoned");
1122 if !self.newly_listed.is_empty() {
1123 s.update_microdescs_listed(&self.newly_listed, mark_listed)?;
1124 self.newly_listed.clear();
1125 }
1126 if !new_mds.is_empty() {
1127 s.store_microdescs(
1128 &new_mds
1129 .iter()
1130 .map(|(text, md)| (*text, md.digest()))
1131 .collect::<Vec<_>>(),
1132 mark_listed,
1133 )?;
1134 }
1135 }
1136
1137 self.register_microdescs(new_mds.into_iter().map(|(_, md)| md), &source, changed);
1138
1139 opt_err_to_result(nonfatal_err)
1140 }
1141 fn advance(self: Box<Self>) -> Box<dyn DirState> {
1142 self
1143 }
1144 fn reset_time(&self) -> Option<SystemTime> {
1145 Some(match self.partial {
1153 PendingNetDir::Yielding {
1157 replace_dir_time,
1158 netdir: None,
1159 ..
1160 } => replace_dir_time,
1161 _ => self.reset_time,
1165 })
1166 }
1167 fn reset(self: Box<Self>) -> Box<dyn DirState> {
1168 let cache_usage = if self.cache_usage == CacheUsage::CacheOnly {
1169 CacheUsage::CacheOnly
1171 } else if self.is_ready(Readiness::Usable) {
1172 CacheUsage::MustDownload
1175 } else {
1176 CacheUsage::CacheOkay
1180 };
1181 Box::new(GetConsensusState::new(
1182 self.rt,
1183 self.config,
1184 cache_usage,
1185 self.prev_netdir,
1186 #[cfg(feature = "dirfilter")]
1187 self.filter,
1188 ))
1189 }
1190}
1191
1192fn pick_download_time(lifetime: &Lifetime) -> SystemTime {
1195 let (lowbound, uncertainty) = client_download_range(lifetime);
1196 lowbound + rand::rng().gen_range_infallible(..=uncertainty)
1197}
1198
1199fn client_download_range(lt: &Lifetime) -> (SystemTime, Duration) {
1202 let valid_after = lt.valid_after();
1203 let valid_until = lt.valid_until();
1204 let voting_interval = lt.voting_period();
1205 let whole_lifetime = valid_until
1206 .duration_since(valid_after)
1207 .expect("valid-after must precede valid-until");
1208
1209 let lowbound = voting_interval + (voting_interval * 3) / 4;
1215 let remainder = whole_lifetime
1216 .checked_sub(lowbound)
1217 .expect("Arithmetic did not work as expected");
1218 let uncertainty = (remainder * 7) / 8;
1219
1220 (valid_after + lowbound, uncertainty)
1221}
1222
1223fn opt_err_to_result(e: Option<Error>) -> Result<()> {
1225 match e {
1226 Some(e) => Err(e),
1227 None => Ok(()),
1228 }
1229}
1230
1231#[derive(Clone, Debug)]
1236pub(crate) struct PoisonedState;
1237
1238impl DirState for PoisonedState {
1239 fn describe(&self) -> String {
1240 unimplemented!()
1241 }
1242 fn missing_docs(&self) -> Vec<DocId> {
1243 unimplemented!()
1244 }
1245 fn is_ready(&self, _ready: Readiness) -> bool {
1246 unimplemented!()
1247 }
1248 fn can_advance(&self) -> bool {
1249 unimplemented!()
1250 }
1251 fn add_from_cache(
1252 &mut self,
1253 _docs: HashMap<DocId, DocumentText>,
1254 _changed: &mut bool,
1255 ) -> Result<()> {
1256 unimplemented!()
1257 }
1258 fn add_from_download(
1259 &mut self,
1260 _text: &str,
1261 _request: &ClientRequest,
1262 _source: DocSource,
1263 _storage: Option<&Mutex<DynStore>>,
1264 _changed: &mut bool,
1265 ) -> Result<()> {
1266 unimplemented!()
1267 }
1268 fn bootstrap_progress(&self) -> event::DirProgress {
1269 unimplemented!()
1270 }
1271 fn dl_config(&self) -> DownloadSchedule {
1272 unimplemented!()
1273 }
1274 fn advance(self: Box<Self>) -> Box<dyn DirState> {
1275 unimplemented!()
1276 }
1277 fn reset_time(&self) -> Option<SystemTime> {
1278 unimplemented!()
1279 }
1280 fn reset(self: Box<Self>) -> Box<dyn DirState> {
1281 unimplemented!()
1282 }
1283}
1284
1285#[cfg(test)]
1286mod test {
1287 #![allow(clippy::bool_assert_comparison)]
1289 #![allow(clippy::clone_on_copy)]
1290 #![allow(clippy::dbg_macro)]
1291 #![allow(clippy::mixed_attributes_style)]
1292 #![allow(clippy::print_stderr)]
1293 #![allow(clippy::print_stdout)]
1294 #![allow(clippy::single_char_pattern)]
1295 #![allow(clippy::unwrap_used)]
1296 #![allow(clippy::unchecked_time_subtraction)]
1297 #![allow(clippy::useless_vec)]
1298 #![allow(clippy::needless_pass_by_value)]
1299 #![allow(clippy::string_slice)] #![allow(clippy::cognitive_complexity)]
1302 use super::*;
1303 use std::convert::TryInto;
1304 use std::sync::Arc;
1305 use tempfile::TempDir;
1306 use time::macros::datetime;
1307 use tor_dircommon::{
1308 authority::{AuthorityContacts, AuthorityContactsBuilder},
1309 config::{DownloadScheduleConfig, NetworkConfig},
1310 };
1311 use tor_netdoc::doc::authcert::AuthCertKeyIds;
1312 use tor_rtcompat::RuntimeSubstExt as _;
1313 use tor_rtmock::simple_time::SimpleMockTimeProvider;
1314
1315 #[test]
1316 fn download_schedule() {
1317 let va = datetime!(2008-08-02 20:00 UTC).into();
1318 let fu = datetime!(2008-08-02 21:00 UTC).into();
1319 let vu = datetime!(2008-08-02 23:00 UTC).into();
1320 let lifetime = Lifetime::new(va, fu, vu).unwrap();
1321
1322 let expected_start: SystemTime = datetime!(2008-08-02 21:45 UTC).into();
1323 let expected_range = Duration::from_millis((75 * 60 * 1000) * 7 / 8);
1324
1325 let (start, range) = client_download_range(&lifetime);
1326 assert_eq!(start, expected_start);
1327 assert_eq!(range, expected_range);
1328
1329 for _ in 0..100 {
1330 let when = pick_download_time(&lifetime);
1331 assert!(when > va);
1332 assert!(when >= expected_start);
1333 assert!(when < vu);
1334 assert!(when <= expected_start + range);
1335 }
1336 }
1337
1338 fn temp_store() -> (TempDir, Mutex<DynStore>) {
1340 let tempdir = TempDir::new().unwrap();
1341
1342 let store = crate::storage::SqliteStore::from_path_and_mistrust(
1343 tempdir.path(),
1344 &fs_mistrust::Mistrust::new_dangerously_trust_everyone(),
1345 false,
1346 )
1347 .unwrap();
1348
1349 (tempdir, Mutex::new(Box::new(store)))
1350 }
1351
1352 fn make_time_shifted_runtime(now: SystemTime, rt: impl Runtime) -> impl Runtime {
1353 let msp = SimpleMockTimeProvider::from_wallclock(now);
1354 rt.with_sleep_provider(msp.clone())
1355 .with_coarse_time_provider(msp)
1356 }
1357
1358 fn make_dirmgr_config(authorities: Option<AuthorityContactsBuilder>) -> Arc<DirMgrConfig> {
1359 let mut netcfg = NetworkConfig::builder();
1360 netcfg.set_fallback_caches(vec![]);
1361 if let Some(a) = authorities {
1362 *netcfg.authorities() = a;
1363 }
1364 let cfg = DirMgrConfig {
1365 cache_dir: "/we_will_never_use_this/".into(),
1366 network: netcfg.build().unwrap(),
1367 ..Default::default()
1368 };
1369 Arc::new(cfg)
1370 }
1371
1372 const CONSENSUS: &str = include_str!("../testdata/mdconsensus1.txt");
1374 const CONSENSUS2: &str = include_str!("../testdata/mdconsensus2.txt");
1375 const AUTHCERT_5696: &str = include_str!("../testdata/cert-5696.txt");
1376 const AUTHCERT_5A23: &str = include_str!("../testdata/cert-5A23.txt");
1377 #[allow(unused)]
1378 const AUTHCERT_7C47: &str = include_str!("../testdata/cert-7C47.txt");
1379 fn test_time() -> SystemTime {
1380 datetime!(2020-08-07 12:42:45 UTC).into()
1381 }
1382 fn rsa(s: &str) -> RsaIdentity {
1383 RsaIdentity::from_hex(s).unwrap()
1384 }
1385 fn test_authorities() -> AuthorityContactsBuilder {
1386 let mut builder = AuthorityContacts::builder();
1387 builder
1388 .v3idents()
1389 .push(rsa("5696AB38CB3852AFA476A5C07B2D4788963D5567"));
1390 builder
1391 .v3idents()
1392 .push(rsa("5A23BA701776C9C1AB1C06E734E92AB3D5350D64"));
1393
1394 builder
1395 }
1396 fn authcert_id_5696() -> AuthCertKeyIds {
1397 AuthCertKeyIds {
1398 id_fingerprint: rsa("5696ab38cb3852afa476a5c07b2d4788963d5567"),
1399 sk_fingerprint: rsa("f6ed4aa64d83caede34e19693a7fcf331aae8a6a"),
1400 }
1401 }
1402 fn authcert_id_5a23() -> AuthCertKeyIds {
1403 AuthCertKeyIds {
1404 id_fingerprint: rsa("5a23ba701776c9c1ab1c06e734e92ab3d5350d64"),
1405 sk_fingerprint: rsa("d08e965cc6dcb6cb6ed776db43e616e93af61177"),
1406 }
1407 }
1408 fn authcert_id_7c47() -> AuthCertKeyIds {
1410 AuthCertKeyIds {
1411 id_fingerprint: rsa("7C47DCB4A90E2C2B7C7AD27BD641D038CF5D7EBE"),
1412 sk_fingerprint: rsa("D3C013E0E6C82E246090D1C0798B75FCB7ACF120"),
1413 }
1414 }
1415 fn microdescs() -> HashMap<MdDigest, String> {
1416 const MICRODESCS: &str = include_str!("../testdata/microdescs.txt");
1417 let text = MICRODESCS;
1418 MicrodescReader::new(text, &AllowAnnotations::AnnotationsNotAllowed)
1419 .unwrap()
1420 .map(|res| {
1421 let anno = res.unwrap();
1422 let text = anno.within(text).unwrap();
1423 let md = anno.into_microdesc();
1424 (*md.digest(), text.to_owned())
1425 })
1426 .collect()
1427 }
1428
1429 #[test]
1430 fn get_consensus_state() {
1431 tor_rtcompat::test_with_one_runtime!(|rt| async move {
1432 let rt = make_time_shifted_runtime(test_time(), rt);
1433 let cfg = make_dirmgr_config(None);
1434
1435 let (_tempdir, store) = temp_store();
1436
1437 let mut state = GetConsensusState::new(
1438 rt.clone(),
1439 cfg,
1440 CacheUsage::CacheOkay,
1441 None,
1442 #[cfg(feature = "dirfilter")]
1443 Arc::new(crate::filter::NilFilter),
1444 );
1445
1446 assert_eq!(&state.describe(), "Looking for a consensus.");
1448
1449 assert!(!state.can_advance());
1451 assert!(!state.is_ready(Readiness::Complete));
1452 assert!(!state.is_ready(Readiness::Usable));
1453
1454 assert!(state.reset_time().is_none());
1456
1457 assert_eq!(
1459 state.bootstrap_progress().to_string(),
1460 "fetching a consensus"
1461 );
1462
1463 let retry = state.dl_config();
1466 assert_eq!(retry, DownloadScheduleConfig::default().retry_consensus());
1467
1468 let docs = state.missing_docs();
1470 assert_eq!(docs.len(), 1);
1471 let docid = docs[0];
1472
1473 assert!(matches!(
1474 docid,
1475 DocId::LatestConsensus {
1476 flavor: ConsensusFlavor::Microdesc,
1477 cache_usage: CacheUsage::CacheOkay,
1478 }
1479 ));
1480 let source = DocSource::DirServer { source: None };
1481
1482 let req = tor_dirclient::request::ConsensusRequest::new(ConsensusFlavor::Microdesc);
1484 let req = crate::docid::ClientRequest::Consensus(req);
1485 let mut changed = false;
1486 let outcome = state.add_from_download(
1487 "this isn't a consensus",
1488 &req,
1489 source.clone(),
1490 Some(&store),
1491 &mut changed,
1492 );
1493 assert!(matches!(outcome, Err(Error::NetDocError { .. })));
1494 assert!(!changed);
1495 assert!(
1497 store
1498 .lock()
1499 .unwrap()
1500 .latest_consensus(ConsensusFlavor::Microdesc, None)
1501 .unwrap()
1502 .is_none()
1503 );
1504
1505 let mut changed = false;
1507 let outcome = state.add_from_download(
1508 CONSENSUS,
1509 &req,
1510 source.clone(),
1511 Some(&store),
1512 &mut changed,
1513 );
1514 assert!(matches!(outcome, Err(Error::UnrecognizedAuthorities)));
1515 assert!(!changed);
1516 assert!(
1517 store
1518 .lock()
1519 .unwrap()
1520 .latest_consensus(ConsensusFlavor::Microdesc, None)
1521 .unwrap()
1522 .is_none()
1523 );
1524
1525 let cfg = make_dirmgr_config(Some(test_authorities()));
1528
1529 let mut state = GetConsensusState::new(
1530 rt.clone(),
1531 cfg,
1532 CacheUsage::CacheOkay,
1533 None,
1534 #[cfg(feature = "dirfilter")]
1535 Arc::new(crate::filter::NilFilter),
1536 );
1537 let mut changed = false;
1538 let outcome =
1539 state.add_from_download(CONSENSUS, &req, source, Some(&store), &mut changed);
1540 assert!(outcome.is_ok());
1541 assert!(changed);
1542 assert!(
1543 store
1544 .lock()
1545 .unwrap()
1546 .latest_consensus(ConsensusFlavor::Microdesc, None)
1547 .unwrap()
1548 .is_some()
1549 );
1550
1551 assert!(state.can_advance());
1553 assert_eq!(&state.describe(), "About to fetch certificates.");
1554 assert_eq!(state.missing_docs(), Vec::new());
1555 let next = Box::new(state).advance();
1556 assert_eq!(
1557 &next.describe(),
1558 "Downloading certificates for consensus (we are missing 2/2)."
1559 );
1560
1561 let cfg = make_dirmgr_config(Some(test_authorities()));
1563 let mut state = GetConsensusState::new(
1564 rt,
1565 cfg,
1566 CacheUsage::CacheOkay,
1567 None,
1568 #[cfg(feature = "dirfilter")]
1569 Arc::new(crate::filter::NilFilter),
1570 );
1571 let text: crate::storage::InputString = CONSENSUS.to_owned().into();
1572 let map = vec![(docid, text.into())].into_iter().collect();
1573 let mut changed = false;
1574 let outcome = state.add_from_cache(map, &mut changed);
1575 assert!(outcome.is_ok());
1576 assert!(changed);
1577 assert!(state.can_advance());
1578 });
1579 }
1580
1581 #[test]
1582 fn get_certs_state() {
1583 tor_rtcompat::test_with_one_runtime!(|rt| async move {
1584 fn new_getcerts_state(rt: impl Runtime) -> Box<dyn DirState> {
1586 let rt = make_time_shifted_runtime(test_time(), rt);
1587 let cfg = make_dirmgr_config(Some(test_authorities()));
1588 let mut state = GetConsensusState::new(
1589 rt,
1590 cfg,
1591 CacheUsage::CacheOkay,
1592 None,
1593 #[cfg(feature = "dirfilter")]
1594 Arc::new(crate::filter::NilFilter),
1595 );
1596 let source = DocSource::DirServer { source: None };
1597 let req = tor_dirclient::request::ConsensusRequest::new(ConsensusFlavor::Microdesc);
1598 let req = crate::docid::ClientRequest::Consensus(req);
1599 let mut changed = false;
1600 let outcome = state.add_from_download(CONSENSUS, &req, source, None, &mut changed);
1601 assert!(outcome.is_ok());
1602 Box::new(state).advance()
1603 }
1604
1605 let (_tempdir, store) = temp_store();
1606 let mut state = new_getcerts_state(rt.clone());
1607 assert_eq!(
1609 &state.describe(),
1610 "Downloading certificates for consensus (we are missing 2/2)."
1611 );
1612 assert!(!state.can_advance());
1613 assert!(!state.is_ready(Readiness::Complete));
1614 assert!(!state.is_ready(Readiness::Usable));
1615 let consensus_expires: SystemTime = datetime!(2020-08-07 12:43:20 UTC).into();
1616 let post_valid_tolerance = crate::DirTolerance::default().post_valid_tolerance();
1617 assert_eq!(
1618 state.reset_time(),
1619 Some(consensus_expires + post_valid_tolerance)
1620 );
1621 let retry = state.dl_config();
1622 assert_eq!(retry, DownloadScheduleConfig::default().retry_certs());
1623
1624 assert_eq!(
1626 state.bootstrap_progress().to_string(),
1627 "fetching authority certificates (0/2)"
1628 );
1629
1630 let missing = state.missing_docs();
1632 assert_eq!(missing.len(), 2); assert!(missing.contains(&DocId::AuthCert(authcert_id_5696())));
1634 assert!(missing.contains(&DocId::AuthCert(authcert_id_5a23())));
1635 assert!(!missing.contains(&DocId::AuthCert(authcert_id_7c47())));
1637
1638 let text1: crate::storage::InputString = AUTHCERT_5696.to_owned().into();
1640 let docs = vec![(DocId::AuthCert(authcert_id_5696()), text1.into())]
1642 .into_iter()
1643 .collect();
1644 let mut changed = false;
1645 let outcome = state.add_from_cache(docs, &mut changed);
1646 assert!(changed);
1647 assert!(outcome.is_ok()); assert!(!state.can_advance()); let missing = state.missing_docs();
1650 assert_eq!(missing.len(), 1); assert!(missing.contains(&DocId::AuthCert(authcert_id_5a23())));
1652 assert_eq!(
1653 state.bootstrap_progress().to_string(),
1654 "fetching authority certificates (1/2)"
1655 );
1656
1657 let source = DocSource::DirServer { source: None };
1660 let mut req = tor_dirclient::request::AuthCertRequest::new();
1661 req.push(authcert_id_5696()); let req = ClientRequest::AuthCert(req);
1663 let mut changed = false;
1664 let outcome = state.add_from_download(
1665 AUTHCERT_5A23,
1666 &req,
1667 source.clone(),
1668 Some(&store),
1669 &mut changed,
1670 );
1671 assert!(matches!(outcome, Err(Error::Unwanted(_))));
1672 assert!(!changed);
1673 let missing2 = state.missing_docs();
1674 assert_eq!(missing, missing2); assert!(
1676 store
1677 .lock()
1678 .unwrap()
1679 .authcerts(&[authcert_id_5a23()])
1680 .unwrap()
1681 .is_empty()
1682 );
1683
1684 let mut req = tor_dirclient::request::AuthCertRequest::new();
1686 req.push(authcert_id_5a23()); let req = ClientRequest::AuthCert(req);
1688 let mut changed = false;
1689 let outcome =
1690 state.add_from_download(AUTHCERT_5A23, &req, source, Some(&store), &mut changed);
1691 assert!(outcome.is_ok()); assert!(changed);
1693 let missing3 = state.missing_docs();
1694 assert!(missing3.is_empty());
1695 assert!(state.can_advance());
1696 assert!(
1697 !store
1698 .lock()
1699 .unwrap()
1700 .authcerts(&[authcert_id_5a23()])
1701 .unwrap()
1702 .is_empty()
1703 );
1704
1705 let next = state.advance();
1706 assert_eq!(
1707 &next.describe(),
1708 "Downloading microdescriptors (we are missing 6)."
1709 );
1710
1711 let state = new_getcerts_state(rt);
1713 let state = state.reset();
1714 assert_eq!(&state.describe(), "Downloading a consensus.");
1715
1716 });
1719 }
1720
1721 #[test]
1722 fn get_microdescs_state() {
1723 tor_rtcompat::test_with_one_runtime!(|rt| async move {
1724 fn new_getmicrodescs_state(rt: impl Runtime) -> GetMicrodescsState<impl Runtime> {
1726 let rt = make_time_shifted_runtime(test_time(), rt);
1727 let cfg = make_dirmgr_config(Some(test_authorities()));
1728 let (signed, rest, consensus) = MdConsensus::parse(CONSENSUS2).unwrap();
1729 let consensus = consensus
1730 .dangerously_assume_timely()
1731 .dangerously_assume_wellsigned();
1732 let meta = ConsensusMeta::from_consensus(signed, rest, &consensus);
1733 GetMicrodescsState::new(
1734 CacheUsage::CacheOkay,
1735 consensus,
1736 meta,
1737 rt,
1738 cfg,
1739 None,
1740 #[cfg(feature = "dirfilter")]
1741 Arc::new(crate::filter::NilFilter),
1742 )
1743 }
1744 fn d64(s: &str) -> MdDigest {
1745 use base64ct::{Base64Unpadded, Encoding as _};
1746 Base64Unpadded::decode_vec(s).unwrap().try_into().unwrap()
1747 }
1748
1749 let state = new_getmicrodescs_state(rt.clone());
1751 let state = Box::new(state).reset();
1752 assert_eq!(&state.describe(), "Looking for a consensus.");
1753
1754 let mut state = new_getmicrodescs_state(rt.clone());
1756 assert_eq!(
1757 &state.describe(),
1758 "Downloading microdescriptors (we are missing 4)."
1759 );
1760 assert!(!state.can_advance());
1761 assert!(!state.is_ready(Readiness::Complete));
1762 assert!(!state.is_ready(Readiness::Usable));
1763 {
1764 let reset_time = state.reset_time().unwrap();
1765 let fresh_until: SystemTime = datetime!(2021-10-27 21:27:00 UTC).into();
1766 let valid_until: SystemTime = datetime!(2021-10-27 21:27:20 UTC).into();
1767 assert!(reset_time >= fresh_until);
1768 assert!(reset_time <= valid_until + state.config.tolerance.post_valid_tolerance());
1769 }
1770 let retry = state.dl_config();
1771 assert_eq!(retry, DownloadScheduleConfig::default().retry_microdescs());
1772 assert_eq!(
1773 state.bootstrap_progress().to_string(),
1774 "fetching microdescriptors (0/4)"
1775 );
1776
1777 let missing = state.missing_docs();
1779 let md_text = microdescs();
1780 assert_eq!(missing.len(), 4);
1781 assert_eq!(md_text.len(), 4);
1782 let md1 = d64("LOXRj8YZP0kwpEAsYOvBZWZWGoWv5b/Bp2Mz2Us8d8g");
1783 let md2 = d64("iOhVp33NyZxMRDMHsVNq575rkpRViIJ9LN9yn++nPG0");
1784 let md3 = d64("/Cd07b3Bl0K0jX2/1cAvsYXJJMi5d8UBU+oWKaLxoGo");
1785 let md4 = d64("z+oOlR7Ga6cg9OoC/A3D3Ey9Rtc4OldhKlpQblMfQKo");
1786 for md_digest in [md1, md2, md3, md4] {
1787 assert!(missing.contains(&DocId::Microdesc(md_digest)));
1788 assert!(md_text.contains_key(&md_digest));
1789 }
1790
1791 let (_tempdir, store) = temp_store();
1793 let doc1: crate::storage::InputString = md_text.get(&md1).unwrap().clone().into();
1794 let docs = vec![(DocId::Microdesc(md1), doc1.into())]
1795 .into_iter()
1796 .collect();
1797 let mut changed = false;
1798 let outcome = state.add_from_cache(docs, &mut changed);
1799 assert!(outcome.is_ok()); assert!(changed);
1801 assert!(!state.can_advance());
1802 assert!(!state.is_ready(Readiness::Complete));
1803 assert!(!state.is_ready(Readiness::Usable));
1804
1805 let missing = state.missing_docs();
1807 assert_eq!(missing.len(), 3);
1808 assert!(!missing.contains(&DocId::Microdesc(md1)));
1809 assert_eq!(
1810 state.bootstrap_progress().to_string(),
1811 "fetching microdescriptors (1/4)"
1812 );
1813
1814 let mut req = tor_dirclient::request::MicrodescRequest::new();
1816 let mut response = "".to_owned();
1817 for md_digest in [md2, md3, md4] {
1818 response.push_str(md_text.get(&md_digest).unwrap());
1819 req.push(md_digest);
1820 }
1821 let req = ClientRequest::Microdescs(req);
1822 let source = DocSource::DirServer { source: None };
1823 let mut changed = false;
1824 let outcome = state.add_from_download(
1825 response.as_str(),
1826 &req,
1827 source,
1828 Some(&store),
1829 &mut changed,
1830 );
1831 assert!(outcome.is_ok()); assert!(changed);
1833 match state.get_netdir_change().unwrap() {
1834 NetDirChange::AttemptReplace { netdir, .. } => {
1835 assert!(netdir.take().is_some());
1836 }
1837 x => panic!("wrong netdir change: {:?}", x),
1838 }
1839 assert!(state.is_ready(Readiness::Complete));
1840 assert!(state.is_ready(Readiness::Usable));
1841 assert_eq!(
1842 store
1843 .lock()
1844 .unwrap()
1845 .microdescs(&[md2, md3, md4])
1846 .unwrap()
1847 .len(),
1848 3
1849 );
1850
1851 let missing = state.missing_docs();
1852 assert!(missing.is_empty());
1853 });
1854 }
1855}