tools: add quiche-client and quiche-server

This adds a client and server application that
supports both HTTP/0.9 and HTTP/3 by default,
with preference towards the latter.

Other major differences compared to the example
binaries are the ability to multiplex requests
to different URLS, and for the client to log
response bodies to an indicated folder.

Common code is extracted into a library crate
but there is suprisingly little in common
between these two apps.

This also changes the quic-interop-runner script
to use these tools instead of the example client and
servers.
diff --git a/.travis.yml b/.travis.yml
index 9ad50bc..c5921ea 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -47,6 +47,9 @@
       # quic-trace-log
       - RUSTFLAGS="-D warnings" cargo build --verbose --manifest-path tools/quic-trace-log/Cargo.toml
       - cargo clippy --manifest-path tools/quic-trace-log/Cargo.toml -- -D warnings
+      # quiche-apps
+      - RUSTFLAGS="-D warnings" cargo build --verbose --manifest-path tools/apps/Cargo.toml
+      - cargo clippy --manifest-path tools/apps/Cargo.toml -- -D warnings
       # x86 cross build
       - RUSTFLAGS="-D warnings" cargo build --target=$TARGET_32
    - name: "nightly Linux x86_64"
@@ -84,6 +87,9 @@
       # quic-trace-log
       - RUSTFLAGS="-D warnings" cargo build --verbose --manifest-path tools/quic-trace-log/Cargo.toml
       - cargo fmt --manifest-path tools/quic-trace-log/Cargo.toml -- --check
+      # quiche-apps
+      - RUSTFLAGS="-D warnings" cargo build --verbose --manifest-path tools/apps/Cargo.toml
+      - cargo fmt --manifest-path tools/apps/Cargo.toml -- --check
    - name: "stable macOS + iOS"
      language: rust
      rust: stable
diff --git a/extras/docker/Dockerfile b/extras/docker/Dockerfile
index 1546483..000a587 100644
--- a/extras/docker/Dockerfile
+++ b/extras/docker/Dockerfile
@@ -8,6 +8,8 @@
 RUN git clone --recurse-submodules --depth 1 https://github.com/cloudflare/quiche
 RUN cd quiche && \
     cargo build --release --examples
+RUN cd quiche && \
+    cargo build --manifest-path tools/apps/Cargo.toml --release
 
 ##
 ## quiche-base: base quiche image
@@ -20,6 +22,8 @@
      /build/quiche/target/release/examples/http3-server \
      /build/quiche/target/release/examples/client \
      /build/quiche/target/release/examples/server \
+     /build/quiche/tools/apps/target/release/quiche-client \
+     /build/quiche/tools/apps/target/release/quiche-server \
      /usr/local/bin/
 ENV PATH="/usr/local/bin/:${PATH}"
 ENV RUST_LOG=info
@@ -39,10 +43,8 @@
 COPY --from=build /build/quiche/examples/cert.crt \
      /build/quiche/examples/cert.key \
      examples/
-COPY --from=build /build/quiche/target/release/examples/client \
-     /build/quiche/target/release/examples/server \
-     /build/quiche/target/release/examples/http3-client \
-     /build/quiche/target/release/examples/http3-server \
+COPY --from=build /build/quiche/tools/apps/target/release/quiche-client \
+    /build/quiche/tools/apps/target/release/quiche-server \
      ./
 ENV RUST_LOG=trace
 
diff --git a/extras/docker/qns/run_endpoint.sh b/extras/docker/qns/run_endpoint.sh
index 9eb3d75..0a1a38e 100644
--- a/extras/docker/qns/run_endpoint.sh
+++ b/extras/docker/qns/run_endpoint.sh
@@ -12,10 +12,10 @@
 QUICHE_DIR=/quiche
 WWW_DIR=/www
 DOWNLOAD_DIR=/downloads
-QUICHE_CLIENT=client
-QUICHE_SERVER=server
-QUICHE_CLIENT_OPT="--no-verify"
-QUICHE_SERVER_OPT="--no-retry"
+QUICHE_CLIENT=quiche-client
+QUICHE_SERVER=quiche-server
+QUICHE_CLIENT_OPT="--no-verify --dump-responses ${DOWNLOAD_DIR}"
+QUICHE_SERVER_OPT="--no-retry --cert examples/cert.crt --key examples/cert.key"
 LOG_DIR=/logs
 LOG=$LOG_DIR/log.txt
 
@@ -36,8 +36,6 @@
         ;;
     http3 )
         echo "supported"
-        QUICHE_CLIENT=http3-client
-        QUICHE_SERVER=http3-server
         ;;
     *)
         echo "unsupported"
@@ -47,18 +45,17 @@
 }
 
 run_quiche_client_tests () {
-    for req in $REQUESTS
-    do
-        # get path only from the url
-        file=$(echo $req | perl -F'/' -an -e 'print $F[-1]')
-        $QUICHE_DIR/$QUICHE_CLIENT $QUICHE_CLIENT_OPT \
-            $CLIENT_PARAMS $req > $DOWNLOAD_DIR/$file 2> $LOG || exit 127
-    done
+    # TODO: https://github.com/marten-seemann/quic-interop-runner/issues/61
+    # remove this sleep when the issue above is resolved.
+    sleep 3
+    $QUICHE_DIR/$QUICHE_CLIENT $QUICHE_CLIENT_OPT \
+        $CLIENT_PARAMS $REQUESTS >& $LOG
+
 }
 
 run_quiche_server_tests() {
     $QUICHE_DIR/$QUICHE_SERVER --listen 0.0.0.0:443 --root $WWW_DIR \
-        $SERVER_PARAMS $QUICHE_SERVER_OPT 2> $LOG || exit 127
+        $SERVER_PARAMS $QUICHE_SERVER_OPT >& $LOG
 }
 
 # Update config based on test case
@@ -76,6 +73,8 @@
     echo "## Test case: $TESTCASE"
     run_quiche_client_tests
 elif [ "$ROLE" == "server" ]; then
+    # Wait for the simulator to start up.
+    /wait-for-it.sh sim:57832 -s -t 30
     echo "## Starting quiche server..."
     echo "## Server params: $SERVER_PARAMS"
     echo "## Test case: $TESTCASE"
diff --git a/tools/apps/Cargo.toml b/tools/apps/Cargo.toml
new file mode 100644
index 0000000..58db78e
--- /dev/null
+++ b/tools/apps/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "quiche_apps"
+version = "0.1.0"
+authors = ["Lucas Pardue <lucaspardue.24.7@gmail.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+docopt = "1"
+env_logger = "0.6"
+mio = "0.6"
+url = "1"
+log = "0.4"
+ring = "0.16"
+quiche = { path = "../../"}
+
+[lib]
+crate-type = ["lib"]
\ No newline at end of file
diff --git a/tools/apps/src/bin/cert.crt b/tools/apps/src/bin/cert.crt
new file mode 100644
index 0000000..adfed31
--- /dev/null
+++ b/tools/apps/src/bin/cert.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDkzCCAnugAwIBAgIUaj26Dyzr2W9R8juKm2pNyrtati0wDQYJKoZIhvcNAQEL
+BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJcXVpYy50ZWNoMB4X
+DTE4MDkzMDIyMTE0OFoXDTE5MDkzMDIyMTE0OFowWTELMAkGA1UEBhMCQVUxEzAR
+BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5
+IEx0ZDESMBAGA1UEAwwJcXVpYy50ZWNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAqrS30fnkI6Q+5SKsBXkIwnhO61x/Wgt0zo5P+0yTAZDYVYtEhRlf
+mJ3esEleO1nq5MtM3d+6aVBJlwtTi8pBOzVfJklnxd07N3rKh3HZbGHybjhJFGT9
+U4sUrcKcCpSKJaEu7IQsQQs1Hh0B67MeqJG3F7OcYCF3OXC11WK3CtDDKcLcsa2x
++WImzsPfayzEjQ4ELTVDP73oQGR6D3HaWauKES4JjI9CMn8EJRCcxjwet+c4U3kQ
+g2z5KDbooBfCfrzmX3/EpMf/RaASaUtZF3kgfDT648dICWUoiparo1V73pg2vDe5
+RsAp4n1A7VCY48VvGEz9Qgcp8QFztpFJnwIDAQABo1MwUTAdBgNVHQ4EFgQUFOlS
+IeYH/41CN5BP/8w8F3e/fkYwHwYDVR0jBBgwFoAUFOlSIeYH/41CN5BP/8w8F3e/
+fkYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAZa7XK3My4Jpe
+SLz0BAj44QtghGdg98QFR3iZEnn0XC09HrkhbjaR8Ma3dn0QyMDRuPLLNl5j3VWu
+rDqngENbuJJBPGkCTzozFfMU6MZzGLK1ljIiGzkMXVEaamSj7GDJ2eR2i2cBugiM
+Yv7N/e8FbSMRBXoYVPjukoA8QwDJhS/oN47vt0+VsTi5wah9d3t0RCruAe/4TETo
+jPxjbEGTQ71dmU66xPZMrnqlGCNa4kN2alCDNfSg1yRp4j10zSmK0jHEHOuiHliW
+/Zc+aLEFcVB1QHmIyvcBIhKiuDbfbkWrqSiel6nLScIvhJaJOrGzQYBfjeZ4TO0m
+IHJUojcgZA==
+-----END CERTIFICATE-----
diff --git a/tools/apps/src/bin/cert.key b/tools/apps/src/bin/cert.key
new file mode 100644
index 0000000..0a2c39b
--- /dev/null
+++ b/tools/apps/src/bin/cert.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCqtLfR+eQjpD7l
+IqwFeQjCeE7rXH9aC3TOjk/7TJMBkNhVi0SFGV+Ynd6wSV47Werky0zd37ppUEmX
+C1OLykE7NV8mSWfF3Ts3esqHcdlsYfJuOEkUZP1TixStwpwKlIoloS7shCxBCzUe
+HQHrsx6okbcXs5xgIXc5cLXVYrcK0MMpwtyxrbH5YibOw99rLMSNDgQtNUM/vehA
+ZHoPcdpZq4oRLgmMj0IyfwQlEJzGPB635zhTeRCDbPkoNuigF8J+vOZff8Skx/9F
+oBJpS1kXeSB8NPrjx0gJZSiKlqujVXvemDa8N7lGwCnifUDtUJjjxW8YTP1CBynx
+AXO2kUmfAgMBAAECggEAdWR0KT1NS8luy0qtu9HBWWM8+pSQq87HFClADZRaYDBI
+5YMxqsqJOD4Q33CFEhHC/HZmtQpfen8RLINIgBCmDV6lwYGnkKWUTJHv53c+y08M
+Vgn1D8Zng+VYYio7/vapjjkrONGoUU6wx7WxFXMHuWsD25PUDTPWdrTxBv6s3A0X
+Le7UtuCdo/xNY4YS6S64SfiEPsBddj1NhoiwOHkXekpNRoAwnizjngubEkiznScu
+gwKCW4nPV8y4CoIYyncGayrKieg03llgRngFiGJKpKeyL2UkX07Fqb2tXuJ36+RA
+9DrluEkYWZCjOS+aaQu+NwxCkUV5pq+HcXQmF5VX+QKBgQDTrgF4sKwcIjm+k3Fp
+bqhMS5stuSQJVn85fCIeQLq3u5DRq9n+UOvq6GvdEXz0SiupLfkXx/pDwiOux2sn
+CcwMaPqWbFE4mSsCFCBkL/PvXSzH2zYesHOplztvcV+gexAjmoCikMBCcM00QpN1
+GScUmQGTk/7BKJYGnVchJOXbfQKBgQDOcoZryCDxUPsg2ZMwkrnpcM+fSTT1gcgf
+I3gbGohagiXVTDU4+S7I7WmsJv+lBUJCWRG0p8JJZb0NsgnrGyOfCKL59xAV5PyT
+xSXMIi2+OH+fQXblII76GqWCs7A7NxtEU2geSy4ePPzSS4G81FN2oeV1OxZ9a6fk
+6cFIzmqsSwKBgQDIBQlg6NiI8RJNcXdeH/EpvtuQNfzGUhR/1jtLCPEmgjcS2Odx
+Nzflzd92knrXP2rIPye7//wMoNsk4UzwI4LLSztWfl21NI5+NVRyNxmyWgHhi9M0
+5pk0bDH+WUv6Ea8rZWgdtNfnMD3HHw3FPZI/FWF2+QZlsRsqfuyA5iPI5QKBgQCu
+D7F2Po5H6FdUIx4O3icRw6PKURbtyDbKykUB1SUR6pmrdU2Kc84WatWl6Fuy7vQm
+rKJZBviwma8EVRA3wfIOrGF9D+noC+FJVffAXTDkKQ6xX6i3FvR1uvHBeW8k/hln
+SkuG/ywrIpCnXjJM21hjtayZYvBbXuF4B/6HPEKEcQKBgQC+DVoOVjsoyd9udTcp
+1v2xvwRVvU/OrPOLXwac1IbTgmb5FJYd8EZI0hdxJhialoTK3OONk04uxdn5tlAB
+QwKBmkXZEr9EIreMp18gbzmDGalx8UcS0j+nIZvmpZXWsIimAKDGEwFc8w+NAN5a
+X5UkSGjM6dnJocH0sLI7hXuVJw==
+-----END PRIVATE KEY-----
diff --git a/tools/apps/src/bin/quiche-client.rs b/tools/apps/src/bin/quiche-client.rs
new file mode 100644
index 0000000..f6ad7fa
--- /dev/null
+++ b/tools/apps/src/bin/quiche-client.rs
@@ -0,0 +1,380 @@
+// Copyright (C) 2020, 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 std::net::ToSocketAddrs;
+
+use std::io::prelude::*;
+
+use ring::rand::*;
+
+use quiche_apps::*;
+
+const MAX_DATAGRAM_SIZE: usize = 1350;
+
+const USAGE: &str = "Usage:
+  quiche-client [options] URL...
+  quiche-client -h | --help
+
+Options:
+  --method METHOD          Use the given HTTP request method [default: GET].
+  --body FILE              Send the given file as request body.
+  --max-data BYTES         Connection-wide flow control limit [default: 10000000].
+  --max-stream-data BYTES  Per-stream flow control limit [default: 1000000].
+  --max-streams-bidi STREAMS  Number of allowed concurrent streams [default: 100].
+  --max-streams-uni STREAMS   Number of allowed concurrent streams [default: 100].
+  --wire-version VERSION   The version number to send to the server [default: babababa].
+  --http-version VERSION   HTTP version to use [default: all].
+  --dump-packets PATH      Dump the incoming packets as files in the given directory.
+  --dump-responses PATH    Dump response payload as files in the given directory.
+  --no-verify              Don't verify server's certificate.
+  --no-grease              Don't send GREASE.
+  -H --header HEADER ...   Add a request header.
+  -n --requests REQUESTS   Send the given number of identical requests [default: 1].
+  -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();
+
+    // Parse CLI parameters.
+    let docopt = docopt::Docopt::new(USAGE).unwrap();
+    let conn_args = CommonArgs::with_docopt(&docopt);
+    let args = ClientArgs::with_docopt(&docopt);
+
+    // Setup the event loop.
+    let poll = mio::Poll::new().unwrap();
+    let mut events = mio::Events::with_capacity(1024);
+
+    // We'll only connect to the first server provided in URL list.
+    let connect_url = &args.urls[0];
+
+    // Resolve server address.
+    let peer_addr = connect_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(args.version).unwrap();
+
+    config.verify_peer(!args.no_verify);
+
+    config.set_application_protos(&conn_args.alpns).unwrap();
+
+    config.set_max_idle_timeout(5000);
+    config.set_max_packet_size(MAX_DATAGRAM_SIZE as u64);
+    config.set_initial_max_data(conn_args.max_data);
+    config.set_initial_max_stream_data_bidi_local(conn_args.max_stream_data);
+    config.set_initial_max_stream_data_bidi_remote(conn_args.max_stream_data);
+    config.set_initial_max_stream_data_uni(conn_args.max_stream_data);
+    config.set_initial_max_streams_bidi(conn_args.max_streams_bidi);
+    config.set_initial_max_streams_uni(conn_args.max_streams_uni);
+    config.set_disable_active_migration(true);
+
+    if conn_args.no_grease {
+        config.grease(false);
+    }
+
+    if std::env::var_os("SSLKEYLOGFILE").is_some() {
+        config.log_keys();
+    }
+
+    let mut http_conn: Option<Box<dyn HttpConn>> = None;
+
+    // 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(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 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) = conn_args.dump_packet_path.as_ref() {
+                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());
+
+            if let Some(h_conn) = http_conn {
+                h_conn.report_incomplete(&req_start);
+            }
+
+            break;
+        }
+
+        // Create a new HTTP connection once the QUIC connection is established.
+        if conn.is_established() && http_conn.is_none() {
+            // At this stage the ALPN negotiation succeeded and selected a
+            // single application protocol name. We'll use this to construct
+            // the correct type of HttpConn but `application_proto()`
+            // returns a slice, so we have to convert it to a str in order
+            // to compare to our lists of protocols. We `unwrap()` because
+            // we need the value and if something fails at this stage, there
+            // is not much anyone can do to recover.
+
+            let app_proto = conn.application_proto();
+            let app_proto = &std::str::from_utf8(&app_proto).unwrap();
+
+            if alpns::HTTP_09.contains(app_proto) {
+                http_conn =
+                    Some(Http09Conn::with_urls(&args.urls, args.reqs_cardinal));
+            } else if alpns::HTTP_3.contains(app_proto) {
+                http_conn = Some(Http3Conn::with_urls(
+                    &mut conn,
+                    &args.urls,
+                    args.reqs_cardinal,
+                    &args.req_headers,
+                    &args.body,
+                    &args.method,
+                ));
+            }
+        }
+
+        // If we have an HTTP connection, first issue the requests then
+        // process received data.
+        if let Some(h_conn) = http_conn.as_mut() {
+            h_conn.send_requests(&mut conn, &args.dump_response_path);
+            h_conn.handle_responses(&mut conn, &mut buf, &req_start);
+        }
+
+        // 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());
+
+            if let Some(h_conn) = http_conn {
+                h_conn.report_incomplete(&req_start);
+            }
+
+            break;
+        }
+    }
+}
+
+/// Application-specific arguments that compliment the `CommonArgs`.
+struct ClientArgs {
+    version: u32,
+    dump_response_path: Option<String>,
+    urls: Vec<url::Url>,
+    reqs_cardinal: u64,
+    req_headers: Vec<String>,
+    no_verify: bool,
+    body: Option<Vec<u8>>,
+    method: String,
+}
+
+impl Args for ClientArgs {
+    fn with_docopt(docopt: &docopt::Docopt) -> Self {
+        let args = docopt.parse().unwrap_or_else(|e| e.exit());
+
+        let version = args.get_str("--wire-version");
+        let version = u32::from_str_radix(version, 16).unwrap();
+
+        let dump_response_path = if args.get_str("--dump-responses") != "" {
+            Some(args.get_str("--dump-responses").to_string())
+        } else {
+            None
+        };
+
+        // URLs (can be multiple).
+        let urls: Vec<url::Url> = args
+            .get_vec("URL")
+            .into_iter()
+            .map(|x| url::Url::parse(x).unwrap())
+            .collect();
+
+        // Request headers (can be multiple).
+        let req_headers = args
+            .get_vec("--header")
+            .into_iter()
+            .map(|x| x.to_string())
+            .collect();
+
+        let reqs_cardinal = args.get_str("--requests");
+        let reqs_cardinal = u64::from_str_radix(reqs_cardinal, 10).unwrap();
+
+        let no_verify = args.get_bool("--no-verify");
+
+        let body = if args.get_bool("--body") {
+            std::fs::read(args.get_str("--body")).ok()
+        } else {
+            None
+        };
+
+        let method = args.get_str("--method").to_string();
+
+        ClientArgs {
+            version,
+            dump_response_path,
+            urls,
+            req_headers,
+            reqs_cardinal,
+            no_verify,
+            body,
+            method,
+        }
+    }
+}
diff --git a/tools/apps/src/bin/quiche-server.rs b/tools/apps/src/bin/quiche-server.rs
new file mode 100644
index 0000000..8380b7a
--- /dev/null
+++ b/tools/apps/src/bin/quiche-server.rs
@@ -0,0 +1,508 @@
+// Copyright (C) 2020, 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 std::net;
+
+use std::io::prelude::*;
+
+use std::collections::HashMap;
+
+use ring::rand::*;
+
+use quiche_apps::*;
+
+const MAX_DATAGRAM_SIZE: usize = 1350;
+
+const USAGE: &str = "Usage:
+  quiche-server [options]
+  quiche-server -h | --help
+
+Options:
+  --listen <addr>             Listen on the given IP:port [default: 127.0.0.1:4433]
+  --cert <file>               TLS certificate path [default: src/bin/cert.crt]
+  --key <file>                TLS certificate key path [default: src/bin/cert.key]
+  --root <dir>                Root directory [default: src/bin/root/]
+  --name <str>                Name of the server [default: quic.tech]
+  --max-data BYTES            Connection-wide flow control limit [default: 10000000].
+  --max-stream-data BYTES     Per-stream flow control limit [default: 1000000].
+  --max-streams-bidi STREAMS  Number of allowed concurrent streams [default: 100].
+  --max-streams-uni STREAMS   Number of allowed concurrent streams [default: 100].
+  --dump-packets PATH         Dump the incoming packets as files in the given directory.
+  --early-data                Enables receiving early data.
+  --no-retry                  Disable stateless retry.
+  --no-grease                 Don't send GREASE.
+  --http-version VERSION      HTTP version to use [default: all].
+  -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();
+
+    // Parse CLI parameters.
+    let docopt = docopt::Docopt::new(USAGE).unwrap();
+    let conn_args = CommonArgs::with_docopt(&docopt);
+    let args = ServerArgs::with_docopt(&docopt);
+
+    // Setup the event loop.
+    let poll = mio::Poll::new().unwrap();
+    let mut events = mio::Events::with_capacity(1024);
+
+    // Create the UDP listening socket, and register it with the event loop.
+    let socket = net::UdpSocket::bind(args.listen).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 connections.
+    let mut config = quiche::Config::new(quiche::PROTOCOL_VERSION).unwrap();
+
+    config.load_cert_chain_from_pem_file(&args.cert).unwrap();
+    config.load_priv_key_from_pem_file(&args.key).unwrap();
+
+    config.set_application_protos(&conn_args.alpns).unwrap();
+
+    config.set_max_idle_timeout(5000);
+    config.set_max_packet_size(MAX_DATAGRAM_SIZE as u64);
+    config.set_initial_max_data(conn_args.max_data);
+    config.set_initial_max_stream_data_bidi_local(conn_args.max_stream_data);
+    config.set_initial_max_stream_data_bidi_remote(conn_args.max_stream_data);
+    config.set_initial_max_stream_data_uni(conn_args.max_stream_data);
+    config.set_initial_max_streams_bidi(conn_args.max_streams_bidi);
+    config.set_initial_max_streams_uni(conn_args.max_streams_uni);
+    config.set_disable_active_migration(true);
+
+    if args.early_data {
+        config.enable_early_data();
+    }
+
+    if conn_args.no_grease {
+        config.grease(false);
+    }
+
+    if std::env::var_os("SSLKEYLOGFILE").is_some() {
+        config.log_keys();
+    }
+
+    let rng = SystemRandom::new();
+    let conn_id_seed =
+        ring::hmac::Key::generate(ring::hmac::HMAC_SHA256, &rng).unwrap();
+
+    let mut clients = ClientMap::new();
+
+    let mut pkt_count = 0;
+
+    loop {
+        // Find the shorter timeout from all the active connections.
+        //
+        // TODO: use event loop that properly supports timers
+        let timeout =
+            clients.values().filter_map(|(_, c)| c.conn.timeout()).min();
+
+        poll.poll(&mut events, 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");
+
+                clients.values_mut().for_each(|(_, c)| c.conn.on_timeout());
+
+                break 'read;
+            }
+
+            let (len, src) = 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;
+                    }
+
+                    panic!("recv() failed: {:?}", e);
+                },
+            };
+
+            debug!("got {} bytes", len);
+
+            let pkt_buf = &mut buf[..len];
+
+            if let Some(target_path) = conn_args.dump_packet_path.as_ref() {
+                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(pkt_buf).ok();
+                }
+            }
+
+            pkt_count += 1;
+
+            // Parse the QUIC packet's header.
+            let hdr = match quiche::Header::from_slice(
+                pkt_buf,
+                quiche::MAX_CONN_ID_LEN,
+            ) {
+                Ok(v) => v,
+
+                Err(e) => {
+                    error!("Parsing packet header failed: {:?}", e);
+                    continue;
+                },
+            };
+
+            trace!("got packet {:?}", hdr);
+
+            let conn_id = ring::hmac::sign(&conn_id_seed, &hdr.dcid);
+            let conn_id = &conn_id.as_ref()[..quiche::MAX_CONN_ID_LEN];
+
+            // Lookup a connection based on the packet's connection ID. If there
+            // is no connection matching, create a new one.
+            let (_, client) = if !clients.contains_key(&hdr.dcid) &&
+                !clients.contains_key(conn_id)
+            {
+                if hdr.ty != quiche::Type::Initial {
+                    error!("Packet is not Initial");
+                    continue;
+                }
+
+                if !quiche::version_is_supported(hdr.version) {
+                    warn!("Doing version negotiation");
+
+                    let len =
+                        quiche::negotiate_version(&hdr.scid, &hdr.dcid, &mut out)
+                            .unwrap();
+
+                    let out = &out[..len];
+
+                    if let Err(e) = socket.send_to(out, &src) {
+                        if e.kind() == std::io::ErrorKind::WouldBlock {
+                            debug!("send() would block");
+                            break;
+                        }
+
+                        panic!("send() failed: {:?}", e);
+                    }
+                    continue;
+                }
+
+                let mut scid = [0; quiche::MAX_CONN_ID_LEN];
+                scid.copy_from_slice(&conn_id);
+
+                let mut odcid = None;
+
+                if !args.no_retry {
+                    // Token is always present in Initial packets.
+                    let token = hdr.token.as_ref().unwrap();
+
+                    // Do stateless retry if the client didn't send a token.
+                    if token.is_empty() {
+                        warn!("Doing stateless retry");
+
+                        let new_token = mint_token(&hdr, &src);
+
+                        let len = quiche::retry(
+                            &hdr.scid, &hdr.dcid, &scid, &new_token, &mut out,
+                        )
+                        .unwrap();
+                        let out = &out[..len];
+
+                        if let Err(e) = socket.send_to(out, &src) {
+                            if e.kind() == std::io::ErrorKind::WouldBlock {
+                                debug!("send() would block");
+                                break;
+                            }
+
+                            panic!("send() failed: {:?}", e);
+                        }
+                        continue;
+                    }
+
+                    odcid = validate_token(&src, token);
+
+                    // The token was not valid, meaning the retry failed, so
+                    // drop the packet.
+                    if odcid == None {
+                        error!("Invalid address validation token");
+                        continue;
+                    }
+
+                    if scid.len() != hdr.dcid.len() {
+                        error!("Invalid destination connection ID");
+                        continue;
+                    }
+
+                    // Reuse the source connection ID we sent in the Retry
+                    // packet, instead of changing it again.
+                    scid.copy_from_slice(&hdr.dcid);
+                }
+
+                debug!(
+                    "New connection: dcid={} scid={}",
+                    hex_dump(&hdr.dcid),
+                    hex_dump(&scid)
+                );
+
+                let conn = quiche::accept(&scid, odcid, &mut config).unwrap();
+
+                let client = Client {
+                    conn,
+                    http_conn: None,
+                    partial_responses: HashMap::new(),
+                };
+
+                clients.insert(scid.to_vec(), (src, client));
+
+                clients.get_mut(&scid[..]).unwrap()
+            } else {
+                match clients.get_mut(&hdr.dcid) {
+                    Some(v) => v,
+
+                    None => clients.get_mut(conn_id).unwrap(),
+                }
+            };
+
+            // Process potentially coalesced packets.
+            let read = match client.conn.recv(pkt_buf) {
+                Ok(v) => v,
+
+                Err(quiche::Error::Done) => {
+                    debug!("{} done reading", client.conn.trace_id());
+                    break;
+                },
+
+                Err(e) => {
+                    error!("{} recv failed: {:?}", client.conn.trace_id(), e);
+                    break 'read;
+                },
+            };
+
+            debug!("{} processed {} bytes", client.conn.trace_id(), read);
+
+            // Create a new HTTP connection as soon as the QUIC connection
+            // is established.
+            if client.http_conn.is_none() &&
+                (client.conn.is_in_early_data() ||
+                    client.conn.is_established())
+            {
+                // At this stage the ALPN negotiation succeeded and selected a
+                // single application protocol name. We'll use this to construct
+                // the correct type of HttpConn but `application_proto()`
+                // returns a slice, so we have to convert it to a str in order
+                // to compare to our lists of protocols. We `unwrap()` because
+                // we need the value and if something fails at this stage, there
+                // is not much anyone can do to recover.
+                let app_proto = client.conn.application_proto();
+                let app_proto = &std::str::from_utf8(&app_proto).unwrap();
+
+                if alpns::HTTP_09.contains(app_proto) {
+                    client.http_conn = Some(Box::new(Http09Conn::default()));
+                } else if alpns::HTTP_3.contains(app_proto) {
+                    client.http_conn =
+                        Some(Http3Conn::with_conn(&mut client.conn));
+                }
+            }
+
+            if client.http_conn.is_some() {
+                let conn = &mut client.conn;
+                let http_conn = client.http_conn.as_mut().unwrap();
+                let partials = &mut client.partial_responses;
+
+                // Handle writable streams.
+                for stream_id in conn.writable() {
+                    http_conn.handle_writable(conn, partials, stream_id);
+                }
+
+                if http_conn
+                    .handle_requests(conn, partials, &args.root, &mut buf)
+                    .is_err()
+                {
+                    break 'read;
+                }
+            }
+        }
+
+        // Generate outgoing QUIC packets for all active connections and send
+        // them on the UDP socket, until quiche reports that there are no more
+        // packets to be sent.
+        for (peer, client) in clients.values_mut() {
+            loop {
+                let write = match client.conn.send(&mut out) {
+                    Ok(v) => v,
+
+                    Err(quiche::Error::Done) => {
+                        debug!("{} done writing", client.conn.trace_id());
+                        break;
+                    },
+
+                    Err(e) => {
+                        error!("{} send failed: {:?}", client.conn.trace_id(), e);
+
+                        client.conn.close(false, 0x1, b"fail").ok();
+                        break;
+                    },
+                };
+
+                // TODO: coalesce packets.
+                if let Err(e) = socket.send_to(&out[..write], &peer) {
+                    if e.kind() == std::io::ErrorKind::WouldBlock {
+                        debug!("send() would block");
+                        break;
+                    }
+
+                    panic!("send() failed: {:?}", e);
+                }
+
+                debug!("{} written {} bytes", client.conn.trace_id(), write);
+            }
+        }
+
+        // Garbage collect closed connections.
+        clients.retain(|_, (_, ref mut c)| {
+            debug!("Collecting garbage");
+
+            if c.conn.is_closed() {
+                info!(
+                    "{} connection collected {:?}",
+                    c.conn.trace_id(),
+                    c.conn.stats()
+                );
+            }
+
+            !c.conn.is_closed()
+        });
+    }
+}
+
+/// Generate a stateless retry token.
+///
+/// The token includes the static string `"quiche"` followed by the IP address
+/// of the client and by the original destination connection ID generated by the
+/// client.
+///
+/// Note that this function is only an example and doesn't do any cryptographic
+/// authenticate of the token. *It should not be used in production system*.
+fn mint_token(hdr: &quiche::Header, src: &net::SocketAddr) -> Vec<u8> {
+    let mut token = Vec::new();
+
+    token.extend_from_slice(b"quiche");
+
+    let addr = match src.ip() {
+        std::net::IpAddr::V4(a) => a.octets().to_vec(),
+        std::net::IpAddr::V6(a) => a.octets().to_vec(),
+    };
+
+    token.extend_from_slice(&addr);
+    token.extend_from_slice(&hdr.dcid);
+
+    token
+}
+
+/// Validates a stateless retry token.
+///
+/// This checks that the ticket includes the `"quiche"` static string, and that
+/// the client IP address matches the address stored in the ticket.
+///
+/// Note that this function is only an example and doesn't do any cryptographic
+/// authenticate of the token. *It should not be used in production system*.
+fn validate_token<'a>(
+    src: &net::SocketAddr, token: &'a [u8],
+) -> Option<&'a [u8]> {
+    if token.len() < 6 {
+        return None;
+    }
+
+    if &token[..6] != b"quiche" {
+        return None;
+    }
+
+    let token = &token[6..];
+
+    let addr = match src.ip() {
+        std::net::IpAddr::V4(a) => a.octets().to_vec(),
+        std::net::IpAddr::V6(a) => a.octets().to_vec(),
+    };
+
+    if token.len() < addr.len() || &token[..addr.len()] != addr.as_slice() {
+        return None;
+    }
+
+    let token = &token[addr.len()..];
+
+    Some(&token[..])
+}
+
+// Application-specific arguments that compliment the `CommonArgs`.
+struct ServerArgs {
+    listen: String,
+    no_retry: bool,
+    root: String,
+    cert: String,
+    key: String,
+    early_data: bool,
+}
+
+impl Args for ServerArgs {
+    fn with_docopt(docopt: &docopt::Docopt) -> Self {
+        let args = docopt.parse().unwrap_or_else(|e| e.exit());
+
+        let listen = args.get_str("--listen").to_string();
+        let no_retry = args.get_bool("--no-retry");
+        let early_data = args.get_bool("--early-data");
+        let root = args.get_str("--root").to_string();
+        let cert = args.get_str("--cert").to_string();
+        let key = args.get_str("--key").to_string();
+
+        ServerArgs {
+            listen,
+            no_retry,
+            root,
+            cert,
+            key,
+            early_data,
+        }
+    }
+}
diff --git a/tools/apps/src/lib.rs b/tools/apps/src/lib.rs
new file mode 100644
index 0000000..baa1e62
--- /dev/null
+++ b/tools/apps/src/lib.rs
@@ -0,0 +1,892 @@
+// Copyright (C) 2020, 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.
+
+//! Quiche application utilities.
+//!
+//! This module provides some utility functions that are common to quiche
+//! applications.
+
+#[macro_use]
+extern crate log;
+
+use std::io::prelude::*;
+
+use std::collections::HashMap;
+
+use std::net;
+
+/// Returns a String containing a pretty printed version of the `buf` slice.
+pub fn hex_dump(buf: &[u8]) -> String {
+    let vec: Vec<String> = buf.iter().map(|b| format!("{:02x}", b)).collect();
+
+    vec.join("")
+}
+
+/// ALPN helpers.
+///
+/// This module contains constants and functions for working with ALPN.
+pub mod alpns {
+    pub const HTTP_09: [&str; 4] = ["hq-25", "hq-24", "hq-23", "http/0.9"];
+    pub const HTTP_3: [&str; 3] = ["h3-25", "h3-24", "h3-23"];
+
+    pub fn length_prefixed(alpns: &[&str]) -> Vec<u8> {
+        let mut out = Vec::new();
+
+        for s in alpns {
+            out.push(s.len() as u8);
+            out.extend_from_slice(s.as_bytes());
+        }
+
+        out
+    }
+}
+
+pub trait Args {
+    fn with_docopt(docopt: &docopt::Docopt) -> Self;
+}
+
+/// Contains commons arguments for creating a quiche QUIC connection.
+pub struct CommonArgs {
+    pub alpns: Vec<u8>,
+    pub max_data: u64,
+    pub max_stream_data: u64,
+    pub max_streams_bidi: u64,
+    pub max_streams_uni: u64,
+    pub dump_packet_path: Option<String>,
+    pub no_grease: bool,
+}
+
+/// Creates a new `CommonArgs` structure using the provided [`Docopt`].
+///
+/// The `Docopt` usage String needs to include the following:
+///
+/// --http-version VERSION   HTTP version to use
+/// --max-data BYTES         Connection-wide flow control limit.
+/// --max-stream-data BYTES  Per-stream flow control limit.
+/// --max-streams-bidi STREAMS  Number of allowed concurrent streams.
+/// --max-streams-uni STREAMS   Number of allowed concurrent streams.
+/// --dump-packets PATH         Dump the incoming packets as files in the
+/// given directory. --no-grease                 Don't send GREASE.
+///
+/// [`Docopt`]: https://docs.rs/docopt/1.1.0/docopt/
+impl Args for CommonArgs {
+    fn with_docopt(docopt: &docopt::Docopt) -> Self {
+        let args = docopt.parse().unwrap_or_else(|e| e.exit());
+
+        let http_version = args.get_str("--http-version");
+        let alpns = match http_version {
+            "HTTP/0.9" => alpns::length_prefixed(&alpns::HTTP_09),
+
+            "HTTP/3" => alpns::length_prefixed(&alpns::HTTP_3),
+
+            "all" => [
+                alpns::length_prefixed(&alpns::HTTP_3),
+                alpns::length_prefixed(&alpns::HTTP_09),
+            ]
+            .concat(),
+
+            _ => panic!("Unsupported HTTP version"),
+        };
+
+        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 max_streams_bidi = args.get_str("--max-streams-bidi");
+        let max_streams_bidi = u64::from_str_radix(max_streams_bidi, 10).unwrap();
+
+        let max_streams_uni = args.get_str("--max-streams-uni");
+        let max_streams_uni = u64::from_str_radix(max_streams_uni, 10).unwrap();
+
+        let dump_packet_path = if args.get_str("--dump-packets") != "" {
+            Some(args.get_str("--dump-packets").to_string())
+        } else {
+            None
+        };
+
+        let no_grease = args.get_bool("--no-grease");
+
+        CommonArgs {
+            alpns,
+            max_data,
+            max_stream_data,
+            max_streams_bidi,
+            max_streams_uni,
+            dump_packet_path,
+            no_grease,
+        }
+    }
+}
+
+pub struct PartialResponse {
+    pub body: Vec<u8>,
+
+    pub written: usize,
+}
+
+pub struct Client {
+    pub conn: std::pin::Pin<Box<quiche::Connection>>,
+
+    pub http_conn: Option<Box<dyn crate::HttpConn>>,
+
+    pub partial_responses: std::collections::HashMap<u64, PartialResponse>,
+}
+
+pub type ClientMap = HashMap<Vec<u8>, (net::SocketAddr, Client)>;
+
+/// Makes a buffered writer for a resource with a target URL.
+///
+/// The file will have the same name as the resource's last path segment value.
+/// Multiple requests for the same URL are indicated by the value of `cardinal`,
+/// any value "N" greater than 1, will cause ".N" to be appended to the
+/// filename.
+fn make_writer(
+    url: &url::Url, target_path: &Option<String>, cardinal: u64,
+) -> Option<std::io::BufWriter<std::fs::File>> {
+    if let Some(tp) = target_path {
+        let resource =
+            url.path_segments().map(|c| c.collect::<Vec<_>>()).unwrap();
+
+        let mut path = format!("{}/{}", tp, resource.iter().last().unwrap());
+
+        if cardinal > 1 {
+            path = format!("{}.{}", path, cardinal);
+        }
+
+        match std::fs::File::create(&path) {
+            Ok(f) => return Some(std::io::BufWriter::new(f)),
+
+            Err(e) => panic!("Bad times: {}", e),
+        }
+    }
+
+    None
+}
+
+pub trait HttpConn {
+    fn send_requests(
+        &mut self, conn: &mut quiche::Connection, target_path: &Option<String>,
+    );
+
+    fn handle_responses(
+        &mut self, conn: &mut quiche::Connection, buf: &mut [u8],
+        req_start: &std::time::Instant,
+    );
+
+    fn report_incomplete(&self, start: &std::time::Instant);
+
+    fn handle_requests(
+        &mut self, conn: &mut std::pin::Pin<Box<quiche::Connection>>,
+        partial_responses: &mut HashMap<u64, PartialResponse>, root: &str,
+        buf: &mut [u8],
+    ) -> quiche::h3::Result<()>;
+
+    fn handle_writable(
+        &mut self, conn: &mut std::pin::Pin<Box<quiche::Connection>>,
+        partial_responses: &mut HashMap<u64, PartialResponse>, stream_id: u64,
+    );
+}
+
+/// Represents an HTTP/0.9 formatted request.
+pub struct Http09Request {
+    url: url::Url,
+    cardinal: u64,
+    request_line: String,
+    stream_id: Option<u64>,
+    response_writer: Option<std::io::BufWriter<std::fs::File>>,
+}
+
+/// Represents an HTTP/3 formatted request.
+struct Http3Request {
+    url: url::Url,
+    cardinal: u64,
+    stream_id: Option<u64>,
+    hdrs: Vec<quiche::h3::Header>,
+    response_writer: Option<std::io::BufWriter<std::fs::File>>,
+}
+
+#[derive(Default)]
+pub struct Http09Conn {
+    stream_id: u64,
+    reqs_sent: usize,
+    reqs_complete: usize,
+    reqs: Vec<Http09Request>,
+}
+
+impl Http09Conn {
+    pub fn with_urls(urls: &[url::Url], reqs_cardinal: u64) -> Box<dyn HttpConn> {
+        let mut reqs = Vec::new();
+        for url in urls {
+            for i in 1..=reqs_cardinal {
+                let request_line = format!("GET {}\r\n", url.path());
+                reqs.push(Http09Request {
+                    url: url.clone(),
+                    cardinal: i,
+                    request_line,
+                    stream_id: None,
+                    response_writer: None,
+                });
+            }
+        }
+
+        let h_conn = Http09Conn {
+            stream_id: 0,
+            reqs_sent: 0,
+            reqs_complete: 0,
+            reqs,
+        };
+
+        Box::new(h_conn)
+    }
+}
+
+impl HttpConn for Http09Conn {
+    fn send_requests(
+        &mut self, conn: &mut quiche::Connection, target_path: &Option<String>,
+    ) {
+        let mut reqs_done = 0;
+
+        for req in self.reqs.iter_mut().skip(self.reqs_sent) {
+            info!("sending HTTP request {:?}", req.request_line);
+
+            match conn.stream_send(
+                self.stream_id,
+                req.request_line.as_bytes(),
+                true,
+            ) {
+                Ok(v) => v,
+
+                Err(quiche::Error::StreamLimit) => {
+                    debug!("not enough stream credits, retry later...");
+                    break;
+                },
+
+                Err(e) => {
+                    error!("failed to send request {:?}", e);
+                    break;
+                },
+            };
+
+            req.stream_id = Some(self.stream_id);
+            req.response_writer =
+                make_writer(&req.url, target_path, req.cardinal);
+
+            self.stream_id += 4;
+
+            reqs_done += 1;
+        }
+
+        self.reqs_sent += reqs_done;
+    }
+
+    fn handle_responses(
+        &mut self, conn: &mut quiche::Connection, buf: &mut [u8],
+        req_start: &std::time::Instant,
+    ) {
+        // Process all readable streams.
+        for s in conn.readable() {
+            while let Ok((read, fin)) = conn.stream_recv(s, buf) {
+                debug!("received {} bytes", read);
+
+                let stream_buf = &buf[..read];
+
+                debug!(
+                    "stream {} has {} bytes (fin? {})",
+                    s,
+                    stream_buf.len(),
+                    fin
+                );
+
+                let req = self
+                    .reqs
+                    .iter_mut()
+                    .find(|r| r.stream_id == Some(s))
+                    .unwrap();
+
+                match &mut req.response_writer {
+                    Some(rw) => {
+                        rw.write_all(&buf[..read]).ok();
+                    },
+
+                    None => {
+                        print!("{}", unsafe {
+                            std::str::from_utf8_unchecked(&stream_buf)
+                        });
+                    },
+                }
+
+                // The server reported that it has no more data to send on
+                // a client-initiated
+                // bidirectional stream, which means
+                // we got the full response. If all responses are received
+                // then close the connection.
+                if &s % 4 == 0 && fin {
+                    self.reqs_complete += 1;
+                    let reqs_count = self.reqs.len();
+
+                    debug!(
+                        "{}/{} responses received",
+                        self.reqs_complete, reqs_count
+                    );
+
+                    if self.reqs_complete == reqs_count {
+                        info!(
+                            "{}/{} response(s) received in {:?}, closing...",
+                            self.reqs_complete,
+                            reqs_count,
+                            req_start.elapsed()
+                        );
+
+                        match conn.close(true, 0x00, b"kthxbye") {
+                            // Already closed.
+                            Ok(_) | Err(quiche::Error::Done) => (),
+
+                            Err(e) => panic!("error closing conn: {:?}", e),
+                        }
+
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    fn report_incomplete(&self, start: &std::time::Instant) {
+        if self.reqs_complete != self.reqs.len() {
+            error!(
+                "connection timed out after {:?} and only completed {}/{} requests",
+                start.elapsed(),
+                self.reqs_complete,
+                self.reqs.len()
+            );
+        }
+    }
+
+    fn handle_requests(
+        &mut self, conn: &mut std::pin::Pin<Box<quiche::Connection>>,
+        partial_responses: &mut HashMap<u64, PartialResponse>, root: &str,
+        buf: &mut [u8],
+    ) -> quiche::h3::Result<()> {
+        // Process all readable streams.
+        for s in conn.readable() {
+            while let Ok((read, fin)) = conn.stream_recv(s, buf) {
+                debug!("{} received {} bytes", conn.trace_id(), read);
+
+                let stream_buf = &buf[..read];
+
+                debug!(
+                    "{} stream {} has {} bytes (fin? {})",
+                    conn.trace_id(),
+                    s,
+                    stream_buf.len(),
+                    fin
+                );
+
+                if stream_buf.len() > 4 && &stream_buf[..4] == b"GET " {
+                    let uri = &buf[4..stream_buf.len()];
+                    let uri = String::from_utf8(uri.to_vec()).unwrap();
+                    let uri = String::from(uri.lines().next().unwrap());
+                    let uri = std::path::Path::new(&uri);
+                    let mut path = std::path::PathBuf::from(root);
+
+                    for c in uri.components() {
+                        if let std::path::Component::Normal(v) = c {
+                            path.push(v)
+                        }
+                    }
+
+                    info!(
+                        "{} got GET request for {:?} on stream {}",
+                        conn.trace_id(),
+                        path,
+                        s
+                    );
+
+                    let body = std::fs::read(path.as_path())
+                        .unwrap_or_else(|_| b"Not Found!\r\n".to_vec());
+
+                    info!(
+                        "{} sending response of size {} on stream {}",
+                        conn.trace_id(),
+                        body.len(),
+                        s
+                    );
+
+                    let written = match conn.stream_send(s, &body, true) {
+                        Ok(v) => v,
+
+                        Err(quiche::Error::Done) => 0,
+
+                        Err(e) => {
+                            error!(
+                                "{} stream send failed {:?}",
+                                conn.trace_id(),
+                                e
+                            );
+                            return Err(From::from(e));
+                        },
+                    };
+
+                    if written < body.len() {
+                        let response = PartialResponse { body, written };
+                        partial_responses.insert(s, response);
+                    }
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn handle_writable(
+        &mut self, conn: &mut std::pin::Pin<Box<quiche::Connection>>,
+        partial_responses: &mut HashMap<u64, PartialResponse>, stream_id: u64,
+    ) {
+        debug!("{} stream {} is writable", conn.trace_id(), stream_id);
+
+        if !partial_responses.contains_key(&stream_id) {
+            return;
+        }
+
+        let resp = partial_responses.get_mut(&stream_id).unwrap();
+        let body = &resp.body[resp.written..];
+
+        let written = match conn.stream_send(stream_id, &body, true) {
+            Ok(v) => v,
+
+            Err(quiche::Error::Done) => 0,
+
+            Err(e) => {
+                error!("{} stream send failed {:?}", conn.trace_id(), e);
+                return;
+            },
+        };
+
+        resp.written += written;
+
+        if resp.written == resp.body.len() {
+            partial_responses.remove(&stream_id);
+        }
+    }
+}
+
+pub struct Http3Conn {
+    h3_conn: quiche::h3::Connection,
+    reqs_sent: usize,
+    reqs_complete: usize,
+    reqs: Vec<Http3Request>,
+    body: Option<Vec<u8>>,
+}
+
+impl Http3Conn {
+    pub fn with_urls(
+        conn: &mut quiche::Connection, urls: &[url::Url], reqs_cardinal: u64,
+        req_headers: &[String], body: &Option<Vec<u8>>, method: &str,
+    ) -> Box<dyn HttpConn> {
+        let mut reqs = Vec::new();
+        for url in urls {
+            for i in 1..=reqs_cardinal {
+                let mut hdrs = vec![
+                    quiche::h3::Header::new(":method", &method),
+                    quiche::h3::Header::new(":scheme", url.scheme()),
+                    quiche::h3::Header::new(
+                        ":authority",
+                        url.host_str().unwrap(),
+                    ),
+                    quiche::h3::Header::new(
+                        ":path",
+                        &url[url::Position::BeforePath..],
+                    ),
+                    quiche::h3::Header::new("user-agent", "quiche"),
+                ];
+
+                // Add custom headers to the request.
+                for header in req_headers {
+                    let header_split: Vec<&str> =
+                        header.splitn(2, ": ").collect();
+                    if header_split.len() != 2 {
+                        panic!("malformed header provided - \"{}\"", header);
+                    }
+
+                    hdrs.push(quiche::h3::Header::new(
+                        header_split[0],
+                        header_split[1],
+                    ));
+                }
+
+                if body.is_some() {
+                    hdrs.push(quiche::h3::Header::new(
+                        "content-length",
+                        &body.as_ref().unwrap().len().to_string(),
+                    ));
+                }
+
+                reqs.push(Http3Request {
+                    url: url.clone(),
+                    cardinal: i,
+                    hdrs,
+                    stream_id: None,
+                    response_writer: None,
+                });
+            }
+        }
+
+        let h_conn = Http3Conn {
+            h3_conn: quiche::h3::Connection::with_transport(
+                conn,
+                &quiche::h3::Config::new().unwrap(),
+            )
+            .unwrap(),
+            reqs_sent: 0,
+            reqs_complete: 0,
+            reqs,
+            body: None,
+        };
+
+        Box::new(h_conn)
+    }
+
+    pub fn with_conn(conn: &mut quiche::Connection) -> Box<dyn HttpConn> {
+        let h_conn = Http3Conn {
+            h3_conn: quiche::h3::Connection::with_transport(
+                conn,
+                &quiche::h3::Config::new().unwrap(),
+            )
+            .unwrap(),
+            reqs_sent: 0,
+            reqs_complete: 0,
+            reqs: Vec::new(),
+            body: None,
+        };
+
+        Box::new(h_conn)
+    }
+
+    /// Builds an HTTP/3 response given a request.
+    fn build_h3_response(
+        root: &str, request: &[quiche::h3::Header],
+    ) -> (Vec<quiche::h3::Header>, Vec<u8>) {
+        let mut file_path = std::path::PathBuf::from(root);
+        let mut path = std::path::Path::new("");
+        let mut method = "";
+
+        // Look for the request's path and method.
+        for hdr in request {
+            match hdr.name() {
+                ":path" => {
+                    path = std::path::Path::new(hdr.value());
+                },
+
+                ":method" => {
+                    method = hdr.value();
+                },
+
+                _ => (),
+            }
+        }
+
+        let (status, body) = match method {
+            "GET" => {
+                for c in path.components() {
+                    if let std::path::Component::Normal(v) = c {
+                        file_path.push(v)
+                    }
+                }
+
+                match std::fs::read(file_path.as_path()) {
+                    Ok(data) => (200, data),
+
+                    Err(_) => (404, b"Not Found!".to_vec()),
+                }
+            },
+
+            _ => (405, Vec::new()),
+        };
+
+        let headers = vec![
+            quiche::h3::Header::new(":status", &status.to_string()),
+            quiche::h3::Header::new("server", "quiche"),
+            quiche::h3::Header::new("content-length", &body.len().to_string()),
+        ];
+
+        (headers, body)
+    }
+}
+
+impl HttpConn for Http3Conn {
+    fn send_requests(
+        &mut self, conn: &mut quiche::Connection, target_path: &Option<String>,
+    ) {
+        let mut reqs_done = 0;
+
+        for req in self.reqs.iter_mut().skip(self.reqs_sent) {
+            info!("sending HTTP request {:?}", req.hdrs);
+
+            let s = match self.h3_conn.send_request(
+                conn,
+                &req.hdrs,
+                self.body.is_none(),
+            ) {
+                Ok(v) => v,
+
+                Err(quiche::h3::Error::TransportError(
+                    quiche::Error::StreamLimit,
+                )) => {
+                    debug!("not enough stream credits, retry later...");
+                    break;
+                },
+
+                Err(e) => {
+                    error!("failed to send request {:?}", e);
+                    break;
+                },
+            };
+
+            req.stream_id = Some(s);
+            req.response_writer =
+                make_writer(&req.url, target_path, req.cardinal);
+
+            if let Some(body) = &self.body {
+                if let Err(e) = self.h3_conn.send_body(conn, s, body, true) {
+                    error!("failed to send request body {:?}", e);
+                    break;
+                }
+            }
+
+            reqs_done += 1;
+        }
+
+        self.reqs_sent += reqs_done;
+    }
+
+    fn handle_responses(
+        &mut self, conn: &mut quiche::Connection, buf: &mut [u8],
+        req_start: &std::time::Instant,
+    ) {
+        loop {
+            match self.h3_conn.poll(conn) {
+                Ok((stream_id, quiche::h3::Event::Headers { list, .. })) => {
+                    info!(
+                        "got response headers {:?} on stream id {}",
+                        list, stream_id
+                    );
+                },
+
+                Ok((stream_id, quiche::h3::Event::Data)) => {
+                    if let Ok(read) = self.h3_conn.recv_body(conn, stream_id, buf)
+                    {
+                        debug!(
+                            "got {} bytes of response data on stream {}",
+                            read, stream_id
+                        );
+
+                        let req = self
+                            .reqs
+                            .iter_mut()
+                            .find(|r| r.stream_id == Some(stream_id))
+                            .unwrap();
+
+                        match &mut req.response_writer {
+                            Some(rw) => {
+                                rw.write_all(&buf[..read]).ok();
+                            },
+
+                            None => {
+                                print!("{}", unsafe {
+                                    std::str::from_utf8_unchecked(&buf[..read])
+                                });
+                            },
+                        }
+                    }
+                },
+
+                Ok((_stream_id, quiche::h3::Event::Finished)) => {
+                    self.reqs_complete += 1;
+                    let reqs_count = self.reqs.len();
+
+                    debug!(
+                        "{}/{} responses received",
+                        self.reqs_complete, reqs_count
+                    );
+
+                    if self.reqs_complete == reqs_count {
+                        info!(
+                            "{}/{} response(s) received in {:?}, closing...",
+                            self.reqs_complete,
+                            reqs_count,
+                            req_start.elapsed()
+                        );
+
+                        match conn.close(true, 0x00, b"kthxbye") {
+                            // Already closed.
+                            Ok(_) | Err(quiche::Error::Done) => (),
+
+                            Err(e) => panic!("error closing conn: {:?}", e),
+                        }
+
+                        break;
+                    }
+                },
+
+                Err(quiche::h3::Error::Done) => {
+                    break;
+                },
+
+                Err(e) => {
+                    error!("HTTP/3 processing failed: {:?}", e);
+
+                    break;
+                },
+            }
+        }
+    }
+
+    fn report_incomplete(&self, start: &std::time::Instant) {
+        if self.reqs_complete != self.reqs.len() {
+            error!(
+                "connection timed out after {:?} and only completed {}/{} requests",
+                start.elapsed(),
+                self.reqs_complete,
+                self.reqs.len()
+            );
+        }
+    }
+
+    fn handle_requests(
+        &mut self, conn: &mut std::pin::Pin<Box<quiche::Connection>>,
+        partial_responses: &mut HashMap<u64, PartialResponse>, root: &str,
+        _buf: &mut [u8],
+    ) -> quiche::h3::Result<()> {
+        // Process HTTP events.
+        loop {
+            match self.h3_conn.poll(conn) {
+                Ok((stream_id, quiche::h3::Event::Headers { list, .. })) => {
+                    info!(
+                        "{} got request {:?} on stream id {}",
+                        conn.trace_id(),
+                        &list,
+                        stream_id
+                    );
+
+                    // We decide the response based on headers alone, so
+                    // stop reading the request stream so that any body
+                    // is ignored and pointless Data events are not
+                    // generated.
+                    conn.stream_shutdown(stream_id, quiche::Shutdown::Read, 0)
+                        .unwrap();
+
+                    let (headers, body) =
+                        Http3Conn::build_h3_response(root, &list);
+
+                    if let Err(e) = self
+                        .h3_conn
+                        .send_response(conn, stream_id, &headers, false)
+                    {
+                        error!("{} stream send failed {:?}", conn.trace_id(), e);
+                    }
+
+                    let written = match self
+                        .h3_conn
+                        .send_body(conn, stream_id, &body, true)
+                    {
+                        Ok(v) => v,
+
+                        Err(quiche::h3::Error::Done) => 0,
+
+                        Err(e) => {
+                            error!(
+                                "{} stream send failed {:?}",
+                                conn.trace_id(),
+                                e
+                            );
+                            break;
+                        },
+                    };
+
+                    if written < body.len() {
+                        let response = PartialResponse { body, written };
+                        partial_responses.insert(stream_id, response);
+                    }
+                },
+
+                Ok((stream_id, quiche::h3::Event::Data)) => {
+                    info!(
+                        "{} got data on stream id {}",
+                        conn.trace_id(),
+                        stream_id
+                    );
+                },
+
+                Ok((_stream_id, quiche::h3::Event::Finished)) => (),
+
+                Err(quiche::h3::Error::Done) => {
+                    break;
+                },
+
+                Err(e) => {
+                    error!("{} HTTP/3 error {:?}", conn.trace_id(), e);
+
+                    return Err(e);
+                },
+            }
+        }
+
+        Ok(())
+    }
+
+    fn handle_writable(
+        &mut self, conn: &mut std::pin::Pin<Box<quiche::Connection>>,
+        partial_responses: &mut HashMap<u64, PartialResponse>, stream_id: u64,
+    ) {
+        debug!("{} stream {} is writable", conn.trace_id(), stream_id);
+
+        if !partial_responses.contains_key(&stream_id) {
+            return;
+        }
+
+        let resp = partial_responses.get_mut(&stream_id).unwrap();
+        let body = &resp.body[resp.written..];
+
+        let written = match self.h3_conn.send_body(conn, stream_id, body, true) {
+            Ok(v) => v,
+
+            Err(quiche::h3::Error::Done) => 0,
+
+            Err(e) => {
+                error!("{} stream send failed {:?}", conn.trace_id(), e);
+                return;
+            },
+        };
+
+        resp.written += written;
+
+        if resp.written == resp.body.len() {
+            partial_responses.remove(&stream_id);
+        }
+    }
+}