Skip to content

Commit a63572d

Browse files
committed
Add Async OHTTP client
Co-authored-by Alex Lewin <[email protected]> [OHTTP](https://datatracker.ietf.org/doc/rfc9458/) lets a client send encrypted requests through a relay so the server can’t see who sent them and the relay can’t see what they contain. The following commit adds optional configurations to enable clients to proxy their requests through an OHTTP relay and gateway. OHTTP functionality is feature flagged off behind `async-ohttp`. If a client provided OHTTP config it will attempt to use the relay instead of the target resource directly.
1 parent 5740ceb commit a63572d

File tree

4 files changed

+171
-1
lines changed

4 files changed

+171
-1
lines changed

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ reqwest = { version = "0.12", features = ["json"], default-features = false, op
2626

2727
# default async runtime
2828
tokio = { version = "1", features = ["time"], optional = true }
29+
bitcoin-ohttp = { version = "0.6.0", optional = true}
30+
url = {version = "2.5.7", optional = true}
31+
bhttp = { version = "0.6.1", optional = true}
32+
http = { version = "1.3.1", optional = true}
33+
2934

3035
[dev-dependencies]
3136
serde_json = "1.0"
@@ -40,6 +45,7 @@ blocking-https = ["blocking", "minreq/https"]
4045
blocking-https-rustls = ["blocking", "minreq/https-rustls"]
4146
blocking-https-native = ["blocking", "minreq/https-native"]
4247
blocking-https-bundled = ["blocking", "minreq/https-bundled"]
48+
async-ohttp = ["async", "bitcoin-ohttp", "bhttp", "reqwest", "tokio", "url", "http"]
4349

4450
tokio = ["dep:tokio"]
4551
async = ["reqwest", "reqwest/socks", "tokio?/time"]

src/async.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ use log::{debug, error, info, trace};
2929
use reqwest::{header, Client, Response};
3030

3131
use crate::api::AddressStats;
32+
#[cfg(feature = "async-ohttp")]
33+
use crate::ohttp::OhttpClient;
3234
use crate::{
3335
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus,
3436
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
@@ -45,6 +47,9 @@ pub struct AsyncClient<S = DefaultSleeper> {
4547

4648
/// Marker for the type of sleeper used
4749
marker: PhantomData<S>,
50+
/// Ohttp config
51+
#[cfg(feature = "async-ohttp")]
52+
ohttp_client: Option<OhttpClient>,
4853
}
4954

5055
impl<S: Sleeper> AsyncClient<S> {
@@ -79,6 +84,8 @@ impl<S: Sleeper> AsyncClient<S> {
7984
client: client_builder.build()?,
8085
max_retries: builder.max_retries,
8186
marker: PhantomData,
87+
#[cfg(feature = "async-ohttp")]
88+
ohttp_client: None,
8289
})
8390
}
8491

@@ -88,9 +95,17 @@ impl<S: Sleeper> AsyncClient<S> {
8895
client,
8996
max_retries: crate::DEFAULT_MAX_RETRIES,
9097
marker: PhantomData,
98+
#[cfg(feature = "async-ohttp")]
99+
ohttp_client: None,
91100
}
92101
}
93102

103+
#[cfg(feature = "async-ohttp")]
104+
pub(crate) fn set_ohttp_client(mut self, ohttp_client: OhttpClient) -> Self {
105+
self.ohttp_client = Some(ohttp_client);
106+
self
107+
}
108+
94109
/// Make an HTTP GET request to given URL, deserializing to any `T` that
95110
/// implement [`bitcoin::consensus::Decodable`].
96111
///
@@ -466,12 +481,32 @@ impl<S: Sleeper> AsyncClient<S> {
466481
let mut attempts = 0;
467482

468483
loop {
469-
match self.client.get(url).send().await? {
484+
let res = {
485+
#[cfg(feature = "async-ohttp")]
486+
if let Some(ohttp_client) = &self.ohttp_client {
487+
let (body, ctx) = ohttp_client.ohttp_encapsulate("get", &url, None)?;
488+
let res = self
489+
.client
490+
.post(ohttp_client.relay_url().to_string())
491+
.header("Content-Type", "message/ohttp-req")
492+
.body(body)
493+
.send()
494+
.await?;
495+
let body = res.bytes().await?.to_vec();
496+
ohttp_client.ohttp_decapsulate(ctx, body)?.into()
497+
} else {
498+
self.client.get(url).send().await?
499+
}
500+
#[cfg(not(feature = "async-ohttp"))]
501+
self.client.get(url).send().await?
502+
};
503+
match res {
470504
resp if attempts < self.max_retries && is_status_retryable(resp.status()) => {
471505
S::sleep(delay).await;
472506
attempts += 1;
473507
delay *= 2;
474508
}
509+
475510
resp => return Ok(resp),
476511
}
477512
}

src/lib.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ pub mod r#async;
8383
#[cfg(feature = "blocking")]
8484
pub mod blocking;
8585

86+
#[cfg(feature = "async-ohttp")]
87+
pub(crate) mod ohttp;
88+
8689
pub use api::*;
8790
#[cfg(feature = "blocking")]
8891
pub use blocking::BlockingClient;
@@ -195,6 +198,20 @@ impl Builder {
195198
pub fn build_async_with_sleeper<S: Sleeper>(self) -> Result<AsyncClient<S>, Error> {
196199
AsyncClient::from_builder(self)
197200
}
201+
202+
#[cfg(feature = "async-ohttp")]
203+
pub async fn build_async_with_ohttp(
204+
self,
205+
ohttp_relay_url: String,
206+
ohttp_gateway_url: String,
207+
) -> Result<AsyncClient, Error> {
208+
use crate::ohttp::OhttpClient;
209+
210+
let ohttp_client = OhttpClient::new(&ohttp_relay_url, &ohttp_gateway_url).await?;
211+
Ok(self
212+
.build_async_with_sleeper()?
213+
.set_ohttp_client(ohttp_client))
214+
}
198215
}
199216

200217
/// Errors that can happen during a request to `Esplora` servers.
@@ -230,6 +247,18 @@ pub enum Error {
230247
InvalidHttpHeaderValue(String),
231248
/// The server sent an invalid response
232249
InvalidResponse,
250+
/// Error from Ohttp library
251+
#[cfg(feature = "async-ohttp")]
252+
Ohttp(bitcoin_ohttp::Error),
253+
/// Error when reading and writing to bhttp payloads
254+
#[cfg(feature = "async-ohttp")]
255+
Bhttp(bhttp::Error),
256+
/// Error when converting the http response to and from bhttp response
257+
#[cfg(feature = "async-ohttp")]
258+
Http(http::Error),
259+
/// Error when parsing the URL
260+
#[cfg(feature = "async-ohttp")]
261+
UrlParsing(url::ParseError),
233262
}
234263

235264
impl fmt::Display for Error {

src/ohttp.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use crate::Error;
2+
use bitcoin_ohttp as ohttp;
3+
use reqwest::Client;
4+
use url::Url;
5+
6+
#[derive(Debug, Clone)]
7+
pub struct OhttpClient {
8+
key_config: ohttp::KeyConfig,
9+
relay_url: Url,
10+
}
11+
12+
impl OhttpClient {
13+
/// Will attempt to fetch the key config from the gateway and then create a new client.
14+
pub(crate) async fn new(relay_url: &str, ohttp_gateway_url: &str) -> Result<Self, Error> {
15+
let client = Client::new();
16+
let gateway_url = Url::parse(ohttp_gateway_url).map_err(Error::UrlParsing)?;
17+
let res = client
18+
.get(format!("{}/.well-known/ohttp-gateway", gateway_url))
19+
.send()
20+
.await
21+
.map_err(Error::Reqwest)?;
22+
let body = res.bytes().await.map_err(Error::Reqwest)?;
23+
let key_config = ohttp::KeyConfig::decode(&body).map_err(Error::Ohttp)?;
24+
Ok(Self {
25+
key_config,
26+
relay_url: Url::parse(relay_url).map_err(Error::UrlParsing)?,
27+
})
28+
}
29+
30+
pub(crate) fn relay_url(&self) -> &Url {
31+
&self.relay_url
32+
}
33+
34+
pub(crate) fn ohttp_encapsulate(
35+
&self,
36+
method: &str,
37+
target_resource: &str,
38+
body: Option<&[u8]>,
39+
) -> Result<(Vec<u8>, ohttp::ClientResponse), Error> {
40+
use std::fmt::Write;
41+
42+
// Bitcoin-hpke takes keyconfig as mutable ref but it doesnt mutate it should fix it upstream but for now we can clone it to avoid changing self to mutable self
43+
let mut key_config = self.key_config.clone();
44+
45+
let ctx = ohttp::ClientRequest::from_config(&mut key_config).map_err(Error::Ohttp)?;
46+
let url = url::Url::parse(target_resource).map_err(Error::UrlParsing)?;
47+
let authority_bytes = url.host().map_or_else(Vec::new, |host| {
48+
let mut authority = host.to_string();
49+
if let Some(port) = url.port() {
50+
write!(authority, ":{port}").unwrap();
51+
}
52+
authority.into_bytes()
53+
});
54+
let mut bhttp_message = bhttp::Message::request(
55+
method.as_bytes().to_vec(),
56+
url.scheme().as_bytes().to_vec(),
57+
authority_bytes,
58+
url.path().as_bytes().to_vec(),
59+
);
60+
// TODO: do we need to add headers?
61+
if let Some(body) = body {
62+
bhttp_message.write_content(body);
63+
}
64+
65+
let mut bhttp_req = Vec::new();
66+
bhttp_message
67+
.write_bhttp(bhttp::Mode::IndeterminateLength, &mut bhttp_req)
68+
.map_err(Error::Bhttp)?;
69+
let (encapsulated, ohttp_ctx) = ctx.encapsulate(&bhttp_req).map_err(Error::Ohttp)?;
70+
71+
return Ok((encapsulated, ohttp_ctx));
72+
}
73+
74+
pub(crate) fn ohttp_decapsulate(
75+
&self,
76+
res_ctx: ohttp::ClientResponse,
77+
ohttp_body: Vec<u8>,
78+
) -> Result<http::Response<Vec<u8>>, Error> {
79+
let bhttp_body = res_ctx.decapsulate(&ohttp_body).map_err(Error::Ohttp)?;
80+
let mut r = std::io::Cursor::new(bhttp_body);
81+
let m: bhttp::Message = bhttp::Message::read_bhttp(&mut r).map_err(Error::Bhttp)?;
82+
let mut builder = http::Response::builder();
83+
for field in m.header().iter() {
84+
builder = builder.header(field.name(), field.value());
85+
}
86+
Ok(builder
87+
.status({
88+
let code = m
89+
.control()
90+
.status()
91+
.ok_or(bhttp::Error::InvalidStatus)
92+
.map_err(Error::Bhttp)?;
93+
http::StatusCode::from_u16(code.code())
94+
.map_err(|_| bhttp::Error::InvalidStatus)
95+
.map_err(Error::Bhttp)?
96+
})
97+
.body(m.content().to_vec())
98+
.map_err(Error::Http)?)
99+
}
100+
}

0 commit comments

Comments
 (0)