relegate legacy media handlers to separate file

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk 2024-08-27 03:35:00 +00:00
parent 84a8e36120
commit fcfb323cd5
6 changed files with 399 additions and 382 deletions

View file

@ -1,102 +1,13 @@
#![allow(deprecated)]
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduit::{
err,
utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
Err, Result,
utils::{self, content_disposition::make_content_disposition},
Result,
};
use ruma::{
api::client::media::{
create_content, get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
get_media_preview,
},
Mxc,
};
use service::media::{Dim, FileMeta, MXC_LENGTH};
use conduit_service::media::MXC_LENGTH;
use ruma::{api::client::media::create_content, Mxc};
use crate::{Ruma, RumaResponse};
/// Cache control for immutable objects
const CACHE_CONTROL_IMMUTABLE: &str = "public,max-age=31536000,immutable";
const CORP_CROSS_ORIGIN: &str = "cross-origin";
/// # `GET /_matrix/media/v3/config`
///
/// Returns max upload size.
pub(crate) async fn get_media_config_route(
State(services): State<crate::State>, _body: Ruma<get_media_config::v3::Request>,
) -> Result<get_media_config::v3::Response> {
Ok(get_media_config::v3::Response {
upload_size: ruma_from_usize(services.globals.config.max_request_size),
})
}
/// # `GET /_matrix/media/v1/config`
///
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
/// clients may call. conduwuit adds these for compatibility purposes.
/// See <https://spec.matrix.org/legacy/legacy/#id27>
///
/// Returns max upload size.
pub(crate) async fn get_media_config_v1_route(
State(services): State<crate::State>, body: Ruma<get_media_config::v3::Request>,
) -> Result<RumaResponse<get_media_config::v3::Response>> {
get_media_config_route(State(services), body)
.await
.map(RumaResponse)
}
/// # `GET /_matrix/media/v3/preview_url`
///
/// Returns URL preview.
#[tracing::instrument(skip_all, fields(%client), name = "url_preview")]
pub(crate) async fn get_media_preview_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_media_preview::v3::Request>,
) -> Result<get_media_preview::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let url = &body.url;
if !services.media.url_preview_allowed(url) {
return Err!(Request(Forbidden(
debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
)));
}
let preview = services.media.get_url_preview(url).await.map_err(|e| {
err!(Request(Unknown(
debug_error!(%sender_user, %url, "Failed to fetch a URL preview: {e}")
)))
})?;
let res = serde_json::value::to_raw_value(&preview).map_err(|e| {
err!(Request(Unknown(
debug_error!(%sender_user, %url, "Failed to parse a URL preview: {e}")
)))
})?;
Ok(get_media_preview::v3::Response::from_raw_value(res))
}
/// # `GET /_matrix/media/v1/preview_url`
///
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
/// clients may call. conduwuit adds these for compatibility purposes.
/// See <https://spec.matrix.org/legacy/legacy/#id27>
///
/// Returns URL preview.
#[tracing::instrument(skip_all, fields(%client), name = "url_preview")]
pub(crate) async fn get_media_preview_v1_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_media_preview::v3::Request>,
) -> Result<RumaResponse<get_media_preview::v3::Response>> {
get_media_preview_route(State(services), InsecureClientIp(client), body)
.await
.map(RumaResponse)
}
use crate::Ruma;
/// # `POST /_matrix/media/v3/upload`
///
@ -132,256 +43,3 @@ pub(crate) async fn create_content_route(
blurhash: None,
})
}
/// # `POST /_matrix/media/v1/upload`
///
/// Permanently save media in the server.
///
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
/// clients may call. conduwuit adds these for compatibility purposes.
/// See <https://spec.matrix.org/legacy/legacy/#id27>
///
/// - Some metadata will be saved in the database
/// - Media will be saved in the media/ directory
#[tracing::instrument(skip_all, fields(%client), name = "media_upload")]
pub(crate) async fn create_content_v1_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<create_content::v3::Request>,
) -> Result<RumaResponse<create_content::v3::Response>> {
create_content_route(State(services), InsecureClientIp(client), body)
.await
.map(RumaResponse)
}
/// # `GET /_matrix/media/v3/download/{serverName}/{mediaId}`
///
/// Load media from our server or over federation.
///
/// - Only allows federation if `allow_remote` is true
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_get")]
pub(crate) async fn get_content_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content::v3::Request>,
) -> Result<get_content::v3::Response> {
let mxc = Mxc {
server_name: &body.server_name,
media_id: &body.media_id,
};
if let Some(FileMeta {
content,
content_type,
content_disposition,
}) = services.media.get(&mxc).await?
{
let content_disposition = make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);
Ok(get_content::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
let response = services
.media
.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
.await
.map_err(|e| err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}")))))?;
let content_disposition =
make_content_disposition(response.content_disposition.as_ref(), response.content_type.as_deref(), None);
Ok(get_content::v3::Response {
file: response.file,
content_type: response.content_type,
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else {
Err!(Request(NotFound("Media not found.")))
}
}
/// # `GET /_matrix/media/v1/download/{serverName}/{mediaId}`
///
/// Load media from our server or over federation.
///
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
/// clients may call. conduwuit adds these for compatibility purposes.
/// See <https://spec.matrix.org/legacy/legacy/#id27>
///
/// - Only allows federation if `allow_remote` is true
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_get")]
pub(crate) async fn get_content_v1_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content::v3::Request>,
) -> Result<RumaResponse<get_content::v3::Response>> {
get_content_route(State(services), InsecureClientIp(client), body)
.await
.map(RumaResponse)
}
/// # `GET /_matrix/media/v3/download/{serverName}/{mediaId}/{fileName}`
///
/// Load media from our server or over federation, permitting desired filename.
///
/// - Only allows federation if `allow_remote` is true
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_get")]
pub(crate) async fn get_content_as_filename_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content_as_filename::v3::Request>,
) -> Result<get_content_as_filename::v3::Response> {
let mxc = Mxc {
server_name: &body.server_name,
media_id: &body.media_id,
};
if let Some(FileMeta {
content,
content_type,
content_disposition,
}) = services.media.get(&mxc).await?
{
let content_disposition =
make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), Some(&body.filename));
Ok(get_content_as_filename::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
let response = services
.media
.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
.await
.map_err(|e| err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}")))))?;
let content_disposition =
make_content_disposition(response.content_disposition.as_ref(), response.content_type.as_deref(), None);
Ok(get_content_as_filename::v3::Response {
content_disposition: Some(content_disposition),
content_type: response.content_type,
file: response.file,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else {
Err!(Request(NotFound("Media not found.")))
}
}
/// # `GET /_matrix/media/v1/download/{serverName}/{mediaId}/{fileName}`
///
/// Load media from our server or over federation, permitting desired filename.
///
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
/// clients may call. conduwuit adds these for compatibility purposes.
/// See <https://spec.matrix.org/legacy/legacy/#id27>
///
/// - Only allows federation if `allow_remote` is true
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_get")]
pub(crate) async fn get_content_as_filename_v1_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content_as_filename::v3::Request>,
) -> Result<RumaResponse<get_content_as_filename::v3::Response>> {
get_content_as_filename_route(State(services), InsecureClientIp(client), body)
.await
.map(RumaResponse)
}
/// # `GET /_matrix/media/v3/thumbnail/{serverName}/{mediaId}`
///
/// Load media thumbnail from our server or over federation.
///
/// - Only allows federation if `allow_remote` is true
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_thumbnail_get")]
pub(crate) async fn get_content_thumbnail_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content_thumbnail::v3::Request>,
) -> Result<get_content_thumbnail::v3::Response> {
let mxc = Mxc {
server_name: &body.server_name,
media_id: &body.media_id,
};
let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
if let Some(FileMeta {
content,
content_type,
content_disposition,
}) = services.media.get_thumbnail(&mxc, &dim).await?
{
let content_disposition = make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);
Ok(get_content_thumbnail::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition: Some(content_disposition),
})
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
let response = services
.media
.fetch_remote_thumbnail_legacy(&body)
.await
.map_err(|e| err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}")))))?;
let content_disposition =
make_content_disposition(response.content_disposition.as_ref(), response.content_type.as_deref(), None);
Ok(get_content_thumbnail::v3::Response {
file: response.file,
content_type: response.content_type,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition: Some(content_disposition),
})
} else {
Err!(Request(NotFound("Media not found.")))
}
}
/// # `GET /_matrix/media/v1/thumbnail/{serverName}/{mediaId}`
///
/// Load media thumbnail from our server or over federation.
///
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
/// clients may call. conduwuit adds these for compatibility purposes.
/// See <https://spec.matrix.org/legacy/legacy/#id27>
///
/// - Only allows federation if `allow_remote` is true
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_thumbnail_get")]
pub(crate) async fn get_content_thumbnail_v1_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content_thumbnail::v3::Request>,
) -> Result<RumaResponse<get_content_thumbnail::v3::Response>> {
get_content_thumbnail_route(State(services), InsecureClientIp(client), body)
.await
.map(RumaResponse)
}

