zcash_client_backend: Add internal HTTP GET support to `tor::Client`
This commit is contained in:
parent
1e5b62bfce
commit
b3de2ab81b
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue