Skip to main content

tor_rpc_connect/
lib.rs

1#![cfg_attr(docsrs, feature(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_time_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#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45#![allow(clippy::collapsible_if)] // See arti#2342
46#![deny(clippy::unused_async)]
47#![deny(clippy::string_slice)] // See arti#2571
48//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
49
50// TODO #1645 (either remove this, or decide to have it everywhere)
51#![cfg_attr(not(all(feature = "full")), allow(unused))]
52
53pub mod auth;
54#[cfg(feature = "rpc-client")]
55pub mod client;
56mod connpt;
57pub mod load;
58#[cfg(feature = "rpc-server")]
59pub mod server;
60#[cfg(test)]
61mod testing;
62
63use std::{io, sync::Arc};
64
65pub use connpt::{ParsedConnectPoint, ResolveError, ResolvedConnectPoint};
66use tor_general_addr::general;
67
68/// An action that an RPC client should take when a connect point fails.
69///
70/// (This terminology is taken from the spec.)
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72#[allow(clippy::exhaustive_enums)]
73pub enum ClientErrorAction {
74    /// The client must stop, and must not make any more connect attempts.
75    Abort,
76    /// The connect point has failed; the client can continue to the next connect point.
77    Decline,
78}
79/// An error that has a [`ClientErrorAction`].
80pub trait HasClientErrorAction {
81    /// Return the action that an RPC client should take based on this error.
82    fn client_action(&self) -> ClientErrorAction;
83}
84impl HasClientErrorAction for tor_config_path::CfgPathError {
85    fn client_action(&self) -> ClientErrorAction {
86        // Every variant of this means a configuration error
87        // or an ill-formed TOML file.
88        ClientErrorAction::Abort
89    }
90}
91impl HasClientErrorAction for tor_config_path::addr::CfgAddrError {
92    fn client_action(&self) -> ClientErrorAction {
93        use ClientErrorAction as A;
94        use tor_config_path::addr::CfgAddrError as CAE;
95        match self {
96            CAE::NoAfUnixSocketSupport(_) => A::Decline,
97            CAE::Path(cfg_path_error) => cfg_path_error.client_action(),
98            CAE::ConstructAfUnixAddress(_) => A::Abort,
99            // No variants are currently captured in this pattern, but they _could_ be in the future.
100            _ => A::Abort,
101        }
102    }
103}
104impl HasClientErrorAction for tor_general_addr::general::AddrParseError {
105    fn client_action(&self) -> ClientErrorAction {
106        use ClientErrorAction as A;
107        use tor_general_addr::general::AddrParseError as E;
108        match self {
109            E::UnrecognizedSchema(_) => A::Decline,
110            E::NoSchema => A::Decline,
111            E::InvalidAfUnixAddress(_) => A::Abort,
112            // We might want to turn this into an Abort in the future, but I think that we might
113            // want to allow "auto" as a port format in CfgAddr.
114            E::InvalidInetAddress(_) => A::Decline,
115            // No variants are currently captured in this pattern, but they _could_ be in the future.
116            _ => A::Abort,
117        }
118    }
119}
120
121/// Return the ClientErrorAction for an IO error encountered
122/// while accessing the filesystem.
123///
124/// Note that this is not an implementation of `HasClientErrorAction`:
125/// We want to decline on a different set of errors for network operation.
126fn fs_error_action(err: &std::io::Error) -> ClientErrorAction {
127    use ClientErrorAction as A;
128    use std::io::ErrorKind as EK;
129    match err.kind() {
130        EK::NotFound => A::Decline,
131        EK::PermissionDenied => A::Decline,
132        EK::ReadOnlyFilesystem => A::Decline,
133        _ => A::Abort,
134    }
135}
136/// Return the ClientErrorAction for an IO error encountered
137/// while opening a socket.
138///
139/// Note that this is not an implementation of `HasClientErrorAction`:
140/// We want to decline on a different set of errors for fs operation.
141fn net_error_action(err: &std::io::Error) -> ClientErrorAction {
142    use ClientErrorAction as A;
143    use std::io::ErrorKind as EK;
144    match err.kind() {
145        EK::ConnectionRefused => A::Decline,
146        EK::ConnectionReset => A::Decline,
147        EK::HostUnreachable => A::Decline,
148        EK::NetworkDown => A::Decline,
149        EK::NetworkUnreachable => A::Decline,
150        _ => A::Abort,
151    }
152}
153impl HasClientErrorAction for fs_mistrust::Error {
154    fn client_action(&self) -> ClientErrorAction {
155        use ClientErrorAction as A;
156        use fs_mistrust::Error as E;
157        match self {
158            E::Multiple(errs) => {
159                if errs.iter().any(|e| e.client_action() == A::Abort) {
160                    A::Abort
161                } else {
162                    A::Decline
163                }
164            }
165            E::Io { err, .. } => fs_error_action(err),
166            E::CouldNotInspect(_, err) => fs_error_action(err),
167
168            E::NotFound(_) => A::Decline,
169            E::BadPermission(_, _, _) | E::BadOwner(_, _) => A::Decline,
170            E::StepsExceeded | E::CurrentDirectory(_) => A::Abort,
171
172            E::BadType(_) => A::Abort,
173
174            // These should be impossible for clients given how we use fs_mistrust in this crate.
175            E::CreatingDir(_)
176            | E::Content(_)
177            | E::NoSuchGroup(_)
178            | E::NoSuchUser(_)
179            | E::MissingField(_)
180            | E::InvalidSubdirectory => A::Abort,
181            E::PasswdGroupIoError(_) => A::Abort,
182            _ => A::Abort,
183        }
184    }
185}
186
187/// A failure to connect or bind to a [`ResolvedConnectPoint`].
188#[derive(Clone, Debug, thiserror::Error)]
189#[non_exhaustive]
190pub enum ConnectError {
191    /// We encountered an IO error while actually opening our socket.
192    #[error("IO error while connecting")]
193    Io(#[source] Arc<io::Error>),
194    /// The connect point told us to abort explicitly.
195    #[error("Encountered an explicit \"abort\"")]
196    ExplicitAbort,
197    /// We couldn't load the cookie file for cookie authentication.
198    #[error("Unable to load cookie file")]
199    LoadCookie(#[from] auth::cookie::CookieAccessError),
200    /// We were told to connect to a socket type that we don't support.
201    #[error("Unsupported socket type")]
202    UnsupportedSocketType,
203    /// We were told to connect using an auth type that we don't support.
204    #[error("Unsupported authentication type")]
205    UnsupportedAuthType,
206    /// Unable to access the location of an AF\_UNIX socket.
207    #[error("Unix domain socket path access")]
208    AfUnixSocketPathAccess(#[from] fs_mistrust::Error),
209    /// Unable to access the location of `socket_address_file`.
210    #[error("Problem accessing socket address file")]
211    SocketAddressFileAccess(#[source] fs_mistrust::Error),
212    /// We couldn't parse the JSON contents of a socket address file.
213    #[error("Invalid JSON contents in socket address file")]
214    SocketAddressFileJson(#[source] Arc<serde_json::Error>),
215    /// We couldn't parse the address in a socket address file.
216    #[error("Invalid address in socket address file")]
217    SocketAddressFileContent(#[source] general::AddrParseError),
218    /// We found an address in the socket address file that didn't match the connect point.
219    #[error("Socket address file contents didn't match connect point")]
220    SocketAddressFileMismatch,
221    /// Another process was holding a lock for this connect point,
222    /// so we couldn't bind to it.
223    #[error("Could not acquire lock: Another process is listening on this connect point")]
224    AlreadyLocked,
225    /// We encountered an internal logic error.
226    //
227    // (We're not using tor_error::Bug here because we want this code to work properly in rpc-client-core.)
228    #[error("Internal error: {0}")]
229    Internal(String),
230}
231
232impl From<io::Error> for ConnectError {
233    fn from(err: io::Error) -> Self {
234        ConnectError::Io(Arc::new(err))
235    }
236}
237impl crate::HasClientErrorAction for ConnectError {
238    fn client_action(&self) -> crate::ClientErrorAction {
239        use crate::ClientErrorAction as A;
240        use ConnectError as E;
241        match self {
242            E::Io(err) => crate::net_error_action(err),
243            E::ExplicitAbort => A::Abort,
244            E::LoadCookie(err) => err.client_action(),
245            E::UnsupportedSocketType => A::Decline,
246            E::UnsupportedAuthType => A::Decline,
247            E::AfUnixSocketPathAccess(err) => err.client_action(),
248            E::SocketAddressFileAccess(err) => err.client_action(),
249            E::SocketAddressFileJson(_) => A::Decline,
250            E::SocketAddressFileContent(_) => A::Decline,
251            E::SocketAddressFileMismatch => A::Decline,
252            E::AlreadyLocked => A::Abort, // (This one can't actually occur for clients.)
253            E::Internal(_) => A::Abort,
254        }
255    }
256}
257#[cfg(any(feature = "rpc-client", feature = "rpc-server"))]
258/// Given a `general::SocketAddr`, try to return the path of its parent directory (if any).
259fn socket_parent_path(addr: &tor_general_addr::general::SocketAddr) -> Option<&std::path::Path> {
260    addr.as_pathname().and_then(|p| p.parent())
261}
262
263/// Default connect point for a user-owned Arti instance.
264pub const USER_DEFAULT_CONNECT_POINT: &str = {
265    cfg_if::cfg_if! {
266        if #[cfg(unix)] {
267r#"
268[connect]
269socket = "unix:${ARTI_LOCAL_DATA}/rpc/arti_rpc_socket"
270auth = "none"
271"#
272        } else {
273r#"
274[connect]
275socket = "inet:127.0.0.1:9180"
276auth = { cookie = { path = "${ARTI_LOCAL_DATA}/rpc/arti_rpc_cookie" } }
277"#
278        }
279    }
280};
281
282/// Default connect point for a system-wide Arti instance.
283///
284/// This is `None` if, on this platform, there is no such default connect point.
285pub const SYSTEM_DEFAULT_CONNECT_POINT: Option<&str> = {
286    cfg_if::cfg_if! {
287        if #[cfg(unix)] {
288            Some(
289r#"
290[connect]
291socket = "unix:/var/run/arti-rpc/arti_rpc_socket"
292auth = "none"
293"#
294            )
295        } else {
296            None
297        }
298    }
299};
300
301/// An enum to reflect whether an authenticated connection to a connect point is allowed to acquire
302/// superuser (admin) capabilities.
303#[derive(Copy, Clone, Debug, PartialEq, Eq)]
304#[non_exhaustive]
305pub enum SuperuserPermission {
306    /// The connection may acquire superuser capabilities.
307    Allowed,
308    /// The connection may not acquire superuser capabilities.
309    NotAllowed,
310}
311
312#[cfg(test)]
313mod test {
314    // @@ begin test lint list maintained by maint/add_warning @@
315    #![allow(clippy::bool_assert_comparison)]
316    #![allow(clippy::clone_on_copy)]
317    #![allow(clippy::dbg_macro)]
318    #![allow(clippy::mixed_attributes_style)]
319    #![allow(clippy::print_stderr)]
320    #![allow(clippy::print_stdout)]
321    #![allow(clippy::single_char_pattern)]
322    #![allow(clippy::unwrap_used)]
323    #![allow(clippy::unchecked_time_subtraction)]
324    #![allow(clippy::useless_vec)]
325    #![allow(clippy::needless_pass_by_value)]
326    #![allow(clippy::string_slice)] // See arti#2571
327    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
328
329    use super::*;
330
331    #[test]
332    fn parse_defaults() {
333        let _parsed: ParsedConnectPoint = USER_DEFAULT_CONNECT_POINT.parse().unwrap();
334        if let Some(s) = SYSTEM_DEFAULT_CONNECT_POINT {
335            let _parsed: ParsedConnectPoint = s.parse().unwrap();
336        }
337    }
338}