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

            
15
use super::*;
16
use std::result::Result;
17

            
18
/// Keyword, which we need to recapitulate because of all the ad-hoc parsing
19
const DIR_SOURCE_KEYWORD: &str = "dir-source";
20

            
21
/// Nickname suffix for superseded authority key entries
22
const SUPERSEDED_SUFFIX: &str = "-legacy";
23

            
24
define_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
            let id = ds.identity;
80
            SupersededAuthorityKey::from_dir_source_and_key(ds, id)
81
        }
82

            
83
        /// Make a superseded authority key entry from a `DirSource` and a key fingerprint
84
        ///
85
        /// As with `from_dir_source`,
86
        /// `ds.nickname` is the real nickname (without `-legacy`).
87
        ///
88
        /// `ds.fingerprint` is ignored in favour of `superseded_key`.
89
        pub fn from_dir_source_and_key(ds: DirSource, superseded_key: Fingerprint) -> Self {
90
            SupersededAuthorityKey {
91
                raw_nickname_string: format!("{}{SUPERSEDED_SUFFIX}", ds.nickname),
92
                real_nickname: ds.nickname,
93
                identity: superseded_key,
94
             $(
95
                ${when F_NORMAL}
96
                ${when not(approx_equal($fname, identity))}
97
                $fname: ds.$fname,
98
             )
99
            }
100
        }
101
    }
102

            
103
    /// A `dir-source` line with unchecked nickname
104
    ///
105
    /// Used for parsing a superseded authority key entry.
106
    ///
107
    /// This is not quite the same as `DirSource`, because `DirSource` has a `Nickname`
108
    /// but the superseded entries' `-legacy` values are not valid nicknames.
109
    ///
110
    /// We can't derive `ItemValueParseable` for `SupersededAuthorityKey`,
111
    /// because we can't parse the `real_nickname` field.
112
    /// Instead we derive `ItemValueParseable` on this and convert it ad-hoc
113
    /// in `ConsensusAuthoritySection`'s parser.
114
    #[derive(Debug, Clone, Deftly)]
115
    #[derive_deftly(ItemValueParseable)]
116
    #[derive_deftly_adhoc] // ignore deftly attrs directed at Constructor
117
    struct RawDirSource {
118
        /// Raw nickname, as parsed
119
        raw_nickname_string: String,
120

            
121
        $DEFINE_NORMAL_FIELDS
122
    }
123

            
124
    impl RawDirSource {
125
        /// Convert into the public representation.
126
        fn into_superseded(self) -> Result<SupersededAuthorityKey, ErrorProblem> {
127
            let RawDirSource { raw_nickname_string, .. } = self;
128
            let real_nickname = raw_nickname_string
129
                .strip_suffix(SUPERSEDED_SUFFIX)
130
                .ok_or(ErrorProblem::Internal("RawDirSource::into_superseded for non `-legacy`"))?
131
                .parse()
132
                .map_err(|_: InvalidNickname| ErrorProblem::InvalidArgument {
133
                    field: "invalid nickname even after stripping `-legacy`",
134
                    column: DIR_SOURCE_KEYWORD.len() + 1, // urgh
135
                })?;
136
            Ok(SupersededAuthorityKey {
137
                real_nickname,
138
                raw_nickname_string,
139
                $( ${when F_NORMAL} $fname: self.$fname, )
140
            })
141
        }
142
    }
