1use anyhow::Context;
4use std::{
5 collections::{BTreeMap, HashMap},
6 str::FromStr as _,
7 sync::Arc,
8};
9use tracing::debug;
10
11use derive_deftly::Deftly;
12use fs_mistrust::{Mistrust, anon_home::PathExt as _};
13use tor_basic_utils::PathExt as _;
14use tor_config::derive::prelude::*;
15use tor_config::{
16 ConfigBuildError, define_map_builder,
17 extend_builder::{ExtendBuilder, ExtendStrategy},
18};
19use tor_config_path::{CfgPath, CfgPathResolver};
20use tor_error::internal;
21use tor_rpc_connect::{
22 ParsedConnectPoint, SuperuserPermission,
23 auth::RpcAuth,
24 load::{LoadError, LoadOptions, LoadOptionsBuilder},
25 server::ListenerGuard,
26};
27use tor_rtcompat::{Runtime, general};
28
29pub(super) fn listener_map_defaults() -> BTreeMap<String, RpcListenerSetConfigBuilder> {
31 toml::from_str(
32 r#"
33 ["user-default"]
34 enable = true
35 dir = "${ARTI_LOCAL_DATA}/rpc/connect.d"
36
37 ["system-default"]
38 enable = false
39 dir = "/etc/arti-rpc/connect.d"
40 "#,
41 )
42 .expect("Could not parse defaults!")
43}
44
45#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
51#[derive_deftly(TorConfig)]
52#[deftly(tor_config(no_default_trait, no_flattenable_trait, pre_build = Self::validate))]
53#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
54#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = pub)))]
55pub(crate) struct RpcListenerSetConfig {
56 #[deftly(tor_config(
64 setter(skip),
65 attr = serde(flatten),
66 field(ty = ConnectPointOptionsBuilder),
69 build = { |this: &Self| this.listener_options.clone() },
70 extend_with = ExtendBuilder::extend_from
71 ))]
72 listener_options: ConnectPointOptionsBuilder,
73
74 #[deftly(tor_config(setter(strip_option), default))]
78 file: Option<CfgPath>,
79
80 #[deftly(tor_config(setter(strip_option), default))]
85 dir: Option<CfgPath>,
86
87 #[deftly(tor_config(
92 setter(skip),
93 field(ty = FileOptionsMapBuilder),
94 build = { |this: &Self| this.file_options.clone() },
95 extend_with = ExtendBuilder::extend_from
96 ))]
97 file_options: FileOptionsMapBuilder,
98}
99
100impl RpcListenerSetConfigBuilder {
101 fn validate(&self) -> Result<(), ConfigBuildError> {
103 match (&self.file, &self.dir, self.file_options.is_empty()) {
104 (Some(_), None, true) => Ok(()),
106 (None, Some(_), _) => Ok(()),
108 (None, None, _) => Err(ConfigBuildError::MissingField {
110 field: "{file or dir}".into(),
111 }),
112 (_, _, _) => Err(ConfigBuildError::Inconsistent {
113 fields: vec!["file".into(), "dir".into(), "file_options".into()],
114 problem: "'file' is mutually exclusive with 'dir' and 'file_options'".into(),
115 }),
116 }
117 }
118
119 #[cfg(any(test, feature = "experimental-api"))]
129 pub fn listener_options(&mut self) -> &mut ConnectPointOptionsBuilder {
130 &mut self.listener_options
131 }
132
133 #[cfg(any(test, feature = "experimental-api"))]
140 pub fn file_options(&mut self) -> &mut FileOptionsMapBuilder {
141 &mut self.file_options
142 }
143}
144
145define_map_builder! {
146 #[derive(Eq, PartialEq)]
148 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
149 pub(crate) struct FileOptionsMapBuilder =>
150 type FileOptionsMap = BTreeMap<String, ConnectPointOptions>;
151}
152
153#[derive(Debug, Clone, Eq, PartialEq, Deftly)]
169#[derive_deftly(TorConfig)]
170#[deftly(tor_config(attr = derive(PartialEq, Eq)))]
171#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
172#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = pub)))]
173pub(crate) struct ConnectPointOptions {
174 #[deftly(tor_config(default = true))]
176 enable: bool,
177}
178
179impl ConnectPointOptionsBuilder {
180 fn is_enabled(&self) -> bool {
182 self.enable != Some(false)
183 }
184
185 fn load_options(&self) -> LoadOptions {
190 LoadOptionsBuilder::default()
191 .disable(!self.is_enabled())
192 .build()
193 .expect("Somehow constructed an invalid LoadOptions")
194 }
195}
196
197#[derive(Clone, Debug)]
202pub(super) struct RpcConnInfo {
203 pub(super) name: String,
207 pub(super) auth: RpcAuth,
209 #[allow(unused)] pub(super) options: ConnectPointOptions,
212 pub(super) allow_superuser: SuperuserPermission,
215}
216
217impl RpcConnInfo {
218 #[allow(clippy::unnecessary_wraps)]
225 fn new(
226 display_name: String,
227 auth: RpcAuth,
228 options: ConnectPointOptions,
229 allow_superuser: SuperuserPermission,
230 ) -> anyhow::Result<Self> {
231 Ok(Self {
232 name: display_name,
233 auth,
234 options,
235 allow_superuser,
236 })
237 }
238}
239
240impl RpcListenerSetConfig {
241 pub(super) async fn bind<R: Runtime>(
248 &self,
249 runtime: &R,
250 config_key: &str,
251 resolver: &CfgPathResolver,
252 mistrust: &Mistrust,
253 ) -> anyhow::Result<Vec<(general::Listener, Arc<RpcConnInfo>, ListenerGuard)>> {
254 if !self.listener_options.is_enabled() {
255 return Ok(vec![]);
258 }
259
260 if let Some(file) = &self.file {
261 let file = file.path(resolver)?;
262 debug!(
263 "Binding to RPC connect point from {}",
264 file.anonymize_home()
265 );
266 let ctx = |action| {
267 format!(
268 "Can't {} RPC connect point from {}",
269 action,
270 file.anonymize_home()
271 )
272 };
273 let options = self
274 .listener_options
275 .build()
276 .with_context(|| ctx("interpret options"))?;
277
278 let conn_pt = ParsedConnectPoint::load_file(file.as_ref(), mistrust)
279 .with_context(|| ctx("load"))?
280 .resolve(resolver)
281 .with_context(|| ctx("resolve"))?;
282 let tor_rpc_connect::server::Listener {
283 listener,
284 auth,
285 guard,
286 ..
287 } = conn_pt
288 .bind(runtime, mistrust)
289 .await
290 .with_context(|| ctx("bind to"))?;
291 return Ok(vec![(
292 listener,
293 Arc::new(RpcConnInfo::new(
294 format!("rpc.listen.\"{}\"", config_key),
295 auth,
296 options,
297 conn_pt.superuser_permission(),
298 )?),
299 guard,
300 )]);
301 }
302
303 if let Some(dir) = &self.dir {
304 let dir = dir.path(resolver)?;
305 debug!("Reading RPC connect directory at {}", dir.anonymize_home());
306 let load_options: HashMap<std::path::PathBuf, LoadOptions> = self
312 .file_options
313 .iter()
314 .map(|(s, or)| (s.into(), or.load_options()))
315 .collect();
316 let mut listeners = Vec::new();
317 let dir_contents =
318 match ParsedConnectPoint::load_dir(dir.as_ref(), mistrust, &load_options) {
319 Ok(contents) => contents,
320 Err(LoadError::Access(fs_mistrust::Error::NotFound(_))) => return Ok(vec![]),
323 Err(e) => {
324 return Err(e).with_context(|| {
325 format!(
326 "Can't read RPC connect point directory at {}",
327 dir.anonymize_home()
328 )
329 });
330 }
331 };
332 for (path, conn_pt_result) in dir_contents {
333 debug!("Binding to connect point from {}", path.display_lossy());
334 let ctx = |action| {
335 format!(
336 "Can't {} RPC connect point {} from dir {}",
337 action,
338 path.display_lossy(),
339 dir.anonymize_home()
340 )
341 };
342
343 let options = {
344 let mut bld = self.listener_options.clone();
345
346 if let Some(override_options) = path
347 .to_str()
348 .and_then(|fname_as_str| self.file_options.get(fname_as_str))
349 {
350 bld.extend_from(override_options.clone(), ExtendStrategy::ReplaceLists);
351 }
352 bld.build().with_context(|| ctx("interpret options"))?
353 };
354
355 let conn_pt = conn_pt_result
356 .with_context(|| ctx("load"))?
357 .resolve(resolver)
358 .with_context(|| ctx("resolve"))?;
359
360 let tor_rpc_connect::server::Listener {
361 listener,
362 auth,
363 guard,
364 ..
365 } = conn_pt
366 .bind(runtime, mistrust)
367 .await
368 .with_context(|| ctx("bind to"))?;
369 listeners.push((
370 listener,
371 Arc::new(RpcConnInfo::new(
372 format!("rpc.listen.\"{}\" ({})", config_key, path.display_lossy()),
373 auth,
374 options,
375 conn_pt.superuser_permission(),
376 )?),
377 guard,
378 ));
379 }
380
381 return Ok(listeners);
382 }
383
384 Err(internal!("Constructed RpcListenerSetConfig had neither 'dir' nor 'file' set.").into())
385 }
386}
387
388pub(super) async fn bind_string<R: Runtime>(
393 connpt: &str,
394 index: usize,
395 runtime: &R,
396 resolver: &CfgPathResolver,
397 mistrust: &Mistrust,
398) -> anyhow::Result<(general::Listener, Arc<RpcConnInfo>, ListenerGuard)> {
399 let ctx = |action| format!("Can't {action} RPC connect point from rpc.listen_default.#{index}");
400
401 let conn_pt = ParsedConnectPoint::from_str(connpt)
402 .with_context(|| ctx("parse"))?
403 .resolve(resolver)
404 .with_context(|| ctx("resolve"))?;
405 let tor_rpc_connect::server::Listener {
406 listener,
407 auth,
408 guard,
409 ..
410 } = conn_pt
411 .bind(runtime, mistrust)
412 .await
413 .with_context(|| ctx("bind to"))?;
414 Ok((
415 listener,
416 Arc::new(RpcConnInfo::new(
417 format!("rpc.listen_default[(#{})]", index),
418 auth,
419 ConnectPointOptions::default(),
420 conn_pt.superuser_permission(),
421 )?),
422 guard,
423 ))
424}
425
426#[cfg(test)]
427mod test {
428 #![allow(clippy::bool_assert_comparison)]
430 #![allow(clippy::clone_on_copy)]
431 #![allow(clippy::dbg_macro)]
432 #![allow(clippy::mixed_attributes_style)]
433 #![allow(clippy::print_stderr)]
434 #![allow(clippy::print_stdout)]
435 #![allow(clippy::single_char_pattern)]
436 #![allow(clippy::unwrap_used)]
437 #![allow(clippy::unchecked_time_subtraction)]
438 #![allow(clippy::useless_vec)]
439 #![allow(clippy::needless_pass_by_value)]
440 #![allow(clippy::string_slice)] use super::*;
444
445 #[test]
446 fn parse_defaults() {
447 let m = listener_map_defaults();
449 assert_eq!(m.len(), 2);
450 }
451}