use std::fs;
use std::time::Duration;
use mini_static::Server;
use tempfile::TempDir;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
const HEADER_TIMEOUT: Duration = Duration::from_millis(100);
const PAST_TIMEOUT: Duration = Duration::from_millis(400);
async fn serve() -> (u16, TempDir, mini_static::ServerHandle) {
let root = TempDir::new().unwrap();
fs::write(root.path().join("test.txt"), b"hello world").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, handle) = server.run(HEADER_TIMEOUT).await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
(port, root, handle)
}
async fn read_one_response(stream: &mut TcpStream) -> String {
let mut buf = vec![0u8; 4096];
let mut seen = Vec::new();
loop {
let n = stream.read(&mut buf).await.unwrap();
assert_ne!(n, 0, "connection closed mid-response");
seen.extend_from_slice(&buf[..n]);
if seen.windows(4).any(|w| w == b"\r\n\r\n") && seen.ends_with(b"hello world") {
return String::from_utf8_lossy(&seen).into_owned();
}
}
}
#[tokio::test]
async fn a_stall_on_the_second_request_of_a_connection_still_times_out() {
let (port, _root, _handle) = serve().await;
let mut stream = TcpStream::connect(format!("127.0.0.1:{port}"))
.await
.unwrap();
stream
.write_all(b"GET /test.txt HTTP/1.1\r\nHost: localhost\r\n\r\n")
.await
.unwrap();
let first = read_one_response(&mut stream).await;
assert!(
first.starts_with("HTTP/1.1 200"),
"first request should serve: {first}"
);
stream
.write_all(b"GET /test.txt HTTP/1.1\r\n")
.await
.unwrap();
tokio::time::sleep(PAST_TIMEOUT).await;
let mut buf = vec![0u8; 1024];
let n = stream.read(&mut buf).await.unwrap_or(0);
let tail = String::from_utf8_lossy(&buf[..n]);
assert!(
n == 0 || tail.starts_with("HTTP/1.1 408") || tail.starts_with("HTTP/1.1 4"),
"a stalled second request must be closed or refused, got: {tail}"
);
}
#[tokio::test]
async fn an_oversized_header_on_the_second_request_is_still_rejected() {
let (port, _root, _handle) = serve().await;
let mut stream = TcpStream::connect(format!("127.0.0.1:{port}"))
.await
.unwrap();
stream
.write_all(b"GET /test.txt HTTP/1.1\r\nHost: localhost\r\n\r\n")
.await
.unwrap();
let first = read_one_response(&mut stream).await;
assert!(first.starts_with("HTTP/1.1 200"), "first request: {first}");
stream
.write_all(b"GET /test.txt HTTP/1.1\r\nX-Big: ")
.await
.unwrap();
let filler = vec![b'a'; 8 * 1024];
let mut wrote_all = true;
for _ in 0..16 {
if stream.write_all(&filler).await.is_err() {
wrote_all = false;
break;
}
}
let mut buf = vec![0u8; 1024];
let n = stream.read(&mut buf).await.unwrap_or(0);
let tail = String::from_utf8_lossy(&buf[..n]);
assert!(
!wrote_all || n == 0 || tail.starts_with("HTTP/1.1 4"),
"an oversized header block on a keep-alive connection must be rejected, got: {tail}"
);
}
#[tokio::test]
async fn sequential_requests_on_one_connection_all_serve() {
let (port, _root, _handle) = serve().await;
let mut stream = TcpStream::connect(format!("127.0.0.1:{port}"))
.await
.unwrap();
for i in 0..3 {
stream
.write_all(b"GET /test.txt HTTP/1.1\r\nHost: localhost\r\n\r\n")
.await
.unwrap();
let response = read_one_response(&mut stream).await;
assert!(
response.starts_with("HTTP/1.1 200"),
"request {i} on a reused connection should serve: {response}"
);
}
}