143
}
144

            
145
/// Description of an authority's identity and address.
146
///
147
/// Corresponds to a dir-source line which is *not* a "superseded authority key entry".
148
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:dir-source>
149
#[derive(Debug, Clone, Deftly)]
150
#[derive_deftly(Constructor, ItemValueParseable, ItemValueEncodable)]
151
#[derive_deftly(SupersededAuthorityKey)]
152
#[allow(clippy::exhaustive_structs)]
153
pub struct DirSource {
154
    /// human-readable nickname for this authority.
155
    #[deftly(constructor)]
156
    pub nickname: Nickname,
157

            
158
    /// Fingerprint for the _authority_ identity key of this
159
    /// authority.
160
    ///
161
    /// This is the same key as the one that signs the authority's
162
    /// certificates.
163
    #[deftly(constructor)]
164
    pub identity: Fingerprint,
165

            
166
    /// IP address for the authority
167
    #[deftly(constructor)]
168
    pub hostname: InternetHost,
169

            
170
    /// IP address for the authority
171
    #[deftly(constructor(default = { net::Ipv6Addr::UNSPECIFIED.into() }))]
172
    pub ip: net::IpAddr,
173

            
174
    /// HTTP directory port for this authority
175
    pub dir_port: u16,
176

            
177
    /// OR port for this authority.
178
    pub or_port: u16,
179

            
180
    #[doc(hidden)]
181
    #[deftly(netdoc(skip))]
182
    pub __non_exhaustive: (),
183
}
184

            
185
/// Authority section as found in a consensus
186
///
187
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority>
188
///
189
/// Note that though you can construct one with an empty `authorities` field,
190
/// that will generate a `Bug` when you encode it.
191
///
192
/// For votes, see [`VoteAuthoritySection`]
193
#[derive(Debug, Clone, Deftly)]
194
#[derive_deftly(Constructor)]
195
#[allow(clippy::exhaustive_structs)]
196
pub struct ConsensusAuthoritySection {
197
    /// Authority entries
198
    ///
199
    /// Always nonempty when parsed; must be nonempty or encoding will fail with `Bug`.
200
    //
201
    // If the user wants to provide an empty vec, at least force them to write it out.
202
    #[deftly(constructor)]
203
    pub authorities: Vec<ConsensusAuthorityEntry>,
204

            
205
    /// Superseded authority key entries
206
    pub superseded_keys: Vec<SupersededAuthorityKey>,
207

            
208
    #[doc(hidden)]
209
    pub __non_exhaustive: (),
210
}
211

            
212
impl NetdocEncodable for ConsensusAuthoritySection {
213
4
    fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
214
        // bind all fields so that if any are added we remember to encode them
215
        let ConsensusAuthoritySection {
216
4
            authorities,
217
4
            superseded_keys,
218
4
            __non_exhaustive,
219
4
        } = self;
220

            
221
4
        if authorities.is_empty() {
222
            return Err(internal!("tried to encode a consensus with 0 authorities"));
223
4
        }
224
26
        for a in authorities {
225
26
            a.encode_unsigned(out)?;
226
        }
227
4
        for s in superseded_keys {
228
            let out = out.item(DIR_SOURCE_KEYWORD);
229
            s.write_item_value_onto(out)?;
230
        }
231
4
        Ok(())
232
4
    }
233
}
234

            
235
impl NetdocParseable for ConsensusAuthoritySection {
236
31596
    fn doctype_for_error() -> &'static str {
237
31596
        "consensus.authorities"
238
31596
    }
239

            
240
3824142
    fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
241
3824142
        ConsensusAuthorityEntry::is_intro_item_keyword(kw)
242
3824142
    }
243

            
244
    fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
245
        ConsensusAuthorityEntry::is_structural_keyword(kw)
246
    }
247

            
248
31596
    fn from_items(input: &mut ItemStream<'_>, stop_at: stop_at!()) -> Result<Self, ErrorProblem> {
249
31596
        let mut accum = ConsensusAuthoritySection {
250
31596
            authorities: vec![],
251
31596
            superseded_keys: vec![],
252
31596
            __non_exhaustive: (),
253
31596
        };
254

            
255
158265
        while let Some(peeked) = input.peek_keyword()? {
256
158265
            if !Self::is_intro_item_keyword(peeked) {
257
31596
                break;
258
126669
            }
259

            
260
            // Well, this is pretty terrible
261
126669
            let rest = input
262
126669
                .whole_input()
263
126669
                .get(input.byte_position()..)
264
126669
                .ok_or(ErrorProblem::Internal("chopped utf8"))?;
265
126669
            let line = rest.split_once('\n').map(|(l, _)| l).unwrap_or(rest);
266
126669
            let mut line = line.split_ascii_whitespace();
267
126669
            assert_eq!(line.next(), Some(DIR_SOURCE_KEYWORD));
268
126669
            let raw_nickname = line
269
126669
                .next()
270
126669
                .ok_or(ErrorProblem::MissingArgument { field: "nickname" })?;
271

            
272
126669
            if raw_nickname.ends_with(SUPERSEDED_SUFFIX) {
273
                let item = input.next().expect("peeked")?;
274
                let s = RawDirSource::from_unparsed(item)?.into_superseded()?;
275
                accum.superseded_keys.push(s);
276
            } else {
277
126669
                let a = ConsensusAuthorityEntry::from_items(input, stop_at)?;
278
126669
                accum.authorities.push(a);
279
            }
280
        }
281

            
282
31596
        if accum.authorities.is_empty() {
283
            return Err(ErrorProblem::MissingItem {
284
                keyword: DIR_SOURCE_KEYWORD,
285
            });
286
31596
        }
287

            
288
31596
        Ok(accum)
289
31596
    }
290
}