#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![warn(missing_docs)]
#![warn(noop_method_call)]
#![warn(unreachable_pub)]
#![warn(clippy::all)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::checked_conversions)]
#![warn(clippy::cognitive_complexity)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::implicit_clone)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::needless_pass_by_value)]
#![warn(clippy::option_option)]
#![deny(clippy::print_stderr)]
#![deny(clippy::print_stdout)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::trait_duplication_in_bounds)]
#![deny(clippy::unchecked_time_subtraction)]
#![deny(clippy::unnecessary_wraps)]
#![warn(clippy::unseparated_literal_suffix)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::mod_module_files)]
#![allow(clippy::let_unit_value)] #![allow(clippy::uninlined_format_args)]
#![allow(clippy::significant_drop_in_scrutinee)] #![allow(clippy::result_large_err)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::needless_lifetimes)] #![allow(mismatched_lifetime_syntaxes)] #![allow(clippy::collapsible_if)] #![deny(clippy::unused_async)]
use std::sync::{Arc, Mutex};
use arti_client::{IntoTorAddr, TorClient};
use ureq::{
http::{Uri, uri::Scheme},
tls::TlsProvider as UreqTlsProvider,
unversioned::{
resolver::{ArrayVec, ResolvedSocketAddrs, Resolver as UreqResolver},
transport::{Buffers, Connector as UreqConnector, LazyBuffers, NextTimeout, Transport},
},
};
use educe::Educe;
use thiserror::Error;
use tor_proto::client::stream::{DataReader, DataWriter};
use tor_rtcompat::{Runtime, ToplevelBlockOn};
#[cfg(feature = "rustls")]
use ureq::unversioned::transport::RustlsConnector;
#[cfg(feature = "native-tls")]
use ureq::unversioned::transport::NativeTlsConnector;
use futures::io::{AsyncReadExt, AsyncWriteExt};
pub use arti_client;
pub use tor_rtcompat;
pub use ureq;
pub fn default_agent() -> Result<ureq::Agent, Error> {
Ok(Connector::new()?.agent())
}
#[derive(Educe)]
#[educe(Debug)]
pub struct Connector<R: Runtime> {
#[educe(Debug(ignore))]
client: Arc<TorClient<R>>,
tls_provider: UreqTlsProvider,
}
pub struct ConnectorBuilder<R: Runtime> {
client: Option<Arc<TorClient<R>>>,
runtime: R,
tls_provider: Option<UreqTlsProvider>,
}
#[derive(Educe)]
#[educe(Debug)]
struct HttpTransport<R: Runtime> {
r: Arc<Mutex<DataReader>>,
w: Arc<Mutex<DataWriter>>,
#[educe(Debug(ignore))]
buffer: LazyBuffers,
rt: R,
}
#[derive(Educe)]
#[educe(Debug)]
pub struct Resolver<R: Runtime> {
#[educe(Debug(ignore))]
client: Arc<TorClient<R>>,
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("unsupported URI scheme in {uri:?}")]
UnsupportedUriScheme {
uri: Uri,
},
#[error("Missing hostname in {uri:?}")]
MissingHostname {
uri: Uri,
},
#[error("Tor connection failed")]
Arti(#[from] arti_client::Error),
#[error("General I/O error")]
Io(#[from] std::io::Error),
#[error("TLS provider in config does not match the one in Connector.")]
TlsConfigMismatch,
}
impl tor_error::HasKind for Error {
#[rustfmt::skip]
fn kind(&self) -> tor_error::ErrorKind {
use tor_error::ErrorKind as EK;
match self {
Error::UnsupportedUriScheme{..} => EK::NotImplemented,
Error::MissingHostname{..} => EK::BadApiUsage,
Error::Arti(e) => e.kind(),
Error::Io(..) => EK::Other,
Error::TlsConfigMismatch => EK::BadApiUsage,
}
}
}
impl std::convert::From<Error> for ureq::Error {
fn from(err: Error) -> Self {
match err {
Error::MissingHostname { uri } => {
ureq::Error::BadUri(format!("Missing hostname in {uri:?}"))
}
Error::UnsupportedUriScheme { uri } => {
ureq::Error::BadUri(format!("Unsupported URI scheme in {uri:?}"))
}
Error::Arti(e) => ureq::Error::Io(std::io::Error::other(e)), Error::Io(e) => ureq::Error::Io(e),
Error::TlsConfigMismatch => {
ureq::Error::Tls("TLS provider in config does not match the one in Connector.")
}
}
}
}
impl<R: Runtime + ToplevelBlockOn> Transport for HttpTransport<R> {
fn buffers(&mut self) -> &mut dyn Buffers {
&mut self.buffer
}
fn transmit_output(&mut self, amount: usize, _timeout: NextTimeout) -> Result<(), ureq::Error> {
let mut writer = self.w.lock().expect("lock poisoned");
let buffer = self.buffer.output();
let data_to_write = &buffer[..amount];
self.rt.block_on(async {
writer.write_all(data_to_write).await?;
writer.flush().await?;
Ok(())
})
}
fn await_input(&mut self, _timeout: NextTimeout) -> Result<bool, ureq::Error> {
let mut reader = self.r.lock().expect("lock poisoned");
let buffers = self.buffer.input_append_buf();
let size = self.rt.block_on(reader.read(buffers))?;
self.buffer.input_appended(size);
Ok(size > 0)
}
fn is_open(&mut self) -> bool {
self.r.lock().is_ok_and(|guard| {
guard
.client_stream_ctrl()
.is_some_and(|ctrl| ctrl.is_connected())
})
}
}
impl ConnectorBuilder<tor_rtcompat::PreferredRuntime> {
pub fn new() -> Result<Self, Error> {
Ok(ConnectorBuilder {
client: None,
runtime: tor_rtcompat::PreferredRuntime::create()?,
tls_provider: None,
})
}
}
impl<R: Runtime> ConnectorBuilder<R> {
pub fn build(self) -> Result<Connector<R>, Error> {
let client = match self.client {
Some(client) => client,
None => TorClient::with_runtime(self.runtime).create_unbootstrapped()?,
};
let tls_provider = self.tls_provider.unwrap_or(get_default_tls_provider());
Ok(Connector {
client,
tls_provider,
})
}
pub fn with_runtime(runtime: R) -> Result<ConnectorBuilder<R>, Error> {
Ok(ConnectorBuilder {
client: None,
runtime,
tls_provider: None,
})
}
pub fn tor_client(mut self, client: Arc<TorClient<R>>) -> ConnectorBuilder<R> {
self.runtime = client.runtime().clone();
self.client = Some(client);
self
}
pub fn tls_provider(mut self, tls_provider: UreqTlsProvider) -> Self {
self.tls_provider = Some(tls_provider);
self
}
}
impl<R: Runtime + ToplevelBlockOn> UreqResolver for Resolver<R> {
fn resolve(
&self,
uri: &Uri,
_config: &ureq::config::Config,
_timeout: NextTimeout,
) -> Result<ResolvedSocketAddrs, ureq::Error> {
let (host, port) = uri_to_host_port(uri)?;
let ips = self
.client
.runtime()
.block_on(async { self.client.resolve(&host).await })
.map_err(Error::from)?;
let mut array_vec: ArrayVec<core::net::SocketAddr, 16> = ArrayVec::from_fn(|_| {
core::net::SocketAddr::new(core::net::IpAddr::V4(core::net::Ipv4Addr::UNSPECIFIED), 0)
});
for ip in ips {
let socket_addr = core::net::SocketAddr::new(ip, port);
array_vec.push(socket_addr);
}
Ok(array_vec)
}
}
impl<R: Runtime + ToplevelBlockOn> Connector<R> {
pub fn with_tor_client(client: Arc<TorClient<R>>) -> Connector<R> {
Connector {
client,
tls_provider: get_default_tls_provider(),
}
}
}
impl<R: Runtime + ToplevelBlockOn> UreqConnector<()> for Connector<R> {
type Out = Box<dyn Transport>;
fn connect(
&self,
details: &ureq::unversioned::transport::ConnectionDetails,
_chained: Option<()>,
) -> Result<Option<Self::Out>, ureq::Error> {
let (host, port) = uri_to_host_port(details.uri)?;
let addr = (host.as_str(), port)
.into_tor_addr()
.map_err(|e| Error::Arti(e.into()))?;
let stream = self
.client
.runtime()
.block_on(async { self.client.connect(addr).await })
.map_err(Error::from)?;
let (r, w) = stream.split();
Ok(Some(Box::new(HttpTransport {
r: Arc::new(Mutex::new(r)),
w: Arc::new(Mutex::new(w)),
buffer: LazyBuffers::new(2048, 2048),
rt: self.client.runtime().clone(),
})))
}
}
impl Connector<tor_rtcompat::PreferredRuntime> {
pub fn new() -> Result<Self, Error> {
Self::builder()?.build()
}
}
impl<R: Runtime + ToplevelBlockOn> Connector<R> {
pub fn resolver(&self) -> Resolver<R> {
Resolver {
client: self.client.clone(),
}
}
pub fn agent(self) -> ureq::Agent {
let resolver = self.resolver();
let ureq_config = ureq::config::Config::builder()
.tls_config(
ureq::tls::TlsConfig::builder()
.provider(self.tls_provider)
.build(),
)
.build();
ureq::Agent::with_parts(ureq_config, self.connector_chain(), resolver)
}
pub fn agent_with_ureq_config(
self,
config: ureq::config::Config,
) -> Result<ureq::Agent, Error> {
let resolver = self.resolver();
if self.tls_provider != config.tls_config().provider() {
return Err(Error::TlsConfigMismatch);
}
Ok(ureq::Agent::with_parts(
config,
self.connector_chain(),
resolver,
))
}
fn connector_chain(self) -> impl UreqConnector {
let chain = self;
#[cfg(feature = "rustls")]
let chain = chain.chain(RustlsConnector::default());
#[cfg(feature = "native-tls")]
let chain = chain.chain(NativeTlsConnector::default());
chain
}
}
pub fn get_default_tls_provider() -> UreqTlsProvider {
if cfg!(feature = "native-tls") {
UreqTlsProvider::NativeTls
} else {
UreqTlsProvider::Rustls
}
}
impl Connector<tor_rtcompat::PreferredRuntime> {
pub fn builder() -> Result<ConnectorBuilder<tor_rtcompat::PreferredRuntime>, Error> {
ConnectorBuilder::new()
}
}
fn uri_to_host_port(uri: &Uri) -> Result<(String, u16), Error> {
let host = uri
.host()
.ok_or_else(|| Error::MissingHostname { uri: uri.clone() })?;
let port = match uri.scheme() {
Some(scheme) if scheme == &Scheme::HTTPS => Ok(443),
Some(scheme) if scheme == &Scheme::HTTP => Ok(80),
Some(_) => Err(Error::UnsupportedUriScheme { uri: uri.clone() }),
None => Err(Error::UnsupportedUriScheme { uri: uri.clone() }),
}?;
Ok((host.to_owned(), port))
}
#[cfg(test)]
mod arti_ureq_test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use arti_client::config::TorClientConfigBuilder;
use std::str::FromStr;
use test_temp_dir::test_temp_dir;
const ARTI_TEST_LIVE_NETWORK: &str = "ARTI_TEST_LIVE_NETWORK";
const ARTI_TESTING_ON_LOCAL: &str = "ARTI_TESTING_ON_LOCAL";
fn assert_equal_types<T>(_: &T, _: &T) {}
fn test_live_network() -> bool {
let run_test = std::env::var(ARTI_TEST_LIVE_NETWORK).is_ok_and(|v| v == "1");
if !run_test {
println!("Skipping test, set {}=1 to run.", ARTI_TEST_LIVE_NETWORK);
}
run_test
}
fn testing_on_local() -> bool {
let run_test = std::env::var(ARTI_TESTING_ON_LOCAL).is_ok_and(|v| v == "1");
if !run_test {
println!("Skipping test, set {}=1 to run.", ARTI_TESTING_ON_LOCAL);
}
run_test
}
fn test_with_tor_client<R: Runtime>(rt: R, f: impl FnOnce(Arc<TorClient<R>>)) {
let temp_dir = test_temp_dir!();
temp_dir.used_by(move |temp_dir| {
let arti_config = TorClientConfigBuilder::from_directories(
temp_dir.join("state"),
temp_dir.join("cache"),
)
.build()
.expect("Failed to build TorClientConfig");
let tor_client = arti_client::TorClient::with_runtime(rt)
.config(arti_config)
.create_unbootstrapped()
.expect("Error creating Tor Client.");
f(tor_client);
});
}
fn request_is_tor(agent: ureq::Agent, https: bool) -> bool {
let mut request = agent
.get(format!(
"http{}://check.torproject.org/api/ip",
if https { "s" } else { "" }
))
.call()
.expect("Failed to make request.");
let response = request
.body_mut()
.read_to_string()
.expect("Failed to read body.");
let json_response: serde_json::Value =
serde_json::from_str(&response).expect("Failed to parse JSON.");
json_response
.get("IsTor")
.expect("Failed to retrieve IsTor property from response")
.as_bool()
.expect("Failed to convert IsTor to bool")
}
#[test]
fn test_equal_types() {
assert_equal_types(&1, &i32::MIN);
assert_equal_types(&1, &i64::MIN);
assert_equal_types(&String::from("foo"), &String::with_capacity(1));
}
#[test]
#[cfg(all(feature = "rustls", not(feature = "native-tls")))]
fn articonnector_new_returns_default() {
if !testing_on_local() {
return;
}
let actual_connector = Connector::new().expect("Failed to create Connector.");
let expected_connector = Connector {
client: TorClient::with_runtime(
tor_rtcompat::PreferredRuntime::create().expect("Failed to create runtime."),
)
.create_unbootstrapped()
.expect("Error creating Tor Client."),
tls_provider: UreqTlsProvider::Rustls,
};
assert_equal_types(&expected_connector, &actual_connector);
assert_equal_types(
&actual_connector.client.runtime().clone(),
&tor_rtcompat::PreferredRuntime::create().expect("Failed to create runtime."),
);
assert_eq!(
&actual_connector.tls_provider,
&ureq::tls::TlsProvider::Rustls,
);
}
#[test]
#[cfg(all(feature = "rustls", not(feature = "native-tls")))]
fn articonnector_with_tor_client() {
if !testing_on_local() {
return;
}
let tor_client = TorClient::with_runtime(
tor_rtcompat::PreferredRuntime::create().expect("Failed to create runtime."),
)
.create_unbootstrapped()
.expect("Error creating Tor Client.");
let actual_connector = Connector::with_tor_client(tor_client);
let expected_connector = Connector {
client: TorClient::with_runtime(
tor_rtcompat::PreferredRuntime::create().expect("Failed to create runtime."),
)
.create_unbootstrapped()
.expect("Error creating Tor Client."),
tls_provider: UreqTlsProvider::Rustls,
};
assert_equal_types(&expected_connector, &actual_connector);
assert_equal_types(
&actual_connector.client.runtime().clone(),
&tor_rtcompat::PreferredRuntime::create().expect("Failed to create runtime."),
);
assert_eq!(
&actual_connector.tls_provider,
&ureq::tls::TlsProvider::Rustls,
);
}
#[test]
fn articonnectorbuilder_new_returns_default() {
if !testing_on_local() {
return;
}
let expected = Connector::new().expect("Failed to create Connector.");
let actual = Connector::<tor_rtcompat::PreferredRuntime>::builder()
.expect("Failed to create ConnectorBuilder.")
.build()
.expect("Failed to create Connector.");
assert_equal_types(&expected, &actual);
assert_equal_types(&expected.client.runtime(), &actual.client.runtime());
assert_eq!(&expected.tls_provider, &actual.tls_provider);
}
#[cfg(all(feature = "tokio", feature = "rustls"))]
#[test]
fn articonnectorbuilder_with_runtime() {
if !testing_on_local() {
return;
}
let arti_connector = ConnectorBuilder::with_runtime(
tor_rtcompat::tokio::TokioRustlsRuntime::create().expect("Failed to create runtime."),
)
.expect("Failed to create ConnectorBuilder.")
.build()
.expect("Failed to create Connector.");
assert_equal_types(
&arti_connector.client.runtime().clone(),
&tor_rtcompat::tokio::TokioRustlsRuntime::create().expect("Failed to create runtime."),
);
let arti_connector = ConnectorBuilder::with_runtime(
tor_rtcompat::PreferredRuntime::create().expect("Failed to create runtime."),
)
.expect("Failed to create ConnectorBuilder.")
.build()
.expect("Failed to create Connector.");
assert_equal_types(
&arti_connector.client.runtime().clone(),
&tor_rtcompat::PreferredRuntime::create().expect("Failed to create runtime."),
);
}
#[cfg(all(feature = "tokio", feature = "rustls"))]
#[test]
fn articonnectorbuilder_set_tor_client() {
let rt =
tor_rtcompat::tokio::TokioRustlsRuntime::create().expect("Failed to create runtime.");
test_with_tor_client(rt.clone(), move |tor_client| {
let arti_connector = ConnectorBuilder::with_runtime(rt)
.expect("Failed to create ConnectorBuilder.")
.tor_client(tor_client.clone().isolated_client())
.build()
.expect("Failed to create Connector.");
assert_equal_types(
&arti_connector.client.runtime().clone(),
&tor_rtcompat::tokio::TokioRustlsRuntime::create()
.expect("Failed to create runtime."),
);
});
}
#[test]
fn test_uri_to_host_port() {
let uri = Uri::from_str("http://torproject.org").expect("Error parsing uri.");
let (host, port) = uri_to_host_port(&uri).expect("Error parsing uri.");
assert_eq!(host, "torproject.org");
assert_eq!(port, 80);
let uri = Uri::from_str("https://torproject.org").expect("Error parsing uri.");
let (host, port) = uri_to_host_port(&uri).expect("Error parsing uri.");
assert_eq!(host, "torproject.org");
assert_eq!(port, 443);
let uri = Uri::from_str("https://www.torproject.org/test").expect("Error parsing uri.");
let (host, port) = uri_to_host_port(&uri).expect("Error parsing uri.");
assert_eq!(host, "www.torproject.org");
assert_eq!(port, 443);
}
#[test]
fn request_goes_over_tor() {
if !test_live_network() {
return;
}
let is_tor = request_is_tor(
default_agent().expect("Failed to retrieve default agent."),
true,
);
assert_eq!(is_tor, true);
}
#[test]
#[cfg(all(feature = "rustls", not(feature = "native-tls")))]
fn request_goes_over_tor_with_unsafe_check() {
if !test_live_network() {
return;
}
let is_tor = request_is_tor(ureq::Agent::new_with_defaults(), true);
assert_eq!(is_tor, false);
let is_tor = request_is_tor(
default_agent().expect("Failed to retrieve default agent."),
true,
);
assert_eq!(is_tor, true);
}
#[test]
fn request_with_bare_http() {
if !test_live_network() {
return;
}
let rt = tor_rtcompat::PreferredRuntime::create().expect("Failed to create runtime.");
test_with_tor_client(rt, |tor_client| {
let arti_connector = Connector::with_tor_client(tor_client);
let is_tor = request_is_tor(arti_connector.agent(), false);
assert_eq!(is_tor, true);
});
}
#[test]
fn test_get_default_tls_provider() {
#[cfg(feature = "native-tls")]
assert_eq!(get_default_tls_provider(), UreqTlsProvider::NativeTls);
#[cfg(not(feature = "native-tls"))]
assert_eq!(get_default_tls_provider(), UreqTlsProvider::Rustls);
}
#[test]
fn test_tor_client_with_get_default_tls_provider() {
if !testing_on_local() {
return;
}
let tor_client = TorClient::with_runtime(
tor_rtcompat::PreferredRuntime::create().expect("Failed to create runtime."),
)
.create_unbootstrapped()
.expect("Error creating Tor Client.");
let arti_connector = Connector::<tor_rtcompat::PreferredRuntime>::builder()
.expect("Failed to create ConnectorBuilder.")
.tor_client(tor_client.clone().isolated_client())
.tls_provider(get_default_tls_provider())
.build()
.expect("Failed to create Connector.");
#[cfg(feature = "native-tls")]
assert_eq!(
&arti_connector.tls_provider,
&ureq::tls::TlsProvider::NativeTls,
);
#[cfg(not(feature = "native-tls"))]
assert_eq!(
&arti_connector.tls_provider,
&ureq::tls::TlsProvider::Rustls,
);
}
}