Files
noq/quinn/examples/client.rs
T
Diva M 94e970d4d6 feat: QUIC Address discovery extension (#12)
add frame types

transport parameter encoding and decoding - to my best understanding

fix typo

add some utility functions

add initial observed address frames

add frame encoding and decoding

adjust stats

minimal debugging for received observed addr

simplify setting extension in transport parameter

rework frame structure and send observed addr frames with path challenge ones

tweak example to start testing

fix encoding, send with handshake

clippy

fix docs

reject observed addr frames when not negotiated

replace request_id with seq_no according to new spec

replace code point for transport parameter

replace code point for frames

remove sending observed address frame in handshake in server side

treat as probing frame in payload processing

ack is already managed by is_ack_eliciting

send with path_response as well

add frame to retransmits and ignore old frames

send observed addr at least once per path

fmt

reword comment

remove trailing whites

keep observed address reports per path

remove addressed TODO

small improvement in readability

add retransmission with fresh info

retransmit just once

fix should send logic

add observed addr event

surface the info

restore trace level of frames

some extra logs

rename roles and var

improve error msg

assuming the default as disabled is ok, remove comment

use safe arithmetic with varints for the seq_no

move transport param code to method instead of From impl

fix example, finally

remove excesive log

add helper fn

carry old report into new path

generate notification only on changed values

downgrade log

add sending test

add resumption test on the acceptance case

add resumption test on the rejection case

spelling

actual spelling and undo debug change

dumb lints

some spelling and formatting

add retransmission test

make a bit more readable

update hexas

make naming consistent, add test

check docs for consistency
2024-11-04 11:07:38 -05:00

188 lines
5.7 KiB
Rust

//! This example demonstrates an HTTP client that requests files from a server.
//!
//! Checkout the `README.md` for guidance.
use std::{
fs,
io::{self, Write},
net::{SocketAddr, ToSocketAddrs},
path::PathBuf,
sync::Arc,
time::{Duration, Instant},
};
use anyhow::{anyhow, Result};
use clap::Parser;
use iroh_quinn as quinn;
use proto::{crypto::rustls::QuicClientConfig, TransportConfig};
use rustls::pki_types::CertificateDer;
use tracing::{error, info};
use url::Url;
mod common;
/// HTTP/0.9 over QUIC client
#[derive(Parser, Debug)]
#[clap(name = "client")]
struct Opt {
/// Perform NSS-compatible TLS key logging to the file specified in `SSLKEYLOGFILE`.
#[clap(long = "keylog")]
keylog: bool,
url: Url,
/// Override hostname used for certificate verification
#[clap(long = "host")]
host: Option<String>,
/// Custom certificate authority to trust, in DER format
#[clap(long = "ca")]
ca: Option<PathBuf>,
/// Simulate NAT rebinding after connecting
#[clap(long = "rebind")]
rebind: bool,
/// Address to bind on
#[clap(long = "bind", default_value = "[::]:0")]
bind: SocketAddr,
}
fn main() {
tracing::subscriber::set_global_default(
tracing_subscriber::FmtSubscriber::builder()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.finish(),
)
.unwrap();
let opt = Opt::parse();
let code = {
if let Err(e) = run(opt) {
eprintln!("ERROR: {e}");
1
} else {
0
}
};
::std::process::exit(code);
}
#[tokio::main]
async fn run(options: Opt) -> Result<()> {
let url = options.url;
let url_host = strip_ipv6_brackets(url.host_str().unwrap());
let remote = (url_host, url.port().unwrap_or(4433))
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow!("couldn't resolve to an address"))?;
let mut roots = rustls::RootCertStore::empty();
if let Some(ca_path) = options.ca {
roots.add(CertificateDer::from(fs::read(ca_path)?))?;
} else {
let dirs = directories_next::ProjectDirs::from("org", "quinn", "quinn-examples").unwrap();
match fs::read(dirs.data_local_dir().join("cert.der")) {
Ok(cert) => {
roots.add(CertificateDer::from(cert))?;
}
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
info!("local server certificate not found");
}
Err(e) => {
error!("failed to open local server certificate: {}", e);
}
}
}
let mut client_crypto = rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth();
client_crypto.alpn_protocols = common::ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect();
if options.keylog {
client_crypto.key_log = Arc::new(rustls::KeyLogFile::new());
}
let mut transport = TransportConfig::default();
transport
.send_observed_address_reports(true)
.receive_observed_address_reports(true);
let mut client_config =
quinn::ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?));
client_config.transport_config(Arc::new(transport));
let mut endpoint = quinn::Endpoint::client(options.bind)?;
endpoint.set_default_client_config(client_config);
let request = format!("GET {}\r\n", url.path());
let start = Instant::now();
let rebind = options.rebind;
let host = options.host.as_deref().unwrap_or(url_host);
eprintln!("connecting to {host} at {remote}");
let conn = endpoint
.connect(remote, host)?
.await
.map_err(|e| anyhow!("failed to connect: {}", e))?;
eprintln!("connected at {:?}", start.elapsed());
let mut external_addresses = conn.observed_external_addr();
tokio::spawn(async move {
loop {
if let Some(new_addr) = *external_addresses.borrow_and_update() {
info!(%new_addr, "new external address report");
}
if external_addresses.changed().await.is_err() {
break;
}
}
});
let (mut send, mut recv) = conn
.open_bi()
.await
.map_err(|e| anyhow!("failed to open stream: {}", e))?;
if rebind {
let socket = std::net::UdpSocket::bind("[::]:0").unwrap();
let addr = socket.local_addr().unwrap();
eprintln!("rebinding to {addr}");
endpoint.rebind(socket).expect("rebind failed");
}
send.write_all(request.as_bytes())
.await
.map_err(|e| anyhow!("failed to send request: {}", e))?;
send.finish().unwrap();
let response_start = Instant::now();
eprintln!("request sent at {:?}", response_start - start);
let resp = recv
.read_to_end(usize::MAX)
.await
.map_err(|e| anyhow!("failed to read response: {}", e))?;
let duration = response_start.elapsed();
eprintln!(
"response received in {:?} - {} KiB/s",
duration,
resp.len() as f32 / (duration_secs(&duration) * 1024.0)
);
io::stdout().write_all(&resp).unwrap();
io::stdout().flush().unwrap();
conn.close(0u32.into(), b"done");
// Give the server a fair chance to receive the close packet
endpoint.wait_idle().await;
Ok(())
}
fn strip_ipv6_brackets(host: &str) -> &str {
// An ipv6 url looks like eg https://[::1]:4433/Cargo.toml, wherein the host [::1] is the
// ipv6 address ::1 wrapped in brackets, per RFC 2732. This strips those.
if host.starts_with('[') && host.ends_with(']') {
&host[1..host.len() - 1]
} else {
host
}
}
fn duration_secs(x: &Duration) -> f32 {
x.as_secs() as f32 + x.subsec_nanos() as f32 * 1e-9
}