From 71dd54f998935d68c5e5dde03b962fb778a87204 Mon Sep 17 00:00:00 2001 From: Jose Quintana <1700322+joseluisq@users.noreply.github.com> Date: Mon, 29 Jan 2024 00:36:32 +0100 Subject: [PATCH] feat: support for `Range` requests out of bounds (#306) * feat: support for `Range` requests out of bounds SWS will make sure to return only what's available in that case which seems to be a very common behavior across web servers. Previously exceeding the length of a file returning `416 Requested Range Not Satisfiable`. Now it will return what's available. ```sh $ curl -IH "Range: bytes=50-9000" http://localhost/index.html \# HTTP/1.1 206 Partial Content \# Server: nginx/1.25.3 \# Date: Sun, 28 Jan 2024 22:09:20 GMT \# Content-Type: text/html \# Content-Length: 486 \# Last-Modified: Mon, 02 Oct 2023 04:49:01 GMT \# Connection: keep-alive \# ETag: "651a4bbd-218" \# Content-Range: bytes 50-535/536 ``` it resolves #295 and relates to https://github.com/orgs/static-web-server/discussions/145 --- src/static_files.rs | 43 ++++++++++++++++++++++++++++++++++--------- tests/static_files.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 87 insertions(+), 26 deletions(-) diff --git a/src/static_files.rs b/src/static_files.rs index 6165696..82423a7 100644 --- a/src/static_files.rs +++ b/src/static_files.rs @@ -672,35 +672,60 @@ fn bytes_range(range: Option, max_len: u64) -> Result<(u64, u64), BadRang return Ok((0, max_len)); }; - let res = range + let resp = range .iter() .map(|(start, end)| { + tracing::trace!("range request received, {:?}-{:?}-{}", start, end, max_len); + let (start, end) = match (start, end) { (Bound::Unbounded, Bound::Unbounded) => (0, max_len), (Bound::Included(a), Bound::Included(b)) => { + // `start` can not be greater than `end` + if a > b { + return Err(BadRange); + } // For the special case where b == the file size (a, if b == max_len { b } else { b + 1 }) } (Bound::Included(a), Bound::Unbounded) => (a, max_len), (Bound::Unbounded, Bound::Included(b)) => { if b > max_len { - return Err(BadRange); + // `Range` request out of bounds, return only what's available + tracing::trace!("unsatisfiable byte range: -{}/{}", b, max_len); + tracing::trace!("returning only what's available: 0-{}", max_len); + (0, max_len) + } else { + (max_len - b, max_len) } - (max_len - b, max_len) } _ => unreachable!(), }; if start < end && end <= max_len { - Ok((start, end)) - } else { - tracing::trace!("unsatisfiable byte range: {}-{}/{}", start, end, max_len); - Err(BadRange) + tracing::trace!("range request to return: {}-{}/{}", start, end, max_len); + return Ok((start, end)); } + + tracing::trace!("unsatisfiable byte range: {}-{}/{}", start, end, max_len); + + if start < end && start <= max_len { + // `Range` request out of bounds, return only what's available + tracing::trace!( + "returning only what's available: {}-{}/{}", + start, + max_len, + max_len + ); + return Ok((start, max_len)); + } + + Err(BadRange) }) .next() - .unwrap_or(Ok((0, max_len))); - res + // NOTE: default to `BadRange` in case of wrong `Range` bytes format + .unwrap_or(Err(BadRange)); + + resp } #[cfg(test)] diff --git a/tests/static_files.rs b/tests/static_files.rs index 0c55a54..b7e0c74 100644 --- a/tests/static_files.rs +++ b/tests/static_files.rs @@ -830,16 +830,16 @@ mod tests { .await { Ok((mut res, _)) => { - assert_eq!(res.status(), 416); + assert_eq!(res.status(), 206); assert_eq!( res.headers()["content-range"], - format!("bytes */{}", buf.len()) + format!("bytes 100-{}/{}", buf.len() - 1, buf.len()) ); - assert_eq!(res.headers().get("content-length"), None); + assert!(res.headers().get("content-length").is_some()); let body = hyper::body::to_bytes(res.body_mut()) .await .expect("unexpected bytes error during `body` conversion"); - assert_eq!(body, ""); + assert!(body.len() > 400); } Err(_) => { panic!("expected a normal response rather than a status error") @@ -1038,17 +1038,14 @@ mod tests { } #[tokio::test] - async fn handle_byte_ranges_bad_2() { + async fn handle_byte_ranges_bad_non_numeric() { + let mut headers = HeaderMap::new(); + headers.insert("range", "bytes=xyx-abc".parse().unwrap()); + let buf = fs::read(root_dir().join("index.html")) .expect("unexpected error during index.html reading"); let buf = Bytes::from(buf); - let mut headers = HeaderMap::new(); - headers.insert( - "range", - format!("bytes=-{}", buf.len() + 1).parse().unwrap(), - ); - for method in [Method::HEAD, Method::GET] { match static_files::handle(&HandleOpts { method: &method, @@ -1075,11 +1072,11 @@ mod tests { res.headers()["content-range"], format!("bytes */{}", buf.len()) ); - assert_eq!(res.headers().get("content-length"), None); + assert!(res.headers().get("content-length").is_none()); let body = hyper::body::to_bytes(res.body_mut()) .await .expect("unexpected bytes error during `body` conversion"); - assert_eq!(body, ""); + assert!(body.is_empty()); } Err(_) => { panic!("expected a normal response rather than a status error") @@ -1089,14 +1086,16 @@ mod tests { } #[tokio::test] - async fn handle_byte_ranges_bad_3() { + async fn handle_byte_ranges_bad_2() { let buf = fs::read(root_dir().join("index.html")) .expect("unexpected error during index.html reading"); let buf = Bytes::from(buf); let mut headers = HeaderMap::new(); - // Range::Unbounded for beginning and end - headers.insert("range", "bytes=".parse().unwrap()); + headers.insert( + "range", + format!("bytes=-{}", buf.len() + 1).parse().unwrap(), + ); for method in [Method::HEAD, Method::GET] { match static_files::handle(&HandleOpts { @@ -1120,10 +1119,47 @@ mod tests { { Ok((mut res, _)) => { assert_eq!(res.status(), 200); + assert!(res.headers().get("content-length").is_some()); let body = hyper::body::to_bytes(res.body_mut()) .await .expect("unexpected bytes error during `body` conversion"); - assert_eq!(body, buf); + assert!(body.len() > 500); + } + Err(_) => { + panic!("expected a normal response rather than a status error") + } + } + } + } + + #[tokio::test] + async fn handle_byte_ranges_bad_3() { + let mut headers = HeaderMap::new(); + // Range::Unbounded for beginning and end + headers.insert("range", "bytes=".parse().unwrap()); + + for method in [Method::HEAD, Method::GET] { + match static_files::handle(&HandleOpts { + method: &method, + headers: &headers, + base_path: &root_dir(), + uri_path: "index.html", + uri_query: None, + #[cfg(feature = "directory-listing")] + dir_listing: false, + #[cfg(feature = "directory-listing")] + dir_listing_order: 6, + #[cfg(feature = "directory-listing")] + dir_listing_format: &DirListFmt::Html, + redirect_trailing_slash: true, + compression_static: false, + ignore_hidden_files: false, + index_files: &[], + }) + .await + { + Ok((res, _)) => { + assert_eq!(res.status(), 416); } Err(_) => { panic!("expected a normal response rather than a status error") -- libgit2 1.7.2