Skip to main content

tor_dirmgr/
as_plugin.rs

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
12use std::sync::{Arc, Mutex};
13use tor_llcrypto::pk::rsa::RsaIdentity;
14use tor_netdoc::{
15    doc::{
16        authcert::AuthCertKeyIds,
17        microdesc::{self, MdDigest},
18        netstatus::ConsensusFlavor,
19    },
20    types::FixedB64,
21};
22
23use crate::storage::DynStore;
24use tor_dircommon::dir_plugin_backend::{DirBackendPlugin, DirBackendPluginError, http};
25use tor_error::into_internal;
26use 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)]
43pub struct DirPlugin {
44    /// The underlying storage.
45    ///
46    /// Typically this uses an underlying sqlite database.
47    pub(crate) store: Arc<Mutex<DynStore>>,
48}
49
50impl 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
62impl 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.
150enum 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
159impl 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.
205fn 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.
211fn 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.
224fn 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
232enum 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
255impl 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}