Skip to content

Commit

Permalink
Add validations
Browse files Browse the repository at this point in the history
Signed-off-by: lloydmeta <[email protected]>
  • Loading branch information
lloydmeta committed Nov 1, 2024
1 parent bed4e57 commit 72ea7e9
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 13 deletions.
5 changes: 5 additions & 0 deletions server/src/api/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ pub struct Standard {
pub message: String,
}

#[derive(Serialize)]
pub struct FailedValidations {
pub errors: Vec<String>,
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct MetadataResponse {
pub source: Source,
Expand Down
37 changes: 25 additions & 12 deletions server/src/api/routing/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use responses::Standard;
use tower_http::catch_panic::CatchPanicLayer;

use crate::api::requests::{ImageResizePathParam, Signature};
use crate::api::responses::{self, MetadataResponse};
use crate::api::responses::{self, FailedValidations, MetadataResponse};
use crate::infra::components::AppComponents;
use crate::infra::config::AuthenticationSettings;
use crate::infra::errors::AppError;
Expand All @@ -30,6 +30,7 @@ use crate::infra::image_caching::{
ImageResizedCacheRequest,
};
use crate::infra::image_manipulation::{Operations, OperationsRunner, SingletonOperationsRunner};
use crate::infra::validations::{SimpleValidator, Validator};

use miniaturs_shared::signature::{ensure_signature_is_valid_for_path_and_query, SignatureError};

Expand Down Expand Up @@ -63,6 +64,7 @@ async fn resize(
signature,
)?;
let operations = Operations::build(&Some(resized_image.into()));
SimpleValidator.validate_operations(&app_components.config.validation_settings, &operations)?;
let processed_image_request = {
ImageResizeRequest {
requested_image_url: image_url.clone(),
Expand Down Expand Up @@ -141,11 +143,12 @@ async fn resize(
.format()
.ok_or(AppError::UnableToDetermineFormat)?;

let original_image = reader_with_format.decode()?;
SimpleValidator
.validate_source_image(&app_components.config.validation_settings, &original_image)?;

let image = SingletonOperationsRunner
.run(
reader_with_format.decode()?,
&processed_image_request.operations,
)
.run(original_image, &processed_image_request.operations)
.await;

let mut cursor = Cursor::new(Vec::new());
Expand Down Expand Up @@ -189,9 +192,12 @@ async fn metadata(
)?;

let operations = Operations::build(&Some(resized_image.into()));
let metadata = MetadataResponse::build(&image_url, &operations);
let mut response_headers = HeaderMap::new();
response_headers.insert(CACHE_CONTROL, CACHE_CONTROL_HEADER_VALUE);

SimpleValidator.validate_operations(&app_components.config.validation_settings, &operations)?;

let metadata = MetadataResponse::build(&image_url, &operations);
Ok((StatusCode::OK, response_headers, Json(metadata)).into_response())
}

Expand Down Expand Up @@ -267,27 +273,34 @@ fn ensure_signature_is_valid(

impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let result = match self {
match self {
Self::CatchAll(anyhow_err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(Standard {
message: anyhow_err.to_string(),
}),
),
).into_response(),
Self::BadSignature(signature) => (
StatusCode::UNAUTHORIZED,
Json(Standard {
message: format!("The signature you provided [{signature}] was not correct"),
}),
),
).into_response(),
Self::UnableToDetermineFormat => (
StatusCode::BAD_REQUEST,
Json(Standard {
message: "An image format could not be determined. Make sure the extension or the content-type header is sensible.".to_string(),
}),
),
};
result.into_response()
).into_response(),
Self::ValidationFailed(errors) => (
StatusCode::BAD_REQUEST,
Json(
FailedValidations {
errors
}
)
).into_response(),
}
}
}

