Handle chunked HTTP correctly

This commit is contained in:
Pascal Engélibert 2026-01-16 16:45:40 +01:00
commit 1d62dae785
6 changed files with 799 additions and 198 deletions

View file

@ -4,6 +4,8 @@ use std::{
sync::mpsc::{Receiver, Sender, channel},
};
use crate::util::print_bin;
const CLIENT_TO_SERVER: u8 = b'C';
const SERVER_TO_CLIENT: u8 = b'S';
@ -15,6 +17,39 @@ pub enum Direction {
ServerToClient,
}
static TEST_RECORD: &[(u64, &str, Direction, &[u8])] =
&[
(0, "upload.wikimedia.org", Direction::ClientToServer, b"GET /aaaaaaaaa HTTP/1.1\r\nHost: upload.wikimedia.org\r\n\r\n"),
(0, "upload.wikimedia.org", Direction::ServerToClient, b"HTTP/1.1 200\r\nContent-Length: 5\r\nDate: Wed, 12 Nov 2025 13:52:58 GMT\r\n\r\nhello"),
(0, "upload.wikimedia.org", Direction::ClientToServer, b"GET /bbbbbbbbb HTTP/1.1\r\nHost: upload.wikimedia.org\r\n\r\n"),
(0, "upload.wikimedia.org", Direction::ServerToClient, b"HTTP/1.1 200\r\nContent-Length: 7\r\nDate: Wed, 12 Nov 2025 13:52:58 GMT\r\n\r\nbonjour"),
(1, "upload.wikimedia.org", Direction::ClientToServer, b"GET /ccccccccc HTTP/1.1\r\nHost: upload.wikimedia.org\r\n\r\n"),
(1, "upload.wikimedia.org", Direction::ServerToClient, b"HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nDate: Wed, 12 Nov 2025 13:52:58 GMT\r\n\r\n6\r\nbanane\r\n"),
(1, "upload.wikimedia.org", Direction::ServerToClient, b"5\r\npomme\r\n"),
(1, "upload.wikimedia.org", Direction::ServerToClient, b"0\r\n\r\n"),
];
fn write_record(
file: &mut std::fs::File,
direction: Direction,
conn_id: u64,
server_name: &str,
data: &[u8],
) {
let server_name = server_name.as_bytes();
file.write_all(&[match direction {
Direction::ClientToServer => CLIENT_TO_SERVER,
Direction::ServerToClient => SERVER_TO_CLIENT,
}])
.unwrap();
file.write_all(&conn_id.to_be_bytes()).unwrap();
file.write_all(&[server_name.len() as u8]).unwrap();
file.write_all(server_name).unwrap();
file.write_all(&(data.len() as u64).to_be_bytes()).unwrap();
file.write_all(&data).unwrap();
file.flush().unwrap();
}
pub struct Recorder {
file: std::fs::File,
receiver: Receiver<(u64, Option<String>, Direction, Vec<u8>)>,
@ -43,21 +78,7 @@ impl Recorder {
let Some(server_name) = server_name else {
continue;
};
let server_name = server_name.as_bytes();
self.file
.write_all(&[match direction {
Direction::ClientToServer => CLIENT_TO_SERVER,
Direction::ServerToClient => SERVER_TO_CLIENT,
}])
.unwrap();
self.file.write_all(&conn_id.to_be_bytes()).unwrap();
self.file.write_all(&[server_name.len() as u8]).unwrap();
self.file.write_all(server_name).unwrap();
self.file
.write_all(&(data.len() as u64).to_be_bytes())
.unwrap();
self.file.write_all(&data).unwrap();
self.file.flush().unwrap();
write_record(&mut self.file, direction, conn_id, &server_name, &data);
}
}
}
@ -186,11 +207,38 @@ pub fn read_record_file(path: &str) -> Records {
println!("Error: incomplete data. stop.");
break;
}
// Replace URL with unique id, to allow for better tracking by making each request unique.
// (proxy may modify some headers, but not the URL)
let mut insert_id = |req_id| {
if direction == Direction::ClientToServer {
let mut spaces = buf
.iter()
.enumerate()
.filter_map(|(i, c)| if *c == b' ' { Some(i) } else { None });
let s1 = spaces.next().unwrap();
let s2 = spaces.next().unwrap();
let new_url = format!("/{conn_id}-{req_id}/");
if s2 - s1 - 1 < new_url.len() {
// Not optimal but good enough
let mut new_buf = Vec::new();
new_buf.extend_from_slice(&buf[0..s1 + 1]);
new_buf.extend_from_slice(new_url.as_bytes());
new_buf.extend_from_slice(&buf[s2..]);
buf = new_buf;
} else {
buf[s1 + 1..s2][0..new_url.len()].copy_from_slice(new_url.as_bytes());
}
}
};
match records.entry(conn_id) {
btree_map::Entry::Occupied(mut entry) => {
(insert_id)(entry.get().1.len());
entry.get_mut().1.push((direction, buf));
}
btree_map::Entry::Vacant(entry) => {
(insert_id)(0);
entry.insert((server_name, vec![(direction, buf)]));
}
}
@ -198,8 +246,13 @@ pub fn read_record_file(path: &str) -> Records {
records
}
pub fn print_records(records: &Records, print_packets: bool) {
pub fn print_records(records: &Records, print_packets: bool, number: Option<u64>) {
for (id, (server_name, records)) in records {
if let Some(number) = number
&& number != *id
{
continue;
}
let server_name = str::from_utf8(server_name.as_slice()).unwrap();
println!("{id} {server_name}");
for (direction, data) in records {
@ -212,7 +265,7 @@ pub fn print_records(records: &Records, print_packets: bool) {
}
}
if print_packets {
let data_tr = if data.len() >= 256 && *direction == Direction::ServerToClient {
/*let data_tr = if data.len() >= 256 && *direction == Direction::ServerToClient {
&data[0..256]
} else {
data.as_slice()
@ -224,8 +277,21 @@ pub fn print_records(records: &Records, print_packets: bool) {
}
if let Some(header_end) = memchr::memmem::find(data, b"\r\n\r\n") {
println!(" --> body len: {}", data.len() - header_end - 4);
}
}*/
print_bin(&data[0..data.len().min(8192)]);
}
}
}
}
pub fn make_test_record(path: &str) {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.unwrap();
for (conn_id, server_name, direction, data) in TEST_RECORD {
write_record(&mut file, *direction, *conn_id, *server_name, *data);
}
}