use net::http::{Response, send};
use std::io::{self, Cursor, Read, Write};
struct FakeStream {
resp: Cursor<Vec<u8>>,
sink: Vec<u8>,
}
impl FakeStream {
fn new(resp: &str) -> FakeStream {
FakeStream {
resp: Cursor::new(resp.as_bytes().to_vec()),
sink: Vec::new(),
}
}
}
impl Read for FakeStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.resp.read(buf)
}
}
impl Write for FakeStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.sink.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn parse(resp: &str) -> io::Result<Response> {
let mut stream = FakeStream::new(resp);
send(&mut stream, "peer", "POST", "/mcp", &[], b"{}")
}
#[test]
fn chunk_size_near_usize_max_is_rejected_not_overflowed() {
let huge = format!("{:x}", usize::MAX - 1);
let resp =
format!("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n{huge}\r\n");
let err = parse(&resp).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(
err.to_string().contains("cap"),
"expected the cap rejection, got {err}"
);
}
#[test]
fn lone_usize_max_chunk_size_is_rejected() {
let huge = format!("{:x}", usize::MAX);
let resp = format!("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n{huge}\r\n");
let err = parse(&resp).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn chunk_size_over_the_cap_is_rejected() {
let resp = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n900000\r\n";
let err = parse(resp).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn chunk_shorter_than_its_declared_size_is_rejected() {
let resp = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n100\r\nonly-a-few\r\n";
let err = parse(resp).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
}
#[test]
fn cr_lf_in_request_target_is_refused_before_anything_is_written() {
let mut stream = FakeStream::new("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
let err = send(
&mut stream,
"peer",
"POST",
"/mcp\r\nAuthorization: Bearer attacker\r\nX: ",
&[("Authorization", "Bearer operator-secret")],
b"{}",
)
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert!(
stream.sink.is_empty(),
"request bytes escaped: {:?}",
String::from_utf8_lossy(&stream.sink)
);
}
#[test]
fn lone_lf_in_request_target_is_refused() {
let mut stream = FakeStream::new("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
let err = send(&mut stream, "peer", "GET", "/a\nX: 1", &[], b"").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn well_formed_chunked_body_still_parses() {
let resp = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n\
5\r\nhello\r\n6;ext=1\r\n world\r\n0\r\nX-Trailer: t\r\n\r\n";
let parsed = parse(resp).unwrap();
assert_eq!(parsed.status, 200);
assert_eq!(parsed.body, b"hello world");
}
#[test]
fn chunked_body_exactly_at_the_cap_still_parses() {
let n = net::http::MAX_RESPONSE;
let mut resp = format!("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n{n:x}\r\n");
resp.push_str(&"a".repeat(n));
resp.push_str("\r\n0\r\n\r\n");
let parsed = parse(&resp).unwrap();
assert_eq!(parsed.body.len(), n);
}