summaryrefslogtreecommitdiff
path: root/crates/arti/src/rpc.rs
blob: 24321d6302e900700cd1c4cd10cd8311d6200996 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
//! Experimental RPC support.

use anyhow::Result;
use arti_rpcserver::RpcMgr;
use derive_builder::Builder;
use fs_mistrust::Mistrust;
use futures::{stream::StreamExt, task::SpawnExt, AsyncReadExt};
use listener::{RpcListenerMap, RpcListenerMapBuilder};
use serde::{Deserialize, Serialize};
use session::ArtiRpcSession;
use std::{io::Result as IoResult, sync::Arc};
use tor_config::{define_list_builder_helper, impl_standard_builder, ConfigBuildError};
use tor_config_path::CfgPathResolver;
use tracing::{debug, info};

use arti_client::TorClient;
use tor_rtcompat::{general, NetStreamListener as _, Runtime};

pub(crate) mod conntarget;
pub(crate) mod listener;
mod proxyinfo;
mod session;

pub(crate) use session::{RpcStateSender, RpcVisibleArtiState};

/// Configuration for Arti's RPC subsystem.
///
/// You cannot change this section on a running Arti client.
#[derive(Debug, Clone, Builder, Eq, PartialEq)]
#[builder(build_fn(error = "ConfigBuildError"))]
#[builder(derive(Debug, Serialize, Deserialize))]
#[builder_struct_attr(non_exhaustive)]
#[non_exhaustive]
pub struct RpcConfig {
    /// If true, then the RPC subsystem is enabled and will listen for connections.
    #[builder(default = "false")] // TODO RPC make this true once we are stable.
    enable: bool,

    /// A set of named locations in which to find connect files.
    #[builder(sub_builder)]
    #[builder_field_attr(serde(default))]
    listen: RpcListenerMap,

    /// A list of default connect points to bind
    /// if no enabled connect points are found under `listen`.
    #[builder(sub_builder)]
    #[builder_field_attr(serde(default))]
    listen_default: ListenDefaults,
}
impl_standard_builder! { RpcConfig }

/// Type alias to enable sub_builder to work.
type ListenDefaults = Vec<String>;

define_list_builder_helper! {
    pub struct ListenDefaultsBuilder {
        values: [String],
    }
    built: Vec<String> = values;
    default = listen_defaults_defaults();
    item_build: |item| Ok(item.clone());
}

/// Return default values for `RpcConfig.listen_default`
fn listen_defaults_defaults() -> Vec<String> {
    vec![tor_rpc_connect::USER_DEFAULT_CONNECT_POINT.to_string()]
}

/// Information about an incoming connection.
///
/// Yielded in a stream from our RPC listeners.
type IncomingConn = (
    general::Stream,
    general::SocketAddr,
    Arc<listener::RpcConnInfo>,
);

/// Bind to all configured RPC listeners in `cfg`.
///
/// On success, return a stream of `IncomingConn`.
#[allow(clippy::cognitive_complexity)] // TODO: Refactor?
async fn launch_all_listeners<R: Runtime>(
    runtime: &R,
    cfg: &RpcConfig,
    resolver: &CfgPathResolver,
    mistrust: &Mistrust,
) -> anyhow::Result<(
    impl futures::Stream<Item = IoResult<IncomingConn>> + Unpin,
    Vec<tor_rpc_connect::server::ListenerGuard>,
)> {
    let mut listeners = Vec::new();
    let mut guards = Vec::new();
    for (name, listener_cfg) in cfg.listen.iter() {
        for (lis, info, guard) in listener_cfg
            .bind(runtime, name.as_str(), resolver, mistrust)
            .await?
        {
            // (Note that `bind` only returns enabled listeners, so we don't need to check here.
            debug!(
                "Listening at {} for {}",
                lis.local_addr()
                    .expect("general::listener without address?")
                    .display_lossy(),
                info.name,
            );
            listeners.push((lis, info));
            guards.push(guard);
        }
    }
    if listeners.is_empty() {
        for (idx, connpt) in cfg.listen_default.iter().enumerate() {
            let display_index = idx + 1; // One-indexed values are more human-readable.
            let (lis, info, guard) =
                listener::bind_string(connpt, display_index, runtime, resolver, mistrust).await?;
            debug!(
                "Listening at {} for {}",
                lis.local_addr()
                    .expect("general::listener without address?")
                    .display_lossy(),
                info.name,
            );
            listeners.push((lis, info));
            guards.push(guard);
        }
    }
    if listeners.is_empty() {
        info!("No RPC listeners configured.");
    }

    let streams = listeners.into_iter().map(|(listener, info)| {
        listener
            .incoming()
            .map(move |accept_result| match accept_result {
                Ok((netstream, addr)) => Ok((netstream, addr, Arc::clone(&info))),
                Err(e) => Err(e),
            })
    });

    Ok((futures::stream::select_all(streams), guards))
}

/// Create an RPC manager, bind to connect points, and open a listener task to accept incoming
/// RPC connections.
pub(crate) async fn launch_rpc_mgr<R: Runtime>(
    runtime: &R,
    cfg: &RpcConfig,
    resolver: &CfgPathResolver,
    mistrust: &Mistrust,
    client: TorClient<R>,
) -> Result<Option<RpcProxySupport>> {
    if !cfg.enable {
        return Ok(None);
    }
    let (rpc_state, rpc_state_sender) = RpcVisibleArtiState::new();

    let rpc_mgr = RpcMgr::new(move |auth| ArtiRpcSession::new(auth, &client, &rpc_state))?;
    // Register methods. Needed since TorClient is generic.
    //
    // TODO: If we accumulate a large number of generics like this, we should do this elsewhere.
    rpc_mgr.register_rpc_methods(TorClient::<R>::rpc_methods());
    rpc_mgr.register_rpc_methods(arti_rpcserver::rpc_methods::<R>());

    let rt_clone = runtime.clone();
    let rpc_mgr_clone = rpc_mgr.clone();

    let (incoming, guards) = launch_all_listeners(runtime, cfg, resolver, mistrust).await?;

    // TODO: Using spawn in this way makes it hard to report whether we
    // succeeded or not. This is something we should fix when we refactor
    // our service-launching code.
    runtime.spawn(async move {
        let result = run_rpc_listener(rt_clone, incoming, rpc_mgr_clone).await;
        if let Err(e) = result {
            tracing::warn!("RPC manager quit with an error: {}", e);
        }
        drop(guards);
    })?;
    Ok(Some(RpcProxySupport {
        rpc_mgr,
        rpc_state_sender,
    }))
}

