1mod rs;
47
48pub mod md;
49#[cfg(feature = "plain-consensus")]
50pub mod plain;
51#[cfg(feature = "ns-vote")]
52pub mod vote;
53
54#[cfg(feature = "build_docs")]
55mod build;
56
57#[cfg(feature = "parse2")]
58use {
59 crate::parse2::{self, ArgumentStream}, };
61
62use crate::doc::authcert::{AuthCert, AuthCertKeyIds};
63use crate::parse::keyword::Keyword;
64use crate::parse::parser::{Section, SectionRules, SectionRulesBuilder};
65use crate::parse::tokenize::{Item, ItemResult, NetDocReader};
66use crate::types::misc::*;
67use crate::util::PeekableIterator;
68use crate::{Error, NetdocErrorKind as EK, NormalItemArgument, Pos, Result};
69use std::collections::{BTreeSet, HashMap, HashSet};
70use std::fmt::{self, Display};
71use std::result::Result as StdResult;
72use std::str::FromStr;
73use std::sync::Arc;
74use std::{net, result, time};
75use tor_error::{HasKind, internal};
76use tor_protover::Protocols;
77
78use bitflags::bitflags;
79use derive_deftly::{Deftly, define_derive_deftly};
80use digest::Digest;
81use std::sync::LazyLock;
82use tor_checkable::{ExternallySigned, timed::TimerangeBound};
83use tor_llcrypto as ll;
84use tor_llcrypto::pk::rsa::RsaIdentity;
85
86use serde::{Deserialize, Deserializer};
87
88#[cfg(feature = "build_docs")]
89pub use build::MdConsensusBuilder;
90#[cfg(all(feature = "build_docs", feature = "plain-consensus"))]
91pub use build::PlainConsensusBuilder;
92#[cfg(feature = "build_docs")]
93ns_export_each_flavor! {
94 ty: RouterStatusBuilder;
95}
96
97ns_export_each_variety! {
98 ty: RouterStatus, Preamble;
99}
100
101use void::ResultVoidExt as _;
102
103#[deprecated]
104#[cfg(feature = "ns_consensus")]
105pub use PlainConsensus as NsConsensus;
106#[deprecated]
107#[cfg(feature = "ns_consensus")]
108pub use PlainRouterStatus as NsRouterStatus;
109#[deprecated]
110#[cfg(feature = "ns_consensus")]
111pub use UncheckedPlainConsensus as UncheckedNsConsensus;
112#[deprecated]
113#[cfg(feature = "ns_consensus")]
114pub use UnvalidatedPlainConsensus as UnvalidatedNsConsensus;
115
116#[cfg(feature = "ns-vote")]
117pub use rs::RouterStatusMdDigestsVote;
118
119#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Default)]
133#[allow(clippy::exhaustive_structs)]
134pub struct IgnoredPublicationTimeSp;
135
136#[derive(Clone, Debug, Deftly)]
144#[derive_deftly(Lifetime)]
145#[cfg_attr(feature = "parse2", derive_deftly(NetdocParseableFields))]
146pub struct Lifetime {
147 #[cfg_attr(feature = "parse2", deftly(netdoc(single_arg)))]
154 valid_after: Iso8601TimeSp,
155 #[cfg_attr(feature = "parse2", deftly(netdoc(single_arg)))]
163 fresh_until: Iso8601TimeSp,
164 #[cfg_attr(feature = "parse2", deftly(netdoc(single_arg)))]
172 valid_until: Iso8601TimeSp,
173}
174
175define_derive_deftly! {
176 Lifetime:
178
179 impl Lifetime {
180 pub fn new(
182 $( $fname: time::SystemTime, )
183 ) -> Result<Self> {
184 let self_ = Lifetime {
188 $( $fname: $fname.into(), )
189 };
190 if self_.valid_after < self_.fresh_until && self_.fresh_until < self_.valid_until {
191 Ok(self_)
192 } else {
193 Err(EK::InvalidLifetime.err())
194 }
195 }
196 $(
197 ${fattrs doc}
198 pub fn $fname(&self) -> time::SystemTime {
199 *self.$fname
200 }
201 )
202 pub fn valid_at(&self, when: time::SystemTime) -> bool {
204 *self.valid_after <= when && when <= *self.valid_until
205 }
206
207 pub fn voting_period(&self) -> time::Duration {
212 let valid_after = self.valid_after();
213 let fresh_until = self.fresh_until();
214 fresh_until
215 .duration_since(valid_after)
216 .expect("Mis-formed lifetime")
217 }
218 }
219}
220use derive_deftly_template_Lifetime;
221
222#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Copy)] #[derive(derive_more::From, derive_more::Into, derive_more::Display, derive_more::FromStr)]
234pub struct ConsensusMethod(u32);
235impl NormalItemArgument for ConsensusMethod {}
236
237#[derive(Debug, Clone, Default, Eq, PartialEq)]
244#[cfg_attr(feature = "parse2", derive(Deftly), derive_deftly(ItemValueParseable))]
245#[non_exhaustive]
246pub struct ConsensusMethods {
247 pub methods: BTreeSet<ConsensusMethod>,
249}
250
251#[cfg(feature = "parse2")]
256pub mod consensus_methods_comma_separated {
257 use super::*;
258 use parse2::ArgumentError as AE;
259 use std::result::Result;
260
261 pub fn from_args<'s>(args: &mut ArgumentStream<'s>) -> Result<ConsensusMethods, AE> {
263 let mut methods = BTreeSet::new();
264 for ent in args.next().ok_or(AE::Missing)?.split(',') {
265 let ent = ent.parse().map_err(|_| AE::Invalid)?;
266 if !methods.insert(ent) {
267 return Err(AE::Invalid);
268 }
269 }
270 Ok(ConsensusMethods { methods })
271 }
272}
273
274#[derive(Debug, Clone, Default, Eq, PartialEq)]
290pub struct NetParams<T> {
291 params: HashMap<String, T>,
293}
294
295impl<T> NetParams<T> {
296 #[allow(unused)]
298 pub fn new() -> Self {
299 NetParams {
300 params: HashMap::new(),
301 }
302 }
303 pub fn get<A: AsRef<str>>(&self, v: A) -> Option<&T> {
305 self.params.get(v.as_ref())
306 }
307 pub fn iter(&self) -> impl Iterator<Item = (&String, &T)> {
309 self.params.iter()
310 }
311 pub fn set(&mut self, k: String, v: T) {
313 self.params.insert(k, v);
314 }
315}
316
317impl<K: Into<String>, T> FromIterator<(K, T)> for NetParams<T> {
318 fn from_iter<I: IntoIterator<Item = (K, T)>>(i: I) -> Self {
319 NetParams {
320 params: i.into_iter().map(|(k, v)| (k.into(), v)).collect(),
321 }
322 }
323}
324
325impl<T> std::iter::Extend<(String, T)> for NetParams<T> {
326 fn extend<I: IntoIterator<Item = (String, T)>>(&mut self, iter: I) {
327 self.params.extend(iter);
328 }
329}
330
331impl<'de, T> Deserialize<'de> for NetParams<T>
332where
333 T: Deserialize<'de>,
334{
335 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
336 where
337 D: Deserializer<'de>,
338 {
339 let params = HashMap::deserialize(deserializer)?;
340 Ok(NetParams { params })
341 }
342}
343
344#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
353pub struct ProtoStatus {
354 recommended: Protocols,
359 required: Protocols,
364}
365
366impl ProtoStatus {
367 pub fn check_protocols(
377 &self,
378 supported_protocols: &Protocols,
379 ) -> StdResult<(), ProtocolSupportError> {
380 let missing_required = self.required.difference(supported_protocols);
382 if !missing_required.is_empty() {
383 return Err(ProtocolSupportError::MissingRequired(missing_required));
384 }
385 let missing_recommended = self.recommended.difference(supported_protocols);
386 if !missing_recommended.is_empty() {
387 return Err(ProtocolSupportError::MissingRecommended(
388 missing_recommended,
389 ));
390 }
391
392 Ok(())
393 }
394}
395
396#[derive(Clone, Debug, thiserror::Error)]
398#[cfg_attr(test, derive(PartialEq))]
399#[non_exhaustive]
400pub enum ProtocolSupportError {
401 #[error("Required protocols are not implemented: {0}")]
403 MissingRequired(Protocols),
404
405 #[error("Recommended protocols are not implemented: {0}")]
409 MissingRecommended(Protocols),
410}
411
412impl ProtocolSupportError {
413 pub fn should_shutdown(&self) -> bool {
415 matches!(self, Self::MissingRequired(_))
416 }
417}
418
419impl HasKind for ProtocolSupportError {
420 fn kind(&self) -> tor_error::ErrorKind {
421 tor_error::ErrorKind::SoftwareDeprecated
422 }
423}
424
425#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
432pub struct ProtoStatuses {
433 client: ProtoStatus,
435 relay: ProtoStatus,
437}
438
439impl ProtoStatuses {
440 pub fn client(&self) -> &ProtoStatus {
442 &self.client
443 }
444
445 pub fn relay(&self) -> &ProtoStatus {
447 &self.relay
448 }
449}
450
451#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
455#[non_exhaustive]
456pub enum ConsensusFlavor {
457 Microdesc,
460 Plain,
465}
466
467impl ConsensusFlavor {
468 pub fn name(&self) -> &'static str {
470 match self {
471 ConsensusFlavor::Plain => "ns", ConsensusFlavor::Microdesc => "microdesc",
473 }
474 }
475 pub fn from_opt_name(name: Option<&str>) -> Result<Self> {
480 match name {
481 Some("microdesc") => Ok(ConsensusFlavor::Microdesc),
482 Some("ns") | None => Ok(ConsensusFlavor::Plain),
483 Some(other) => {
484 Err(EK::BadDocumentType.with_msg(format!("unrecognized flavor {:?}", other)))
485 }
486 }
487 }
488}
489
490#[derive(Debug, Clone)]
492#[non_exhaustive]
493pub struct Signature {
494 pub digestname: String,
499 pub key_ids: AuthCertKeyIds,
502 pub signature: Vec<u8>,
504}
505
506#[derive(Debug, Clone)]
508#[non_exhaustive]
509pub struct SignatureGroup {
510 pub sha256: Option<[u8; 32]>,
512 pub sha1: Option<[u8; 20]>,
514 pub signatures: Vec<Signature>,
516}
517
518#[derive(
520 Debug, Clone, Copy, Eq, PartialEq, derive_more::From, derive_more::Into, derive_more::AsRef,
521)]
522pub struct SharedRandVal([u8; 32]);
524
525#[derive(Debug, Clone)]
528#[non_exhaustive]
529#[cfg_attr(feature = "parse2", derive(Deftly), derive_deftly(ItemValueParseable))]
530pub struct SharedRandStatus {
531 pub n_reveals: u8,
533 pub value: SharedRandVal,
540
541 pub timestamp: Option<Iso8601TimeNoSp>,
545}
546
547#[derive(Debug, Clone)]
551#[non_exhaustive]
552pub struct DirSource {
553 pub nickname: String,
555 pub identity: RsaIdentity,
561 pub ip: net::IpAddr,
563 pub dir_port: u16,
565 pub or_port: u16,
567}
568
569bitflags! {
570 #[derive(Clone, Copy, Debug)]
581 pub struct RelayFlags: u16 {
582 const AUTHORITY = (1<<0);
584 const BAD_EXIT = (1<<1);
589 const EXIT = (1<<2);
591 const FAST = (1<<3);
593 const GUARD = (1<<4);
598 const HSDIR = (1<<5);
601 const MIDDLE_ONLY = (1<<6);
608 const NO_ED_CONSENSUS = (1<<7);
610 const STABLE = (1<<8);
612 const STALE_DESC = (1<<9);
615 const RUNNING = (1<<10);
620 const VALID = (1<<11);
626 const V2DIR = (1<<12);
629 }
630}
631
632#[non_exhaustive]
634#[derive(Debug, Clone, Copy)]
635pub enum RelayWeight {
636 Unmeasured(u32),
638 Measured(u32),
640}
641
642impl RelayWeight {
643 pub fn is_measured(&self) -> bool {
645 matches!(self, RelayWeight::Measured(_))
646 }
647 pub fn is_nonzero(&self) -> bool {
649 !matches!(self, RelayWeight::Unmeasured(0) | RelayWeight::Measured(0))
650 }
651}
652
653#[derive(Debug, Clone)]
655#[non_exhaustive]
656pub struct ConsensusVoterInfo {
657 pub dir_source: DirSource,
659 pub contact: String,
661 pub vote_digest: Vec<u8>,
664}
665
666#[derive(Debug, Clone)]
668#[non_exhaustive]
669pub struct Footer {
670 pub weights: NetParams<i32>,
676}
677
678pub type MdConsensus = md::Consensus;
681
682pub type UnvalidatedMdConsensus = md::UnvalidatedConsensus;
685
686pub type UncheckedMdConsensus = md::UncheckedConsensus;
689
690#[cfg(feature = "plain-consensus")]
691pub type PlainConsensus = plain::Consensus;
694
695#[cfg(feature = "plain-consensus")]
696pub type UnvalidatedPlainConsensus = plain::UnvalidatedConsensus;
699
700#[cfg(feature = "plain-consensus")]
701pub type UncheckedPlainConsensus = plain::UncheckedConsensus;
704
705decl_keyword! {
706 #[non_exhaustive]
711 #[allow(missing_docs)]
712 pub NetstatusKwd {
713 "network-status-version" => NETWORK_STATUS_VERSION,
715 "vote-status" => VOTE_STATUS,
716 "consensus-methods" => CONSENSUS_METHODS,
717 "consensus-method" => CONSENSUS_METHOD,
718 "published" => PUBLISHED,
719 "valid-after" => VALID_AFTER,
720 "fresh-until" => FRESH_UNTIL,
721 "valid-until" => VALID_UNTIL,
722 "voting-delay" => VOTING_DELAY,
723 "client-versions" => CLIENT_VERSIONS,
724 "server-versions" => SERVER_VERSIONS,
725 "known-flags" => KNOWN_FLAGS,
726 "flag-thresholds" => FLAG_THRESHOLDS,
727 "recommended-client-protocols" => RECOMMENDED_CLIENT_PROTOCOLS,
728 "required-client-protocols" => REQUIRED_CLIENT_PROTOCOLS,
729 "recommended-relay-protocols" => RECOMMENDED_RELAY_PROTOCOLS,
730 "required-relay-protocols" => REQUIRED_RELAY_PROTOCOLS,
731 "params" => PARAMS,
732 "bandwidth-file-headers" => BANDWIDTH_FILE_HEADERS,
733 "bandwidth-file-digest" => BANDWIDTH_FILE_DIGEST,
734 "shared-rand-previous-value" => SHARED_RAND_PREVIOUS_VALUE,
738 "shared-rand-current-value" => SHARED_RAND_CURRENT_VALUE,
739
740 "dir-source" => DIR_SOURCE,
742 "contact" => CONTACT,
743
744 "legacy-dir-key" => LEGACY_DIR_KEY,
746 "shared-rand-participate" => SHARED_RAND_PARTICIPATE,
747 "shared-rand-commit" => SHARED_RAND_COMMIT,
748
749 "vote-digest" => VOTE_DIGEST,
751
752 "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION,
754
755 "r" => RS_R,
757 "a" => RS_A,
758 "s" => RS_S,
759 "v" => RS_V,
760 "pr" => RS_PR,
761 "w" => RS_W,
762 "p" => RS_P,
763 "m" => RS_M,
764 "id" => RS_ID,
765
766 "directory-footer" => DIRECTORY_FOOTER,
768 "bandwidth-weights" => BANDWIDTH_WEIGHTS,
769 "directory-signature" => DIRECTORY_SIGNATURE,
770 }
771}
772
773static NS_HEADER_RULES_COMMON_: LazyLock<SectionRulesBuilder<NetstatusKwd>> = LazyLock::new(|| {
775 use NetstatusKwd::*;
776 let mut rules = SectionRules::builder();
777 rules.add(NETWORK_STATUS_VERSION.rule().required().args(1..=2));
778 rules.add(VOTE_STATUS.rule().required().args(1..));
779 rules.add(VALID_AFTER.rule().required());
780 rules.add(FRESH_UNTIL.rule().required());
781 rules.add(VALID_UNTIL.rule().required());
782 rules.add(VOTING_DELAY.rule().args(2..));
783 rules.add(CLIENT_VERSIONS.rule());
784 rules.add(SERVER_VERSIONS.rule());
785 rules.add(KNOWN_FLAGS.rule().required());
786 rules.add(RECOMMENDED_CLIENT_PROTOCOLS.rule().args(1..));
787 rules.add(RECOMMENDED_RELAY_PROTOCOLS.rule().args(1..));
788 rules.add(REQUIRED_CLIENT_PROTOCOLS.rule().args(1..));
789 rules.add(REQUIRED_RELAY_PROTOCOLS.rule().args(1..));
790 rules.add(PARAMS.rule());
791 rules
792});
793static NS_HEADER_RULES_CONSENSUS: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
795 use NetstatusKwd::*;
796 let mut rules = NS_HEADER_RULES_COMMON_.clone();
797 rules.add(CONSENSUS_METHOD.rule().args(1..=1));
798 rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..));
799 rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..));
800 rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
801 rules.build()
802});
803static NS_VOTERINFO_RULES_CONSENSUS: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
834 use NetstatusKwd::*;
835 let mut rules = SectionRules::builder();
836 rules.add(DIR_SOURCE.rule().required().args(6..));
837 rules.add(CONTACT.rule().required());
838 rules.add(VOTE_DIGEST.rule().required());
839 rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
840 rules.build()
841});
842static NS_ROUTERSTATUS_RULES_COMMON_: LazyLock<SectionRulesBuilder<NetstatusKwd>> =
844 LazyLock::new(|| {
845 use NetstatusKwd::*;
846 let mut rules = SectionRules::builder();
847 rules.add(RS_A.rule().may_repeat().args(1..));
848 rules.add(RS_S.rule().required());
849 rules.add(RS_V.rule());
850 rules.add(RS_PR.rule().required());
851 rules.add(RS_W.rule());
852 rules.add(RS_P.rule().args(2..));
853 rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
854 rules
855 });
856
857static NS_ROUTERSTATUS_RULES_PLAIN: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
859 use NetstatusKwd::*;
860 let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
861 rules.add(RS_R.rule().required().args(8..));
862 rules.build()
863});
864
865static NS_ROUTERSTATUS_RULES_MDCON: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
878 use NetstatusKwd::*;
879 let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
880 rules.add(RS_R.rule().required().args(6..));
881 rules.add(RS_M.rule().required().args(1..));
882 rules.build()
883});
884static NS_FOOTER_RULES: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
886 use NetstatusKwd::*;
887 let mut rules = SectionRules::builder();
888 rules.add(DIRECTORY_FOOTER.rule().required().no_args());
889 rules.add(BANDWIDTH_WEIGHTS.rule());
891 rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
892 rules.build()
893});
894
895impl ProtoStatus {
896 fn from_section(
898 sec: &Section<'_, NetstatusKwd>,
899 recommend_token: NetstatusKwd,
900 required_token: NetstatusKwd,
901 ) -> Result<ProtoStatus> {
902 fn parse(t: Option<&Item<'_, NetstatusKwd>>) -> Result<Protocols> {
904 if let Some(item) = t {
905 item.args_as_str()
906 .parse::<Protocols>()
907 .map_err(|e| EK::BadArgument.at_pos(item.pos()).with_source(e))
908 } else {
909 Ok(Protocols::new())
910 }
911 }
912
913 let recommended = parse(sec.get(recommend_token))?;
914 let required = parse(sec.get(required_token))?;
915 Ok(ProtoStatus {
916 recommended,
917 required,
918 })
919 }
920
921 pub fn required_protocols(&self) -> &Protocols {
928 &self.required
929 }
930
931 pub fn recommended_protocols(&self) -> &Protocols {
936 &self.recommended
937 }
938}
939
940impl<T> std::str::FromStr for NetParams<T>
941where
942 T: std::str::FromStr,
943 T::Err: std::error::Error,
944{
945 type Err = Error;
946 fn from_str(s: &str) -> Result<Self> {
947 fn parse_pair<U>(p: &str) -> Result<(String, U)>
949 where
950 U: std::str::FromStr,
951 U::Err: std::error::Error,
952 {
953 let parts: Vec<_> = p.splitn(2, '=').collect();
954 if parts.len() != 2 {
955 return Err(EK::BadArgument
956 .at_pos(Pos::at(p))
957 .with_msg("Missing = in key=value list"));
958 }
959 let num = parts[1].parse::<U>().map_err(|e| {
960 EK::BadArgument
961 .at_pos(Pos::at(parts[1]))
962 .with_msg(e.to_string())
963 })?;
964 Ok((parts[0].to_string(), num))
965 }
966
967 let params = s
968 .split(' ')
969 .filter(|p| !p.is_empty())
970 .map(parse_pair)
971 .collect::<Result<HashMap<_, _>>>()?;
972 Ok(NetParams { params })
973 }
974}
975
976impl FromStr for SharedRandVal {
977 type Err = Error;
978 fn from_str(s: &str) -> Result<Self> {
979 let val: B64 = s.parse()?;
980 let val = SharedRandVal(val.into_array()?);
981 Ok(val)
982 }
983}
984impl Display for SharedRandVal {
985 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
986 Display::fmt(&B64::from(Vec::from(self.0)), f)
987 }
988}
989impl NormalItemArgument for SharedRandVal {}
990
991impl SharedRandStatus {
992 fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<Self> {
995 match item.kwd() {
996 NetstatusKwd::SHARED_RAND_PREVIOUS_VALUE | NetstatusKwd::SHARED_RAND_CURRENT_VALUE => {}
997 _ => {
998 return Err(Error::from(internal!(
999 "wrong keyword {:?} on shared-random value",
1000 item.kwd()
1001 ))
1002 .at_pos(item.pos()));
1003 }
1004 }
1005 let n_reveals: u8 = item.parse_arg(0)?;
1006 let value: SharedRandVal = item.parse_arg(1)?;
1007 let timestamp = item.parse_optional_arg::<Iso8601TimeNoSp>(2)?;
1009 Ok(SharedRandStatus {
1010 n_reveals,
1011 value,
1012 timestamp,
1013 })
1014 }
1015
1016 pub fn value(&self) -> &SharedRandVal {
1018 &self.value
1019 }
1020
1021 pub fn timestamp(&self) -> Option<std::time::SystemTime> {
1023 self.timestamp.map(|t| t.0)
1024 }
1025}
1026
1027impl DirSource {
1028 fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<Self> {
1030 if item.kwd() != NetstatusKwd::DIR_SOURCE {
1031 return Err(
1032 Error::from(internal!("Bad keyword {:?} on dir-source", item.kwd()))
1033 .at_pos(item.pos()),
1034 );
1035 }
1036 let nickname = item.required_arg(0)?.to_string();
1037 let identity = item.parse_arg::<Fingerprint>(1)?.into();
1038 let ip = item.parse_arg(3)?;
1039 let dir_port = item.parse_arg(4)?;
1040 let or_port = item.parse_arg(5)?;
1041
1042 Ok(DirSource {
1043 nickname,
1044 identity,
1045 ip,
1046 dir_port,
1047 or_port,
1048 })
1049 }
1050}
1051
1052impl ConsensusVoterInfo {
1053 fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result<ConsensusVoterInfo> {
1055 use NetstatusKwd::*;
1056 #[allow(clippy::unwrap_used)]
1059 let first = sec.first_item().unwrap();
1060 if first.kwd() != DIR_SOURCE {
1061 return Err(Error::from(internal!(
1062 "Wrong keyword {:?} at start of voter info",
1063 first.kwd()
1064 ))
1065 .at_pos(first.pos()));
1066 }
1067 let dir_source = DirSource::from_item(sec.required(DIR_SOURCE)?)?;
1068
1069 let contact = sec.required(CONTACT)?.args_as_str().to_string();
1070
1071 let vote_digest = sec.required(VOTE_DIGEST)?.parse_arg::<B16>(0)?.into();
1072
1073 Ok(ConsensusVoterInfo {
1074 dir_source,
1075 contact,
1076 vote_digest,
1077 })
1078 }
1079}
1080
1081impl std::str::FromStr for RelayFlags {
1082 type Err = void::Void;
1083 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
1084 Ok(match s {
1085 "Authority" => RelayFlags::AUTHORITY,
1086 "BadExit" => RelayFlags::BAD_EXIT,
1087 "Exit" => RelayFlags::EXIT,
1088 "Fast" => RelayFlags::FAST,
1089 "Guard" => RelayFlags::GUARD,
1090 "HSDir" => RelayFlags::HSDIR,
1091 "MiddleOnly" => RelayFlags::MIDDLE_ONLY,
1092 "NoEdConsensus" => RelayFlags::NO_ED_CONSENSUS,
1093 "Stable" => RelayFlags::STABLE,
1094 "StaleDesc" => RelayFlags::STALE_DESC,
1095 "Running" => RelayFlags::RUNNING,
1096 "Valid" => RelayFlags::VALID,
1097 "V2Dir" => RelayFlags::V2DIR,
1098 _ => RelayFlags::empty(),
1099 })
1100 }
1101}
1102
1103struct RelayFlagsParser<'s> {
1105 flags: RelayFlags,
1107
1108 prev: Option<&'s str>,
1112}
1113
1114impl<'s> RelayFlagsParser<'s> {
1115 fn new() -> Self {
1117 RelayFlagsParser {
1119 flags: RelayFlags::RUNNING | RelayFlags::VALID,
1120 prev: None,
1121 }
1122 }
1123 fn add(&mut self, arg: &'s str) -> StdResult<(), &'static str> {
1125 if let Some(prev) = self.prev {
1126 if prev >= arg {
1127 return Err("Flags out of order");
1129 }
1130 }
1131 let fl = arg.parse().void_unwrap();
1132 self.flags |= fl;
1133 self.prev = Some(arg);
1134 Ok(())
1135 }
1136 fn finish(self) -> RelayFlags {
1138 self.flags
1139 }
1140}
1141
1142impl RelayFlags {
1143 fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<RelayFlags> {
1145 if item.kwd() != NetstatusKwd::RS_S {
1146 return Err(
1147 Error::from(internal!("Wrong keyword {:?} for S line", item.kwd()))
1148 .at_pos(item.pos()),
1149 );
1150 }
1151 let mut flags = RelayFlagsParser::new();
1152
1153 for s in item.args() {
1154 flags
1155 .add(s)
1156 .map_err(|msg| EK::BadArgument.at_pos(item.pos()).with_msg(msg))?;
1157 }
1158
1159 Ok(flags.finish())
1160 }
1161}
1162
1163impl Default for RelayWeight {
1164 fn default() -> RelayWeight {
1165 RelayWeight::Unmeasured(0)
1166 }
1167}
1168
1169impl RelayWeight {
1170 fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<RelayWeight> {
1172 if item.kwd() != NetstatusKwd::RS_W {
1173 return Err(
1174 Error::from(internal!("Wrong keyword {:?} on W line", item.kwd()))
1175 .at_pos(item.pos()),
1176 );
1177 }
1178
1179 let params = item.args_as_str().parse()?;
1180
1181 Self::from_net_params(¶ms).map_err(|e| e.at_pos(item.pos()))
1182 }
1183
1184 fn from_net_params(params: &NetParams<u32>) -> Result<RelayWeight> {
1188 let bw = params.params.get("Bandwidth");
1189 let unmeas = params.params.get("Unmeasured");
1190
1191 let bw = match bw {
1192 None => return Ok(RelayWeight::Unmeasured(0)),
1193 Some(b) => *b,
1194 };
1195
1196 match unmeas {
1197 None | Some(0) => Ok(RelayWeight::Measured(bw)),
1198 Some(1) => Ok(RelayWeight::Unmeasured(bw)),
1199 _ => Err(EK::BadArgument.with_msg("unmeasured value")),
1200 }
1201 }
1202}
1203
1204#[cfg(feature = "parse2")]
1208mod parse2_impls {
1209 use super::*;
1210 use parse2::ArgumentError as AE;
1211 use parse2::ErrorProblem as EP;
1212 use parse2::{ArgumentStream, ItemArgumentParseable, ItemValueParseable};
1213 use std::result::Result;
1214
1215 impl ItemValueParseable for RelayWeight {
1216 fn from_unparsed(item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
1217 item.check_no_object()?;
1218 (|| {
1219 let params = item.args_copy().into_remaining().parse()?;
1220 Self::from_net_params(¶ms)
1221 })()
1222 .map_err(item.invalid_argument_handler("weights"))
1223 }
1224 }
1225
1226 impl ItemValueParseable for RelayFlags {
1227 fn from_unparsed(item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
1228 item.check_no_object()?;
1229 let mut flags = RelayFlagsParser::new();
1230 for arg in item.args_copy() {
1231 flags
1232 .add(arg)
1233 .map_err(item.invalid_argument_handler("flags"))?;
1234 }
1235 Ok(flags.finish())
1236 }
1237 }
1238
1239 impl ItemValueParseable for rs::Version {
1240 fn from_unparsed(mut item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
1241 item.check_no_object()?;
1242 item.args_mut()
1243 .into_remaining()
1244 .parse()
1245 .map_err(item.invalid_argument_handler("version"))
1246 }
1247 }
1248
1249 impl ItemArgumentParseable for IgnoredPublicationTimeSp {
1250 fn from_args(a: &mut ArgumentStream) -> Result<IgnoredPublicationTimeSp, AE> {
1251 let mut next_arg = || a.next().ok_or(AE::Missing);
1252 let _: &str = next_arg()?;
1253 let _: &str = next_arg()?;
1254 Ok(IgnoredPublicationTimeSp)
1255 }
1256 }
1257}
1258
1259impl Footer {
1260 fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result<Footer> {
1262 use NetstatusKwd::*;
1263 sec.required(DIRECTORY_FOOTER)?;
1264
1265 let weights = sec
1266 .maybe(BANDWIDTH_WEIGHTS)
1267 .args_as_str()
1268 .unwrap_or("")
1269 .parse()?;
1270
1271 Ok(Footer { weights })
1272 }
1273}
1274
1275enum SigCheckResult {
1277 Valid,
1279 Invalid,
1282 MissingCert,
1285}
1286
1287impl Signature {
1288 fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<Signature> {
1290 if item.kwd() != NetstatusKwd::DIRECTORY_SIGNATURE {
1291 return Err(Error::from(internal!(
1292 "Wrong keyword {:?} for directory signature",
1293 item.kwd()
1294 ))
1295 .at_pos(item.pos()));
1296 }
1297
1298 let (alg, id_fp, sk_fp) = if item.n_args() > 2 {
1299 (
1300 item.required_arg(0)?,
1301 item.required_arg(1)?,
1302 item.required_arg(2)?,
1303 )
1304 } else {
1305 ("sha1", item.required_arg(0)?, item.required_arg(1)?)
1306 };
1307
1308 let digestname = alg.to_string();
1309 let id_fingerprint = id_fp.parse::<Fingerprint>()?.into();
1310 let sk_fingerprint = sk_fp.parse::<Fingerprint>()?.into();
1311 let key_ids = AuthCertKeyIds {
1312 id_fingerprint,
1313 sk_fingerprint,
1314 };
1315 let signature = item.obj("SIGNATURE")?;
1316
1317 Ok(Signature {
1318 digestname,
1319 key_ids,
1320 signature,
1321 })
1322 }
1323
1324 fn matches_cert(&self, cert: &AuthCert) -> bool {
1327 cert.key_ids() == &self.key_ids
1328 }
1329
1330 fn find_cert<'a>(&self, certs: &'a [AuthCert]) -> Option<&'a AuthCert> {
1333 certs.iter().find(|&c| self.matches_cert(c))
1334 }
1335
1336 fn check_signature(&self, signed_digest: &[u8], certs: &[AuthCert]) -> SigCheckResult {
1340 match self.find_cert(certs) {
1341 None => SigCheckResult::MissingCert,
1342 Some(cert) => {
1343 let key = cert.signing_key();
1344 match key.verify(signed_digest, &self.signature[..]) {
1345 Ok(()) => SigCheckResult::Valid,
1346 Err(_) => SigCheckResult::Invalid,
1347 }
1348 }
1349 }
1350 }
1351}
1352
1353impl SignatureGroup {
1354 fn list_missing(&self, certs: &[AuthCert]) -> (usize, Vec<&Signature>) {
1361 let mut ok: HashSet<RsaIdentity> = HashSet::new();
1362 let mut missing = Vec::new();
1363 for sig in &self.signatures {
1364 let id_fingerprint = &sig.key_ids.id_fingerprint;
1365 if ok.contains(id_fingerprint) {
1366 continue;
1367 }
1368 if sig.find_cert(certs).is_some() {
1369 ok.insert(*id_fingerprint);
1370 continue;
1371 }
1372
1373 missing.push(sig);
1374 }
1375 (ok.len(), missing)
1376 }
1377
1378 fn could_validate(&self, authorities: &[&RsaIdentity]) -> bool {
1382 let mut signed_by: HashSet<RsaIdentity> = HashSet::new();
1383 for sig in &self.signatures {
1384 let id_fp = &sig.key_ids.id_fingerprint;
1385 if signed_by.contains(id_fp) {
1386 continue;
1388 }
1389 if authorities.contains(&id_fp) {
1390 signed_by.insert(*id_fp);
1391 }
1392 }
1393
1394 signed_by.len() > (authorities.len() / 2)
1395 }
1396
1397 fn validate(&self, n_authorities: u16, certs: &[AuthCert]) -> bool {
1404 let mut ok: HashSet<RsaIdentity> = HashSet::new();
1408
1409 for sig in &self.signatures {
1410 let id_fingerprint = &sig.key_ids.id_fingerprint;
1411 if ok.contains(id_fingerprint) {
1412 continue;
1415 }
1416
1417 let d: Option<&[u8]> = match sig.digestname.as_ref() {
1418 "sha256" => self.sha256.as_ref().map(|a| &a[..]),
1419 "sha1" => self.sha1.as_ref().map(|a| &a[..]),
1420 _ => None, };
1422 if d.is_none() {
1423 continue;
1426 }
1427
1428 #[allow(clippy::unwrap_used)]
1430 match sig.check_signature(d.as_ref().unwrap(), certs) {
1431 SigCheckResult::Valid => {
1432 ok.insert(*id_fingerprint);
1433 }
1434 _ => continue,
1435 }
1436 }
1437
1438 ok.len() > (n_authorities / 2) as usize
1439 }
1440}
1441
1442#[cfg(test)]
1443mod test {
1444 #![allow(clippy::bool_assert_comparison)]
1446 #![allow(clippy::clone_on_copy)]
1447 #![allow(clippy::dbg_macro)]
1448 #![allow(clippy::mixed_attributes_style)]
1449 #![allow(clippy::print_stderr)]
1450 #![allow(clippy::print_stdout)]
1451 #![allow(clippy::single_char_pattern)]
1452 #![allow(clippy::unwrap_used)]
1453 #![allow(clippy::unchecked_duration_subtraction)]
1454 #![allow(clippy::useless_vec)]
1455 #![allow(clippy::needless_pass_by_value)]
1456 use super::*;
1458 use hex_literal::hex;
1459 #[cfg(all(feature = "ns-vote", feature = "parse2"))]
1460 use {
1461 crate::parse2::{NetdocSigned as _, parse_netdoc},
1462 std::fs,
1463 };
1464
1465 const CERTS: &str = include_str!("../../testdata/authcerts2.txt");
1466 const CONSENSUS: &str = include_str!("../../testdata/mdconsensus1.txt");
1467
1468 #[cfg(feature = "plain-consensus")]
1469 const PLAIN_CERTS: &str = include_str!("../../testdata2/cached-certs");
1470 #[cfg(feature = "plain-consensus")]
1471 const PLAIN_CONSENSUS: &str = include_str!("../../testdata2/cached-consensus");
1472
1473 fn read_bad(fname: &str) -> String {
1474 use std::fs;
1475 use std::path::PathBuf;
1476 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1477 path.push("testdata");
1478 path.push("bad-mdconsensus");
1479 path.push(fname);
1480
1481 fs::read_to_string(path).unwrap()
1482 }
1483
1484 #[test]
1485 fn parse_and_validate_md() -> Result<()> {
1486 use std::net::SocketAddr;
1487 use tor_checkable::{SelfSigned, Timebound};
1488 let mut certs = Vec::new();
1489 for cert in AuthCert::parse_multiple(CERTS)? {
1490 let cert = cert?.check_signature()?.dangerously_assume_timely();
1491 certs.push(cert);
1492 }
1493 let auth_ids: Vec<_> = certs.iter().map(|c| &c.key_ids().id_fingerprint).collect();
1494
1495 assert_eq!(certs.len(), 3);
1496
1497 let (_, _, consensus) = MdConsensus::parse(CONSENSUS)?;
1498 let consensus = consensus.dangerously_assume_timely().set_n_authorities(3);
1499
1500 assert!(consensus.authorities_are_correct(&auth_ids));
1502 assert!(consensus.authorities_are_correct(&auth_ids[0..1]));
1504 {
1505 let bad_auth_id = (*b"xxxxxxxxxxxxxxxxxxxx").into();
1508 assert!(!consensus.authorities_are_correct(&[&bad_auth_id]));
1509 }
1510
1511 let missing = consensus.key_is_correct(&[]).err().unwrap();
1512 assert_eq!(3, missing.len());
1513 assert!(consensus.key_is_correct(&certs).is_ok());
1514 let missing = consensus.key_is_correct(&certs[0..1]).err().unwrap();
1515 assert_eq!(2, missing.len());
1516
1517 let same_three_times = vec![certs[0].clone(), certs[0].clone(), certs[0].clone()];
1519 let missing = consensus.key_is_correct(&same_three_times).err().unwrap();
1520
1521 assert_eq!(2, missing.len());
1522 assert!(consensus.is_well_signed(&same_three_times).is_err());
1523
1524 assert!(consensus.key_is_correct(&certs).is_ok());
1525 let consensus = consensus.check_signature(&certs)?;
1526
1527 assert_eq!(6, consensus.relays().len());
1528 let r0 = &consensus.relays()[0];
1529 assert_eq!(
1530 r0.md_digest(),
1531 &hex!("73dabe0a0468f4f7a67810a18d11e36731bb1d2ec3634db459100609f3b3f535")
1532 );
1533 assert_eq!(
1534 r0.rsa_identity().as_bytes(),
1535 &hex!("0a3057af2910415794d8ea430309d9ac5f5d524b")
1536 );
1537 assert!(!r0.weight().is_measured());
1538 assert!(!r0.weight().is_nonzero());
1539 let pv = &r0.protovers();
1540 assert!(pv.supports_subver("HSDir", 2));
1541 assert!(!pv.supports_subver("HSDir", 3));
1542 let ip4 = "127.0.0.1:5002".parse::<SocketAddr>().unwrap();
1543 let ip6 = "[::1]:5002".parse::<SocketAddr>().unwrap();
1544 assert!(r0.addrs().any(|a| a == ip4));
1545 assert!(r0.addrs().any(|a| a == ip6));
1546
1547 Ok(())
1548 }
1549
1550 #[test]
1551 #[cfg(feature = "plain-consensus")]
1552 fn parse_and_validate_ns() -> Result<()> {
1553 use tor_checkable::{SelfSigned, Timebound};
1554 let mut certs = Vec::new();
1555 for cert in AuthCert::parse_multiple(PLAIN_CERTS)? {
1556 let cert = cert?.check_signature()?.dangerously_assume_timely();
1557 certs.push(cert);
1558 }
1559 let auth_ids: Vec<_> = certs.iter().map(|c| &c.key_ids().id_fingerprint).collect();
1560 assert_eq!(certs.len(), 4);
1561
1562 let (_, _, consensus) = PlainConsensus::parse(PLAIN_CONSENSUS)?;
1563 let consensus = consensus.dangerously_assume_timely().set_n_authorities(3);
1564 assert!(consensus.authorities_are_correct(&auth_ids));
1566 assert!(consensus.authorities_are_correct(&auth_ids[0..1]));
1568
1569 assert!(consensus.key_is_correct(&certs).is_ok());
1570
1571 let _consensus = consensus.check_signature(&certs)?;
1572
1573 Ok(())
1574 }
1575
1576 #[test]
1577 #[cfg(all(feature = "ns-vote", feature = "parse2"))]
1578 fn parse2_vote() -> anyhow::Result<()> {
1579 let file = "testdata2/v3-status-votes--1";
1580 let text = fs::read_to_string(file)?;
1581
1582 use crate::parse2::poc::netstatus::NetworkStatusSignedVote;
1584
1585 let doc: NetworkStatusSignedVote = parse_netdoc(&text, file)?;
1586
1587 println!("{doc:?}");
1588 println!("{:#?}", doc.inspect_unverified().0.r[0]);
1589
1590 Ok(())
1591 }
1592
1593 #[test]
1594 fn test_bad() {
1595 use crate::Pos;
1596 fn check(fname: &str, e: &Error) {
1597 let content = read_bad(fname);
1598 let res = MdConsensus::parse(&content);
1599 assert!(res.is_err());
1600 assert_eq!(&res.err().unwrap(), e);
1601 }
1602
1603 check(
1604 "bad-flags",
1605 &EK::BadArgument
1606 .at_pos(Pos::from_line(27, 1))
1607 .with_msg("Flags out of order"),
1608 );
1609 check(
1610 "bad-md-digest",
1611 &EK::BadArgument
1612 .at_pos(Pos::from_line(40, 3))
1613 .with_msg("Invalid base64"),
1614 );
1615 check(
1616 "bad-weight",
1617 &EK::BadArgument
1618 .at_pos(Pos::from_line(67, 141))
1619 .with_msg("invalid digit found in string"),
1620 );
1621 check(
1622 "bad-weights",
1623 &EK::BadArgument
1624 .at_pos(Pos::from_line(51, 13))
1625 .with_msg("invalid digit found in string"),
1626 );
1627 check(
1628 "wrong-order",
1629 &EK::WrongSortOrder.at_pos(Pos::from_line(52, 1)),
1630 );
1631 check(
1632 "wrong-start",
1633 &EK::UnexpectedToken
1634 .with_msg("vote-status")
1635 .at_pos(Pos::from_line(1, 1)),
1636 );
1637 check("wrong-version", &EK::BadDocumentVersion.with_msg("10"));
1638 }
1639
1640 fn gettok(s: &str) -> Result<Item<'_, NetstatusKwd>> {
1641 let mut reader = NetDocReader::new(s)?;
1642 let tok = reader.next().unwrap();
1643 assert!(reader.next().is_none());
1644 tok
1645 }
1646
1647 #[test]
1648 fn test_weight() {
1649 let w = gettok("w Unmeasured=1 Bandwidth=6\n").unwrap();
1650 let w = RelayWeight::from_item(&w).unwrap();
1651 assert!(!w.is_measured());
1652 assert!(w.is_nonzero());
1653
1654 let w = gettok("w Bandwidth=10\n").unwrap();
1655 let w = RelayWeight::from_item(&w).unwrap();
1656 assert!(w.is_measured());
1657 assert!(w.is_nonzero());
1658
1659 let w = RelayWeight::default();
1660 assert!(!w.is_measured());
1661 assert!(!w.is_nonzero());
1662
1663 let w = gettok("w Mustelid=66 Cheato=7 Unmeasured=1\n").unwrap();
1664 let w = RelayWeight::from_item(&w).unwrap();
1665 assert!(!w.is_measured());
1666 assert!(!w.is_nonzero());
1667
1668 let w = gettok("r foo\n").unwrap();
1669 let w = RelayWeight::from_item(&w);
1670 assert!(w.is_err());
1671
1672 let w = gettok("r Bandwidth=6 Unmeasured=Frog\n").unwrap();
1673 let w = RelayWeight::from_item(&w);
1674 assert!(w.is_err());
1675
1676 let w = gettok("r Bandwidth=6 Unmeasured=3\n").unwrap();
1677 let w = RelayWeight::from_item(&w);
1678 assert!(w.is_err());
1679 }
1680
1681 #[test]
1682 fn test_netparam() {
1683 let p = "Hello=600 Goodbye=5 Fred=7"
1684 .parse::<NetParams<u32>>()
1685 .unwrap();
1686 assert_eq!(p.get("Hello"), Some(&600_u32));
1687
1688 let p = "Hello=Goodbye=5 Fred=7".parse::<NetParams<u32>>();
1689 assert!(p.is_err());
1690
1691 let p = "Hello=Goodbye Fred=7".parse::<NetParams<u32>>();
1692 assert!(p.is_err());
1693 }
1694
1695 #[test]
1696 fn test_sharedrand() {
1697 let sr =
1698 gettok("shared-rand-previous-value 9 5LodY4yWxFhTKtxpV9wAgNA9N8flhUCH0NqQv1/05y4\n")
1699 .unwrap();
1700 let sr = SharedRandStatus::from_item(&sr).unwrap();
1701
1702 assert_eq!(sr.n_reveals, 9);
1703 assert_eq!(
1704 sr.value.0,
1705 hex!("e4ba1d638c96c458532adc6957dc0080d03d37c7e5854087d0da90bf5ff4e72e")
1706 );
1707 assert!(sr.timestamp.is_none());
1708
1709 let sr2 = gettok(
1710 "shared-rand-current-value 9 \
1711 5LodY4yWxFhTKtxpV9wAgNA9N8flhUCH0NqQv1/05y4 2022-01-20T12:34:56\n",
1712 )
1713 .unwrap();
1714 let sr2 = SharedRandStatus::from_item(&sr2).unwrap();
1715 assert_eq!(sr2.n_reveals, sr.n_reveals);
1716 assert_eq!(sr2.value.0, sr.value.0);
1717 assert_eq!(
1718 sr2.timestamp.unwrap().0,
1719 humantime::parse_rfc3339("2022-01-20T12:34:56Z").unwrap()
1720 );
1721
1722 let sr = gettok("foo bar\n").unwrap();
1723 let sr = SharedRandStatus::from_item(&sr);
1724 assert!(sr.is_err());
1725 }
1726
1727 #[test]
1728 fn test_protostatus() {
1729 let my_protocols: Protocols = "Link=7 Cons=1-5 Desc=3-10".parse().unwrap();
1730
1731 let outcome = ProtoStatus {
1732 recommended: "Link=7".parse().unwrap(),
1733 required: "Desc=5".parse().unwrap(),
1734 }
1735 .check_protocols(&my_protocols);
1736 assert!(outcome.is_ok());
1737
1738 let outcome = ProtoStatus {
1739 recommended: "Microdesc=4 Link=7".parse().unwrap(),
1740 required: "Desc=5".parse().unwrap(),
1741 }
1742 .check_protocols(&my_protocols);
1743 assert_eq!(
1744 outcome,
1745 Err(ProtocolSupportError::MissingRecommended(
1746 "Microdesc=4".parse().unwrap()
1747 ))
1748 );
1749
1750 let outcome = ProtoStatus {
1751 recommended: "Microdesc=4 Link=7".parse().unwrap(),
1752 required: "Desc=5 Cons=5-12 Wombat=15".parse().unwrap(),
1753 }
1754 .check_protocols(&my_protocols);
1755 assert_eq!(
1756 outcome,
1757 Err(ProtocolSupportError::MissingRequired(
1758 "Cons=6-12 Wombat=15".parse().unwrap()
1759 ))
1760 );
1761 }
1762
1763 #[test]
1764 fn serialize_protostatus() {
1765 let ps = ProtoStatuses {
1766 client: ProtoStatus {
1767 recommended: "Link=1-5 LinkAuth=2-5".parse().unwrap(),
1768 required: "Link=5 LinkAuth=3".parse().unwrap(),
1769 },
1770 relay: ProtoStatus {
1771 recommended: "Wombat=20-30 Knish=20-30".parse().unwrap(),
1772 required: "Wombat=20-22 Knish=25-27".parse().unwrap(),
1773 },
1774 };
1775 let json = serde_json::to_string(&ps).unwrap();
1776 let ps2 = serde_json::from_str(json.as_str()).unwrap();
1777 assert_eq!(ps, ps2);
1778
1779 let ps3: ProtoStatuses = serde_json::from_str(
1780 r#"{
1781 "client":{
1782 "required":"Link=5 LinkAuth=3",
1783 "recommended":"Link=1-5 LinkAuth=2-5"
1784 },
1785 "relay":{
1786 "required":"Wombat=20-22 Knish=25-27",
1787 "recommended":"Wombat=20-30 Knish=20-30"
1788 }
1789 }"#,
1790 )
1791 .unwrap();
1792 assert_eq!(ps, ps3);
1793 }
1794}