refactor(media-proxy): split into modules and harden streaming (#2437)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-03 22:04:00 +02:00
1 parent 16ae98e189
commit d7d4e8da03
214 files changed
+47213 -18092

No files matched your search

@@ -0,0 +1,103 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::{
asset_hash::{self, AssetHash},
disposition::{self, Decision, PUBLIC_MEDIA_MAX_HEADER_VALUE_BYTES},
query::Query,
test_fixtures::ADVERSARIAL_TEXT_INPUTS,
upload_relay::token::token_from_query,
};
const RELAY_TOKEN_BYTES_MAX: usize = 16 * 1024;
#[test]
fn adversarial_text_never_escapes_the_asset_hash_digest_contract() {
for text in ADVERSARIAL_TEXT_INPUTS {
let parsed = AssetHash::parse(text);
assert!(
text.ends_with(parsed.digest()),
"the digest is not a suffix of {text}"
);
assert_eq!(
parsed.is_animated(),
text.starts_with("a_"),
"the animation flag disagrees with {text}"
);
assert_eq!(
text.len() - parsed.digest().len(),
if parsed.is_animated() { 2 } else { 0 },
"the digest dropped bytes from {text}"
);
assert_eq!(parsed.digest(), asset_hash::strip_animation_prefix(text));
}
}
#[test]
fn adversarial_query_text_yields_at_most_one_relay_token() {
for text in ADVERSARIAL_TEXT_INPUTS {
let query = Query::parse(text);
for key in ["t", "token", ""] {
let Ok(token) = token_from_query(query.get(key)) else {
continue;
};
assert!(!token.is_empty(), "an empty token was accepted from {text}");
assert!(
!token.contains('&'),
"a multi-parameter token was accepted from {text}"
);
assert!(
token.len() <= RELAY_TOKEN_BYTES_MAX,
"an unbounded token was accepted from {text}"
);
}
if let Ok(token) = token_from_query(Some(text)) {
assert!(!token.is_empty(), "an empty token was accepted from {text}");
assert!(token.len() <= RELAY_TOKEN_BYTES_MAX);
}
}
}
#[test]
fn adversarial_filenames_never_break_the_content_disposition_header() {
for text in ADVERSARIAL_TEXT_INPUTS {
for (decision, directive) in [
(Decision::Inline, "inline"),
(Decision::Attachment, "attachment"),
] {
let value = disposition::header(decision, Some(text))
.expect("disposition header")
.into_header_value();
let rendered = value.to_str().expect("the disposition header is ascii");
assert!(
rendered.len() <= PUBLIC_MEDIA_MAX_HEADER_VALUE_BYTES,
"unbounded disposition for {text}"
);
assert!(
!rendered.contains(['\r', '\n', '\0']),
"the disposition for {text} can split the header block"
);
assert!(
rendered.starts_with(directive),
"the disposition for {text} lost its directive"
);
if text.is_empty() {
assert_eq!(rendered, directive);
continue;
}
assert_eq!(
rendered.matches("; filename=\"").count(),
1,
"the disposition for {text} repeated the filename parameter"
);
let quoted = rendered
.strip_prefix(directive)
.and_then(|rest| rest.strip_prefix("; filename=\""))
.and_then(|rest| rest.split('"').next())
.expect("a terminated quoted filename");
assert!(
!quoted.contains(['\\', '/']),
"the ascii fallback for {text} kept a path or escape character"
);
}
}
}
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::{
disposition::{self, Decision},
http_headers,
range::ByteRange,
};
use http::{HeaderMap, header};
fn disposition_string(decision: Decision, filename: Option<&str>) -> String {
disposition::header(decision, filename)
.expect("disposition header")
.into_header_value()
.to_str()
.expect("disposition header is ascii")
.to_owned()
}
#[test]
fn disposition_policy_blocks_scriptable_documents() {
assert_eq!(disposition::decide("image/png", false), Decision::Inline);
assert_eq!(
disposition::decide("video/webm; codecs=vp9", false),
Decision::Inline
);
assert_eq!(
disposition::decide("image/svg+xml", false),
Decision::Attachment
);
assert_eq!(
disposition::decide("application/pdf; charset=binary", false),
Decision::Attachment
);
assert_eq!(disposition::decide("image/png", true), Decision::Attachment);
}
#[test]
fn disposition_filename_uses_ascii_and_rfc5987_forms() {
assert_eq!(
disposition_string(Decision::Attachment, Some("photo.png")),
"attachment; filename=\"photo.png\""
);
assert_eq!(
disposition_string(Decision::Attachment, Some("résumé/\".png")),
"attachment; filename=\"r__sum____.png\"; filename*=UTF-8''r%C3%A9sum%C3%A9%2F%22.png"
);
assert_eq!(disposition_string(Decision::Inline, Some("")), "inline");
}
#[test]
fn public_media_headers_never_negotiate_a_conditional_validator() {
let mut responses = Vec::new();
for content_type in ["image/png", "video/mp4", "audio/mpeg", "image/svg+xml"] {
for byte_range in [None, Some(ByteRange { start: 10, end: 19 })] {
let mut headers = HeaderMap::new();
http_headers::add_media_headers(&mut headers, 100, content_type, byte_range);
responses.push(headers);
}
}
let mut unsatisfiable = HeaderMap::new();
http_headers::add_unsatisfiable_headers(&mut unsatisfiable, 4096);
responses.push(unsatisfiable);
let mut security_only = HeaderMap::new();
http_headers::add_security_headers(&mut security_only);
responses.push(security_only);
for headers in &responses {
for negotiated in ["etag", "if-none-match", "if-modified-since", "age"] {
assert!(
headers.get(negotiated).is_none(),
"{negotiated} was negotiated on a public media response"
);
}
if let Some(vary) = headers.get(header::VARY) {
assert_eq!(vary, "Accept-Encoding");
}
}
}
@@ -0,0 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
mod adversarial;
mod headers;
mod parsers;
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::{
asset_hash::AssetHash,
constants::{DEFAULT_IMAGE_SIZE, parse_image_size},
image_quality::ImageQuality,
image_transform::EncodeEffort,
query::Query,
};
#[test]
fn asset_hash_parsing_stays_lenient_about_the_digest_shape() {
for raw in [
"",
"deadbee",
"deadbeef0",
"DEADBEEF",
"gggggggg",
"0123abcd",
"a-deadbeef",
] {
let parsed = AssetHash::parse(raw);
assert_eq!(parsed.digest(), raw, "the digest rewrote {raw}");
assert!(!parsed.is_animated(), "{raw} was read as animated");
}
for (raw, digest) in [
("a_deadbeef", "deadbeef"),
("a_DEADBEEF", "DEADBEEF"),
("a_gggggggg", "gggggggg"),
("a_", ""),
] {
let parsed = AssetHash::parse(raw);
assert_eq!(parsed.digest(), digest, "the digest rewrote {raw}");
assert!(parsed.is_animated(), "{raw} lost its animation prefix");
}
}
#[test]
fn malformed_query_values_fall_back_instead_of_failing_the_request() {
for raw in ["", "-1", "1e3", "99999999999", "128px", " 128"] {
assert_eq!(
parse_image_size(Some(raw)),
DEFAULT_IMAGE_SIZE,
"size {raw} did not fall back"
);
}
// A value that parses is snapped up the ladder, never dropped to the default: the old era
// pinned this so an off-ladder request can never be served fewer pixels than it asked for.
assert_eq!(parse_image_size(Some("0")), 16);
assert_eq!(parse_image_size(Some("777")), 1024);
assert_eq!(parse_image_size(Some("256")), 256);
assert_eq!(parse_image_size(None), DEFAULT_IMAGE_SIZE);
for raw in ["", " ", "LOW", "lossy", "%FF", "high "] {
assert_eq!(
ImageQuality::parse_lenient(raw),
ImageQuality::High,
"quality {raw} did not fall back"
);
}
assert_eq!(ImageQuality::parse_lenient("low"), ImageQuality::Low);
for raw in ["", "-1", "nine", "10.5", "256"] {
assert_eq!(
EncodeEffort::parse_lenient(raw),
None,
"effort {raw} was accepted"
);
}
assert_eq!(
EncodeEffort::parse_lenient("250").map(EncodeEffort::get),
Some(9)
);
let query = Query::parse("animated=yes&download=TRUE&passthrough=1&empty=");
assert!(!query.bool_value("animated", false));
assert!(!query.bool_value("animated", true));
assert!(query.bool_value("download", false));
assert!(query.bool_value("passthrough", false));
assert!(!query.bool_value("empty", true));
assert!(query.bool_value("missing", true));
assert!(!query.bool_value("missing", false));
}