1
//! Types for conveniently constructing TorClients.
2

            
3
#![allow(missing_docs, clippy::missing_docs_in_private_items)]
4

            
5
use crate::{
6
    err::ErrorDetail, BootstrapBehavior, InertTorClient, Result, TorClient, TorClientConfig,
7
};
8
use std::{
9
    result::Result as StdResult,
10
    sync::Arc,
11
    time::{Duration, Instant},
12
};
13
use tor_dirmgr::{DirMgrConfig, DirMgrStore};
14
use tor_error::{ErrorKind, HasKind as _};
15
use tor_rtcompat::Runtime;
16

            
17
/// An object that knows how to construct some kind of DirProvider.
18
///
19
/// Note that this type is only actually exposed when the `experimental-api`
20
/// feature is enabled.
21
#[allow(unreachable_pub)]
22
#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
23
pub trait DirProviderBuilder<R: Runtime>: Send + Sync {
24
    fn build(
25
        &self,
26
        runtime: R,
27
        store: DirMgrStore<R>,
28
        circmgr: Arc<tor_circmgr::CircMgr<R>>,
29
        config: DirMgrConfig,
30
    ) -> Result<Arc<dyn tor_dirmgr::DirProvider + 'static>>;
31
}
32

            
33
/// A DirProviderBuilder that constructs a regular DirMgr.
34
#[derive(Clone, Debug)]
35
struct DirMgrBuilder {}
36

            
37
impl<R: Runtime> DirProviderBuilder<R> for DirMgrBuilder {
38
8
    fn build(
39
8
        &self,
40
8
        runtime: R,
41
8
        store: DirMgrStore<R>,
42
8
        circmgr: Arc<tor_circmgr::CircMgr<R>>,
43
8
        config: DirMgrConfig,
44
8
    ) -> Result<Arc<dyn tor_dirmgr::DirProvider + 'static>> {
45
8
        let dirmgr = tor_dirmgr::DirMgr::create_unbootstrapped(config, runtime, store, circmgr)
46
8
            .map_err(ErrorDetail::DirMgrSetup)?;
47
8
        Ok(Arc::new(dirmgr))
48
8
    }
