#[cfg(not(feature = "h3"))]
mod disabled {
#[test]
#[ignore = "h3 integration tests require --features h3"]
fn h3_integration_requires_h3_feature() {
}
}
#[cfg(feature = "h3")]
mod enabled {
use quiche::h3::NameValue;
use std::net::{SocketAddr, UdpSocket};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
use ugi::StatusCode;
const MAX_DATAGRAM_SIZE: usize = 1350;
fn spawn_quic_server(shutdown: Arc<AtomicBool>) -> SocketAddr {
let socket = UdpSocket::bind(("127.0.0.1", 0)).unwrap();
let addr = socket.local_addr().unwrap();
let cert_pem = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
let cert_pem_str = cert_pem.cert.pem();
let key_pem_str = cert_pem.signing_key.serialize_pem();
thread::spawn(move || {
let tmp = tempfile::tempdir().unwrap();
let cert_path = tmp.path().join("cert.pem");
let key_path = tmp.path().join("key.pem");
std::fs::write(&cert_path, &cert_pem_str).unwrap();
std::fs::write(&key_path, &key_pem_str).unwrap();
let mut config = quiche::Config::new(quiche::PROTOCOL_VERSION).unwrap();
config
.load_cert_chain_from_pem_file(cert_path.to_str().unwrap())
.unwrap();
config
.load_priv_key_from_pem_file(key_path.to_str().unwrap())
.unwrap();
config
.set_application_protos(quiche::h3::APPLICATION_PROTOCOL)
.unwrap();
config.set_max_idle_timeout(30_000);
config.set_max_recv_udp_payload_size(MAX_DATAGRAM_SIZE);
config.set_max_send_udp_payload_size(MAX_DATAGRAM_SIZE);
config.set_initial_max_data(10_000_000);
config.set_initial_max_stream_data_bidi_local(1_000_000);
config.set_initial_max_stream_data_bidi_remote(1_000_000);
config.set_initial_max_stream_data_uni(1_000_000);
config.set_initial_max_streams_bidi(100);
config.set_initial_max_streams_uni(100);
config.set_disable_active_migration(true);
config.enable_early_data();
let h3_config = quiche::h3::Config::new().unwrap();
let mut clients: std::collections::HashMap<
quiche::ConnectionId<'static>,
(quiche::Connection, Option<quiche::h3::Connection>),
> = std::collections::HashMap::new();
let mut dcid_map: std::collections::HashMap<
quiche::ConnectionId<'static>,
quiche::ConnectionId<'static>,
> = std::collections::HashMap::new();
let mut buf = [0u8; 65535];
let mut out = [0u8; MAX_DATAGRAM_SIZE];
socket
.set_read_timeout(Some(Duration::from_millis(10)))
.unwrap();
while !shutdown.load(Ordering::Relaxed) {
for (_, (conn, _)) in clients.iter_mut() {
conn.on_timeout();
}
loop {
let (read, from) = match socket.recv_from(&mut buf) {
Ok(v) => v,
Err(_) => break,
};
let hdr =
match quiche::Header::from_slice(&mut buf[..read], quiche::MAX_CONN_ID_LEN)
{
Ok(h) => h,
Err(_) => continue,
};
let scid_key = if let Some(sk) = dcid_map.get(&hdr.dcid) {
sk.clone()
} else if clients.contains_key(&hdr.dcid) {
hdr.dcid.clone().into_owned()
} else {
if hdr.ty != quiche::Type::Initial {
continue;
}
let mut scid_bytes = [0u8; quiche::MAX_CONN_ID_LEN];
use std::time::SystemTime;
let seed = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64;
scid_bytes[..8].copy_from_slice(&seed.to_le_bytes());
let scid = quiche::ConnectionId::from_vec(scid_bytes.to_vec());
let conn = quiche::accept(&scid, None, addr, from, &mut config).unwrap();
dcid_map.insert(hdr.dcid.clone().into_owned(), scid.clone());
clients.insert(scid.clone(), (conn, None));
scid
};
let (conn, h3_opt) = match clients.get_mut(&scid_key) {
Some(v) => v,
None => continue,
};
let info = quiche::RecvInfo { from, to: addr };
let _ = conn.recv(&mut buf[..read], info);
if conn.is_established() && h3_opt.is_none() {
if let Ok(h3) = quiche::h3::Connection::with_transport(conn, &h3_config) {
*h3_opt = Some(h3);
}
}
if let Some(h3) = h3_opt.as_mut() {
loop {
match h3.poll(conn) {
Ok((stream_id, quiche::h3::Event::Headers { list, .. })) => {
let path = list
.iter()
.find(|h| h.name() == b":path")
.map(|h| h.value().to_vec());
let (status, body) = match path.as_deref() {
Some(b"/ping") => (200, b"pong".to_vec()),
Some(b"/json") => (200, br#"{"message":"hello"}"#.to_vec()),
_ => (404, b"not found".to_vec()),
};
let status_bytes = status.to_string().into_bytes();
let content_length = body.len().to_string().into_bytes();
let response_headers = vec![
quiche::h3::Header::new(b":status", &status_bytes),
quiche::h3::Header::new(b"content-length", &content_length),
];
let _ =
h3.send_response(conn, stream_id, &response_headers, false);
let _ = h3.send_body(conn, stream_id, &body, true);
}
Ok((_, quiche::h3::Event::Data)) => {}
Ok((_, quiche::h3::Event::Finished)) => {}
Ok((_, quiche::h3::Event::Reset(_))) => {}
Ok((_, quiche::h3::Event::GoAway)) => {}
Ok((_, quiche::h3::Event::PriorityUpdate)) => {}
Err(quiche::h3::Error::Done) => break,
Err(_) => break,
}
}
}
loop {
match conn.send(&mut out) {
Ok((written, info)) => {
let _ = socket.send_to(&out[..written], info.to);
}
Err(quiche::Error::Done) => break,
Err(_) => break,
}
}
}
let mut to_remove = vec![];
for (scid, (conn, _)) in clients.iter_mut() {
if conn.is_closed() {
to_remove.push(scid.clone());
}
}
for id in to_remove {
clients.remove(&id);
}
}
});
addr
}
#[test]
fn h3_basic_get_request() {
let shutdown = Arc::new(AtomicBool::new(false));
let server_addr = spawn_quic_server(Arc::clone(&shutdown));
let base_url = format!("https://localhost:{}", server_addr.port());
async_io::block_on(async move {
let client = ugi::Client::builder()
.http3_only()
.timeout(Duration::from_secs(10))
.build()
.unwrap();
let resp = client.get(&format!("{base_url}/ping")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = resp.text().await.unwrap();
assert_eq!(body, "pong");
});
shutdown.store(true, Ordering::Relaxed);
thread::sleep(Duration::from_millis(50));
}
#[test]
fn h3_json_response() {
let shutdown = Arc::new(AtomicBool::new(false));
let server_addr = spawn_quic_server(Arc::clone(&shutdown));
let base_url = format!("https://localhost:{}", server_addr.port());
async_io::block_on(async move {
let client = ugi::Client::builder()
.http3_only()
.timeout(Duration::from_secs(10))
.build()
.unwrap();
let resp = client.get(&format!("{base_url}/json")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = resp.text().await.unwrap();
assert_eq!(body, r#"{"message":"hello"}"#);
});
shutdown.store(true, Ordering::Relaxed);
thread::sleep(Duration::from_millis(50));
}
}