Expand Down
58 changes: 58 additions & 0 deletions server/src/infra/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ const SHARED_SECRET_ENV_KEY: &str = "MINIATURS_SHARED_SECRET";
const PROCESSED_IMAGES_BUCKET_NAME_ENV_KEY: &str = "PROCESSED_IMAGES_BUCKET";
const UNPROCESSED_IMAGES_BUCKET_NAME_ENV_KEY: &str = "UNPROCESSED_IMAGES_BUCKET";
const REQUIRE_PATH_STYLE_S3_KEY: &str = "REQUIRE_PATH_STYLE_S3";
const MAX_RESIZE_TARGET_WIDTH: &str = "MAX_RESIZE_TARGET_WIDTH";
const MAX_RESIZE_TARGET_HEIGHT: &str = "MAX_RESIZE_TARGET_HEIGHT";
const MAX_SOURCE_TARGET_WIDTH: &str = "MAX_SOURCE_TARGET_WIDTH";
const MAX_SOURCE_TARGET_HEIGHT: &str = "MAX_SOURCE_TARGET_HEIGHT";

#[derive(Clone)]
pub struct Config {
pub authentication_settings: AuthenticationSettings,
pub image_cache_settings: ImageCacheSettings,
pub aws_settings: AwsSettings,
pub validation_settings: ValidationSettings,
}

#[derive(Clone)]
Expand All @@ -32,6 +37,31 @@ pub struct AwsSettings {
pub path_style_s3: bool,
}

#[derive(Clone)]
pub struct ValidationSettings {
// Max width that we will resize to (pixels)
pub max_resize_target_width: u32,
// Max height that we will resize to (pixels)
pub max_resize_target_height: u32,
// Max width the source image can have (pixels)
pub max_source_image_width: u32,
// Max height the source image can have (pixels)
pub max_source_image_height: u32,
}

static MAX_PIXELS_DEFAULT: u32 = 10000;

impl Default for ValidationSettings {
fn default() -> Self {
Self {
max_resize_target_width: MAX_PIXELS_DEFAULT,
max_resize_target_height: MAX_PIXELS_DEFAULT,
max_source_image_width: MAX_PIXELS_DEFAULT,
max_source_image_height: MAX_PIXELS_DEFAULT,
}
}
}

impl Config {
pub async fn load_env() -> anyhow::Result<Config> {
let shared_secret = env::var(SHARED_SECRET_ENV_KEY)
Expand Down Expand Up @@ -59,10 +89,38 @@ impl Config {
path_style_s3,
};

let mut validation_settings = ValidationSettings::default();

if let Some(max_resize_target_width) = env::var(MAX_RESIZE_TARGET_WIDTH)
.ok()
.and_then(|s| s.parse().ok())
{
validation_settings.max_resize_target_width = max_resize_target_width;
}
if let Some(max_resize_target_height) = env::var(MAX_RESIZE_TARGET_HEIGHT)
.ok()
.and_then(|s| s.parse().ok())
{
validation_settings.max_resize_target_height = max_resize_target_height;
}
if let Some(max_source_image_width) = env::var(MAX_SOURCE_TARGET_WIDTH)
.ok()
.and_then(|s| s.parse().ok())
{
validation_settings.max_source_image_width = max_source_image_width;
}
if let Some(max_source_image_height) = env::var(MAX_SOURCE_TARGET_HEIGHT)
.ok()
.and_then(|s| s.parse().ok())
{
validation_settings.max_source_image_height = max_source_image_height;
}

Ok(Config {
authentication_settings,
image_cache_settings,
aws_settings,
validation_settings,
})
}
}
9 changes: 9 additions & 0 deletions server/src/infra/errors.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use super::validations::ValidationErrors;

#[derive(Debug)]
pub enum AppError {
CatchAll(anyhow::Error),
BadSignature(String),
ValidationFailed(Vec<String>),
UnableToDetermineFormat,
}

Expand All @@ -15,3 +18,9 @@ where
AppError::CatchAll(err.into())
}
}

impl From<ValidationErrors> for AppError {
fn from(value: ValidationErrors) -> Self {
AppError::ValidationFailed(value.0)
}
}
1 change: 1 addition & 0 deletions server/src/infra/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pub mod config;
pub mod errors;
pub mod image_caching;
pub mod image_manipulation;
pub mod validations;
164 changes: 164 additions & 0 deletions server/src/infra/validations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use image::DynamicImage;

