#![cfg(feature = "http")]
#![forbid(unsafe_code)]
use crate::rng;
#[cfg(feature = "http")]
mod sim {
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use lgwks_std::http::{
self, BodyPolicy, Error, FailureKind, FailureStage, Options, Truncation,
};
use crate::sim_http::rng::Rng;
fn read_request_head(stream: &mut TcpStream) -> std::io::Result<Vec<u8>> {
let mut request = [0_u8; 1024];
let mut head = Vec::new();
loop {
let read = stream.read(&mut request)?;
if read == 0 {
break;
}
head.extend_from_slice(&request[..read]);
if head.windows(4).any(|window| window == b"\r\n\r\n") {
break;
}
}
Ok(head)
}
const CEILINGS: [usize; 4] = [73, 1000, 3003, 10_000];
const SEEDS: u64 = 48;
fn serve_once(listener: &TcpListener, reply: &[u8]) -> std::io::Result<Vec<u8>> {
let (mut stream, _) = listener.accept()?;
let head = read_request_head(&mut stream)?;
stream.write_all(reply)?;
Ok(head)
}
fn response(content_length: usize, body: &[u8]) -> Vec<u8> {
let mut reply = format!(
"HTTP/1.1 200 OK\r\nContent-Length: {content_length}\r\nConnection: close\r\n\r\n"
)
.into_bytes();
reply.extend_from_slice(body);
reply
}
struct CeilingScript {
ceiling: usize,
body_len: usize,
preview: bool,
}
fn ceiling_script(seed: u64) -> CeilingScript {
let mut rng = Rng::new(seed);
let ceiling = CEILINGS[rng.below(CEILINGS.len())];
let body_len = match rng.below(5) {
0 => ceiling,
1 => ceiling.saturating_sub(1),
2 => ceiling.saturating_add(1),
3 => ceiling.saturating_sub(rng.below(8).saturating_add(1)),
_ => ceiling.saturating_mul(2),
};
CeilingScript {
ceiling,
body_len: body_len.max(1),
preview: rng.below(2) == 1,
}
}
#[test]
fn seeded_ceiling_families_match_the_declared_outcome() -> Result<(), Box<dyn std::error::Error>>
{
for seed in 0..SEEDS {
let script = ceiling_script(seed);
let listener = TcpListener::bind("127.0.0.1:0")?;
let port = listener.local_addr()?.port();
let body = vec![b'x'; script.body_len];
let reply = response(script.body_len, &body);
let url = format!("http://127.0.0.1:{port}/");
let options = Options::default()
.max_body_bytes(script.ceiling)
.body_policy(if script.preview {
BodyPolicy::Preview
} else {
BodyPolicy::Whole
});
let result = std::thread::scope(|scope| {
scope.spawn(|| serve_once(&listener, &reply));
http::get_with(&url, &options)
});
let over_ceiling = script.body_len > script.ceiling;
match result {
Ok(response) => {
assert!(
!over_ceiling || script.preview,
"seed {seed}: a {}-byte body past a {}-byte ceiling under Whole must be \
refused, not returned",
script.body_len,
script.ceiling
);
let expected_len = script.body_len.min(script.ceiling);
assert_eq!(
response.body().len(),
expected_len,
"seed {seed}: body {} ceiling {} policy preview={}",
script.body_len,
script.ceiling,
script.preview
);
let expected_truncation = if over_ceiling {
Truncation::Cut
} else {
Truncation::Complete
};
assert_eq!(
response.truncation, expected_truncation,
"seed {seed}: truncation must be a fact about the body, not the policy"
);
}
Err(error) => {
assert!(
!script.preview && over_ceiling,
"seed {seed}: a body within the ceiling must not be refused: {error:?}"
);
assert_eq!(
error,
Error::BodyTooLarge {
limit: script.ceiling
},
"seed {seed}: an over-ceiling whole body is refused with its limit"
);
}
}
}
let first: Vec<usize> = (0..SEEDS)
.map(|seed| ceiling_script(seed).body_len)
.collect();
let second: Vec<usize> = (0..SEEDS)
.map(|seed| ceiling_script(seed).body_len)
.collect();
assert_eq!(
first, second,
"the same seed must derive the same script on every replay"
);
Ok(())
}
struct TornScript {
content_length: usize,
sent: usize,
chunks: Vec<usize>,
}
fn torn_script(seed: u64) -> TornScript {
let mut rng = Rng::new(seed ^ 0x7011_0000);
let content_length = CEILINGS[rng.below(CEILINGS.len())];
let complete = rng.below(2) == 1;
let sent = if complete {
content_length
} else {
rng.below(content_length)
};
let chunk_count = rng.below(4).saturating_add(1);
let chunks = (0..chunk_count)
.map(|_| rng.below(256).saturating_add(1))
.collect();
TornScript {
content_length,
sent,
chunks,
}
}
fn serve_torn(listener: &TcpListener, script: &TornScript) -> std::io::Result<()> {
let (mut stream, _) = listener.accept()?;
let _head = read_request_head(&mut stream)?;
let header = format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
script.content_length
);
stream.write_all(header.as_bytes())?;
let mut written = 0_usize;
let mut index = 0_usize;
let pieces = script.chunks.len().max(1);
while written < script.sent {
let piece = script
.chunks
.get(index.checked_rem(pieces).unwrap_or(0))
.copied()
.unwrap_or(1);
let take = piece.min(script.sent.saturating_sub(written)).max(1);
stream.write_all(&vec![b'x'; take])?;
written = written.saturating_add(take);
index = index.saturating_add(1);
}
Ok(())
}
#[test]
fn a_seeded_torn_transport_is_never_a_complete_body() -> Result<(), Box<dyn std::error::Error>>
{
for seed in 0..SEEDS {
let script = torn_script(seed);
let listener = TcpListener::bind("127.0.0.1:0")?;
let port = listener.local_addr()?.port();
let url = format!("http://127.0.0.1:{port}/");
let options = Options::default().max_body_bytes(script.content_length);
let result = std::thread::scope(|scope| {
scope.spawn(|| serve_torn(&listener, &script));
http::get_with(&url, &options)
});
if script.sent == script.content_length {
let response = result.map_err(|error| {
format!("seed {seed}: a complete body must not error: {error:?}")
})?;
assert_eq!(
response.body().len(),
script.content_length,
"seed {seed}: every declared body byte is retained"
);
assert_eq!(
response.truncation,
Truncation::Complete,
"seed {seed}: a body that ended at its declared length is complete"
);
} else {
let error = result.err().ok_or_else(|| {
format!(
"seed {seed}: a body torn at {} of {} bytes must be refused",
script.sent, script.content_length
)
})?;
assert!(
matches!(
error,
Error::Failure {
stage: FailureStage::Body,
kind: FailureKind::Transport,
..
}
),
"seed {seed}: a torn transport is a transport failure at the body stage, got \
{error:?}"
);
}
}
Ok(())
}
fn tenant_request(url: &str, id: &str) -> Result<u16, Error> {
let options = Options::default().header("X-Tenant", id).max_body_bytes(64);
http::get_with(url, &options).map(|response| response.status)
}
fn serve_many(listener: &TcpListener, reply: &[u8], count: usize) -> Vec<Vec<u8>> {
let mut heads = Vec::new();
for _ in 0..count {
match serve_once(listener, reply) {
Ok(head) => heads.push(head),
Err(_) => break,
}
}
heads
}
#[test]
fn two_tenants_on_one_endpoint_stay_isolated() -> Result<(), Box<dyn std::error::Error>> {
for seed in 0..SEEDS {
let mut rng = Rng::new(seed ^ 0x7e11_0000);
let left_id = format!("tenant-{}-left", rng.below(1_000));
let mut right_id = format!("tenant-{}-right", rng.below(1_000));
if right_id == left_id {
right_id.push('!');
}
let listener = TcpListener::bind("127.0.0.1:0")?;
let port = listener.local_addr()?.port();
let reply = response(2, b"ok");
let url = format!("http://127.0.0.1:{port}/");
let (left_status, right_status, heads) =
std::thread::scope(|scope| -> Result<_, Box<dyn std::error::Error>> {
let server = scope.spawn(|| serve_many(&listener, &reply, 2));
let left_url = url.clone();
let left_tenant = left_id.clone();
let left = scope.spawn(move || tenant_request(&left_url, &left_tenant));
let right_tenant = right_id.clone();
let right = scope.spawn(move || tenant_request(&url, &right_tenant));
let left_status = left.join().map_err(|_| "left tenant panicked")??;
let right_status = right.join().map_err(|_| "right tenant panicked")??;
let heads = server.join().map_err(|_| "tenant server panicked")?;
Ok((left_status, right_status, heads))
})?;
assert_eq!(
(left_status, right_status),
(200, 200),
"seed {seed}: both tenants reach the endpoint"
);
let mut seen = Vec::new();
for head in &heads {
let text = String::from_utf8_lossy(head).to_ascii_lowercase();
let values: Vec<String> = text
.lines()
.filter_map(|line| line.split_once(':'))
.filter(|entry| entry.0.trim() == "x-tenant")
.map(|entry| entry.1.trim().to_owned())
.collect();
assert_eq!(
values.len(),
1,
"seed {seed}: each request carries exactly one tenant header, saw {values:?}"
);
seen.push(values[0].clone());
}
seen.sort();
let mut expected = vec![left_id.to_ascii_lowercase(), right_id.to_ascii_lowercase()];
expected.sort();
assert_eq!(
seen, expected,
"seed {seed}: each tenant's request carries only its own value"
);
}
Ok(())
}
}