View file

@ -0,0 +1,343 @@
#![allow(deprecated)]
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduit::{
err,
utils::{content_disposition::make_content_disposition, math::ruma_from_usize},
Err, Result,
};
use conduit_service::media::{Dim, FileMeta, CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN};
use ruma::{
api::client::media::{
create_content, get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
get_media_preview,
},
Mxc,
};
use crate::{client::create_content_route, Ruma, RumaResponse};
/// # `GET /_matrix/media/v3/config`
///
/// Returns max upload size.
pub(crate) async fn get_media_config_legacy_route(
State(services): State<crate::State>, _body: Ruma<get_media_config::v3::Request>,
) -> Result<get_media_config::v3::Response> {
Ok(get_media_config::v3::Response {
upload_size: ruma_from_usize(services.globals.config.max_request_size),
})
}
/// # `GET /_matrix/media/v1/config`
///
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
/// clients may call. conduwuit adds these for compatibility purposes.
/// See <https://spec.matrix.org/legacy/legacy/#id27>
///
/// Returns max upload size.
pub(crate) async fn get_media_config_legacy_legacy_route(
State(services): State<crate::State>, body: Ruma<get_media_config::v3::Request>,
) -> Result<RumaResponse<get_media_config::v3::Response>> {
get_media_config_legacy_route(State(services), body)
.await
.map(RumaResponse)
}
/// # `GET /_matrix/media/v3/preview_url`
///
/// Returns URL preview.
#[tracing::instrument(skip_all, fields(%client), name = "url_preview_legacy")]
pub(crate) async fn get_media_preview_legacy_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_media_preview::v3::Request>,
) -> Result<get_media_preview::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let url = &body.url;
if !services.media.url_preview_allowed(url) {
return Err!(Request(Forbidden(
debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
)));
}
let preview = services.media.get_url_preview(url).await.map_err(|e| {
err!(Request(Unknown(
debug_error!(%sender_user, %url, "Failed to fetch a URL preview: {e}")
)))
})?;
let res = serde_json::value::to_raw_value(&preview).map_err(|e| {
err!(Request(Unknown(
debug_error!(%sender_user, %url, "Failed to parse a URL preview: {e}")
)))
})?;
Ok(get_media_preview::v3::Response::from_raw_value(res))
}
/// # `GET /_matrix/media/v1/preview_url`
///
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
/// clients may call. conduwuit adds these for compatibility purposes.
/// See <https://spec.matrix.org/legacy/legacy/#id27>
///
/// Returns URL preview.
pub(crate) async fn get_media_preview_legacy_legacy_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_media_preview::v3::Request>,
) -> Result<RumaResponse<get_media_preview::v3::Response>> {
get_media_preview_legacy_route(State(services), InsecureClientIp(client), body)
.await
.map(RumaResponse)
}
/// # `POST /_matrix/media/v1/upload`
///
/// Permanently save media in the server.
///
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
/// clients may call. conduwuit adds these for compatibility purposes.
/// See <https://spec.matrix.org/legacy/legacy/#id27>
///
/// - Some metadata will be saved in the database
/// - Media will be saved in the media/ directory
pub(crate) async fn create_content_legacy_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<create_content::v3::Request>,
) -> Result<RumaResponse<create_content::v3::Response>> {
create_content_route(State(services), InsecureClientIp(client), body)
.await
.map(RumaResponse)
}
/// # `GET /_matrix/media/v3/download/{serverName}/{mediaId}`
///
/// Load media from our server or over federation.
///
/// - Only allows federation if `allow_remote` is true
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy")]
pub(crate) async fn get_content_legacy_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content::v3::Request>,
) -> Result<get_content::v3::Response> {
let mxc = Mxc {
server_name: &body.server_name,
media_id: &body.media_id,
};
if let Some(FileMeta {
content,
content_type,
content_disposition,
}) = services.media.get(&mxc).await?
{
let content_disposition = make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);
Ok(get_content::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
let response = services
.media
.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
.await
.map_err(|e| err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}")))))?;
let content_disposition =
make_content_disposition(response.content_disposition.as_ref(), response.content_type.as_deref(), None);
Ok(get_content::v3::Response {
file: response.file,
content_type: response.content_type,
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else {
Err!(Request(NotFound("Media not found.")))
}
}
/// # `GET /_matrix/media/v1/download/{serverName}/{mediaId}`
///
/// Load media from our server or over federation.
///
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
/// clients may call. conduwuit adds these for compatibility purposes.
/// See <https://spec.matrix.org/legacy/legacy/#id27>
///
/// - Only allows federation if `allow_remote` is true
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy")]
pub(crate) async fn get_content_legacy_legacy_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content::v3::Request>,
) -> Result<RumaResponse<get_content::v3::Response>> {
get_content_legacy_route(State(services), InsecureClientIp(client), body)
.await
.map(RumaResponse)
}
/// # `GET /_matrix/media/v3/download/{serverName}/{mediaId}/{fileName}`
///
/// Load media from our server or over federation, permitting desired filename.
///
/// - Only allows federation if `allow_remote` is true
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy")]
pub(crate) async fn get_content_as_filename_legacy_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content_as_filename::v3::Request>,
) -> Result<get_content_as_filename::v3::Response> {
let mxc = Mxc {
server_name: &body.server_name,
media_id: &body.media_id,
};
if let Some(FileMeta {
content,
content_type,
content_disposition,
}) = services.media.get(&mxc).await?
{
let content_disposition =
make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), Some(&body.filename));
Ok(get_content_as_filename::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
let response = services
.media
.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
.await
.map_err(|e| err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}")))))?;
let content_disposition =
make_content_disposition(response.content_disposition.as_ref(), response.content_type.as_deref(), None);
Ok(get_content_as_filename::v3::Response {
content_disposition: Some(content_disposition),
content_type: response.content_type,
file: response.file,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else {
Err!(Request(NotFound("Media not found.")))
}
}
/// # `GET /_matrix/media/v1/download/{serverName}/{mediaId}/{fileName}`
///
/// Load media from our server or over federation, permitting desired filename.
///
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
/// clients may call. conduwuit adds these for compatibility purposes.
/// See <https://spec.matrix.org/legacy/legacy/#id27>
///
/// - Only allows federation if `allow_remote` is true
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
pub(crate) async fn get_content_as_filename_legacy_legacy_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content_as_filename::v3::Request>,
) -> Result<RumaResponse<get_content_as_filename::v3::Response>> {
get_content_as_filename_legacy_route(State(services), InsecureClientIp(client), body)
.await
.map(RumaResponse)
}
/// # `GET /_matrix/media/v3/thumbnail/{serverName}/{mediaId}`
///
/// Load media thumbnail from our server or over federation.
///
/// - Only allows federation if `allow_remote` is true
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_thumbnail_get_legacy")]
pub(crate) async fn get_content_thumbnail_legacy_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content_thumbnail::v3::Request>,
) -> Result<get_content_thumbnail::v3::Response> {
let mxc = Mxc {
server_name: &body.server_name,
media_id: &body.media_id,
};
let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
if let Some(FileMeta {
content,
content_type,
content_disposition,
}) = services.media.get_thumbnail(&mxc, &dim).await?
{
let content_disposition = make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);
Ok(get_content_thumbnail::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition: Some(content_disposition),
})
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
let response = services
.media
.fetch_remote_thumbnail_legacy(&body)
.await
.map_err(|e| err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}")))))?;
let content_disposition =
make_content_disposition(response.content_disposition.as_ref(), response.content_type.as_deref(), None);
Ok(get_content_thumbnail::v3::Response {
file: response.file,
content_type: response.content_type,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition: Some(content_disposition),
})
} else {
Err!(Request(NotFound("Media not found.")))
}
}
/// # `GET /_matrix/media/v1/thumbnail/{serverName}/{mediaId}`
///
/// Load media thumbnail from our server or over federation.
///
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
/// clients may call. conduwuit adds these for compatibility purposes.
/// See <https://spec.matrix.org/legacy/legacy/#id27>
///
/// - Only allows federation if `allow_remote` is true
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
pub(crate) async fn get_content_thumbnail_legacy_legacy_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content_thumbnail::v3::Request>,
) -> Result<RumaResponse<get_content_thumbnail::v3::Response>> {
get_content_thumbnail_legacy_route(State(services), InsecureClientIp(client), body)
.await
.map(RumaResponse)
}

