Skip to content

Commit 196b143

Browse files
committed
Extract HttpRepository into tuf-hyper, port to hyper 1.x
This extracts HttpRepository into its own crate, so its easier for users to not pull in the http stack unless it is necessary. It also updates hyper to 1.x, and adds some examples on how to create a client using tuf-hyper.
1 parent 515be8e commit 196b143

16 files changed

Lines changed: 1961 additions & 633 deletions

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
resolver = "2"
33
members = [
44
"tuf",
5+
"tuf-hyper",
56
"interop-tests",
67
]

interop-tests/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ publish = false
1212

1313
[dependencies]
1414
chrono = { version = "0.4.42", features = [ "serde" ] }
15-
data-encoding = "2.9.0"
15+
data-encoding = "2.10"
1616
futures-executor = "0.3.31"
1717
serde = "1"
1818
serde_json = "1"

tuf-hyper/Cargo.toml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[package]
2+
name = "tuf-hyper"
3+
version = "0.3.0-beta14"
4+
edition = "2024"
5+
rust-version = "1.88.0"
6+
authors = [ "Erick Tryzelaar <etryzelaar@google.com>" ]
7+
description = "HTTP repository support for TUF using Hyper"
8+
homepage = "https://github.com/theupdateframework/rust-tuf"
9+
repository = "https://github.com/theupdateframework/rust-tuf"
10+
license = "MIT/Apache-2.0"
11+
12+
[dependencies]
13+
futures-io = "0.3.31"
14+
futures-util = { version = "0.3.31", features = ["io"] }
15+
http = "1"
16+
http-body-util = "0.1"
17+
hyper = { version = "1", default-features = false, features = ["client", "http1"] }
18+
percent-encoding = "2.1"
19+
tower = { version = "0.4", default-features = false, features = ["util"] }
20+
tuf = { path = "../tuf", default-features = false }
21+
22+
[dev-dependencies]
23+
assert_matches = "1.5.0"
24+
clap = { version = "4", features = ["derive", "env"] }
25+
futures-executor = "0.3.31"
26+
hyper = { version = "1", features = ["client", "server", "http1"] }
27+
hyper-util = { version = "0.1", features = ["client-legacy", "client-pool", "http1", "tokio", "server", "server-auto", "service"] }
28+
pretty_assertions = "1"
29+
smol = "2"
30+
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "net"] }
31+
32+
[[example]]
33+
name = "server-tokio"
34+
35+
[[example]]
36+
name = "client-tokio"
37+
38+
[[example]]
39+
name = "client-smol"

