index : static-web-server.git

ascending towards madness

author Jose Quintana <joseluisquintana20@gmail.com> 2021-04-30 10:16:57.0 +00:00:00
committer Jose Quintana <joseluisquintana20@gmail.com> 2021-04-30 10:16:57.0 +00:00:00
commit
74b9eaf151668980a34a98420e6d7d8ead9dc20f [patch]
tree
29fa27a20129cf62836c4e00ea6d3e83ecff97ed
parent
4290e21c76e2904a489be14fa08878aeafac141b
download
74b9eaf151668980a34a98420e6d7d8ead9dc20f.tar.gz

refactor: just one file metadata per request as possible



Diff

 src/fs.rs | 219 +++++++++++++++++++++++++++++++++++++++++----------------------
 1 file changed, 144 insertions(+), 75 deletions(-)

diff --git a/src/fs.rs b/src/fs.rs
index 4f5ff62..c0b3cc5 100644
--- a/src/fs.rs
+++ b/src/fs.rs
@@ -40,20 +40,47 @@ pub async fn handle_request(
    path: &str,
) -> Result<Response<Body>, StatusCode> {
    let base = Arc::new(base.into());
    let path = path_from_tail(base, path).await?;
    file_reply(headers, path).await
    let res = path_from_tail(base, path).await?;
    file_reply(headers, res).await
}

fn path_from_tail(
    base: Arc<PathBuf>,
    tail: &str,
) -> impl Future<Output = Result<(ArcPath, Metadata, bool), StatusCode>> + Send {
    future::ready(sanitize_path(base.as_ref(), tail)).and_then(|mut buf| async {
        match tokio::fs::metadata(&buf).await {
            Ok(meta) => {
                let mut auto_index = false;
                if meta.is_dir() {
                    tracing::debug!("dir: appending index.html to directory path");
                    buf.push("index.html");
                    auto_index = true;
                }
                tracing::trace!("dir: {:?}", buf);
                Ok((ArcPath(Arc::new(buf)), meta, auto_index))
            }
            Err(err) => {
                tracing::debug!("file not found: {:?}", err);
                Err(StatusCode::NOT_FOUND)
            }
        }
    })
}

/// Reply with a file content.
fn file_reply(
    headers: &HeaderMap<HeaderValue>,
    path: ArcPath,
    res: (ArcPath, Metadata, bool),
) -> impl Future<Output = Result<Response<Body>, StatusCode>> + Send {
    // TODO: directory listing

    let (path, meta, auto_index) = res;
    let conditionals = get_conditional_headers(headers);
    TkFile::open(path.clone()).then(move |res| match res {
        Ok(f) => Either::Left(file_conditional(f, path, conditionals)),
        Ok(f) => Either::Left(file_conditional(f, path, meta, auto_index, conditionals)),
        Err(err) => {
            let rej = match err.kind() {
            let status = match err.kind() {
                io::ErrorKind::NotFound => {
                    tracing::debug!("file not found: {:?}", path.as_ref().display());
                    StatusCode::NOT_FOUND
@@ -71,7 +98,7 @@ fn file_reply(
                    StatusCode::INTERNAL_SERVER_ERROR
                }
            };
            Either::Right(future::err(rej))
            Either::Right(future::err(status))
        }
    })
}
@@ -90,25 +117,6 @@ fn get_conditional_headers(header_list: &HeaderMap<HeaderValue>) -> Conditionals
    }
}

fn path_from_tail(
    base: Arc<PathBuf>,
    tail: &str,
) -> impl Future<Output = Result<ArcPath, StatusCode>> + Send {
    future::ready(sanitize_path(base.as_ref(), tail)).and_then(|mut buf| async {
        let is_dir = tokio::fs::metadata(buf.clone())
            .await
            .map(|m| m.is_dir())
            .unwrap_or(false);

        if is_dir {
            tracing::debug!("dir: appending index.html to directory path");
            buf.push("index.html");
        }
        tracing::trace!("dir: {:?}", buf);
        Ok(ArcPath(Arc::new(buf)))
    })
}

