index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2024-01-28 23:36:32.0 +00:00:00
committer GitHub <noreply@github.com> 2024-01-28 23:36:32.0 +00:00:00
commit
71dd54f998935d68c5e5dde03b962fb778a87204 [patch]
tree
8ce1f5a4220eeaee823393e80243e617e9b4026e
parent
289356b8db0163e404ff7c79d2fc722eb95769fb
download
71dd54f998935d68c5e5dde03b962fb778a87204.tar.gz

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

Diff

 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<Range>, 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")