From 053b42553a4b2b63b69b038d426f2a676c2b9f43 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 1 Jul 2022 16:44:22 -0500 Subject: [PATCH 1/5] serve gzipped version of asset if it exists --- nexus/src/external_api/console_api.rs | 83 +++++++++++--------- nexus/test-utils/src/http_testing.rs | 1 + nexus/tests/integration_tests/console_api.rs | 12 +++ nexus/tests/static/assets/gzipped.txt.gz | 1 + 4 files changed, 62 insertions(+), 35 deletions(-) create mode 100644 nexus/tests/static/assets/gzipped.txt.gz diff --git a/nexus/src/external_api/console_api.rs b/nexus/src/external_api/console_api.rs index e5e898b27c..818271c8ca 100644 --- a/nexus/src/external_api/console_api.rs +++ b/nexus/src/external_api/console_api.rs @@ -619,6 +619,17 @@ pub async fn console_settings_page( console_index_or_login_redirect(rqctx).await } +fn with_gz_ext(path: Vec) -> Vec { + if path.len() == 0 { + return path; + } + + let mut new_path = path.clone(); + let last = new_path.pop().unwrap(); // we know there's one there + new_path.push(format!("{last}.gz")); + new_path +} + /// Fetch a static asset from `/assets`. 404 on virtually all /// errors. No auth. NO SENSITIVE FILES. #[endpoint { @@ -633,11 +644,32 @@ pub async fn asset( let apictx = rqctx.context(); let path = path_params.into_inner().path; - let file = match &apictx.console_config.static_dir { - // important: we only serve assets from assets/ within static_dir - Some(static_dir) => find_file(path, &static_dir.join("assets")), - _ => Err(not_found("static_dir undefined")), - }?; + // bail unless the extension is allowed + let filename = path.last().ok_or(not_found("no path"))?; + let ext = PathBuf::from(filename) + .extension() + .map(|ext| ext.to_os_string()) + .unwrap_or_else(|| OsString::from("disallowed")); + if !ALLOWED_EXTENSIONS.contains(&ext) { + return Err(not_found("file extension not allowed")); + } + + // important: we only serve assets from assets/ within static_dir + let assets_dir = &apictx + .console_config + .static_dir + .as_ref() + .ok_or(not_found("static_dir undefined"))? + .join("assets"); + + let path_gz = with_gz_ext(path.clone()); + + // try to find the gzipped version, fall back to non-gz + let (file, gzipped) = match find_file(path_gz, &assets_dir) { + Ok(file) => (file, true), + _ => (find_file(path, &assets_dir)?, false), + }; + let file_contents = tokio::fs::read(&file) .await .map_err(|e| not_found(&format!("accessing {:?}: {:#}", file, e)))?; @@ -647,11 +679,19 @@ pub async fn asset( .first() .map_or_else(|| "text/plain".to_string(), |m| m.to_string()); - Ok(Response::builder() + let mut resp = Response::builder() .status(StatusCode::OK) .header(http::header::CONTENT_TYPE, &content_type) - .header(http::header::CACHE_CONTROL, cache_control_header_value(apictx)) - .body(file_contents.into())?) + .header( + http::header::CACHE_CONTROL, + cache_control_header_value(apictx), + ); + + if gzipped { + resp = resp.header(http::header::CONTENT_ENCODING, "gzip"); + } + + Ok(resp.body(file_contents.into())?) } fn cache_control_header_value(apictx: &Arc) -> String { @@ -694,14 +734,6 @@ lazy_static! { ); } -fn file_ext_allowed(path: &PathBuf) -> bool { - let ext = path - .extension() - .map(|ext| ext.to_os_string()) - .unwrap_or_else(|| OsString::from("disallowed")); - ALLOWED_EXTENSIONS.contains(&ext) -} - /// Starting from `root_dir`, follow the segments of `path` down the file tree /// until we find a file (or not). Do not follow symlinks. fn find_file( @@ -734,10 +766,6 @@ fn find_file( return Err(not_found("expected a non-directory")); } - if !file_ext_allowed(¤t) { - return Err(not_found("file extension not allowed")); - } - Ok(current) } @@ -821,19 +849,4 @@ mod test { assert_eq!(error.status_code, StatusCode::NOT_FOUND); assert_eq!(error.internal_message, "attempted to follow a symlink"); } - - #[test] - fn test_find_file_404_on_disallowed_ext() { - let root = current_dir().unwrap(); - let error = - find_file(get_path("tests/static/assets/blocked.ext"), &root) - .unwrap_err(); - assert_eq!(error.status_code, StatusCode::NOT_FOUND); - assert_eq!(error.internal_message, "file extension not allowed",); - - let error = find_file(get_path("tests/static/assets/no_ext"), &root) - .unwrap_err(); - assert_eq!(error.status_code, StatusCode::NOT_FOUND); - assert_eq!(error.internal_message, "file extension not allowed"); - } } diff --git a/nexus/test-utils/src/http_testing.rs b/nexus/test-utils/src/http_testing.rs index 2328f10a2a..03122218bc 100644 --- a/nexus/test-utils/src/http_testing.rs +++ b/nexus/test-utils/src/http_testing.rs @@ -85,6 +85,7 @@ impl<'a> RequestBuilder<'a> { expected_status: None, allowed_headers: Some(vec![ http::header::CACHE_CONTROL, + http::header::CONTENT_ENCODING, http::header::CONTENT_LENGTH, http::header::CONTENT_TYPE, http::header::DATE, diff --git a/nexus/tests/integration_tests/console_api.rs b/nexus/tests/integration_tests/console_api.rs index 685bf358d4..a8fb90415f 100644 --- a/nexus/tests/integration_tests/console_api.rs +++ b/nexus/tests/integration_tests/console_api.rs @@ -189,11 +189,23 @@ async fn test_assets(cptestctx: &ControlPlaneTestContext) { // existing file is returned let resp = RequestBuilder::new(&testctx, Method::GET, "/assets/hello.txt") + .expect_status(Some(StatusCode::OK)) .execute() .await .expect("failed to get existing file"); assert_eq!(resp.body, "hello there".as_bytes()); + + // file with existing gzipped version is returned + let resp = + RequestBuilder::new(&testctx, Method::GET, "/assets/gzipped.txt") + .expect_status(Some(StatusCode::OK)) + .expect_response_header(http::header::CONTENT_ENCODING, "gzip") + .execute() + .await + .expect("failed to get existing file"); + + assert_eq!(resp.body, "pretend this is gzipped beep boop".as_bytes()); } #[tokio::test] diff --git a/nexus/tests/static/assets/gzipped.txt.gz b/nexus/tests/static/assets/gzipped.txt.gz new file mode 100644 index 0000000000..83ccc5df48 --- /dev/null +++ b/nexus/tests/static/assets/gzipped.txt.gz @@ -0,0 +1 @@ +pretend this is gzipped beep boop \ No newline at end of file From 0eee3a381dbb35c813da72b7e828169199729845 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 1 Jul 2022 21:27:21 -0500 Subject: [PATCH 2/5] fix clippy, only send gzip if accept-encoding header says to --- nexus/src/external_api/console_api.rs | 50 ++++++++++++------- nexus/tests/integration_tests/console_api.rs | 38 +++++++++++++- nexus/tests/static/assets/gzip-and-not.txt | 1 + .../{gzipped.txt.gz => gzip-and-not.txt.gz} | 0 nexus/tests/static/assets/gzip-only.txt.gz | 1 + 5 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 nexus/tests/static/assets/gzip-and-not.txt rename nexus/tests/static/assets/{gzipped.txt.gz => gzip-and-not.txt.gz} (100%) create mode 100644 nexus/tests/static/assets/gzip-only.txt.gz diff --git a/nexus/src/external_api/console_api.rs b/nexus/src/external_api/console_api.rs index 818271c8ca..84aff581d1 100644 --- a/nexus/src/external_api/console_api.rs +++ b/nexus/src/external_api/console_api.rs @@ -624,14 +624,16 @@ fn with_gz_ext(path: Vec) -> Vec { return path; } - let mut new_path = path.clone(); + let mut new_path = path; let last = new_path.pop().unwrap(); // we know there's one there new_path.push(format!("{last}.gz")); new_path } /// Fetch a static asset from `/assets`. 404 on virtually all -/// errors. No auth. NO SENSITIVE FILES. +/// errors. No auth. NO SENSITIVE FILES. Will serve a gzipped version if the +/// `.gz` file is present in the directory and `Accept-Encoding: gzip` is +/// present on the request. #[endpoint { method = GET, path = "/assets/{path:.*}", @@ -644,9 +646,10 @@ pub async fn asset( let apictx = rqctx.context(); let path = path_params.into_inner().path; - // bail unless the extension is allowed - let filename = path.last().ok_or(not_found("no path"))?; - let ext = PathBuf::from(filename) + // Bail unless the extension is allowed + let filename = + PathBuf::from(path.last().ok_or_else(|| not_found("no path"))?); + let ext = filename .extension() .map(|ext| ext.to_os_string()) .unwrap_or_else(|| OsString::from("disallowed")); @@ -654,28 +657,39 @@ pub async fn asset( return Err(not_found("file extension not allowed")); } - // important: we only serve assets from assets/ within static_dir + // We only serve assets from assets/ within static_dir let assets_dir = &apictx .console_config .static_dir .as_ref() - .ok_or(not_found("static_dir undefined"))? + .ok_or_else(|| not_found("static_dir undefined"))? .join("assets"); + let request = &rqctx.request.lock().await; + let accept_encoding = request.headers().get(http::header::ACCEPT_ENCODING); + + let accept_gz = accept_encoding.map_or(false, |val| { + val.to_str().map_or(false, |s| s.contains("gzip")) + }); + let path_gz = with_gz_ext(path.clone()); - // try to find the gzipped version, fall back to non-gz - let (file, gzipped) = match find_file(path_gz, &assets_dir) { - Ok(file) => (file, true), - _ => (find_file(path, &assets_dir)?, false), - }; + // If req accepts gzip and we have a gzipped version, serve that. Otherwise + // fall back to non-gz. If neither file found, bubble up 404. + let (file_path, set_content_encoding_gzip) = + match (accept_gz, find_file(path_gz, &assets_dir)) { + (true, Ok(gzipped_file)) => (gzipped_file, true), + _ => (find_file(path, &assets_dir)?, false), + }; - let file_contents = tokio::fs::read(&file) - .await - .map_err(|e| not_found(&format!("accessing {:?}: {:#}", file, e)))?; + // File read is the same regardless of gzip + let file_contents = tokio::fs::read(&file_path).await.map_err(|e| { + not_found(&format!("accessing {:?}: {:#}", file_path, e)) + })?; - // Derive the MIME type from the file name - let content_type = mime_guess::from_path(&file) + // Derive the MIME type from the file name. Have to use filename and not + // file_path because the latter might have a .gz on the end. + let content_type = mime_guess::from_path(filename) .first() .map_or_else(|| "text/plain".to_string(), |m| m.to_string()); @@ -687,7 +701,7 @@ pub async fn asset( cache_control_header_value(apictx), ); - if gzipped { + if set_content_encoding_gzip { resp = resp.header(http::header::CONTENT_ENCODING, "gzip"); } diff --git a/nexus/tests/integration_tests/console_api.rs b/nexus/tests/integration_tests/console_api.rs index a8fb90415f..fdcf7e3fdf 100644 --- a/nexus/tests/integration_tests/console_api.rs +++ b/nexus/tests/integration_tests/console_api.rs @@ -195,10 +195,32 @@ async fn test_assets(cptestctx: &ControlPlaneTestContext) { .expect("failed to get existing file"); assert_eq!(resp.body, "hello there".as_bytes()); + // make sure we're not including the gzip header on non-gzipped files + assert_eq!(resp.headers.get(http::header::CONTENT_ENCODING), None); - // file with existing gzipped version is returned + // file with only gzipped version 404s if request doesn't have accept-encoding: gzip + let _ = RequestBuilder::new(&testctx, Method::GET, "/assets/gzip-only.txt") + .expect_status(Some(StatusCode::NOT_FOUND)) + .execute() + .await + .expect("failed to 404 on gzip file without accept-encoding: gzip"); + + // file with only gzipped version is returned if request accepts gzip let resp = - RequestBuilder::new(&testctx, Method::GET, "/assets/gzipped.txt") + RequestBuilder::new(&testctx, Method::GET, "/assets/gzip-only.txt") + .header(http::header::ACCEPT_ENCODING, "gzip") + .expect_status(Some(StatusCode::OK)) + .expect_response_header(http::header::CONTENT_ENCODING, "gzip") + .execute() + .await + .expect("failed to get existing file"); + + assert_eq!(resp.body, "nothing but gzip".as_bytes()); + + // file with both gzip and not returns gzipped if request accepts gzip + let resp = + RequestBuilder::new(&testctx, Method::GET, "/assets/gzip-and-not.txt") + .header(http::header::ACCEPT_ENCODING, "gzip") .expect_status(Some(StatusCode::OK)) .expect_response_header(http::header::CONTENT_ENCODING, "gzip") .execute() @@ -206,6 +228,18 @@ async fn test_assets(cptestctx: &ControlPlaneTestContext) { .expect("failed to get existing file"); assert_eq!(resp.body, "pretend this is gzipped beep boop".as_bytes()); + + // returns non-gzipped if request doesn't accept gzip + let resp = + RequestBuilder::new(&testctx, Method::GET, "/assets/gzip-and-not.txt") + .expect_status(Some(StatusCode::OK)) + .execute() + .await + .expect("failed to get existing file"); + + assert_eq!(resp.body, "not gzipped but I know a guy".as_bytes()); + // make sure we're not including the gzip header on non-gzipped files + assert_eq!(resp.headers.get(http::header::CONTENT_ENCODING), None); } #[tokio::test] diff --git a/nexus/tests/static/assets/gzip-and-not.txt b/nexus/tests/static/assets/gzip-and-not.txt new file mode 100644 index 0000000000..d95fa1e903 --- /dev/null +++ b/nexus/tests/static/assets/gzip-and-not.txt @@ -0,0 +1 @@ +not gzipped but I know a guy \ No newline at end of file diff --git a/nexus/tests/static/assets/gzipped.txt.gz b/nexus/tests/static/assets/gzip-and-not.txt.gz similarity index 100% rename from nexus/tests/static/assets/gzipped.txt.gz rename to nexus/tests/static/assets/gzip-and-not.txt.gz diff --git a/nexus/tests/static/assets/gzip-only.txt.gz b/nexus/tests/static/assets/gzip-only.txt.gz new file mode 100644 index 0000000000..f390951853 --- /dev/null +++ b/nexus/tests/static/assets/gzip-only.txt.gz @@ -0,0 +1 @@ +nothing but gzip \ No newline at end of file From 861518358d7beb6a366e4411264887a921387697 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 4 Jul 2022 15:32:46 -0500 Subject: [PATCH 3/5] don't bother finding gzipped file if we're not going to serve it anyway --- nexus/src/external_api/console_api.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/nexus/src/external_api/console_api.rs b/nexus/src/external_api/console_api.rs index 84aff581d1..b47081898f 100644 --- a/nexus/src/external_api/console_api.rs +++ b/nexus/src/external_api/console_api.rs @@ -676,11 +676,14 @@ pub async fn asset( // If req accepts gzip and we have a gzipped version, serve that. Otherwise // fall back to non-gz. If neither file found, bubble up 404. - let (file_path, set_content_encoding_gzip) = - match (accept_gz, find_file(path_gz, &assets_dir)) { - (true, Ok(gzipped_file)) => (gzipped_file, true), + let (file_path, set_content_encoding_gzip) = if accept_gz { + match find_file(path_gz, &assets_dir) { + Ok(gzipped_file) => (gzipped_file, true), _ => (find_file(path, &assets_dir)?, false), - }; + } + } else { + (find_file(path, &assets_dir)?, false) + }; // File read is the same regardless of gzip let file_contents = tokio::fs::read(&file_path).await.map_err(|e| { From 78dc3e41c8e1c21804f8ad5b75a5370a5b301604 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 5 Jul 2022 10:05:48 -0500 Subject: [PATCH 4/5] code golf, thanks @jgallagher --- nexus/src/external_api/console_api.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/nexus/src/external_api/console_api.rs b/nexus/src/external_api/console_api.rs index b47081898f..d9271483e9 100644 --- a/nexus/src/external_api/console_api.rs +++ b/nexus/src/external_api/console_api.rs @@ -676,14 +676,11 @@ pub async fn asset( // If req accepts gzip and we have a gzipped version, serve that. Otherwise // fall back to non-gz. If neither file found, bubble up 404. - let (file_path, set_content_encoding_gzip) = if accept_gz { - match find_file(path_gz, &assets_dir) { - Ok(gzipped_file) => (gzipped_file, true), + let (file_path, set_content_encoding_gzip) = + match accept_gz.then(|| find_file(path_gz, &assets_dir)) { + Some(Ok(gzipped_file)) => (gzipped_file, true), _ => (find_file(path, &assets_dir)?, false), - } - } else { - (find_file(path, &assets_dir)?, false) - }; + }; // File read is the same regardless of gzip let file_contents = tokio::fs::read(&file_path).await.map_err(|e| { From 4782e84fb0fd022844992d9e25de6a0de5eb97f5 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 5 Jul 2022 12:01:48 -0500 Subject: [PATCH 5/5] debatably simplify by passing around PathBuf instead of Vec --- nexus/src/external_api/console_api.rs | 102 ++++++++----------- nexus/tests/integration_tests/console_api.rs | 15 +++ 2 files changed, 59 insertions(+), 58 deletions(-) diff --git a/nexus/src/external_api/console_api.rs b/nexus/src/external_api/console_api.rs index d9271483e9..e68f9b0882 100644 --- a/nexus/src/external_api/console_api.rs +++ b/nexus/src/external_api/console_api.rs @@ -619,14 +619,14 @@ pub async fn console_settings_page( console_index_or_login_redirect(rqctx).await } -fn with_gz_ext(path: Vec) -> Vec { - if path.len() == 0 { - return path; - } - - let mut new_path = path; - let last = new_path.pop().unwrap(); // we know there's one there - new_path.push(format!("{last}.gz")); +/// Make a new PathBuf with `.gz` on the end +fn with_gz_ext(path: &PathBuf) -> PathBuf { + let mut new_path = path.clone(); + let new_ext = match path.extension().map(|ext| ext.to_str()) { + Some(Some(curr_ext)) => format!("{curr_ext}.gz"), + _ => "gz".to_string(), + }; + new_path.set_extension(new_ext); new_path } @@ -644,15 +644,12 @@ pub async fn asset( path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let path = path_params.into_inner().path; + let path = PathBuf::from_iter(path_params.into_inner().path); // Bail unless the extension is allowed - let filename = - PathBuf::from(path.last().ok_or_else(|| not_found("no path"))?); - let ext = filename + let ext = path .extension() - .map(|ext| ext.to_os_string()) - .unwrap_or_else(|| OsString::from("disallowed")); + .map_or_else(|| OsString::from("disallowed"), |ext| ext.to_os_string()); if !ALLOWED_EXTENSIONS.contains(&ext) { return Err(not_found("file extension not allowed")); } @@ -667,39 +664,33 @@ pub async fn asset( let request = &rqctx.request.lock().await; let accept_encoding = request.headers().get(http::header::ACCEPT_ENCODING); - let accept_gz = accept_encoding.map_or(false, |val| { val.to_str().map_or(false, |s| s.contains("gzip")) }); - let path_gz = with_gz_ext(path.clone()); - // If req accepts gzip and we have a gzipped version, serve that. Otherwise // fall back to non-gz. If neither file found, bubble up 404. - let (file_path, set_content_encoding_gzip) = - match accept_gz.then(|| find_file(path_gz, &assets_dir)) { - Some(Ok(gzipped_file)) => (gzipped_file, true), - _ => (find_file(path, &assets_dir)?, false), + let (path_to_read, set_content_encoding_gzip) = + match accept_gz.then(|| find_file(&with_gz_ext(&path), &assets_dir)) { + Some(Ok(gzipped_path)) => (gzipped_path, true), + _ => (find_file(&path, &assets_dir)?, false), }; // File read is the same regardless of gzip - let file_contents = tokio::fs::read(&file_path).await.map_err(|e| { - not_found(&format!("accessing {:?}: {:#}", file_path, e)) + let file_contents = tokio::fs::read(&path_to_read).await.map_err(|e| { + not_found(&format!("accessing {:?}: {:#}", path_to_read, e)) })?; - // Derive the MIME type from the file name. Have to use filename and not - // file_path because the latter might have a .gz on the end. - let content_type = mime_guess::from_path(filename) - .first() - .map_or_else(|| "text/plain".to_string(), |m| m.to_string()); + // Derive the MIME type from the file name (can't use path_to_read because + // it might end with .gz) + let content_type = path.file_name().map_or("text/plain", |f| { + mime_guess::from_path(f).first_raw().unwrap_or("text/plain") + }); let mut resp = Response::builder() .status(StatusCode::OK) - .header(http::header::CONTENT_TYPE, &content_type) - .header( - http::header::CACHE_CONTROL, - cache_control_header_value(apictx), - ); + .header(http::header::CONTENT_TYPE, content_type) + .header(http::header::CACHE_CONTROL, cache_control_value(apictx)); if set_content_encoding_gzip { resp = resp.header(http::header::CONTENT_ENCODING, "gzip"); @@ -708,11 +699,9 @@ pub async fn asset( Ok(resp.body(file_contents.into())?) } -fn cache_control_header_value(apictx: &Arc) -> String { - format!( - "max-age={}", - apictx.console_config.cache_control_max_age.num_seconds() - ) +fn cache_control_value(apictx: &Arc) -> String { + let max_age = apictx.console_config.cache_control_max_age.num_seconds(); + format!("max-age={max_age}") } pub async fn serve_console_index( @@ -730,7 +719,7 @@ pub async fn serve_console_index( Ok(Response::builder() .status(StatusCode::OK) .header(http::header::CONTENT_TYPE, "text/html; charset=UTF-8") - .header(http::header::CACHE_CONTROL, cache_control_header_value(apictx)) + .header(http::header::CACHE_CONTROL, cache_control_value(apictx)) .body(file_contents.into())?) } @@ -750,12 +739,9 @@ lazy_static! { /// Starting from `root_dir`, follow the segments of `path` down the file tree /// until we find a file (or not). Do not follow symlinks. -fn find_file( - path: Vec, - root_dir: &PathBuf, -) -> Result { +fn find_file(path: &PathBuf, root_dir: &PathBuf) -> Result { let mut current = root_dir.to_owned(); // start from `root_dir` - for segment in &path { + for segment in path.into_iter() { // If we hit a non-directory thing already and we still have segments // left in the path, bail. We have nowhere to go. if !current.is_dir() { @@ -789,24 +775,22 @@ mod test { use http::StatusCode; use std::{env::current_dir, path::PathBuf}; - fn get_path(path_str: &str) -> Vec { - path_str.split("/").map(|s| s.to_string()).collect() - } - #[test] fn test_find_file_finds_file() { let root = current_dir().unwrap(); - let file = find_file(get_path("tests/static/assets/hello.txt"), &root); + let file = + find_file(&PathBuf::from("tests/static/assets/hello.txt"), &root); assert!(file.is_ok()); - let file = find_file(get_path("tests/static/index.html"), &root); + let file = find_file(&PathBuf::from("tests/static/index.html"), &root); assert!(file.is_ok()); } #[test] fn test_find_file_404_on_nonexistent() { let root = current_dir().unwrap(); - let error = find_file(get_path("tests/static/nonexistent.svg"), &root) - .unwrap_err(); + let error = + find_file(&PathBuf::from("tests/static/nonexistent.svg"), &root) + .unwrap_err(); assert_eq!(error.status_code, StatusCode::NOT_FOUND); assert_eq!(error.internal_message, "failed to get file metadata",); } @@ -814,9 +798,11 @@ mod test { #[test] fn test_find_file_404_on_nonexistent_nested() { let root = current_dir().unwrap(); - let error = - find_file(get_path("tests/static/a/b/c/nonexistent.svg"), &root) - .unwrap_err(); + let error = find_file( + &PathBuf::from("tests/static/a/b/c/nonexistent.svg"), + &root, + ) + .unwrap_err(); assert_eq!(error.status_code, StatusCode::NOT_FOUND); assert_eq!(error.internal_message, "failed to get file metadata") } @@ -825,7 +811,7 @@ mod test { fn test_find_file_404_on_directory() { let root = current_dir().unwrap(); let error = - find_file(get_path("tests/static/assets/a_directory"), &root) + find_file(&PathBuf::from("tests/static/assets/a_directory"), &root) .unwrap_err(); assert_eq!(error.status_code, StatusCode::NOT_FOUND); assert_eq!(error.internal_message, "expected a non-directory"); @@ -845,7 +831,7 @@ mod test { .is_symlink()); // so we 404 - let error = find_file(get_path(path_str), &root).unwrap_err(); + let error = find_file(&PathBuf::from(path_str), &root).unwrap_err(); assert_eq!(error.status_code, StatusCode::NOT_FOUND); assert_eq!(error.internal_message, "attempted to follow a symlink"); } @@ -859,7 +845,7 @@ mod test { assert!(root.join(PathBuf::from(path_str)).exists()); // but it 404s because the path goes through a symlink - let error = find_file(get_path(path_str), &root).unwrap_err(); + let error = find_file(&PathBuf::from(path_str), &root).unwrap_err(); assert_eq!(error.status_code, StatusCode::NOT_FOUND); assert_eq!(error.internal_message, "attempted to follow a symlink"); } diff --git a/nexus/tests/integration_tests/console_api.rs b/nexus/tests/integration_tests/console_api.rs index fdcf7e3fdf..3f5600494e 100644 --- a/nexus/tests/integration_tests/console_api.rs +++ b/nexus/tests/integration_tests/console_api.rs @@ -198,6 +198,21 @@ async fn test_assets(cptestctx: &ControlPlaneTestContext) { // make sure we're not including the gzip header on non-gzipped files assert_eq!(resp.headers.get(http::header::CONTENT_ENCODING), None); + // file in a directory is returned + let resp = RequestBuilder::new( + &testctx, + Method::GET, + "/assets/a_directory/another_file.txt", + ) + .expect_status(Some(StatusCode::OK)) + .execute() + .await + .expect("failed to get existing file"); + + assert_eq!(resp.body, "some words".as_bytes()); + // make sure we're not including the gzip header on non-gzipped files + assert_eq!(resp.headers.get(http::header::CONTENT_ENCODING), None); + // file with only gzipped version 404s if request doesn't have accept-encoding: gzip let _ = RequestBuilder::new(&testctx, Method::GET, "/assets/gzip-only.txt") .expect_status(Some(StatusCode::NOT_FOUND))