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
            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)]
137
pub 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)]
180
pub 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

            
196
impl 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

            
219
impl NetdocParseable for ConsensusAuthoritySection {
220
220
    fn doctype_for_error() -> &'static str {
221
220
        "consensus.authorities"
222
220
    }
223

            
224
22084
    fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
225
22084
        ConsensusAuthorityEntry::is_intro_item_keyword(kw)
226
22084
    }
227

            
228
    fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
229
        ConsensusAuthorityEntry::is_structural_keyword(kw)
230
    }
231

            
232
12
    fn from_items(input: &mut ItemStream<'_>, stop_at: stop_at!()) -> Result<Self, ErrorProblem> {
233
12
        let mut accum = ConsensusAuthoritySection {
234
12
            authorities: vec![],
235
12
            superseded_keys: vec![],
236
12
            __non_exhaustive: (),
237
12
        };
238

            
239
100
        while let Some(peeked) = input.peek_keyword()? {
240
100
            if !Self::is_intro_item_keyword(peeked) {
241
12
                break;
242
88
            }
243

            
244
            // Well, this is pretty terrible
245
88
            let rest = &input.whole_input()[input.byte_position()..];
246
88
            let line = rest.split_once('\n').map(|(l, _)| l).unwrap_or(rest);
247
88
            let mut line = line.split_ascii_whitespace();
248
88
            assert_eq!(line.next(), Some(DIR_SOURCE_KEYWORD));
249
88
            let raw_nickname = line
250
88
                .next()
251
88
                .ok_or(ErrorProblem::MissingArgument { field: "nickname" })?;
252

            
253
88
            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
88
                let a = ConsensusAuthorityEntry::from_items(input, stop_at)?;
259
88
                accum.authorities.push(a);
260
            }
261
        }
262

            
263
12
        if accum.authorities.is_empty() {
264
            return Err(ErrorProblem::MissingItem {
265
                keyword: DIR_SOURCE_KEYWORD,
266
            });
267
12
        }
268

            
269
12
        Ok(accum)
270
12
    }
271
}