use super::{config::ValidationSettings, image_manipulation::Operations};

pub trait Validator {
fn validate_operations(
&self,
settings: &ValidationSettings,
operations: &Operations,
) -> Result<(), ValidationErrors>;

fn validate_source_image(
&self,
settings: &ValidationSettings,
image: &DynamicImage,
) -> Result<(), ValidationErrors>;
}

pub struct SimpleValidator;

pub struct ValidationErrors(pub Vec<String>);

impl Validator for SimpleValidator {
fn validate_operations(
&self,
settings: &ValidationSettings,
operations: &Operations,
) -> Result<(), ValidationErrors> {
let problems = operations
.0
.iter()
.fold(Vec::new(), |mut next, op| match *op {
crate::infra::image_manipulation::Operation::Resize { width, height } => {
if width > settings.max_resize_target_width {
next.push(format!(
"Resize target width [{width}] too large, must be [{}] or lower",
settings.max_resize_target_width
));
}
if height > settings.max_resize_target_height {
next.push(format!(
"Resize target height [{height}] too large, must be [{}] or lower",
settings.max_resize_target_height
));
}
next
}
crate::infra::image_manipulation::Operation::FlipHorizontally => next,
crate::infra::image_manipulation::Operation::FlipVertically => next,
});
if problems.is_empty() {
Ok(())
} else {
Err(ValidationErrors(problems))
}
}

fn validate_source_image(
&self,
settings: &ValidationSettings,
image: &DynamicImage,
) -> Result<(), ValidationErrors> {
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",
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",
image.height(),
settings.max_source_image_height
));
}
if problems.is_empty() {
Ok(())
} else {
Err(ValidationErrors(problems))
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_empty_operations_validation() {
let settings = ValidationSettings::default();
let operations = Operations(vec![]);
assert!(SimpleValidator
.validate_operations(&settings, &operations)
.is_ok());
}

#[test]
fn test_non_empty_good_operations_validation() {
let settings = ValidationSettings::default();
let operations = Operations(vec![
crate::infra::image_manipulation::Operation::Resize {
width: settings.max_resize_target_width,
height: settings.max_resize_target_height,
},
crate::infra::image_manipulation::Operation::FlipHorizontally,
crate::infra::image_manipulation::Operation::FlipVertically,
]);
assert!(SimpleValidator
.validate_operations(&settings, &operations)
.is_ok());
}

#[test]
fn test_non_empty_vad_operations_validation() {
let settings = ValidationSettings::default();
let operations = Operations(vec![
crate::infra::image_manipulation::Operation::Resize {
width: settings.max_resize_target_width + 1,
height: settings.max_resize_target_height + 1,
},
crate::infra::image_manipulation::Operation::FlipHorizontally,
crate::infra::image_manipulation::Operation::FlipVertically,
]);
let r = SimpleValidator.validate_operations(&settings, &operations);
assert!(r.is_err());
if let Err(problems) = r {
assert_eq!(2, problems.0.len());
} else {
panic!("Fail")
}
}

#[test]
fn test_non_empty_good_image_validation() {
let settings = ValidationSettings::default();
let image = DynamicImage::new(
settings.max_source_image_width,
settings.max_source_image_height,
image::ColorType::Rgb8,
);
assert!(SimpleValidator
.validate_source_image(&settings, &image)
.is_ok());
}

#[test]
fn test_non_empty_bad_image_validation() {
let settings = ValidationSettings::default();
let image = DynamicImage::new(
settings.max_source_image_width + 1,
settings.max_source_image_height + 1,
image::ColorType::Rgb8,
);
let r = SimpleValidator.validate_source_image(&settings, &image);
assert!(r.is_err());
if let Err(problems) = r {
assert_eq!(2, problems.0.len());
} else {
panic!("Fail")
}
}
}
Loading

0 comments on commit 72ea7e9

Please sign in to comment.