use ::h2::{RecvStream, client};
use beeper::{MatchId, h1, h2, pseudo_header};
use bytes::Bytes;
use httlib_huffman as huffman;
use http::{HeaderName, HeaderValue, Request, Response, header};
use std::{net::SocketAddr, time::Duration};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
};
use utils::{
server,
test::{Direction, Hook, TestProgram},
};
use xbpf::OpenObject;
const TEST_HEADER_NAME: &str = "testheader";
const TEST_HEADER: HeaderName = HeaderName::from_static(TEST_HEADER_NAME);
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: &[(&str, HeaderValue)]) -> u32 {
headers
.iter()
.fold(0, |acc, (k, v)| acc + (k.len() + v.len() + 32) as u32)
}
fn assert_match_eq(prog: &TestProgram, mid: MatchId, expected: Option<&HeaderValue>) {
let actual_hf = prog.get_match(mid).expect("get_match");
let actual = actual_hf.map(|val| huffman_decode(&val));
if expected.is_none() {
assert!(
actual.is_none(),
"get_match({mid:?}): {}, expected: none",
actual.unwrap()
);
} else {
let expected = expected.unwrap().to_str().unwrap();
assert!(
actual.is_some(),
"get_match({mid:?}): 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_h1_parser(prog_fd: i32, hook: Hook) -> h1::AttachedParser {
let mut h1 = h1::Parser::new();
h1.match_h2_preface().expect("match preface");
let suffix = hook.to_string();
h1.matched_fn("matched_h1")
.parse_fn(format!("parse_h1_{suffix}"), hook.into())
.extract_fn(format!("extract_h1_match_{suffix}"), hook.into())
.attach(prog_fd)
.expect("attach parser")
}
fn attach_h2_parser(prog_fd: i32, hook: Hook, hdrs: &[&str]) -> (h2::AttachedParser, Vec<MatchId>) {
let mut h2 = h2::Parser::new();
let mut mids = Vec::new();
for hdr in hdrs {
mids.push(
h2.capture_hdr(hdr)
.unwrap_or_else(|e| panic!("capture {hdr:?}: {e}")),
);
}
let suffix = hook.to_string();
let h2 = h2
.parse_fn(format!("parse_h2_{suffix}"), hook.into())
.extract_fn(format!("extract_h2_match_{suffix}"), hook.into())
.attach(prog_fd)
.expect("attach parser");
(h2, mids)
}
fn attach_at<'obj>(
addr: SocketAddr,
open_obj: &'obj mut OpenObject,
hook: Hook,
hdrs: &[&str],
) -> (
TestProgram<'obj>,
h1::AttachedParser,
h2::AttachedParser,
Vec<MatchId>,
) {
let prog = TestProgram::attach_to(addr, open_obj, Direction::Downstream, hook)
.expect("attach program");
let h1 = attach_h1_parser(prog.prog_fd(), hook);
let (h2, mids) = attach_h2_parser(prog.prog_fd(), hook, hdrs);
(prog, h1, h2, mids)
}
fn conn_at(
hook: Hook,
local_addr: SocketAddr,
remote_addr: SocketAddr,
) -> (SocketAddr, SocketAddr) {
match hook {
Hook::Msg => (local_addr, remote_addr),
Hook::Skb => (remote_addr, local_addr),
}
}
#[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_h1_parser(prog.prog_fd(), Hook::Msg);
let (_h2, mids) =
attach_h2_parser(prog.prog_fd(), Hook::Msg, &[pseudo_header::METHOD.as_str()]);
let client = Client::connect(addr, None).await;
client.get(format!("http://{}", addr), &[]).await;
let method_val = HeaderValue::from_static("GET");
assert_match_eq(&prog, mids[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, _h1, _h2, mids) = attach_at(
addr,
&mut open_obj,
Hook::Msg,
&[header::AUTHORIZATION.as_str()],
);
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, mids[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, _h1, _h2, mids) = attach_at(addr, &mut open_obj, Hook::Msg, &[TEST_HEADER_NAME]);
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, mids[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, _h1, _h2, mids) = attach_at(addr, &mut open_obj, Hook::Msg, &[TEST_HEADER_NAME]);
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, mids[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, _h1, _h2, mids) = attach_at(
addr,
&mut open_obj,
Hook::Msg,
&[header::USER_AGENT.as_str(), pseudo_header::PATH.as_str()],
);
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, mids[0], Some(&user_agent_val));
assert_match_eq(&prog, mids[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, _h1, _h2, mids) = attach_at(
addr,
&mut open_obj,
Hook::Msg,
&[TEST_HEADER_NAME, pseudo_header::PATH.as_str()],
);
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, mids[0], Some(&test_header_val));
assert_match_eq(&prog, mids[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, _h1, _h2, mids) = attach_at(
addr,
&mut open_obj,
Hook::Msg,
&[
header::USER_AGENT.as_str(),
header::ACCEPT_LANGUAGE.as_str(),
],
);
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, mids[0], Some(&user_agent_val));
assert_match_eq(&prog, mids[1], Some(&lang_val));
client
.get(
format!("http://{}", addr),
&[(header::VIA, HeaderValue::from_static("the hive"))],
)
.await;
assert_match_eq(&prog, mids[0], None);
assert_match_eq(&prog, mids[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, mids[0], Some(&user_agent_val));
assert_match_eq(&prog, mids[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, _h1, h2, mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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_NAME, "in-the-hive"),
(Some(19), "accept", "*/*"),
],
))
.await;
assert_eq!(
prog.get_match(mids[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_NAME, test_header_val.clone()),
(header::ACCEPT.as_str(), accept_val.clone()),
];
let info = h2
.dynamic_table_info(client.local_addr, client.remote_addr)
.expect("connection is known")
.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, _h1, _h2, mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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(mids[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(mids[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, _h1, h2, mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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("connection is known")
.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(mids[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("connection is known")
.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, _h1, h2, mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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(mids[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("connection is known")
.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, _h1, h2, mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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(mids[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("connection is known")
.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, _h1, _h2, mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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(mids[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, _h1, h2, _mids) = attach_at(addr, &mut open_obj, Hook::Msg, &[]);
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("connection is known")
.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, _h1, h2, mids) = attach_at(
addr,
&mut open_obj,
Hook::Msg,
&[TEST_HEADER_NAME, header::USER_AGENT.as_str()],
);
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("connection is known")
.expect("dynamic_table_info");
let authority = addr.to_string();
let expected_dt = &[
(TEST_HEADER_NAME, test_header_val.clone()),
(
pseudo_header::AUTHORITY.as_str(),
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, mids[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("connection is known")
.expect("dynamic_table_info");
let expected_dt = &[
(TEST_HEADER_NAME, test_header_val.clone()),
(
pseudo_header::AUTHORITY.as_str(),
HeaderValue::from_str(&authority.as_str()).unwrap(),
),
(header::USER_AGENT.as_str(), 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, mids[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("connection is known")
.expect("dynamic_table_info");
let expected_dt = &[
(TEST_HEADER_NAME, test_header_val.clone()),
(header::USER_AGENT.as_str(), user_agent_val.clone()),
(header::USER_AGENT.as_str(), 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, mids[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, _h1, h2, mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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(mids[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("connection is known")
.expect("dynamic_table_info");
let expected_dt = &[
(header::ACCEPT.as_str(), padded_val.clone()),
(header::ACCEPT.as_str(), 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, _h1, h2, mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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(mids[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("connection is known")
.expect("dynamic_table_info");
let expected_dt = &[(header::ACCEPT.as_str(), 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, _h1, h2, mids) = attach_at(
addr,
&mut open_obj,
Hook::Msg,
&[TEST_HEADER_NAME, header::USER_AGENT.as_str()],
);
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("connection is known")
.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, mids[1], Some(&other_agent_val));
client
.get(url.clone(), &[(header::USER_AGENT, long_val.clone())])
.await;
assert_match_eq(&prog, mids[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, _h1, h2, mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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(mids[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("connection is known")
.expect("dynamic_table_info");
let expected_dt = &[(header::ACCEPT.as_str(), 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, _h1, h2, mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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(mids[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("connection is known")
.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, _h1, h2, _mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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("connection is known")
.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, _h1, _h2, mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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(mids[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(mids[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, _h1, h2, _mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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("connection is known")
.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, _h1, h2, _mids) =
attach_at(addr, &mut open_obj, Hook::Msg, &[header::ACCEPT.as_str()]);
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("connection is known")
.expect("dynamic_table_info");
let expected_dt = &[(header::ACCEPT.as_str(), 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_h1_parser(prog.prog_fd(), Hook::Msg);
let (_h2, mids) = attach_h2_parser(prog.prog_fd(), Hook::Msg, &[header::ACCEPT.as_str()]);
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(mids[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(mids[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_h1_parser(prog.prog_fd(), Hook::Msg);
let (_h2, mids) = attach_h2_parser(prog.prog_fd(), Hook::Msg, &[short.as_str()]);
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(mids[0]).expect("get_match"),
None,
"a field whose name only starts like the pattern was captured"
);
}
#[tokio::test]
async fn parse_header_field_indexed_in_static_table_in_skb() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let (prog, _h1, _h2, mids) = attach_at(
addr,
&mut open_obj,
Hook::Skb,
&[pseudo_header::METHOD.as_str()],
);
let client = Client::connect(addr, None).await;
client.get(format!("http://{}", addr), &[]).await;
let method_val = HeaderValue::from_static("GET");
assert_match_eq(&prog, mids[0], Some(&method_val));
}
#[tokio::test]
async fn parse_header_field_incremental_indexing_indexed_in_dynamic_table_in_skb() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let (prog, _h1, _h2, mids) = attach_at(
addr,
&mut open_obj,
Hook::Skb,
&[
header::USER_AGENT.as_str(),
header::ACCEPT_LANGUAGE.as_str(),
],
);
let user_agent_val = HeaderValue::from_static("beeper");
let lang_val = HeaderValue::from_static("sumsum");
let hdrs = [
(header::USER_AGENT, user_agent_val.clone()),
(header::ACCEPT_LANGUAGE, lang_val.clone()),
];
let client = Client::connect(addr, None).await;
client.get(format!("http://{}", addr), &hdrs).await;
assert_match_eq(&prog, mids[0], Some(&user_agent_val));
assert_match_eq(&prog, mids[1], Some(&lang_val));
client.get(format!("http://{}", addr), &hdrs).await;
assert_match_eq(&prog, mids[0], Some(&user_agent_val));
assert_match_eq(&prog, mids[1], Some(&lang_val));
}
#[tokio::test]
async fn update_dynamic_table_size_in_skb() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let (_prog, _h1, h2, _mids) = attach_at(addr, &mut open_obj, Hook::Skb, &[]);
let client = Client::connect(addr, Some(1234)).await;
client.get(format!("http://{}", addr), &[]).await;
let (local, remote) = conn_at(Hook::Skb, client.local_addr, client.remote_addr);
let max_size = h2
.dynamic_table_info(local, remote)
.expect("connection is known")
.expect("dynamic_table_info")
.max_size;
assert_eq!(max_size, 1234);
}
#[tokio::test]
async fn parse_every_frame_of_a_single_write_in_skb() {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let (prog, _h1, h2, mids) =
attach_at(addr, &mut open_obj, Hook::Skb, &[header::ACCEPT.as_str()]);
let authority = addr.to_string();
let mut client = RawClient::connect(addr).await;
client
.request_all(&[
(
0,
raw_request_block(&authority, &[(Some(19), "accept", "text/plain")]),
),
(
0,
raw_request_block(&authority, &[(Some(19), "accept", "text/html")]),
),
])
.await;
assert_eq!(
prog.get_match(mids[0]).expect("get_match").as_deref(),
Some(b"text/html".as_slice()),
"the second frame of the sk_buff was not parsed where it starts"
);
let (local, remote) = conn_at(Hook::Skb, client.local_addr, client.remote_addr);
let info = h2
.dynamic_table_info(local, remote)
.expect("connection is known")
.expect("dynamic_table_info");
assert_eq!(info.count, 2);
}
async fn report_the_dynamic_table_on_a_skipped_frame(hook: Hook) {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let (prog, _h1, _h2, _mids) = attach_at(addr, &mut open_obj, hook, &[header::ACCEPT.as_str()]);
let mut client = RawClient::connect(addr).await;
client
.request(raw_request_block(
&addr.to_string(),
&[(Some(19), "accept", "*/*")],
))
.await;
assert_eq!(prog.last_dt_counts(), (0, 1));
client.send_raw(&frame(0x06, 0, 0, &[0; 8])).await;
client.read_frame(0x06).await;
assert_eq!(prog.last_dt_counts(), (1, 1));
}
#[tokio::test]
async fn report_the_dynamic_table_on_a_skipped_frame_in_msg() {
report_the_dynamic_table_on_a_skipped_frame(Hook::Msg).await;
}
#[tokio::test]
async fn report_the_dynamic_table_on_a_skipped_frame_in_skb() {
report_the_dynamic_table_on_a_skipped_frame(Hook::Skb).await;
}
async fn forget_a_connection(hook: Hook) {
let addr = server::launch().await.expect("launch server");
let mut open_obj = OpenObject::new();
let (prog, _h1, h2, mids) = attach_at(addr, &mut open_obj, hook, &[header::ACCEPT.as_str()]);
let authority = addr.to_string();
let mut client = RawClient::connect(addr).await;
client
.request(raw_request_block(
&authority,
&[(Some(19), "accept", "*/*")],
))
.await;
let (local, remote) = conn_at(hook, client.local_addr, client.remote_addr);
let info = h2
.dynamic_table_info(local, remote)
.expect("connection is known")
.expect("dynamic_table_info");
assert_eq!(info.count, 1);
h2.forget_conn(local, remote).expect("forget_conn");
assert!(
h2.dynamic_table_info(local, remote).is_none(),
"the dynamic table of a forgotten connection is still there"
);
h2.forget_conn(local, remote).expect("forget_conn twice");
client.request(vec![0x82, 0x86, 0x84, 0xBE]).await;
assert_eq!(prog.get_match(mids[0]).expect("get_match"), None);
let info = h2
.dynamic_table_info(local, remote)
.expect("connection is known")
.expect("dynamic_table_info");
assert_eq!(info.count, 0);
}
#[tokio::test]
async fn forget_a_connection_in_msg() {
forget_a_connection(Hook::Msg).await;
}
#[tokio::test]
async fn forget_a_connection_in_skb() {
forget_a_connection(Hook::Skb).await;
}