/// Backend function to implement an RPC listener: runs in a loop.
async fn run_rpc_listener<R: Runtime>(
    runtime: R,
    mut incoming: impl futures::Stream<Item = IoResult<IncomingConn>> + Unpin,
    rpc_mgr: Arc<RpcMgr>,
) -> Result<()> {
    while let Some((stream, _addr, info)) = incoming.next().await.transpose()? {
        debug!("Received incoming RPC connection from {}", &info.name);

        let connection = rpc_mgr.new_connection(info.auth.clone());
        let (input, output) = stream.split();

        runtime.spawn(async {
            let result = connection.run(input, output).await;
            if let Err(e) = result {
                tracing::warn!("RPC session ended with an error: {}", e);
            }
        })?;
    }
    Ok(())
}

/// Information passed to a SOCKS proxy or similar stream provider when running with RPC support.
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
pub(crate) struct RpcProxySupport {
    /// An RPC manager to use for looking up objects as possible stream targets.
    pub(crate) rpc_mgr: Arc<arti_rpcserver::RpcMgr>,
    /// An RPCStateSender to use for registering the list of known proxy ports.
    pub(crate) rpc_state_sender: RpcStateSender,
}

#[cfg(test)]
mod test {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::mixed_attributes_style)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_duration_subtraction)]
    #![allow(clippy::useless_vec)]
    #![allow(clippy::needless_pass_by_value)]
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->

    use listener::{ConnectPointOptionsBuilder, RpcListenerSetConfigBuilder};
    use tor_config_path::CfgPath;
    use tor_rpc_connect::ParsedConnectPoint;

    use super::*;

    #[test]
    fn rpc_method_names() {
        // We run this from a nice high level module, to ensure that as many method names as
        // possible will be in-scope.
        let problems = tor_rpcbase::check_method_names([]);

        for (m, err) in &problems {
            eprintln!("Bad method name {m:?}: {err}");
        }
        assert!(problems.is_empty());
    }

    #[test]
    fn parse_listener_defaults() {
        for string in listen_defaults_defaults() {
            let _parsed: ParsedConnectPoint = string.parse().unwrap();
        }
    }

    #[test]
    fn parsing_and_building() {
        fn build(s: &str) -> Result<RpcConfig, anyhow::Error> {
            let b: RpcConfigBuilder = toml::from_str(s)?;
            Ok(b.build()?)
        }

        let mut user_defaults_builder = RpcListenerSetConfigBuilder::default();
        user_defaults_builder.listener_options().enable(true);
        user_defaults_builder.dir(CfgPath::new("${ARTI_LOCAL_DATA}/rpc/connect.d".to_string()));
        let mut system_defaults_builder = RpcListenerSetConfigBuilder::default();
        system_defaults_builder.listener_options().enable(false);
        system_defaults_builder.dir(CfgPath::new("/etc/arti-rpc/connect.d".to_string()));

        // Make sure that an empty configuration gets us the defaults.
        let defaults = build("").unwrap();
        assert_eq!(
            defaults,
            RpcConfig {
                enable: false,
                listen: vec![
                    (
                        "user-default".to_string(),
                        user_defaults_builder.build().unwrap()
                    ),
                    (
                        "system-default".to_string(),
                        system_defaults_builder.build().unwrap()
                    ),
                ]
                .into_iter()
                .collect(),
                listen_default: listen_defaults_defaults()
            }
        );

        // Make sure that overriding specific options works as expected.
        let altered = build(
            r#"
[listen."user-default"]
enable = false
[listen."system-default"]
dir = "/usr/local/etc/arti-rpc/connect.d"
file_options = { "tmp.toml" = { "enable" = false } }
[listen."my-connpt"]
file = "/home/dante/.paradiso/connpt.toml"
"#,
        )
        .unwrap();
        let mut altered_user_defaults = user_defaults_builder.clone();
        altered_user_defaults.listener_options().enable(false);
        let mut altered_system_defaults = system_defaults_builder.clone();
        altered_system_defaults.dir(CfgPath::new(
            "/usr/local/etc/arti-rpc/connect.d".to_string(),
        ));
        let mut opt = ConnectPointOptionsBuilder::default();
        opt.enable(false);
        altered_system_defaults
            .file_options()
            .insert("tmp.toml".to_string(), opt);
        let mut my_connpt = RpcListenerSetConfigBuilder::default();
        my_connpt.file(CfgPath::new(
            "/home/dante/.paradiso/connpt.toml".to_string(),
        ));

        assert_eq!(
            altered,
            RpcConfig {
                enable: false,
                listen: vec![
                    (
                        "user-default".to_string(),
                        altered_user_defaults.build().unwrap()
                    ),
                    (
                        "system-default".to_string(),
                        altered_system_defaults.build().unwrap()
                    ),
                    ("my-connpt".to_string(), my_connpt.build().unwrap()),
                ]
                .into_iter()
                .collect(),
                listen_default: listen_defaults_defaults()
            }
        );
    }
}