use std::io;
use std::io::Write;
use std::net::{ToSocketAddrs, UdpSocket};
use std::time::{Duration, Instant};
use purecrypto::quic::transport_params::TransportParameters;
use purecrypto::quic::{QuicConfig, QuicConnection, StreamId};
use crate::error::{Error, Result};
use crate::{Request, Response};
pub(crate) mod varint {
use crate::error::{Error, Result};
pub const MAX: u64 = (1u64 << 62) - 1;
#[allow(dead_code)]
pub const fn encoded_len(value: u64) -> usize {
if value < 1 << 6 {
1
} else if value < 1 << 14 {
2
} else if value < 1 << 30 {
4
} else {
8
}
}
pub fn encode(value: u64, out: &mut Vec<u8>) {
debug_assert!(value <= MAX, "QUIC varint out of range: {value:#x}");
if value < 1 << 6 {
out.push(value as u8);
} else if value < 1 << 14 {
let bytes = (value as u16).to_be_bytes();
out.push(bytes[0] | 0x40);
out.push(bytes[1]);
} else if value < 1 << 30 {
let bytes = (value as u32).to_be_bytes();
out.push(bytes[0] | 0x80);
out.push(bytes[1]);
out.push(bytes[2]);
out.push(bytes[3]);
} else {
let bytes = value.to_be_bytes();
out.push(bytes[0] | 0xC0);
out.extend_from_slice(&bytes[1..]);
}
}
pub fn decode(buf: &[u8]) -> Result<(u64, usize)> {
if buf.is_empty() {
return Err(Error::BadResponse("varint: empty input".into()));
}
let tag = buf[0] >> 6;
let n: usize = 1 << tag; if buf.len() < n {
return Err(Error::BadResponse(format!(
"varint: need {n} bytes, have {}",
buf.len()
)));
}
let mut v: u64 = (buf[0] & 0x3F) as u64;
for &b in &buf[1..n] {
v = (v << 8) | (b as u64);
}
Ok((v, n))
}
}
#[allow(dead_code)]
pub(crate) mod frame_type {
pub const DATA: u64 = 0x00;
pub const HEADERS: u64 = 0x01;
pub const CANCEL_PUSH: u64 = 0x03;
pub const SETTINGS: u64 = 0x04;
pub const PUSH_PROMISE: u64 = 0x05;
pub const GOAWAY: u64 = 0x07;
pub const MAX_PUSH_ID: u64 = 0x0D;
}
#[allow(dead_code)]
pub(crate) mod uni_stream_type {
pub const CONTROL: u64 = 0x00;
pub const PUSH: u64 = 0x01;
pub const QPACK_ENCODER: u64 = 0x02;
pub const QPACK_DECODER: u64 = 0x03;
}
#[allow(dead_code)]
pub(crate) mod settings_id {
pub const QPACK_MAX_TABLE_CAPACITY: u64 = 0x01;
pub const MAX_FIELD_SECTION_SIZE: u64 = 0x06;
pub const QPACK_BLOCKED_STREAMS: u64 = 0x07;
}
pub(crate) const QPACK_MAX_TABLE_CAPACITY: u64 = 4096;
pub(crate) const QPACK_BLOCKED_STREAMS: u64 = 0;
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Frame {
pub ty: u64,
pub len: u64,
}
impl Frame {
pub fn encode_header(ty: u64, len: u64, out: &mut Vec<u8>) {
varint::encode(ty, out);
varint::encode(len, out);
}
pub fn decode_header(buf: &[u8]) -> Result<(Frame, usize)> {
let (ty, n1) = varint::decode(buf)?;
let (len, n2) = varint::decode(&buf[n1..])?;
Ok((Frame { ty, len }, n1 + n2))
}
}
pub(crate) mod qpack {
use crate::error::{Error, Result};
const MAX_DECODED_HEADER_LIST: usize = 256 * 1024;
pub(crate) const MAX_DYNAMIC_CAPACITY: usize = 4096;
const ENTRY_OVERHEAD: usize = 32;
#[derive(Debug, Default)]
pub(crate) struct DynamicTable {
entries: std::collections::VecDeque<(String, String)>,
size: usize,
capacity: usize,
insert_count: u64,
dropped: u64,
}
impl DynamicTable {
pub(crate) fn new() -> Self {
DynamicTable::default()
}
pub(crate) fn insert_count(&self) -> u64 {
self.insert_count
}
pub(crate) fn set_capacity(&mut self, cap: u64) -> Result<()> {
if cap > MAX_DYNAMIC_CAPACITY as u64 {
return Err(Error::BadResponse(format!(
"qpack: encoder set capacity {cap} above advertised max {MAX_DYNAMIC_CAPACITY}"
)));
}
self.capacity = cap as usize;
self.evict_to_fit(0);
Ok(())
}
fn evict_to_fit(&mut self, incoming: usize) {
while self.size + incoming > self.capacity {
match self.entries.pop_front() {
Some((n, v)) => {
self.size -= n.len() + v.len() + ENTRY_OVERHEAD;
self.dropped += 1;
}
None => break,
}
}
}
pub(crate) fn insert(&mut self, name: String, value: String) -> Result<()> {
let entry_size = name.len() + value.len() + ENTRY_OVERHEAD;
if entry_size > self.capacity {
return Err(Error::BadResponse(format!(
"qpack: dynamic entry size {entry_size} exceeds capacity {}",
self.capacity
)));
}
self.evict_to_fit(entry_size);
self.size += entry_size;
self.entries.push_back((name, value));
self.insert_count += 1;
Ok(())
}
pub(crate) fn get_absolute(&self, abs: u64) -> Result<&(String, String)> {
if abs < self.dropped || abs >= self.insert_count {
return Err(Error::BadResponse(format!(
"qpack: dynamic absolute index {abs} not live (dropped {}, count {})",
self.dropped, self.insert_count
)));
}
let pos = (abs - self.dropped) as usize;
self.entries
.get(pos)
.ok_or_else(|| Error::BadResponse("qpack: dynamic index out of range".into()))
}
}
pub(crate) fn apply_encoder_instructions(
table: &mut DynamicTable,
buf: &[u8],
) -> Result<usize> {
let mut p = 0;
while p < buf.len() {
match decode_encoder_instruction(table, &buf[p..])? {
Some(used) => p += used,
None => break, }
}
Ok(p)
}
fn decode_encoder_instruction(table: &mut DynamicTable, buf: &[u8]) -> Result<Option<usize>> {
if buf.is_empty() {
return Ok(None);
}
let b0 = buf[0];
if b0 & 0b1000_0000 != 0 {
let t_static = b0 & 0b0100_0000 != 0;
let (idx, used) = decode_int(b0, 6, &buf[1..])?;
let mut p = 1 + used;
let name = if t_static {
let (n, _) = *STATIC_TABLE.get(idx as usize).ok_or_else(|| {
Error::BadResponse(format!(
"qpack: encoder static name index {idx} out of range"
))
})?;
n.to_string()
} else {
let abs = relative_to_absolute_insert(table.insert_count(), idx)?;
table.get_absolute(abs)?.0.clone()
};
let (value, vlen) = match try_decode_literal_string_7bit(&buf[p..])? {
Some(x) => x,
None => return Ok(None),
};
p += vlen;
table.insert(name, value)?;
Ok(Some(p))
} else if b0 & 0b0100_0000 != 0 {
let huffman = b0 & 0b0010_0000 != 0;
let (nlen, used) = decode_int(b0, 5, &buf[1..])?;
let mut p = 1 + used;
let nlen = nlen as usize;
if p + nlen > buf.len() {
return Ok(None);
}
let name = decode_string_bytes(&buf[p..p + nlen], huffman, "encoder literal name")?;
p += nlen;
let (value, vlen) = match try_decode_literal_string_7bit(&buf[p..])? {
Some(x) => x,
None => return Ok(None),
};
p += vlen;
table.insert(name, value)?;
Ok(Some(p))
} else if b0 & 0b0010_0000 != 0 {
let (cap, used) = decode_int(b0, 5, &buf[1..])?;
table.set_capacity(cap)?;
Ok(Some(1 + used))
} else {
let (idx, used) = decode_int(b0, 5, &buf[1..])?;
let abs = relative_to_absolute_insert(table.insert_count(), idx)?;
let (n, v) = table.get_absolute(abs)?.clone();
table.insert(n, v)?;
Ok(Some(1 + used))
}
}
fn relative_to_absolute_insert(insert_count: u64, rel: u64) -> Result<u64> {
insert_count
.checked_sub(1)
.and_then(|last| last.checked_sub(rel))
.ok_or_else(|| {
Error::BadResponse(format!(
"qpack: relative index {rel} out of range (insert count {insert_count})"
))
})
}
fn decode_string_bytes(bytes: &[u8], huffman: bool, what: &str) -> Result<String> {
if huffman {
let decoded = huffman_decode(bytes)?;
String::from_utf8(decoded)
.map_err(|_| Error::BadResponse(format!("qpack: {what} not utf-8")))
} else {
std::str::from_utf8(bytes)
.map_err(|_| Error::BadResponse(format!("qpack: {what} not utf-8")))
.map(|s| s.to_string())
}
}
fn try_decode_literal_string_7bit(buf: &[u8]) -> Result<Option<(String, usize)>> {
if buf.is_empty() {
return Ok(None);
}
let b = buf[0];
let huffman = b & 0b1000_0000 != 0;
let (slen, used) = match decode_int_partial(b, 7, &buf[1..])? {
Some(x) => x,
None => return Ok(None),
};
let start = 1 + used;
let end = start + slen as usize;
if end > buf.len() {
return Ok(None);
}
let s = decode_string_bytes(&buf[start..end], huffman, "literal string")?;
Ok(Some((s, end)))
}
fn decode_int_partial(first: u8, prefix_bits: u8, rest: &[u8]) -> Result<Option<(u64, usize)>> {
debug_assert!((1..=8).contains(&prefix_bits));
let mask = ((1u16 << prefix_bits) - 1) as u8;
let prefix = (first & mask) as u64;
let max_prefix = mask as u64;
if prefix < max_prefix {
return Ok(Some((prefix, 0)));
}
let mut value = max_prefix;
let mut shift = 0u32;
let mut used = 0usize;
for &b in rest {
used += 1;
value = value
.checked_add(((b & 0x7F) as u64) << shift)
.ok_or_else(|| Error::BadResponse("qpack int overflow".into()))?;
if b & 0x80 == 0 {
return Ok(Some((value, used)));
}
shift += 7;
if shift > 63 {
return Err(Error::BadResponse("qpack int too long".into()));
}
}
Ok(None) }
pub static STATIC_TABLE: &[(&str, &str)] = &[
(":authority", ""), (":path", "/"), ("age", "0"), ("content-disposition", ""), ("content-length", "0"), ("cookie", ""), ("date", ""), ("etag", ""), ("if-modified-since", ""), ("if-none-match", ""), ("last-modified", ""), ("link", ""), ("location", ""), ("referer", ""), ("set-cookie", ""), (":method", "CONNECT"), (":method", "DELETE"), (":method", "GET"), (":method", "HEAD"), (":method", "OPTIONS"), (":method", "POST"), (":method", "PUT"), (":scheme", "http"), (":scheme", "https"), (":status", "103"), (":status", "200"), (":status", "304"), (":status", "404"), (":status", "503"), ("accept", "*/*"), ("accept", "application/dns-message"), ("accept-encoding", "gzip, deflate, br"), ("accept-ranges", "bytes"), ("access-control-allow-headers", "cache-control"), ("access-control-allow-headers", "content-type"), ("access-control-allow-origin", "*"), ("cache-control", "max-age=0"), ("cache-control", "max-age=2592000"), ("cache-control", "max-age=604800"), ("cache-control", "no-cache"), ("cache-control", "no-store"), ("cache-control", "public, max-age=31536000"), ("content-encoding", "br"), ("content-encoding", "gzip"), ("content-type", "application/dns-message"), ("content-type", "application/javascript"), ("content-type", "application/json"), ("content-type", "application/x-www-form-urlencoded"), ("content-type", "image/gif"), ("content-type", "image/jpeg"), ("content-type", "image/png"), ("content-type", "text/css"), ("content-type", "text/html; charset=utf-8"), ("content-type", "text/plain"), ("content-type", "text/plain;charset=utf-8"), ("range", "bytes=0-"), ("strict-transport-security", "max-age=31536000"), (
"strict-transport-security",
"max-age=31536000; includesubdomains",
), (
"strict-transport-security",
"max-age=31536000; includesubdomains; preload",
), ("vary", "accept-encoding"), ("vary", "origin"), ("x-content-type-options", "nosniff"), ("x-xss-protection", "1; mode=block"), (":status", "100"), (":status", "204"), (":status", "206"), (":status", "302"), (":status", "400"), (":status", "403"), (":status", "421"), (":status", "425"), (":status", "500"), ("accept-language", ""), ("access-control-allow-credentials", "FALSE"), ("access-control-allow-credentials", "TRUE"), ("access-control-allow-headers", "*"), ("access-control-allow-methods", "get"), ("access-control-allow-methods", "get, post, options"), ("access-control-allow-methods", "options"), ("access-control-expose-headers", "content-length"), ("access-control-request-headers", "content-type"), ("access-control-request-method", "get"), ("access-control-request-method", "post"), ("alt-svc", "clear"), ("authorization", ""), (
"content-security-policy",
"script-src 'none'; object-src 'none'; base-uri 'none'",
), ("early-data", "1"), ("expect-ct", ""), ("forwarded", ""), ("if-range", ""), ("origin", ""), ("purpose", "prefetch"), ("server", ""), ("timing-allow-origin", "*"), ("upgrade-insecure-requests", "1"), ("user-agent", ""), ("x-forwarded-for", ""), ("x-frame-options", "deny"), ("x-frame-options", "sameorigin"), ];
pub fn find_indexed(name: &str, value: &str) -> Option<usize> {
STATIC_TABLE
.iter()
.position(|(n, v)| *n == name && *v == value)
}
pub fn find_name(name: &str) -> Option<usize> {
STATIC_TABLE.iter().position(|(n, _)| *n == name)
}
pub fn encode_int(value: u64, prefix_bits: u8, prefix_high_bits: u8, out: &mut Vec<u8>) {
debug_assert!((1..=8).contains(&prefix_bits));
let max_prefix = (1u64 << prefix_bits) - 1;
if value < max_prefix {
out.push(prefix_high_bits | (value as u8));
} else {
out.push(prefix_high_bits | (max_prefix as u8));
let mut rem = value - max_prefix;
while rem >= 128 {
out.push(((rem & 0x7F) as u8) | 0x80);
rem >>= 7;
}
out.push(rem as u8);
}
}
pub fn decode_int(first: u8, prefix_bits: u8, rest: &[u8]) -> Result<(u64, usize)> {
debug_assert!((1..=8).contains(&prefix_bits));
let mask = ((1u16 << prefix_bits) - 1) as u8;
let prefix = (first & mask) as u64;
let max_prefix = mask as u64;
if prefix < max_prefix {
return Ok((prefix, 0));
}
let mut value = max_prefix;
let mut shift = 0u32;
let mut used = 0usize;
for &b in rest {
used += 1;
value = value
.checked_add(((b & 0x7F) as u64) << shift)
.ok_or_else(|| Error::BadResponse("qpack int overflow".into()))?;
if b & 0x80 == 0 {
return Ok((value, used));
}
shift += 7;
if shift > 63 {
return Err(Error::BadResponse("qpack int too long".into()));
}
}
Err(Error::BadResponse("qpack int truncated".into()))
}
pub fn encode_string_7bit(s: &str, out: &mut Vec<u8>) {
encode_int(s.len() as u64, 7, 0x00, out);
out.extend_from_slice(s.as_bytes());
}
pub fn encode_name_3bit(s: &str, out: &mut Vec<u8>) {
encode_int(s.len() as u64, 3, 0b0010_0000, out);
out.extend_from_slice(s.as_bytes());
}
pub fn encode_field(name: &str, value: &str, out: &mut Vec<u8>) {
if let Some(idx) = find_indexed(name, value) {
encode_int(idx as u64, 6, 0b1100_0000, out);
return;
}
if let Some(idx) = find_name(name) {
encode_int(idx as u64, 4, 0b0101_0000, out);
encode_string_7bit(value, out);
return;
}
encode_name_3bit(name, out);
encode_string_7bit(value, out);
}
pub fn encode_field_section(fields: &[(String, String)]) -> Vec<u8> {
let mut buf = Vec::with_capacity(64);
encode_int(0, 8, 0x00, &mut buf);
encode_int(0, 7, 0x00, &mut buf);
for (n, v) in fields {
encode_field(n, v, &mut buf);
}
buf
}
pub type Fields = Vec<(String, String)>;
#[cfg_attr(not(test), allow(dead_code))]
pub fn decode_field_section(buf: &[u8]) -> Result<Fields> {
let table = DynamicTable::new();
decode_field_section_with(buf, &table)
}
fn max_entries() -> u64 {
(MAX_DYNAMIC_CAPACITY / ENTRY_OVERHEAD) as u64
}
pub(crate) fn decode_required_insert_count(encoded: u64, total_inserts: u64) -> Result<u64> {
if encoded == 0 {
return Ok(0);
}
let max_entries = max_entries();
let full_range = 2 * max_entries;
if encoded > full_range {
return Err(Error::BadResponse(format!(
"qpack: encoded Required Insert Count {encoded} exceeds 2*MaxEntries {full_range}"
)));
}
let max_value = total_inserts + max_entries;
let max_wrapped = (max_value / full_range) * full_range;
let mut ric = max_wrapped + encoded - 1;
if ric > max_value {
if ric <= full_range {
return Err(Error::BadResponse(
"qpack: Required Insert Count wrap underflow".into(),
));
}
ric -= full_range;
}
if ric == 0 {
return Err(Error::BadResponse(
"qpack: reconstructed Required Insert Count is zero".into(),
));
}
Ok(ric)
}
pub(crate) fn block_references_dynamic_table(buf: &[u8]) -> bool {
if buf.is_empty() {
return false;
}
match decode_int(buf[0], 8, &buf[1..]) {
Ok((enc_ric, _)) => enc_ric != 0,
Err(_) => false,
}
}
fn decode_base(first: u8, rest: &[u8], ric: u64) -> Result<(u64, usize)> {
let sign = first & 0b1000_0000 != 0;
let (delta_base, used) = decode_int(first, 7, rest)?;
let base = if sign {
ric.checked_sub(delta_base)
.and_then(|x| x.checked_sub(1))
.ok_or_else(|| Error::BadResponse("qpack: negative Base".into()))?
} else {
ric.checked_add(delta_base)
.ok_or_else(|| Error::BadResponse("qpack: Base overflow".into()))?
};
Ok((base, used))
}
pub(crate) fn decode_field_section_with(buf: &[u8], table: &DynamicTable) -> Result<Fields> {
if buf.is_empty() {
return Err(Error::BadResponse("qpack: empty field section".into()));
}
let (enc_ric, n1) = decode_int(buf[0], 8, &buf[1..])?;
let ric = decode_required_insert_count(enc_ric, table.insert_count())?;
if ric > table.insert_count() {
return Err(Error::BadResponse(format!(
"qpack: Required Insert Count {ric} exceeds available inserts {}",
table.insert_count()
)));
}
let mut p = 1 + n1;
if p >= buf.len() {
return Err(Error::BadResponse(
"qpack: truncated field-section prefix".into(),
));
}
let (base, n2) = decode_base(buf[p], &buf[p + 1..], ric)?;
p += 1 + n2;
let mut out: Fields = Vec::new();
let mut list_size: usize = 0;
while p < buf.len() {
let b = buf[p];
let entry: (String, String) = if b & 0b1000_0000 != 0 {
let t_static = b & 0b0100_0000 != 0;
let (idx, used) = decode_int(b, 6, &buf[p + 1..])?;
p += 1 + used;
if t_static {
let (n, v) = *STATIC_TABLE.get(idx as usize).ok_or_else(|| {
Error::BadResponse(format!("qpack: static index out of range: {idx}"))
})?;
(n.to_string(), v.to_string())
} else {
let abs = field_relative_to_absolute(base, idx)?;
table.get_absolute(abs)?.clone()
}
} else if b & 0b0100_0000 != 0 {
let t_static = b & 0b0001_0000 != 0;
let (idx, used) = decode_int(b, 4, &buf[p + 1..])?;
p += 1 + used;
let name = if t_static {
let (n, _) = *STATIC_TABLE.get(idx as usize).ok_or_else(|| {
Error::BadResponse(format!("qpack: static name index out of range: {idx}"))
})?;
n.to_string()
} else {
let abs = field_relative_to_absolute(base, idx)?;
table.get_absolute(abs)?.0.clone()
};
let value = decode_literal_string_7bit(&buf[p..])?;
p += value.1;
(name, value.0)
} else if b & 0b0010_0000 != 0 {
let huffman = b & 0b0000_1000 != 0;
let (nlen, used) = decode_int(b, 3, &buf[p + 1..])?;
p += 1 + used;
let nlen = nlen as usize;
if p + nlen > buf.len() {
return Err(Error::BadResponse("qpack: truncated literal name".into()));
}
let name = decode_string_bytes(&buf[p..p + nlen], huffman, "literal name")?;
p += nlen;
let value = decode_literal_string_7bit(&buf[p..])?;
p += value.1;
(name, value.0)
} else if b & 0b0001_0000 != 0 {
let (idx, used) = decode_int(b, 4, &buf[p + 1..])?;
p += 1 + used;
let abs = post_base_to_absolute(base, idx)?;
table.get_absolute(abs)?.clone()
} else {
let (idx, used) = decode_int(b, 3, &buf[p + 1..])?;
p += 1 + used;
let abs = post_base_to_absolute(base, idx)?;
let name = table.get_absolute(abs)?.0.clone();
let value = decode_literal_string_7bit(&buf[p..])?;
p += value.1;
(name, value.0)
};
list_size = list_size
.saturating_add(entry.0.len())
.saturating_add(entry.1.len())
.saturating_add(32);
if list_size > MAX_DECODED_HEADER_LIST {
return Err(Error::BadResponse(
"qpack: decoded header list exceeds limit".into(),
));
}
out.push(entry);
}
Ok(out)
}
fn field_relative_to_absolute(base: u64, rel: u64) -> Result<u64> {
base.checked_sub(1)
.and_then(|x| x.checked_sub(rel))
.ok_or_else(|| {
Error::BadResponse(format!(
"qpack: field relative index {rel} out of range (Base {base})"
))
})
}
fn post_base_to_absolute(base: u64, idx: u64) -> Result<u64> {
base.checked_add(idx)
.ok_or_else(|| Error::BadResponse("qpack: post-base index overflow".into()))
}
pub(crate) fn decode_literal_string_7bit(buf: &[u8]) -> Result<(String, usize)> {
if buf.is_empty() {
return Err(Error::BadResponse("qpack: missing literal string".into()));
}
let b = buf[0];
let huffman = b & 0b1000_0000 != 0;
let (slen, used) = decode_int(b, 7, &buf[1..])?;
let start = 1 + used;
let end = start + slen as usize;
if end > buf.len() {
return Err(Error::BadResponse("qpack: truncated literal value".into()));
}
let raw = &buf[start..end];
let s = if huffman {
let bytes = huffman_decode(raw)?;
String::from_utf8(bytes)
.map_err(|_| Error::BadResponse("qpack: literal value not utf-8".into()))?
} else {
std::str::from_utf8(raw)
.map_err(|_| Error::BadResponse("qpack: literal value not utf-8".into()))?
.to_string()
};
Ok((s, end))
}
const HUFFMAN: [(u32, u8); 257] = [
(0x1ff8, 13),
(0x7fffd8, 23),
(0xfffffe2, 28),
(0xfffffe3, 28),
(0xfffffe4, 28),
(0xfffffe5, 28),
(0xfffffe6, 28),
(0xfffffe7, 28),
(0xfffffe8, 28),
(0xffffea, 24),
(0x3ffffffc, 30),
(0xfffffe9, 28),
(0xfffffea, 28),
(0x3ffffffd, 30),
(0xfffffeb, 28),
(0xfffffec, 28),
(0xfffffed, 28),
(0xfffffee, 28),
(0xfffffef, 28),
(0xffffff0, 28),
(0xffffff1, 28),
(0xffffff2, 28),
(0x3ffffffe, 30),
(0xffffff3, 28),
(0xffffff4, 28),
(0xffffff5, 28),
(0xffffff6, 28),
(0xffffff7, 28),
(0xffffff8, 28),
(0xffffff9, 28),
(0xffffffa, 28),
(0xffffffb, 28),
(0x14, 6),
(0x3f8, 10),
(0x3f9, 10),
(0xffa, 12),
(0x1ff9, 13),
(0x15, 6),
(0xf8, 8),
(0x7fa, 11),
(0x3fa, 10),
(0x3fb, 10),
(0xf9, 8),
(0x7fb, 11),
(0xfa, 8),
(0x16, 6),
(0x17, 6),
(0x18, 6),
(0x0, 5),
(0x1, 5),
(0x2, 5),
(0x19, 6),
(0x1a, 6),
(0x1b, 6),
(0x1c, 6),
(0x1d, 6),
(0x1e, 6),
(0x1f, 6),
(0x5c, 7),
(0xfb, 8),
(0x7ffc, 15),
(0x20, 6),
(0xffb, 12),
(0x3fc, 10),
(0x1ffa, 13),
(0x21, 6),
(0x5d, 7),
(0x5e, 7),
(0x5f, 7),
(0x60, 7),
(0x61, 7),
(0x62, 7),
(0x63, 7),
(0x64, 7),
(0x65, 7),
(0x66, 7),
(0x67, 7),
(0x68, 7),
(0x69, 7),
(0x6a, 7),
(0x6b, 7),
(0x6c, 7),
(0x6d, 7),
(0x6e, 7),
(0x6f, 7),
(0x70, 7),
(0x71, 7),
(0x72, 7),
(0xfc, 8),
(0x73, 7),
(0xfd, 8),
(0x1ffb, 13),
(0x7fff0, 19),
(0x1ffc, 13),
(0x3ffc, 14),
(0x22, 6),
(0x7ffd, 15),
(0x3, 5),
(0x23, 6),
(0x4, 5),
(0x24, 6),
(0x5, 5),
(0x25, 6),
(0x26, 6),
(0x27, 6),
(0x6, 5),
(0x74, 7),
(0x75, 7),
(0x28, 6),
(0x29, 6),
(0x2a, 6),
(0x7, 5),
(0x2b, 6),
(0x76, 7),
(0x2c, 6),
(0x8, 5),
(0x9, 5),
(0x2d, 6),
(0x77, 7),
(0x78, 7),
(0x79, 7),
(0x7a, 7),
(0x7b, 7),
(0x7ffe, 15),
(0x7fc, 11),
(0x3ffd, 14),
(0x1ffd, 13),
(0xffffffc, 28),
(0xfffe6, 20),
(0x3fffd2, 22),
(0xfffe7, 20),
(0xfffe8, 20),
(0x3fffd3, 22),
(0x3fffd4, 22),
(0x3fffd5, 22),
(0x7fffd9, 23),
(0x3fffd6, 22),
(0x7fffda, 23),
(0x7fffdb, 23),
(0x7fffdc, 23),
(0x7fffdd, 23),
(0x7fffde, 23),
(0xffffeb, 24),
(0x7fffdf, 23),
(0xffffec, 24),
(0xffffed, 24),
(0x3fffd7, 22),
(0x7fffe0, 23),
(0xffffee, 24),
(0x7fffe1, 23),
(0x7fffe2, 23),
(0x7fffe3, 23),
(0x7fffe4, 23),
(0x1fffdc, 21),
(0x3fffd8, 22),
(0x7fffe5, 23),
(0x3fffd9, 22),
(0x7fffe6, 23),
(0x7fffe7, 23),
(0xffffef, 24),
(0x3fffda, 22),
(0x1fffdd, 21),
(0xfffe9, 20),
(0x3fffdb, 22),
(0x3fffdc, 22),
(0x7fffe8, 23),
(0x7fffe9, 23),
(0x1fffde, 21),
(0x7fffea, 23),
(0x3fffdd, 22),
(0x3fffde, 22),
(0xfffff0, 24),
(0x1fffdf, 21),
(0x3fffdf, 22),
(0x7fffeb, 23),
(0x7fffec, 23),
(0x1fffe0, 21),
(0x1fffe1, 21),
(0x3fffe0, 22),
(0x1fffe2, 21),
(0x7fffed, 23),
(0x3fffe1, 22),
(0x7fffee, 23),
(0x7fffef, 23),
(0xfffea, 20),
(0x3fffe2, 22),
(0x3fffe3, 22),
(0x3fffe4, 22),
(0x7ffff0, 23),
(0x3fffe5, 22),
(0x3fffe6, 22),
(0x7ffff1, 23),
(0x3ffffe0, 26),
(0x3ffffe1, 26),
(0xfffeb, 20),
(0x7fff1, 19),
(0x3fffe7, 22),
(0x7ffff2, 23),
(0x3fffe8, 22),
(0x1ffffec, 25),
(0x3ffffe2, 26),
(0x3ffffe3, 26),
(0x3ffffe4, 26),
(0x7ffffde, 27),
(0x7ffffdf, 27),
(0x3ffffe5, 26),
(0xfffff1, 24),
(0x1ffffed, 25),
(0x7fff2, 19),
(0x1fffe3, 21),
(0x3ffffe6, 26),
(0x7ffffe0, 27),
(0x7ffffe1, 27),
(0x3ffffe7, 26),
(0x7ffffe2, 27),
(0xfffff2, 24),
(0x1fffe4, 21),
(0x1fffe5, 21),
(0x3ffffe8, 26),
(0x3ffffe9, 26),
(0xffffffd, 28),
(0x7ffffe3, 27),
(0x7ffffe4, 27),
(0x7ffffe5, 27),
(0xfffec, 20),
(0xfffff3, 24),
(0xfffed, 20),
(0x1fffe6, 21),
(0x3fffe9, 22),
(0x1fffe7, 21),
(0x1fffe8, 21),
(0x7ffff3, 23),
(0x3fffea, 22),
(0x3fffeb, 22),
(0x1ffffee, 25),
(0x1ffffef, 25),
(0xfffff4, 24),
(0xfffff5, 24),
(0x3ffffea, 26),
(0x7ffff4, 23),
(0x3ffffeb, 26),
(0x7ffffe6, 27),
(0x3ffffec, 26),
(0x3ffffed, 26),
(0x7ffffe7, 27),
(0x7ffffe8, 27),
(0x7ffffe9, 27),
(0x7ffffea, 27),
(0x7ffffeb, 27),
(0xffffffe, 28),
(0x7ffffec, 27),
(0x7ffffed, 27),
(0x7ffffee, 27),
(0x7ffffef, 27),
(0x7fffff0, 27),
(0x3ffffee, 26),
(0x3fffffff, 30), ];
pub(crate) fn huffman_decode(input: &[u8]) -> Result<Vec<u8>> {
let mut out = Vec::with_capacity(input.len().saturating_mul(2));
let mut acc: u64 = 0;
let mut acc_len: u8 = 0;
for &byte in input {
acc = (acc << 8) | (byte as u64);
acc_len += 8;
while acc_len >= 5 {
let mut matched = false;
let max_len = acc_len.min(30);
for try_len in 5..=max_len {
let code = (acc >> (acc_len - try_len)) & ((1u64 << try_len) - 1);
if let Some(sym) = lookup_huffman(code as u32, try_len) {
if sym == 256 {
return Err(Error::BadResponse(
"qpack: EOS symbol in Huffman literal".into(),
));
}
out.push(sym as u8);
acc_len -= try_len;
matched = true;
break;
}
}
if !matched {
break;
}
}
}
if acc_len >= 8 {
return Err(Error::BadResponse(
"qpack: trailing Huffman bits >= 8".into(),
));
}
if acc_len > 0 {
let pad_mask = (1u64 << acc_len) - 1;
let tail = acc & pad_mask;
if tail != pad_mask {
return Err(Error::BadResponse("qpack: bad Huffman padding".into()));
}
}
Ok(out)
}
fn lookup_huffman(code: u32, len: u8) -> Option<u16> {
for (i, (c, l)) in HUFFMAN.iter().enumerate() {
if *l == len && *c == code {
return Some(i as u16);
}
}
None
}
}
const MAX_RESPONSE_BYTES: usize = 256 * 1024 * 1024;
const MAX_HEADERS_FRAME_LEN: u64 = 256 * 1024;
const MAX_TOTAL_DEADLINE: Duration = Duration::from_secs(300);
const MAX_DATAGRAM: usize = 65_535;
struct Http3State {
dyn_table: qpack::DynamicTable,
decoder_stream: Option<StreamId>,
uni: std::collections::HashMap<u64, UniStreamState>,
}
#[derive(Default)]
struct UniStreamState {
buf: Vec<u8>,
ty: Option<u64>,
}
const MAX_UNI_BUFFER: usize = 64 * 1024;
impl Http3State {
fn new(decoder_stream: Option<StreamId>) -> Self {
Http3State {
dyn_table: qpack::DynamicTable::new(),
decoder_stream,
uni: std::collections::HashMap::new(),
}
}
}
pub fn send(req: Request, trace: &mut dyn Write) -> Result<Response> {
if req.url.scheme != "https" {
return Err(Error::UnsupportedScheme(format!(
"http/3 requires https://, not {}://",
req.url.scheme
)));
}
let mut conn = build_client(&req)?;
let (sock, peer) = open_udp(&req)?;
let _ = writeln!(trace, "* Trying {peer} (UDP)...");
handshake(&mut conn, &sock, peer, req.read_timeout)?;
let _ = writeln!(
trace,
"* Connected to {} ({}) port {} (QUIC)",
req.url.host,
peer.ip(),
peer.port()
);
let _ = writeln!(trace, "* QUIC connected, TLS 1.3 handshake complete");
let _ = writeln!(trace, "* ALPN: server accepted h3");
let _ = writeln!(trace, "* using HTTP/3");
let _ = open_control_stream(&mut conn);
let decoder_stream = open_qpack_streams(&mut conn);
let mut state = Http3State::new(decoder_stream);
let request_stream = conn
.open_bidi()
.map_err(|e| Error::BadResponse(format!("http3: open_bidi failed: {e:?}")))?;
write_request(&mut conn, request_stream, &req, trace)?;
if !req.body.is_empty() {
let _ = writeln!(trace, "* uploading {} body bytes", req.body.len());
}
pump(&mut conn, &sock, peer, req.read_timeout)?;
read_response(
&mut conn,
&sock,
peer,
request_stream,
&req,
&mut state,
trace,
)
}
fn drain_uni_streams(conn: &mut QuicConnection, state: &mut Http3State) -> Result<()> {
let ids: Vec<StreamId> = conn
.readable_streams()
.filter(|s| s.is_uni() && s.is_server_initiated())
.collect();
for sid in ids {
let mut tmp = vec![0u8; 16 * 1024];
while let Ok((n, _fin)) = conn.read(sid, &mut tmp) {
if n == 0 {
break;
}
let entry = state.uni.entry(sid.value()).or_default();
if entry.buf.len() + n > MAX_UNI_BUFFER {
return Err(Error::BadResponse(
"http3: server uni-stream buffer exceeded limit".into(),
));
}
entry.buf.extend_from_slice(&tmp[..n]);
}
process_uni_stream(state, sid.value())?;
}
Ok(())
}
fn process_uni_stream(state: &mut Http3State, sid: u64) -> Result<()> {
let entry = state.uni.entry(sid).or_default();
if entry.ty.is_none() {
match varint::decode(&entry.buf) {
Ok((ty, used)) => {
entry.ty = Some(ty);
entry.buf.drain(..used);
}
Err(_) => return Ok(()), }
}
match entry.ty {
Some(uni_stream_type::QPACK_ENCODER) => {
let consumed = qpack::apply_encoder_instructions(&mut state.dyn_table, &entry.buf)?;
let entry = state.uni.get_mut(&sid).expect("entry present");
entry.buf.drain(..consumed);
}
_ => {
entry.buf.clear();
}
}
Ok(())
}
fn build_client(req: &Request) -> Result<QuicConnection> {
let roots = match &req.ca_bundle {
Some(path) => crate::tls::pc_roots::load_from_file(path)?,
None => crate::tls::pc_roots::load_system_roots()?,
};
let tls = purecrypto::tls::Config::builder()
.tls_only()
.roots(roots)
.server_name(req.url.host.clone())
.verify_certificates(req.verify_tls)
.alpn(vec![b"h3".to_vec()])
.build();
let transport_params = TransportParameters {
max_idle_timeout_ms: Some(30_000),
max_udp_payload_size: Some(1452),
initial_max_data: Some(10 * 1024 * 1024),
initial_max_stream_data_bidi_local: Some(2 * 1024 * 1024),
initial_max_stream_data_bidi_remote: Some(2 * 1024 * 1024),
initial_max_stream_data_uni: Some(2 * 1024 * 1024),
initial_max_streams_bidi: Some(100),
initial_max_streams_uni: Some(100),
active_connection_id_limit: Some(2),
..Default::default()
};
let cfg = QuicConfig {
tls,
transport_params,
require_retry: false,
retry_secret: None,
};
QuicConnection::client(cfg, &req.url.host)
.map_err(|e| Error::BadResponse(format!("http3: build client: {e:?}")))
}
fn open_udp(req: &Request) -> Result<(UdpSocket, std::net::SocketAddr)> {
let host_port = format!("{}:{}", req.url.host, req.url.port);
let peer = host_port
.to_socket_addrs()?
.next()
.ok_or_else(|| Error::InvalidUrl(req.url.host.clone()))?;
let bind = if peer.is_ipv4() {
"0.0.0.0:0"
} else {
"[::]:0"
};
let sock = UdpSocket::bind(bind)?;
sock.connect(peer)?;
sock.set_read_timeout(Some(Duration::from_millis(100)))?;
sock.set_write_timeout(req.read_timeout)?;
Ok((sock, peer))
}
fn pump_once(
conn: &mut QuicConnection,
sock: &UdpSocket,
peer: std::net::SocketAddr,
can_block: bool,
) -> Result<bool> {
let mut sent_anything = false;
loop {
let dg = conn.pop_datagram();
if dg.is_empty() {
break;
}
sock.send(&dg)?;
sent_anything = true;
}
let mut buf = vec![0u8; MAX_DATAGRAM];
let mut got_anything = false;
if can_block {
match sock.recv(&mut buf) {
Ok(n) => {
conn.feed_datagram_from(peer, &buf[..n])
.map_err(|e| Error::BadResponse(format!("http3: feed: {e:?}")))?;
got_anything = true;
}
Err(e)
if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut => {
}
Err(e) => return Err(Error::Io(e)),
}
}
if let Some(_dl) = conn.next_timeout() {
conn.on_timeout(Duration::ZERO);
}
loop {
let dg = conn.pop_datagram();
if dg.is_empty() {
break;
}
sock.send(&dg)?;
sent_anything = true;
}
Ok(sent_anything || got_anything)
}
fn handshake(
conn: &mut QuicConnection,
sock: &UdpSocket,
peer: std::net::SocketAddr,
deadline_hint: Option<Duration>,
) -> Result<()> {
let total_deadline = deadline_hint
.unwrap_or(MAX_TOTAL_DEADLINE)
.min(MAX_TOTAL_DEADLINE);
let start = Instant::now();
while !conn.is_handshake_complete() {
if start.elapsed() > total_deadline {
return Err(Error::Io(io::Error::new(
io::ErrorKind::TimedOut,
"http3: QUIC handshake timed out",
)));
}
pump_once(conn, sock, peer, true)?;
if conn.is_closed() {
return Err(Error::BadResponse(
"http3: connection closed mid-handshake".into(),
));
}
}
Ok(())
}
fn open_control_stream(conn: &mut QuicConnection) -> Result<()> {
let sid = conn
.open_uni()
.map_err(|e| Error::BadResponse(format!("http3: open_uni: {e:?}")))?;
let mut prefix = Vec::with_capacity(16);
varint::encode(uni_stream_type::CONTROL, &mut prefix);
let mut settings = Vec::with_capacity(8);
varint::encode(settings_id::QPACK_MAX_TABLE_CAPACITY, &mut settings);
varint::encode(QPACK_MAX_TABLE_CAPACITY, &mut settings);
varint::encode(settings_id::QPACK_BLOCKED_STREAMS, &mut settings);
varint::encode(QPACK_BLOCKED_STREAMS, &mut settings);
Frame::encode_header(frame_type::SETTINGS, settings.len() as u64, &mut prefix);
prefix.extend_from_slice(&settings);
write_all(conn, sid, &prefix)?;
Ok(())
}
fn open_qpack_streams(conn: &mut QuicConnection) -> Option<StreamId> {
if let Ok(enc) = conn.open_uni() {
let mut buf = Vec::with_capacity(1);
varint::encode(uni_stream_type::QPACK_ENCODER, &mut buf);
let _ = write_all(conn, enc, &buf);
}
let dec = conn.open_uni().ok()?;
let mut buf = Vec::with_capacity(1);
varint::encode(uni_stream_type::QPACK_DECODER, &mut buf);
if write_all(conn, dec, &buf).is_err() {
return None;
}
Some(dec)
}
fn encode_section_ack(stream_id: u64, out: &mut Vec<u8>) {
qpack::encode_int(stream_id, 7, 0b1000_0000, out);
}
fn write_all(conn: &mut QuicConnection, sid: StreamId, mut data: &[u8]) -> Result<()> {
while !data.is_empty() {
let n = conn
.write(sid, data)
.map_err(|e| Error::BadResponse(format!("http3: stream write: {e:?}")))?;
if n == 0 {
return Err(Error::BadResponse(
"http3: stream write blocked (flow control)".into(),
));
}
data = &data[n..];
}
Ok(())
}
fn write_request(
conn: &mut QuicConnection,
sid: StreamId,
req: &Request,
trace: &mut dyn Write,
) -> Result<()> {
let host_port = if req.url.port == 443 {
req.url.host.clone()
} else {
format!("{}:{}", req.url.host, req.url.port)
};
let mut fields: Vec<(String, String)> = Vec::with_capacity(req.headers.len() + 5);
fields.push((":method".into(), req.method.clone()));
fields.push((":scheme".into(), "https".into()));
fields.push((":authority".into(), host_port));
fields.push((":path".into(), req.url.path.clone()));
let mut have_ua = false;
let mut have_accept_enc = false;
for (k, v) in &req.headers {
let kl = k.to_ascii_lowercase();
if kl.starts_with(':')
|| kl == "host"
|| kl == "connection"
|| kl == "transfer-encoding"
|| kl == "upgrade"
|| kl == "keep-alive"
|| kl == "proxy-connection"
{
continue;
}
if kl == "user-agent" {
have_ua = true;
}
if kl == "accept-encoding" {
have_accept_enc = true;
}
fields.push((kl, v.clone()));
}
if !have_ua {
fields.push((
"user-agent".into(),
format!("rsurl/{}", env!("CARGO_PKG_VERSION")),
));
}
if !have_accept_enc {
fields.push(("accept-encoding".into(), "gzip, deflate".into()));
}
if !req.body.is_empty() {
fields.push(("content-length".into(), req.body.len().to_string()));
}
{
let path = fields
.iter()
.find(|(k, _)| k == ":path")
.map(|(_, v)| v.as_str())
.unwrap_or("/");
let _ = writeln!(trace, "> {} {path} HTTP/3", req.method);
if let Some((_, authority)) = fields.iter().find(|(k, _)| k == ":authority") {
let _ = writeln!(trace, "> Host: {authority}");
}
for (k, v) in &fields {
if !k.starts_with(':') {
let _ = writeln!(trace, "> {k}: {v}");
}
}
let _ = writeln!(trace, "> ");
}
let qpack_payload = qpack::encode_field_section(&fields);
let mut out = Vec::with_capacity(qpack_payload.len() + 16);
Frame::encode_header(frame_type::HEADERS, qpack_payload.len() as u64, &mut out);
out.extend_from_slice(&qpack_payload);
if !req.body.is_empty() {
Frame::encode_header(frame_type::DATA, req.body.len() as u64, &mut out);
out.extend_from_slice(&req.body);
}
write_all(conn, sid, &out)?;
conn.finish(sid)
.map_err(|e| Error::BadResponse(format!("http3: stream finish: {e:?}")))?;
Ok(())
}
fn pump(
conn: &mut QuicConnection,
sock: &UdpSocket,
peer: std::net::SocketAddr,
_read_timeout: Option<Duration>,
) -> Result<()> {
for _ in 0..3 {
pump_once(conn, sock, peer, false)?;
}
Ok(())
}
fn read_response(
conn: &mut QuicConnection,
sock: &UdpSocket,
peer: std::net::SocketAddr,
sid: StreamId,
req: &Request,
state: &mut Http3State,
trace: &mut dyn Write,
) -> Result<Response> {
let total_deadline = req
.read_timeout
.unwrap_or(MAX_TOTAL_DEADLINE)
.min(MAX_TOTAL_DEADLINE);
let start = Instant::now();
let mut stream_buf: Vec<u8> = Vec::new();
let mut headers: Option<qpack::Fields> = None;
let mut body: Vec<u8> = Vec::new();
loop {
if start.elapsed() > total_deadline {
return Err(Error::Io(io::Error::new(
io::ErrorKind::TimedOut,
"http3: response timed out",
)));
}
if conn.is_closed() {
return Err(Error::BadResponse("http3: peer closed connection".into()));
}
drain_uni_streams(conn, state)?;
let mut tmp = vec![0u8; 16 * 1024];
let (n, fin) = match conn.read(sid, &mut tmp) {
Ok(x) => x,
Err(e) => return Err(Error::BadResponse(format!("http3: stream read: {e:?}"))),
};
if n > 0 {
if stream_buf.len() + n > MAX_RESPONSE_BYTES {
return Err(Error::BadResponse("http3: response too large".into()));
}
stream_buf.extend_from_slice(&tmp[..n]);
}
loop {
let (consumed, ack_owed) =
match try_consume_frame(&stream_buf, &mut headers, &mut body, &state.dyn_table) {
FrameOutcome::Consumed(n, ack) => (n, ack),
FrameOutcome::NeedMore => break,
FrameOutcome::Err(e) => return Err(e),
};
if ack_owed {
send_section_ack(conn, sid, state);
}
stream_buf.drain(..consumed);
if stream_buf.is_empty() {
break;
}
}
if fin {
if !stream_buf.is_empty() {
return Err(Error::BadResponse(
"http3: stream FIN with partial frame in buffer".into(),
));
}
break;
}
pump_once(conn, sock, peer, true)?;
}
let fields = headers.ok_or_else(|| Error::BadResponse("http3: no HEADERS frame".into()))?;
finalize_response(fields, body, trace)
}
enum FrameOutcome {
Consumed(usize, bool),
NeedMore,
Err(Error),
}
fn try_consume_frame(
buf: &[u8],
headers: &mut Option<qpack::Fields>,
body: &mut Vec<u8>,
dyn_table: &qpack::DynamicTable,
) -> FrameOutcome {
let (frame, hdr_len) = match Frame::decode_header(buf) {
Ok(x) => x,
Err(_) => return FrameOutcome::NeedMore,
};
match frame.ty {
frame_type::HEADERS if frame.len > MAX_HEADERS_FRAME_LEN => {
return FrameOutcome::Err(Error::BadResponse(
"http3: HEADERS frame length exceeds limit".into(),
));
}
frame_type::DATA => {
let remaining = MAX_RESPONSE_BYTES.saturating_sub(body.len()) as u64;
if frame.len > remaining {
return FrameOutcome::Err(Error::BadResponse(
"http3: DATA frame length exceeds response budget".into(),
));
}
}
_ => {}
}
let total = hdr_len.saturating_add(frame.len as usize);
if buf.len() < total {
return FrameOutcome::NeedMore;
}
let payload = &buf[hdr_len..total];
match frame.ty {
frame_type::HEADERS => match qpack::decode_field_section_with(payload, dyn_table) {
Ok(fields) => {
if headers.is_some() {
} else {
*headers = Some(fields);
}
let ack_owed = qpack::block_references_dynamic_table(payload);
FrameOutcome::Consumed(total, ack_owed)
}
Err(e) => FrameOutcome::Err(e),
},
frame_type::DATA => {
body.extend_from_slice(payload);
FrameOutcome::Consumed(total, false)
}
_ => FrameOutcome::Consumed(total, false),
}
}
fn send_section_ack(conn: &mut QuicConnection, request_sid: StreamId, state: &Http3State) {
if let Some(dec) = state.decoder_stream {
let mut out = Vec::with_capacity(4);
encode_section_ack(request_sid.value(), &mut out);
let _ = write_all(conn, dec, &out);
}
}
fn finalize_response(
fields: qpack::Fields,
body: Vec<u8>,
trace: &mut dyn Write,
) -> Result<Response> {
let mut status: Option<u16> = None;
let mut hdrs: Vec<(String, String)> = Vec::with_capacity(fields.len());
for (k, v) in fields {
if k == ":status" {
status = Some(
v.parse()
.map_err(|_| Error::BadResponse(format!("http3: bad :status {v:?}")))?,
);
} else if k.starts_with(':') {
continue;
} else {
hdrs.push((k, v));
}
}
let status = status.ok_or_else(|| Error::BadResponse("http3: missing :status".into()))?;
let _ = writeln!(trace, "< HTTP/3 {status}");
for (k, v) in &hdrs {
let _ = writeln!(trace, "< {k}: {v}");
}
let _ = writeln!(trace, "< ");
let _ = writeln!(trace, "* Received {} body bytes", body.len());
let (hdrs, body) = crate::http::maybe_decode_body(hdrs, body, trace)?;
Ok(Response {
status,
reason: String::new(),
version: "HTTP/3".to_string(),
headers: hdrs,
body,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn varint_round_trip_size_classes() {
let cases: &[(u64, usize)] = &[
(0, 1),
(63, 1),
(64, 2),
(16_383, 2),
(16_384, 4),
((1 << 30) - 1, 4),
(1 << 30, 8),
(varint::MAX, 8),
];
for &(value, expected_len) in cases {
assert_eq!(varint::encoded_len(value), expected_len, "len({value})");
let mut buf = Vec::new();
varint::encode(value, &mut buf);
assert_eq!(buf.len(), expected_len, "encoded bytes for {value}");
let (decoded, n) = varint::decode(&buf).expect("decode");
assert_eq!(decoded, value, "round-trip value");
assert_eq!(n, expected_len, "round-trip length");
}
}
#[test]
fn varint_rejects_empty_and_truncated() {
assert!(varint::decode(&[]).is_err());
assert!(varint::decode(&[0x40]).is_err());
assert!(varint::decode(&[0xC0, 0x00, 0x00]).is_err());
}
#[test]
fn varint_accepts_non_minimal_encoding() {
let (v, n) = varint::decode(&[0x40, 0x00]).unwrap();
assert_eq!(v, 0);
assert_eq!(n, 2);
}
#[test]
fn qpack_static_table_has_99_entries() {
assert_eq!(
qpack::STATIC_TABLE.len(),
99,
"QPACK static table must have 99 entries per RFC 9204 Appendix A"
);
}
#[test]
fn qpack_static_table_known_landmarks() {
assert_eq!(qpack::STATIC_TABLE[0], (":authority", ""));
assert_eq!(qpack::STATIC_TABLE[17], (":method", "GET"));
assert_eq!(qpack::STATIC_TABLE[23], (":scheme", "https"));
assert_eq!(qpack::STATIC_TABLE[25], (":status", "200"));
assert_eq!(qpack::STATIC_TABLE[98], ("x-frame-options", "sameorigin"));
}
#[test]
fn qpack_indexed_lookup_finds_get_and_https() {
assert_eq!(qpack::find_indexed(":method", "GET"), Some(17));
assert_eq!(qpack::find_indexed(":scheme", "https"), Some(23));
assert_eq!(qpack::find_indexed(":status", "200"), Some(25));
assert_eq!(qpack::find_name(":method"), Some(15));
assert_eq!(qpack::find_indexed(":method", "UNKNOWN"), None);
}
#[test]
fn qpack_int_round_trip_prefix_sizes() {
for &(value, prefix, pattern) in &[
(0u64, 8u8, 0x00u8),
(1, 5, 0xE0),
(10, 5, 0xE0),
(30, 5, 0xE0), (31, 5, 0xE0), (1000, 5, 0xE0),
(254, 8, 0x00),
(255, 8, 0x00),
(1 << 20, 7, 0x00),
] {
let mut buf = Vec::new();
qpack::encode_int(value, prefix, pattern, &mut buf);
let (decoded, used) = qpack::decode_int(buf[0], prefix, &buf[1..]).expect("decode_int");
assert_eq!(decoded, value, "value {value}, prefix {prefix}");
assert_eq!(used + 1, buf.len(), "used+1 == buf.len for value {value}");
}
}
#[test]
fn http3_frame_header_round_trip() {
let cases: &[(u64, u64)] = &[
(frame_type::DATA, 0),
(frame_type::HEADERS, 17),
(frame_type::SETTINGS, 63),
(frame_type::HEADERS, 64),
(frame_type::DATA, 16_383),
(frame_type::DATA, 16_384),
(frame_type::DATA, 1 << 20),
];
for &(ty, len) in cases {
let mut buf = Vec::new();
Frame::encode_header(ty, len, &mut buf);
let (parsed, used) = Frame::decode_header(&buf).expect("decode_header");
assert_eq!(parsed, Frame { ty, len });
assert_eq!(used, buf.len(), "exact consumption for ({ty},{len})");
}
}
#[test]
fn qpack_encode_decode_round_trip_indexed_and_literal() {
let fields = vec![
(":method".to_string(), "GET".to_string()),
(":scheme".to_string(), "https".to_string()),
(":authority".to_string(), "example.com".to_string()),
(":path".to_string(), "/index.html".to_string()),
("user-agent".to_string(), "rsurl/test".to_string()),
("x-custom".to_string(), "hello".to_string()),
];
let wire = qpack::encode_field_section(&fields);
let decoded = qpack::decode_field_section(&wire).expect("decode");
assert_eq!(decoded, fields);
}
#[test]
fn qpack_huffman_decodes_rfc7541_c4_www_example_com() {
let encoded = [
0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff,
];
let out = qpack::huffman_decode(&encoded).expect("decode");
assert_eq!(out, b"www.example.com");
}
#[test]
fn qpack_huffman_decodes_rfc7541_c4_no_cache() {
let encoded = [0xa8, 0xeb, 0x10, 0x64, 0x9c, 0xbf];
let out = qpack::huffman_decode(&encoded).expect("decode");
assert_eq!(out, b"no-cache");
}
#[test]
fn qpack_huffman_decodes_rfc7541_c4_custom_key_and_value() {
let key_encoded = [0x25, 0xa8, 0x49, 0xe9, 0x5b, 0xa9, 0x7d, 0x7f];
let key = qpack::huffman_decode(&key_encoded).expect("decode key");
assert_eq!(key, b"custom-key");
let val_encoded = [0x25, 0xa8, 0x49, 0xe9, 0x5b, 0xb8, 0xe8, 0xb4, 0xbf];
let val = qpack::huffman_decode(&val_encoded).expect("decode value");
assert_eq!(val, b"custom-value");
}
#[test]
fn qpack_huffman_decodes_hand_built_get() {
let encoded = [0xC5, 0x83, 0x7F];
let out = qpack::huffman_decode(&encoded).expect("decode");
assert_eq!(out, b"GET");
}
#[test]
fn qpack_huffman_decoder_rejects_eos_in_literal() {
let encoded = [0xFF, 0xFF, 0xFF, 0xFF];
let err = qpack::huffman_decode(&encoded).unwrap_err();
match err {
Error::BadResponse(m) => {
assert!(
m.contains("EOS") || m.contains("Huffman"),
"unexpected message: {m}"
);
}
other => panic!("expected BadResponse, got {other:?}"),
}
}
#[test]
fn qpack_huffman_decoder_rejects_bad_padding() {
let encoded = [0xC6];
let err = qpack::huffman_decode(&encoded).unwrap_err();
match err {
Error::BadResponse(m) => assert!(m.contains("padding"), "msg: {m}"),
other => panic!("expected BadResponse(padding), got {other:?}"),
}
}
#[test]
fn qpack_decoder_handles_huffman_literal_value_end_to_end() {
let www_huffman: [u8; 12] = [
0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff,
];
let mut buf = Vec::new();
qpack::encode_int(0, 8, 0x00, &mut buf);
qpack::encode_int(0, 7, 0x00, &mut buf);
qpack::encode_int(0, 4, 0b0101_0000, &mut buf);
buf.push(0x80 | 12);
buf.extend_from_slice(&www_huffman);
let fields = qpack::decode_field_section(&buf).expect("decode");
assert_eq!(
fields,
vec![(":authority".to_string(), "www.example.com".to_string())]
);
}
#[test]
fn qpack_decoder_handles_huffman_literal_name_end_to_end() {
let key_huffman: [u8; 8] = [0x25, 0xa8, 0x49, 0xe9, 0x5b, 0xa9, 0x7d, 0x7f];
let val_huffman: [u8; 9] = [0x25, 0xa8, 0x49, 0xe9, 0x5b, 0xb8, 0xe8, 0xb4, 0xbf];
let mut buf = Vec::new();
qpack::encode_int(0, 8, 0x00, &mut buf);
qpack::encode_int(0, 7, 0x00, &mut buf);
qpack::encode_int(key_huffman.len() as u64, 3, 0b0010_1000, &mut buf);
buf.extend_from_slice(&key_huffman);
qpack::encode_int(val_huffman.len() as u64, 7, 0x80, &mut buf);
buf.extend_from_slice(&val_huffman);
let fields = qpack::decode_field_section(&buf).expect("decode");
assert_eq!(
fields,
vec![("custom-key".to_string(), "custom-value".to_string())]
);
}
#[test]
fn send_rejects_non_https() {
let req = Request::get("http://example.com/").unwrap();
let err = send(req, &mut std::io::sink()).unwrap_err();
match err {
Error::UnsupportedScheme(_) => {}
other => panic!("expected UnsupportedScheme, got {other:?}"),
}
}
#[test]
fn qpack_decompression_bomb_is_rejected() {
let mut buf = Vec::new();
qpack::encode_int(0, 8, 0x00, &mut buf);
qpack::encode_int(0, 7, 0x00, &mut buf);
let name = b"a";
let value = vec![b'x'; 1024];
for _ in 0..512 {
qpack::encode_int(name.len() as u64, 3, 0b0010_0000, &mut buf);
buf.extend_from_slice(name);
qpack::encode_int(value.len() as u64, 7, 0x00, &mut buf);
buf.extend_from_slice(&value);
}
let err = qpack::decode_field_section(&buf).unwrap_err();
assert!(matches!(err, Error::BadResponse(_)));
}
#[test]
fn oversized_headers_frame_len_is_rejected() {
let mut buf = Vec::new();
Frame::encode_header(frame_type::HEADERS, MAX_HEADERS_FRAME_LEN + 1, &mut buf);
let mut headers = None;
let mut body = Vec::new();
let table = qpack::DynamicTable::new();
assert!(matches!(
try_consume_frame(&buf, &mut headers, &mut body, &table),
FrameOutcome::Err(Error::BadResponse(_))
));
}
#[test]
fn data_frame_len_past_budget_is_rejected() {
let mut buf = Vec::new();
Frame::encode_header(frame_type::DATA, (MAX_RESPONSE_BYTES + 1) as u64, &mut buf);
let mut headers = None;
let mut body = Vec::new();
let table = qpack::DynamicTable::new();
assert!(matches!(
try_consume_frame(&buf, &mut headers, &mut body, &table),
FrameOutcome::Err(Error::BadResponse(_))
));
}
fn table_at_max() -> qpack::DynamicTable {
let mut t = qpack::DynamicTable::new();
t.set_capacity(qpack::MAX_DYNAMIC_CAPACITY as u64)
.expect("set capacity");
t
}
fn enc_insert_literal(name: &str, value: &str, out: &mut Vec<u8>) {
qpack::encode_int(name.len() as u64, 5, 0b0100_0000, out);
out.extend_from_slice(name.as_bytes());
qpack::encode_string_7bit(value, out);
}
fn enc_insert_name_ref(t_static: bool, idx: u64, value: &str, out: &mut Vec<u8>) {
let pat = 0b1000_0000 | if t_static { 0b0100_0000 } else { 0 };
qpack::encode_int(idx, 6, pat, out);
qpack::encode_string_7bit(value, out);
}
fn enc_set_capacity(cap: u64, out: &mut Vec<u8>) {
qpack::encode_int(cap, 5, 0b0010_0000, out);
}
fn enc_duplicate(rel: u64, out: &mut Vec<u8>) {
qpack::encode_int(rel, 5, 0b0000_0000, out);
}
#[test]
fn qpack_encoder_insert_literal_name_adds_entry() {
let mut t = table_at_max();
let mut wire = Vec::new();
enc_insert_literal("x-custom", "hello", &mut wire);
let used = qpack::apply_encoder_instructions(&mut t, &wire).expect("apply");
assert_eq!(used, wire.len(), "consumed the whole instruction");
assert_eq!(t.insert_count(), 1);
assert_eq!(
t.get_absolute(0).expect("entry"),
&("x-custom".to_string(), "hello".to_string())
);
}
#[test]
fn qpack_encoder_insert_name_ref_static_and_dynamic() {
let mut t = table_at_max();
let mut wire = Vec::new();
enc_insert_name_ref(true, 0, "example.com", &mut wire);
enc_insert_name_ref(false, 0, "other.example", &mut wire);
let used = qpack::apply_encoder_instructions(&mut t, &wire).expect("apply");
assert_eq!(used, wire.len());
assert_eq!(t.insert_count(), 2);
assert_eq!(
t.get_absolute(0).unwrap(),
&(":authority".to_string(), "example.com".to_string())
);
assert_eq!(
t.get_absolute(1).unwrap(),
&(":authority".to_string(), "other.example".to_string())
);
}
#[test]
fn qpack_encoder_set_capacity_resizes_and_evicts() {
let mut t = table_at_max();
let mut wire = Vec::new();
enc_insert_literal("aaaa", "bbbb", &mut wire); enc_insert_literal("cccc", "dddd", &mut wire); qpack::apply_encoder_instructions(&mut t, &wire).expect("apply");
assert_eq!(t.insert_count(), 2);
assert!(t.get_absolute(0).is_ok());
let mut shrink = Vec::new();
enc_set_capacity(60, &mut shrink);
qpack::apply_encoder_instructions(&mut t, &shrink).expect("shrink");
assert!(t.get_absolute(0).is_err(), "oldest evicted");
assert_eq!(
t.get_absolute(1).unwrap(),
&("cccc".to_string(), "dddd".to_string())
);
assert_eq!(t.insert_count(), 2);
}
#[test]
fn qpack_encoder_duplicate_readds_entry() {
let mut t = table_at_max();
let mut wire = Vec::new();
enc_insert_literal("foo", "bar", &mut wire);
enc_duplicate(0, &mut wire);
qpack::apply_encoder_instructions(&mut t, &wire).expect("apply");
assert_eq!(t.insert_count(), 2);
assert_eq!(t.get_absolute(0).unwrap(), t.get_absolute(1).unwrap());
assert_eq!(
t.get_absolute(1).unwrap(),
&("foo".to_string(), "bar".to_string())
);
}
#[test]
fn qpack_encoder_eviction_on_overflow() {
let mut t = qpack::DynamicTable::new();
t.set_capacity(100).unwrap();
let mut wire = Vec::new();
enc_insert_literal("aaaa", "1111", &mut wire); enc_insert_literal("bbbb", "2222", &mut wire); enc_insert_literal("cccc", "3333", &mut wire); qpack::apply_encoder_instructions(&mut t, &wire).expect("apply");
assert_eq!(t.insert_count(), 3);
assert!(t.get_absolute(0).is_err(), "abs 0 evicted");
assert_eq!(
t.get_absolute(1).unwrap(),
&("bbbb".to_string(), "2222".to_string())
);
assert_eq!(
t.get_absolute(2).unwrap(),
&("cccc".to_string(), "3333".to_string())
);
}
#[test]
fn qpack_encoder_capacity_above_advertised_is_rejected() {
let mut t = qpack::DynamicTable::new();
let mut wire = Vec::new();
enc_set_capacity(qpack::MAX_DYNAMIC_CAPACITY as u64 + 1, &mut wire);
assert!(qpack::apply_encoder_instructions(&mut t, &wire).is_err());
}
#[test]
fn qpack_encoder_partial_instruction_is_held() {
let mut t = table_at_max();
let mut full = Vec::new();
enc_insert_literal("name", "value", &mut full);
let used = qpack::apply_encoder_instructions(&mut t, &full[..full.len() - 1])
.expect("partial apply");
assert_eq!(used, 0, "no complete instruction yet");
assert_eq!(t.insert_count(), 0);
let used = qpack::apply_encoder_instructions(&mut t, &full).expect("apply");
assert_eq!(used, full.len());
assert_eq!(t.insert_count(), 1);
}
#[test]
fn qpack_encoder_rfc9204_appendix_b2_cross_check() {
let mut wire: Vec<u8> = vec![0x3f, 0xbd, 0x01];
wire.extend_from_slice(&[0xc0, 0x0f]);
wire.extend_from_slice(b"www.example.com");
wire.extend_from_slice(&[0xc1, 0x0c]);
wire.extend_from_slice(b"/sample/path");
let mut t = qpack::DynamicTable::new();
let used = qpack::apply_encoder_instructions(&mut t, &wire).expect("apply");
assert_eq!(used, wire.len(), "consumed entire Appendix B.2 stream");
assert_eq!(t.insert_count(), 2);
assert_eq!(
t.get_absolute(0).unwrap(),
&(":authority".to_string(), "www.example.com".to_string())
);
assert_eq!(
t.get_absolute(1).unwrap(),
&(":path".to_string(), "/sample/path".to_string())
);
let block: [u8; 4] = [0x03, 0x81, 0x10, 0x11];
let fields = qpack::decode_field_section_with(&block, &t).expect("decode block");
assert_eq!(
fields,
vec![
(":authority".to_string(), "www.example.com".to_string()),
(":path".to_string(), "/sample/path".to_string()),
]
);
assert!(qpack::block_references_dynamic_table(&block));
}
#[test]
fn qpack_required_insert_count_round_trips() {
let max_entries = (qpack::MAX_DYNAMIC_CAPACITY / 32) as u64;
let full_range = 2 * max_entries; for &(ric, total) in &[
(1u64, 1u64),
(5, 10),
(128, 200),
(255, 300),
(400, 400), (300, 400), (512, 600), ] {
let enc = (ric % full_range) + 1;
let got = qpack::decode_required_insert_count(enc, total).expect("decode RIC");
assert_eq!(got, ric, "RIC {ric} total {total} enc {enc}");
}
assert_eq!(qpack::decode_required_insert_count(0, 99).unwrap(), 0);
}
#[test]
fn qpack_required_insert_count_rejects_out_of_range() {
let max_entries = (qpack::MAX_DYNAMIC_CAPACITY / 32) as u64;
let full_range = 2 * max_entries;
assert!(qpack::decode_required_insert_count(full_range + 1, 0).is_err());
}
fn enc_prefix(enc_ric: u64, sign: bool, delta_base: u64, out: &mut Vec<u8>) {
qpack::encode_int(enc_ric, 8, 0x00, out);
let pat = if sign { 0b1000_0000 } else { 0 };
qpack::encode_int(delta_base, 7, pat, out);
}
#[test]
fn qpack_decode_with_dynamic_indexed_post_base_and_name_ref() {
let mut t = table_at_max();
let mut enc = Vec::new();
enc_insert_literal("x-a", "va", &mut enc);
enc_insert_literal("x-b", "vb", &mut enc);
enc_insert_literal("x-c", "vc", &mut enc);
qpack::apply_encoder_instructions(&mut t, &enc).expect("inserts");
assert_eq!(t.insert_count(), 3);
let mut block = Vec::new();
enc_prefix(4, true, 0, &mut block);
qpack::encode_int(1, 6, 0b1000_0000, &mut block);
qpack::encode_int(0, 6, 0b1000_0000, &mut block);
qpack::encode_int(0, 4, 0b0001_0000, &mut block);
qpack::encode_int(1, 4, 0b0100_0000, &mut block);
qpack::encode_string_7bit("lit", &mut block);
let fields = qpack::decode_field_section_with(&block, &t).expect("decode");
assert_eq!(
fields,
vec![
("x-a".to_string(), "va".to_string()),
("x-b".to_string(), "vb".to_string()),
("x-c".to_string(), "vc".to_string()),
("x-a".to_string(), "lit".to_string()),
]
);
assert!(qpack::block_references_dynamic_table(&block));
}
#[test]
fn qpack_decode_post_base_name_reference() {
let mut t = table_at_max();
let mut enc = Vec::new();
enc_insert_literal("x-name", "ignored", &mut enc); qpack::apply_encoder_instructions(&mut t, &enc).expect("insert");
let mut block = Vec::new();
enc_prefix(2, true, 0, &mut block); qpack::encode_int(0, 3, 0b0000_0000, &mut block);
qpack::encode_string_7bit("v", &mut block);
let fields = qpack::decode_field_section_with(&block, &t).expect("decode");
assert_eq!(fields, vec![("x-name".to_string(), "v".to_string())]);
}
#[test]
fn qpack_decode_mixes_static_and_dynamic() {
let mut t = table_at_max();
let mut enc = Vec::new();
enc_insert_literal("x-dyn", "dynval", &mut enc); qpack::apply_encoder_instructions(&mut t, &enc).expect("insert");
let mut block = Vec::new();
enc_prefix(2, false, 0, &mut block); qpack::encode_int(25, 6, 0b1100_0000, &mut block);
qpack::encode_int(0, 6, 0b1000_0000, &mut block);
let fields = qpack::decode_field_section_with(&block, &t).expect("decode");
assert_eq!(
fields,
vec![
(":status".to_string(), "200".to_string()),
("x-dyn".to_string(), "dynval".to_string()),
]
);
}
#[test]
fn qpack_decode_unsatisfiable_required_insert_count_errors() {
let mut t = table_at_max();
let mut enc = Vec::new();
enc_insert_literal("a", "b", &mut enc); qpack::apply_encoder_instructions(&mut t, &enc).unwrap();
let mut block = Vec::new();
enc_prefix(6, false, 0, &mut block);
qpack::encode_int(0, 6, 0b1000_0000, &mut block);
let err = qpack::decode_field_section_with(&block, &t).unwrap_err();
assert!(matches!(err, Error::BadResponse(_)), "got {err:?}");
}
#[test]
fn qpack_decode_dynamic_index_out_of_range_errors() {
let mut t = table_at_max();
let mut enc = Vec::new();
enc_insert_literal("a", "b", &mut enc); qpack::apply_encoder_instructions(&mut t, &enc).unwrap();
let mut block = Vec::new();
enc_prefix(2, false, 0, &mut block); qpack::encode_int(5, 6, 0b1000_0000, &mut block);
assert!(qpack::decode_field_section_with(&block, &t).is_err());
}
#[test]
fn qpack_decode_dynamic_reference_bomb_trips_list_cap() {
let mut t = table_at_max();
let mut enc = Vec::new();
let big = "x".repeat(3000);
enc_insert_literal("a", &big, &mut enc); qpack::apply_encoder_instructions(&mut t, &enc).expect("insert");
let mut block = Vec::new();
enc_prefix(2, false, 0, &mut block); for _ in 0..100 {
qpack::encode_int(0, 6, 0b1000_0000, &mut block);
}
let err = qpack::decode_field_section_with(&block, &t).unwrap_err();
match err {
Error::BadResponse(m) => assert!(m.contains("header list"), "msg: {m}"),
other => panic!("expected header-list-cap error, got {other:?}"),
}
}
#[test]
fn qpack_block_references_dynamic_table_predicate() {
let mut zero = Vec::new();
enc_prefix(0, false, 0, &mut zero);
assert!(!qpack::block_references_dynamic_table(&zero));
let mut nonzero = Vec::new();
enc_prefix(2, false, 0, &mut nonzero);
assert!(qpack::block_references_dynamic_table(&nonzero));
}
#[test]
fn qpack_section_ack_encoding() {
let mut out = Vec::new();
encode_section_ack(0, &mut out);
assert_eq!(out, vec![0x80]);
let mut out = Vec::new();
encode_section_ack(4, &mut out);
assert_eq!(out, vec![0x84]);
}
}