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 serde = "flatten",
66 field(
67 ty = "ConnectPointOptionsBuilder"),
70 build = "|this: &Self| this.listener_options.clone()",
71 extend_with = "ExtendBuilder::extend_from"
72 ))]
73 listener_options: ConnectPointOptionsBuilder,
74
75 #[deftly(tor_config(setter(strip_option), default))]
79 file: Option<CfgPath>,
80
81 #[deftly(tor_config(setter(strip_option), default))]
86 dir: Option<CfgPath>,
87
88 #[deftly(tor_config(
93 setter(skip),
94 field(ty = "FileOptionsMapBuilder"),
95 build = "|this: &Self| this.file_options.clone()",
96 extend_with = "ExtendBuilder::extend_from"
97 ))]
98 file_options: FileOptionsMapBuilder,
99}
100
101impl RpcListenerSetConfigBuilder {
102 fn validate(&self) -> Result<(), ConfigBuildError> {
104 match (&self.file, &self.dir, self.file_options.is_empty()) {
105 (Some(_), None, true) => Ok(()),
107 (None, Some(_), _) => Ok(()),
109 (None, None, _) => Err(ConfigBuildError::MissingField {
111 field: "{file or dir}".into(),
112 }),
113 (_, _, _) => Err(ConfigBuildError::Inconsistent {
114 fields: vec!["file".into(), "dir".into(), "file_options".into()],
115 problem: "'file' is mutually exclusive with 'dir' and 'file_options'".into(),
116 }),
117 }
118 }
119
120 #[cfg(any(test, feature = "experimental-api"))]
130 pub fn listener_options(&mut self) -> &mut ConnectPointOptionsBuilder {
131 &mut self.listener_options
132 }
133
134 #[cfg(any(test, feature = "experimental-api"))]
141 pub fn file_options(&mut self) -> &mut FileOptionsMapBuilder {
142 &mut self.file_options
143 }
144}
145
146define_map_builder! {
147 #[derive(Eq, PartialEq)]
149 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
150 pub(crate) struct FileOptionsMapBuilder =>
151 type FileOptionsMap = BTreeMap<String, ConnectPointOptions>;
152}
153
154#[derive(Debug, Clone, Eq, PartialEq, Deftly)]
170#[derive_deftly(TorConfig)]
171#[deftly(tor_config(attr = "derive(PartialEq, Eq)"))]
172#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
173#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
174pub(crate) struct ConnectPointOptions {
175 #[deftly(tor_config(default = "true"))]
177 enable: bool,
178}
179
180impl ConnectPointOptionsBuilder {
181 fn is_enabled(&self) -> bool {
183 self.enable != Some(false)
184 }
185
186 fn load_options(&self) -> LoadOptions {
191 LoadOptionsBuilder::default()
192 .disable(!self.is_enabled())
193 .build()
194 .expect("Somehow constructed an invalid LoadOptions")
195 }
196}
197
198#[derive(Clone, Debug)]
203pub(super) struct RpcConnInfo {
204 pub(super) name: String,
208 pub(super) auth: RpcAuth,
210 #[allow(unused)] pub(super) options: ConnectPointOptions,
213 pub(super) allow_superuser: SuperuserPermission,
216}
217
218impl RpcConnInfo {
219 #[allow(clippy::unnecessary_wraps)]
226 fn new(
227 display_name: String,
228 auth: RpcAuth,
229 options: ConnectPointOptions,
230 allow_superuser: SuperuserPermission,
231 ) -> anyhow::Result<Self> {
232 Ok(Self {
233 name: display_name,
234 auth,
235 options,
236 allow_superuser,
237 })
238 }
239}
240
241impl RpcListenerSetConfig {
242 pub(super) async fn bind<R: Runtime>(
249 &self,
250 runtime: &R,
251 config_key: &str,
252 resolver: &CfgPathResolver,
253 mistrust: &Mistrust,
254 ) -> anyhow::Result<Vec<(general::Listener, Arc<RpcConnInfo>, ListenerGuard)>> {
255 if !self.listener_options.is_enabled() {
256 return Ok(vec![]);
259 }
260
261 if let Some(file) = &self.file {
262 let file = file.path(resolver)?;
263 debug!(
264 "Binding to RPC connect point from {}",
265 file.anonymize_home()
266 );
267 let ctx = |action| {
268 format!(
269 "Can't {} RPC connect point from {}",
270 action,
271 file.anonymize_home()
272 )
273 };
274 let options = self
275 .listener_options
276 .build()
277 .with_context(|| ctx("interpret options"))?;
278
279 let conn_pt = ParsedConnectPoint::load_file(file.as_ref(), mistrust)
280 .with_context(|| ctx("load"))?
281 .resolve(resolver)
282 .with_context(|| ctx("resolve"))?;
283 let tor_rpc_connect::server::Listener {
284 listener,
285 auth,
286 guard,
287 ..
288 } = conn_pt
289 .bind(runtime, mistrust)
290 .await
291 .with_context(|| ctx("bind to"))?;
292 return Ok(vec![(
293 listener,
294 Arc::new(RpcConnInfo::new(
295 format!("rpc.listen.\"{}\"", config_key),
296 auth,
297 options,
298 conn_pt.superuser_permission(),
299 )?),
300 guard,
301 )]);
302 }
303
304 if let Some(dir) = &self.dir {
305 let dir = dir.path(resolver)?;
306 debug!("Reading RPC connect directory at {}", dir.anonymize_home());
307 let load_options: HashMap<std::path::PathBuf, LoadOptions> = self
313 .file_options
314 .iter()
315 .map(|(s, or)| (s.into(), or.load_options()))
316 .collect();
317 let mut listeners = Vec::new();
318 let dir_contents =
319 match ParsedConnectPoint::load_dir(dir.as_ref(), mistrust, &load_options) {
320 Ok(contents) => contents,
321 Err(LoadError::Access(fs_mistrust::Error::NotFound(_))) => return Ok(vec![]),
324 Err(e) => {
325 return Err(e).with_context(|| {
326 format!(
327 "Can't read RPC connect point directory at {}",
328 dir.anonymize_home()
329 )
330 });
331 }
332 };
333 for (path, conn_pt_result) in dir_contents {
334 debug!("Binding to connect point from {}", path.display_lossy());
335 let ctx = |action| {
336 format!(
337 "Can't {} RPC connect point {} from dir {}",
338 action,
339 path.display_lossy(),
340 dir.anonymize_home()
341 )
342 };
343
344 let options = {
345 let mut bld = self.listener_options.clone();
346
347 if let Some(override_options) = path
348 .to_str()
349 .and_then(|fname_as_str| self.file_options.get(fname_as_str))
350 {
351 bld.extend_from(override_options.clone(), ExtendStrategy::ReplaceLists);
352 }
353 bld.build().with_context(|| ctx("interpret options"))?
354 };
355
356 let conn_pt = conn_pt_result
357 .with_context(|| ctx("load"))?
358 .resolve(resolver)
359 .with_context(|| ctx("resolve"))?;
360
361 let tor_rpc_connect::server::Listener {
362 listener,
363 auth,
364 guard,
365 ..
366 } = conn_pt
367 .bind(runtime, mistrust)
368 .await
369 .with_context(|| ctx("bind to"))?;
370 listeners.push((
371 listener,
372 Arc::new(RpcConnInfo::new(
373 format!("rpc.listen.\"{}\" ({})", config_key, path.display_lossy()),
374 auth,
375 options,
376 conn_pt.superuser_permission(),
377 )?),
378 guard,
379 ));
380 }
381
382 return Ok(listeners);
383 }
384
385 Err(internal!("Constructed RpcListenerSetConfig had neither 'dir' nor 'file' set.").into())
386 }
387}
388
389pub(super) async fn bind_string<R: Runtime>(
394 connpt: &str,
395 index: usize,
396 runtime: &R,
397 resolver: &CfgPathResolver,
398 mistrust: &Mistrust,
399) -> anyhow::Result<(general::Listener, Arc<RpcConnInfo>, ListenerGuard)> {
400 let ctx = |action| format!("Can't {action} RPC connect point from rpc.listen_default.#{index}");
401
402 let conn_pt = ParsedConnectPoint::from_str(connpt)
403 .with_context(|| ctx("parse"))?
404 .resolve(resolver)
405 .with_context(|| ctx("resolve"))?;
406 let tor_rpc_connect::server::Listener {
407 listener,
408 auth,
409 guard,
410 ..
411 } = conn_pt
412 .bind(runtime, mistrust)
413 .await
414 .with_context(|| ctx("bind to"))?;
415 Ok((
416 listener,
417 Arc::new(RpcConnInfo::new(
418 format!("rpc.listen_default[(#{})]", index),
419 auth,
420 ConnectPointOptions::default(),
421 conn_pt.superuser_permission(),
422 )?),
423 guard,
424 ))
425}
426
427#[cfg(test)]
428mod test {
429 #![allow(clippy::bool_assert_comparison)]
431 #![allow(clippy::clone_on_copy)]
432 #![allow(clippy::dbg_macro)]
433 #![allow(clippy::mixed_attributes_style)]
434 #![allow(clippy::print_stderr)]
435 #![allow(clippy::print_stdout)]
436 #![allow(clippy::single_char_pattern)]
437 #![allow(clippy::unwrap_used)]
438 #![allow(clippy::unchecked_time_subtraction)]
439 #![allow(clippy::useless_vec)]
440 #![allow(clippy::needless_pass_by_value)]
441 use super::*;
444
445 #[test]
446 fn parse_defaults() {
447 let m = listener_map_defaults();
449 assert_eq!(m.len(), 2);
450 }
451}