mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(media): keep legacy route media-only
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -17,8 +17,9 @@ use crate::validation::{
|
||||
};
|
||||
|
||||
/// Upload route semantics. `Media` transforms recognized media, `Upload`
|
||||
/// preserves exact non-media bytes, and `Legacy` keeps the historical route
|
||||
/// while applying the same sanitizer whenever the body is recognized media.
|
||||
/// preserves exact non-media bytes, and `Legacy` keeps the historical upload
|
||||
/// authorization while otherwise enforcing the same media-only policy as
|
||||
/// `Media`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UploadRouteMode {
|
||||
Media,
|
||||
@@ -97,22 +98,7 @@ pub async fn process_streaming_ingest(
|
||||
|
||||
let source_probe = crate::sanitize::probe_media(&source_path, &sniff, config).await?;
|
||||
let is_media = source_probe.is_some();
|
||||
match (mode, is_media) {
|
||||
(UploadRouteMode::Media, false) => {
|
||||
return Err(MediaError::UnsupportedMedia(
|
||||
"non-media attachment".to_string(),
|
||||
));
|
||||
}
|
||||
(UploadRouteMode::Upload, true) => {
|
||||
return Err(MediaError::UnsupportedMedia(
|
||||
source_probe
|
||||
.as_ref()
|
||||
.map(|probe| probe.mime.clone())
|
||||
.unwrap_or_else(|| "media".to_string()),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
enforce_route_policy(mode, source_probe.as_ref())?;
|
||||
|
||||
enforce_source_size(source_probe.as_ref(), source_size, config)?;
|
||||
|
||||
@@ -307,6 +293,21 @@ pub async fn process_streaming_ingest(
|
||||
))
|
||||
}
|
||||
|
||||
fn enforce_route_policy(
|
||||
mode: UploadRouteMode,
|
||||
source_probe: Option<&crate::sanitize::MediaProbe>,
|
||||
) -> Result<(), MediaError> {
|
||||
match (mode, source_probe) {
|
||||
(UploadRouteMode::Media | UploadRouteMode::Legacy, None) => Err(
|
||||
MediaError::UnsupportedMedia("non-media attachment".to_string()),
|
||||
),
|
||||
(UploadRouteMode::Upload, Some(probe)) => {
|
||||
Err(MediaError::UnsupportedMedia(probe.mime.clone()))
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_source(
|
||||
body_stream: impl futures_core::Stream<Item = Result<Bytes, axum::Error>> + Send + 'static,
|
||||
path: &std::path::Path,
|
||||
@@ -970,6 +971,20 @@ fn build_descriptor(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn image_probe() -> crate::sanitize::MediaProbe {
|
||||
crate::sanitize::MediaProbe {
|
||||
class: crate::sanitize::MediaClass::Image,
|
||||
mime: "image/jpeg".to_string(),
|
||||
ext: "jpg".to_string(),
|
||||
video_codec: None,
|
||||
audio_codec: None,
|
||||
width: Some(1),
|
||||
height: Some(1),
|
||||
duration_secs: None,
|
||||
frame_count: Some(1),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_config() -> MediaConfig {
|
||||
MediaConfig {
|
||||
s3_endpoint: String::new(),
|
||||
@@ -1123,6 +1138,28 @@ mod tests {
|
||||
assert_eq!(detect("connection reset"), std::io::ErrorKind::Other);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_policy_keeps_legacy_media_only() {
|
||||
let probe = image_probe();
|
||||
|
||||
assert!(enforce_route_policy(UploadRouteMode::Media, Some(&probe)).is_ok());
|
||||
assert!(enforce_route_policy(UploadRouteMode::Legacy, Some(&probe)).is_ok());
|
||||
assert!(enforce_route_policy(UploadRouteMode::Upload, None).is_ok());
|
||||
|
||||
assert!(matches!(
|
||||
enforce_route_policy(UploadRouteMode::Media, None),
|
||||
Err(MediaError::UnsupportedMedia(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
enforce_route_policy(UploadRouteMode::Legacy, None),
|
||||
Err(MediaError::UnsupportedMedia(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
enforce_route_policy(UploadRouteMode::Upload, Some(&probe)),
|
||||
Err(MediaError::UnsupportedMedia(mime)) if mime == "image/jpeg"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_descriptor_no_meta() {
|
||||
// When meta is None, all optional fields should be None.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! Routes:
|
||||
//! PUT /media — BUD-05 sanitizing media upload
|
||||
//! PUT /upload — BUD-02 exact-byte non-media upload
|
||||
//! PUT /media/upload — temporary legacy compatibility route
|
||||
//! PUT /media/upload — temporary media-only legacy auth route
|
||||
//! GET /media/{sha256_ext} — BUD-01 serve blob
|
||||
//! HEAD /media/{sha256_ext} — BUD-01 existence check
|
||||
|
||||
@@ -282,7 +282,7 @@ async fn upload_attribution(
|
||||
})
|
||||
}
|
||||
|
||||
/// PUT /media, /upload, or the temporary /media/upload compatibility route.
|
||||
/// PUT /media, /upload, or the temporary media-only /media/upload route.
|
||||
///
|
||||
/// Auth is validated via the [`AuthenticatedUpload`] extractor BEFORE the body
|
||||
/// is read, preventing unauthenticated clients from forcing body buffering.
|
||||
|
||||
@@ -56,6 +56,19 @@ async fn upload_file(client: &Client, keys: &Keys, body: &[u8]) -> reqwest::Resp
|
||||
.expect("file upload request")
|
||||
}
|
||||
|
||||
async fn upload_legacy_media(client: &Client, keys: &Keys, body: &[u8]) -> reqwest::Response {
|
||||
let sha256 = hex::encode(Sha256::digest(body));
|
||||
let auth = sign_blossom_auth_for_verb(keys, &sha256, "upload");
|
||||
client
|
||||
.put(format!("{}/media/upload", relay_http_url()))
|
||||
.header("Authorization", blossom_auth_header(&auth))
|
||||
.header("X-SHA-256", &sha256)
|
||||
.body(body.to_vec())
|
||||
.send()
|
||||
.await
|
||||
.expect("legacy media upload request")
|
||||
}
|
||||
|
||||
fn blossom_auth_header(event: &nostr::Event) -> String {
|
||||
format!(
|
||||
"Nostr {}",
|
||||
@@ -486,19 +499,38 @@ async fn test_legacy_media_upload_alias_sanitizes_with_upload_verb() {
|
||||
let client = http_client();
|
||||
let keys = Keys::generate();
|
||||
let jpeg = tiny_jpeg();
|
||||
let sha256 = hex::encode(Sha256::digest(&jpeg));
|
||||
let auth = sign_blossom_auth_for_verb(&keys, &sha256, "upload");
|
||||
let resp = client
|
||||
.put(format!("{}/media/upload", relay_http_url()))
|
||||
.header("Authorization", blossom_auth_header(&auth))
|
||||
.header("X-SHA-256", &sha256)
|
||||
.body(jpeg)
|
||||
.send()
|
||||
.await
|
||||
.expect("legacy upload request");
|
||||
let resp = upload_legacy_media(&client, &keys, &jpeg).await;
|
||||
assert_eq!(resp.status(), 200);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_legacy_media_upload_alias_rejects_pdf() {
|
||||
let client = http_client();
|
||||
let keys = Keys::generate();
|
||||
let pdf = b"%PDF-1.4 fake pdf content here for testing";
|
||||
let resp = upload_legacy_media(&client, &keys, pdf).await;
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
415,
|
||||
"legacy media route must reject recognized non-media files"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_legacy_media_upload_alias_rejects_opaque_binary() {
|
||||
let client = http_client();
|
||||
let keys = Keys::generate();
|
||||
let opaque: Vec<u8> = (0..1000).map(|i| (i * 37 % 256) as u8).collect();
|
||||
let resp = upload_legacy_media(&client, &keys, &opaque).await;
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
415,
|
||||
"legacy media route must reject unknown non-media files"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_concurrent_upload_same_file() {
|
||||
|
||||
Reference in New Issue
Block a user