diff --git a/Cargo.lock b/Cargo.lock index b1dc36cb5..848734e26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,7 @@ dependencies = [ "futures", "humantime-serde", "hyper", + "once_cell", "pin-project", "postage", "serde", diff --git a/crates/arti-client/Cargo.toml b/crates/arti-client/Cargo.toml index dabf83046..b7f9f9e59 100644 --- a/crates/arti-client/Cargo.toml +++ b/crates/arti-client/Cargo.toml @@ -54,3 +54,4 @@ tokio-util = { version = "0.6", features = ["compat"] } anyhow = "1.0.5" tracing-subscriber = "0.3.0" tempfile = "3.3" +once_cell = "1.9" diff --git a/crates/arti-client/examples/lazy-init.rs b/crates/arti-client/examples/lazy-init.rs new file mode 100644 index 000000000..faef3acbb --- /dev/null +++ b/crates/arti-client/examples/lazy-init.rs @@ -0,0 +1,76 @@ +use anyhow::Result; +use arti_client::{BootstrapBehavior, TorClient, TorClientConfig}; +use tokio_crate as tokio; +use tor_rtcompat::tokio::TokioNativeTlsRuntime; + +use futures::io::{AsyncReadExt, AsyncWriteExt}; +use once_cell::sync::OnceCell; + +static TOR_CLIENT: OnceCell> = OnceCell::new(); + +/// Get a `TorClient` by copying the globally shared client stored in `TOR_CLIENT`. +/// If that client hasn't been initialized yet, initializes it first. +/// +/// # Errors +/// +/// Errors if called outside a Tokio runtime context, or creating the Tor client +/// failed. +pub fn get_tor_client() -> Result> { + let client = TOR_CLIENT.get_or_try_init(|| -> Result> { + // The client config includes things like where to store persistent Tor network state. + // The defaults provided are the same as the Arti standalone application, and save data + // to a conventional place depending on operating system (for example, ~/.local/share/arti + // on Linux platforms) + let config = TorClientConfig::default(); + + // Get a `tor_rtcompat::Runtime` from the currently running Tokio runtime. + let rt = TokioNativeTlsRuntime::current()?; + + eprintln!("creating unbootstrapped Tor client"); + + // Create an unbootstrapped Tor client. Bootstrapping will happen when the client is used, + // since we specified `BootstrapBehavior::Ondemand`. + Ok(TorClient::create_unbootstrapped( + rt, + config, + BootstrapBehavior::OnDemand, + )?) + })?; + + Ok(client.clone()) +} + +#[tokio::main] +async fn main() -> Result<()> { + // Arti uses the `tracing` crate for logging. Install a handler for this, to print Arti's logs. + tracing_subscriber::fmt::init(); + + eprintln!("getting shared Tor client..."); + + let tor_client = get_tor_client()?; + + eprintln!("connecting to example.com..."); + + // Initiate a connection over Tor to example.com, port 80. + let mut stream = tor_client.connect(("example.com", 80)).await?; + + eprintln!("sending request..."); + + stream + .write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n") + .await?; + + // IMPORTANT: Make sure the request was written. + // Arti buffers data, so flushing the buffer is usually required. + stream.flush().await?; + + eprintln!("reading response..."); + + // Read and print the result. + let mut buf = Vec::new(); + stream.read_to_end(&mut buf).await?; + + println!("{}", String::from_utf8_lossy(&buf)); + + Ok(()) +} diff --git a/crates/arti-client/src/client.rs b/crates/arti-client/src/client.rs index ae7cc79da..ffa5094ed 100644 --- a/crates/arti-client/src/client.rs +++ b/crates/arti-client/src/client.rs @@ -80,6 +80,29 @@ pub struct TorClient { /// mutex used to prevent two tasks from trying to bootstrap at once. bootstrap_in_progress: Arc>, + + /// Whether or not we should call `bootstrap` before doing things that require + /// bootstrapping. If this is `false`, we will just call `wait_for_bootstrap` + /// instead. + should_bootstrap: BootstrapBehavior, +} + +/// Preferences for whether a [`TorClient`] should bootstrap on its own or not. +/// +/// *[See the documentation for `create_unbootstrapped` for a full explanation.](TorClient::create_unbootstrapped)* +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum BootstrapBehavior { + /// Bootstrap the client automatically when requests are made that require the client to be + /// bootstrapped. + OnDemand, + /// Make no attempts to automatically bootstrap. [`TorClient::bootstrap`] must be manually + /// invoked in order for the [`TorClient`] to become useful. + /// + /// Attempts to use the client (e.g. by creating connections or resolving hosts over the Tor + /// network) before calling [`bootstrap`](TorClient::bootstrap) will fail, and + /// return an error that has kind [`ErrorKind::BootstrapRequired`](crate::ErrorKind::BootstrapRequired). + Manual, } /// Preferences for how to route a stream over the Tor network. @@ -301,25 +324,49 @@ impl TorClient { runtime: R, config: TorClientConfig, ) -> crate::Result> { - let ret = TorClient::create_unbootstrapped(runtime, config)?; + let ret = TorClient::create_unbootstrapped(runtime, config, BootstrapBehavior::Manual)?; ret.bootstrap().await?; Ok(ret) } - /// Create a `TorClient` without bootstrapping a connection to the network. The returned client - /// will not be usable until [`bootstrap`](TorClient::bootstrap) is called. + /// Create a `TorClient` without bootstrapping a connection to the network. + /// + /// The behaviour of this client depends on the value of `autobootstrap`. + /// + /// ## If `autobootstrap` is [`Manual`](BootstrapBehavior::Manual) + /// + /// If `autobootstrap` is set to `Manual`, the returned `TorClient` (and its clones) will never + /// attempt to bootstrap themselves. You must manually call [`bootstrap`](TorClient::bootstrap) + /// in order for the client(s) to become usable. /// /// Attempts to use the client (e.g. by creating connections or resolving hosts over the Tor /// network) before calling [`bootstrap`](TorClient::bootstrap) will fail, and - /// return an error that has kind [`ErrorKind::BootstrapRequired`](crate::ErrorKind::BootstrapRequired). However, if a bootstrap - /// attempt is in progress but not complete, attempts to use the client will block instead. - pub fn create_unbootstrapped(runtime: R, config: TorClientConfig) -> crate::Result { - TorClient::create_inner(runtime, config).map_err(ErrorDetail::into) + /// return an error that has kind [`ErrorKind::BootstrapRequired`](crate::ErrorKind::BootstrapRequired). + /// + /// This option is useful if you wish to have control over the bootstrap process (for example, + /// you might wish to avoid initiating network connections until explicit user confirmation + /// is given). + /// + /// ## If `autobootstrap` is [`OnDemand`](BootstrapBehavior::OnDemand) + /// + /// If `autoboostrap` is set to `OnDemand`, the returned `TorClient` (and its clones) will + /// automatically bootstrap before doing any operation that would require them to be + /// bootstrapped. + pub fn create_unbootstrapped( + runtime: R, + config: TorClientConfig, + autobootstrap: BootstrapBehavior, + ) -> crate::Result { + TorClient::create_inner(runtime, config, autobootstrap).map_err(ErrorDetail::into) } /// Implementation of `create_unbootstrapped`, split out in order to avoid manually specifying /// double error conversions. - fn create_inner(runtime: R, config: TorClientConfig) -> StdResult { + fn create_inner( + runtime: R, + config: TorClientConfig, + autobootstrap: BootstrapBehavior, + ) -> StdResult { let circ_cfg = config.get_circmgr_config()?; let dir_cfg = config.get_dirmgr_config()?; let statemgr = FsStateMgr::from_path(config.storage.expand_state_dir()?)?; @@ -405,6 +452,7 @@ impl TorClient { reconfigure_lock: Arc::new(Mutex::new(())), status_receiver, bootstrap_in_progress: Arc::new(AsyncMutex::new(())), + should_bootstrap: autobootstrap, }) } @@ -454,12 +502,25 @@ impl TorClient { Ok(()) } + /// ## For `BootstrapBehavior::Ondemand` clients + /// + /// Initiate a bootstrap by calling `bootstrap` (which is idempotent, so attempts to + /// bootstrap twice will just do nothing). + /// + /// ## For `BootstrapBehavior::Manual` clients + /// /// Check whether a bootstrap is in progress; if one is, wait until it finishes /// and then return. (Otherwise, return immediately.) async fn wait_for_bootstrap(&self) -> StdResult<(), ErrorDetail> { - // Grab the lock, and immediately release it. That will ensure that nobody else is trying to bootstrap. - self.bootstrap_in_progress.lock().await; - + match self.should_bootstrap { + BootstrapBehavior::OnDemand => { + self.bootstrap_inner().await?; + } + BootstrapBehavior::Manual => { + // Grab the lock, and immediately release it. That will ensure that nobody else is trying to bootstrap. + self.bootstrap_in_progress.lock().await; + } + } Ok(()) } @@ -1038,7 +1099,7 @@ mod test { let cfg = TorClientConfigBuilder::from_directories(state_dir, cache_dir) .build() .unwrap(); - let _ = TorClient::create_unbootstrapped(rt, cfg).unwrap(); + let _ = TorClient::create_unbootstrapped(rt, cfg, BootstrapBehavior::Manual).unwrap(); }); } @@ -1050,7 +1111,8 @@ mod test { let cfg = TorClientConfigBuilder::from_directories(state_dir, cache_dir) .build() .unwrap(); - let client = TorClient::create_unbootstrapped(rt, cfg).unwrap(); + let client = + TorClient::create_unbootstrapped(rt, cfg, BootstrapBehavior::Manual).unwrap(); let result = client.connect("example.com:80").await; assert!(result.is_err()); assert_eq!(result.err().unwrap().kind(), ErrorKind::BootstrapRequired); diff --git a/crates/arti-client/src/lib.rs b/crates/arti-client/src/lib.rs index f45459fe1..8c50e189f 100644 --- a/crates/arti-client/src/lib.rs +++ b/crates/arti-client/src/lib.rs @@ -196,7 +196,7 @@ pub mod config; pub mod status; pub use address::{DangerouslyIntoTorAddr, IntoTorAddr, TorAddr, TorAddrError}; -pub use client::{StreamPrefs, TorClient}; +pub use client::{BootstrapBehavior, StreamPrefs, TorClient}; pub use config::TorClientConfig; pub use tor_circmgr::IsolationToken; diff --git a/crates/tor-dirmgr/src/lib.rs b/crates/tor-dirmgr/src/lib.rs index 9c072c1f7..69b41001c 100644 --- a/crates/tor-dirmgr/src/lib.rs +++ b/crates/tor-dirmgr/src/lib.rs @@ -78,7 +78,7 @@ use tor_netdoc::doc::netstatus::ConsensusFlavor; use futures::{channel::oneshot, task::SpawnExt}; use tor_rtcompat::{Runtime, SleepProviderExt}; -use tracing::{info, trace, warn}; +use tracing::{debug, info, trace, warn}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; @@ -270,7 +270,7 @@ impl DirMgr { .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) .is_err() { - info!("Attempted to bootstrap twice; ignoring."); + debug!("Attempted to bootstrap twice; ignoring."); return Ok(()); }