From b62eeef80c778d7acc539e550f2e462557465611 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 22 Dec 2024 12:35:06 +0000 Subject: [PATCH] chore(autumn): conditional animation stripping feat(autumn): add /original redirect for files fix(autumn): block non-images for non-attachment tags fix(ci): use correct port for Rust test --- .github/workflows/rust.yaml | 4 +- crates/services/autumn/src/api.rs | 84 +++++++++++++++++++------------ 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index 8c8396fae..f0bd33894 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -67,7 +67,7 @@ jobs: if: github.event_name != 'pull_request' && github.ref_name == 'main' uses: nev7n/wait_for_response@v1 with: - url: "http://localhost:8000/" + url: "http://localhost:14702/" - name: Checkout API repository if: github.event_name != 'pull_request' && github.ref_name == 'main' @@ -79,7 +79,7 @@ jobs: - name: Download OpenAPI specification if: github.event_name != 'pull_request' && github.ref_name == 'main' - run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json + run: curl http://localhost:14702/openapi.json -o api/OpenAPI.json - name: Commit changes if: github.event_name != 'pull_request' && github.ref_name == 'main' diff --git a/crates/services/autumn/src/api.rs b/crates/services/autumn/src/api.rs index 59aac9a0e..d068c2dcd 100644 --- a/crates/services/autumn/src/api.rs +++ b/crates/services/autumn/src/api.rs @@ -216,6 +216,27 @@ async fn upload_file( nanoid::nanoid!(42) }; + // Determine the mime type for the file + let mime_type = determine_mime_type(&mut file.contents, &buf, &filename); + + // Check blocklist for mime type + if config + .files + .blocked_mime_types + .iter() + .any(|m| m == mime_type) + { + return Err(create_error!(FileTypeNotAllowed)); + } + + // Determine metadata for the file + let metadata = generate_metadata(&file.contents, mime_type); + + // Block non-images for non-attachment uploads + if !matches!(tag, Tag::attachments) && !matches!(metadata, Metadata::Image { .. }) { + return Err(create_error!(FileTypeNotAllowed)); + } + // Find an existing hash and use that if possible let file_hash_exists = if let Ok(file_hash) = db .fetch_attachment_hash(&format!("{original_hash:02x}")) @@ -239,22 +260,6 @@ async fn upload_file( false }; - // Determine the mime type for the file - let mime_type = determine_mime_type(&mut file.contents, &buf, &filename); - - // Check blocklist for mime type - if config - .files - .blocked_mime_types - .iter() - .any(|m| m == mime_type) - { - return Err(create_error!(FileTypeNotAllowed)); - } - - // Determine metadata for the file - let metadata = generate_metadata(&file.contents, mime_type); - // Strip metadata let (buf, metadata) = strip_metadata(file.contents, buf, metadata, mime_type).await?; @@ -322,21 +327,23 @@ pub static CACHE_CONTROL: &str = "public, max-age=604800, must-revalidate"; /// Fetch preview of file /// -/// This route will only return image content. +/// This route will only return image content.
/// For all other file types, please use the fetch route (you will receive a redirect if you try to use this route anyways!). /// /// Depending on the given tag, the file will be re-processed to fit the criteria: /// -/// | Tag | Image Resolution | -/// | :-: | --- | -/// | attachments | Up to 1280px on any axis | -/// | avatars | Up to 128px on any axis | -/// | backgrounds | Up to 1280x720px | -/// | icons | Up to 128px on any axis | -/// | banners | Up to 480px on any axis | -/// | emojis | Up to 128px on any axis | +/// | Tag | Image Resolution | Animations stripped by preview | +/// | :-: | --- | :-: | +/// | attachments | Up to 1280px on any axis | ❌ | +/// | avatars | Up to 128px on any axis | ✅ | +/// | backgrounds | Up to 1280x720px | ❌ | +/// | icons | Up to 128px on any axis | ✅ | +/// | banners | Up to 480px on any axis | ❌ | +/// | emojis | Up to 128px on any axis | ❌ | /// /// aspect ratio will always be preserved +/// +/// to fetch animated variant, suffix `/{file_name}` or `/original` to the path #[utoipa::path( get, path = "/{tag}/{file_id}", @@ -352,8 +359,8 @@ async fn fetch_preview( State(db): State, Path((tag, file_id)): Path<(Tag, String)>, ) -> Result { - let tag: &'static str = tag.into(); - let file = db.fetch_attachment(tag, &file_id).await?; + let tag_str: &'static str = tag.clone().into(); + let file = db.fetch_attachment(tag_str, &file_id).await?; // Ignore deleted files if file.deleted.is_some_and(|v| v) { @@ -367,10 +374,14 @@ async fn fetch_preview( let hash = file.as_hash(&db).await?; - // Only process image files and don't process GIFs - if !matches!(hash.metadata, Metadata::Image { .. }) || hash.content_type == "image/gif" { + let is_animated = hash.content_type == "image/gif"; // TODO: extract this data from files + + // Only process image files and don't process GIFs if not avatar or icon + if !matches!(hash.metadata, Metadata::Image { .. }) + || (is_animated && !matches!(tag, Tag::avatars | Tag::icons)) + { return Ok( - Redirect::permanent(&format!("/{tag}/{file_id}/{}", file.filename)).into_response(), + Redirect::permanent(&format!("/{tag_str}/{file_id}/{}", file.filename)).into_response(), ); } @@ -380,7 +391,7 @@ async fn fetch_preview( // Read image and create thumbnail let data = create_thumbnail( decode_image(&mut Cursor::new(data), &file.content_type)?, - tag, + tag_str, ) .await; @@ -398,6 +409,8 @@ async fn fetch_preview( /// Fetch original file /// /// Content disposition header will be set to 'attachment' to prevent browser from rendering anything. +/// +/// Using `original` as the file name parameter will redirect you to the original file. #[utoipa::path( get, path = "/{tag}/{file_id}/{file_name}", @@ -414,7 +427,8 @@ async fn fetch_file( State(db): State, Path((tag, file_id, file_name)): Path<(Tag, String, String)>, ) -> Result { - let file = db.fetch_attachment(tag.into(), &file_id).await?; + let tag: &'static str = tag.clone().into(); + let file = db.fetch_attachment(tag, &file_id).await?; // Ignore deleted files if file.deleted.is_some_and(|v| v) { @@ -428,6 +442,12 @@ async fn fetch_file( // Ensure filename is correct if file_name != file.filename { + if file_name == "original" { + return Ok( + Redirect::permanent(&format!("/{tag}/{file_id}/{}", file.filename)).into_response(), + ); + } + return Err(create_error!(NotFound)); }