1
//! Functionality to connect to an RPC server.
2

            
3
use std::{collections::HashMap, path::PathBuf, str::FromStr as _, sync::Arc};
4

            
5
use fs_mistrust::Mistrust;
6
use tor_config_path::{CfgPath, CfgPathResolver};
7
use tor_rpc_connect::{
8
    ClientErrorAction, HasClientErrorAction, ParsedConnectPoint,
9
    auth::RpcAuth,
10
    load::{LoadError, LoadOptions},
11
};
12

            
13
use crate::{
14
    RpcConn, conn::ConnectError, msgs::response::UnparsedResponse, nb_stream::PollingStream,
15
};
16

            
17
use super::ConnectFailure;
18

            
19
/// An error occurred while trying to construct or manipulate an [`RpcConnBuilder`].
20
#[derive(Clone, Debug, thiserror::Error)]
21
#[non_exhaustive]
22
pub enum BuilderError {
23
    /// We couldn't decode a provided connect string.
24
    #[error("Invalid connect string.")]
25
    InvalidConnectString,
26
}
27

            
28
/// Information about how to construct a connection to an Arti instance.
29
//
30
// TODO RPC: Once we have our formats more settled, add a link to a piece of documentation
31
// explaining what a connect point is and how to make one.
32
#[derive(Default, Clone, Debug)]
33
pub struct RpcConnBuilder {
34
    /// Path entries provided programmatically.
35
    ///
36
    /// These are considered after entries in
37
    /// the `$ARTI_RPC_CONNECT_PATH_OVERRIDE` environment variable,
38
    /// but before any other entries.
39
    /// (See `RPCConnBuilder::new` for details.)
40
    ///
41
    /// These entries are stored in reverse order.
42
    prepend_path_reversed: Vec<SearchEntry>,
43
}
44

            
45
/// A single entry in the search path used to find connect points.
46
///
47
/// Includes information on where we got this entry
48
/// (environment variable, application, or default).
49
#[derive(Clone, Debug)]
50
struct SearchEntry {
51
    /// The source telling us this entry.
52
    source: ConnPtOrigin,
53
    /// The location to search.
54
    location: SearchLocation,
55
}
56

            
57
/// A single location in the search path used to find connect points.
58
#[derive(Clone, Debug)]
59
enum SearchLocation {
60
    /// A literal connect point entry to parse.
61
    Literal(String),
62
    /// A path to a connect file, or a directory full of connect files.
63
    Path {
64
        /// The path to load.
65
        path: CfgPath,
66

            
67
        /// If true, then this entry comes from a builtin default,
68
        /// and relative paths should cause the connect attempt to be declined.
69
        ///
70
        /// Otherwise, this entry comes from the user or application,
71
        /// and relative paths should cause the connect attempt to abort.
72
        is_default_entry: bool,
73
    },
74
}
75

            
76
/// Diagnostic: An explanation of where we found a connect point,
77
/// and why we looked there.
78
#[derive(Debug, Clone)]
79
pub struct ConnPtDescription {
80
    /// What told us to look in this location
81
    source: ConnPtOrigin,
82
    /// Where we found the connect point.
83
    location: ConnPtLocation,
84
}
85

            
86
impl std::fmt::Display for ConnPtDescription {
87
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88
        write!(
89
            f,
90
            "connect point in {}, from {}",
91
            &self.location, &self.source
92
        )
93
    }
