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_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 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");
let response = response.await.expect("response");
assert!(response.status().is_success());
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
}
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>) {
let id = self.next_stream_id;
self.next_stream_id += 2;
self.stream
.write_all(&frame(0x01, 0x05, id, &block))
.await
.expect("request");
self.stream.flush().await.expect("flush");
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 = &[(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_match_eq(&prog, 1, Some(&test_header_val));
}