tuf-hyper/examples/client-smol.rs

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
//! A TUF client using the Smol runtime (`smol::block_on`) and hyper legacy client.
2+
//!
3+
//! Demonstrates implementing a custom `SmolExecutor` (`hyper::rt::Executor`),
4+
//! `SmolStream` (`hyper::rt::Read` + `hyper::rt::Write` + `Connection`), and
5+
//! `SmolConnector` (`Service<Uri>`), constructing an `HttpRepositoryBuilder` on Smol,
6+
//! setting up a local `EphemeralRepository` or `FileSystemRepository`, initializing
7+
//! the client with trusted root keys, running `.update().await`, and fetching target files.
8+
//!
9+
//! # Usage
10+
//! ```bash
11+
//! # Make sure `server-tokio` is running first:
12+
//! # cargo run --example server-tokio
13+
//!
14+
//! cargo run --example client-smol -- [options]
15+
//!
16+
//! Options:
17+
//! --url, -u <url> TUF server URL (default: http://127.0.0.1:8080)
18+
//! --dir, -d <directory> Local metadata/target storage directory (FileSystemRepository)
19+
//! --help, -h Print usage and exit
20+
//! ```
21+
22+
use clap::Parser;
23+
use futures_io::{AsyncRead, AsyncWrite};
24+
use futures_util::io::AsyncReadExt as _;
25+
use http::{Request, Response, Uri};
26+
use hyper::body::{Bytes, Incoming};
27+
use hyper::rt::{Read, ReadBufCursor, Write};
28+
use hyper_util::client::pool::cache;
29+
use std::future::Future;
30+
use std::io;
31+
use std::path::PathBuf;
32+
use std::pin::Pin;
33+
use std::task::{Context, Poll};
34+
use tower::{Service, ServiceBuilder, ServiceExt as _};
35+
use tuf::client::{Client, Config};
36+
use tuf::crypto::{Ed25519PrivateKey, PrivateKey};
37+
use tuf::metadata::{MetadataVersion, TargetPath};
38+
use tuf::pouf::Pouf1;
39+
use tuf::repository::{
40+
EphemeralRepository, FileSystemRepository, RepositoryProvider, RepositoryStorage,
41+
};
42+
use tuf_hyper::HttpRepositoryBuilder;
43+
44+
type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
45+
46+
#[derive(Parser, Debug)]
47+
#[command(about = "A TUF client using the Smol runtime and hyper pooling client.")]
48+
struct Cli {
49+
/// TUF server URL
50+
#[arg(
51+
short = 'u',
52+
long,
53+
env = "TUF_SERVER_URL",
54+
default_value = "http://127.0.0.1:8080"
55+
)]
56+
url: Uri,
57+
58+
/// Local metadata/target storage directory (FileSystemRepository)
59+
#[arg(short = 'd', long, env = "TUF_LOCAL_DIR")]
60+
dir: Option<PathBuf>,
61+
}
62+
63+
const ED25519_1_PK8: &[u8] = include_bytes!("../../tuf/tests/ed25519/ed25519-1.pk8.der");
64+
65+
/// A custom hyper runtime executor powered by `smol::spawn`.
66+
#[derive(Clone, Copy, Debug)]
67+
pub struct SmolExecutor;
68+
69+
impl<F> hyper::rt::Executor<F> for SmolExecutor
70+
where
71+
F: std::future::Future + Send + 'static,
72+
F::Output: Send + 'static,
73+
{
74+
fn execute(&self, fut: F) {
75+
smol::spawn(async move {
76+
fut.await;
77+
})
78+
.detach();
79+
}
80+
}
81+
82+
/// A wrapper around `smol::net::TcpStream` that implements hyper's `Read` and `Write`.
83+
pub struct SmolStream {
84+
inner: smol::net::TcpStream,
85+
}
86+
87+
impl Read for SmolStream {
88+
fn poll_read(
89+
mut self: Pin<&mut Self>,
90+
cx: &mut Context<'_>,
91+
mut cursor: ReadBufCursor<'_>,
92+
) -> Poll<io::Result<()>> {
93+
let buf = unsafe { cursor.as_mut() };
94+
// Guarantee memory soundness under Miri by zero-initializing uninitialized memory
95+
// before converting to a `&mut [u8]` slice for async I/O.
96+
for byte in buf.iter_mut() {
97+
*byte = std::mem::MaybeUninit::new(0);
98+
}
99+
let slice =
100+
unsafe { std::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, buf.len()) };
101+
match Pin::new(&mut self.inner).poll_read(cx, slice) {
102+
Poll::Ready(Ok(n)) => {
103+
unsafe { cursor.advance(n) };
104+
Poll::Ready(Ok(()))
105+
}
106+
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
107+
Poll::Pending => Poll::Pending,
108+
}
109+
}
110+
}
111+
112+
impl Write for SmolStream {
113+
fn poll_write(
114+
mut self: Pin<&mut Self>,
115+
cx: &mut Context<'_>,
116+
buf: &[u8],
117+
) -> Poll<io::Result<usize>> {
118+
Pin::new(&mut self.inner).poll_write(cx, buf)
119+
}
120+
121+
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
122+
Pin::new(&mut self.inner).poll_flush(cx)
123+
}
124+
125+
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
126+
Pin::new(&mut self.inner).poll_close(cx)
127+
}
128+
}
129+
130+
/// Wrapper around `hyper::client::conn::http1::SendRequest<B>` implementing `Service<Request<B>>`.
131+
#[derive(Debug)]
132+
pub struct ConnectionService<B>(pub hyper::client::conn::http1::SendRequest<B>);
133+
134+
impl<B> Service<Request<B>> for ConnectionService<B>
135+
where
136+
B: hyper::body::Body + Send + 'static + Unpin,
137+
B::Data: Send,
138+
B::Error: Into<BoxError>,
139+
{
140+
type Response = Response<Incoming>;
141+
type Error = BoxError;
142+
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
143+
144+
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
145+
self.0.poll_ready(cx).map_err(Into::into)
146+
}
147+
148+
fn call(&mut self, req: Request<B>) -> Self::Future {
149+
let fut = self.0.send_request(req);
150+
Box::pin(async move { fut.await.map_err(Into::into) })
151+
}
152+
}
153+
154+
/// A custom hyper `Service<Uri>` connector that establishes TCP connections via `smol::net::TcpStream`
155+
/// and performs HTTP/1 handshakes.
156+
#[derive(Clone, Copy, Debug)]
157+
pub struct SmolConnector;
158+
159+
impl Service<Uri> for SmolConnector {
160+
type Response = ConnectionService<http_body_util::Empty<Bytes>>;
161+
type Error = BoxError;
162+
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
163+
164+
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
165+
Poll::Ready(Ok(()))
166+
}
167+
168+
fn call(&mut self, uri: Uri) -> Self::Future {
169+
Box::pin(async move {
170+
let host = uri.host().ok_or_else(|| {
171+
io::Error::new(io::ErrorKind::InvalidInput, "Missing host in URI")
172+
})?;
173+
let port = uri.port_u16().unwrap_or(80);
174+
let addr_str = format!("{}:{}", host, port);
175+
let stream = smol::net::TcpStream::connect(addr_str).await?;
176+
let (sender, conn) =
177+
hyper::client::conn::http1::handshake(SmolStream { inner: stream }).await?;
178+
smol::spawn(async move {
179+
if let Err(err) = conn.await {
180+
eprintln!("Connection failed: {:?}", err);
181+
}
182+
})
183+
.detach();
184+
Ok(ConnectionService(sender))
185+
})
186+
}
187+
}
188+
189+
async fn async_main() -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
190+
let cli = Cli::parse();
191+
let uri = cli.url;
192+
let local_dir = cli.dir;
193+
println!("Connecting to TUF HTTP repository at {} via Smol", uri);
194+
195+
// Build the pooling client using hyper_util::client::pool::cache and SmolExecutor
196+
let pool = cache::builder()
197+
.executor(SmolExecutor)
198+
.build(SmolConnector);
199+
let hyper_client = ServiceBuilder::new().service_fn(
200+
move |req: Request<http_body_util::Empty<Bytes>>| {
201+
let mut pool = pool.clone();
202+
async move {
203+
let mut conn = pool.call(req.uri().clone()).await?;
204+
conn.ready().await?.call(req).await
205+
}
206+
},
207+
);
208+
209+
let remote = HttpRepositoryBuilder::<_, Pouf1>::new(uri, hyper_client)
210+
.user_agent("tuf-client-smol-example/0.1")
211+
.build();
212+
213+
// Load the trusted root public key used by our example server.
214+
let root_key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8)?;
215+
let root_public_key = root_key.public();
216+
217+
let config = Config::default();
218+
219+
match local_dir {
220+
Some(dir) => {
221+
println!("Using local FileSystemRepository at {:?}", dir);
222+
let local = FileSystemRepository::<Pouf1>::new(dir);
223+
run_client(config, root_public_key, local, remote).await?;
224+
}
225+
None => {
226+
println!("Using local EphemeralRepository (in-memory)");
227+
let local = EphemeralRepository::<Pouf1>::new();
228+
run_client(config, root_public_key, local, remote).await?;
229+
}
230+
}
231+
232+
Ok(())
233+
}
234+
235+
async fn run_client<L, R>(
236+
config: Config,
237+
root_public_key: &tuf::crypto::PublicKey,
238+
local: L,
239+
remote: R,
240+
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>>
241+
where
242+
L: RepositoryProvider<Pouf1> + RepositoryStorage<Pouf1> + Send + Sync + 'static,
243+
R: RepositoryProvider<Pouf1> + Send + Sync + 'static,
244+
{
245+
let mut client = Client::with_trusted_root_keys(
246+
config,
247+
MetadataVersion::Number(1),
248+
1,
249+
[root_public_key],
250+
local,
251+
remote,
252+
)
253+
.await?;
254+
255+
println!("Updating client metadata...");
256+
let _ = client.update().await?;
257+
println!("Client metadata updated successfully.");
258+
259+
// Fetch the target file into the local repository and verify its content.
260+
let target_path = TargetPath::new("foo-bar")?;
261+
println!("Fetching target '{}' to local storage...", target_path);
262+
client.fetch_target_to_local(&target_path).await?;
263+
264+
let mut target_reader = client.fetch_target(&target_path).await?;
265+
let mut target_data = Vec::new();
266+
target_reader.read_to_end(&mut target_data).await?;
267+
println!(
268+
"Successfully fetched target '{}': {:?}",
269+
target_path,
270+
String::from_utf8_lossy(&target_data)
271+
);
272+
273+
Ok(())
274+
}
275+
276+
fn main() -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
277+
smol::block_on(async_main())
278+
}

0 commit comments

Comments
 (0)