diff --git a/README.md b/README.md index 389d48a6..39b49d43 100644 --- a/README.md +++ b/README.md @@ -2124,7 +2124,10 @@ Use `HttpServer::bind` directly when you already want the server type, connection, and `serve_requests` for a fixed number of sequential connections. Use `with_read_timeout` and `with_write_timeout` to apply socket-level timeouts to each accepted connection; pass `None` to leave the corresponding -socket timeout unset. Add `Transfer-Encoding: chunked` to an `HttpResponse` to +socket timeout unset. Use `with_max_request_head_bytes` to set the maximum +HTTP request-head size accepted for HTTP/1 parsing and h2c upgrade handoff; +the default remains 64 KiB, and zero is rejected before serving. Add +`Transfer-Encoding: chunked` to an `HttpResponse` to write the complete response body with HTTP/1.x chunked transfer framing instead of an automatic `Content-Length`; response trailers added with `HttpResponse::trailer` are written after the terminating zero-size chunk. Add a diff --git a/crates/rttp-server/src/server/connection.rs b/crates/rttp-server/src/server/connection.rs index bb751d2d..737c9fa9 100644 --- a/crates/rttp-server/src/server/connection.rs +++ b/crates/rttp-server/src/server/connection.rs @@ -5,6 +5,7 @@ pub struct HttpServer { pub(crate) read_timeout: Option, pub(crate) write_timeout: Option, pub(crate) max_request_body_bytes: usize, + pub(crate) max_request_head_bytes: usize, pub(crate) http2_policy: Http2ServerPolicy, } @@ -42,6 +43,7 @@ impl HttpServer { read_timeout: None, write_timeout: None, max_request_body_bytes: MAX_REQUEST_BODY_BYTES, + max_request_head_bytes: MAX_REQUEST_HEAD_BYTES, http2_policy: Http2ServerPolicy::default(), }); } @@ -74,6 +76,21 @@ impl HttpServer { self } + /// Sets the maximum number of HTTP request-head bytes accepted per request. + /// + /// The default is 64 KiB. The same limit is enforced for HTTP/1 parsing and + /// h2c upgrade request-head parsing. Zero is rejected before serving. + pub fn with_max_request_head_bytes(mut self, max_request_head_bytes: usize) -> io::Result { + if max_request_head_bytes == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "max request head bytes must be greater than zero", + )); + } + self.max_request_head_bytes = max_request_head_bytes; + Ok(self) + } + /// Sets the fixed bounds advertised and enforced for accepted h2c connections. pub fn with_http2_policy(mut self, policy: Http2ServerPolicy) -> Self { self.http2_policy = policy; @@ -139,6 +156,7 @@ impl HttpServer { let request = match self.normalize_connection_error(Request::read_next_from_with_continue( &mut reader, self.max_request_body_bytes, + self.max_request_head_bytes, )) { Ok(Some(request)) => request, Ok(None) => break, @@ -234,12 +252,14 @@ impl HttpServer { }; let mut reader = BufReader::new(stream); let request = match self.normalize_connection_error( - Request::read_next_from_with_continue(&mut reader, self.max_request_body_bytes).and_then( - |request| { - request - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "incomplete HTTP request")) - }, - ), + Request::read_next_from_with_continue( + &mut reader, + self.max_request_body_bytes, + self.max_request_head_bytes, + ) + .and_then(|request| { + request.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "incomplete HTTP request")) + }), ) { Ok(request) => request, Err(err) if is_expectation_failed_error(&err) => { @@ -296,11 +316,14 @@ impl HttpServer { self.configure_stream(&stream)?; let mut reader = BufReader::new(stream); let (request, body_kind) = match self.normalize_connection_error( - Request::read_next_head_from_with_continue(&mut reader, self.max_request_body_bytes) - .and_then(|request| { - request - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "incomplete HTTP request")) - }), + Request::read_next_head_from_with_continue( + &mut reader, + self.max_request_body_bytes, + self.max_request_head_bytes, + ) + .and_then(|request| { + request.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "incomplete HTTP request")) + }), ) { Ok(request) => request, Err(err) if is_expectation_failed_error(&err) => { @@ -342,12 +365,16 @@ impl HttpServer { self.configure_stream(&stream)?; let mut reader = BufReader::new(stream); let request = match self.normalize_connection_error( - Request::read_next_head_from_with_continue(&mut reader, self.max_request_body_bytes) - .and_then(|request| { - request - .map(|(request, _)| request) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "incomplete HTTP request")) - }), + Request::read_next_head_from_with_continue( + &mut reader, + self.max_request_body_bytes, + self.max_request_head_bytes, + ) + .and_then(|request| { + request + .map(|(request, _)| request) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "incomplete HTTP request")) + }), ) { Ok(request) => request, Err(err) if is_expectation_failed_error(&err) => { diff --git a/crates/rttp-server/src/server/http1.rs b/crates/rttp-server/src/server/http1.rs index 74bc3c74..a82665b4 100644 --- a/crates/rttp-server/src/server/http1.rs +++ b/crates/rttp-server/src/server/http1.rs @@ -151,8 +151,11 @@ pub(crate) fn find_header_end(raw: &[u8]) -> Option { raw.windows(4).position(|window| window == b"\r\n\r\n") } -pub(crate) fn reject_oversized_request_head(length: usize) -> io::Result<()> { - if length > MAX_REQUEST_HEAD_BYTES { +pub(crate) fn reject_oversized_request_head( + length: usize, + max_request_head_bytes: usize, +) -> io::Result<()> { + if length > max_request_head_bytes { Err(io::Error::new( io::ErrorKind::InvalidData, "request head is too large", diff --git a/crates/rttp-server/src/server/request.rs b/crates/rttp-server/src/server/request.rs index c50bfdd2..fd4e4458 100644 --- a/crates/rttp-server/src/server/request.rs +++ b/crates/rttp-server/src/server/request.rs @@ -1167,6 +1167,7 @@ impl Request { pub(crate) fn read_next_from_with_continue( reader: &mut BufReader, max_request_body_bytes: usize, + max_request_head_bytes: usize, ) -> io::Result> where S: Read + Write, @@ -1183,6 +1184,7 @@ impl Request { return Ok(Some(Self::from_raw_frame_with_body_kind( &raw, RequestBodyKind::ContentLength(content_length), + max_request_head_bytes, )?)); } } @@ -1225,7 +1227,7 @@ impl Request { match find_header_end(&combined) { Some(header_end) => { let take = header_end + 4 - raw.len(); - reject_oversized_request_head(header_end + 4)?; + reject_oversized_request_head(header_end + 4, max_request_head_bytes)?; raw.extend_from_slice(&available[..take]); reader.consume(take); let head = parse_request_head(&raw[..header_end])?; @@ -1256,7 +1258,7 @@ impl Request { } None => { let take = available.len(); - reject_oversized_request_head(raw.len().saturating_add(take))?; + reject_oversized_request_head(raw.len().saturating_add(take), max_request_head_bytes)?; raw.extend_from_slice(available); reader.consume(take); } @@ -1267,24 +1269,28 @@ impl Request { pub(crate) fn read_next_head_from_with_continue( reader: &mut BufReader, max_request_body_bytes: usize, + max_request_head_bytes: usize, ) -> io::Result> where S: Read + Write, { - Self::read_next_head_and_body_kind_from_with_continue(reader, max_request_body_bytes)?.map_or( - Ok(None), - |(head, kind)| { - Ok(Some(( - Self::from_head_body_kind_and_trailers(head, Vec::new(), kind, Vec::new()), - kind, - ))) - }, - ) + Self::read_next_head_and_body_kind_from_with_continue( + reader, + max_request_body_bytes, + max_request_head_bytes, + )? + .map_or(Ok(None), |(head, kind)| { + Ok(Some(( + Self::from_head_body_kind_and_trailers(head, Vec::new(), kind, Vec::new()), + kind, + ))) + }) } pub(crate) fn read_next_head_and_body_kind_from_with_continue( reader: &mut BufReader, max_request_body_bytes: usize, + max_request_head_bytes: usize, ) -> io::Result> where S: Read + Write, @@ -1308,7 +1314,7 @@ impl Request { match find_header_end(&combined) { Some(header_end) => { let take = header_end + 4 - raw.len(); - reject_oversized_request_head(header_end + 4)?; + reject_oversized_request_head(header_end + 4, max_request_head_bytes)?; raw.extend_from_slice(&available[..take]); reader.consume(take); let head = parse_request_head(&raw[..header_end])?; @@ -1323,7 +1329,7 @@ impl Request { } None => { let take = available.len(); - reject_oversized_request_head(raw.len().saturating_add(take))?; + reject_oversized_request_head(raw.len().saturating_add(take), max_request_head_bytes)?; raw.extend_from_slice(available); reader.consume(take); } @@ -1348,6 +1354,7 @@ impl Request { return Ok(Some(Self::from_raw_frame_with_body_kind( &raw, RequestBodyKind::ContentLength(content_length), + MAX_REQUEST_HEAD_BYTES, )?)); } } @@ -1390,7 +1397,7 @@ impl Request { match find_header_end(&combined) { Some(header_end) => { let take = header_end + 4 - raw.len(); - reject_oversized_request_head(header_end + 4)?; + reject_oversized_request_head(header_end + 4, MAX_REQUEST_HEAD_BYTES)?; raw.extend_from_slice(&available[..take]); reader.consume(take); let head = parse_request_head(&raw[..header_end])?; @@ -1421,7 +1428,7 @@ impl Request { } None => { let take = available.len(); - reject_oversized_request_head(raw.len().saturating_add(take))?; + reject_oversized_request_head(raw.len().saturating_add(take), MAX_REQUEST_HEAD_BYTES)?; raw.extend_from_slice(available); reader.consume(take); } @@ -1433,16 +1440,20 @@ impl Request { pub(crate) fn from_raw_frame(raw: &[u8]) -> io::Result { let header_end = find_header_end(raw) .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "incomplete HTTP request"))?; - reject_oversized_request_head(header_end + 4)?; + reject_oversized_request_head(header_end + 4, MAX_REQUEST_HEAD_BYTES)?; let head = parse_request_head(&raw[..header_end])?; let body_kind = request_body_kind(&head.headers)?; Self::from_raw_frame_with_head_and_body_kind(raw, header_end, head, body_kind) } - fn from_raw_frame_with_body_kind(raw: &[u8], body_kind: RequestBodyKind) -> io::Result { + fn from_raw_frame_with_body_kind( + raw: &[u8], + body_kind: RequestBodyKind, + max_request_head_bytes: usize, + ) -> io::Result { let header_end = find_header_end(raw) .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "incomplete HTTP request"))?; - reject_oversized_request_head(header_end + 4)?; + reject_oversized_request_head(header_end + 4, max_request_head_bytes)?; let head = parse_request_head(&raw[..header_end])?; Self::from_raw_frame_with_head_and_body_kind(raw, header_end, head, body_kind) } @@ -2057,7 +2068,8 @@ impl HttpRequest { pub fn parse(raw: &[u8]) -> Result { let header_end = find_header_end(raw) .ok_or_else(|| HttpParseError::new("request is missing header terminator"))?; - reject_oversized_request_head(header_end + 4).map_err(HttpParseError::from_io_error)?; + reject_oversized_request_head(header_end + 4, MAX_REQUEST_HEAD_BYTES) + .map_err(HttpParseError::from_io_error)?; let head = parse_request_head(&raw[..header_end]).map_err(HttpParseError::from_io_error)?; let body_bytes = &raw[(header_end + 4)..]; diff --git a/crates/rttp-server/src/server/server_tests.rs b/crates/rttp-server/src/server/server_tests.rs index a7df10ad..3b2b18cb 100644 --- a/crates/rttp-server/src/server/server_tests.rs +++ b/crates/rttp-server/src/server/server_tests.rs @@ -1,5 +1,30 @@ use std::net::TcpStream as StdTcpStream; +#[test] +fn with_max_request_head_bytes_rejects_zero_before_serving() { + let error = match HttpServer::bind("127.0.0.1:0") + .expect("bind server") + .with_max_request_head_bytes(0) + { + Ok(_) => panic!("zero request-head limit must fail"), + Err(error) => error, + }; + assert_eq!(io::ErrorKind::InvalidInput, error.kind()); + assert_eq!( + "max request head bytes must be greater than zero", + error.to_string() + ); +} + +#[test] +fn with_max_request_head_bytes_accepts_nonzero_limit() { + let server = HttpServer::bind("127.0.0.1:0") + .expect("bind server") + .with_max_request_head_bytes(1) + .expect("nonzero request-head limit must be accepted"); + assert_eq!(1, server.max_request_head_bytes); +} + #[test] fn request_cache_control_combines_case_insensitive_header_fields() { let request = Request::from_raw_frame( diff --git a/crates/rttp/README.md b/crates/rttp/README.md index 273e77a5..3325db26 100644 --- a/crates/rttp/README.md +++ b/crates/rttp/README.md @@ -64,6 +64,9 @@ to port `0` in tests. `HttpServer::accept_one` serves one connection. the same listener. `HttpServer::with_read_timeout` and `HttpServer::with_write_timeout` apply socket-level timeouts to each accepted connection; pass `None` to leave the corresponding socket timeout unset. +`HttpServer::with_max_request_head_bytes` sets the maximum HTTP request-head +size accepted for HTTP/1 parsing and h2c upgrade handoff; the default remains +64 KiB, and zero is rejected before serving. Add `Transfer-Encoding: chunked` to an `HttpResponse` to write the complete response body with HTTP/1.x chunked transfer framing instead of an automatic diff --git a/crates/rttp/tests/http2_feature.rs b/crates/rttp/tests/http2_feature.rs index 576aef77..5f650994 100644 --- a/crates/rttp/tests/http2_feature.rs +++ b/crates/rttp/tests/http2_feature.rs @@ -520,6 +520,109 @@ fn complete_h2c_upgrade(stream: &mut TcpStream, authority: &str, settings_payloa write_h2_frame(stream, H2_FRAME_SETTINGS, H2_FLAG_ACK, 0, &[]); } +fn h2c_upgrade_request_head_exactly(authority: &str, size: usize) -> Vec { + let settings = base64url_encode_unpadded(&[]); + let prefix = format!( + "GET /upgrade HTTP/1.1\r\n\ + Host: {authority}\r\n\ + Connection: keep-alive, HTTP2-Settings, Upgrade\r\n\ + Upgrade: h2c\r\n\ + HTTP2-Settings: {settings}\r\n\ + X-Pad: " + ); + let suffix = b"\r\n\r\n"; + assert!( + size >= prefix.len() + suffix.len(), + "h2c request-head size must cover the framing prefix" + ); + let mut raw = prefix.into_bytes(); + raw.resize(size - suffix.len(), b'x'); + raw.extend_from_slice(suffix); + raw +} + +fn send_h2c_upgrade_request(server: rttp::server::HttpServer, raw: &[u8]) -> (String, bool) { + let addr = server.local_addr().expect("server addr"); + let (tx, rx) = mpsc::channel(); + + let handle = thread::spawn(move || { + server.accept_one(|_| { + tx.send(()).expect("send unexpected handler call"); + HttpResponse::ok("unexpected") + }) + }); + + let mut stream = TcpStream::connect(addr).expect("connect h2c upgrade server"); + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .expect("set client read timeout"); + stream.write_all(raw).expect("write h2c upgrade request"); + stream + .shutdown(std::net::Shutdown::Write) + .expect("shutdown client write"); + + let mut response = String::new(); + stream.read_to_string(&mut response).expect("read response"); + let result = handle.join().expect("server thread"); + assert!( + result.is_ok(), + "h2c upgrade request should be handled: {result:?}" + ); + (response, rx.try_recv().is_ok()) +} + +fn serve_h2c_upgrade_after_head(server: rttp::server::HttpServer, raw: &[u8]) -> String { + let addr = server.local_addr().expect("server addr"); + let handle = thread::spawn(move || { + server + .accept_one(|request| { + assert_eq!("HTTP/2", request.version()); + HttpResponse::ok("h2c accepted") + }) + .expect("serve h2c upgrade request") + }); + + let mut stream = TcpStream::connect(addr).expect("connect h2c upgrade server"); + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .expect("set client read timeout"); + stream.write_all(raw).expect("write h2c upgrade request"); + + let mut response = Vec::new(); + let mut byte = [0; 1]; + while !response.ends_with(b"\r\n\r\n") { + stream + .read_exact(&mut byte) + .expect("read h2c upgrade response"); + response.push(byte[0]); + } + let response = String::from_utf8(response).expect("utf8 upgrade response"); + assert!(response.starts_with("HTTP/1.1 101 Switching Protocols\r\n")); + + stream + .write_all(H2_PREFACE) + .expect("write h2c upgraded client preface"); + let settings = read_h2_frame(&mut stream); + assert_eq!(H2_FRAME_SETTINGS, settings.frame_type); + write_h2_frame(&mut stream, H2_FRAME_SETTINGS, H2_FLAG_ACK, 0, &[]); + write_h2_frame( + &mut stream, + H2_FRAME_HEADERS, + H2_FLAG_END_HEADERS | H2_FLAG_END_STREAM, + 3, + &h2_get_headers(b"/settings", addr.to_string().as_bytes()), + ); + + let response_headers = read_h2_frame(&mut stream); + assert_eq!(H2_FRAME_HEADERS, response_headers.frame_type); + let response_body = read_h2_frame(&mut stream); + assert_eq!(H2_FRAME_DATA, response_body.frame_type); + assert_eq!(b"h2c accepted", response_body.payload.as_slice()); + + handle.join().expect("server thread"); + response +} + fn assert_malformed_settings_rejected_before_handler( initial_payload: &[u8], initial_flags: u8, @@ -2044,6 +2147,126 @@ fn h2c_upgrade_rejects_malformed_http2_settings_before_handler_dispatch() { ))); } +#[test] +fn h2c_upgrade_enforces_configured_request_head_limit_boundaries() { + let bind = || { + rttp::Http::server("127.0.0.1:0") + .expect("bind server") + .with_max_request_head_bytes(512) + .expect("set request-head limit") + }; + + let server = bind(); + let below = + h2c_upgrade_request_head_exactly(&server.local_addr().expect("server addr").to_string(), 511); + let response = serve_h2c_upgrade_after_head(server, &below); + assert!(response.starts_with("HTTP/1.1 101 Switching Protocols\r\n")); + + let server = bind(); + let exact = + h2c_upgrade_request_head_exactly(&server.local_addr().expect("server addr").to_string(), 512); + let response = serve_h2c_upgrade_after_head(server, &exact); + assert!(response.starts_with("HTTP/1.1 101 Switching Protocols\r\n")); + + let server = bind(); + let oversized = + h2c_upgrade_request_head_exactly(&server.local_addr().expect("server addr").to_string(), 513); + let (response, handler_called) = send_h2c_upgrade_request(server, &oversized); + assert!( + !handler_called, + "oversized h2c request must not reach handler" + ); + assert_eq!( + "HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\nConnection: close\r\n\r\nBad Request", + response + ); +} + +#[test] +fn h2c_upgrade_rejects_malformed_request_head_within_configured_limit() { + let server = rttp::Http::server("127.0.0.1:0") + .expect("bind server") + .with_max_request_head_bytes(512) + .expect("set request-head limit"); + let (response, handler_called) = send_h2c_upgrade_request( + server, + b"GET /too many parts HTTP/1.1\r\nHost: localhost\r\nUpgrade: h2c\r\n\r\n", + ); + assert!( + !handler_called, + "malformed h2c request must not reach handler" + ); + assert_eq!( + "HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\nConnection: close\r\n\r\nBad Request", + response + ); +} + +#[test] +fn h2c_upgrade_request_head_limit_is_independent_of_request_body_limit() { + let server = rttp::Http::server("127.0.0.1:0") + .expect("bind server") + .with_max_request_body_bytes(1_048_576) + .with_max_request_head_bytes(512) + .expect("set request-head limit"); + let oversized = + h2c_upgrade_request_head_exactly(&server.local_addr().expect("server addr").to_string(), 513); + let (response, handler_called) = send_h2c_upgrade_request(server, &oversized); + assert!( + !handler_called, + "oversized h2c request head must not reach handler" + ); + assert_eq!( + "HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\nConnection: close\r\n\r\nBad Request", + response + ); + + let server = rttp::Http::server("127.0.0.1:0") + .expect("bind server") + .with_max_request_body_bytes(4) + .with_max_request_head_bytes(512) + .expect("set request-head limit"); + let addr = server.local_addr().expect("server addr"); + let (tx, rx) = mpsc::channel(); + let handle = thread::spawn(move || { + server.accept_one(|request| { + tx.send(request.body().to_vec()) + .expect("record unexpected handler call"); + HttpResponse::ok("unexpected") + }) + }); + + let mut stream = TcpStream::connect(addr).expect("connect h2c upgrade server"); + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .expect("set client read timeout"); + complete_h2c_upgrade(&mut stream, &addr.to_string(), &[]); + write_h2_frame( + &mut stream, + H2_FRAME_HEADERS, + H2_FLAG_END_HEADERS, + 3, + &h2_post_headers(b"/upload", addr.to_string().as_bytes()), + ); + write_h2_frame(&mut stream, H2_FRAME_DATA, 0, 3, b"abc"); + write_h2_frame(&mut stream, H2_FRAME_DATA, H2_FLAG_END_STREAM, 3, b"de"); + stream.flush().expect("flush oversized h2 request"); + stream + .shutdown(std::net::Shutdown::Write) + .expect("shutdown h2 request write side"); + + let error = handle + .join() + .expect("server thread") + .expect_err("oversized h2c request body must fail before dispatch"); + assert_eq!(io::ErrorKind::InvalidData, error.kind()); + assert_eq!("request body is too large", error.to_string()); + assert!( + rx.try_recv().is_err(), + "oversized request must not reach handler" + ); +} + #[test] fn cross_crate_http11_h2c_upgrade_client_server_matrix() { let server = rttp::Http::server("127.0.0.1:0") diff --git a/crates/rttp/tests/test_server.rs b/crates/rttp/tests/test_server.rs index fce5d368..052f505b 100644 --- a/crates/rttp/tests/test_server.rs +++ b/crates/rttp/tests/test_server.rs @@ -749,6 +749,46 @@ fn assert_bad_request_without_handler(raw: &[u8]) { ); } +fn http1_request_head_exactly(size: usize) -> Vec { + let prefix = b"GET /limit HTTP/1.1\r\nHost: localhost\r\nX-Pad: "; + let suffix = b"\r\n\r\n"; + assert!( + size >= prefix.len() + suffix.len(), + "request-head size must cover the framing prefix" + ); + let mut raw = prefix.to_vec(); + raw.resize(size - suffix.len(), b'x'); + raw.extend_from_slice(suffix); + raw +} + +fn send_configured_raw_request( + server: rttp::server::HttpServer, + raw: &[u8], +) -> (String, Option, io::Result<()>) { + let addr = server.local_addr().expect("server addr"); + let (tx, rx) = mpsc::channel(); + + let handle = thread::spawn(move || { + server.accept_one(|request| { + tx.send(request).expect("send parsed request"); + HttpResponse::ok("accepted") + }) + }); + + let mut stream = TcpStream::connect(addr).expect("connect server"); + stream.write_all(raw).expect("write request"); + stream + .shutdown(std::net::Shutdown::Write) + .expect("shutdown write"); + + let mut response = String::new(); + stream.read_to_string(&mut response).expect("read response"); + + let serve_result = handle.join().expect("server thread"); + (response, rx.try_recv().ok(), serve_result) +} + fn reserve_local_addr() -> (TcpListener, SocketAddr) { let listener = TcpListener::bind("127.0.0.1:0").expect("reserve local addr"); let addr = listener.local_addr().expect("reserved addr"); @@ -5891,6 +5931,140 @@ fn server_returns_bad_request_for_oversized_request_head() { ); } +#[test] +fn server_accepts_default_request_head_exactly_at_64kib() { + let raw = http1_request_head_exactly(64 * 1024); + let (response, handler_called) = send_raw_request(&raw); + + assert!(handler_called); + assert_eq!( + "HTTP/1.1 200 OK\r\nContent-Length: 10\r\nConnection: close\r\n\r\nunexpected", + response + ); +} + +#[test] +fn with_max_request_head_bytes_rejects_zero_before_serving() { + let error = match rttp::Http::server("127.0.0.1:0") + .expect("bind server") + .with_max_request_head_bytes(0) + { + Ok(_) => panic!("zero request-head limit must fail"), + Err(error) => error, + }; + assert_eq!(io::ErrorKind::InvalidInput, error.kind()); + assert_eq!( + "max request head bytes must be greater than zero", + error.to_string() + ); +} + +#[test] +fn server_enforces_configured_request_head_limit_boundaries() { + let below = http1_request_head_exactly(255); + let exact = http1_request_head_exactly(256); + let oversized = http1_request_head_exactly(257); + + let server = rttp::Http::server("127.0.0.1:0") + .expect("bind server") + .with_max_request_head_bytes(256) + .expect("set request-head limit"); + let (response, request, serve_result) = send_configured_raw_request(server, &below); + serve_result.expect("serve below-limit request"); + assert!(request.is_some()); + assert_eq!( + "HTTP/1.1 200 OK\r\nContent-Length: 8\r\nConnection: close\r\n\r\naccepted", + response + ); + + let server = rttp::Http::server("127.0.0.1:0") + .expect("bind server") + .with_max_request_head_bytes(256) + .expect("set request-head limit"); + let (response, request, serve_result) = send_configured_raw_request(server, &exact); + serve_result.expect("serve exact-limit request"); + assert!(request.is_some()); + assert_eq!( + "HTTP/1.1 200 OK\r\nContent-Length: 8\r\nConnection: close\r\n\r\naccepted", + response + ); + + let server = rttp::Http::server("127.0.0.1:0") + .expect("bind server") + .with_max_request_head_bytes(256) + .expect("set request-head limit"); + let (response, request, serve_result) = send_configured_raw_request(server, &oversized); + serve_result.expect("serve oversized request-head rejection"); + assert!( + request.is_none(), + "oversized request must not reach handler" + ); + assert_eq!( + "HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\nConnection: close\r\n\r\nBad Request", + response + ); +} + +#[test] +fn server_rejects_malformed_request_head_within_configured_limit() { + let server = rttp::Http::server("127.0.0.1:0") + .expect("bind server") + .with_max_request_head_bytes(256) + .expect("set request-head limit"); + let (response, request, serve_result) = send_configured_raw_request( + server, + b"GET /too many parts HTTP/1.1\r\nHost: localhost\r\n\r\n", + ); + serve_result.expect("serve malformed request-head rejection"); + assert!( + request.is_none(), + "malformed request must not reach handler" + ); + assert_eq!( + "HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\nConnection: close\r\n\r\nBad Request", + response + ); +} + +#[test] +fn configured_request_head_limit_is_independent_of_request_body_limit() { + let oversized_head = http1_request_head_exactly(257); + let server = rttp::Http::server("127.0.0.1:0") + .expect("bind server") + .with_max_request_body_bytes(1_048_576) + .with_max_request_head_bytes(256) + .expect("set request-head limit"); + let (response, request, serve_result) = send_configured_raw_request(server, &oversized_head); + serve_result.expect("serve oversized request-head rejection"); + assert!( + request.is_none(), + "oversized request head must not reach handler" + ); + assert_eq!( + "HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\nConnection: close\r\n\r\nBad Request", + response + ); + + let server = rttp::Http::server("127.0.0.1:0") + .expect("bind server") + .with_max_request_body_bytes(4) + .with_max_request_head_bytes(256) + .expect("set request-head limit"); + let (response, request, serve_result) = send_configured_raw_request( + server, + b"POST /upload HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nabcde", + ); + serve_result.expect("serve oversized request-body rejection"); + assert!( + request.is_none(), + "oversized request body must not reach handler" + ); + assert_eq!( + "HTTP/1.1 413 Payload Too Large\r\nContent-Length: 17\r\nConnection: close\r\n\r\nPayload Too Large", + response + ); +} + #[test] fn server_returns_bad_request_for_oversized_content_length_body() { let server = rttp::Http::server("127.0.0.1:0").expect("bind server");