1
//! TEMPORARY, EXPERIMENTAL implementation of a [`Store`]-backed [`DirBackendPlugin`].
2
//!
3
//! This is throw-away code. It exists so that arti relays can pretend to be rudimentary directory
4
//! caches before tor-dirserver is finished.
5
//!
6
//! [`Store`]: crate::storage::Store
7
//
8
// NOTE: There are several points where we say "LIMITATION" below to indicate a place
9
// where this implementation falls short of the specified standard.  We are NOT planning
10
// to fix these, since this is throw-away code.
11

            
12
use std::sync::{Arc, Mutex};
13
use tor_llcrypto::pk::rsa::RsaIdentity;
14
use tor_netdoc::{
15
    doc::{
16
        authcert::AuthCertKeyIds,
17
        microdesc::{self, MdDigest},
18
        netstatus::ConsensusFlavor,
19
    },
20
    types::FixedB64,
21
};
22

            
23
use crate::storage::DynStore;
24
use tor_dircommon::dir_plugin_backend::{DirBackendPlugin, DirBackendPluginError, http};
25
use tor_error::into_internal;
26
use web_time_compat::SystemTime;
27

            
28
// LIMITATION: There is no compression support.
29
//
30
// LIMITATION: We copy every object that we serve; memory usage would be terrible in production.
31
// LIMITATION: We grab the DB lock every time we have a request.
32
// LIMITATION: We make more DB queries than strictly necessary.
33
//
34
// LIMITATION: Since the DirMgr downloads on a client's schedule, we will fetch consensuses
35
// slightly later than a relay properly should.
36
//
37
// LIMITATION: We may cause lock contention.
38

            
39
/// An implementation of [`DirBackendPlugin`] based on a [`DirMgr`]'s sqlite database.
40
///
41
/// [`DirMgr`]: crate::DirMgr
42
#[derive(Clone)]
43
pub struct DirPlugin {
44
    /// The underlying storage.
45
    ///
46
    /// Typically this uses an underlying sqlite database.
47
    pub(crate) store: Arc<Mutex<DynStore>>,
48
}
49

            
50
impl DirBackendPlugin for DirPlugin {
51
    fn get(
52
        &self,
53
        request: &http::Request<()>,
54
    ) -> Result<http::Response<Box<[u8]>>, DirBackendPluginError> {
55
        match self.handle_request(request) {
56
            Ok(v) => Ok(v),
57
            Err(e) => e.into_response(),
58
        }
59
    }
60
}
61

            
62
impl DirPlugin {
63
    /// As [`DirBackendPlugin::get`], but return a [`ReqError`] on failure.
64
    fn handle_request(
65
        &self,
66
        request: &http::Request<()>,
67
    ) -> Result<http::Response<Box<[u8]>>, ReqError> {
68
        if request.method() != http::Method::GET {
69
            return Err(ReqError::Unhandled);
70
        }
71
        let what = UriInterpretation::interpret(request.uri().path())?;
72

            
73
        match what {
74
            UriInterpretation::MdsByDigest(items) => self.get_mds(&items[..]),
75
            UriInterpretation::LatestMdConsensus => self.get_md_consensus(request),
76
            UriInterpretation::AuthCertsByFpSk(items) => self.get_certs(&items[..]),
77
        }
78
    }
79

            
80
    /// Try to handle a request for microdescriptors
81
    fn get_mds(&self, items: &[MdDigest]) -> Result<http::Response<Box<[u8]>>, ReqError> {
82
        let store = self.store.lock().expect("poisoned lock");
83

            
84
        let mds = store.microdescs(items)?;
85
        if mds.is_empty() {
86
            Err(ReqError::NotFound)
87
        } else {
88
            let s: String = mds.values().map(|v| v.as_str()).collect();
89
            let mut r = http::Response::new(s.into_bytes().into());
90
            *r.status_mut() = http::StatusCode::OK;
91
            Ok(r)
92
        }
93
    }
94

            
95
    /// Try to handle a request for a microdescriptor consensus.
96
    fn get_md_consensus(
97
        &self,
98
        request: &http::Request<()>,
99
    ) -> Result<http::Response<Box<[u8]>>, ReqError> {
100
        // LIMITATION: We ignore which authority FPs the client believes in.
101
        // LIMITATION: We ignore any diff requests.
102

            
103
        let if_modified_since: Option<SystemTime> = request
104
            .headers()
105
            .get("If-Modified-Since")
106
            .map(|hv| parse_date(hv.as_bytes()))
107
            .transpose()?;
108
        let store = self.store.lock().expect("poisoned lock");
109

            
110
        if let Some(ims) = if_modified_since {
111
            let meta = store.latest_consensus_meta(ConsensusFlavor::Microdesc)?;
112
            let Some(meta) = meta else {
113
                return Err(ReqError::NotFound);
114
            };
115
            if meta.lifetime().valid_after() < ims {
116
                return Err(ReqError::NotModified);
117
            }
118
        }
119

            
120
        let pending_ok = Some(false);
121
        let latest = store.latest_consensus(ConsensusFlavor::Microdesc, pending_ok)?;
122
        let Some(latest) = latest else {
123
            return Err(ReqError::NotFound);
124
        };
125

            
126
        // LIMITATION: We just copy the whole whole multi-megabyte consensus.
127
        let body = latest.as_ref().into();
128
        let mut r = http::Response::new(body);
129
        *r.status_mut() = http::StatusCode::OK;
130
        Ok(r)
131
    }
132

            
133
    /// Try to handle a request for certificates.
134
    fn get_certs(&self, items: &[AuthCertKeyIds]) -> Result<http::Response<Box<[u8]>>, ReqError> {
135
        let store = self.store.lock().expect("poisoned lock");
136

            
137
        let certs = store.authcerts(items)?;
138
        if certs.is_empty() {
139
            Err(ReqError::NotFound)
140
        } else {
141
            let s: String = certs.values().map(|v| v.as_str()).collect();
142
            let mut r = http::Response::new(s.into_bytes().into());
143
            *r.status_mut() = http::StatusCode::OK;
144
            Ok(r)
145
        }
146
    }
147
}
148

            
149
/// An accepted interpretation of a directory URI.
150
enum UriInterpretation {
151
    /// A request for one or more microdescriptors by their SHA256 digest.
152
    MdsByDigest(Vec<MdDigest>),
153
    /// A request for the latest MD consensus.
154
    LatestMdConsensus,
155
    /// A request for one or more authcerts by fingerprint digest and signing-key digest.
156
    AuthCertsByFpSk(Vec<AuthCertKeyIds>),
157
}
158

            
159
impl UriInterpretation {
160
    /// Based on a relative path, figure out what the client is asking for.
161
    fn interpret(path: &str) -> Result<Self, ReqError> {
162
        // Remove optional "tor" part.
163
        // LIMITATION: Maybe this is no longer optional? I didn't check. It will be fine.
164
        let path = path.strip_prefix("/tor").unwrap_or(path);
165

            
166
        // LIMITATION: We ignore the ".z" suffix since we're not doing compression.
167
        let (path, _dot_z) = match path.strip_suffix(".z") {
168
            Some(p) => (p, true),
169
            None => (path, false),
170
        };
171

            
172
        if path.starts_with("/status-vote/current/consensus-md") {
173
            // LIMITATION: We don't handle the "/diff" URIs.
174
            // LIMITATION: We don't serve "ns" consensuses.
175
            // LIMITATION: We ignore the list of fingerprints that follow this part of the path.
176
            // LIMITATION: We behave incorrectly if asked for a flavor that begins with, but is not,
177
            //    "md".
178

            
179
            Ok(Self::LatestMdConsensus)
180
        } else if let Some(remainder) = path.strip_prefix("/micro/d/") {
181
            let mut digests: Vec<_> = remainder
182
                .split('-')
183
                .map(decode_md_digest)
184
                .collect::<Result<Vec<_>, _>>()
185
                .map_err(ReqError::Invalid)?;
186
            digests.sort();
187
            Ok(UriInterpretation::MdsByDigest(digests))
188
        } else if let Some(remainder) = path.strip_prefix("/keys/fp-sk/") {
189
            // LIMITATION: we don't handle other "keys" URIs, since I think clients don't use them.
190
            let mut pairs: Vec<_> = remainder
191
                .split('+')
192
                .map(decode_fp_pair)
193
                .collect::<Result<Vec<_>, _>>()
194
                .map_err(ReqError::Invalid)?;
195
            pairs.sort();
196
            Ok(UriInterpretation::AuthCertsByFpSk(pairs))
197
        } else {
198
            // This is some type of URI we don't handle.
199
            Err(ReqError::Unhandled)
200
        }
201
    }
202
}
203

            
204
/// Try to parse a base64 MD digest as it appears in URIs.
205
fn decode_md_digest(s: &str) -> Result<MdDigest, &'static str> {
206
    let d: FixedB64<{ microdesc::DOC_DIGEST_LEN }> = s.parse().map_err(|_| "Invalid MD base64")?;
