1use std::{fmt::Debug, time::Duration};
2
3use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE, HeaderValue};
4use ruma::{
5 Mxc, ServerName,
6 api::{
7 OutgoingRequest,
8 client::media,
9 error::ErrorKind::{NotFound, Unrecognized},
10 federation,
11 federation::authenticated_media::{Content, FileOrLocation},
12 },
13};
14use tuwunel_core::{
15 Err, Error, Result, debug_warn, err, implement,
16 utils::content_disposition::make_content_disposition,
17};
18
19use super::{Dim, Media};
20use crate::federation::scheme::{FedAuth, FedPath};
21
22#[implement(super::Service)]
23#[tracing::instrument(level = "debug", skip(self))]
24pub async fn fetch_remote_thumbnail(
25 &self,
26 mxc: &Mxc<'_>,
27 server: Option<&ServerName>,
28 timeout_ms: Duration,
29 dim: &Dim,
30) -> Result<Media> {
31 self.check_fetch_authorized(mxc)?;
32
33 let result = self
34 .fetch_thumbnail_authenticated(mxc, server, timeout_ms, dim)
35 .await;
36
37 if let Err(Error::Request(NotFound, ..)) = &result
38 && self.services.server.config.request_legacy_media
39 {
40 return self
41 .fetch_thumbnail_unauthenticated(mxc, server, timeout_ms, dim)
42 .await;
43 }
44
45 result
46}
47
48#[implement(super::Service)]
49#[tracing::instrument(level = "debug", skip(self))]
50pub async fn fetch_remote_content(
51 &self,
52 mxc: &Mxc<'_>,
53 server: Option<&ServerName>,
54 timeout_ms: Duration,
55) -> Result<Media> {
56 self.check_fetch_authorized(mxc)?;
57
58 let result = self
59 .fetch_content_authenticated(mxc, server, timeout_ms)
60 .await;
61
62 if let Err(Error::Request(NotFound, ..)) = &result
63 && self.services.server.config.request_legacy_media
64 {
65 return self
66 .fetch_content_unauthenticated(mxc, server, timeout_ms)
67 .await;
68 }
69
70 result
71}
72
73#[implement(super::Service)]
74async fn fetch_thumbnail_authenticated(
75 &self,
76 mxc: &Mxc<'_>,
77 server: Option<&ServerName>,
78 timeout_ms: Duration,
79 dim: &Dim,
80) -> Result<Media> {
81 use federation::authenticated_media::get_content_thumbnail::v1::{Request, Response};
82
83 let request = Request {
84 media_id: mxc.media_id.into(),
85 method: dim.method.clone().into(),
86 width: dim.width.into(),
87 height: dim.height.into(),
88 animated: true.into(),
89 timeout_ms,
90 };
91
92 let Response { content, .. } = self
93 .federation_request(mxc, server, request)
94 .await?;
95
96 match content {
97 | FileOrLocation::File(content) =>
98 self.handle_thumbnail_file(mxc, dim, content)
99 .await,
100 | FileOrLocation::Location(location) => self.handle_location(mxc, &location).await,
101 }
102}
103
104#[implement(super::Service)]
105async fn fetch_content_authenticated(
106 &self,
107 mxc: &Mxc<'_>,
108 server: Option<&ServerName>,
109 timeout_ms: Duration,
110) -> Result<Media> {
111 use federation::authenticated_media::get_content::v1::{Request, Response};
112
113 let request = Request {
114 media_id: mxc.media_id.into(),
115 timeout_ms,
116 };
117
118 let Response { content, .. } = self
119 .federation_request(mxc, server, request)
120 .await?;
121
122 match content {
123 | FileOrLocation::File(content) => self.handle_content_file(mxc, content).await,
124 | FileOrLocation::Location(location) => self.handle_location(mxc, &location).await,
125 }
126}
127
128#[expect(deprecated)]
129#[implement(super::Service)]
130async fn fetch_thumbnail_unauthenticated(
131 &self,
132 mxc: &Mxc<'_>,
133 server: Option<&ServerName>,
134 timeout_ms: Duration,
135 dim: &Dim,
136) -> Result<Media> {
137 use media::get_content_thumbnail::v3::{Request, Response};
138
139 let request = Request {
140 allow_remote: true,
141 allow_redirect: true,
142 animated: true.into(),
143 method: dim.method.clone().into(),
144 width: dim.width.into(),
145 height: dim.height.into(),
146 server_name: mxc.server_name.into(),
147 media_id: mxc.media_id.into(),
148 timeout_ms,
149 };
150
151 let Response {
152 file, content_type, content_disposition, ..
153 } = self
154 .federation_request(mxc, server, request)
155 .await?;
156
157 let content = Content { file, content_type, content_disposition };
158
159 self.handle_thumbnail_file(mxc, dim, content)
160 .await
161}
162
163#[expect(deprecated)]
164#[implement(super::Service)]
165async fn fetch_content_unauthenticated(
166 &self,
167 mxc: &Mxc<'_>,
168 server: Option<&ServerName>,
169 timeout_ms: Duration,
170) -> Result<Media> {
171 use media::get_content::v3::{Request, Response};
172
173 let request = Request {
174 allow_remote: true,
175 allow_redirect: true,
176 server_name: mxc.server_name.into(),
177 media_id: mxc.media_id.into(),
178 timeout_ms,
179 };
180
181 let Response {
182 file, content_type, content_disposition, ..
183 } = self
184 .federation_request(mxc, server, request)
185 .await?;
186
187 let content = Content { file, content_type, content_disposition };
188
189 self.handle_content_file(mxc, content).await
190}
191
192#[implement(super::Service)]
193async fn handle_thumbnail_file(
194 &self,
195 mxc: &Mxc<'_>,
196 dim: &Dim,
197 content: Content,
198) -> Result<Media> {
199 let content_disposition = make_content_disposition(
200 content.content_disposition.as_ref(),
201 content.content_type.as_deref(),
202 None,
203 );
204
205 self.upload_thumbnail(
206 mxc,
207 Some(&content_disposition),
208 content.content_type.as_deref(),
209 dim,
210 &content.file,
211 )
212 .await
213 .map(|()| Media {
214 content: content.file,
215 content_type: content.content_type.map(Into::into),
216 content_disposition: Some(content_disposition),
217 })
218}
219
220#[implement(super::Service)]
221async fn handle_content_file(&self, mxc: &Mxc<'_>, content: Content) -> Result<Media> {
222 let content_disposition = make_content_disposition(
223 content.content_disposition.as_ref(),
224 content.content_type.as_deref(),
225 None,
226 );
227
228 self.create(
229 mxc,
230 None,
231 Some(&content_disposition),
232 content.content_type.as_deref(),
233 &content.file,
234 )
235 .await
236 .map(|()| Media {
237 content: content.file,
238 content_type: content.content_type.map(Into::into),
239 content_disposition: Some(content_disposition),
240 })
241}
242
243#[implement(super::Service)]
244async fn handle_location(&self, mxc: &Mxc<'_>, location: &str) -> Result<Media> {
245 self.location_request(location)
246 .await
247 .map_err(|error| {
248 err!(Request(NotFound(
249 debug_warn!(%mxc, ?location, ?error, "Fetching media from location failed")
250 )))
251 })
252}
253
254#[implement(super::Service)]
255async fn location_request(&self, location: &str) -> Result<Media> {
256 let response = self
257 .services
258 .client
259 .extern_media
260 .get(location)
261 .send()
262 .await?;
263
264 let content_type = response
265 .headers()
266 .get(CONTENT_TYPE)
267 .map(HeaderValue::to_str)
268 .and_then(Result::ok)
269 .map(str::to_owned);
270
271 let content_disposition = response
272 .headers()
273 .get(CONTENT_DISPOSITION)
274 .map(HeaderValue::as_bytes)
275 .map(TryFrom::try_from)
276 .and_then(Result::ok);
277
278 response
279 .bytes()
280 .await
281 .map(Vec::from)
282 .map_err(Into::into)
283 .map(|content| Media {
284 content,
285 content_type: content_type.clone(),
286 content_disposition: Some(make_content_disposition(
287 content_disposition.as_ref(),
288 content_type.as_deref(),
289 None,
290 )),
291 })
292}
293
294#[implement(super::Service)]
295async fn federation_request<Request>(
296 &self,
297 mxc: &Mxc<'_>,
298 server: Option<&ServerName>,
299 request: Request,
300) -> Result<Request::IncomingResponse>
301where
302 Request: OutgoingRequest + Send + Debug,
303 Request::Authentication: FedAuth,
304 Request::PathBuilder: FedPath,
305{
306 self.services
307 .federation
308 .execute(server.unwrap_or(mxc.server_name), request)
309 .await
310 .map_err(|error| handle_federation_error(mxc, server, error))
311}
312
313fn handle_federation_error(mxc: &Mxc<'_>, server: Option<&ServerName>, error: Error) -> Error {
316 let fallback =
317 || err!(Request(NotFound(debug_error!(%mxc, ?server, ?error, "Remote media not found"))));
318
319 if error.kind() == NotFound || error.kind() == Unrecognized {
321 return fallback();
322 }
323
324 if error.status_code().is_redirection()
327 || error.status_code().is_client_error()
328 || error.status_code().is_server_error()
329 {
330 return fallback();
331 }
332
333 error
336}
337
338#[implement(super::Service)]
339#[expect(deprecated)]
340pub async fn fetch_remote_thumbnail_legacy(
341 &self,
342 body: &media::get_content_thumbnail::v3::Request,
343) -> Result<media::get_content_thumbnail::v3::Response> {
344 let mxc = Mxc {
345 server_name: &body.server_name,
346 media_id: &body.media_id,
347 };
348
349 self.check_legacy_freeze()?;
350 self.check_fetch_authorized(&mxc)?;
351 let response = self
352 .services
353 .federation
354 .execute(mxc.server_name, media::get_content_thumbnail::v3::Request {
355 allow_remote: body.allow_remote,
356 height: body.height,
357 width: body.width,
358 method: body.method.clone(),
359 server_name: body.server_name.clone(),
360 media_id: body.media_id.clone(),
361 timeout_ms: body.timeout_ms,
362 allow_redirect: body.allow_redirect,
363 animated: body.animated,
364 })
365 .await?;
366
367 let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
368 self.upload_thumbnail(&mxc, None, response.content_type.as_deref(), &dim, &response.file)
369 .await?;
370
371 Ok(response)
372}
373
374#[implement(super::Service)]
375#[expect(deprecated)]
376pub async fn fetch_remote_content_legacy(
377 &self,
378 mxc: &Mxc<'_>,
379 allow_redirect: bool,
380 timeout_ms: Duration,
381) -> Result<media::get_content::v3::Response, Error> {
382 self.check_legacy_freeze()?;
383 self.check_fetch_authorized(mxc)?;
384 let response = self
385 .services
386 .federation
387 .execute(mxc.server_name, media::get_content::v3::Request {
388 allow_remote: true,
389 server_name: mxc.server_name.into(),
390 media_id: mxc.media_id.into(),
391 timeout_ms,
392 allow_redirect,
393 })
394 .await?;
395
396 let content_disposition = make_content_disposition(
397 response.content_disposition.as_ref(),
398 response.content_type.as_deref(),
399 None,
400 );
401
402 self.create(
403 mxc,
404 None,
405 Some(&content_disposition),
406 response.content_type.as_deref(),
407 &response.file,
408 )
409 .await?;
410
411 Ok(response)
412}
413
414#[implement(super::Service)]
415fn check_fetch_authorized(&self, mxc: &Mxc<'_>) -> Result {
416 if self
417 .services
418 .server
419 .config
420 .prevent_media_downloads_from
421 .is_match(mxc.server_name.host())
422 || self
423 .services
424 .server
425 .config
426 .is_forbidden_remote_server_name(mxc.server_name)
427 {
428 debug_warn!(%mxc, "Received request for media on blocklisted server");
431 return Err!(Request(NotFound("Media not found.")));
432 }
433
434 Ok(())
435}
436
437#[implement(super::Service)]
438fn check_legacy_freeze(&self) -> Result {
439 self.services
440 .server
441 .config
442 .freeze_legacy_media
443 .then_some(())
444 .ok_or(err!(Request(NotFound("Remote media is frozen."))))
445}