94
}
95

            
96
/// Diagnostic: a source telling us where to look for a connect point.
97
#[derive(Clone, Copy, Debug)]
98
enum ConnPtOrigin {
99
    /// Found the search entry from an environment variable.
100
    EnvVar(&'static str),
101
    /// Application manually inserted the search entry.
102
    Application,
103
    /// The search entry was a built-in default
104
    Default,
105
}
106

            
107
impl std::fmt::Display for ConnPtOrigin {
108
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109
        match self {
110
            ConnPtOrigin::EnvVar(varname) => write!(f, "${}", varname),
111
            ConnPtOrigin::Application => write!(f, "application"),
112
            ConnPtOrigin::Default => write!(f, "default list"),
113
        }
114
    }
115
}
116

            
117
/// Diagnostic: Where we found a connect point.
118
#[derive(Clone, Debug)]
119
enum ConnPtLocation {
120
    /// The connect point was given as a literal string.
121
    Literal(String),
122
    /// We expanded a CfgPath to find the location of a connect file on disk.
123
    File {
124
        /// The path as configured
125
        path: CfgPath,
126
        /// The expanded path.
127
        expanded: Option<PathBuf>,
128
    },
129
    /// We expanded a CfgPath to find a directory, and found the connect file
130
    /// within that directory
131
    WithinDir {
132
        /// The path of the directory as configured.
133
        path: CfgPath,
134
        /// The location of the file.
135
        file: PathBuf,
136
    },
137
}
138

            
139
impl std::fmt::Display for ConnPtLocation {
140
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141
        // Note: here we use Path::display(), which in other crates we forbid
142
        // and use tor_basic_utils::PathExt::display_lossy().
143
        //
144
        // Here we make an exception, since arti-rpc-client-core is meant to have
145
        // minimal dependencies on our other crates.
146
        #[allow(clippy::disallowed_methods)]
147
        match self {
148
            ConnPtLocation::Literal(s) => write!(f, "literal string {:?}", s),
149
            ConnPtLocation::File {
150
                path,
151
                expanded: Some(ex),
152
            } => {
153
                write!(f, "file {} [{}]", path, ex.display())
154
            }
155
            ConnPtLocation::File {
156
                path,
157
                expanded: None,
158
            } => {
159
                write!(f, "file {} [cannot expand]", path)
160
            }
161

            
162
            ConnPtLocation::WithinDir {
163
                path,
164
                file: expanded,
165
            } => {
166
                write!(f, "file {} in directory {}", expanded.display(), path)
167
            }
168
        }
169
    }
170
}
171

            
172
impl RpcConnBuilder {
173
    /// Create a new `RpcConnBuilder` to try connecting to an Arti instance.
174
    ///
175
    /// By default, we search:
176
    ///   - Any connect points listed in the environment variable `$ARTI_RPC_CONNECT_PATH_OVERRIDE`
177
    ///   - Any connect points passed to `RpcConnBuilder::prepend_*`
178
    ///     (Since these variables are _prepended_,
179
    ///     the ones that are prepended _last_ will be considered _first_.)
180
    ///   - Any connect points listed in the environment variable `$ARTI_RPC_CONNECT_PATH`
181
    ///   - Any connect files in `${ARTI_LOCAL_DATA}/rpc/connect.d`
182
    ///   - Any connect files in `/etc/arti-rpc/connect.d` (unix only)
183
    ///   - [`tor_rpc_connect::USER_DEFAULT_CONNECT_POINT`]
184
    ///   - [`tor_rpc_connect::SYSTEM_DEFAULT_CONNECT_POINT`] if present
185
    //
186
    // TODO RPC: Once we have our formats more settled, add a link to a piece of documentation
187
    // explaining what a connect point is and how to make one.
188
    pub fn new() -> Self {
189
        Self::default()
190
    }
191

            
192
    /// Prepend a single literal connect point to the search path in this RpcConnBuilder.
193
    ///
194
    /// This entry will be considered before any entries in
195
    /// the `$ARTI_RPC_CONNECT_PATH` environment variable
196
    /// but after any entry in
197
    /// the `$ARTI_RPC_CONNECT_PATH_OVERRIDE` environment variable.
198
    ///
199
    /// This entry must be a literal connect point, expressed as a TOML table.
200
    pub fn prepend_literal_entry(&mut self, s: String) {
201
        self.prepend_internal(SearchLocation::Literal(s));
202
    }
203

            
204
    /// Prepend a single path entry to the search path in this RpcConnBuilder.
205
    ///
206
    /// This entry will be considered before any entries in
207
    /// the `$ARTI_RPC_CONNECT_PATH` environment variable,
208
    /// but after any entry in
209
    /// the `$ARTI_RPC_CONNECT_PATH_OVERRIDE` environment variable.
210
    ///
211
    /// This entry must be a path to a file or directory.
212
    /// It may contain variables to expand;
213
    /// they will be expanded according to the rules of [`CfgPath`],
214
    /// using the variables of [`tor_config_path::arti_client_base_resolver`].
215
    pub fn prepend_path(&mut self, p: String) {
216
        self.prepend_internal(SearchLocation::Path {
217
            path: CfgPath::new(p),
218
            is_default_entry: false,
219
        });
220
    }
221

            
222
    /// Prepend a single literal path entry to the search path in this RpcConnBuilder.
223
    ///
224
    /// This entry will be considered before any entries in
225
    /// the `$ARTI_RPC_CONNECT_PATH` environment variable,
226
    /// but after any entry in
227
    /// the `$ARTI_RPC_CONNECT_PATH_OVERRIDE` environment variable.
228
    ///
229
    /// Variables in this entry will not be expanded.
230
    pub fn prepend_literal_path(&mut self, p: PathBuf) {
231
        self.prepend_internal(SearchLocation::Path {
232
            path: CfgPath::new_literal(p),
233
            is_default_entry: false,
234
        });
235
    }
236

            
237
    /// Prepend the application-provided [`SearchLocation`] to the path.
238
    fn prepend_internal(&mut self, location: SearchLocation) {
239
        self.prepend_path_reversed.push(SearchEntry {
240
            source: ConnPtOrigin::Application,
241
            location,
242
        });
243
    }
244

            
245
    /// Return the list of default path entries that we search _after_
246
    /// all user-provided entries.
247
    fn default_path_entries() -> Vec<SearchEntry> {
248
        use SearchLocation::*;
249
        let dflt = |location| SearchEntry {
250
            source: ConnPtOrigin::Default,
251
            location,
252
        };
253
        let mut result = vec![
254
            dflt(Path {
255
                path: CfgPath::new("${ARTI_LOCAL_DATA}/rpc/connect.d/".to_owned()),
256
                is_default_entry: true,
257
            }),
258
            #[cfg(unix)]
259
            dflt(Path {
260
                path: CfgPath::new_literal("/etc/arti-rpc/connect.d/"),
261
                is_default_entry: true,
262
            }),
263
            dflt(Literal(
264
                tor_rpc_connect::USER_DEFAULT_CONNECT_POINT.to_owned(),
265
            )),
266
        ];
267
        if let Some(p) = tor_rpc_connect::SYSTEM_DEFAULT_CONNECT_POINT {
268
            result.push(dflt(Literal(p.to_owned())));
269
        }
270
        result
271
    }
272

            
273
    /// Return a vector of every PathEntry that we should try to connect to.
274
    fn all_entries(&self) -> Result<Vec<SearchEntry>, ConnectError> {
275
        let mut entries = SearchEntry::from_env_var("ARTI_RPC_CONNECT_PATH_OVERRIDE")?;
276
        entries.extend(self.prepend_path_reversed.iter().rev().cloned());
277
        entries.extend(SearchEntry::from_env_var("ARTI_RPC_CONNECT_PATH")?);
278
        entries.extend(Self::default_path_entries());
279
        Ok(entries)
280
    }
281

            
282
    /// Try to connect to an Arti process as specified by this Builder.
283
    pub fn connect(&self) -> Result<RpcConn, ConnectFailure> {
284
        let resolver = tor_config_path::arti_client_base_resolver();
285
        // TODO RPC: Make this configurable.  (Currently, you can override it with
286
        // the environment variable FS_MISTRUST_DISABLE_PERMISSIONS_CHECKS.)
287
        let mistrust = Mistrust::default();
288
        let options = HashMap::new();
289
        let all_entries = self.all_entries().map_err(|e| ConnectFailure {
290
            declined: vec![],
291
            final_desc: None,
292
            final_error: e,
293
        })?;
294
        let mut declined = Vec::new();
295
        for (description, load_result) in all_entries
296
            .into_iter()
297
            .flat_map(|ent| ent.load(&resolver, &mistrust, &options))
298
        {
299
            match load_result.and_then(|e| try_connect(&e, &resolver, &mistrust)) {
300
                Ok(conn) => return Ok(conn),
301
                Err(e) => match e.client_action() {
302
                    ClientErrorAction::Abort => {
303
                        return Err(ConnectFailure {
304
                            declined,
305
                            final_desc: Some(description),
306
                            final_error: e,
307
                        });
308
                    }
309
                    ClientErrorAction::Decline => {
310
                        declined.push((description, e));
311
                    }
312
                },
313
            }
314
        }
315
        Err(ConnectFailure {
316
            declined,
317
            final_desc: None,
318
            final_error: ConnectError::AllAttemptsDeclined,
319
        })
320
    }
321
}
322

            
323
/// Helper: Try to resolve any variables in parsed,
324
/// and open and authenticate an RPC connection to it.
325
///
326
/// This is a separate function from `RpcConnBuilder::connect` to make error handling easier to read.
327
fn try_connect(
328
    parsed: &ParsedConnectPoint,
329
    resolver: &CfgPathResolver,
330
    mistrust: &Mistrust,
331
) -> Result<RpcConn, ConnectError> {
332
    use tor_rpc_connect::client::Stream as S;
333
    let tor_rpc_connect::client::Connection { stream, auth, .. } =
334
        parsed.resolve(resolver)?.connect(mistrust)?;
335
    let wrap_io_err = |e| tor_rpc_connect::ConnectError::Io(Arc::new(e));
336

            
337
    let stream: Box<dyn crate::nb_stream::MioStream> = match stream {
338
        S::Tcp(tcp_stream) => {
339
            tcp_stream.set_nonblocking(true).map_err(wrap_io_err)?;
340
            Box::new(mio::net::TcpStream::from_std(tcp_stream))
341
        }
342
        #[cfg(unix)]
343
        S::Unix(unix_stream) => {
344
            unix_stream.set_nonblocking(true).map_err(wrap_io_err)?;
345
            Box::new(mio::net::UnixStream::from_std(unix_stream))
346
        }
347
        _ => return Err(ConnectError::StreamTypeUnsupported),
348
    };
349

            
350
    let mut stream = PollingStream::new(stream).map_err(wrap_io_err)?;
351
    let banner = stream
352
        .interact()
353
        .map_err(wrap_io_err)?
354
        .ok_or(ConnectError::InvalidBanner)?;
355
    check_banner(&banner)?;
356

            
357
    let mut conn = RpcConn::new(stream);
358

            
359
    // TODO RPC: remove this "scheme name" from the protocol?
360
    let session_id = match auth {
361
        RpcAuth::Inherent => conn.authenticate_inherent("auth:inherent")?,
362
        RpcAuth::Cookie {
363
            secret,
364
            server_address,
365
        } => conn.authenticate_cookie(secret.load()?.as_ref(), &server_address)?,
366
        _ => return Err(ConnectError::AuthenticationNotSupported),
367
    };
368
    conn.session = Some(session_id);
369

            
370
    Ok(conn)
371
}
372

            
373
/// Return Ok if `msg` is a banner indicating the correct protocol.
374
fn check_banner(msg: &UnparsedResponse) -> Result<(), ConnectError> {
375
    /// Structure to indicate that this is indeed an Arti RPC connection.
376
    #[derive(serde::Deserialize)]
377
    struct BannerMsg {
378
        /// Ignored value
379
        #[allow(dead_code)]
380
        arti_rpc: serde_json::Value,
381
    }
382
    let _: BannerMsg =
383
        serde_json::from_str(msg.as_str()).map_err(|_| ConnectError::InvalidBanner)?;
384
    Ok(())
385
}
386

            
387
impl SearchEntry {
388
    /// Return an iterator over ParsedConnPoints from this `SearchEntry`.
389
    fn load<'a>(
390
        &self,
391
        resolver: &CfgPathResolver,
392
        mistrust: &Mistrust,
393
        options: &'a HashMap<PathBuf, LoadOptions>,
394
    ) -> ConnPtIterator<'a> {
