zcash_client_backend: Add internal HTTP GET support to `tor::Client`

This commit is contained in:
Jack Grigg 2024-06-14 11:48:18 +00:00
parent 1e5b62bfce
commit b3de2ab81b
7 changed files with 292 additions and 10 deletions

26
Cargo.lock generated
View File

@ -1910,9 +1910,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.4.1"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
dependencies = [
"bytes",
"futures-channel",
@ -1944,9 +1944,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.6"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956"
checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
dependencies = [
"bytes",
"futures-channel",
@ -4136,6 +4136,16 @@ dependencies = [
"syn 2.0.63",
]
[[package]]
name = "tokio-rustls"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.15"
@ -5590,6 +5600,9 @@ dependencies = [
"group",
"gumdrop",
"hex",
"http-body-util",
"hyper",
"hyper-util",
"incrementalmerkletree",
"jubjub",
"memuse",
@ -5599,18 +5612,23 @@ dependencies = [
"percent-encoding",
"proptest",
"prost",
"rand 0.8.5",
"rand_core 0.6.4",
"rayon",
"sapling-crypto",
"secrecy",
"serde",
"serde_json",
"shardtree",
"subtle",
"time",
"tokio",
"tokio-rustls",
"tonic",
"tonic-build",
"tor-rtcompat",
"tracing",
"webpki-roots",
"which",
"zcash_address",
"zcash_encoding",

View File

@ -85,6 +85,15 @@ bs58 = { version = "0.5", features = ["check"] }
byteorder = "1"
hex = "0.4"
percent-encoding = "2.1.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# HTTP
hyper = "1"
http-body-util = "0.1"
hyper-util = { version = "0.1.1", features = ["tokio"] }
tokio-rustls = "0.24"
webpki-roots = "0.25"
# Logging and metrics
memuse = "0.2.1"

View File

@ -392,10 +392,6 @@ criteria = "safe-to-deploy"
version = "0.9.0"
criteria = "safe-to-deploy"
[[exemptions.digest]]
version = "0.10.7"
criteria = "safe-to-deploy"
[[exemptions.directories]]
version = "5.0.1"
criteria = "safe-to-deploy"
@ -589,7 +585,7 @@ version = "1.1.1"
criteria = "safe-to-deploy"
[[exemptions.hyper]]
version = "1.4.1"
version = "1.3.1"
criteria = "safe-to-deploy"
[[exemptions.hyper-timeout]]
@ -597,7 +593,7 @@ version = "0.4.1"
criteria = "safe-to-deploy"
[[exemptions.hyper-util]]
version = "0.1.6"
version = "0.1.5"
criteria = "safe-to-deploy"
[[exemptions.iana-time-zone]]

View File

@ -365,6 +365,11 @@ who = "Benjamin Bouvier <public@benj.me>"
criteria = "safe-to-deploy"
version = "0.1.3"
[[audits.bytecode-alliance.audits.digest]]
who = "Benjamin Bouvier <public@benj.me>"
criteria = "safe-to-deploy"
delta = "0.9.0 -> 0.10.3"
[[audits.bytecode-alliance.audits.ed25519]]
who = "Alex Crichton <alex@alexcrichton.com>"
criteria = "safe-to-deploy"
@ -570,6 +575,12 @@ criteria = "safe-to-deploy"
version = "1.1.4"
notes = "uses unsafe to implement thread local storage of objects"
[[audits.bytecode-alliance.audits.tokio-rustls]]
who = "Pat Hickey <phickey@fastly.com>"
criteria = "safe-to-deploy"
version = "0.24.0"
notes = "no unsafe, no build, no ambient capabilities"
[[audits.bytecode-alliance.audits.tracing-subscriber]]
who = "Pat Hickey <phickey@fastly.com>"
criteria = "safe-to-deploy"
@ -1375,6 +1386,11 @@ who = "David Cook <dcook@divviup.org>"
criteria = "safe-to-deploy"
version = "0.2.2"
[[audits.isrg.audits.digest]]
who = "David Cook <dcook@divviup.org>"
criteria = "safe-to-deploy"
delta = "0.10.6 -> 0.10.7"
[[audits.isrg.audits.either]]
who = "David Cook <dcook@divviup.org>"
criteria = "safe-to-deploy"
@ -1697,6 +1713,12 @@ otherwise the unsafety is documented and left to the caller to verify.
"""
aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
[[audits.mozilla.audits.digest]]
who = "Mike Hommey <mh+mozilla@glandium.org>"
criteria = "safe-to-deploy"
delta = "0.10.3 -> 0.10.6"
aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
[[audits.mozilla.audits.displaydoc]]
who = "Makoto Kato <m_kato@ga2.so-net.ne.jp>"
criteria = "safe-to-deploy"

View File

@ -100,6 +100,8 @@ nom = "7"
# `hyper::Error`, `hyper::http::Error`, `serde_json::Error`. We could avoid this with
# changes to error handling.
arti-client = { workspace = true, optional = true }
hyper = { workspace = true, optional = true, features = ["client", "http1"] }
serde_json = { workspace = true, optional = true }
# Dependencies used internally:
# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.)
@ -118,6 +120,14 @@ rayon.workspace = true
tokio = { workspace = true, optional = true, features = ["fs"] }
tor-rtcompat = { workspace = true, optional = true }
# - HTTP through Tor
http-body-util = { workspace = true, optional = true }
hyper-util = { workspace = true, optional = true }
rand = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
tokio-rustls = { workspace = true, optional = true }
webpki-roots = { workspace = true, optional = true }
[build-dependencies]
tonic-build = { workspace = true, features = ["prost"] }
which = "4"
@ -163,8 +173,16 @@ sync = [
## operations.
tor = [
"dep:arti-client",
"dep:futures-util",
"dep:http-body-util",
"dep:hyper",
"dep:hyper-util",
"dep:serde",
"dep:serde_json",
"dep:tokio",
"dep:tokio-rustls",
"dep:tor-rtcompat",
"dep:webpki-roots",
]
## Exposes APIs that are useful for testing, such as `proptest` strategies.

View File

@ -6,6 +6,8 @@ use arti_client::{config::TorClientConfigBuilder, TorClient};
use tor_rtcompat::PreferredRuntime;
use tracing::debug;
mod http;
/// A Tor client that exposes capabilities designed for Zcash wallets.
pub struct Client {
inner: TorClient<PreferredRuntime>,
@ -48,6 +50,8 @@ impl Client {
pub enum Error {
/// The directory passed to [`Client::create`] does not exist.
MissingTorDirectory,
/// An error occurred while using HTTP-over-Tor.
Http(self::http::HttpError),
/// An IO error occurred while interacting with the filesystem.
Io(io::Error),
/// A Tor-specific error.
@ -58,6 +62,7 @@ impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::MissingTorDirectory => write!(f, "Tor directory is missing"),
Error::Http(e) => write!(f, "HTTP-over-Tor error: {}", e),
Error::Io(e) => write!(f, "IO error: {}", e),
Error::Tor(e) => write!(f, "Tor error: {}", e),
}
@ -68,12 +73,19 @@ impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::MissingTorDirectory => None,
Error::Http(e) => Some(e),
Error::Io(e) => Some(e),
Error::Tor(e) => Some(e),
}
}
}
impl From<self::http::HttpError> for Error {
fn from(e: self::http::HttpError) -> Self {
Error::Http(e)
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)

View File

@ -0,0 +1,207 @@
//! HTTP requests over Tor.
use std::{fmt, future::Future, io, sync::Arc};
use futures_util::task::SpawnExt;
use http_body_util::{BodyExt, Empty};
use hyper::{
body::{Buf, Bytes, Incoming},
client::conn,
http::{request::Builder, uri::Scheme},
Request, Response, Uri,
};
use hyper_util::rt::TokioIo;
use serde::de::DeserializeOwned;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_rustls::{
rustls::{ClientConfig, OwnedTrustAnchor, RootCertStore, ServerName},
TlsConnector,
};
use tor_rtcompat::PreferredRuntime;
use tracing::{debug, error};
use super::{Client, Error};
impl Client {
#[tracing::instrument(skip(self, h, f))]
async fn get<T, F: Future<Output = Result<T, Error>>>(
&self,
url: Uri,
h: impl FnOnce(Builder) -> Builder,
f: impl FnOnce(Incoming) -> F,
) -> Result<Response<T>, Error> {
let is_https = url.scheme().ok_or_else(|| HttpError::NonHttpUrl)? == &Scheme::HTTPS;
let host = url.host().ok_or_else(|| HttpError::NonHttpUrl)?.to_string();
let port = match url.port_u16() {
Some(port) => port,
None if is_https => 443,
None => 80,
};
// Connect to the server.
debug!("Connecting through Tor to {}:{}", host, port);
let stream = self.inner.connect((host.as_str(), port)).await?;
if is_https {
// On apple-darwin targets there's an issue with the native TLS implementation
// when used over Tor circuits. We use Rustls instead.
//
// https://gitlab.torproject.org/tpo/core/arti/-/issues/715
let mut root_store = RootCertStore::empty();
root_store.add_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|root| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
root.subject,
root.spki,
root.name_constraints,
)
}));
let config = ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(Arc::new(config));
let dnsname = ServerName::try_from(host.as_str()).expect("Already checked");
let stream = connector
.connect(dnsname, stream)
.await
.map_err(HttpError::Tls)?;
make_http_request(stream, url, h, f).await
} else {
make_http_request(stream, url, h, f).await
}
}
async fn get_json<T: DeserializeOwned>(&self, url: Uri) -> Result<Response<T>, Error> {
self.get(
url,
|builder| builder.header(hyper::header::ACCEPT, "application/json"),
|body| async {
Ok(serde_json::from_reader(
body.collect()
.await
.map_err(HttpError::from)?
.aggregate()
.reader(),
)
.map_err(HttpError::from)?)
},
)
.await
}
}
async fn make_http_request<T, F: Future<Output = Result<T, Error>>>(
stream: impl AsyncRead + AsyncWrite + Unpin + Send + 'static,
url: Uri,
h: impl FnOnce(Builder) -> Builder,
f: impl FnOnce(Incoming) -> F,
) -> Result<Response<T>, Error> {
debug!("Making request");
let (mut sender, connection) = conn::http1::handshake(TokioIo::new(stream))
.await
.map_err(HttpError::from)?;
// Spawn a task to poll the connection and drive the HTTP state.
PreferredRuntime::current()?
.spawn(async move {
if let Err(e) = connection.await {
error!("Connection failed: {}", e);
}
})
.map_err(HttpError::from)?;
let req = h(Request::builder()
.header(
hyper::header::HOST,
url.authority().expect("Already checked").as_str(),
)
.uri(url))
.body(Empty::<Bytes>::new())
.map_err(HttpError::from)?;
let (parts, body) = sender
.send_request(req)
.await
.map_err(HttpError::from)?
.into_parts();
debug!("Response status code: {}", parts.status);
if parts.status.is_success() {
Ok(Response::from_parts(parts, f(body).await?))
} else {
Err(Error::Http(HttpError::Unsuccessful(parts.status)))
}
}
/// Errors that can occurr while using HTTP-over-Tor.
#[derive(Debug)]
pub enum HttpError {
/// A non-HTTP URL was encountered.
NonHttpUrl,
/// An HTTP error.
Http(hyper::http::Error),
/// A [`hyper`] error.
Hyper(hyper::Error),
/// A JSON parsing error.
Json(serde_json::Error),
/// An error occurred while spawning a background worker task for driving the HTTP
/// connection.
Spawn(futures_util::task::SpawnError),
/// A TLS-specific IO error.
Tls(io::Error),
/// The status code indicated that the request was unsuccessful.
Unsuccessful(hyper::http::StatusCode),
}
impl fmt::Display for HttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HttpError::NonHttpUrl => write!(f, "Only HTTP or HTTPS URLs are supported"),
HttpError::Http(e) => write!(f, "HTTP error: {}", e),
HttpError::Hyper(e) => write!(f, "Hyper error: {}", e),
HttpError::Json(e) => write!(f, "Failed to parse JSON: {}", e),
HttpError::Spawn(e) => write!(f, "Failed to spawn task: {}", e),
HttpError::Tls(e) => write!(f, "TLS error: {}", e),
HttpError::Unsuccessful(status) => write!(f, "Request was unsuccessful ({:?})", status),
}
}
}
impl std::error::Error for HttpError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
HttpError::NonHttpUrl => None,
HttpError::Http(e) => Some(e),
HttpError::Hyper(e) => Some(e),
HttpError::Json(e) => Some(e),
HttpError::Spawn(e) => Some(e),
HttpError::Tls(e) => Some(e),
HttpError::Unsuccessful(_) => None,
}
}
}
impl From<hyper::http::Error> for HttpError {
fn from(e: hyper::http::Error) -> Self {
HttpError::Http(e)
}
}
impl From<hyper::Error> for HttpError {
fn from(e: hyper::Error) -> Self {
HttpError::Hyper(e)
}
}
impl From<serde_json::Error> for HttpError {
fn from(e: serde_json::Error) -> Self {
HttpError::Json(e)
}
}
impl From<futures_util::task::SpawnError> for HttpError {
fn from(e: futures_util::task::SpawnError) -> Self {
HttpError::Spawn(e)
}
}