Skip to content

Commit

Permalink
Add file size 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 5bb314a commit 298a5f0
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 50 deletions.
1 change: 1 addition & 0 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
84 changes: 50 additions & 34 deletions server/src/api/routing/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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};

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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));

Expand All @@ -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)
Expand Down Expand Up @@ -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())
Expand Down
23 changes: 20 additions & 3 deletions server/src/infra/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
}
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -119,15 +136,15 @@ impl Config {
fn read_env_var<T>(env_var_key: &str) -> anyhow::Result<Option<T>>
where
T: FromStr,
<T as FromStr>::Err: std::error::Error,
<T as FromStr>::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: <T as FromStr>::Err| {
anyhow::anyhow!("Could not convert {env_var_key}: [{}]", e.to_string())
})?)),
}
}
Loading

0 comments on commit 298a5f0

Please sign in to comment.