From fcfb323cd55ac22b166eb67dffc35241fb41f68d Mon Sep 17 00:00:00 2001
From: Jason Volk <jason@zemos.net>
Date: Tue, 27 Aug 2024 03:35:00 +0000
Subject: [PATCH] relegate legacy media handlers to separate file

Signed-off-by: Jason Volk <jason@zemos.net>
---
 src/api/client/media.rs        | 352 +--------------------------------
 src/api/client/media_legacy.rs | 343 ++++++++++++++++++++++++++++++++
 src/api/client/mod.rs          |   2 +
 src/api/router.rs              |  75 +++----
 src/core/config/mod.rs         |   3 +
 src/service/media/mod.rs       |   6 +
 6 files changed, 399 insertions(+), 382 deletions(-)
 create mode 100644 src/api/client/media_legacy.rs

diff --git a/src/api/client/media.rs b/src/api/client/media.rs
index 41a34d75..b8140ff7 100644
--- a/src/api/client/media.rs
+++ b/src/api/client/media.rs
@@ -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)
-}
diff --git a/src/api/client/media_legacy.rs b/src/api/client/media_legacy.rs
new file mode 100644
index 00000000..e87b9a2b
--- /dev/null
+++ b/src/api/client/media_legacy.rs
@@ -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)
+}
diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs
index cf13cf7e..03c87e5d 100644
--- a/src/api/client/mod.rs
+++ b/src/api/client/mod.rs
@@ -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::*;
diff --git a/src/api/router.rs b/src/api/router.rs
index a76cac32..05dac3ad 100644
--- a/src/api/router.rs
+++ b/src/api/router.rs
@@ -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."))
+}
diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs
index 8836c2c9..a9469e5b 100644
--- a/src/core/config/mod.rs
+++ b/src/core/config/mod.rs
@@ -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 {
diff --git a/src/service/media/mod.rs b/src/service/media/mod.rs
index 04436f7c..2faa13d8 100644
--- a/src/service/media/mod.rs
+++ b/src/service/media/mod.rs
@@ -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>> {