blob: 086b751ac2704e2e1b26e987b2ab0f1b12a2741a [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.
#[macro_use]
extern crate log;
use ring::rand::*;
const LOCAL_CONN_ID_LEN: usize = 16;
const MAX_DATAGRAM_SIZE: usize = 1350;
const USAGE: &str = "Usage:
http3-client [options] URL
http3-client -h | --help
Options:
--max-data BYTES Connection-wide flow control limit [default: 10000000].
--max-stream-data BYTES Per-stream flow control limit [default: 1000000].
--wire-version VERSION The version number to send to the server [default: babababa].
--no-verify Don't verify server's certificate.
--no-grease Don't send GREASE.
-h --help Show this screen.
";
fn main() -> Result<(), Box<std::error::Error>> {
let mut buf = [0; 65535];
let mut out = [0; MAX_DATAGRAM_SIZE];
env_logger::builder()
.default_format_timestamp_nanos(true)
.init();
let args = docopt::Docopt::new(USAGE)
.and_then(|dopt| dopt.parse())
.unwrap_or_else(|e| e.exit());
let url = url::Url::parse(args.get_str("URL"))?;
let socket = std::net::UdpSocket::bind("0.0.0.0:0")?;
socket.connect(&url)?;
let poll = mio::Poll::new()?;
let mut events = mio::Events::with_capacity(1024);
let socket = mio::net::UdpSocket::from_socket(socket)?;
poll.register(
&socket,
mio::Token(0),
mio::Ready::readable(),
mio::PollOpt::edge(),
)?;
let mut scid = [0; LOCAL_CONN_ID_LEN];
SystemRandom::new().fill(&mut scid[..])?;
let max_data = args.get_str("--max-data");
let max_data = u64::from_str_radix(max_data, 10)?;
let max_stream_data = args.get_str("--max-stream-data");
let max_stream_data = u64::from_str_radix(max_stream_data, 10)?;
let version = args.get_str("--wire-version");
let version = u32::from_str_radix(version, 16)?;
let mut config = quiche::Config::new(version)?;
config.verify_peer(true);
config.set_application_protos(b"\x05h3-19")?;
config.set_idle_timeout(5000);
config.set_max_packet_size(MAX_DATAGRAM_SIZE as u64);
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_migration(true);
let mut http3_conn = None;
if args.get_bool("--no-verify") {
config.verify_peer(false);
}
if args.get_bool("--no-grease") {
config.grease(false);
}
if std::env::var_os("SSLKEYLOGFILE").is_some() {
config.log_keys();
}
let mut conn = quiche::connect(url.domain(), &scid, &mut config)?;
let write = match conn.send(&mut out) {
Ok(v) => v,
Err(e) => panic!("{} initial send failed: {:?}", conn.trace_id(), e),
};
socket.send(&out[..write])?;
debug!("{} written {}", conn.trace_id(), write);
let req_start = std::time::Instant::now();
loop {
poll.poll(&mut events, conn.timeout())?;
'read: loop {
if events.is_empty() {
debug!("timed out");
conn.on_timeout();
break 'read;
}
let len = match socket.recv(&mut buf) {
Ok(v) => v,
Err(e) => {
if e.kind() == std::io::ErrorKind::WouldBlock {
debug!("recv() would block");
break 'read;
}
panic!("recv() failed: {:?}", e);
},
};
debug!("{} got {} bytes", conn.trace_id(), len);
// Process potentially coalesced packets.
let read = match conn.recv(&mut buf[..len]) {
Ok(v) => v,
Err(quiche::Error::Done) => {
debug!("{} done reading", conn.trace_id());
break;
},
Err(e) => {
error!("{} recv failed: {:?}", conn.trace_id(), e);
conn.close(false, e.to_wire(), b"fail")?;
break 'read;
},
};
debug!("{} processed {} bytes", conn.trace_id(), read);
}
if conn.is_closed() {
info!("{} connection closed, {:?}", conn.trace_id(), conn.stats());
break;
}
if conn.is_established() && http3_conn.is_none() {
if conn.application_proto() != b"h3-19" {
// TODO a better error code?
conn.close(false, 0x0, b"I don't support your ALPNs")?;
break;
}
let h3_config = quiche::h3::Config::new(0, 1024, 0, 0)?;
let mut h3_conn =
quiche::h3::Connection::with_transport(&mut conn, &h3_config)?;
let mut path = String::from(url.path());
if let Some(query) = url.query() {
path.push('?');
path.push_str(query);
}
let req = vec![
quiche::h3::Header::new(":method", "GET"),
quiche::h3::Header::new(":scheme", url.scheme()),
quiche::h3::Header::new(":authority", url.host_str().unwrap()),
quiche::h3::Header::new(":path", &path),
quiche::h3::Header::new("user-agent", "quiche"),
];
info!("{} sending HTTP request {:?}", conn.trace_id(), req);
if let Err(e) = h3_conn.send_request(&mut conn, &req, true) {
error!("{} failed to send request {:?}", conn.trace_id(), e);
break;
}
http3_conn = Some(h3_conn);
}
if let Some(http3_conn) = &mut http3_conn {
loop {
match http3_conn.poll(&mut conn) {
Ok((stream_id, quiche::h3::Event::Headers(headers))) => {
info!(
"{} got response headers {:?} on stream id {}",
conn.trace_id(),
headers,
stream_id
);
},
Ok((stream_id, quiche::h3::Event::Data(data))) => {
debug!(
"{} got response data of length {} in stream id {}",
conn.trace_id(),
data.len(),
stream_id
);
print!("{}", unsafe {
std::str::from_utf8_unchecked(&data)
});
if conn.stream_finished(stream_id) {
info!(
"{} response received in {:?}, closing...",
conn.trace_id(),
req_start.elapsed()
);
match conn.close(true, 0x00, b"kthxbye") {
// Already closed.
Ok(_) | Err(quiche::Error::Done) => (),
Err(e) => panic!("error closing conn: {:?}", e),
}
}
},
Err(quiche::h3::Error::Done) => {
break;
},
Err(e) => {
error!(
"{} HTTP/3 processing failed: {:?}",
conn.trace_id(),
e
);
break;
},
}
}
}
loop {
let write = match conn.send(&mut out) {
Ok(v) => v,
Err(quiche::Error::Done) => {
debug!("{} done writing", conn.trace_id());
break;
},
Err(e) => {
error!("{} send failed: {:?}", conn.trace_id(), e);
conn.close(false, e.to_wire(), b"fail")?;
break;
},
};
// TODO: coalesce packets.
socket.send(&out[..write])?;
debug!("{} written {}", conn.trace_id(), write);
}
if conn.is_closed() {
info!("{} connection closed, {:?}", conn.trace_id(), conn.stats());
break;
}
}
Ok(())
}