395
        // Create a ConnPtDescription given a connect point's location, so we can describe
396
        // an error origin.
397
        let descr = |location| ConnPtDescription {
398
            source: self.source,
399
            location,
400
        };
401

            
402
        match &self.location {
403
            SearchLocation::Literal(s) => ConnPtIterator::Singleton(
404
                descr(ConnPtLocation::Literal(s.clone())),
405
                // It's a literal entry, so we just try to parse it.
406
                ParsedConnectPoint::from_str(s).map_err(|e| ConnectError::from(LoadError::from(e))),
407
            ),
408
            SearchLocation::Path {
409
                path: cfgpath,
410
                is_default_entry,
411
            } => {
412
                // Create a ConnPtDescription given an optional expanded path.
413
                let descr_file = |expanded| {
414
                    descr(ConnPtLocation::File {
415
                        path: cfgpath.clone(),
416
                        expanded,
417
                    })
418
                };
419

            
420
                // It's a path, so we need to expand it...
421
                let path = match cfgpath.path(resolver) {
422
                    Ok(p) => p,
423
                    Err(e) => {
424
                        return ConnPtIterator::Singleton(
425
                            descr_file(None),
426
                            Err(ConnectError::CannotResolvePath(e)),
427
                        );
428
                    }
429
                };
430
                if !path.is_absolute() {
431
                    if *is_default_entry {
432
                        return ConnPtIterator::Done;
433
                    } else {
434
                        return ConnPtIterator::Singleton(
435
                            descr_file(Some(path)),
436
                            Err(ConnectError::RelativeConnectFile),
437
                        );
438
                    }
439
                }
440
                // ..then try to load it as a directory...
441
                match ParsedConnectPoint::load_dir(&path, mistrust, options) {
442
                    Ok(iter) => ConnPtIterator::Dir(self.source, cfgpath.clone(), iter),
443
                    Err(LoadError::NotADirectory) => {
444
                        // ... and if that fails, try to load it as a file.
445
                        let loaded =
446
                            ParsedConnectPoint::load_file(&path, mistrust).map_err(|e| e.into());
447
                        ConnPtIterator::Singleton(descr_file(Some(path)), loaded)
448
                    }
449
                    Err(other) => {
450
                        ConnPtIterator::Singleton(descr_file(Some(path)), Err(other.into()))
451
                    }
452
                }
453
            }
454
        }
