mirror of
https://github.com/n0-computer/noq.git
synced 2026-10-03 12:40:46 +00:00
94e970d4d6
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
188 lines
5.7 KiB
Rust
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
|
|
}
|