1use 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#[derive(Clone)]
43pub struct DirPlugin {
44 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 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 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 fn get_md_consensus(
97 &self,
98 request: &http::Request<()>,
99 ) -> Result<http::Response<Box<[u8]>>, ReqError> {
100 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 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 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
149enum UriInterpretation {
151 MdsByDigest(Vec<MdDigest>),
153 LatestMdConsensus,
155 AuthCertsByFpSk(Vec<AuthCertKeyIds>),
157}
158
159impl UriInterpretation {
160 fn interpret(path: &str) -> Result<Self, ReqError> {
162 let path = path.strip_prefix("/tor").unwrap_or(path);
165
166 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 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 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 Err(ReqError::Unhandled)
200 }
201 }
202}
203
204fn 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
210fn 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
223fn 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#[derive(Clone, Debug, thiserror::Error)]
231#[allow(clippy::large_enum_variant)] enum ReqError {
233 #[error("Request was not valid: {0}")]
235 Invalid(&'static str),
236
237 #[error("Not modified since If-Modified-Since")]
240 NotModified,
241
242 #[error("No document present")]
244 NotFound,
245
246 #[error("Database failure")]
248 DbFailed(#[from] crate::Error),
249
250 #[error("We don't handle this kind of request")]
252 Unhandled,
253}
254
255impl ReqError {
256 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}