diff --git a/server/Cargo.toml b/server/Cargo.toml index 73b2b98..bbc03f1 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -34,6 +34,7 @@ sha256 = "1.5" http-body-util = "0.1" bytes = "1.7" tower-http = { version = "0.6.1", features = ["catch-panic"] } +bytesize = "1.3" [dev-dependencies] ctor = "0.2.8" diff --git a/server/src/api/routing/handlers.rs b/server/src/api/routing/handlers.rs index 5a8a3f8..ea23d04 100644 --- a/server/src/api/routing/handlers.rs +++ b/server/src/api/routing/handlers.rs @@ -7,8 +7,9 @@ use axum::http::{HeaderMap, HeaderValue, StatusCode, Uri}; use axum::response::{Html, IntoResponse, Response}; use axum::{response::Json, routing::*, Router}; +use bytesize::ByteSize; use image::{ImageFormat, ImageReader}; -use reqwest::header::{CACHE_CONTROL, CONTENT_TYPE}; +use reqwest::header::{CACHE_CONTROL, CONTENT_LENGTH, CONTENT_TYPE}; use responses::Standard; use tower_http::catch_panic::CatchPanicLayer; @@ -22,7 +23,7 @@ use crate::infra::image_caching::{ ImageResizedCacheRequest, }; use crate::infra::image_manipulation::{Operations, OperationsRunner, SingletonOperationsRunner}; -use crate::infra::validations::{SimpleValidator, Validator}; +use crate::infra::validations::{SingletonValidator, Validator}; use miniaturs_shared::signature::{ensure_signature_is_valid_for_path_and_query, SignatureError}; @@ -55,8 +56,9 @@ async fn resize( &uri, signature, )?; + let validation_settings = &app_components.config.validation_settings; let operations = Operations::build(&Some(resized_image.into())); - SimpleValidator.validate_operations(&app_components.config.validation_settings, &operations)?; + SingletonValidator.validate_operations(validation_settings, &operations)?; let processed_image_request = { ImageResizeRequest { requested_image_url: image_url.clone(), @@ -87,35 +89,49 @@ async fn resize( .get(&unprocessed_cache_retrieve_req) .await?; - let (response_status_code, bytes, maybe_content_type_string) = - if let Some(cached_fetched) = maybe_cached_fetched_image { - ( - StatusCode::OK, - cached_fetched.bytes, - cached_fetched.requested.content_type, - ) - } else { - let mut proxy_response = app_components.http_client.get(&image_url).send().await?; - let status_code = proxy_response.status(); - let headers = proxy_response.headers_mut(); - let maybe_content_type = headers.remove(CONTENT_TYPE); - - let maybe_content_type_string = - maybe_content_type.and_then(|h| h.to_str().map(|s| s.to_string()).ok()); - - let cache_fetched_req = ImageFetchedCacheRequest { - request: unprocessed_cache_retrieve_req, - content_type: maybe_content_type_string.clone(), - }; - let bytes: Vec<_> = proxy_response.bytes().await?.into(); - app_components - .unprocessed_images_cacher - .set(&bytes, &cache_fetched_req) - .await?; - - let response_status_code = StatusCode::from_u16(status_code.as_u16())?; - (response_status_code, bytes, maybe_content_type_string) + let (response_status_code, bytes, maybe_content_type_string) = if let Some(cached_fetched) = + maybe_cached_fetched_image + { + ( + StatusCode::OK, + cached_fetched.bytes, + cached_fetched.requested.content_type, + ) + } else { + let mut proxy_response = app_components.http_client.get(&image_url).send().await?; + let status_code = proxy_response.status(); + let headers = proxy_response.headers_mut(); + + let maybe_content_length = headers.remove(CONTENT_LENGTH); + let maybe_content_length_bytesize = + maybe_content_length.and_then(|h| h.to_str().ok()?.parse().ok()); + + if let Some(content_length_bytesize) = maybe_content_length_bytesize { + SingletonValidator + .validate_image_download_size(validation_settings, content_length_bytesize)?; + } + + let maybe_content_type = headers.remove(CONTENT_TYPE); + + let maybe_content_type_string = + maybe_content_type.and_then(|h| h.to_str().map(|s| s.to_string()).ok()); + + let cache_fetched_req = ImageFetchedCacheRequest { + request: unprocessed_cache_retrieve_req, + content_type: maybe_content_type_string.clone(), }; + let bytes: Vec<_> = proxy_response.bytes().await?.into(); + + SingletonValidator + .validate_image_size(validation_settings, ByteSize::b(bytes.len() as u64))?; + app_components + .unprocessed_images_cacher + .set(&bytes, &cache_fetched_req) + .await?; + + let response_status_code = StatusCode::from_u16(status_code.as_u16())?; + (response_status_code, bytes, maybe_content_type_string) + }; let mut image_reader = ImageReader::new(Cursor::new(bytes)); @@ -136,8 +152,7 @@ async fn resize( .ok_or(AppError::UnableToDetermineFormat)?; let original_image = reader_with_format.decode()?; - SimpleValidator - .validate_source_image(&app_components.config.validation_settings, &original_image)?; + SingletonValidator.validate_source_image(validation_settings, &original_image)?; let image = SingletonOperationsRunner .run(original_image, &processed_image_request.operations) @@ -187,7 +202,8 @@ async fn metadata( let mut response_headers = HeaderMap::new(); response_headers.insert(CACHE_CONTROL, CACHE_CONTROL_HEADER_VALUE); - SimpleValidator.validate_operations(&app_components.config.validation_settings, &operations)?; + SingletonValidator + .validate_operations(&app_components.config.validation_settings, &operations)?; let metadata = MetadataResponse::build(&image_url, &operations); Ok((StatusCode::OK, response_headers, Json(metadata)).into_response()) diff --git a/server/src/infra/config.rs b/server/src/infra/config.rs index f368c9d..d33257f 100644 --- a/server/src/infra/config.rs +++ b/server/src/infra/config.rs @@ -5,6 +5,7 @@ use std::{ use anyhow::Context; use aws_config::{BehaviorVersion, SdkConfig}; +use bytesize::ByteSize; const SHARED_SECRET_ENV_KEY: &str = "MINIATURS_SHARED_SECRET"; const PROCESSED_IMAGES_BUCKET_NAME_ENV_KEY: &str = "PROCESSED_IMAGES_BUCKET"; @@ -14,6 +15,8 @@ const MAX_RESIZE_TARGET_WIDTH: &str = "MAX_RESIZE_TARGET_WIDTH"; const MAX_RESIZE_TARGET_HEIGHT: &str = "MAX_RESIZE_TARGET_HEIGHT"; const MAX_SOURCE_IMAGE_WIDTH: &str = "MAX_SOURCE_IMAGE_WIDTH"; const MAX_SOURCE_IMAGE_HEIGHT: &str = "MAX_SOURCE_IMAGE_HEIGHT"; +const MAX_IMAGE_DOWNLOAD_SIZE_KEY: &str = "MAX_IMAGE_DOWNLOAD_SIZE"; +const MAX_IMAGE_FILE_SIZE_KEY: &str = "MAX_IMAGE_FILE_SIZE"; #[derive(Clone)] pub struct Config { @@ -50,9 +53,15 @@ pub struct ValidationSettings { pub max_source_image_width: u32, // Max height the source image can have (pixels) pub max_source_image_height: u32, + // Max image download size + pub max_source_image_download_size: ByteSize, + // Max image size + pub max_source_image_size: ByteSize, } static MAX_PIXELS_DEFAULT: u32 = 10000; +static MAX_IMAGE_DOWNLOAD_SIZE: ByteSize = ByteSize::mb(10); +static MAX_IMAGE_FILE_SIZE: ByteSize = ByteSize::mb(10); impl Default for ValidationSettings { fn default() -> Self { @@ -61,6 +70,8 @@ impl Default for ValidationSettings { max_resize_target_height: MAX_PIXELS_DEFAULT, max_source_image_width: MAX_PIXELS_DEFAULT, max_source_image_height: MAX_PIXELS_DEFAULT, + max_source_image_download_size: MAX_IMAGE_DOWNLOAD_SIZE, + max_source_image_size: MAX_IMAGE_FILE_SIZE, } } } @@ -106,6 +117,12 @@ impl Config { if let Some(max_source_image_height) = read_env_var(MAX_SOURCE_IMAGE_HEIGHT)? { validation_settings.max_source_image_height = max_source_image_height; } + if let Some(max_source_image_download_size) = read_env_var(MAX_IMAGE_DOWNLOAD_SIZE_KEY)? { + validation_settings.max_source_image_download_size = max_source_image_download_size; + } + if let Some(max_source_image_size) = read_env_var(MAX_IMAGE_FILE_SIZE_KEY)? { + validation_settings.max_source_image_size = max_source_image_size; + } Ok(Config { authentication_settings, @@ -119,15 +136,15 @@ impl Config { fn read_env_var(env_var_key: &str) -> anyhow::Result> where T: FromStr, - ::Err: std::error::Error, + ::Err: ToString, { match env::var(env_var_key) { Err(VarError::NotPresent) => Ok(None), Err(VarError::NotUnicode(s)) => Err(anyhow::anyhow!( "Could not decode env var {env_var_key} [{s:?}]" )), - Ok(s) => Ok(Some(s.parse().map_err(|e| { - anyhow::anyhow!("Could not convert {env_var_key}: [{e}]") + Ok(s) => Ok(Some(s.parse().map_err(|e: ::Err| { + anyhow::anyhow!("Could not convert {env_var_key}: [{}]", e.to_string()) })?)), } } diff --git a/server/src/infra/validations.rs b/server/src/infra/validations.rs index a6b094a..7a9a1b3 100644 --- a/server/src/infra/validations.rs +++ b/server/src/infra/validations.rs @@ -1,3 +1,4 @@ +use bytesize::ByteSize; use image::DynamicImage; use super::{config::ValidationSettings, image_manipulation::Operations}; @@ -14,13 +15,25 @@ pub trait Validator { settings: &ValidationSettings, image: &DynamicImage, ) -> Result<(), ValidationErrors>; + + fn validate_image_download_size( + &self, + settings: &ValidationSettings, + image_download_size: ByteSize, + ) -> Result<(), ValidationErrors>; + + fn validate_image_size( + &self, + settings: &ValidationSettings, + image_: ByteSize, + ) -> Result<(), ValidationErrors>; } -pub struct SimpleValidator; +pub struct SingletonValidator; pub struct ValidationErrors(pub Vec); -impl Validator for SimpleValidator { +impl Validator for SingletonValidator { fn validate_operations( &self, settings: &ValidationSettings, @@ -63,14 +76,14 @@ impl Validator for SimpleValidator { let mut problems = Vec::new(); if image.width() > settings.max_source_image_width { problems.push(format!( - "Source image width [{}] to large, must be [{}] or lower", + "Source image width [{}] too large, must be [{}] or lower", image.width(), settings.max_source_image_width )); } if image.height() > settings.max_source_image_height { problems.push(format!( - "Source image height [{}] to large, must be [{}] or lower", + "Source image height [{}] too large, must be [{}] or lower", image.height(), settings.max_source_image_height )); @@ -81,6 +94,36 @@ impl Validator for SimpleValidator { Err(ValidationErrors(problems)) } } + + fn validate_image_download_size( + &self, + settings: &ValidationSettings, + image_download_size: ByteSize, + ) -> Result<(), ValidationErrors> { + if image_download_size > settings.max_source_image_download_size { + Err(ValidationErrors(vec![format!( + "Image download size [{image_download_size}] is too large, must be [{}] or lower", + settings.max_source_image_download_size + )])) + } else { + Ok(()) + } + } + + fn validate_image_size( + &self, + settings: &ValidationSettings, + image_size: ByteSize, + ) -> Result<(), ValidationErrors> { + if image_size > settings.max_source_image_size { + Err(ValidationErrors(vec![format!( + "Image size [{image_size}] is too large, must be [{}] or lower", + settings.max_source_image_size + )])) + } else { + Ok(()) + } + } } #[cfg(test)] @@ -91,7 +134,7 @@ mod tests { fn test_empty_operations_validation() { let settings = ValidationSettings::default(); let operations = Operations(vec![]); - assert!(SimpleValidator + assert!(SingletonValidator .validate_operations(&settings, &operations) .is_ok()); } @@ -107,7 +150,7 @@ mod tests { crate::infra::image_manipulation::Operation::FlipHorizontally, crate::infra::image_manipulation::Operation::FlipVertically, ]); - assert!(SimpleValidator + assert!(SingletonValidator .validate_operations(&settings, &operations) .is_ok()); } @@ -123,7 +166,7 @@ mod tests { crate::infra::image_manipulation::Operation::FlipHorizontally, crate::infra::image_manipulation::Operation::FlipVertically, ]); - let r = SimpleValidator.validate_operations(&settings, &operations); + let r = SingletonValidator.validate_operations(&settings, &operations); let errors = r.err().unwrap(); assert_eq!(1, errors.0.len()); @@ -141,7 +184,7 @@ mod tests { crate::infra::image_manipulation::Operation::FlipHorizontally, crate::infra::image_manipulation::Operation::FlipVertically, ]); - let r = SimpleValidator.validate_operations(&settings, &operations); + let r = SingletonValidator.validate_operations(&settings, &operations); let errors = r.err().unwrap(); assert_eq!(1, errors.0.len()); @@ -159,7 +202,7 @@ mod tests { crate::infra::image_manipulation::Operation::FlipHorizontally, crate::infra::image_manipulation::Operation::FlipVertically, ]); - let r = SimpleValidator.validate_operations(&settings, &operations); + let r = SingletonValidator.validate_operations(&settings, &operations); let errors = r.err().unwrap(); assert_eq!(2, errors.0.len()); @@ -175,7 +218,7 @@ mod tests { settings.max_source_image_height, image::ColorType::Rgb8, ); - assert!(SimpleValidator + assert!(SingletonValidator .validate_source_image(&settings, &image) .is_ok()); } @@ -188,7 +231,7 @@ mod tests { settings.max_source_image_height, image::ColorType::Rgb8, ); - let r = SimpleValidator.validate_source_image(&settings, &image); + let r = SingletonValidator.validate_source_image(&settings, &image); let err = r.err().unwrap(); assert_eq!(1, err.0.len()); assert!(err.0[0].starts_with("Source image width")); @@ -202,7 +245,7 @@ mod tests { settings.max_source_image_height + 1, image::ColorType::Rgb8, ); - let r = SimpleValidator.validate_source_image(&settings, &image); + let r = SingletonValidator.validate_source_image(&settings, &image); let err = r.err().unwrap(); assert_eq!(1, err.0.len()); assert!(err.0[0].starts_with("Source image height")); @@ -216,10 +259,47 @@ mod tests { settings.max_source_image_height + 1, image::ColorType::Rgb8, ); - let r = SimpleValidator.validate_source_image(&settings, &image); + let r = SingletonValidator.validate_source_image(&settings, &image); let err = r.err().unwrap(); assert_eq!(2, err.0.len()); assert!(err.0[0].starts_with("Source image width")); assert!(err.0[1].starts_with("Source image height")); } + + #[test] + fn test_image_download_size_validation_ok() { + let settings = ValidationSettings::default(); + let r = SingletonValidator + .validate_image_download_size(&settings, settings.max_source_image_download_size); + assert!(r.is_ok()); + } + #[test] + fn test_image_download_size_validation_err() { + let settings = ValidationSettings::default(); + let r = SingletonValidator.validate_image_download_size( + &settings, + settings.max_source_image_download_size + ByteSize::b(1), + ); + assert!(r.is_err()); + let err = r.err().unwrap(); + assert_eq!(1, err.0.len()); + assert!(err.0[0].starts_with("Image download size")); + } + #[test] + fn test_image_size_validation_ok() { + let settings = ValidationSettings::default(); + let r = SingletonValidator.validate_image_size(&settings, settings.max_source_image_size); + assert!(r.is_ok()); + } + + #[test] + fn test_image_size_validation_err() { + let settings = ValidationSettings::default(); + let r = SingletonValidator + .validate_image_size(&settings, settings.max_source_image_size + ByteSize::b(1)); + assert!(r.is_err()); + let err = r.err().unwrap(); + assert_eq!(1, err.0.len()); + assert!(err.0[0].starts_with("Image size")); + } }