1
//! Types for conveniently constructing TorClients.
2

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

            
5
use crate::{
6
    BootstrapBehavior, InertTorClient, Result, TorClient, TorClientConfig, err::ErrorDetail,
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
use tracing::instrument;
17

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
251
22
        let result: Result<TorClient<R>> = TorClient::create_inner(
252
22
            self.runtime.clone(),
253
22
            &self.config,
254
22
            self.bootstrap_behavior,
255
22
            self.dirmgr_builder.as_ref(),
256
22
            dirmgr_extensions,
257
        )
258
22
        .map_err(ErrorDetail::into);
259

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

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

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

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

            
322
#[cfg(test)]
323
mod test {
324
    use tor_rtcompat::PreferredRuntime;
325

            
326
    use super::*;
327

            
328
    fn must_be_send_and_sync<S: Send + Sync>() {}
329

            
330
    #[test]
331
    fn builder_is_send() {
332
        must_be_send_and_sync::<TorClientBuilder<PreferredRuntime>>();
333
    }
334
}