fn sanitize_path(base: impl AsRef<Path>, tail: &str) -> Result<PathBuf, StatusCode> {
    let mut buf = PathBuf::from(base.as_ref());
    let p = match percent_decode_str(tail).decode_utf8() {
@@ -198,59 +206,22 @@ impl Conditionals {
fn file_conditional(
    f: TkFile,
    path: ArcPath,
    meta: Metadata,
    auto_index: bool,
    conditionals: Conditionals,
) -> impl Future<Output = Result<Response<Body>, StatusCode>> + Send {
    file_metadata(f).map_ok(move |(file, meta)| {
        let mut len = meta.len();
        let modified = meta.modified().ok().map(LastModified::from);

        match conditionals.check(modified) {
            Cond::NoBody(resp) => resp,
            Cond::WithBody(range) => {
                bytes_range(range, len)
                    .map(|(start, end)| {
                        let sub_len = end - start;
                        let buf_size = optimal_buf_size(&meta);
                        let stream = file_stream(file, buf_size, (start, end));
                        let body = Body::wrap_stream(stream);

                        let mut resp = Response::new(body);

                        if sub_len != len {
                            *resp.status_mut() = StatusCode::PARTIAL_CONTENT;
                            resp.headers_mut().typed_insert(
                                ContentRange::bytes(start..end, len).expect("valid ContentRange"),
                            );

                            len = sub_len;
                        }

                        let mime = mime_guess::from_path(path.as_ref()).first_or_octet_stream();

                        resp.headers_mut().typed_insert(ContentLength(len));
                        resp.headers_mut().typed_insert(ContentType::from(mime));
                        resp.headers_mut().typed_insert(AcceptRanges::bytes());

                        if let Some(last_modified) = modified {
                            resp.headers_mut().typed_insert(last_modified);
                        }

                        resp
                    })
                    .unwrap_or_else(|BadRange| {
                        // bad byte range
                        let mut resp = Response::new(Body::empty());
                        *resp.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
                        resp.headers_mut()
                            .typed_insert(ContentRange::unsatisfied_bytes(len));
                        resp
                    })
            }
        }
    })
    file_metadata(f, meta, auto_index)
        .map_ok(|(file, meta)| response_body(file, meta, path, conditionals))
}

async fn file_metadata(f: TkFile) -> Result<(TkFile, Metadata), StatusCode> {
async fn file_metadata(
    f: TkFile,
    meta: Metadata,
    auto_index: bool,
) -> Result<(TkFile, Metadata), StatusCode> {
    if !auto_index {
        return Ok((f, meta));
    }
    match f.metadata().await {
        Ok(meta) => Ok((f, meta)),
        Err(err) => {
@@ -260,6 +231,59 @@ async fn file_metadata(f: TkFile) -> Result<(TkFile, Metadata), StatusCode> {
    }
}

fn response_body(
    file: TkFile,
    meta: Metadata,
    path: ArcPath,
    conditionals: Conditionals,
) -> Response<Body> {
    let mut len = meta.len();
    let modified = meta.modified().ok().map(LastModified::from);
    match conditionals.check(modified) {
        Cond::NoBody(resp) => resp,
        Cond::WithBody(range) => {
            bytes_range(range, len)
                .map(|(start, end)| {
                    let sub_len = end - start;
                    let buf_size = optimal_buf_size(&meta);
                    let stream = file_stream(file, buf_size, (start, end));
                    let body = Body::wrap_stream(stream);

                    let mut resp = Response::new(body);

                    if sub_len != len {
                        *resp.status_mut() = StatusCode::PARTIAL_CONTENT;
                        resp.headers_mut().typed_insert(
                            ContentRange::bytes(start..end, len).expect("valid ContentRange"),
                        );

                        len = sub_len;
                    }

                    let mime = mime_guess::from_path(path.as_ref()).first_or_octet_stream();

                    resp.headers_mut().typed_insert(ContentLength(len));
                    resp.headers_mut().typed_insert(ContentType::from(mime));
                    resp.headers_mut().typed_insert(AcceptRanges::bytes());

                    if let Some(last_modified) = modified {
                        resp.headers_mut().typed_insert(last_modified);
                    }

                    resp
                })
                .unwrap_or_else(|BadRange| {
                    // bad byte range
                    let mut resp = Response::new(Body::empty());
                    *resp.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
                    resp.headers_mut()
                        .typed_insert(ContentRange::unsatisfied_bytes(len));
                    resp
                })
        }
    }
}

struct BadRange;

fn bytes_range(range: Option<Range>, max_len: u64) -> Result<(u64, u64), BadRange> {
@@ -280,7 +304,14 @@ fn bytes_range(range: Option<Range>, max_len: u64) -> Result<(u64, u64), BadRang

            let end = match end {
                Bound::Unbounded => max_len,
                Bound::Included(s) => s + 1,
                Bound::Included(s) => {
                    // For the special case where s == the file size
                    if s == max_len {
                        s
                    } else {
                        s + 1
                    }
                }
                Bound::Excluded(s) => s,
            };

@@ -380,3 +411,41 @@ fn get_block_size(metadata: &Metadata) -> usize {
fn get_block_size(_metadata: &Metadata) -> usize {
    DEFAULT_READ_BUF_SIZE
}

#[cfg(test)]
mod tests {
    use super::sanitize_path;
    use bytes::BytesMut;

    #[test]
    fn test_sanitize_path() {
        let base = "/var/www";

        fn p(s: &str) -> &::std::path::Path {
            s.as_ref()
        }

        assert_eq!(
            sanitize_path(base, "/foo.html").unwrap(),
            p("/var/www/foo.html")
        );

        // bad paths
        sanitize_path(base, "/../foo.html").expect_err("dot dot");

        sanitize_path(base, "/C:\\/foo.html").expect_err("C:\\");
    }

    #[test]
    fn test_reserve_at_least() {
        let mut buf = BytesMut::new();
        let cap = 8_192;

        assert_eq!(buf.len(), 0);
        assert_eq!(buf.capacity(), 0);

        super::reserve_at_least(&mut buf, cap);
        assert_eq!(buf.len(), 0);
        assert_eq!(buf.capacity(), cap);
    }
}