use std::io::{self, Read}; use brotli::BrotliDecompress; use bytes::Bytes; use flate2::bufread::{DeflateDecoder, GzDecoder}; use http::header::ALLOW; use http::{header, Method, Response}; use http::{Request, StatusCode}; use http_body::Body as HttpBody; use hyper::Body; use include_dir::Dir; use tower::{service_fn, ServiceExt}; use crate::fs::disk::DiskFilesystem; use crate::fs::include_dir::IncludeDirFilesystem; use crate::{ServeDir, ServeFile}; #[tokio::test] async fn basic() { let svc = ServeDir::new(DiskFilesystem::from(".")); let req = Request::builder() .uri("/README.md") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers()["content-type"], "text/markdown"); let body = body_into_text(res.into_body()).await; let contents = std::fs::read_to_string("./README.md").unwrap(); assert_eq!(body, contents); } #[tokio::test] async fn basic_with_index() { let svc = ServeDir::new(DiskFilesystem::from("test-files")); let req = Request::new(Body::empty()); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers()[header::CONTENT_TYPE], "text/html"); let body = body_into_text(res.into_body()).await; assert_eq!(body, "HTML!\n"); } #[tokio::test] async fn head_request() { let svc = ServeDir::new(DiskFilesystem::from("test-files")); let req = Request::builder() .uri("/precompressed.txt") .method(Method::HEAD) .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.headers()["content-type"], "text/plain"); assert_eq!(res.headers()["content-length"], "23"); let body = res.into_body().data().await; assert!(body.is_none()); } #[tokio::test] async fn precompresed_head_request() { let svc = ServeDir::new(DiskFilesystem::from("test-files")).precompressed_gzip(); let req = Request::builder() .uri("/precompressed.txt") .header("Accept-Encoding", "gzip") .method(Method::HEAD) .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.headers()["content-type"], "text/plain"); assert_eq!(res.headers()["content-encoding"], "gzip"); assert_eq!(res.headers()["content-length"], "59"); let body = res.into_body().data().await; assert!(body.is_none()); } #[tokio::test] async fn with_custom_chunk_size() { let svc = ServeDir::new(DiskFilesystem::from(".")).with_buf_chunk_size(1024 * 32); let req = Request::builder() .uri("/README.md") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers()["content-type"], "text/markdown"); let body = body_into_text(res.into_body()).await; let contents = std::fs::read_to_string("./README.md").unwrap(); assert_eq!(body, contents); } #[tokio::test] async fn precompressed_gzip() { let svc = ServeDir::new(DiskFilesystem::from("test-files")).precompressed_gzip(); let req = Request::builder() .uri("/precompressed.txt") .header("Accept-Encoding", "gzip") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.headers()["content-type"], "text/plain"); assert_eq!(res.headers()["content-encoding"], "gzip"); let body = res.into_body().data().await.unwrap().unwrap(); let mut decoder = GzDecoder::new(&body[..]); let mut decompressed = String::new(); decoder.read_to_string(&mut decompressed).unwrap(); assert!(decompressed.starts_with("\"This is a test file!\"")); } #[tokio::test] async fn precompressed_br() { let svc = ServeDir::new(DiskFilesystem::from("test-files")).precompressed_br(); let req = Request::builder() .uri("/precompressed.txt") .header("Accept-Encoding", "br") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.headers()["content-type"], "text/plain"); assert_eq!(res.headers()["content-encoding"], "br"); let body = res.into_body().data().await.unwrap().unwrap(); let mut decompressed = Vec::new(); BrotliDecompress(&mut &body[..], &mut decompressed).unwrap(); let decompressed = String::from_utf8(decompressed.to_vec()).unwrap(); assert!(decompressed.starts_with("\"This is a test file!\"")); } #[tokio::test] async fn precompressed_deflate() { let svc = ServeDir::new(DiskFilesystem::from("test-files")).precompressed_deflate(); let request = Request::builder() .uri("/precompressed.txt") .header("Accept-Encoding", "deflate,br") .body(Body::empty()) .unwrap(); let res = svc.oneshot(request).await.unwrap(); assert_eq!(res.headers()["content-type"], "text/plain"); assert_eq!(res.headers()["content-encoding"], "deflate"); let body = res.into_body().data().await.unwrap().unwrap(); let mut decoder = DeflateDecoder::new(&body[..]); let mut decompressed = String::new(); decoder.read_to_string(&mut decompressed).unwrap(); assert!(decompressed.starts_with("\"This is a test file!\"")); } #[tokio::test] async fn unsupported_precompression_alogrithm_fallbacks_to_uncompressed() { let svc = ServeDir::new(DiskFilesystem::from("test-files")).precompressed_gzip(); let request = Request::builder() .uri("/precompressed.txt") .header("Accept-Encoding", "br") .body(Body::empty()) .unwrap(); let res = svc.oneshot(request).await.unwrap(); assert_eq!(res.headers()["content-type"], "text/plain"); assert!(res.headers().get("content-encoding").is_none()); let body = res.into_body().data().await.unwrap().unwrap(); let body = String::from_utf8(body.to_vec()).unwrap(); assert!(body.starts_with("\"This is a test file!\"")); } #[tokio::test] async fn only_precompressed_variant_existing() { let svc = ServeDir::new(DiskFilesystem::from("test-files")).precompressed_gzip(); let request = Request::builder() .uri("/only_gzipped.txt") .body(Body::empty()) .unwrap(); let res = svc.clone().oneshot(request).await.unwrap(); assert_eq!(res.status(), StatusCode::NOT_FOUND); // Should reply with gzipped file if client supports it let request = Request::builder() .uri("/only_gzipped.txt") .header("Accept-Encoding", "gzip") .body(Body::empty()) .unwrap(); let res = svc.oneshot(request).await.unwrap(); assert_eq!(res.headers()["content-type"], "text/plain"); assert_eq!(res.headers()["content-encoding"], "gzip"); let body = res.into_body().data().await.unwrap().unwrap(); let mut decoder = GzDecoder::new(&body[..]); let mut decompressed = String::new(); decoder.read_to_string(&mut decompressed).unwrap(); assert!(decompressed.starts_with("\"This is a test file\"")); } #[tokio::test] async fn missing_precompressed_variant_fallbacks_to_uncompressed() { let svc = ServeDir::new(DiskFilesystem::from("test-files")).precompressed_gzip(); let request = Request::builder() .uri("/missing_precompressed.txt") .header("Accept-Encoding", "gzip") .body(Body::empty()) .unwrap(); let res = svc.oneshot(request).await.unwrap(); assert_eq!(res.headers()["content-type"], "text/plain"); // Uncompressed file is served because compressed version is missing assert!(res.headers().get("content-encoding").is_none()); let body = res.into_body().data().await.unwrap().unwrap(); let body = String::from_utf8(body.to_vec()).unwrap(); assert!(body.starts_with("Test file!")); } #[tokio::test] async fn missing_precompressed_variant_fallbacks_to_uncompressed_for_head_request() { let svc = ServeDir::new(DiskFilesystem::from("test-files")).precompressed_gzip(); let request = Request::builder() .uri("/missing_precompressed.txt") .header("Accept-Encoding", "gzip") .method(Method::HEAD) .body(Body::empty()) .unwrap(); let res = svc.oneshot(request).await.unwrap(); assert_eq!(res.headers()["content-type"], "text/plain"); assert_eq!(res.headers()["content-length"], "11"); // Uncompressed file is served because compressed version is missing assert!(res.headers().get("content-encoding").is_none()); assert!(res.into_body().data().await.is_none()); } #[tokio::test] async fn access_to_sub_dirs() { let svc = ServeDir::new(DiskFilesystem::from("..")); let req = Request::builder() .uri("/http_dir/Cargo.toml") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers()["content-type"], "text/x-toml"); let body = body_into_text(res.into_body()).await; let contents = std::fs::read_to_string("Cargo.toml").unwrap(); assert_eq!(body, contents); } #[tokio::test] async fn not_found() { let svc = ServeDir::new(DiskFilesystem::from(".")); let req = Request::builder() .uri("/not-found") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::NOT_FOUND); assert!(res.headers().get(header::CONTENT_TYPE).is_none()); let body = body_into_text(res.into_body()).await; assert!(body.is_empty()); } #[tokio::test] async fn not_found_precompressed() { let svc = ServeDir::new(DiskFilesystem::from("test-files")).precompressed_gzip(); let req = Request::builder() .uri("/not-found") .header("Accept-Encoding", "gzip") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::NOT_FOUND); assert!(res.headers().get(header::CONTENT_TYPE).is_none()); let body = body_into_text(res.into_body()).await; assert!(body.is_empty()); } #[tokio::test] async fn fallbacks_to_different_precompressed_variant_if_not_found_for_head_request() { let svc = ServeDir::new(DiskFilesystem::from("test-files")) .precompressed_gzip() .precompressed_br(); let req = Request::builder() .uri("/precompressed_br.txt") .header("Accept-Encoding", "gzip,br,deflate") .method(Method::HEAD) .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.headers()["content-type"], "text/plain"); assert_eq!(res.headers()["content-encoding"], "br"); assert_eq!(res.headers()["content-length"], "15"); assert!(res.into_body().data().await.is_none()); } #[tokio::test] async fn fallbacks_to_different_precompressed_variant_if_not_found() { let svc = ServeDir::new(DiskFilesystem::from("test-files")) .precompressed_gzip() .precompressed_br(); let req = Request::builder() .uri("/precompressed_br.txt") .header("Accept-Encoding", "gzip,br,deflate") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.headers()["content-type"], "text/plain"); assert_eq!(res.headers()["content-encoding"], "br"); let body = res.into_body().data().await.unwrap().unwrap(); let mut decompressed = Vec::new(); BrotliDecompress(&mut &body[..], &mut decompressed).unwrap(); let decompressed = String::from_utf8(decompressed.to_vec()).unwrap(); assert!(decompressed.starts_with("Test file")); } #[tokio::test] async fn redirect_to_trailing_slash_on_dir() { let svc = ServeDir::new(DiskFilesystem::from(".")); let req = Request::builder().uri("/src").body(Body::empty()).unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::TEMPORARY_REDIRECT); let location = &res.headers()[header::LOCATION]; assert_eq!(location, "/src/"); } #[tokio::test] async fn empty_directory_without_index() { let svc = ServeDir::new(DiskFilesystem::from(".")).append_index_html_on_directories(false); let req = Request::new(Body::empty()); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::NOT_FOUND); assert!(res.headers().get(header::CONTENT_TYPE).is_none()); let body = body_into_text(res.into_body()).await; assert!(body.is_empty()); } async fn body_into_text(body: B) -> String where B: HttpBody + Unpin, B::Error: std::fmt::Debug, { let bytes = hyper::body::to_bytes(body).await.unwrap(); String::from_utf8(bytes.to_vec()).unwrap() } #[tokio::test] async fn access_cjk_percent_encoded_uri_path() { // percent encoding present of 你好世界.txt let cjk_filename_encoded = "%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C.txt"; let svc = ServeDir::new(DiskFilesystem::from("test-files")); let req = Request::builder() .uri(format!("/{cjk_filename_encoded}")) .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers()["content-type"], "text/plain"); } #[tokio::test] async fn access_space_percent_encoded_uri_path() { let encoded_filename = "filename%20with%20space.txt"; let svc = ServeDir::new(DiskFilesystem::from("test-files")); let req = Request::builder() .uri(format!("/{encoded_filename}")) .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers()["content-type"], "text/plain"); } #[tokio::test] async fn read_partial_in_bounds() { let svc = ServeDir::new(DiskFilesystem::from(".")); let bytes_start_incl = 9; let bytes_end_incl = 10; let req = Request::builder() .uri("/README.md") .header( "Range", format!("bytes={bytes_start_incl}-{bytes_end_incl}"), ) .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); let file_contents = std::fs::read("./README.md").unwrap(); assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT); assert_eq!( res.headers()["content-length"], (bytes_end_incl - bytes_start_incl + 1).to_string() ); assert!(res.headers()["content-range"] .to_str() .unwrap() .starts_with(&format!( "bytes {}-{}/{}", bytes_start_incl, bytes_end_incl, file_contents.len() ))); assert_eq!(res.headers()["content-type"], "text/markdown"); let body = hyper::body::to_bytes(res.into_body()).await.ok().unwrap(); let source = Bytes::from(file_contents[bytes_start_incl..=bytes_end_incl].to_vec()); assert_eq!(body, source); } #[tokio::test] async fn read_partial_truncate_out_of_bounds_range() { let svc = ServeDir::new(DiskFilesystem::from("./test-files")); let bytes_start_incl = 0; let bytes_end_excl = 9999999; let requested_len = bytes_end_excl - bytes_start_incl; let req = Request::builder() .uri("/index.html") .header( "Range", format!("bytes={}-{}", bytes_start_incl, requested_len - 1), ) .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT); let file_contents = std::fs::read("./test-files/index.html").unwrap(); assert_eq!( res.headers()["content-range"], &format!("bytes 0-12/{}", file_contents.len()) ) } #[tokio::test] async fn read_partial_rejects_out_of_bounds_range() { let svc = ServeDir::new(DiskFilesystem::from("./test-files")); let req = Request::builder() .uri("/index.html") .header("Range", format!("bytes={}-{}", 99999, 99999999)) .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::RANGE_NOT_SATISFIABLE); let file_contents = std::fs::read("./test-files/index.html").unwrap(); assert_eq!( res.headers()["content-range"], &format!("bytes */{}", file_contents.len()) ) } #[tokio::test] async fn read_partial_errs_on_garbage_header() { let svc = ServeDir::new(DiskFilesystem::from(".")); let req = Request::builder() .uri("/README.md") .header("Range", "bad_format") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::RANGE_NOT_SATISFIABLE); let file_contents = std::fs::read("./README.md").unwrap(); assert_eq!( res.headers()["content-range"], &format!("bytes */{}", file_contents.len()) ) } #[tokio::test] async fn read_partial_errs_on_bad_range() { let svc = ServeDir::new(DiskFilesystem::from(".")); let req = Request::builder() .uri("/README.md") .header("Range", "bytes=-1-15") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::RANGE_NOT_SATISFIABLE); let file_contents = std::fs::read("./README.md").unwrap(); assert_eq!( res.headers()["content-range"], &format!("bytes */{}", file_contents.len()) ) } #[tokio::test] async fn last_modified() { let svc = ServeDir::new(DiskFilesystem::from(".")); let req = Request::builder() .uri("/README.md") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let last_modified = res .headers() .get(header::LAST_MODIFIED) .expect("Missing last modified header!"); // -- If-Modified-Since let svc = ServeDir::new(DiskFilesystem::from(".")); let req = Request::builder() .uri("/README.md") .header(header::IF_MODIFIED_SINCE, last_modified) .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::NOT_MODIFIED); let body = res.into_body().data().await; assert!(body.is_none()); let svc = ServeDir::new(DiskFilesystem::from(".")); let req = Request::builder() .uri("/README.md") .header(header::IF_MODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let readme_bytes = include_bytes!("../README.md"); let body = res.into_body().data().await.unwrap().unwrap(); assert_eq!(body.as_ref(), readme_bytes); // -- If-Unmodified-Since let svc = ServeDir::new(DiskFilesystem::from(".")); let req = Request::builder() .uri("/README.md") .header(header::IF_UNMODIFIED_SINCE, last_modified) .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let body = res.into_body().data().await.unwrap().unwrap(); assert_eq!(body.as_ref(), readme_bytes); let svc = ServeDir::new(DiskFilesystem::from(".")); let req = Request::builder() .uri("/README.md") .header(header::IF_UNMODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED); let body = res.into_body().data().await; assert!(body.is_none()); } #[tokio::test] async fn with_fallback_svc() { async fn fallback(req: Request) -> io::Result> { Ok(Response::new(Body::from(format!( "from fallback {}", req.uri().path() )))) } let svc = ServeDir::new(DiskFilesystem::from(".")).fallback(service_fn(fallback)); let req = Request::builder() .uri("/doesnt-exist") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let body = body_into_text(res.into_body()).await; assert_eq!(body, "from fallback /doesnt-exist"); } #[tokio::test] async fn with_fallback_serve_file() { let filesystem = DiskFilesystem::from("."); let svc = ServeDir::new(filesystem.clone()).fallback(ServeFile::new("README.md", filesystem)); let req = Request::builder() .uri("/doesnt-exist") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers()["content-type"], "text/markdown"); let body = body_into_text(res.into_body()).await; let contents = std::fs::read_to_string("./README.md").unwrap(); assert_eq!(body, contents); } #[tokio::test] async fn method_not_allowed() { let svc = ServeDir::new(DiskFilesystem::from(".")); let req = Request::builder() .method(Method::POST) .uri("/README.md") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(res.headers()[ALLOW], "GET,HEAD"); } #[tokio::test] async fn calling_fallback_on_not_allowed() { async fn fallback(req: Request) -> io::Result> { Ok(Response::new(Body::from(format!( "from fallback {}", req.uri().path() )))) } let svc = ServeDir::new(DiskFilesystem::from(".")) .call_fallback_on_method_not_allowed(true) .fallback(service_fn(fallback)); let req = Request::builder() .method(Method::POST) .uri("/doesnt-exist") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let body = body_into_text(res.into_body()).await; assert_eq!(body, "from fallback /doesnt-exist"); } #[tokio::test] async fn with_fallback_svc_and_not_append_index_html_on_directories() { async fn fallback(req: Request) -> io::Result> { Ok(Response::new(Body::from(format!( "from fallback {}", req.uri().path() )))) } let svc = ServeDir::new(DiskFilesystem::from(".")) .append_index_html_on_directories(false) .fallback(service_fn(fallback)); let req = Request::builder().uri("/").body(Body::empty()).unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let body = body_into_text(res.into_body()).await; assert_eq!(body, "from fallback /"); } // https://github.com/tower-rs/tower-http/issues/308 #[tokio::test] async fn calls_fallback_on_invalid_paths() { async fn fallback(_: T) -> Result, io::Error> { let mut res = Response::new(Body::empty()); res.headers_mut() .insert("from-fallback", "1".parse().unwrap()); Ok(res) } let svc = ServeDir::new(DiskFilesystem::from(".")).fallback(service_fn(fallback)); let req = Request::builder() .uri("/weird_%c3%28_path") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.headers()["from-fallback"], "1"); } #[tokio::test] async fn include_dir_basic_with_index() { static ROOT: Dir<'_> = include_dir::include_dir!("test-files"); let svc = ServeDir::new(IncludeDirFilesystem::new(ROOT.clone())); let req = Request::new(Body::empty()); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers()[header::CONTENT_TYPE], "text/html"); let body = body_into_text(res.into_body()).await; assert_eq!(body, "HTML!\n"); } #[tokio::test] async fn serve_file_basic() { let svc = ServeFile::new("README.md", DiskFilesystem::from(".")); let req = Request::builder() .uri("/README.md") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers()["content-type"], "text/markdown"); let body = body_into_text(res.into_body()).await; let contents = std::fs::read_to_string("./README.md").unwrap(); assert_eq!(body, contents); } #[tokio::test] async fn serve_file_not_found() { let svc = ServeFile::new("README.md", DiskFilesystem::from(".")); let req = Request::builder() .uri("/not-exist") .body(Body::empty()) .unwrap(); let res = svc.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers()["content-type"], "text/markdown"); let body = body_into_text(res.into_body()).await; let contents = std::fs::read_to_string("./README.md").unwrap(); assert_eq!(body, contents); }