diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index 15bc483c..6c0ec33c 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -70,6 +70,8 @@ trust_cloudflare = false easypwned = "" # Tenor API Key tenor_key = "" +# Admin keys for future admin api and for bonfire reported image fetching +admin_keys = [] [api.security.captcha] # hCaptcha configuration diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index 6eccbf59..1c354769 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -228,6 +228,7 @@ pub struct ApiSecurity { pub trust_cloudflare: bool, pub easypwned: String, pub tenor_key: String, + pub admin_keys: Vec, } #[derive(Deserialize, Debug, Clone)] diff --git a/crates/services/autumn/src/api.rs b/crates/services/autumn/src/api.rs index c0e45d72..e30dbe78 100644 --- a/crates/services/autumn/src/api.rs +++ b/crates/services/autumn/src/api.rs @@ -5,7 +5,7 @@ use std::{ use axum::{ extract::{DefaultBodyLimit, Path, State}, - http::{header, Method}, + http::{header, HeaderMap, Method}, response::{IntoResponse, Redirect, Response}, routing::{get, post}, Json, Router, @@ -58,6 +58,7 @@ pub async fn router() -> Router { ) .route("/:tag/:file_id", get(fetch_preview)) .route("/:tag/:file_id/:file_name", get(fetch_file)) + .route("/mod/:tag/:file_id/:file_name", get(fetch_file_mod)) .layer(cors) } @@ -503,3 +504,67 @@ async fn fetch_file( .into_response() }) } + +/// Fetch original file (Moderation) +/// +/// This is intentionally left out of the OpenApi docs. +/// It uses the config key api.security.admin_keys to determine access. This is intended for server to server communication. +async fn fetch_file_mod( + State(db): State, + headers: HeaderMap, + Path((tag, file_id, file_name)): Path<(Tag, String, String)>, +) -> Result { + let config = revolt_config::config().await; + + let token = headers + .get("X-Admin-Token") + .and_then(|value| value.to_str().ok()) + .ok_or_else(|| create_error!(NotAuthenticated))?; + + if !config + .api + .security + .admin_keys + .iter() + .any(|tok| tok == token) + { + return Err(create_error!(NotAuthenticated)); + } + + let tag: &'static str = tag.clone().into(); + let file = db.fetch_attachment(tag, &file_id).await?; + + // Ignore files that haven't been attached + if file.used_for.is_none() { + return Err(create_error!(NotFound)); + } + + // Ensure filename is correct + if file_name != file.filename { + if file_name == "original" { + let safe_filename = encode_component(&file.filename); + + return Ok( + Redirect::permanent(&format!("/{tag}/{file_id}/{}", safe_filename)).into_response(), + ); + } + + return Err(create_error!(NotFound)); + } + + let hash = file.as_hash(&db).await?; + retrieve_file_by_hash(&hash).await.map(|data| { + ( + [ + (header::CONTENT_TYPE, hash.content_type), + (header::CONTENT_DISPOSITION, "attachment".to_owned()), + ( + header::CACHE_CONTROL, + "private, max-age=300, must-revalidate".to_owned(), + ), + ], + data, + ) + .into_response() + }) +}