455
    }
456

            
457
    /// Return a list of `SearchEntry` as specified in an environment variable with a given name.
458
    fn from_env_var(varname: &'static str) -> Result<Vec<Self>, ConnectError> {
459
        match std::env::var(varname) {
460
            Ok(s) if s.is_empty() => Ok(vec![]),
461
            Ok(s) => Self::from_env_string(varname, &s),
462
            Err(std::env::VarError::NotPresent) => Ok(vec![]),
463
            Err(_) => Err(ConnectError::BadEnvironment), // TODO RPC: Preserve more information?
464
        }
465
    }
466

            
467
    /// Return a list of `SearchEntry` as specified in the value `s` from an envvar called `varname`.
468
    fn from_env_string(varname: &'static str, s: &str) -> Result<Vec<Self>, ConnectError> {
469
        // TODO RPC: Possibly we should be using std::env::split_paths, if it behaves correctly
470
        // with our url-escaped entries.
471
        s.split(PATH_SEP_CHAR)
472
            .map(|s| {
473
                Ok(SearchEntry {
474
                    source: ConnPtOrigin::EnvVar(varname),
475
                    location: SearchLocation::from_env_string_elt(s)?,
476
                })
477
            })
478
            .collect()
479
    }
480
}
481

            
482
impl SearchLocation {
483
    /// Return a `SearchLocation` from a single entry within an environment variable.
484
    fn from_env_string_elt(s: &str) -> Result<SearchLocation, ConnectError> {
485
        match s.bytes().next() {
486
            Some(b'%') | Some(b'[') => Ok(Self::Literal(
487
                percent_encoding::percent_decode_str(s)
488
                    .decode_utf8()
489
                    .map_err(|_| ConnectError::BadEnvironment)?
490
                    .into_owned(),
491
            )),
492
            _ => Ok(Self::Path {
493
                path: CfgPath::new(s.to_owned()),
494
                is_default_entry: false,
495
            }),
496
        }
497
    }
498
}
499

            
500
/// Character used to separate path environment variables.
501
const PATH_SEP_CHAR: char = {
502
    cfg_if::cfg_if! {
503
         if #[cfg(windows)] { ';' } else { ':' }
504
    }
505
};
506

            
507
/// Iterator over connect points returned by PathEntry::load().
508
enum ConnPtIterator<'a> {
509
    /// Iterator over a directory
510
    Dir(
511
        /// Origin of the directory
512
        ConnPtOrigin,
513
        /// The directory as configured
514
        CfgPath,
515
        /// Iterator over the elements loaded from the directory
516
        tor_rpc_connect::load::ConnPointIterator<'a>,
517
    ),
518
    /// A single connect point or error
519
    Singleton(ConnPtDescription, Result<ParsedConnectPoint, ConnectError>),
520
    /// An exhausted iterator
521
    Done,
522
}
523

            
524
impl<'a> Iterator for ConnPtIterator<'a> {
525
    // TODO RPC yield the pathbuf too, for better errors.
526
    type Item = (ConnPtDescription, Result<ParsedConnectPoint, ConnectError>);
527

            
528
    fn next(&mut self) -> Option<Self::Item> {
529
        let mut t = ConnPtIterator::Done;
530
        std::mem::swap(self, &mut t);
531
        match t {
532
            ConnPtIterator::Dir(source, cfgpath, mut iter) => {
533
                let next = iter
534
                    .next()
535
                    .map(|(path, res)| (path, res.map_err(|e| e.into())));
536
                let Some((expanded, result)) = next else {
537
                    *self = ConnPtIterator::Done;
538
                    return None;
539
                };
540
                let description = ConnPtDescription {
541
                    source,
542
                    location: ConnPtLocation::WithinDir {
543
                        path: cfgpath.clone(),
544
                        file: expanded,
545
                    },
546
                };
547
                *self = ConnPtIterator::Dir(source, cfgpath, iter);
548
                Some((description, result))
549
            }
550
            ConnPtIterator::Singleton(desc, res) => Some((desc, res)),
551
            ConnPtIterator::Done => None,
552
        }
553
    }
554
}