use std::{net::SocketAddr, time::Duration};
use ::h2::{RecvStream, client};
use beeper::{h1, h2};
use bytes::Bytes;
use httlib_huffman as huffman;
use http::{HeaderName, HeaderValue, Request, Response, header};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
};
use utils::{
server,
test::{Direction, TestProgram},
};
use xbpf::OpenObject;
const TEST_HEADER: HeaderName = HeaderName::from_static("testheader");
const METHOD_HEADER: HeaderName = HeaderName::from_static("method");
const AUTHORITY_HEADER: HeaderName = HeaderName::from_static("authority");
const PATH_HEADER: HeaderName = HeaderName::from_static("path");
fn huffman_encode(s: &str) -> Vec<u8> {
let mut coded = Vec::new();
huffman::encode(s.as_bytes(), &mut coded).expect("encode");
assert!(
coded.len() < 127,
"huffman_encode only encodes a one byte length"
);
let mut out = vec![0x80 | coded.len() as u8];
out.extend_from_slice(&coded);
out
}
fn huffman_decode(val: &[u8]) -> String {
let mut res = Vec::new();
huffman::decode(val, &mut res, huffman::DecoderSpeed::OneBit).unwrap();
String::from_utf8(res).unwrap()
}
fn dynamic_table_size_for_headers(headers: &[(HeaderName, HeaderValue)]) -> u32 {
headers.iter().fold(0, |acc, (k, v)| {
acc + (k.as_str().len() + v.len() + 32) as u32
})
}
fn assert_match_eq(prog: &TestProgram, idx: usize, expected: Option<&HeaderValue>) {
let actual_hf = prog.get_match(idx).expect("get_match");
let actual = actual_hf.map(|val| huffman_decode(&val));
if expected.is_none() {
assert!(
actual.is_none(),
"get_match({idx}): {}, expected: none",
actual.unwrap()
);
} else {
let expected = expected.unwrap().to_str().unwrap();
assert!(
actual.is_some(),
"get_match({idx}): none, expected: {expected}"
);
assert_eq!(actual.unwrap().as_str(), expected);
}
}
struct Client {
send_request: client::SendRequest<Bytes>,
local_addr: SocketAddr,
remote_addr: SocketAddr,
}
impl Client {
async fn connect(addr: SocketAddr, header_table_size: Option<u32>) -> Self {
let stream = TcpStream::connect(addr).await.expect("connect");
let local_addr = stream.local_addr().expect("local_addr");
let remote_addr = stream.peer_addr().expect("peer_addr");
let mut builder = client::Builder::new();
if let Some(size) = header_table_size {
builder.header_table_size(size);
}
let (send_request, connection) = builder
.handshake::<_, Bytes>(stream)
.await
.expect("handshake");
tokio::spawn(async move {
connection.await.expect("connection");
});
Self {
send_request,
local_addr,
remote_addr,
}
}
#[allow(unused_results)]
async fn get(
&self,
uri: String,
headers: &[(header::HeaderName, HeaderValue)],
) -> Response<RecvStream> {
let response = self.send(uri, headers).await;
assert!(
response.status().is_success(),
"status: {}",
response.status()
);
response
}
#[allow(unused_results)]
async fn send(
&self,
uri: String,
headers: &[(header::HeaderName, HeaderValue)],
) -> Response<RecvStream> {
let mut req = Request::builder().method("GET").uri(uri);
for (name, value) in headers {
req = req.header(name, value);
}
let request = req.body(()).expect("build request");
let mut send_request = self.send_request.clone().ready().await.expect("ready");
let (response, _) = send_request
.send_request(request, true)
.expect("send_request");
response.await.expect("response")
}
}
const PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
const FIRST_DYNAMIC_INDEX: u8 = 62;
fn frame(kind: u8, flags: u8, stream: u32, payload: &[u8]) -> Vec<u8> {
let mut f = Vec::new();
f.extend_from_slice(&(payload.len() as u32).to_be_bytes()[1..]);
f.push(kind);
f.push(flags);
f.extend_from_slice(&stream.to_be_bytes());
f.extend_from_slice(payload);
f
}
fn raw_str(s: &str) -> Vec<u8> {
assert!(s.len() < 127, "raw_str only encodes a one byte length");
let mut out = vec![s.len() as u8];
out.extend_from_slice(s.as_bytes());
out
}
fn long_raw_str(s: &str) -> Vec<u8> {
let mut out = Vec::new();
if s.len() < 0x7F {
out.push(s.len() as u8);
} else {
assert!(s.len() < 0x7F + 128, "long_raw_str only encodes two bytes");
out.push(0x7F);
out.push((s.len() - 0x7F) as u8);
}
out.extend_from_slice(s.as_bytes());
out
}
struct RawClient {
stream: TcpStream,
local_addr: SocketAddr,
remote_addr: SocketAddr,
next_stream_id: u32,
}
impl RawClient {
async fn connect(addr: SocketAddr) -> Self {
let mut stream = TcpStream::connect(addr).await.expect("connect");
let local_addr = stream.local_addr().expect("local_addr");
let remote_addr = stream.peer_addr().expect("peer_addr");
stream.write_all(PREFACE).await.expect("preface");
stream
.write_all(&frame(0x04, 0, 0, &[]))
.await
.expect("settings");
stream.flush().await.expect("flush");
let mut client = Self {
stream,
local_addr,
remote_addr,
next_stream_id: 1,
};
client.read_frame(0x04).await;
client
.stream
.write_all(&frame(0x04, 0x01, 0, &[]))
.await
.expect("settings ack");
client.stream.flush().await.expect("flush");
client
}
async fn read_frame(&mut self, kind: u8) -> Vec<u8> {
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
loop {
let mut head = [0; 9];
tokio::time::timeout_at(deadline, self.stream.read_exact(&mut head))
.await
.expect("timed out waiting for a frame")
.expect("read frame header");
let len = u32::from_be_bytes([0, head[0], head[1], head[2]]) as usize;
let mut payload = vec![0; len];
tokio::time::timeout_at(deadline, self.stream.read_exact(&mut payload))
.await
.expect("timed out reading a frame")
.expect("read frame payload");
if head[3] == kind {
return payload;
}
}
}
async fn request(&mut self, block: Vec<u8>) {
self.request_all(&[(0, block)]).await;
}
async fn request_continued(&mut self, block: Vec<u8>, split: usize) {
let id = self.next_stream_id;
self.next_stream_id += 2;
let mut out = Vec::new();
out.extend_from_slice(&frame(0x01, 0x01, id, &block[..split]));
out.extend_from_slice(&frame(0x09, 0x04, id, &block[split..]));
self.stream.write_all(&out).await.expect("request");
self.stream.flush().await.expect("flush");
self.read_frame(0x01).await;
}
async fn request_all(&mut self, reqs: &[(u8, Vec<u8>)]) {
let mut out = Vec::new();
for (flags, payload) in reqs {
let id = self.next_stream_id;
self.next_stream_id += 2;
out.extend_from_slice(&frame(0x01, 0x05 | flags, id, payload));
}
self.stream.write_all(&out).await.expect("request");
self.stream.flush().await.expect("flush");
for _ in reqs {
self.read_frame(0x01).await;
}
}
async fn send_raw(&mut self, bytes: &[u8]) {
self.stream.write_all(bytes).await.expect("write");
self.stream.flush().await.expect("flush");
}
}
fn attach_preface_parser(prog_fd: i32) -> h1::AttachedParser {
h1::Parser::new()
.match_h2_preface()
.replace_parse_msg("parse_h1")
.replace_matched("matched_h1")
.replace_extract("extract_h1_match")
.attach(prog_fd)
.expect("attach parser")
}
fn attach_h2_parser(prog_fd: i32, hdrs: &[HeaderName]) -> h2::AttachedParser {
let mut h2 = h2::Parser::new();
for hdr in hdrs {
h2 = h2.capture_hdr(hdr).expect(&format!("capture {:?}", hdr));
}
h2.replace_parse_msg("parse_h2")
.replace_extract("extract_h2_match")
.attach(prog_fd)
.expect("attach parser")
}
#[tokio::test]
async fn parse_header_field_indexed_in_static_table() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog = TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach");
let _h1 = attach_preface_parser(prog.prog_fd());
let _h2 = attach_h2_parser(prog.prog_fd(), &[METHOD_HEADER]);
let client = Client::connect(addr, None).await;
client.get(format!("http://{}", addr), &[]).await;
let method_val = HeaderValue::from_static("GET");
assert_match_eq(&prog, 0, Some(&method_val));
}
#[tokio::test]
async fn parse_header_field_no_indexing_name_indexed_in_static_table() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let _h2 = attach_h2_parser(prog.prog_fd(), &[header::AUTHORIZATION]);
let auth_val = HeaderValue::from_static("Basic YmVlbGluZTpiZWVsaW5l");
let client = Client::connect(addr, None).await;
client
.get(
format!("http://{}", addr),
&[(header::AUTHORIZATION, auth_val.clone())],
)
.await;
assert_match_eq(&prog, 0, Some(&auth_val));
}
#[tokio::test]
async fn parse_header_field_never_indexing_name_indexed_in_static_table() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let _h2 = attach_h2_parser(prog.prog_fd(), &[TEST_HEADER]);
let mut test_header_val = HeaderValue::from_static("my secret");
test_header_val.set_sensitive(true);
let client = Client::connect(addr, None).await;
client
.get(
format!("http://{}", addr),
&[(TEST_HEADER, test_header_val.clone())],
)
.await;
assert_match_eq(&prog, 0, Some(&test_header_val));
}
#[tokio::test]
async fn parse_header_field_never_indexing_new_name() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let _h2 = attach_h2_parser(prog.prog_fd(), &[TEST_HEADER]);
let mut test_header_val = HeaderValue::from_static("my secret");
test_header_val.set_sensitive(true);
let client = Client::connect(addr, None).await;
client
.get(
format!("http://{}", addr),
&[(TEST_HEADER, test_header_val.clone())],
)
.await;
assert_match_eq(&prog, 0, Some(&test_header_val));
}
#[tokio::test]
async fn parse_header_field_incremental_indexing_name_indexed_in_static_table() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let _h2 = attach_h2_parser(prog.prog_fd(), &[header::USER_AGENT, PATH_HEADER]);
let user_agent_val = HeaderValue::from_static("beeper");
let path = "/bee/1234";
let path_val = HeaderValue::from_static(path);
let client = Client::connect(addr, None).await;
client
.get(
format!("http://{}{}", addr, path),
&[(header::USER_AGENT, user_agent_val.clone())],
)
.await;
assert_match_eq(&prog, 0, Some(&user_agent_val));
assert_match_eq(&prog, 1, Some(&path_val));
}
#[tokio::test]
async fn parse_header_field_incremental_indexing_new_name() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let _h2 = attach_h2_parser(prog.prog_fd(), &[TEST_HEADER, PATH_HEADER]);
let test_header_val = HeaderValue::from_static("beeper");
let path = "/bee/1234";
let path_val = HeaderValue::from_static(&path);
let client = Client::connect(addr, None).await;
client
.get(
format!("http://{}{}", addr, path),
&[(TEST_HEADER, test_header_val.clone())],
)
.await;
assert_match_eq(&prog, 0, Some(&test_header_val));
assert_match_eq(&prog, 1, Some(&path_val));
}
#[tokio::test]
async fn parse_header_field_incremental_indexing_indexed_in_dynamic_table() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let _h2 = attach_h2_parser(
prog.prog_fd(),
&[header::USER_AGENT, header::ACCEPT_LANGUAGE],
);
let user_agent_val = HeaderValue::from_static("beeper");
let lang_val = HeaderValue::from_static("sumsum");
let client = Client::connect(addr, None).await;
client
.get(
format!("http://{}", addr),
&[
(header::USER_AGENT, user_agent_val.clone()),
(header::ACCEPT_LANGUAGE, lang_val.clone()),
],
)
.await;
assert_match_eq(&prog, 0, Some(&user_agent_val));
assert_match_eq(&prog, 1, Some(&lang_val));
client
.get(
format!("http://{}", addr),
&[(header::VIA, HeaderValue::from_static("the hive"))],
)
.await;
assert_match_eq(&prog, 0, None);
assert_match_eq(&prog, 1, None);
client
.get(
format!("http://{}", addr),
&[
(header::ACCEPT_LANGUAGE, lang_val.clone()),
(header::USER_AGENT, user_agent_val.clone()),
],
)
.await;
assert_match_eq(&prog, 0, Some(&user_agent_val));
assert_match_eq(&prog, 1, Some(&lang_val));
}
fn raw_request_block(authority: &str, indexed: &[(Option<u8>, &str, &str)]) -> Vec<u8> {
let mut block = vec![0x82, 0x86, 0x84];
block.push(0x01);
block.extend_from_slice(&raw_str(authority));
for (name_idx, name, value) in indexed {
match name_idx {
Some(idx) => block.push(0x40 | idx),
None => {
block.push(0x40);
block.extend_from_slice(&raw_str(name));
}
}
block.extend_from_slice(&raw_str(value));
}
block
}
#[tokio::test]
async fn parse_header_field_incremental_indexing_not_huffman_encoded() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let accept_val = HeaderValue::from_static("*/*");
let test_header_val = HeaderValue::from_static("in-the-hive");
let mut client = RawClient::connect(addr).await;
client
.request(raw_request_block(
&addr.to_string(),
&[
(None, TEST_HEADER.as_str(), "in-the-hive"),
(Some(19), "accept", "*/*"),
],
))
.await;
assert_eq!(
prog.get_match(0).expect("get_match").as_deref(),
Some(accept_val.as_bytes()),
"a value that was not Huffman coded did not come back as it was sent"
);
let expected_dt = &[
(TEST_HEADER, test_header_val.clone()),
(header::ACCEPT, accept_val.clone()),
];
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
assert_eq!(info.count, expected_dt.len() as u32);
assert_eq!(info.size, dynamic_table_size_for_headers(expected_dt));
assert_eq!(info.max_size, 4096);
}
#[tokio::test]
async fn resolve_index_of_entry_that_was_not_huffman_encoded() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let _h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let accept_val = HeaderValue::from_static("*/*");
let mut client = RawClient::connect(addr).await;
client
.request(raw_request_block(
&addr.to_string(),
&[(Some(19), "accept", "*/*")],
))
.await;
assert_eq!(
prog.get_match(0).expect("get_match").as_deref(),
Some(accept_val.as_bytes())
);
let mut block = vec![0x82, 0x86, 0x84];
block.push(0x01);
block.extend_from_slice(&raw_str(&addr.to_string()));
block.push(0x80 | FIRST_DYNAMIC_INDEX);
client.request(block).await;
assert_eq!(
prog.get_match(0).expect("get_match").as_deref(),
Some(accept_val.as_bytes()),
"an entry that was not Huffman coded did not resolve from the table"
);
}
#[tokio::test]
async fn ignore_frame_that_ends_before_it_claims_to() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let mut client = RawClient::connect(addr).await;
let accept_val = HeaderValue::from_static("*/*");
client
.request(raw_request_block(
&addr.to_string(),
&[(Some(19), "accept", "*/*")],
))
.await;
let before = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
assert_eq!(before.count, 1);
let block = raw_request_block(&addr.to_string(), &[(Some(19), "accept", "*/*")]);
let mut truncated = frame(0x01, 0x05, 3, &block);
truncated[0] = 0;
truncated[1] = 0;
truncated[2] = 100;
client.send_raw(&truncated).await;
assert_eq!(
prog.get_match(0).expect("get_match").as_deref(),
Some(accept_val.as_bytes()),
"a frame that was never fully sent changed what was captured"
);
let after = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
assert_eq!(after.count, before.count);
assert_eq!(after.size, before.size);
}
#[tokio::test]
async fn ignore_header_field_indexed_past_the_end_of_the_table() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let mut client = RawClient::connect(addr).await;
let mut block = vec![0x82, 0x86, 0x84];
block.push(0x01);
block.extend_from_slice(&raw_str(&addr.to_string()));
block.push(0x80 | FIRST_DYNAMIC_INDEX);
client.send_raw(&frame(0x01, 0x05, 1, &block)).await;
assert_eq!(
prog.get_match(0).expect("get_match"),
None,
"an index no entry sits at resolved to something"
);
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
assert_eq!(info.count, 0);
}
#[tokio::test]
async fn ignore_header_field_whose_value_runs_past_the_frame() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let mut client = RawClient::connect(addr).await;
let mut block = vec![0x82, 0x86, 0x84];
block.push(0x01);
block.extend_from_slice(&raw_str(&addr.to_string()));
block.push(0x40 | 19);
block.push(100);
block.extend_from_slice(b"ab");
client.send_raw(&frame(0x01, 0x05, 1, &block)).await;
assert_eq!(
prog.get_match(0).expect("get_match"),
None,
"a value reaching past the frame was captured"
);
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
assert_eq!(info.count, 0);
assert_eq!(info.size, 0);
}
#[tokio::test]
async fn parse_frame_after_an_unknown_one() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let _h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let mut client = RawClient::connect(addr).await;
client.send_raw(&frame(0xFA, 0, 0, b"beeper")).await;
let accept_val = HeaderValue::from_static("*/*");
client
.request(raw_request_block(
&addr.to_string(),
&[(Some(19), "accept", "*/*")],
))
.await;
assert_eq!(
prog.get_match(0).expect("get_match").as_deref(),
Some(accept_val.as_bytes()),
"the parser did not recover from a frame it skipped"
);
}
#[tokio::test]
async fn update_dynamic_table_size() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[]);
let client = Client::connect(addr, Some(1234)).await;
client.get(format!("http://{}", addr), &[]).await;
let max_size = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info")
.max_size;
assert_eq!(max_size, 1234);
}
#[tokio::test]
async fn evict_header_field_from_dynamic_table() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[TEST_HEADER, header::USER_AGENT]);
let test_header_val = HeaderValue::from_static("asdfqwerasdfqwerasdfqwerasdfqwer");
let user_agent_val = HeaderValue::from_static("test-agent");
let client = Client::connect(addr, Some(254)).await;
client
.get(
format!("http://{}", addr),
&[(TEST_HEADER, test_header_val.clone())],
)
.await;
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
let authority = addr.to_string();
let expected_dt = &[
(TEST_HEADER, test_header_val.clone()),
(
AUTHORITY_HEADER,
HeaderValue::from_str(&authority.as_str()).unwrap(),
),
];
assert_eq!(info.max_size, 254);
assert_eq!(info.count, expected_dt.len() as u32);
assert_eq!(info.size, dynamic_table_size_for_headers(expected_dt));
assert_match_eq(&prog, 0, Some(&test_header_val));
client
.get(
format!("http://{}", addr),
&[(header::USER_AGENT, user_agent_val.clone())],
)
.await;
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
let expected_dt = &[
(TEST_HEADER, test_header_val.clone()),
(
AUTHORITY_HEADER,
HeaderValue::from_str(&authority.as_str()).unwrap(),
),
(header::USER_AGENT, user_agent_val.clone()),
];
assert_eq!(info.max_size, 254);
assert_eq!(info.count, expected_dt.len() as u32);
assert_eq!(info.size, dynamic_table_size_for_headers(expected_dt));
assert_match_eq(&prog, 1, Some(&user_agent_val));
client
.get(
format!("http://{}", addr),
&[(header::USER_AGENT, test_header_val.clone())],
)
.await;
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
let expected_dt = &[
(TEST_HEADER, test_header_val.clone()),
(header::USER_AGENT, user_agent_val.clone()),
(header::USER_AGENT, test_header_val.clone()),
];
assert_eq!(info.max_size, 254);
assert_eq!(info.count, expected_dt.len() as u32);
assert_eq!(info.size, dynamic_table_size_for_headers(expected_dt));
assert_eq!(info.deleted, 1);
assert_match_eq(&prog, 1, Some(&test_header_val));
}
const PADDED_FLAG: u8 = 0x08;
const PRIORITY_FLAG: u8 = 0x20;
fn padded(block: Vec<u8>, pad: &[u8]) -> Vec<u8> {
let mut payload = vec![pad.len() as u8];
payload.extend_from_slice(&block);
payload.extend_from_slice(pad);
payload
}
fn prioritised(block: Vec<u8>) -> Vec<u8> {
let mut payload = vec![0x00, 0x00, 0x00, 0x00, 0x10];
payload.extend_from_slice(&block);
payload
}
#[tokio::test]
async fn parse_padded_header_frame() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let authority = addr.to_string();
let padded_val = HeaderValue::from_static("padded");
let next_val = HeaderValue::from_static("after-the-padding");
let pad = [0x40, 0x00, 0x00];
let mut client = RawClient::connect(addr).await;
client
.request_all(&[
(
PADDED_FLAG,
padded(
raw_request_block(&authority, &[(Some(19), "accept", "padded")]),
&pad,
),
),
(
0,
raw_request_block(&authority, &[(Some(19), "accept", "after-the-padding")]),
),
])
.await;
assert_eq!(
prog.get_match(0).expect("get_match").as_deref(),
Some(next_val.as_bytes()),
"the frame after the padded one was not found"
);
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
let expected_dt = &[
(header::ACCEPT, padded_val.clone()),
(header::ACCEPT, next_val.clone()),
];
assert_eq!(
info.count,
expected_dt.len() as u32,
"the padding was read as a header field"
);
assert_eq!(info.size, dynamic_table_size_for_headers(expected_dt));
}
#[tokio::test]
async fn parse_header_frame_that_carries_a_priority() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let authority = addr.to_string();
let accept_val = HeaderValue::from_static("after-the-priority");
let mut client = RawClient::connect(addr).await;
client
.request_all(&[(
PRIORITY_FLAG,
prioritised(raw_request_block(
&authority,
&[(Some(19), "accept", "after-the-priority")],
)),
)])
.await;
assert_eq!(
prog.get_match(0).expect("get_match").as_deref(),
Some(accept_val.as_bytes()),
"the block was not read from behind the priority"
);
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
let expected_dt = &[(header::ACCEPT, accept_val.clone())];
assert_eq!(
info.count,
expected_dt.len() as u32,
"the priority was read as a header field"
);
assert_eq!(info.size, dynamic_table_size_for_headers(expected_dt));
}
#[tokio::test]
async fn resolve_index_of_entry_added_after_an_eviction() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[TEST_HEADER, header::USER_AGENT]);
let long_val = HeaderValue::from_static("asdfqwerasdfqwerasdfqwerasdfqwer");
let agent_val = HeaderValue::from_static("test-agent");
let other_agent_val = HeaderValue::from_static("other-agent");
let url = format!("http://{}", addr);
let client = Client::connect(addr, Some(254)).await;
client
.get(url.clone(), &[(TEST_HEADER, long_val.clone())])
.await;
client
.get(url.clone(), &[(header::USER_AGENT, agent_val.clone())])
.await;
client
.get(url.clone(), &[(header::USER_AGENT, long_val.clone())])
.await;
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
assert_eq!(
info.deleted, 1,
"nothing was evicted, so the entries below are stored where they would be anyway"
);
client
.get(
url.clone(),
&[(header::USER_AGENT, other_agent_val.clone())],
)
.await;
assert_match_eq(&prog, 1, Some(&other_agent_val));
client
.get(url.clone(), &[(header::USER_AGENT, long_val.clone())])
.await;
assert_match_eq(&prog, 1, Some(&long_val));
}
#[tokio::test]
async fn parse_header_block_split_over_a_continuation_frame() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let authority = addr.to_string();
let accept_val = HeaderValue::from_static("in-the-continuation");
let block = raw_request_block(&authority, &[(Some(19), "accept", "in-the-continuation")]);
let mut client = RawClient::connect(addr).await;
client.request_continued(block, 3).await;
assert_eq!(
prog.get_match(0).expect("get_match").as_deref(),
Some(accept_val.as_bytes()),
"the field in the continuation frame was not read"
);
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
let expected_dt = &[(header::ACCEPT, accept_val.clone())];
assert_eq!(info.count, expected_dt.len() as u32);
assert_eq!(info.size, dynamic_table_size_for_headers(expected_dt));
assert_eq!(
info.dirty, 0,
"a block that breaks between fields left the table looking untrustworthy"
);
}
#[tokio::test]
async fn mark_the_table_as_drifted_when_a_continuation_frame_splits_a_field() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let authority = addr.to_string();
let accept_val = HeaderValue::from_static("across-the-break");
let block = raw_request_block(&authority, &[(Some(19), "accept", "across-the-break")]);
let mut client = RawClient::connect(addr).await;
client.request_continued(block, 3 + 1 + 1 + 2).await;
assert_eq!(
prog.get_match(0).expect("get_match").as_deref(),
Some(accept_val.as_bytes()),
"the field behind the break was not read"
);
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
assert_eq!(
info.dirty, 1,
"a block that breaks inside a field left the table looking trustworthy"
);
}
#[tokio::test]
async fn update_dynamic_table_size_past_the_width_of_a_u16() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let client = Client::connect(addr, Some(65536)).await;
client.get(format!("http://{}", addr), &[]).await;
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
assert_eq!(info.max_size, 65536);
}
#[tokio::test]
async fn resolve_a_captured_index_against_the_table_it_was_read_from() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let _h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let authority = addr.to_string();
let first = HeaderValue::from_static("first");
let mut client = RawClient::connect(addr).await;
client
.request(raw_request_block(
&authority,
&[(Some(19), "accept", "first")],
))
.await;
assert_eq!(
prog.get_match(0).expect("get_match").as_deref(),
Some(first.as_bytes())
);
let mut block = vec![0x82, 0x86, 0x84];
block.push(0x01);
block.extend_from_slice(&raw_str(&authority));
block.push(0x80 | FIRST_DYNAMIC_INDEX);
block.push(0x40 | 58);
block.extend_from_slice(&raw_str("junk"));
client.request(block).await;
assert_eq!(
prog.get_match(0).expect("get_match").as_deref(),
Some(first.as_bytes()),
"the capture followed the index into the entry that took its place"
);
}
#[tokio::test]
async fn ignore_a_value_that_runs_into_the_frame_behind_it() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let authority = addr.to_string();
let mut client = RawClient::connect(addr).await;
let mut block = vec![0x82, 0x86, 0x84];
block.push(0x01);
block.extend_from_slice(&raw_str(&authority));
block.push(0x40 | 19);
block.push(100);
block.extend_from_slice(b"ab");
let mut out = frame(0x01, 0x05, 1, &block);
out.extend_from_slice(&frame(0x01, 0x05, 3, &raw_request_block(&authority, &[])));
client.send_raw(&out).await;
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
assert_eq!(
info.count, 0,
"a value reaching into the next frame was added to the table"
);
assert_eq!(info.size, 0);
}
#[tokio::test]
async fn size_a_dynamic_table_entry_that_is_longer_than_an_entry_holds() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let authority = addr.to_string();
let long = "a".repeat(130);
let long_val = HeaderValue::from_str(&long).expect("header value");
let mut block = vec![0x82, 0x86, 0x84];
block.push(0x01);
block.extend_from_slice(&raw_str(&authority));
block.push(0x40 | 19);
block.extend_from_slice(&long_raw_str(&long));
let mut client = RawClient::connect(addr).await;
client.request(block).await;
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("dynamic_table_info");
let expected_dt = &[(header::ACCEPT, long_val.clone())];
assert_eq!(info.count, expected_dt.len() as u32);
assert_eq!(
info.size,
dynamic_table_size_for_headers(expected_dt),
"a field longer than an entry holds was sized by what was kept of it"
);
}
#[tokio::test]
async fn ignore_an_index_that_only_wraps_into_the_table() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let _h1 = attach_preface_parser(prog.prog_fd());
let _h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]);
let authority = addr.to_string();
let secret = HeaderValue::from_static("secret");
let mut client = RawClient::connect(addr).await;
client
.request(raw_request_block(
&authority,
&[(Some(19), "accept", "secret")],
))
.await;
assert_eq!(
prog.get_match(0).expect("get_match").as_deref(),
Some(secret.as_bytes())
);
let idx: u32 = 0x8000 + FIRST_DYNAMIC_INDEX as u32;
let mut block = vec![0x82, 0x86, 0x84];
block.push(0x01);
block.extend_from_slice(&raw_str(&authority));
block.push(0xFF);
let mut rest = idx - 0x7F;
while rest >= 0x80 {
block.push(0x80 | (rest & 0x7F) as u8);
rest >>= 7;
}
block.push(rest as u8);
client.send_raw(&frame(0x01, 0x05, 3, &block)).await;
assert_eq!(
prog.get_match(0).expect("get_match"),
None,
"an index past the end of the table resolved to an entry inside it"
);
}
#[tokio::test]
async fn match_a_field_name_by_the_whole_name() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let prog =
TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program");
let short = HeaderName::from_static("a");
let mut coded_short = Vec::new();
huffman::encode(short.as_str().as_bytes(), &mut coded_short).expect("encode");
let mut coded_long = Vec::new();
huffman::encode(b"a&b", &mut coded_long).expect("encode");
assert!(coded_long.starts_with(&coded_short));
let _h1 = attach_preface_parser(prog.prog_fd());
let _h2 = attach_h2_parser(prog.prog_fd(), &[short]);
let authority = addr.to_string();
let mut block = vec![0x82, 0x86, 0x84];
block.push(0x01);
block.extend_from_slice(&raw_str(&authority));
block.push(0x40);
block.extend_from_slice(&huffman_encode("a&b"));
block.extend_from_slice(&raw_str("not-the-one"));
let mut client = RawClient::connect(addr).await;
client.send_raw(&frame(0x01, 0x05, 1, &block)).await;
assert_eq!(
prog.get_match(0).expect("get_match"),
None,
"a field whose name only starts like the pattern was captured"
);
}