View file

@ -9,6 +9,7 @@ pub(super) mod directory;
pub(super) mod filter;
pub(super) mod keys;
pub(super) mod media;
pub(super) mod media_legacy;
pub(super) mod membership;
pub(super) mod message;
pub(super) mod openid;
@ -46,6 +47,7 @@ pub(super) use directory::*;
pub(super) use filter::*;
pub(super) use keys::*;
pub(super) use media::*;
pub(super) use media_legacy::*;
pub(super) use membership::*;
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room, validate_and_add_event_id};
pub(super) use message::*;

View file

@ -19,7 +19,7 @@ use crate::{client, server};
pub fn build(router: Router<State>, server: &Server) -> Router<State> {
let config = &server.config;
let router = router
let mut router = router
.ruma_route(client::get_supported_versions_route)
.ruma_route(client::get_register_available_route)
.ruma_route(client::register_route)
@ -138,37 +138,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(client::search_events_route)
.ruma_route(client::turn_server_route)
.ruma_route(client::send_event_to_device_route)
.ruma_route(client::get_media_config_route)
.ruma_route(client::get_media_preview_route)
.ruma_route(client::create_content_route)
// legacy v1 media routes
.route(
"/_matrix/media/v1/preview_url",
get(client::get_media_preview_v1_route)
)
.route(
"/_matrix/media/v1/config",
get(client::get_media_config_v1_route)
)
.route(
"/_matrix/media/v1/upload",
post(client::create_content_v1_route)
)
.route(
"/_matrix/media/v1/download/:server_name/:media_id",
get(client::get_content_v1_route)
)
.route(
"/_matrix/media/v1/download/:server_name/:media_id/:file_name",
get(client::get_content_as_filename_v1_route)
)
.route(
"/_matrix/media/v1/thumbnail/:server_name/:media_id",
get(client::get_content_thumbnail_v1_route)
)
.ruma_route(client::get_content_route)
.ruma_route(client::get_content_as_filename_route)
.ruma_route(client::get_content_thumbnail_route)
.ruma_route(client::get_devices_route)
.ruma_route(client::get_device_route)
.ruma_route(client::update_device_route)
@ -202,7 +172,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.route("/client/server.json", get(client::syncv3_client_server_json));
if config.allow_federation {
router
router = router
.ruma_route(server::get_server_version_route)
.route("/_matrix/key/v2/server", get(server::get_server_keys_route))
.route("/_matrix/key/v2/server/:key_id", get(server::get_server_keys_deprecated_route))
@ -232,14 +202,45 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(server::well_known_server)
.ruma_route(server::get_content_route)
.ruma_route(server::get_content_thumbnail_route)
.route("/_conduwuit/local_user_count", get(client::conduwuit_local_user_count))
.route("/_conduwuit/local_user_count", get(client::conduwuit_local_user_count));
} else {
router
router = router
.route("/_matrix/federation/*path", any(federation_disabled))
.route("/.well-known/matrix/server", any(federation_disabled))
.route("/_matrix/key/*path", any(federation_disabled))
.route("/_conduwuit/local_user_count", any(federation_disabled))
.route("/_conduwuit/local_user_count", any(federation_disabled));
}
if config.allow_legacy_media {
router = router
.ruma_route(client::get_media_config_legacy_route)
.ruma_route(client::get_media_preview_legacy_route)
.ruma_route(client::get_content_legacy_route)
.ruma_route(client::get_content_as_filename_legacy_route)
.ruma_route(client::get_content_thumbnail_legacy_route)
.route("/_matrix/media/v1/config", get(client::get_media_config_legacy_legacy_route))
.route("/_matrix/media/v1/upload", post(client::create_content_legacy_route))
.route(
"/_matrix/media/v1/preview_url",
get(client::get_media_preview_legacy_legacy_route),
)
.route(
"/_matrix/media/v1/download/:server_name/:media_id",
get(client::get_content_legacy_legacy_route),
)
.route(
"/_matrix/media/v1/download/:server_name/:media_id/:file_name",
get(client::get_content_as_filename_legacy_legacy_route),
)
.route(
"/_matrix/media/v1/thumbnail/:server_name/:media_id",
get(client::get_content_thumbnail_legacy_legacy_route),
);
} else {
router = router.route("/_matrix/media/*path", any(legacy_media_disabled));
}
router
}
async fn initial_sync(_uri: Uri) -> impl IntoResponse {
@ -247,3 +248,7 @@ async fn initial_sync(_uri: Uri) -> impl IntoResponse {
}
async fn federation_disabled() -> impl IntoResponse { err!(Config("allow_federation", "Federation is disabled.")) }
async fn legacy_media_disabled() -> impl IntoResponse {
err!(Config("allow_legacy_media", "Unauthenticated media is disabled."))
}

View file

@ -289,6 +289,8 @@ pub struct Config {
#[serde(default)]
pub allow_guests_auto_join_rooms: bool,
#[serde(default = "true_fn")]
pub allow_legacy_media: bool,
#[serde(default = "true_fn")]
pub media_startup_check: bool,
#[serde(default)]
@ -745,6 +747,7 @@ impl fmt::Display for Config {
line("Media integrity checks on startup", &self.media_startup_check.to_string());
line("Media compatibility filesystem links", &self.media_compat_file_link.to_string());
line("Prune missing media from database", &self.prune_missing_media.to_string());
line("Allow legacy (unauthenticated) media", &self.allow_legacy_media.to_string());
line("Prevent Media Downloads From", {
let mut lst = vec![];
for domain in &self.prevent_media_downloads_from {

View file

@ -47,6 +47,12 @@ struct Services {
/// generated MXC ID (`media-id`) length
pub const MXC_LENGTH: usize = 32;
/// Cache control for immutable objects.
pub const CACHE_CONTROL_IMMUTABLE: &str = "public,max-age=31536000,immutable";
/// Default cross-origin resource policy.
pub const CORP_CROSS_ORIGIN: &str = "cross-origin";
#[async_trait]
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {