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