Skip to main content

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}