tor_rpc_connect/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
2#![doc = include_str!("../README.md")]
3// @@ begin lint list maintained by maint/add_warning @@
4#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6#![warn(missing_docs)]
7#![warn(noop_method_call)]
8#![warn(unreachable_pub)]
9#![warn(clippy::all)]
10#![deny(clippy::await_holding_lock)]
11#![deny(clippy::cargo_common_metadata)]
12#![deny(clippy::cast_lossless)]
13#![deny(clippy::checked_conversions)]
14#![warn(clippy::cognitive_complexity)]
15#![deny(clippy::debug_assert_with_mut_call)]
16#![deny(clippy::exhaustive_enums)]
17#![deny(clippy::exhaustive_structs)]
18#![deny(clippy::expl_impl_clone_on_copy)]
19#![deny(clippy::fallible_impl_from)]
20#![deny(clippy::implicit_clone)]
21#![deny(clippy::large_stack_arrays)]
22#![warn(clippy::manual_ok_or)]
23#![deny(clippy::missing_docs_in_private_items)]
24#![warn(clippy::needless_borrow)]
25#![warn(clippy::needless_pass_by_value)]
26#![warn(clippy::option_option)]
27#![deny(clippy::print_stderr)]
28#![deny(clippy::print_stdout)]
29#![warn(clippy::rc_buffer)]
30#![deny(clippy::ref_option_ref)]
31#![warn(clippy::semicolon_if_nothing_returned)]
32#![warn(clippy::trait_duplication_in_bounds)]
33#![deny(clippy::unchecked_duration_subtraction)]
34#![deny(clippy::unnecessary_wraps)]
35#![warn(clippy::unseparated_literal_suffix)]
36#![deny(clippy::unwrap_used)]
37#![deny(clippy::mod_module_files)]
38#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39#![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43#![allow(clippy::needless_lifetimes)] // See arti#1765
44//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
45
46// TODO #1645 (either remove this, or decide to have it everywhere)
47#![cfg_attr(not(all(feature = "full")), allow(unused))]
48
49pub mod auth;
50#[cfg(feature = "rpc-client")]
51pub mod client;
52mod connpt;
53pub mod load;
54#[cfg(feature = "rpc-server")]
55pub mod server;
56#[cfg(test)]
57mod testing;
58
59use std::{io, sync::Arc};
60
61pub use connpt::{ParsedConnectPoint, ResolveError, ResolvedConnectPoint};
62
63/// An action that an RPC client should take when a connect point fails.
64///
65/// (This terminology is taken from the spec.)
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67#[allow(clippy::exhaustive_enums)]
68pub enum ClientErrorAction {
69    /// The client must stop, and must not make any more connect attempts.
70    Abort,
71    /// The connect point has failed; the client can continue to the next connect point.
72    Decline,
73}
74/// An error that has a [`ClientErrorAction`].
75pub trait HasClientErrorAction {
76    /// Return the action that an RPC client should take based on this error.
77    fn client_action(&self) -> ClientErrorAction;
78}
79impl HasClientErrorAction for tor_config_path::CfgPathError {
80    fn client_action(&self) -> ClientErrorAction {
81        // Every variant of this means a configuration error
82        // or an ill-formed TOML file.
83        ClientErrorAction::Abort
84    }
85}
86impl HasClientErrorAction for tor_config_path::addr::CfgAddrError {
87    fn client_action(&self) -> ClientErrorAction {
88        use tor_config_path::addr::CfgAddrError as CAE;
89        use ClientErrorAction as A;
90        match self {
91            CAE::NoAfUnixSocketSupport(_) => A::Decline,
92            CAE::Path(cfg_path_error) => cfg_path_error.client_action(),
93            CAE::ConstructAfUnixAddress(_) => A::Abort,
94            // No variants are currently captured in this pattern, but they _could_ be in the future.
95            _ => A::Abort,
96        }
97    }
98}
99/// Return the ClientErrorAction for an IO error encountered
100/// while accessing the filesystem.
101///
102/// Note that this is not an implementation of `HasClientErrorAction`:
103/// We want to decline on a different set of errors for network operation.
104fn fs_error_action(err: &std::io::Error) -> ClientErrorAction {
105    use std::io::ErrorKind as EK;
106    use ClientErrorAction as A;
107    match err.kind() {
108        EK::NotFound => A::Decline,
109        EK::PermissionDenied => A::Decline,
110        _ => A::Abort,
111    }
112}
113/// Return the ClientErrorAction for an IO error encountered
114/// while opening a socket.
115///
116/// Note that this is not an implementation of `HasClientErrorAction`:
117/// We want to decline on a different set of errors for fs operation.
118fn net_error_action(err: &std::io::Error) -> ClientErrorAction {
119    use std::io::ErrorKind as EK;
120    use ClientErrorAction as A;
121    match err.kind() {
122        EK::ConnectionRefused => A::Decline,
123        EK::ConnectionReset => A::Decline,
124        // TODO MSRV 1.83; revisit once some of `io_error_more` is stabilized.
125        // see https://github.com/rust-lang/rust/pull/128316
126        _ => A::Abort,
127    }
128}
129impl HasClientErrorAction for fs_mistrust::Error {
130    fn client_action(&self) -> ClientErrorAction {
131        use fs_mistrust::Error as E;
132        use ClientErrorAction as A;
133        match self {
134            E::Multiple(errs) => {
135                if errs.iter().any(|e| e.client_action() == A::Abort) {
136                    A::Abort
137                } else {
138                    A::Decline
139                }
140            }
141            E::Io { err, .. } => fs_error_action(err),
142            E::CouldNotInspect(_, err) => fs_error_action(err),
143
144            E::NotFound(_) => A::Decline,
145            E::BadPermission(_, _, _) | E::BadOwner(_, _) => A::Decline,
146            E::StepsExceeded | E::CurrentDirectory(_) => A::Abort,
147
148            E::BadType(_) => A::Abort,
149
150            // These should be impossible for clients given how we use fs_mistrust in this crate.
151            E::CreatingDir(_)
152            | E::Content(_)
153            | E::NoSuchGroup(_)
154            | E::NoSuchUser(_)
155            | E::MissingField(_)
156            | E::InvalidSubdirectory => A::Abort,
157            E::PasswdGroupIoError(_) => A::Abort,
158            _ => A::Abort,
159        }
160    }
161}
162
163/// A failure to connect or bind to a [`ResolvedConnectPoint`].
164#[derive(Clone, Debug, thiserror::Error)]
165#[non_exhaustive]
166pub enum ConnectError {
167    /// We encountered an IO error while actually opening our socket.
168    #[error("IO error while connecting")]
169    Io(#[source] Arc<io::Error>),
170    /// The connect point told us to abort explicitly.
171    #[error("Encountered an explicit \"abort\"")]
172    ExplicitAbort,
173    /// We couldn't load the cookie file for cookie authentication.
174    #[error("Unable to load cookie file")]
175    LoadCookie(#[from] auth::cookie::CookieAccessError),
176    /// We were told to connect to a socket type that we don't support.
177    #[error("Unsupported socket type")]
178    UnsupportedSocketType,
179    /// We were told to connect using an auth type that we don't support.
180    #[error("Unsupported authentication type")]
181    UnsupportedAuthType,
182    /// Unable to access the location of an AF\_UNIX socket.
183    #[error("Unix domain socket path access")]
184    AfUnixSocketPathAccess(#[from] fs_mistrust::Error),
185    /// Another process was holding a lock for this connect point,
186    /// so we couldn't bind to it.
187    #[error("Could not acquire lock: Another process is listening on this connect point")]
188    AlreadyLocked,
189}
190
191impl From<io::Error> for ConnectError {
192    fn from(err: io::Error) -> Self {
193        ConnectError::Io(Arc::new(err))
194    }
195}
196impl crate::HasClientErrorAction for ConnectError {
197    fn client_action(&self) -> crate::ClientErrorAction {
198        use crate::ClientErrorAction as A;
199        use ConnectError as E;
200        match self {
201            E::Io(err) => crate::net_error_action(err),
202            E::ExplicitAbort => A::Abort,
203            E::LoadCookie(err) => err.client_action(),
204            E::UnsupportedSocketType => A::Decline,
205            E::UnsupportedAuthType => A::Decline,
206            E::AfUnixSocketPathAccess(err) => err.client_action(),
207            E::AlreadyLocked => A::Abort, // (This one can't actually occur for clients.)
208        }
209    }
210}
211#[cfg(any(feature = "rpc-client", feature = "rpc-server"))]
212/// Given a `general::SocketAddr`, try to return the path of its parent directory (if any).
213fn socket_parent_path(addr: &tor_general_addr::general::SocketAddr) -> Option<&std::path::Path> {
214    addr.as_pathname().and_then(|p| p.parent())
215}
216
217/// Default connect point for a user-owned Arti instance.
218pub const USER_DEFAULT_CONNECT_POINT: &str = {
219    cfg_if::cfg_if! {
220        if #[cfg(unix)] {
221r#"
222[connect]
223socket = "unix:${ARTI_LOCAL_DATA}/rpc/arti_rpc_socket"
224auth = "none"
225"#
226        } else {
227r#"
228[connect]
229socket = "inet:127.0.0.1:9180"
230auth = { cookie = { path = "${ARTI_LOCAL_DATA}/rpc/arti_rpc_cookie" } }
231"#
232        }
233    }
234};
235
236/// Default connect point for a system-wide Arti instance.
237///
238/// This is `None` if, on this platform, there is no such default connect point.
239pub const SYSTEM_DEFAULT_CONNECT_POINT: Option<&str> = {
240    cfg_if::cfg_if! {
241        if #[cfg(unix)] {
242            Some(
243r#"
244[connect]
245socket = "unix:/var/run/arti-rpc/arti_rpc_socket"
246auth = "none"
247"#
248            )
249        } else {
250            None
251        }
252    }
253};
254
255#[cfg(test)]
256mod test {
257    // @@ begin test lint list maintained by maint/add_warning @@
258    #![allow(clippy::bool_assert_comparison)]
259    #![allow(clippy::clone_on_copy)]
260    #![allow(clippy::dbg_macro)]
261    #![allow(clippy::mixed_attributes_style)]
262    #![allow(clippy::print_stderr)]
263    #![allow(clippy::print_stdout)]
264    #![allow(clippy::single_char_pattern)]
265    #![allow(clippy::unwrap_used)]
266    #![allow(clippy::unchecked_duration_subtraction)]
267    #![allow(clippy::useless_vec)]
268    #![allow(clippy::needless_pass_by_value)]
269    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
270
271    use super::*;
272
273    #[test]
274    fn parse_defaults() {
275        let _parsed: ParsedConnectPoint = USER_DEFAULT_CONNECT_POINT.parse().unwrap();
276        if let Some(s) = SYSTEM_DEFAULT_CONNECT_POINT {
277            let _parsed: ParsedConnectPoint = s.parse().unwrap();
278        }
279    }
280}