Skip to main content

tuwunel_api/client/
media_legacy.rs

1#![expect(deprecated)]
2
3use axum::{
4	extract::State,
5	response::{IntoResponse, Redirect, Response},
6};
7use reqwest::Url;
8use ruma::{
9	Mxc,
10	api::client::media::{
11		get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
12		get_media_preview,
13	},
14};
15use tuwunel_core::{
16	Err, Result, err,
17	utils::{content_disposition::make_content_disposition, math::ruma_from_usize},
18};
19use tuwunel_service::media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, Media};
20
21use crate::{ClientIp, Ruma, RumaResponse};
22
23/// # `GET /_matrix/media/v3/config`
24///
25/// Returns max upload size.
26pub(crate) async fn get_media_config_legacy_route(
27	State(services): State<crate::State>,
28	_body: Ruma<get_media_config::v3::Request>,
29) -> Result<get_media_config::v3::Response> {
30	Ok(get_media_config::v3::Response {
31		upload_size: ruma_from_usize(services.server.config.max_request_size),
32	})
33}
34
35/// # `GET /_matrix/media/v3/preview_url`
36///
37/// Returns URL preview.
38#[tracing::instrument(skip_all, fields(%client), name = "url_preview_legacy", level = "debug")]
39pub(crate) async fn get_media_preview_legacy_route(
40	State(services): State<crate::State>,
41	ClientIp(client): ClientIp,
42	body: Ruma<get_media_preview::v3::Request>,
43) -> Result<get_media_preview::v3::Response> {
44	let sender_user = body.sender_user();
45
46	let url = &body.url;
47	let url = Url::parse(&body.url).map_err(|e| {
48		err!(Request(InvalidParam(
49			debug_warn!(%sender_user, %url, "Requested URL is not valid: {e}")
50		)))
51	})?;
52
53	if !services.media.url_preview_allowed(&url) {
54		return Err!(Request(Forbidden(
55			debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
56		)));
57	}
58
59	let preview = services
60		.media
61		.get_url_preview(&url)
62		.await
63		.map_err(|e| {
64			err!(Request(Unknown(
65				debug_error!(%sender_user, %url, "Failed to fetch a URL preview: {e}")
66			)))
67		})?;
68
69	serde_json::value::to_raw_value(&preview)
70		.map(get_media_preview::v3::Response::from_raw_value)
71		.map_err(|error| {
72			err!(Request(Unknown(
73				debug_error!(%sender_user, %url, "Failed to parse URL preview: {error}")
74			)))
75		})
76}
77
78/// # `GET /_matrix/media/v3/download/{serverName}/{mediaId}`
79///
80/// Load media from our server or over federation.
81///
82/// - Only allows federation if `allow_remote` is true
83/// - Only redirects if `allow_redirect` is true
84/// - Uses client-provided `timeout_ms` if available, else defaults to 20
85///   seconds
86#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy", level = "debug")]
87pub(crate) async fn get_content_legacy_route(
88	State(services): State<crate::State>,
89	ClientIp(client): ClientIp,
90	body: Ruma<get_content::v3::Request>,
91) -> Result<Response> {
92	let mxc = Mxc {
93		server_name: &body.server_name,
94		media_id: &body.media_id,
95	};
96
97	if body.allow_redirect
98		&& services.globals.server_is_ours(&body.server_name)
99		&& let Some(url) = services
100			.media
101			.redirect_url(&mxc, &Dim::default())
102			.await?
103	{
104		return Ok(Redirect::temporary(url.as_str()).into_response());
105	}
106
107	match services
108		.media
109		.get(&mxc, Some(body.timeout_ms))
110		.await
111	{
112		| Ok(Media {
113			content,
114			content_type,
115			content_disposition,
116		}) => {
117			let content_disposition = make_content_disposition(
118				content_disposition.as_ref(),
119				content_type.as_deref(),
120				None,
121			);
122
123			let response = get_content::v3::Response {
124				file: content,
125				content_type: content_type.map(Into::into),
126				content_disposition: Some(content_disposition),
127				cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
128				cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
129			};
130
131			Ok(RumaResponse(response).into_response())
132		},
133		| Err(e) =>
134			if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
135				let response = services
136					.media
137					.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
138					.await
139					.map_err(|e| {
140						err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
141					})?;
142
143				let content_disposition = make_content_disposition(
144					response.content_disposition.as_ref(),
145					response.content_type.as_deref(),
146					None,
147				);
148
149				let response = get_content::v3::Response {
150					file: response.file,
151					content_type: response.content_type,
152					content_disposition: Some(content_disposition),
153					cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
154					cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
155				};
156
157				Ok(RumaResponse(response).into_response())
158			} else {
159				Err(e)
160			},
161	}
162}
163
164/// # `GET /_matrix/media/v3/download/{serverName}/{mediaId}/{fileName}`
165///
166/// Load media from our server or over federation, permitting desired filename.
167///
168/// - Only allows federation if `allow_remote` is true
169/// - Only redirects if `allow_redirect` is true
170/// - Uses client-provided `timeout_ms` if available, else defaults to 20
171///   seconds
172#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy", level = "debug")]
173pub(crate) async fn get_content_as_filename_legacy_route(
174	State(services): State<crate::State>,
175	ClientIp(client): ClientIp,
176	body: Ruma<get_content_as_filename::v3::Request>,
177) -> Result<Response> {
178	let mxc = Mxc {
179		server_name: &body.server_name,
180		media_id: &body.media_id,
181	};
182
183	if body.allow_redirect
184		&& services.globals.server_is_ours(&body.server_name)
185		&& let Some(url) = services
186			.media
187			.redirect_url(&mxc, &Dim::default())
188			.await?
189	{
190		return Ok(Redirect::temporary(url.as_str()).into_response());
191	}
192
193	match services
194		.media
195		.get(&mxc, Some(body.timeout_ms))
196		.await
197	{
198		| Ok(Media {
199			content,
200			content_type,
201			content_disposition,
202		}) => {
203			let content_disposition = make_content_disposition(
204				content_disposition.as_ref(),
205				content_type.as_deref(),
206				Some(&body.filename),
207			);
208
209			let response = get_content_as_filename::v3::Response {
210				file: content,
211				content_type: content_type.map(Into::into),
212				content_disposition: Some(content_disposition),
213				cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
214				cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
215			};
216
217			Ok(RumaResponse(response).into_response())
218		},
219		| Err(e) =>
220			if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
221				let response = services
222					.media
223					.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
224					.await
225					.map_err(|e| {
226						err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
227					})?;
228
229				let content_disposition = make_content_disposition(
230					response.content_disposition.as_ref(),
231					response.content_type.as_deref(),
232					None,
233				);
234
235				let response = get_content_as_filename::v3::Response {
236					content_disposition: Some(content_disposition),
237					content_type: response.content_type,
238					file: response.file,
239					cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
240					cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
241				};
242
243				Ok(RumaResponse(response).into_response())
244			} else {
245				Err(e)
246			},
247	}
248}
249
250/// # `GET /_matrix/media/v3/thumbnail/{serverName}/{mediaId}`
251///
252/// Load media thumbnail from our server or over federation.
253///
254/// - Only allows federation if `allow_remote` is true
255/// - Only redirects if `allow_redirect` is true
256/// - Uses client-provided `timeout_ms` if available, else defaults to 20
257///   seconds
258#[tracing::instrument(skip_all, fields(%client), name = "media_thumbnail_get_legacy", level = "debug")]
259pub(crate) async fn get_content_thumbnail_legacy_route(
260	State(services): State<crate::State>,
261	ClientIp(client): ClientIp,
262	body: Ruma<get_content_thumbnail::v3::Request>,
263) -> Result<Response> {
264	let mxc = Mxc {
265		server_name: &body.server_name,
266		media_id: &body.media_id,
267	};
268
269	let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
270
271	if body.allow_redirect
272		&& services.globals.server_is_ours(&body.server_name)
273		&& let Some(url) = services.media.redirect_url(&mxc, &dim).await?
274	{
275		return Ok(Redirect::temporary(url.as_str()).into_response());
276	}
277
278	match services
279		.media
280		.get_thumbnail(&mxc, &dim, Some(body.timeout_ms))
281		.await
282	{
283		| Ok(Media {
284			content,
285			content_type,
286			content_disposition,
287		}) => {
288			let content_disposition = make_content_disposition(
289				content_disposition.as_ref(),
290				content_type.as_deref(),
291				None,
292			);
293
294			let response = get_content_thumbnail::v3::Response {
295				file: content,
296				content_type: content_type.map(Into::into),
297				cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
298				cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
299				content_disposition: Some(content_disposition),
300			};
301
302			Ok(RumaResponse(response).into_response())
303		},
304		| Err(e) =>
305			if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
306				let response = services
307					.media
308					.fetch_remote_thumbnail_legacy(&body)
309					.await
310					.map_err(|e| {
311						err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
312					})?;
313
314				let content_disposition = make_content_disposition(
315					response.content_disposition.as_ref(),
316					response.content_type.as_deref(),
317					None,
318				);
319
320				let response = get_content_thumbnail::v3::Response {
321					file: response.file,
322					content_type: response.content_type,
323					cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
324					cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
325					content_disposition: Some(content_disposition),
326				};
327
328				Ok(RumaResponse(response).into_response())
329			} else {
330				Err(e)
331			},
332	}
333}