blob: e8302bf4160396fd735e92170b883c6612f7dd1c [file] [log] [blame]
// Copyright (C) 2019, Cloudflare, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use quiche::h3::NameValue;
use ring::rand::*;
use crate::Http3TestError;
pub fn run(
test: &mut crate::Http3Test, peer_addr: std::net::SocketAddr,
verify_peer: bool, idle_timeout: u64, max_data: u64, early_data: bool,
session_file: Option<String>,
) -> Result<(), Http3TestError> {
const MAX_DATAGRAM_SIZE: usize = 1350;
let mut buf = [0; 65535];
let mut out = [0; MAX_DATAGRAM_SIZE];
let max_stream_data = max_data;
let version = if let Some(v) = std::env::var_os("QUIC_VERSION") {
match v.to_str() {
Some("current") => quiche::PROTOCOL_VERSION,
Some(v) => u32::from_str_radix(v, 16).unwrap(),
_ => 0xbaba_baba,
}
} else {
0xbaba_baba
};
let mut reqs_count = 0;
let mut reqs_complete = 0;
// Setup the event loop.
let mut poll = mio::Poll::new().unwrap();
let mut events = mio::Events::with_capacity(1024);
info!("connecting to {:}", peer_addr);
// Bind to INADDR_ANY or IN6ADDR_ANY depending on the IP family of the
// server address. This is needed on macOS and BSD variants that don't
// support binding to IN6ADDR_ANY for both v4 and v6.
let bind_addr = match peer_addr {
std::net::SocketAddr::V4(_) => "0.0.0.0:0",
std::net::SocketAddr::V6(_) => "[::]:0",
};
// Create the UDP socket backing the QUIC connection, and register it with
// the event loop.
let mut socket =
mio::net::UdpSocket::bind(bind_addr.parse().unwrap()).unwrap();
poll.registry()
.register(&mut socket, mio::Token(0), mio::Interest::READABLE)
.unwrap();
// Create the configuration for the QUIC connection.
let mut config = quiche::Config::new(version).unwrap();
config.verify_peer(verify_peer);
config
.set_application_protos(quiche::h3::APPLICATION_PROTOCOL)
.unwrap();
config.set_max_idle_timeout(idle_timeout);
config.set_max_recv_udp_payload_size(MAX_DATAGRAM_SIZE);
config.set_initial_max_data(max_data);
config.set_initial_max_stream_data_bidi_local(max_stream_data);
config.set_initial_max_stream_data_bidi_remote(max_stream_data);
config.set_initial_max_stream_data_uni(max_stream_data);
config.set_initial_max_streams_bidi(100);
config.set_initial_max_streams_uni(100);
config.set_disable_active_migration(true);
if early_data {
config.enable_early_data();
debug!("early data enabled");
}
let mut http3_conn = None;
if std::env::var_os("SSLKEYLOGFILE").is_some() {
config.log_keys();
}
// Generate a random source connection ID for the connection.
let mut scid = [0; quiche::MAX_CONN_ID_LEN];
SystemRandom::new().fill(&mut scid[..]).unwrap();
let scid = quiche::ConnectionId::from_ref(&scid);
// Create a QUIC connection and initiate handshake.
let url = &test.endpoint();
let mut conn =
quiche::connect(url.domain(), &scid, peer_addr, &mut config).unwrap();
if let Some(session_file) = &session_file {
if let Ok(session) = std::fs::read(session_file) {
conn.set_session(&session).ok();
}
}
let (write, send_info) = conn.send(&mut out).expect("initial send failed");
while let Err(e) = socket.send_to(&out[..write], send_info.to) {
if e.kind() == std::io::ErrorKind::WouldBlock {
debug!("send() would block");
continue;
}
return Err(Http3TestError::Other(format!("send() failed: {:?}", e)));
}
debug!("written {}", write);
let req_start = std::time::Instant::now();
loop {
if !conn.is_in_early_data() || http3_conn.is_some() {
poll.poll(&mut events, conn.timeout()).unwrap();
}
// Read incoming UDP packets from the socket and feed them to quiche,
// until there are no more packets to read.
'read: loop {
// If the event loop reported no events, it means that the timeout
// has expired, so handle it without attempting to read packets. We
// will then proceed with the send loop.
if events.is_empty() {
debug!("timed out");
conn.on_timeout();
break 'read;
}
let (len, from) = match socket.recv_from(&mut buf) {
Ok(v) => v,
Err(e) => {
// There are no more UDP packets to read, so end the read
// loop.
if e.kind() == std::io::ErrorKind::WouldBlock {
debug!("recv() would block");
break 'read;
}
return Err(Http3TestError::Other(format!(
"recv() failed: {:?}",
e
)));
},
};
debug!("got {} bytes", len);
let recv_info = quiche::RecvInfo { from };
// Process potentially coalesced packets.
let read = match conn.recv(&mut buf[..len], recv_info) {
Ok(v) => v,
Err(quiche::Error::Done) => {
debug!("done reading");
break;
},
Err(e) => {
error!("recv failed: {:?}", e);
break 'read;
},
};
debug!("processed {} bytes", read);
}
if conn.is_closed() {
info!("connection closed, {:?}", conn.stats());
if !conn.is_established() {
error!("connection timed out after {:?}", req_start.elapsed(),);
return Err(Http3TestError::HandshakeFail);
}
if reqs_complete != reqs_count {
error!("Client timed out after {:?} and only completed {}/{} requests",
req_start.elapsed(), reqs_complete, reqs_count);
return Err(Http3TestError::HttpFail);
}
if let Some(session_file) = session_file {
if let Some(session) = conn.session() {
std::fs::write(session_file, &session).ok();
}
}
break;
}
// Create a new HTTP/3 connection and end an HTTP request as soon as
// the QUIC connection is established.
if (conn.is_established() || conn.is_in_early_data()) &&
http3_conn.is_none()
{
let h3_config = quiche::h3::Config::new().unwrap();
let mut h3_conn =
quiche::h3::Connection::with_transport(&mut conn, &h3_config)
.unwrap();
reqs_count = test.requests_count();
match test.send_requests(&mut conn, &mut h3_conn) {
Ok(_) => (),
Err(quiche::h3::Error::Done) => (),
Err(e) => {
return Err(Http3TestError::Other(format!(
"error sending: {:?}",
e
)));
},
};
http3_conn = Some(h3_conn);
}
if let Some(http3_conn) = &mut http3_conn {
// Process HTTP/3 events.
loop {
match http3_conn.poll(&mut conn) {
Ok((stream_id, quiche::h3::Event::Headers { list, .. })) => {
info!(
"got response headers {:?} on stream id {}",
hdrs_to_strings(&list),
stream_id
);
test.add_response_headers(stream_id, &list);
},
Ok((stream_id, quiche::h3::Event::Data)) => {
while let Ok(read) =
http3_conn.recv_body(&mut conn, stream_id, &mut buf)
{
info!(
"got {} bytes of response data on stream {}",
read, stream_id
);
test.add_response_body(stream_id, &buf, read);
}
},
Ok((_stream_id, quiche::h3::Event::Finished)) => {
reqs_complete += 1;
info!(
"{}/{} responses received",
reqs_complete, reqs_count
);
if reqs_complete == reqs_count {
info!(
"Completed test run. {}/{} response(s) received in {:?}, closing...",
reqs_complete,
reqs_count,
req_start.elapsed()
);
match conn.close(true, 0x00, b"kthxbye") {
// Already closed.
Ok(_) | Err(quiche::Error::Done) => (),
Err(e) => {
return Err(Http3TestError::Other(format!(
"error closing conn: {:?}",
e
)));
},
}
test.assert();
break;
}
match test.send_requests(&mut conn, http3_conn) {
Ok(_) => (),
Err(quiche::h3::Error::Done) => (),
Err(e) => {
return Err(Http3TestError::Other(format!(
"error sending request: {:?}",
e
)));
},
}
},
Ok((stream_id, quiche::h3::Event::Reset(e))) => {
reqs_complete += 1;
info!("request was reset by peer with {}", e);
test.set_reset_stream_error(stream_id, e);
if reqs_complete == reqs_count {
info!(
"Completed test run. {}/{} response(s) received in {:?}, closing...",
reqs_complete,
reqs_count,
req_start.elapsed()
);
match conn.close(true, 0x00, b"kthxbye") {
// Already closed.
Ok(_) | Err(quiche::Error::Done) => (),
Err(e) => {
return Err(Http3TestError::Other(format!(
"error closing conn: {:?}",
e
)));
},
}
test.assert();
break;
}
},
Ok((_flow_id, quiche::h3::Event::Datagram)) => (),
Ok((_, quiche::h3::Event::PriorityUpdate)) => (),
Ok((_goaway_id, quiche::h3::Event::GoAway)) => (),
Err(quiche::h3::Error::Done) => {
break;
},
Err(e) => {
error!("HTTP/3 processing failed: {:?}", e);
break;
},
}
}
}
// Generate outgoing QUIC packets and send them on the UDP socket, until
// quiche reports that there are no more packets to be sent.
loop {
let (write, send_info) = match conn.send(&mut out) {
Ok(v) => v,
Err(quiche::Error::Done) => {
debug!("done writing");
break;
},
Err(e) => {
error!("send failed: {:?}", e);
conn.close(false, 0x1, b"fail").ok();
break;
},
};
if let Err(e) = socket.send_to(&out[..write], send_info.to) {
if e.kind() == std::io::ErrorKind::WouldBlock {
debug!("send() would block");
break;
}
return Err(Http3TestError::Other(format!(
"send() failed: {:?}",
e
)));
}
debug!("written {}", write);
}
if conn.is_closed() {
info!("connection closed, {:?}", conn.stats());
if reqs_complete != reqs_count {
error!("Client timed out after {:?} and only completed {}/{} requests",
req_start.elapsed(), reqs_complete, reqs_count);
return Err(Http3TestError::HttpFail);
}
if let Some(session_file) = session_file {
if let Some(session) = conn.session() {
std::fs::write(session_file, &session).ok();
}
}
break;
}
}
Ok(())
}
fn hdrs_to_strings(hdrs: &[quiche::h3::Header]) -> Vec<(String, String)> {
hdrs.iter()
.map(|h| {
(
String::from_utf8(h.name().into()).unwrap(),
String::from_utf8(h.value().into()).unwrap(),
)
})
.collect()
}