49
}
50

            
51
/// An object for constructing a [`TorClient`].
52
///
53
/// Returned by [`TorClient::builder()`].
54
#[derive(Clone)]
55
#[must_use]
56
pub struct TorClientBuilder<R: Runtime> {
57
    /// The runtime for the client to use
58
    runtime: R,
59
    /// The client's configuration.
60
    config: TorClientConfig,
61
    /// How the client should behave when it is asked to do something on the Tor
62
    /// network before `bootstrap()` is called.
63
    bootstrap_behavior: BootstrapBehavior,
64
    /// Optional object to construct a DirProvider.
65
    ///
66
    /// Wrapped in an Arc so that we don't need to force DirProviderBuilder to
67
    /// implement Clone.
68
    dirmgr_builder: Arc<dyn DirProviderBuilder<R>>,
69
    /// If present, an amount of time to wait when trying to acquire the filesystem locks for our
70
    /// storage.
71
    local_resource_timeout: Option<Duration>,
72
    /// Optional directory filter to install for testing purposes.
73
    ///
74
    /// Only available when `arti-client` is built with the `dirfilter` and `experimental-api` features.
75
    #[cfg(feature = "dirfilter")]
76
    dirfilter: tor_dirmgr::filter::FilterConfig,
77
}
78

            
79
/// Longest allowable duration to wait for local resources to be available
80
/// when creating a TorClient.
81
///
82
/// This value may change in future versions of Arti.
83
/// It is an error to configure
84
/// a [`local_resource_timeout`](TorClientBuilder)
85
/// with a larger value than this.
86
///
87
/// (Reducing this value would count as a breaking change.)
88
pub const MAX_LOCAL_RESOURCE_TIMEOUT: Duration = Duration::new(5, 0);
89

            
90
impl<R: Runtime> TorClientBuilder<R> {
91
    /// Construct a new TorClientBuilder with the given runtime.
92
44
    pub(crate) fn new(runtime: R) -> Self {
93
44
        Self {
94
44
            runtime,
95
44
            config: TorClientConfig::default(),
96
44
            bootstrap_behavior: BootstrapBehavior::default(),
97
44
            dirmgr_builder: Arc::new(DirMgrBuilder {}),
98
44
            local_resource_timeout: None,
99
44
            #[cfg(feature = "dirfilter")]
100
44
            dirfilter: None,
101
44
        }
102
44
    }
103

            
104
    /// Set the configuration for the `TorClient` under construction.
105
    ///
106
    /// If not called, then a compiled-in default configuration will be used.
107
44
    pub fn config(mut self, config: TorClientConfig) -> Self {
108
44
        self.config = config;
109
44
        self
110
44
    }
111

            
112
    /// Set the bootstrap behavior for the `TorClient` under construction.
113
    ///
114
    /// If not called, then the default ([`BootstrapBehavior::OnDemand`]) will
115
    /// be used.
116
6
    pub fn bootstrap_behavior(mut self, bootstrap_behavior: BootstrapBehavior) -> Self {
117
6
        self.bootstrap_behavior = bootstrap_behavior;
118
6
        self
119
6
    }
120

            
121
    /// Set a timeout that we should allow when trying to acquire our local resources
122
    /// (including lock files.)
123
    ///
124
    /// If no timeout is set, we wait for a short while (currently 500 msec) when invoked with
125
    /// [`create_bootstrapped`](Self::create_bootstrapped) or
126
    /// [`create_unbootstrapped_async`](Self::create_unbootstrapped_async),
127
    /// and we do not wait at all if invoked with
128
    /// [`create_unbootstrapped`](Self::create_unbootstrapped).
129
    ///
130
    /// (This difference in default behavior is meant to avoid unintentional blocking.
131
    /// If you call this method, subsequent calls to `create_bootstrapped` may block
132
    /// the current thread.)
133
    ///
134
    /// The provided timeout value may not be larger than [`MAX_LOCAL_RESOURCE_TIMEOUT`].
135
    pub fn local_resource_timeout(mut self, timeout: Duration) -> Self {
136
        self.local_resource_timeout = Some(timeout);
137
        self
138
    }
139

            
140
    /// Override the default function used to construct the directory provider.
141
    ///
142
    /// Only available when compiled with the `experimental-api` feature: this
143
    /// code is unstable.
144
    #[cfg(all(feature = "experimental-api", feature = "error_detail"))]
145
    pub fn dirmgr_builder<B>(mut self, builder: Arc<dyn DirProviderBuilder<R>>) -> Self
146
    where
147
        B: DirProviderBuilder<R> + 'static,
148
    {
149
        self.dirmgr_builder = builder;
150
        self
151
    }
152

            
153
    /// Install a [`DirFilter`](tor_dirmgr::filter::DirFilter) to
154
    ///
155
    /// Only available when compiled with the `dirfilter` feature: this code
156
    /// is unstable and not recommended for production use.
157
    #[cfg(feature = "dirfilter")]
158
    pub fn dirfilter<F>(mut self, filter: F) -> Self
159
    where
160
        F: Into<Arc<dyn tor_dirmgr::filter::DirFilter + 'static>>,
161
    {
162
        self.dirfilter = Some(filter.into());
163
        self
164
    }
165

            
166
    /// Create a `TorClient` from this builder, without automatically launching
167
    /// the bootstrap process.
168
    ///
169
    /// If you have left the default [`BootstrapBehavior`] in place, the client
170
    /// will bootstrap itself as soon any attempt is made to use it.  You can
171
    /// also bootstrap the client yourself by running its
172
    /// [`bootstrap()`](TorClient::bootstrap) method.
173
    ///
174
    /// If you have replaced the default behavior with [`BootstrapBehavior::Manual`],
175
    /// any attempts to use the client will fail with an error of kind
176
    /// [`ErrorKind::BootstrapRequired`],
177
    /// until you have called [`TorClient::bootstrap`] yourself.
178
    /// This option is useful if you wish to have control over the bootstrap
179
    /// process (for example, you might wish to avoid initiating network
180
    /// connections until explicit user confirmation is given).
181
    ///
182
    /// If a [local_resource_timeout](Self::local_resource_timeout) has been set, this function may
183
    /// block the current thread.
184
    /// Use [`create_unbootstrapped_async`](Self::create_unbootstrapped_async)
185
    /// if that is not what you want.
186
8
    pub fn create_unbootstrapped(&self) -> Result<TorClient<R>> {
187
8
        let timeout = self.local_resource_timeout_or(Duration::from_millis(0))?;
188
8
        let give_up_at = Instant::now() + timeout;
189
8
        let mut first_attempt = true;
190

            
191
        loop {
192
8
            match self.create_unbootstrapped_inner(Instant::now, give_up_at, first_attempt) {
193
                Err(delay) => {
194
                    first_attempt = false;
195
                    std::thread::sleep(delay);
196
                }
197
8
                Ok(other) => return other,
198
            }
199
        }
200
8
    }
201

            
202
    /// Like create_unbootstrapped, but does not block the thread while trying to acquire the lock.
203
    ///
204
    /// If no [`local_resource_timeout`](Self::local_resource_timeout) has been set, this function may
205
    /// delay a short while (currently 500 msec) for local resources (such as lock files) to be available.
206
    /// Set `local_resource_timeout` to 0 if you do not want this behavior.
207
    pub async fn create_unbootstrapped_async(&self) -> Result<TorClient<R>> {
208
        // TODO: This code is largely duplicated from create_unbootstrapped above.  It might be good
209
        // to have a single shared implementation to handle both the sync and async cases, but I am
210
        // concerned that doing so would just add a lot of complexity.
211
        let timeout = self.local_resource_timeout_or(Duration::from_millis(500))?;
212
        let give_up_at = self.runtime.now() + timeout;
213
        let mut first_attempt = true;
214

            
215
        loop {
216
            match self.create_unbootstrapped_inner(|| self.runtime.now(), give_up_at, first_attempt)
217
            {
218
                Err(delay) => {
219
                    first_attempt = false;
220
                    self.runtime.sleep(delay).await;
221
                }
222
                Ok(other) => return other,
223
            }
224
        }
225
    }
226

            
227
    /// Helper for create_bootstrapped and create_bootstrapped_async.
228
    ///
229
    /// Does not retry on `LocalResourceAlreadyInUse`; instead, returns a time that we should wait,
230
    /// and log a message if `first_attempt` is true.
231
8
    fn create_unbootstrapped_inner<F>(
232
8
        &self,
233
8
        now: F,
234
8
        give_up_at: Instant,
235
8
        first_attempt: bool,
236
8
    ) -> StdResult<Result<TorClient<R>>, Duration>
237
8
    where
238
8
        F: FnOnce() -> Instant,
239
8
    {
240
8
        #[allow(unused_mut)]
241
8
        let mut dirmgr_extensions = tor_dirmgr::config::DirMgrExtensions::default();
242
8
        #[cfg(feature = "dirfilter")]
243
8
        {
244
8
            dirmgr_extensions.filter.clone_from(&self.dirfilter);
245
8
        }
246
8

            
247
8
        let result: Result<TorClient<R>> = TorClient::create_inner(
248
8
            self.runtime.clone(),
249
8
            &self.config,
250
8
            self.bootstrap_behavior,
251
8
            self.dirmgr_builder.as_ref(),
252
8
            dirmgr_extensions,
253
8
        )
254
8
        .map_err(ErrorDetail::into);
255

            
256
        match result {
257
            Err(e) if e.kind() == ErrorKind::LocalResourceAlreadyInUse => {
258
                let now = now();
259
                if now >= give_up_at {
260
                    // no time remaining; return the error that we got.
261
                    Ok(Err(e))
262
                } else {
263
                    let remaining = give_up_at.saturating_duration_since(now);
264
                    if first_attempt {
265
                        tracing::info!(
266
                            "Looks like another TorClient may be running; retrying for up to {}",
267
                            humantime::Duration::from(remaining),
268
                        );
269
                    }
270
                    // We'll retry at least once.
271
                    // TODO: Maybe use a smarter backoff strategy here?
272
                    Err(Duration::from_millis(50).min(remaining))
273
                }
274
            }
275
            // We either succeeded, or failed for a reason other than LocalResourceAlreadyInUse
276
8
            other => Ok(other),
277
        }
278
8
    }
279

            
280
    /// Create a TorClient from this builder, and try to bootstrap it.
281
    pub async fn create_bootstrapped(&self) -> Result<TorClient<R>> {
282
        let r = self.create_unbootstrapped_async().await?;
283
        r.bootstrap().await?;
284
        Ok(r)
285
    }
286

            
287
    /// Return the local_resource_timeout, or `dflt` if none is defined.
288
    ///
289
    /// Give an error if the value is above MAX_LOCAL_RESOURCE_TIMEOUT
290
8
    fn local_resource_timeout_or(&self, dflt: Duration) -> Result<Duration> {
291
8
        let timeout = self.local_resource_timeout.unwrap_or(dflt);
292
8
        if timeout > MAX_LOCAL_RESOURCE_TIMEOUT {
293
            return Err(
294
                ErrorDetail::Configuration(tor_config::ConfigBuildError::Invalid {
295
                    field: "local_resource_timeout".into(),
296
                    problem: "local resource timeout too large".into(),
297
                })
298
                .into(),
299
            );
300
8
        }
301
8
        Ok(timeout)
302
8
    }
303

            
304
    /// Create an `InertTorClient` from this builder, without launching
305
    /// the bootstrap process, or connecting to the network.
306
    ///
307
    /// It is currently unspecified whether constructing an `InertTorClient`
308
    /// will hold any locks that prevent opening a `TorClient` with the same
309
    /// directories.
310
    //
311
    // TODO(#1576): reach a decision here.
312
    #[allow(clippy::unnecessary_wraps)]
313
36
    pub fn create_inert(&self) -> Result<InertTorClient> {
314
36
        Ok(InertTorClient::new(&self.config)?)
315
36
    }
316
}
317

            
318
#[cfg(test)]
319
mod test {
320
    use tor_rtcompat::PreferredRuntime;
321

            
322
    use super::*;
323

            
324
    fn must_be_send_and_sync<S: Send + Sync>() {}
325

            
326
    #[test]
327
    fn builder_is_send() {
328
        must_be_send_and_sync::<TorClientBuilder<PreferredRuntime>>();
329
    }
330
}