207
    Ok(d.0)
208
}
209

            
210
/// Try to parse an authority fingerprint pair as it appears in URIs.
211
fn decode_fp_pair(s: &str) -> Result<AuthCertKeyIds, &'static str> {
212
    let Some((fp, sk)) = s.split_once('-') else {
213
        return Err("misformed fingerprint pair");
214
    };
215
    let fp = RsaIdentity::from_hex(fp).ok_or("Invalid fp in fingerprint pair")?;
216
    let sk = RsaIdentity::from_hex(sk).ok_or("Invalid sk in fingerprint pair")?;
217
    Ok(AuthCertKeyIds {
218
        id_fingerprint: fp,
219
        sk_fingerprint: sk,
220
    })
221
}
222

            
223
/// Try to parse an HTTP date.
224
fn parse_date(b: &[u8]) -> Result<SystemTime, ReqError> {
225
    let s = str::from_utf8(b).map_err(|_| ReqError::Invalid("invalid date"))?;
226
    httpdate::parse_http_date(s).map_err(|_| ReqError::Invalid("invalid date"))
227
}
228

            
229
/// An error that has occurred while trying to answer a request.
230
#[derive(Clone, Debug, thiserror::Error)]
231
#[allow(clippy::large_enum_variant)] // LIMITATION
232
enum ReqError {
233
    /// We found an error while parsing the request.
234
    #[error("Request was not valid: {0}")]
235
    Invalid(&'static str),
236

            
237
    /// The user specified an If-Modified-Since header, and the
238
    /// document was older than that.
239
    #[error("Not modified since If-Modified-Since")]
240
    NotModified,
241

            
242
    /// We tried looking for the requested document but it wasn't there.
243
    #[error("No document present")]
244
    NotFound,
245

            
246
    /// Some kind of error occurred while looking up an object in the `Store`.
247
    #[error("Database failure")]
248
    DbFailed(#[from] crate::Error),
249

            
250
    /// The URI or method was something we don't  deal with.
251
    #[error("We don't handle this kind of request")]
252
    Unhandled,
253
}
254

            
255
impl ReqError {
256
    /// Convert this error into _either_ a response (if we know what to tell the client)
257
    /// or a [`DirBackendPluginError`] (if we don't).
258
    fn into_response(self) -> Result<http::Response<Box<[u8]>>, DirBackendPluginError> {
259
        match self {
260
            ReqError::Invalid(msg) => {
261
                let mut r = http::Response::new(msg.as_bytes().into());
262
                *r.status_mut() = http::StatusCode::BAD_REQUEST;
263
                Ok(r)
264
            }
265
            ReqError::NotModified => {
266
                let mut r = http::Response::new("Not modified".as_bytes().into());
267
                *r.status_mut() = http::StatusCode::NOT_MODIFIED;
268
                Ok(r)
269
            }
270
            ReqError::NotFound => {
271
                let mut r = http::Response::new("Not found".as_bytes().into());
272
                *r.status_mut() = http::StatusCode::NOT_FOUND;
273
                Ok(r)
274
            }
275
            ReqError::Unhandled => Err(DirBackendPluginError::UriNotHandledByPlugin),
276
            ReqError::DbFailed(e) => Err(DirBackendPluginError::Bug(into_internal!(
277
                "Database failed"
278
            )(e))),
279
        }
280
    }
281
}