Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 44 additions & 17 deletions crates/rttp-server/src/server/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub struct HttpServer {
pub(crate) read_timeout: Option<Duration>,
pub(crate) write_timeout: Option<Duration>,
pub(crate) max_request_body_bytes: usize,
pub(crate) max_request_head_bytes: usize,
pub(crate) http2_policy: Http2ServerPolicy,
}

Expand Down Expand Up @@ -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(),
});
}
Expand Down Expand Up @@ -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<Self> {
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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down
7 changes: 5 additions & 2 deletions crates/rttp-server/src/server/http1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,11 @@ pub(crate) fn find_header_end(raw: &[u8]) -> Option<usize> {
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",
Expand Down
50 changes: 31 additions & 19 deletions crates/rttp-server/src/server/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,7 @@ impl Request {
pub(crate) fn read_next_from_with_continue<S>(
reader: &mut BufReader<S>,
max_request_body_bytes: usize,
max_request_head_bytes: usize,
) -> io::Result<Option<Self>>
where
S: Read + Write,
Expand All @@ -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,
)?));
}
}
Expand Down Expand Up @@ -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])?;
Expand Down Expand Up @@ -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);
}
Expand All @@ -1267,24 +1269,28 @@ impl Request {
pub(crate) fn read_next_head_from_with_continue<S>(
reader: &mut BufReader<S>,
max_request_body_bytes: usize,
max_request_head_bytes: usize,
) -> io::Result<Option<(Self, RequestBodyKind)>>
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<S>(
reader: &mut BufReader<S>,
max_request_body_bytes: usize,
max_request_head_bytes: usize,
) -> io::Result<Option<(RequestHead, RequestBodyKind)>>
where
S: Read + Write,
Expand All @@ -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])?;
Expand All @@ -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);
}
Expand All @@ -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,
)?));
}
}
Expand Down Expand Up @@ -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])?;
Expand Down Expand Up @@ -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);
}
Expand All @@ -1433,16 +1440,20 @@ impl Request {
pub(crate) fn from_raw_frame(raw: &[u8]) -> io::Result<Self> {
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<Self> {
fn from_raw_frame_with_body_kind(
raw: &[u8],
body_kind: RequestBodyKind,
max_request_head_bytes: usize,
) -> io::Result<Self> {
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)
}
Expand Down Expand Up @@ -2057,7 +2068,8 @@ impl HttpRequest {
pub fn parse(raw: &[u8]) -> Result<Self, HttpParseError> {
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)..];

Expand Down
25 changes: 25 additions & 0 deletions crates/rttp-server/src/server/server_tests.rs
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
3 changes: 3 additions & 0 deletions crates/rttp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading