diff --git a/Cargo.toml b/Cargo.toml index 589a0af..a7e6a47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,14 @@ bitcoin = { version = "0.30.0", features = ["serde", "std"], default-features = # Temporary dependency on internals until the rust-bitcoin devs release the hex-conservative crate. bitcoin-internals = { version = "0.1.0", features = ["alloc"] } log = "^0.4" -ureq = { version = "2.5.0", features = ["json"], optional = true } +ureq = { version = "2.5.0", optional = true, features = ["json"]} reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] } +hyper = { version = "0.14", optional = true, features = ["http1", "client", "runtime"], default-features = false } +arti-client = { version = "0.12.0", optional = true } +tor-rtcompat = { version = "0.9.6", optional = true, features = ["tokio"]} +tls-api = { version = "0.9.0", optional = true } +tls-api-native-tls = { version = "0.9.0", optional = true } +arti-hyper = { version = "0.12.0", optional = true, features = ["default"] } [dev-dependencies] serde_json = "1.0" @@ -36,10 +42,14 @@ zip = "=0.6.3" base64ct = "<1.6.0" [features] -default = ["blocking", "async", "async-https"] +default = ["blocking", "async", "async-https", "async-arti-hyper"] blocking = ["ureq", "ureq/socks-proxy"] async = ["reqwest", "reqwest/socks"] async-https = ["async", "reqwest/default-tls"] async-https-native = ["async", "reqwest/native-tls"] async-https-rustls = ["async", "reqwest/rustls-tls"] async-https-rustls-manual-roots = ["async", "reqwest/rustls-tls-manual-roots"] +# TODO: (@leonardo) Should I rename it to async-anonymized ? +async-arti-hyper = ["hyper", "arti-client", "tor-rtcompat", "tls-api", "tls-api-native-tls", "arti-hyper"] +async-arti-hyper-native = ["async-arti-hyper", "arti-hyper/native-tls"] +async-arti-hyper-rustls = ["async-arti-hyper", "arti-hyper/rustls"] \ No newline at end of file diff --git a/src/async.rs b/src/async.rs index fcdf23e..1a98d13 100644 --- a/src/async.rs +++ b/src/async.rs @@ -9,11 +9,14 @@ // You may not use this file except in accordance with one or both of these // licenses. -//! Esplora by way of `reqwest` HTTP client. +//! Esplora by way of `reqwest`, and `arti-hyper` HTTP client. use std::collections::HashMap; use std::str::FromStr; +use arti_client::{TorClient, TorClientConfig}; + +use arti_hyper::ArtiHttpConnector; use bitcoin::consensus::{deserialize, serialize}; use bitcoin::hashes::hex::FromHex; use bitcoin::hashes::{sha256, Hash}; @@ -22,10 +25,14 @@ use bitcoin::{ }; use bitcoin_internals::hex::display::DisplayHex; +use hyper::Body; #[allow(unused_imports)] use log::{debug, error, info, trace}; use reqwest::{Client, StatusCode}; +use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder}; +use tls_api_native_tls::TlsConnector; +use tor_rtcompat::PreferredRuntime; use crate::{BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus}; @@ -429,3 +436,42 @@ impl AsyncClient { &self.client } } + +#[derive(Debug, Clone)] +pub struct AsyncAnonymizedClient { + url: String, + client: hyper::Client>, +} + +impl AsyncAnonymizedClient { + /// build an async [`TorClient`] with default Tor configuration + async fn create_tor_client() -> Result, arti_client::Error> { + let config = TorClientConfig::default(); + TorClient::create_bootstrapped(config).await + } + + /// build an [`AsyncAnonymizedClient`] from a [`Builder`] + pub async fn from_builder(builder: Builder) -> Result { + let tor_client = Self::create_tor_client().await?.isolated_client(); + + // TODO: (@leonardo) how to improve this error handling/propagation ? + let tls_conn: TlsConnector = TlsConnector::builder() + .map_err(|_| Error::TlsConnector)? + .build() + .map_err(|_| Error::TlsConnector)?; + + let connector = ArtiHttpConnector::new(tor_client, tls_conn); + + // TODO: (@leonardo) how to handle/pass the timeout option ? + let client = hyper::Client::builder().build::<_, Body>(connector); + Ok(Self::from_client(builder.base_url, client)) + } + + /// build an async client from the base url and [`Client`] + pub fn from_client( + url: String, + client: hyper::Client>, + ) -> Self { + AsyncAnonymizedClient { url, client } + } +} diff --git a/src/lib.rs b/src/lib.rs index a5fc313..9701ae6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,9 @@ //! async Esplora client to query Esplora's backend. //! //! The library provides the possibility to build a blocking -//! client using [`ureq`] and an async client using [`reqwest`]. -//! The library supports communicating to Esplora via a proxy +//! client using [`ureq`], an async client using [`reqwest`], +//! and an anonymized async client using [`arti-hyper`]. +//! The library supports communicating to Esplora via a Tor, proxy, //! and also using TLS (SSL) for secure communication. //! //! @@ -35,6 +36,19 @@ //! # } //! ``` //! +//! Here is an example of how to create an anonymized asynchronous client. +//! +//! ```no_run +//! # #[cfg(feature = "arti-hyper")] +//! # { +//! use esplora_client::Builder; +//! let builder = Builder::new("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/testnet/api"); +//! let async_client = builder.build_async_anonymized(); +//! # Ok::<(), esplora_client::Error>(()); +//! # } +//! ``` +//! +//! //! ## Features //! //! By default the library enables all features. To specify @@ -54,6 +68,12 @@ //! * `async-https-rustls-manual-roots` enables [`reqwest`], the async client with support for //! proxying and TLS (SSL) using the `rustls` TLS backend without using its the default root //! certificates. +//! * `async-arti-hyper` enables [`arti-hyper`], the async anonymized client support for TLS (SSL) over Tor, +//! using the default [`arti-hyper`] TLS backend. +//! * `async-arti-hyper-native` enables [`arti-hyper`], the async anonymized client support for TLS (SSL) over Tor, +//! using the platform's native TLS backend (likely OpenSSL). +//! * `async-arti-hyper-rustls` enables [`arti-hyper`], the async anonymized client support for TLS (SSL) over Tor, +//! using the `rustls` TLS backend without using its the default root certificates. //! //! @@ -77,6 +97,8 @@ pub use api::*; pub use blocking::BlockingClient; #[cfg(feature = "async")] pub use r#async::AsyncClient; +#[cfg(feature = "async-arti-hyper")] +pub use r#async::AsyncAnonymizedClient; /// Get a fee value in sats/vbytes from the estimates /// that matches the confirmation target set as parameter. @@ -109,7 +131,7 @@ pub struct Builder { /// the `socks` feature enabled. /// /// The proxy is ignored when targeting `wasm32`. - pub proxy: Option, + pub proxy: Option, // TODO: (@leonardo) should this be available for `async-arti-hyper` /// Socket timeout. pub timeout: Option, } @@ -147,6 +169,11 @@ impl Builder { pub fn build_async(self) -> Result { AsyncClient::from_builder(self) } + + // build an asynchronous anonymized (Tor) client from builder + pub async fn build_async_anonymized(self) -> Result { + AsyncAnonymizedClient::from_builder(self).await + } } /// Errors that can happen during a sync with `Esplora` @@ -161,6 +188,12 @@ pub enum Error { /// Error during reqwest HTTP request #[cfg(feature = "async")] Reqwest(::reqwest::Error), + /// Error during Tor client creation + #[cfg(feature = "async-arti-hyper")] + ArtiClient(::arti_client::Error), + /// Error during TlsConnector building + #[cfg(feature = "async-arti-hyper")] + TlsConnector, /// HTTP response error HttpResponse { status: u16, message: String }, /// IO error during ureq response read @@ -206,6 +239,8 @@ impl std::error::Error for Error {} impl_error!(::ureq::Transport, UreqTransport, Error); #[cfg(feature = "async")] impl_error!(::reqwest::Error, Reqwest, Error); +#[cfg(feature = "async-arti-hyper")] +impl_error!(::arti_client::Error, ArtiClient, Error); impl_error!(io::Error, Io, Error); impl_error!(std::num::ParseIntError, Parsing, Error); impl_error!(consensus::encode::Error, BitcoinEncoding, Error);