mirror of
https://gitlab.computer.surgery/matrix/grapevine.git
synced 2025-12-16 23:31:24 +01:00
919 lines
26 KiB
Rust
919 lines
26 KiB
Rust
use std::time::Duration;
|
|
|
|
use axum::response::IntoResponse;
|
|
use http::{
|
|
HeaderName, HeaderValue, Method,
|
|
header::{CONTENT_DISPOSITION, CONTENT_SECURITY_POLICY, CONTENT_TYPE},
|
|
};
|
|
use phf::{Set, phf_set};
|
|
use ruma::{
|
|
api::{
|
|
client::{
|
|
authenticated_media as authenticated_media_client,
|
|
error::ErrorKind,
|
|
media::{self as legacy_media, create_content},
|
|
},
|
|
federation::authenticated_media as authenticated_media_fed,
|
|
},
|
|
http_headers::{ContentDisposition, ContentDispositionType},
|
|
};
|
|
use tracing::{debug, error, info, warn};
|
|
|
|
use crate::{
|
|
Ar, Error, Ra, Result,
|
|
service::media::FileMeta,
|
|
services,
|
|
utils::{self, MxcData},
|
|
};
|
|
|
|
const MXC_LENGTH: usize = 32;
|
|
|
|
/// `Content-Type`s that can be rendered inline in a browser without risking XSS
|
|
///
|
|
/// Cargo-culted from Synapse. Note that SVG can contain inline JavaScript.
|
|
static INLINE_CONTENT_TYPES: Set<&str> = phf_set! {
|
|
// Keep sorted
|
|
"application/json",
|
|
"application/ld+json",
|
|
"audio/aac",
|
|
"audio/flac",
|
|
"audio/mp4",
|
|
"audio/mpeg",
|
|
"audio/ogg",
|
|
"audio/wav",
|
|
"audio/wave",
|
|
"audio/webm",
|
|
"audio/x-flac",
|
|
"audio/x-pn-wav",
|
|
"audio/x-wav",
|
|
"image/apng",
|
|
"image/avif",
|
|
"image/gif",
|
|
"image/jpeg",
|
|
"image/png",
|
|
"image/webp",
|
|
"text/css",
|
|
"text/csv",
|
|
"text/plain",
|
|
"video/mp4",
|
|
"video/ogg",
|
|
"video/quicktime",
|
|
"video/webm",
|
|
};
|
|
|
|
/// Value for the `Content-Security-Policy` header
|
|
///
|
|
/// Cargo-culted from Synapse.
|
|
fn content_security_policy() -> HeaderValue {
|
|
[
|
|
"sandbox",
|
|
"default-src 'none'",
|
|
"script-src 'none'",
|
|
"plugin-types application/pdf",
|
|
"style-src 'unsafe-inline'",
|
|
"media-src 'self'",
|
|
"object-src 'self'",
|
|
]
|
|
.join("; ")
|
|
.try_into()
|
|
.expect("hardcoded header value should be valid")
|
|
}
|
|
|
|
/// Determine a `Content-Disposition` header that prevents XSS
|
|
// TODO: In some of the places this function is called, we could parse the
|
|
// desired filename out of an existing `Content-Disposition` header value, such
|
|
// as what we're storing in the database or what we receive over federation.
|
|
// Doing this correctly is tricky, so I'm skipping it for now.
|
|
fn content_disposition_for(
|
|
content_type: Option<&str>,
|
|
filename: Option<String>,
|
|
) -> ContentDisposition {
|
|
let disposition_type = match content_type {
|
|
Some(x) if INLINE_CONTENT_TYPES.contains(x) => {
|
|
ContentDispositionType::Inline
|
|
}
|
|
_ => ContentDispositionType::Attachment,
|
|
};
|
|
ContentDisposition {
|
|
disposition_type,
|
|
filename,
|
|
}
|
|
}
|
|
|
|
/// Set a header, but panic if it was already set
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the header was already set.
|
|
fn set_header_or_panic(
|
|
response: &mut axum::response::Response,
|
|
header_name: HeaderName,
|
|
header_value: HeaderValue,
|
|
) {
|
|
if let Some(header_value) = response.headers().get(&header_name) {
|
|
error!(?header_name, ?header_value, "unexpected pre-existing header");
|
|
panic!(
|
|
"expected {header_name:?} to be unset but it was set to \
|
|
{header_value:?}"
|
|
);
|
|
}
|
|
|
|
response.headers_mut().insert(header_name, header_value);
|
|
}
|
|
|
|
/// # `GET /_matrix/media/r0/config`
|
|
///
|
|
/// Returns max upload size.
|
|
#[allow(deprecated)] // unauthenticated media
|
|
pub(crate) async fn get_media_config_legacy_route(
|
|
_body: Ar<legacy_media::get_media_config::v3::Request>,
|
|
) -> Result<Ra<legacy_media::get_media_config::v3::Response>> {
|
|
Ok(Ra(legacy_media::get_media_config::v3::Response {
|
|
upload_size: services().globals.max_request_size(),
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/client/v1/media/config`
|
|
///
|
|
/// Returns max upload size.
|
|
pub(crate) async fn get_media_config_route(
|
|
_body: Ar<authenticated_media_client::get_media_config::v1::Request>,
|
|
) -> Result<Ra<authenticated_media_client::get_media_config::v1::Response>> {
|
|
Ok(Ra(authenticated_media_client::get_media_config::v1::Response {
|
|
upload_size: services().globals.max_request_size(),
|
|
}))
|
|
}
|
|
|
|
/// # `POST /_matrix/media/r0/upload`
|
|
///
|
|
/// Permanently save media in the server.
|
|
///
|
|
/// - Some metadata will be saved in the database
|
|
/// - Media will be saved in the media/ directory
|
|
pub(crate) async fn create_content_route(
|
|
body: Ar<create_content::v3::Request>,
|
|
) -> Result<Ra<create_content::v3::Response>> {
|
|
let media_id = utils::random_string(MXC_LENGTH);
|
|
let mxc = MxcData::new(services().globals.server_name(), &media_id)?;
|
|
|
|
services()
|
|
.media
|
|
.create(
|
|
mxc.clone().into(),
|
|
body.filename
|
|
.clone()
|
|
.map(|filename| ContentDisposition {
|
|
disposition_type: ContentDispositionType::Inline,
|
|
filename: Some(filename),
|
|
})
|
|
.as_ref(),
|
|
body.content_type.clone(),
|
|
&body.file,
|
|
)
|
|
.await?;
|
|
|
|
Ok(Ra(create_content::v3::Response {
|
|
content_uri: mxc.into(),
|
|
blurhash: None,
|
|
}))
|
|
}
|
|
|
|
/// Whether or not to allow remote content to be loaded
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
enum AllowRemote {
|
|
Yes,
|
|
No,
|
|
}
|
|
|
|
impl From<bool> for AllowRemote {
|
|
fn from(allow: bool) -> Self {
|
|
if allow {
|
|
Self::Yes
|
|
} else {
|
|
Self::No
|
|
}
|
|
}
|
|
}
|
|
|
|
struct RemoteResponse {
|
|
#[allow(unused)]
|
|
metadata: authenticated_media_fed::ContentMetadata,
|
|
content: authenticated_media_fed::Content,
|
|
}
|
|
|
|
/// Fetches remote media content from a URL specified in a
|
|
/// `/_matrix/federation/v1/media/*/{mediaId}` `Location` header
|
|
#[tracing::instrument]
|
|
async fn get_redirected_content(
|
|
location: String,
|
|
) -> Result<authenticated_media_fed::Content> {
|
|
let location = location.parse().map_err(|error| {
|
|
warn!(location, %error, "Invalid redirect location");
|
|
Error::BadServerResponse("Invalid redirect location")
|
|
})?;
|
|
let response = services()
|
|
.globals
|
|
.federation_client()
|
|
.execute(reqwest::Request::new(Method::GET, location))
|
|
.await?;
|
|
|
|
let content_type = response
|
|
.headers()
|
|
.get(CONTENT_TYPE)
|
|
.map(|value| {
|
|
value.to_str().map_err(|error| {
|
|
error!(
|
|
?value,
|
|
%error,
|
|
"Invalid Content-Type header"
|
|
);
|
|
Error::BadServerResponse("Invalid Content-Type header")
|
|
})
|
|
})
|
|
.transpose()?
|
|
.map(str::to_owned);
|
|
|
|
let content_disposition = response
|
|
.headers()
|
|
.get(CONTENT_DISPOSITION)
|
|
.map(|value| {
|
|
ContentDisposition::try_from(value.as_bytes()).map_err(|error| {
|
|
error!(
|
|
?value,
|
|
%error,
|
|
"Invalid Content-Disposition header"
|
|
);
|
|
Error::BadServerResponse("Invalid Content-Disposition header")
|
|
})
|
|
})
|
|
.transpose()?;
|
|
|
|
Ok(authenticated_media_fed::Content {
|
|
file: response.bytes().await?.to_vec(),
|
|
content_type,
|
|
content_disposition,
|
|
})
|
|
}
|
|
|
|
#[tracing::instrument(skip_all)]
|
|
async fn get_remote_content_via_federation_api(
|
|
mxc: &MxcData<'_>,
|
|
) -> Result<RemoteResponse, Error> {
|
|
let authenticated_media_fed::get_content::v1::Response {
|
|
metadata,
|
|
content,
|
|
} = services()
|
|
.sending
|
|
.send_federation_request(
|
|
mxc.server_name,
|
|
authenticated_media_fed::get_content::v1::Request {
|
|
media_id: mxc.media_id.to_owned(),
|
|
timeout_ms: Duration::from_secs(20),
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
let content = match content {
|
|
authenticated_media_fed::FileOrLocation::File(content) => {
|
|
debug!("Got media from remote server");
|
|
content
|
|
}
|
|
authenticated_media_fed::FileOrLocation::Location(location) => {
|
|
debug!(location, "Following redirect");
|
|
get_redirected_content(location).await?
|
|
}
|
|
};
|
|
|
|
Ok(RemoteResponse {
|
|
metadata,
|
|
content,
|
|
})
|
|
}
|
|
|
|
#[allow(deprecated)] // unauthenticated media
|
|
#[tracing::instrument(skip_all)]
|
|
async fn get_remote_content_via_legacy_api(
|
|
mxc: &MxcData<'_>,
|
|
) -> Result<RemoteResponse, Error> {
|
|
let content_response = services()
|
|
.sending
|
|
.send_federation_request(
|
|
mxc.server_name,
|
|
legacy_media::get_content::v3::Request {
|
|
allow_remote: false,
|
|
server_name: mxc.server_name.to_owned(),
|
|
media_id: mxc.media_id.to_owned(),
|
|
timeout_ms: Duration::from_secs(20),
|
|
allow_redirect: false,
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
Ok(RemoteResponse {
|
|
metadata: authenticated_media_fed::ContentMetadata {},
|
|
content: authenticated_media_fed::Content {
|
|
file: content_response.file,
|
|
content_disposition: content_response.content_disposition,
|
|
content_type: content_response.content_type,
|
|
},
|
|
})
|
|
}
|
|
|
|
#[tracing::instrument]
|
|
pub(crate) async fn get_remote_content(
|
|
mxc: &MxcData<'_>,
|
|
) -> Result<RemoteResponse, Error> {
|
|
let fed_result = get_remote_content_via_federation_api(mxc).await;
|
|
|
|
let response = match fed_result {
|
|
Ok(response) => {
|
|
debug!("Got remote content via authenticated media API");
|
|
response
|
|
}
|
|
Err(Error::Federation(_, error))
|
|
if error.error_kind() == Some(&ErrorKind::Unrecognized)
|
|
// https://github.com/t2bot/matrix-media-repo/issues/609
|
|
|| error.error_kind() == Some(&ErrorKind::Unauthorized) =>
|
|
{
|
|
info!(
|
|
"Remote server does not support authenticated media, falling \
|
|
back to deprecated API"
|
|
);
|
|
|
|
get_remote_content_via_legacy_api(mxc).await?
|
|
}
|
|
Err(e) => {
|
|
return Err(e);
|
|
}
|
|
};
|
|
|
|
services()
|
|
.media
|
|
.create(
|
|
mxc.clone().into(),
|
|
response.content.content_disposition.as_ref(),
|
|
response.content.content_type.clone(),
|
|
&response.content.file,
|
|
)
|
|
.await?;
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
/// # `GET /_matrix/media/r0/download/{serverName}/{mediaId}`
|
|
///
|
|
/// Load media from our server or over federation.
|
|
///
|
|
/// - Only allows federation if `allow_remote` is true
|
|
#[allow(deprecated)] // unauthenticated media
|
|
pub(crate) async fn get_content_legacy_route(
|
|
body: Ar<legacy_media::get_content::v3::Request>,
|
|
) -> Result<axum::response::Response> {
|
|
use authenticated_media_client::get_content::v1::{
|
|
Request as AmRequest, Response as AmResponse,
|
|
};
|
|
use legacy_media::get_content::v3::{
|
|
Request as LegacyRequest, Response as LegacyResponse,
|
|
};
|
|
|
|
fn convert_request(
|
|
LegacyRequest {
|
|
server_name,
|
|
media_id,
|
|
timeout_ms,
|
|
..
|
|
}: LegacyRequest,
|
|
) -> AmRequest {
|
|
AmRequest {
|
|
server_name,
|
|
media_id,
|
|
timeout_ms,
|
|
}
|
|
}
|
|
|
|
fn convert_response(
|
|
AmResponse {
|
|
file,
|
|
content_type,
|
|
content_disposition,
|
|
}: AmResponse,
|
|
) -> LegacyResponse {
|
|
LegacyResponse {
|
|
file,
|
|
content_type,
|
|
content_disposition,
|
|
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
|
}
|
|
}
|
|
|
|
let allow_remote = body.allow_remote.into();
|
|
|
|
get_content_route_ruma(body.map_body(convert_request), allow_remote)
|
|
.await
|
|
.map(|response| {
|
|
let response = convert_response(response);
|
|
let mut r = Ra(response).into_response();
|
|
|
|
set_header_or_panic(
|
|
&mut r,
|
|
CONTENT_SECURITY_POLICY,
|
|
content_security_policy(),
|
|
);
|
|
|
|
r
|
|
})
|
|
}
|
|
|
|
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}`
|
|
///
|
|
/// Load media from our server or over federation.
|
|
pub(crate) async fn get_content_route(
|
|
body: Ar<authenticated_media_client::get_content::v1::Request>,
|
|
) -> Result<axum::response::Response> {
|
|
get_content_route_ruma(body, AllowRemote::Yes).await.map(|x| {
|
|
let mut r = Ra(x).into_response();
|
|
|
|
set_header_or_panic(
|
|
&mut r,
|
|
CONTENT_SECURITY_POLICY,
|
|
content_security_policy(),
|
|
);
|
|
|
|
r
|
|
})
|
|
}
|
|
|
|
async fn get_content_route_ruma(
|
|
body: Ar<authenticated_media_client::get_content::v1::Request>,
|
|
allow_remote: AllowRemote,
|
|
) -> Result<authenticated_media_client::get_content::v1::Response> {
|
|
let mxc = MxcData::new(&body.server_name, &body.media_id)?;
|
|
|
|
if let Some((
|
|
FileMeta {
|
|
content_type,
|
|
..
|
|
},
|
|
file,
|
|
)) = services().media.get(mxc.clone().into()).await?
|
|
{
|
|
Ok(authenticated_media_client::get_content::v1::Response {
|
|
file,
|
|
content_disposition: Some(content_disposition_for(
|
|
content_type.as_deref(),
|
|
None,
|
|
)),
|
|
content_type,
|
|
})
|
|
} else if &*body.server_name != services().globals.server_name()
|
|
&& allow_remote == AllowRemote::Yes
|
|
{
|
|
let remote_response = get_remote_content(&mxc).await?;
|
|
Ok(authenticated_media_client::get_content::v1::Response {
|
|
file: remote_response.content.file,
|
|
content_disposition: Some(content_disposition_for(
|
|
remote_response.content.content_type.as_deref(),
|
|
None,
|
|
)),
|
|
content_type: remote_response.content.content_type,
|
|
})
|
|
} else {
|
|
Err(Error::BadRequest(ErrorKind::NotYetUploaded, "Media not found."))
|
|
}
|
|
}
|
|
|
|
/// # `GET /_matrix/media/r0/download/{serverName}/{mediaId}/{fileName}`
|
|
///
|
|
/// Load media from our server or over federation, permitting desired filename.
|
|
///
|
|
/// - Only allows federation if `allow_remote` is true
|
|
#[allow(deprecated)] // unauthenticated media
|
|
pub(crate) async fn get_content_as_filename_legacy_route(
|
|
body: Ar<legacy_media::get_content_as_filename::v3::Request>,
|
|
) -> Result<axum::response::Response> {
|
|
use authenticated_media_client::get_content_as_filename::v1::{
|
|
Request as AmRequest, Response as AmResponse,
|
|
};
|
|
use legacy_media::get_content_as_filename::v3::{
|
|
Request as LegacyRequest, Response as LegacyResponse,
|
|
};
|
|
|
|
fn convert_request(
|
|
LegacyRequest {
|
|
server_name,
|
|
media_id,
|
|
filename,
|
|
timeout_ms,
|
|
..
|
|
}: LegacyRequest,
|
|
) -> AmRequest {
|
|
AmRequest {
|
|
server_name,
|
|
media_id,
|
|
filename,
|
|
timeout_ms,
|
|
}
|
|
}
|
|
|
|
fn convert_response(
|
|
AmResponse {
|
|
file,
|
|
content_type,
|
|
content_disposition,
|
|
}: AmResponse,
|
|
) -> LegacyResponse {
|
|
LegacyResponse {
|
|
file,
|
|
content_type,
|
|
content_disposition,
|
|
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
|
}
|
|
}
|
|
|
|
let allow_remote = body.allow_remote.into();
|
|
get_content_as_filename_route_ruma(
|
|
body.map_body(convert_request),
|
|
allow_remote,
|
|
)
|
|
.await
|
|
.map(|response| {
|
|
let response = convert_response(response);
|
|
let mut r = Ra(response).into_response();
|
|
|
|
set_header_or_panic(
|
|
&mut r,
|
|
CONTENT_SECURITY_POLICY,
|
|
content_security_policy(),
|
|
);
|
|
|
|
r
|
|
})
|
|
}
|
|
|
|
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}/{fileName}`
|
|
///
|
|
/// Load media from our server or over federation, permitting desired filename.
|
|
pub(crate) async fn get_content_as_filename_route(
|
|
body: Ar<authenticated_media_client::get_content_as_filename::v1::Request>,
|
|
) -> Result<axum::response::Response> {
|
|
get_content_as_filename_route_ruma(body, AllowRemote::Yes).await.map(|x| {
|
|
let mut r = Ra(x).into_response();
|
|
|
|
set_header_or_panic(
|
|
&mut r,
|
|
CONTENT_SECURITY_POLICY,
|
|
content_security_policy(),
|
|
);
|
|
|
|
r
|
|
})
|
|
}
|
|
|
|
async fn get_content_as_filename_route_ruma(
|
|
body: Ar<authenticated_media_client::get_content_as_filename::v1::Request>,
|
|
allow_remote: AllowRemote,
|
|
) -> Result<authenticated_media_client::get_content_as_filename::v1::Response> {
|
|
let mxc = MxcData::new(&body.server_name, &body.media_id)?;
|
|
|
|
if let Some((
|
|
FileMeta {
|
|
content_type,
|
|
..
|
|
},
|
|
file,
|
|
)) = services().media.get(mxc.clone().into()).await?
|
|
{
|
|
Ok(authenticated_media_client::get_content_as_filename::v1::Response {
|
|
file,
|
|
content_disposition: Some(content_disposition_for(
|
|
content_type.as_deref(),
|
|
Some(body.filename.clone()),
|
|
)),
|
|
content_type,
|
|
})
|
|
} else if &*body.server_name != services().globals.server_name()
|
|
&& allow_remote == AllowRemote::Yes
|
|
{
|
|
let remote_response = get_remote_content(&mxc).await?;
|
|
|
|
Ok(authenticated_media_client::get_content_as_filename::v1::Response {
|
|
content_disposition: Some(content_disposition_for(
|
|
remote_response.content.content_type.as_deref(),
|
|
Some(body.filename.clone()),
|
|
)),
|
|
content_type: remote_response.content.content_type,
|
|
file: remote_response.content.file,
|
|
})
|
|
} else {
|
|
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
|
}
|
|
}
|
|
|
|
fn fix_thumbnail_headers(r: &mut axum::response::Response) {
|
|
let content_type = r
|
|
.headers()
|
|
.get(CONTENT_TYPE)
|
|
.and_then(|x| std::str::from_utf8(x.as_ref()).ok())
|
|
.map(ToOwned::to_owned);
|
|
|
|
set_header_or_panic(r, CONTENT_SECURITY_POLICY, content_security_policy());
|
|
set_header_or_panic(
|
|
r,
|
|
CONTENT_DISPOSITION,
|
|
content_disposition_for(content_type.as_deref(), None)
|
|
.to_string()
|
|
.try_into()
|
|
.expect("generated header value should be valid"),
|
|
);
|
|
}
|
|
|
|
/// # `GET /_matrix/media/r0/thumbnail/{serverName}/{mediaId}`
|
|
///
|
|
/// Load media thumbnail from our server or over federation.
|
|
///
|
|
/// - Only allows federation if `allow_remote` is true
|
|
#[allow(deprecated)] // unauthenticated media
|
|
pub(crate) async fn get_content_thumbnail_legacy_route(
|
|
body: Ar<legacy_media::get_content_thumbnail::v3::Request>,
|
|
) -> Result<axum::response::Response> {
|
|
use authenticated_media_client::get_content_thumbnail::v1::{
|
|
Request as AmRequest, Response as AmResponse,
|
|
};
|
|
use legacy_media::get_content_thumbnail::v3::{
|
|
Request as LegacyRequest, Response as LegacyResponse,
|
|
};
|
|
|
|
fn convert_request(
|
|
LegacyRequest {
|
|
server_name,
|
|
media_id,
|
|
method,
|
|
width,
|
|
height,
|
|
timeout_ms,
|
|
animated,
|
|
..
|
|
}: LegacyRequest,
|
|
) -> AmRequest {
|
|
AmRequest {
|
|
server_name,
|
|
media_id,
|
|
method,
|
|
width,
|
|
height,
|
|
timeout_ms,
|
|
animated,
|
|
}
|
|
}
|
|
|
|
fn convert_response(
|
|
AmResponse {
|
|
file,
|
|
content_type,
|
|
..
|
|
}: AmResponse,
|
|
) -> LegacyResponse {
|
|
LegacyResponse {
|
|
file,
|
|
content_type,
|
|
content_disposition: None,
|
|
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
|
}
|
|
}
|
|
|
|
let allow_remote = body.allow_remote.into();
|
|
|
|
get_content_thumbnail_route_ruma(
|
|
body.map_body(convert_request),
|
|
allow_remote,
|
|
)
|
|
.await
|
|
.map(|response| {
|
|
let response = convert_response(response);
|
|
let mut r = Ra(response).into_response();
|
|
|
|
fix_thumbnail_headers(&mut r);
|
|
|
|
r
|
|
})
|
|
}
|
|
|
|
/// # `GET /_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}`
|
|
///
|
|
/// Load media thumbnail from our server or over federation.
|
|
pub(crate) async fn get_content_thumbnail_route(
|
|
body: Ar<authenticated_media_client::get_content_thumbnail::v1::Request>,
|
|
) -> Result<axum::response::Response> {
|
|
get_content_thumbnail_route_ruma(body, AllowRemote::Yes).await.map(|x| {
|
|
let mut r = Ra(x).into_response();
|
|
|
|
fix_thumbnail_headers(&mut r);
|
|
|
|
r
|
|
})
|
|
}
|
|
|
|
#[tracing::instrument(skip_all)]
|
|
async fn get_remote_thumbnail_via_federation_api(
|
|
server_name: &ruma::ServerName,
|
|
request: authenticated_media_fed::get_content_thumbnail::v1::Request,
|
|
) -> Result<RemoteResponse, Error> {
|
|
let authenticated_media_fed::get_content_thumbnail::v1::Response {
|
|
metadata,
|
|
content,
|
|
} = services()
|
|
.sending
|
|
.send_federation_request(server_name, request)
|
|
.await?;
|
|
|
|
let content = match content {
|
|
authenticated_media_fed::FileOrLocation::File(content) => {
|
|
debug!("Got thumbnail from remote server");
|
|
content
|
|
}
|
|
authenticated_media_fed::FileOrLocation::Location(location) => {
|
|
debug!(location, "Following redirect");
|
|
get_redirected_content(location).await?
|
|
}
|
|
};
|
|
|
|
Ok(RemoteResponse {
|
|
metadata,
|
|
content,
|
|
})
|
|
}
|
|
|
|
#[allow(deprecated)] // unauthenticated media
|
|
#[tracing::instrument(skip_all)]
|
|
async fn get_remote_thumbnail_via_legacy_api(
|
|
server_name: &ruma::ServerName,
|
|
authenticated_media_fed::get_content_thumbnail::v1::Request {
|
|
media_id,
|
|
method,
|
|
width,
|
|
height,
|
|
timeout_ms,
|
|
animated,
|
|
}: authenticated_media_fed::get_content_thumbnail::v1::Request,
|
|
) -> Result<RemoteResponse, Error> {
|
|
let content_response = services()
|
|
.sending
|
|
.send_federation_request(
|
|
server_name,
|
|
legacy_media::get_content_thumbnail::v3::Request {
|
|
server_name: server_name.to_owned(),
|
|
allow_remote: false,
|
|
allow_redirect: false,
|
|
media_id,
|
|
method,
|
|
width,
|
|
height,
|
|
timeout_ms,
|
|
animated,
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
Ok(RemoteResponse {
|
|
metadata: authenticated_media_fed::ContentMetadata {},
|
|
content: authenticated_media_fed::Content {
|
|
file: content_response.file,
|
|
content_disposition: None,
|
|
content_type: content_response.content_type,
|
|
},
|
|
})
|
|
}
|
|
|
|
#[tracing::instrument]
|
|
pub(crate) async fn get_remote_thumbnail(
|
|
server_name: &ruma::ServerName,
|
|
request: authenticated_media_fed::get_content_thumbnail::v1::Request,
|
|
) -> Result<RemoteResponse, Error> {
|
|
let fed_result =
|
|
get_remote_thumbnail_via_federation_api(server_name, request.clone())
|
|
.await;
|
|
|
|
let response = match fed_result {
|
|
Ok(response) => {
|
|
debug!("Got remote content via authenticated media API");
|
|
response
|
|
}
|
|
Err(Error::Federation(_, error))
|
|
if error.error_kind() == Some(&ErrorKind::Unrecognized)
|
|
// https://github.com/t2bot/matrix-media-repo/issues/609
|
|
|| error.error_kind() == Some(&ErrorKind::Unauthorized) =>
|
|
{
|
|
info!(
|
|
"Remote server does not support authenticated media, falling \
|
|
back to deprecated API"
|
|
);
|
|
|
|
get_remote_thumbnail_via_legacy_api(server_name, request.clone())
|
|
.await?
|
|
}
|
|
Err(e) => {
|
|
return Err(e);
|
|
}
|
|
};
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
async fn get_content_thumbnail_route_ruma(
|
|
body: Ar<authenticated_media_client::get_content_thumbnail::v1::Request>,
|
|
allow_remote: AllowRemote,
|
|
) -> Result<authenticated_media_client::get_content_thumbnail::v1::Response> {
|
|
let mxc = MxcData::new(&body.server_name, &body.media_id)?;
|
|
let width = body.width.try_into().map_err(|_| {
|
|
Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid.")
|
|
})?;
|
|
let height = body.height.try_into().map_err(|_| {
|
|
Error::BadRequest(ErrorKind::InvalidParam, "Height is invalid.")
|
|
})?;
|
|
|
|
let make_response = |file, content_type| {
|
|
authenticated_media_client::get_content_thumbnail::v1::Response {
|
|
file,
|
|
content_type,
|
|
content_disposition: None,
|
|
}
|
|
};
|
|
|
|
if let Some((
|
|
FileMeta {
|
|
content_type,
|
|
..
|
|
},
|
|
file,
|
|
)) = services()
|
|
.media
|
|
.get_thumbnail(mxc.clone().into(), width, height)
|
|
.await?
|
|
{
|
|
return Ok(make_response(file, content_type));
|
|
}
|
|
|
|
if &*body.server_name != services().globals.server_name()
|
|
&& allow_remote == AllowRemote::Yes
|
|
{
|
|
let get_thumbnail_response = get_remote_thumbnail(
|
|
&body.server_name,
|
|
authenticated_media_fed::get_content_thumbnail::v1::Request {
|
|
height: body.height,
|
|
width: body.width,
|
|
method: body.method.clone(),
|
|
media_id: body.media_id.clone(),
|
|
timeout_ms: Duration::from_secs(20),
|
|
// we don't support animated thumbnails, so don't try requesting
|
|
// one - we're allowed to ignore the client's request for an
|
|
// animated thumbnail
|
|
animated: Some(false),
|
|
},
|
|
)
|
|
.await;
|
|
|
|
match get_thumbnail_response {
|
|
Ok(resp) => {
|
|
services()
|
|
.media
|
|
.upload_thumbnail(
|
|
mxc.clone().into(),
|
|
None,
|
|
resp.content.content_type.clone(),
|
|
width,
|
|
height,
|
|
&resp.content.file,
|
|
)
|
|
.await?;
|
|
|
|
return Ok(make_response(
|
|
resp.content.file,
|
|
resp.content.content_type,
|
|
));
|
|
}
|
|
Err(error) => warn!(
|
|
%error,
|
|
"Failed to fetch thumbnail via federation, trying to fetch \
|
|
original media and create thumbnail ourselves"
|
|
),
|
|
}
|
|
|
|
get_remote_content(&mxc).await?;
|
|
|
|
if let Some((
|
|
FileMeta {
|
|
content_type,
|
|
..
|
|
},
|
|
file,
|
|
)) =
|
|
services().media.get_thumbnail(mxc.into(), width, height).await?
|
|
{
|
|
return Ok(make_response(file, content_type));
|
|
}
|
|
|
|
error!("Source media doesn't exist even after fetching it from remote");
|
|
}
|
|
|
|
Err(Error::BadRequest(ErrorKind::NotYetUploaded, "Media not found."))
|
|
}
|