You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
658 lines
20 KiB
658 lines
20 KiB
// 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 std::net;
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use ring::rand::*;
|
|
|
|
use quiche::h3::NameValue;
|
|
|
|
const MAX_DATAGRAM_SIZE: usize = 1350;
|
|
|
|
struct PartialResponse {
|
|
headers: Option<Vec<quiche::h3::Header>>,
|
|
|
|
body: Vec<u8>,
|
|
|
|
written: usize,
|
|
}
|
|
|
|
struct Client {
|
|
conn: std::pin::Pin<Box<quiche::Connection>>,
|
|
|
|
http3_conn: Option<quiche::h3::Connection>,
|
|
|
|
partial_responses: HashMap<u64, PartialResponse>,
|
|
}
|
|
|
|
type ClientMap = HashMap<Vec<u8>, (net::SocketAddr, Client)>;
|
|
|
|
fn main() {
|
|
let mut buf = [0; 65535];
|
|
let mut out = [0; MAX_DATAGRAM_SIZE];
|
|
|
|
let mut args = std::env::args();
|
|
|
|
let cmd = &args.next().unwrap();
|
|
|
|
if args.len() != 0 {
|
|
println!("Usage: {}", cmd);
|
|
println!("\nSee tools/apps/ for more complete implementations.");
|
|
return;
|
|
}
|
|
|
|
// 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("127.0.0.1:4433").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("examples/cert.crt")
|
|
.unwrap();
|
|
config
|
|
.load_priv_key_from_pem_file("examples/cert.key")
|
|
.unwrap();
|
|
|
|
config
|
|
.set_application_protos(quiche::h3::APPLICATION_PROTOCOL)
|
|
.unwrap();
|
|
|
|
config.set_max_idle_timeout(5000);
|
|
config.set_max_udp_payload_size(MAX_DATAGRAM_SIZE as u64);
|
|
config.set_initial_max_data(10_000_000);
|
|
config.set_initial_max_stream_data_bidi_local(1_000_000);
|
|
config.set_initial_max_stream_data_bidi_remote(1_000_000);
|
|
config.set_initial_max_stream_data_uni(1_000_000);
|
|
config.set_initial_max_streams_bidi(100);
|
|
config.set_initial_max_streams_uni(100);
|
|
config.set_disable_active_migration(true);
|
|
config.enable_early_data();
|
|
|
|
let h3_config = quiche::h3::Config::new().unwrap();
|
|
|
|
let rng = SystemRandom::new();
|
|
let conn_id_seed =
|
|
ring::hmac::Key::generate(ring::hmac::HMAC_SHA256, &rng).unwrap();
|
|
|
|
let mut clients = ClientMap::new();
|
|
|
|
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];
|
|
|
|
// 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 'read;
|
|
},
|
|
};
|
|
|
|
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 'read;
|
|
}
|
|
|
|
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 'read;
|
|
}
|
|
|
|
let mut scid = [0; quiche::MAX_CONN_ID_LEN];
|
|
scid.copy_from_slice(&conn_id);
|
|
|
|
// 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,
|
|
hdr.version,
|
|
&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 'read;
|
|
}
|
|
|
|
let 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 'read;
|
|
}
|
|
|
|
if scid.len() != hdr.dcid.len() {
|
|
error!("Invalid destination connection ID");
|
|
continue 'read;
|
|
}
|
|
|
|
// 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,
|
|
http3_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(e) => {
|
|
error!("{} recv failed: {:?}", client.conn.trace_id(), e);
|
|
continue 'read;
|
|
},
|
|
};
|
|
|
|
debug!("{} processed {} bytes", client.conn.trace_id(), read);
|
|
|
|
// Create a new HTTP/3 connection as soon as the QUIC connection
|
|
// is established.
|
|
if (client.conn.is_in_early_data() || client.conn.is_established()) &&
|
|
client.http3_conn.is_none()
|
|
{
|
|
debug!(
|
|
"{} QUIC handshake completed, now trying HTTP/3",
|
|
client.conn.trace_id()
|
|
);
|
|
|
|
let h3_conn = match quiche::h3::Connection::with_transport(
|
|
&mut client.conn,
|
|
&h3_config,
|
|
) {
|
|
Ok(v) => v,
|
|
|
|
Err(e) => {
|
|
error!("failed to create HTTP/3 connection: {}", e);
|
|
continue 'read;
|
|
},
|
|
};
|
|
|
|
// TODO: sanity check h3 connection before adding to map
|
|
client.http3_conn = Some(h3_conn);
|
|
}
|
|
|
|
if client.http3_conn.is_some() {
|
|
// Handle writable streams.
|
|
for stream_id in client.conn.writable() {
|
|
handle_writable(client, stream_id);
|
|
}
|
|
|
|
// Process HTTP/3 events.
|
|
loop {
|
|
let http3_conn = client.http3_conn.as_mut().unwrap();
|
|
|
|
match http3_conn.poll(&mut client.conn) {
|
|
Ok((
|
|
stream_id,
|
|
quiche::h3::Event::Headers { list, .. },
|
|
)) => {
|
|
handle_request(
|
|
client,
|
|
stream_id,
|
|
&list,
|
|
"examples/root",
|
|
);
|
|
},
|
|
|
|
Ok((stream_id, quiche::h3::Event::Data)) => {
|
|
info!(
|
|
"{} got data on stream id {}",
|
|
client.conn.trace_id(),
|
|
stream_id
|
|
);
|
|
},
|
|
|
|
Ok((_stream_id, quiche::h3::Event::Finished)) => (),
|
|
|
|
Ok((_flow_id, quiche::h3::Event::Datagram)) => (),
|
|
|
|
Ok((_goaway_id, quiche::h3::Event::GoAway)) => (),
|
|
|
|
Err(quiche::h3::Error::Done) => {
|
|
break;
|
|
},
|
|
|
|
Err(e) => {
|
|
error!(
|
|
"{} HTTP/3 error {:?}",
|
|
client.conn.trace_id(),
|
|
e
|
|
);
|
|
|
|
break;
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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[..])
|
|
}
|
|
|
|
/// Handles incoming HTTP/3 requests.
|
|
fn handle_request(
|
|
client: &mut Client, stream_id: u64, headers: &[quiche::h3::Header],
|
|
root: &str,
|
|
) {
|
|
let conn = &mut client.conn;
|
|
let http3_conn = &mut client.http3_conn.as_mut().unwrap();
|
|
|
|
info!(
|
|
"{} got request {:?} on stream id {}",
|
|
conn.trace_id(),
|
|
headers,
|
|
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) = build_response(root, headers);
|
|
|
|
match http3_conn.send_response(conn, stream_id, &headers, false) {
|
|
Ok(v) => v,
|
|
|
|
Err(quiche::h3::Error::StreamBlocked) => {
|
|
let response = PartialResponse {
|
|
headers: Some(headers),
|
|
body,
|
|
written: 0,
|
|
};
|
|
|
|
client.partial_responses.insert(stream_id, response);
|
|
return;
|
|
},
|
|
|
|
Err(e) => {
|
|
error!("{} stream send failed {:?}", conn.trace_id(), e);
|
|
return;
|
|
},
|
|
}
|
|
|
|
let written = match http3_conn.send_body(conn, stream_id, &body, true) {
|
|
Ok(v) => v,
|
|
|
|
Err(e) => {
|
|
error!("{} stream send failed {:?}", conn.trace_id(), e);
|
|
return;
|
|
},
|
|
};
|
|
|
|
if written < body.len() {
|
|
let response = PartialResponse {
|
|
headers: None,
|
|
body,
|
|
written,
|
|
};
|
|
|
|
client.partial_responses.insert(stream_id, response);
|
|
}
|
|
}
|
|
|
|
/// Builds an HTTP/3 response given a request.
|
|
fn build_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)
|
|
}
|
|
|
|
/// Handles newly writable streams.
|
|
fn handle_writable(client: &mut Client, stream_id: u64) {
|
|
let conn = &mut client.conn;
|
|
let http3_conn = &mut client.http3_conn.as_mut().unwrap();
|
|
|
|
debug!("{} stream {} is writable", conn.trace_id(), stream_id);
|
|
|
|
if !client.partial_responses.contains_key(&stream_id) {
|
|
return;
|
|
}
|
|
|
|
let resp = client.partial_responses.get_mut(&stream_id).unwrap();
|
|
|
|
if let Some(ref headers) = resp.headers {
|
|
match http3_conn.send_response(conn, stream_id, &headers, false) {
|
|
Ok(_) => (),
|
|
|
|
Err(quiche::h3::Error::StreamBlocked) => {
|
|
return;
|
|
},
|
|
|
|
Err(e) => {
|
|
error!("{} stream send failed {:?}", conn.trace_id(), e);
|
|
return;
|
|
},
|
|
}
|
|
}
|
|
|
|
resp.headers = None;
|
|
|
|
let body = &resp.body[resp.written..];
|
|
|
|
let written = match http3_conn.send_body(conn, stream_id, body, true) {
|
|
Ok(v) => v,
|
|
|
|
Err(e) => {
|
|
error!("{} stream send failed {:?}", conn.trace_id(), e);
|
|
return;
|
|
},
|
|
};
|
|
|
|
resp.written += written;
|
|
|
|
if resp.written == resp.body.len() {
|
|
client.partial_responses.remove(&stream_id);
|
|
}
|
|
}
|
|
|
|
fn hex_dump(buf: &[u8]) -> String {
|
|
let vec: Vec<String> = buf.iter().map(|b| format!("{:02x}", b)).collect();
|
|
|
|
vec.join("")
|
|
}
|