#[cfg(test)]
mod security_tests {
use crate::message::body::HttpBody;
use crate::message::http_value::{HttpMethod, HttpVersion};
use crate::message::meta::HttpMeta;
use crate::message::start_line::RequestStartLine;
use crate::security::safety::HttpSafety;
use std::io::Cursor;
use tokio::io::BufReader;
#[test]
fn test_start_line_missing_http_version() {
let result = RequestStartLine::parse("GET /index.html");
assert!(
result.is_err(),
"Should reject start line without HTTP version"
);
assert_eq!(result.unwrap_err(), "Malformed request line");
}
#[test]
fn test_start_line_missing_request_target() {
let result = RequestStartLine::parse("GET HTTP/1.1");
assert!(
result.is_err(),
"Should reject start line without request target"
);
}
#[test]
fn test_start_line_missing_method() {
let result = RequestStartLine::parse("/index.html HTTP/1.1");
assert!(result.is_err(), "Should reject start line without method");
}
#[test]
fn test_start_line_only_method() {
let result = RequestStartLine::parse("GET");
assert!(result.is_err(), "Should reject start line with only method");
}
#[test]
fn test_start_line_empty() {
let result = RequestStartLine::parse("");
assert!(result.is_err(), "Should reject empty start line");
}
#[test]
fn test_start_line_extra_whitespace() {
let result = RequestStartLine::parse("GET /index.html HTTP/1.1");
assert!(result.is_ok());
let line = result.unwrap();
assert_eq!(line.path, "/index.html");
}
#[test]
fn test_start_line_invalid_method_name() {
let result = RequestStartLine::parse("INVALID_METHOD /index.html HTTP/1.1");
assert!(result.is_ok());
let line = result.unwrap();
assert_eq!(line.method, HttpMethod::UNKNOWN);
}
#[test]
fn test_start_line_lowercase_method() {
let result = RequestStartLine::parse("get /index.html HTTP/1.1");
assert!(result.is_ok());
let line = result.unwrap();
assert_eq!(line.path, "/index.html");
}
#[test]
fn test_start_line_invalid_http_version() {
let result = RequestStartLine::parse("GET /index.html HTTP/3.0");
assert!(result.is_ok());
let line = result.unwrap();
assert_eq!(line.path, "/index.html");
}
#[test]
fn test_start_line_malformed_http_version() {
let result = RequestStartLine::parse("GET /index.html HTTPX");
assert!(result.is_ok());
let line = result.unwrap();
assert_eq!(line.path, "/index.html");
}
#[test]
fn test_start_line_crlf_injection_in_method() {
let result = RequestStartLine::parse("GET\r\nInjected: header\r\n /index.html HTTP/1.1");
assert!(result.is_err(), "Should reject CRLF in method");
}
#[test]
fn test_start_line_crlf_injection_in_path() {
let result = RequestStartLine::parse("GET /index.html\r\nInjected: header\r\n HTTP/1.1");
assert!(result.is_err(), "Should reject CRLF in path");
}
#[test]
fn test_start_line_null_byte_in_path() {
let result = RequestStartLine::parse("GET /index\0.html HTTP/1.1");
assert!(result.is_ok());
let line = result.unwrap();
assert!(line.path.contains('\0'), "Path contains null byte");
}
#[test]
fn test_start_line_too_many_parts() {
let result = RequestStartLine::parse("GET /index.html HTTP/1.1 EXTRA");
assert!(
result.is_err(),
"Should reject start line with too many parts"
);
}
#[test]
fn test_start_line_unicode_method() {
let result = RequestStartLine::parse("GÉT /index.html HTTP/1.1");
assert!(result.is_ok());
let line = result.unwrap();
assert_eq!(line.method, HttpMethod::UNKNOWN);
}
#[tokio::test]
async fn test_header_crlf_injection_in_value() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
let safety = HttpSafety::default();
let headers = b"Host: example.com\r\nUser-Agent: Test\r\nInjected: header\r\n\r\n";
let cursor = Cursor::new(headers.to_vec());
let mut reader = BufReader::new(cursor);
let result = meta
.append_from_request_stream(&mut reader, &safety, true)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_header_null_byte_injection() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
let safety = HttpSafety::default();
let headers = b"Host: example.com\0malicious.com\r\n\r\n";
let cursor = Cursor::new(headers.to_vec());
let mut reader = BufReader::new(cursor);
let result = meta
.append_from_request_stream(&mut reader, &safety, true)
.await;
assert!(result.is_ok());
if let Some(host) = meta.header.get("Host") {
match host {
crate::message::meta::HeaderValue::Single(s) => {
assert!(s.contains('\0'), "Header contains null byte");
}
crate::message::meta::HeaderValue::Multiple(v) => {
assert!(
v.iter().any(|s| s.contains('\0')),
"Header contains null byte"
);
}
}
}
}
#[tokio::test]
async fn test_header_oversized_header_name() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
let safety = HttpSafety::default().with_max_header_size(1024);
let long_name = "X-".to_string() + &"A".repeat(2048);
let headers = format!("{}: value\r\n\r\n", long_name);
let cursor = Cursor::new(headers.as_bytes().to_vec());
let mut reader = BufReader::new(cursor);
let result = meta
.append_from_request_stream(&mut reader, &safety, true)
.await;
assert!(result.is_err(), "Should reject oversized header name");
}
#[tokio::test]
async fn test_header_oversized_header_value() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
let safety = HttpSafety::default().with_max_header_size(1024);
let long_value = "A".repeat(10240);
let headers = format!("X-Large: {}\r\n\r\n", long_value);
let cursor = Cursor::new(headers.as_bytes().to_vec());
let mut reader = BufReader::new(cursor);
let result = meta
.append_from_request_stream(&mut reader, &safety, true)
.await;
assert!(result.is_err(), "Should reject oversized header value");
}
#[tokio::test]
async fn test_header_many_headers_exceeding_limit() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
let safety = HttpSafety::default().with_max_header_size(2048);
let mut headers = String::new();
for i in 0..100 {
headers.push_str(&format!("X-Header-{}: value-{}\r\n", i, i));
}
headers.push_str("\r\n");
let cursor = Cursor::new(headers.as_bytes().to_vec());
let mut reader = BufReader::new(cursor);
let result = meta
.append_from_request_stream(&mut reader, &safety, true)
.await;
assert!(
result.is_err(),
"Should reject too many headers exceeding size limit"
);
}
#[tokio::test]
async fn test_header_duplicate_host_header() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
let safety = HttpSafety::default();
let headers = b"Host: example.com\r\nHost: malicious.com\r\n\r\n";
let cursor = Cursor::new(headers.to_vec());
let mut reader = BufReader::new(cursor);
let result = meta
.append_from_request_stream(&mut reader, &safety, true)
.await;
assert!(result.is_ok());
if let Some(host) = meta.header.get("Host") {
assert!(host.len() > 0);
}
}
#[tokio::test]
async fn test_header_duplicate_content_length() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
let safety = HttpSafety::default();
let headers = b"Content-Length: 10\r\nContent-Length: 20\r\n\r\n";
let cursor = Cursor::new(headers.to_vec());
let mut reader = BufReader::new(cursor);
let result = meta
.append_from_request_stream(&mut reader, &safety, true)
.await;
assert!(result.is_ok());
assert!(meta.get_content_length().is_some());
}
#[tokio::test]
async fn test_header_line_folding() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
let safety = HttpSafety::default();
let request = b"GET / HTTP/1.1\r\nX-Long-Header: part1\r\n part2\r\n\r\n";
let cursor = Cursor::new(request.to_vec());
let mut reader = BufReader::new(cursor);
let result = meta
.append_from_request_stream(&mut reader, &safety, false)
.await;
assert!(result.is_ok());
println!("Headers parsed: {:?}", meta.header);
if let Some(header_value) = meta.header.get("x-long-header") {
let value = header_value.first();
println!("X-Long-Header value: {:?}", value);
assert_eq!(value, "part1", "Line folding was rejected");
} else {
assert_eq!(meta.header.len(), 0, "Parser rejects line folded headers");
}
}
#[tokio::test]
async fn test_header_no_colon_separator() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
let safety = HttpSafety::default();
let headers = b"InvalidHeader\r\n\r\n";
let cursor = Cursor::new(headers.to_vec());
let mut reader = BufReader::new(cursor);
let result = meta
.append_from_request_stream(&mut reader, &safety, true)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_header_control_characters() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
let safety = HttpSafety::default();
let headers = b"X-Control: value\x01\x02\x03\r\n\r\n";
let cursor = Cursor::new(headers.to_vec());
let mut reader = BufReader::new(cursor);
let result = meta
.append_from_request_stream(&mut reader, &safety, true)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_chunked_invalid_hex_size() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let body_data = b"GGGG\r\ndata\r\n0\r\n\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(
result.is_err(),
"Parser correctly rejects non-hex chunk size"
);
}
#[tokio::test]
async fn test_chunked_negative_size() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let body_data = b"-10\r\ndata\r\n0\r\n\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(result.is_err(), "Should reject negative chunk size");
}
#[tokio::test]
async fn test_chunked_size_overflow() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let body_data = b"FFFFFFFFFFFFFFFF\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(result.is_err(), "Should reject overflow-sized chunk");
}
#[tokio::test]
async fn test_chunked_missing_crlf_after_size() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let body_data = b"5data\r\n0\r\n\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(
result.is_err() || result.is_ok(),
"Behavior depends on parser"
);
}
#[tokio::test]
async fn test_chunked_missing_crlf_after_data() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let body_data = b"4\r\ndata0\r\n\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(
result.is_err(),
"Should reject missing CRLF after chunk data"
);
}
#[tokio::test]
async fn test_chunked_only_lf_terminator() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let body_data = b"4\ndata\n0\n\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(result.is_err(), "Should reject LF-only terminators");
}
#[tokio::test]
async fn test_chunked_size_exceeds_body_limit() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default().with_max_body_size(100);
let body_data = b"200\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(
result.is_err(),
"Should reject chunk exceeding body size limit"
);
}
#[tokio::test]
async fn test_chunked_cumulative_size_exceeds_limit() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default().with_max_body_size(50);
let body_data = b"1E\r\n012345678901234567890123456789\r\n1E\r\n012345678901234567890123456789\r\n0\r\n\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(
result.is_err(),
"Should reject cumulative size exceeding limit"
);
}
#[tokio::test]
async fn test_chunked_zero_size_not_last() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let body_data = b"0\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_chunked_trailer_header_injection() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let body_data = b"5\r\nhello\r\n0\r\nX-Injected: malicious\r\nX-Evil: header\r\n\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_chunked_chunk_extension_overflow() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let extension = "x".repeat(10000);
let body_data = format!("5;{}\r\nhello\r\n0\r\n\r\n", extension);
let cursor = Cursor::new(body_data.as_bytes().to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(result.is_ok() || result.is_err());
}
#[tokio::test]
async fn test_chunked_no_final_zero_chunk() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let body_data = b"5\r\nhello\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(result.is_err(), "Should reject missing final zero chunk");
}
#[tokio::test]
async fn test_chunked_valid_simple() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let body_data = b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(result.is_ok(), "Valid chunked encoding should succeed");
if let Ok(HttpBody::Buffer { data, .. }) = result {
assert_eq!(data.len(), 11); }
}
#[tokio::test]
async fn test_chunked_empty_chunks() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let body_data = b"0\r\n\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_chunked_uppercase_hex() {
let mut meta = HttpMeta::new(Default::default(), Default::default());
meta.header
.insert("transfer-encoding".to_string(), "chunked".into());
let safety = HttpSafety::default();
let body_data = b"A\r\n0123456789\r\n0\r\n\r\n";
let cursor = Cursor::new(body_data.to_vec());
let mut reader = BufReader::new(cursor);
let result = HttpBody::read_buffer(&mut reader, &mut meta, &safety).await;
assert!(result.is_ok());
}
}