blob: d1de8431cd78569fc49ff23f50b588089c2902a4 [file] [log] [blame]
// Copyright (C) 2018, Cloudflare, Inc.
// Copyright (C) 2018, Alessandro Ghedini
// 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 std::net::ToSocketAddrs;
use std::io::prelude::*;
use ring::rand::*;
const MAX_DATAGRAM_SIZE: usize = 1350;
const HTTP_REQ_STREAM_ID: u64 = 4;
const USAGE: &str = "Usage:
client [options] URL
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].
--dump-packets PATH Dump the incoming packets as files in the given directory.
--no-verify Don't verify server's certificate.
-h --help Show this screen.
";
fn main() {
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 max_data = args.get_str("--max-data");
let max_data = u64::from_str_radix(max_data, 10).unwrap();
let max_stream_data = args.get_str("--max-stream-data");
let max_stream_data = u64::from_str_radix(max_stream_data, 10).unwrap();
let version = args.get_str("--wire-version");
let version = u32::from_str_radix(version, 16).unwrap();
let dump_path = if args.get_str("--dump-packets") != "" {
Some(args.get_str("--dump-packets"))
} else {
None
};
let url = url::Url::parse(args.get_str("URL")).unwrap();
// Setup the event loop.
let poll = mio::Poll::new().unwrap();
let mut events = mio::Events::with_capacity(1024);
// Resolve server address.
let peer_addr = url.to_socket_addrs().unwrap().next().unwrap();
// 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 socket = std::net::UdpSocket::bind(bind_addr).unwrap();
socket.connect(peer_addr).unwrap();
let socket = mio::net::UdpSocket::from_socket(socket).unwrap();
poll.register(
&socket,
mio::Token(0),
mio::Ready::readable(),
mio::PollOpt::edge(),
)
.unwrap();
// Create the configuration for the QUIC connection.
let mut config = quiche::Config::new(version).unwrap();
config.verify_peer(true);
config
.set_application_protos(b"\x05hq-23\x08http/0.9")
.unwrap();
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_streams_bidi(100);
config.set_initial_max_streams_uni(100);
config.set_disable_active_migration(true);
if args.get_bool("--no-verify") {
config.verify_peer(false);
}
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();
// Create a QUIC connection and initiate handshake.
let mut conn = quiche::connect(url.domain(), &scid, &mut config).unwrap();
info!(
"connecting to {:} from {:} with scid {}",
peer_addr,
socket.local_addr().unwrap(),
hex_dump(&scid)
);
let write = conn.send(&mut out).expect("initial send failed");
while let Err(e) = socket.send(&out[..write]) {
if e.kind() == std::io::ErrorKind::WouldBlock {
debug!("send() would block");
continue;
}
panic!("send() failed: {:?}", e);
}
debug!("written {}", write);
let req_start = std::time::Instant::now();
let mut req_sent = false;
let mut pkt_count = 0;
loop {
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 = match socket.recv(&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;
}
panic!("recv() failed: {:?}", e);
},
};
debug!("got {} bytes", len);
if let Some(target_path) = dump_path {
let path = format!("{}/{}.pkt", target_path, pkt_count);
if let Ok(f) = std::fs::File::create(&path) {
let mut f = std::io::BufWriter::new(f);
f.write_all(&buf[..len]).ok();
}
}
pkt_count += 1;
// Process potentially coalesced packets.
let read = match conn.recv(&mut buf[..len]) {
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());
break;
}
// Send an HTTP request as soon as the connection is established.
if conn.is_established() && !req_sent {
info!("sending HTTP request for {}", url.path());
let req = format!("GET {}\r\n", url.path());
conn.stream_send(HTTP_REQ_STREAM_ID, req.as_bytes(), true)
.unwrap();
req_sent = true;
}
// Process all readable streams.
for s in conn.readable() {
while let Ok((read, fin)) = conn.stream_recv(s, &mut buf) {
debug!("received {} bytes", read);
let stream_buf = &buf[..read];
debug!(
"stream {} has {} bytes (fin? {})",
s,
stream_buf.len(),
fin
);
print!("{}", unsafe {
std::str::from_utf8_unchecked(&stream_buf)
});
// The server reported that it has no more data to send, which
// we got the full response. Close the connection.
if s == HTTP_REQ_STREAM_ID && fin {
info!(
"response received in {:?}, closing...",
req_start.elapsed()
);
conn.close(true, 0x00, b"kthxbye").unwrap();
}
}
}
// 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 = 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(&out[..write]) {
if e.kind() == std::io::ErrorKind::WouldBlock {
debug!("send() would block");
break;
}
panic!("send() failed: {:?}", e);
}
debug!("written {}", write);
}
if conn.is_closed() {
info!("connection closed, {:?}", conn.stats());
break;
}
}
}
fn hex_dump(buf: &[u8]) -> String {
let vec: Vec<String> = buf.iter().map(|b| format!("{:02x}", b)).collect();
vec.join("")
}