tor_netdoc/doc/netstatus/dir_source.rs
1//! `dir-source` items, including the mutant `-legacy` version
2//!
3//! A `dir-source` line is normally an authority entry.
4//! But it might also be a "superseded authority key entry".
5//! That has a "nickname" ending in `-legacy` and appears only in consensuses.
6//! (Note that `-legacy` is not legal syntax for a nickname.)
7//!
8//! <https://spec.torproject.org/dir-spec/consensus-formats.html#item:dir-source>
9//!
10//! This module will also handle the decoding of consensus authority sections,
11//! which are fiddly because they can contain a mixture of things.
12//!
13//! <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority>
14
15use super::*;
16use std::result::Result;
17
18/// Keyword, which we need to recapitulate because of all the ad-hoc parsing
19const DIR_SOURCE_KEYWORD: &str = "dir-source";
20
21/// Nickname suffix for superseded authority key entries
22const SUPERSEDED_SUFFIX: &str = "-legacy";
23
24define_derive_deftly! {
25 /// Derive `SupersededAuthorityKey` and its impls
26 ///
27 /// This includes `SomeDirSource`, a parsing helper type.
28 ///
29 /// This macro exists to avoid recapitulating the `dir-source` line field list many times.
30 /// (The `ItemValueParseable` derive doesn't support `#[deftly(netdoc(flatten))]` for args.)
31 SupersededAuthorityKey for struct:
32
33 ${defcond F_NORMAL not(approx_equal($fname, nickname))}
34
35 ${define DEFINE_NORMAL_FIELDS { $(
36 ${when F_NORMAL}
37 ${fattrs !_no_such_attr} // derive-deftly has no way to say all attrs even deftly
38 $fname: $ftype,
39 ) }}
40
41 /// A `dir-source` line that *is* a "superseded authority key entry"
42 ///
43 /// Construct using [`from_dir_source`](SupersededAuthorityKey::from_dir_source).
44 ///
45 // The fields are private and we don't use Constructor because otherwise a caller
46 // could create a SupersededAuthorityKey with mismatched `real_nickname` and
47 // `raw_nickname_string` which would encode surprisingly.
48 //
49 /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:dir-source>
50 #[derive(Debug, Clone, Deftly, amplify::Getters)]
51 #[derive_deftly(ItemValueEncodable)]
52 #[derive_deftly_adhoc] // ignore deftly attrs directed at Constructor
53 pub struct SupersededAuthorityKey {
54 /// Real nickname for this authority, not including the `-legacy`
55 #[deftly(netdoc(skip))]
56 real_nickname: Nickname,
57
58 /// The raw nickname, including "-legacy"
59 // We want #[getter(as_deref)] but it doesn't exist. We open-code it, below.
60 #[getter(skip)]
61 raw_nickname_string: String,
62
63 $DEFINE_NORMAL_FIELDS
64 }
65
66 impl SupersededAuthorityKey {
67 /// The raw nickname, including "-legacy"
68 pub fn raw_nickname_string(&self) -> &str {
69 &self.raw_nickname_string
70 }
71
72 /// Make a superseded authority key entry from the data in a `DirSource`
73 ///
74 /// `ds.nickname` is the real nickname (without `-legacy`).
75 // We don't need to check this because `-` is not allowed in a Nickname.
76 ///
77 /// `ds.fingerprint` is the *superseded* key.
78 pub fn from_dir_source(ds: DirSource) -> Self {
79 SupersededAuthorityKey {
80 raw_nickname_string: format!("{}{SUPERSEDED_SUFFIX}", ds.nickname),
81 real_nickname: ds.nickname,
82 $( ${when F_NORMAL} $fname: ds.$fname, )
83 }
84 }
85 }
86
87 /// A `dir-source` line with unchecked nickname
88 ///
89 /// Used for parsing a superseded authority key entry.
90 ///
91 /// This is not quite the same as `DirSource`, because `DirSource` has a `Nickname`
92 /// but the superseded entries' `-legacy` values are not valid nicknames.
93 ///
94 /// We can't derive `ItemValueParseable` for `SupersededAuthorityKey`,
95 /// because we can't parse the `real_nickname` field.
96 /// Instead we derive `ItemValueParseable` on this and convert it ad-hoc
97 /// in `ConsensusAuthoritySection`'s parser.
98 #[derive(Debug, Clone, Deftly)]
99 #[derive_deftly(ItemValueParseable)]
100 #[derive_deftly_adhoc] // ignore deftly attrs directed at Constructor
101 struct RawDirSource {
102 /// Raw nickname, as parsed
103 raw_nickname_string: String,
104
105 $DEFINE_NORMAL_FIELDS
106 }
107
108 impl RawDirSource {
109 /// Convert into the public representation.
110 fn into_superseded(self) -> Result<SupersededAuthorityKey, ErrorProblem> {
111 let RawDirSource { raw_nickname_string, .. } = self;
112 let real_nickname = raw_nickname_string
113 .strip_suffix(SUPERSEDED_SUFFIX)
114 .ok_or(ErrorProblem::Internal("RawDirSource::into_superseded for non `-legacy`"))?
115 .parse()
116 .map_err(|_: InvalidNickname| ErrorProblem::InvalidArgument {
117 field: "invalid nickname even after stripping `-legacy`",
118 column: DIR_SOURCE_KEYWORD.len() + 1, // urgh
119 })?;
120 Ok(SupersededAuthorityKey {
121 real_nickname,
122 raw_nickname_string,
123 $( ${when F_NORMAL} $fname: self.$fname, )
124 })
125 }
126 }
127}
128
129/// Description of an authority's identity and address.
130///
131/// Corresponds to a dir-source line which is *not* a "superseded authority key entry".
132/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:dir-source>
133#[derive(Debug, Clone, Deftly)]
134#[derive_deftly(Constructor, ItemValueParseable, ItemValueEncodable)]
135#[derive_deftly(SupersededAuthorityKey)]
136#[allow(clippy::exhaustive_structs)]
137pub struct DirSource {
138 /// human-readable nickname for this authority.
139 #[deftly(constructor)]
140 pub nickname: Nickname,
141
142 /// Fingerprint for the _authority_ identity key of this
143 /// authority.
144 ///
145 /// This is the same key as the one that signs the authority's
146 /// certificates.
147 #[deftly(constructor)]
148 pub identity: Fingerprint,
149
150 /// IP address for the authority
151 #[deftly(constructor)]
152 pub hostname: InternetHost,
153
154 /// IP address for the authority
155 #[deftly(constructor(default = { net::Ipv6Addr::UNSPECIFIED.into() }))]
156 pub ip: net::IpAddr,
157
158 /// HTTP directory port for this authority
159 pub dir_port: u16,
160
161 /// OR port for this authority.
162 pub or_port: u16,
163
164 #[doc(hidden)]
165 #[deftly(netdoc(skip))]
166 pub __non_exhaustive: (),
167}
168
169/// Authority section as found in a consensus
170///
171/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority>
172///
173/// Note that though you can construct one with an empty `authorities` field,
174/// that will generate a `Bug` when you encode it.
175///
176/// For votes, see [`VoteAuthoritySection`]
177#[derive(Debug, Clone, Deftly)]
178#[derive_deftly(Constructor)]
179#[allow(clippy::exhaustive_structs)]
180pub struct ConsensusAuthoritySection {
181 /// Authority entries
182 ///
183 /// Always nonempty when parsed; must be nonempty or encoding will fail with `Bug`.
184 //
185 // If the user wants to provide an empty vec, at least force them to write it out.
186 #[deftly(constructor)]
187 pub authorities: Vec<ConsensusAuthorityEntry>,
188
189 /// Superseded authority key entries
190 pub superseded_keys: Vec<SupersededAuthorityKey>,
191
192 #[doc(hidden)]
193 pub __non_exhaustive: (),
194}
195
196impl NetdocEncodable for ConsensusAuthoritySection {
197 fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
198 // bind all fields so that if any are added we remember to encode them
199 let ConsensusAuthoritySection {
200 authorities,
201 superseded_keys,
202 __non_exhaustive,
203 } = self;
204
205 if authorities.is_empty() {
206 return Err(internal!("tried to encode a consensus with 0 authorities"));
207 }
208 for a in authorities {
209 a.encode_unsigned(out)?;
210 }
211 for s in superseded_keys {
212 let out = out.item(DIR_SOURCE_KEYWORD);
213 s.write_item_value_onto(out)?;
214 }
215 Ok(())
216 }
217}
218
219impl NetdocParseable for ConsensusAuthoritySection {
220 fn doctype_for_error() -> &'static str {
221 "consensus.authorities"
222 }
223
224 fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
225 ConsensusAuthorityEntry::is_intro_item_keyword(kw)
226 }
227
228 fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
229 ConsensusAuthorityEntry::is_structural_keyword(kw)
230 }
231
232 fn from_items(input: &mut ItemStream<'_>, stop_at: stop_at!()) -> Result<Self, ErrorProblem> {
233 let mut accum = ConsensusAuthoritySection {
234 authorities: vec![],
235 superseded_keys: vec![],
236 __non_exhaustive: (),
237 };
238
239 while let Some(peeked) = input.peek_keyword()? {
240 if !Self::is_intro_item_keyword(peeked) {
241 break;
242 }
243
244 // Well, this is pretty terrible
245 let rest = &input.whole_input()[input.byte_position()..];
246 let line = rest.split_once('\n').map(|(l, _)| l).unwrap_or(rest);
247 let mut line = line.split_ascii_whitespace();
248 assert_eq!(line.next(), Some(DIR_SOURCE_KEYWORD));
249 let raw_nickname = line
250 .next()
251 .ok_or(ErrorProblem::MissingArgument { field: "nickname" })?;
252
253 if raw_nickname.ends_with(SUPERSEDED_SUFFIX) {
254 let item = input.next().expect("peeked")?;
255 let s = RawDirSource::from_unparsed(item)?.into_superseded()?;
256 accum.superseded_keys.push(s);
257 } else {
258 let a = ConsensusAuthorityEntry::from_items(input, stop_at)?;
259 accum.authorities.push(a);
260 }
261 }
262
263 if accum.authorities.is_empty() {
264 return Err(ErrorProblem::MissingItem {
265 keyword: DIR_SOURCE_KEYWORD,
266 });
267 }
268
269 Ok(accum)
270 }
271}