Skip to content
Snippets Groups Projects
Commit 434b5118 authored by 🥺's avatar 🥺 :transgender_flag: Committed by 🥺
Browse files

media: return our detected MIME type for Content-Type

parent 4185a337
No related branches found
No related tags found
No related merge requests found
...@@ -19,7 +19,9 @@ ...@@ -19,7 +19,9 @@
services, services,
utils::{ utils::{
self, self,
content_disposition::{content_disposition_type, make_content_disposition, sanitise_filename}, content_disposition::{
content_disposition_type, make_content_disposition, make_content_type, sanitise_filename,
},
server_name::server_is_ours, server_name::server_is_ours,
}, },
Error, Result, Ruma, RumaResponse, Error, Result, Ruma, RumaResponse,
...@@ -127,6 +129,8 @@ pub(crate) async fn create_content_route( ...@@ -127,6 +129,8 @@ pub(crate) async fn create_content_route(
utils::random_string(MXC_LENGTH) utils::random_string(MXC_LENGTH)
); );
let content_type = Some(make_content_type(&body.file, &body.content_type).to_owned());
services() services()
.media .media
.create( .create(
...@@ -137,20 +141,18 @@ pub(crate) async fn create_content_route( ...@@ -137,20 +141,18 @@ pub(crate) async fn create_content_route(
.map(|filename| { .map(|filename| {
format!( format!(
"{}; filename={}", "{}; filename={}",
content_disposition_type(&body.file, &body.content_type), content_disposition_type(&body.file, &content_type),
sanitise_filename(filename.to_owned()) sanitise_filename(filename.to_owned())
) )
}) })
.as_deref(), .as_deref(),
body.content_type.as_deref(), content_type.as_deref(),
&body.file, &body.file,
) )
.await?; .await?;
let content_uri = mxc.into();
Ok(create_content::v3::Response { Ok(create_content::v3::Response {
content_uri, content_uri: mxc.into(),
blurhash: None, blurhash: None,
}) })
} }
...@@ -189,6 +191,7 @@ pub(crate) async fn get_content_route(body: Ruma<get_content::v3::Request>) -> R ...@@ -189,6 +191,7 @@ pub(crate) async fn get_content_route(body: Ruma<get_content::v3::Request>) -> R
}) = services().media.get(mxc.clone()).await? }) = services().media.get(mxc.clone()).await?
{ {
let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition)); let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition));
let content_type = Some(make_content_type(&file, &content_type).to_owned());
Ok(get_content::v3::Response { Ok(get_content::v3::Response {
file, file,
...@@ -216,10 +219,11 @@ pub(crate) async fn get_content_route(body: Ruma<get_content::v3::Request>) -> R ...@@ -216,10 +219,11 @@ pub(crate) async fn get_content_route(body: Ruma<get_content::v3::Request>) -> R
&response.content_type, &response.content_type,
response.content_disposition, response.content_disposition,
)); ));
let content_type = Some(make_content_type(&response.file, &response.content_type).to_owned());
Ok(get_content::v3::Response { Ok(get_content::v3::Response {
file: response.file, file: response.file,
content_type: response.content_type, content_type,
content_disposition, content_disposition,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()), cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
...@@ -267,6 +271,7 @@ pub(crate) async fn get_content_as_filename_route( ...@@ -267,6 +271,7 @@ pub(crate) async fn get_content_as_filename_route(
}) = services().media.get(mxc.clone()).await? }) = services().media.get(mxc.clone()).await?
{ {
let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition)); let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition));
let content_type = Some(make_content_type(&file, &content_type).to_owned());
Ok(get_content_as_filename::v3::Response { Ok(get_content_as_filename::v3::Response {
file, file,
...@@ -291,10 +296,13 @@ pub(crate) async fn get_content_as_filename_route( ...@@ -291,10 +296,13 @@ pub(crate) async fn get_content_as_filename_route(
&remote_content_response.content_type, &remote_content_response.content_type,
remote_content_response.content_disposition, remote_content_response.content_disposition,
)); ));
let content_type = Some(
make_content_type(&remote_content_response.file, &remote_content_response.content_type).to_owned(),
);
Ok(get_content_as_filename::v3::Response { Ok(get_content_as_filename::v3::Response {
content_disposition, content_disposition,
content_type: remote_content_response.content_type, content_type,
file: remote_content_response.file, file: remote_content_response.file,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()), cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
...@@ -359,6 +367,7 @@ pub(crate) async fn get_content_thumbnail_route( ...@@ -359,6 +367,7 @@ pub(crate) async fn get_content_thumbnail_route(
.await? .await?
{ {
let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition)); let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition));
let content_type = Some(make_content_type(&file, &content_type).to_owned());
Ok(get_content_thumbnail::v3::Response { Ok(get_content_thumbnail::v3::Response {
file, file,
...@@ -371,7 +380,7 @@ pub(crate) async fn get_content_thumbnail_route( ...@@ -371,7 +380,7 @@ pub(crate) async fn get_content_thumbnail_route(
if services() if services()
.globals .globals
.prevent_media_downloads_from() .prevent_media_downloads_from()
.contains(&body.server_name.clone()) .contains(&body.server_name)
{ {
// we'll lie to the client and say the blocked server's media was not found and // we'll lie to the client and say the blocked server's media was not found and
// log. the client has no way of telling anyways so this is a security bonus. // log. the client has no way of telling anyways so this is a security bonus.
...@@ -415,10 +424,13 @@ pub(crate) async fn get_content_thumbnail_route( ...@@ -415,10 +424,13 @@ pub(crate) async fn get_content_thumbnail_route(
&get_thumbnail_response.content_type, &get_thumbnail_response.content_type,
get_thumbnail_response.content_disposition, get_thumbnail_response.content_disposition,
)); ));
let content_type = Some(
make_content_type(&get_thumbnail_response.file, &get_thumbnail_response.content_type).to_owned(),
);
Ok(get_content_thumbnail::v3::Response { Ok(get_content_thumbnail::v3::Response {
file: get_thumbnail_response.file, file: get_thumbnail_response.file,
content_type: get_thumbnail_response.content_type, content_type,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()), cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
content_disposition, content_disposition,
...@@ -486,20 +498,22 @@ async fn get_remote_content( ...@@ -486,20 +498,22 @@ async fn get_remote_content(
content_response.content_disposition, content_response.content_disposition,
)); ));
let content_type = Some(make_content_type(&content_response.file, &content_response.content_type).to_owned());
services() services()
.media .media
.create( .create(
None, None,
mxc.to_owned(), mxc.to_owned(),
content_disposition.as_deref(), content_disposition.as_deref(),
content_response.content_type.as_deref(), content_type.as_deref(),
&content_response.file, &content_response.file,
) )
.await?; .await?;
Ok(get_content::v3::Response { Ok(get_content::v3::Response {
file: content_response.file, file: content_response.file,
content_type: content_response.content_type, content_type,
content_disposition, content_disposition,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()), cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
use crate::debug_info; use crate::debug_info;
const ATTACHMENT: &str = "attachment";
const INLINE: &str = "inline";
const APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
const IMAGE_SVG_XML: &str = "image/svg+xml";
/// Returns a Content-Disposition of `attachment` or `inline`, depending on the /// Returns a Content-Disposition of `attachment` or `inline`, depending on the
/// *parsed* contents of the file uploaded via format magic keys using `infer` /// *parsed* contents of the file uploaded via format magic keys using `infer`
/// crate (basically libmagic without needing libmagic). /// crate (basically libmagic without needing libmagic).
...@@ -12,9 +17,10 @@ ...@@ -12,9 +17,10 @@
/// ///
/// TODO: add a "strict" function for comparing the Content-Type with what we /// TODO: add a "strict" function for comparing the Content-Type with what we
/// detected: `file_type.mime_type() != content_type` /// detected: `file_type.mime_type() != content_type`
pub(crate) fn content_disposition_type(buf: &[u8], _content_type: &Option<String>) -> &'static str { #[tracing::instrument(skip(buf))]
pub(crate) fn content_disposition_type(buf: &[u8], content_type: &Option<String>) -> &'static str {
let Some(file_type) = infer::get(buf) else { let Some(file_type) = infer::get(buf) else {
return "attachment"; return ATTACHMENT;
}; };
debug_info!("MIME type: {}", file_type.mime_type()); debug_info!("MIME type: {}", file_type.mime_type());
...@@ -22,13 +28,35 @@ pub(crate) fn content_disposition_type(buf: &[u8], _content_type: &Option<String ...@@ -22,13 +28,35 @@ pub(crate) fn content_disposition_type(buf: &[u8], _content_type: &Option<String
match file_type.matcher_type() { match file_type.matcher_type() {
MatcherType::Image | MatcherType::Audio | MatcherType::Text | MatcherType::Video => { MatcherType::Image | MatcherType::Audio | MatcherType::Text | MatcherType::Video => {
if file_type.mime_type().contains("xml") { if file_type.mime_type().contains("xml") {
"attachment" ATTACHMENT
} else { } else {
"inline" INLINE
} }
}, },
_ => "attachment", _ => ATTACHMENT,
}
}
/// overrides the Content-Type with what we detected
///
/// SVG is special-cased due to the MIME type being classified as `text/xml` but
/// browsers need `image/svg+xml`
#[tracing::instrument(skip(buf))]
pub(crate) fn make_content_type(buf: &[u8], content_type: &Option<String>) -> &'static str {
let Some(file_type) = infer::get(buf) else {
debug_info!("Failed to infer the file's contents");
return APPLICATION_OCTET_STREAM;
};
let Some(claimed_content_type) = content_type else {
return file_type.mime_type();
};
if claimed_content_type.contains("svg") && file_type.mime_type().contains("xml") {
return IMAGE_SVG_XML;
} }
file_type.mime_type()
} }
/// sanitises the file name for the Content-Disposition using /// sanitises the file name for the Content-Disposition using
...@@ -46,8 +74,10 @@ pub(crate) fn sanitise_filename(filename: String) -> String { ...@@ -46,8 +74,10 @@ pub(crate) fn sanitise_filename(filename: String) -> String {
/// creates the final Content-Disposition based on whether the filename exists /// creates the final Content-Disposition based on whether the filename exists
/// or not. /// or not.
/// ///
/// if filename exists: `Content-Disposition: attachment/inline; /// if filename exists:
/// filename=filename.ext` else: `Content-Disposition: attachment/inline` /// `Content-Disposition: attachment/inline; filename=filename.ext`
///
/// else: `Content-Disposition: attachment/inline`
#[tracing::instrument(skip(file))] #[tracing::instrument(skip(file))]
pub(crate) fn make_content_disposition( pub(crate) fn make_content_disposition(
file: &[u8], content_type: &Option<String>, content_disposition: Option<String>, file: &[u8], content_type: &Option<String>, content_disposition: Option<String>,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment