From 27d6d94355852fd4455de23005f0acfece87ae78 Mon Sep 17 00:00:00 2001 From: Matthias Ahouansou Date: Sat, 24 Aug 2024 10:27:03 +0100 Subject: [PATCH] feat: add support for authenticated media requests --- src/api/client_server/media.rs | 288 +++++++++++++++++++++++++-------- src/api/server_server.rs | 88 ++++++++++ src/main.rs | 6 + 3 files changed, 317 insertions(+), 65 deletions(-) diff --git a/src/api/client_server/media.rs b/src/api/client_server/media.rs index 803d516e..0473c2ad 100644 --- a/src/api/client_server/media.rs +++ b/src/api/client_server/media.rs @@ -4,15 +4,21 @@ use std::time::Duration; use crate::{service::media::FileMeta, services, utils, Error, Result, Ruma}; +use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE}; use ruma::{ - api::client::{ - error::ErrorKind, - media::{ - create_content, get_content, get_content_as_filename, get_content_thumbnail, - get_media_config, + api::{ + client::{ + authenticated_media::{ + get_content, get_content_as_filename, get_content_thumbnail, get_media_config, + }, + error::ErrorKind, + media::{self, create_content}, }, + federation::authenticated_media::{self as federation_media, FileOrLocation}, }, http_headers::{ContentDisposition, ContentDispositionType}, + media::Method, + ServerName, UInt, }; const MXC_LENGTH: usize = 32; @@ -21,9 +27,20 @@ const MXC_LENGTH: usize = 32; /// /// Returns max upload size. pub async fn get_media_config_route( - _body: Ruma, -) -> Result { - Ok(get_media_config::v3::Response { + _body: Ruma, +) -> Result { + Ok(media::get_media_config::v3::Response { + upload_size: services().globals.max_request_size().into(), + }) +} + +/// # `GET /_matrix/client/v1/media/config` +/// +/// Returns max upload size. +pub async fn get_media_config_auth_route( + _body: Ruma, +) -> Result { + Ok(get_media_config::v1::Response { upload_size: services().globals.max_request_size().into(), }) } @@ -64,19 +81,24 @@ pub async fn create_content_route( pub async fn get_remote_content( mxc: &str, - server_name: &ruma::ServerName, + server_name: &ServerName, media_id: String, -) -> Result { - let content_response = services() +) -> Result { + let media::get_content::v3::Response { + file, + content_type, + content_disposition, + .. + } = services() .sending .send_federation_request( server_name, - get_content::v3::Request { - allow_remote: false, + media::get_content::v3::Request { server_name: server_name.to_owned(), media_id, timeout_ms: Duration::from_secs(20), - allow_redirect: false, + allow_remote: false, + allow_redirect: true, }, ) .await?; @@ -85,13 +107,17 @@ pub async fn get_remote_content( .media .create( mxc.to_owned(), - content_response.content_disposition.clone(), - content_response.content_type.as_deref(), - &content_response.file, + content_disposition.clone(), + content_type.as_deref(), + &file, ) .await?; - Ok(content_response) + Ok(get_content::v1::Response { + file, + content_type, + content_disposition, + }) } /// # `GET /_matrix/media/r0/download/{serverName}/{mediaId}` @@ -100,9 +126,37 @@ pub async fn get_remote_content( /// /// - Only allows federation if `allow_remote` is true pub async fn get_content_route( - body: Ruma, -) -> Result { - let mxc = format!("mxc://{}/{}", body.server_name, body.media_id); + body: Ruma, +) -> Result { + let get_content::v1::Response { + file, + content_disposition, + content_type, + } = get_content(&body.server_name, body.media_id.clone(), body.allow_remote).await?; + + Ok(media::get_content::v3::Response { + file, + content_type, + content_disposition, + cross_origin_resource_policy: Some("cross-origin".to_owned()), + }) +} + +/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}` +/// +/// Load media from our server or over federation. +pub async fn get_content_auth_route( + body: Ruma, +) -> Result { + get_content(&body.server_name, body.media_id.clone(), true).await +} + +async fn get_content( + server_name: &ServerName, + media_id: String, + allow_remote: bool, +) -> Result { + let mxc = format!("mxc://{}/{}", server_name, media_id); if let Some(FileMeta { content_disposition, @@ -110,21 +164,19 @@ pub async fn get_content_route( file, }) = services().media.get(mxc.clone()).await? { - Ok(get_content::v3::Response { + Ok(get_content::v1::Response { file, content_type, content_disposition: Some(content_disposition), - cross_origin_resource_policy: Some("cross-origin".to_owned()), }) - } else if &*body.server_name != services().globals.server_name() && body.allow_remote { + } else if server_name != services().globals.server_name() && allow_remote { let remote_content_response = - get_remote_content(&mxc, &body.server_name, body.media_id.clone()).await?; + get_remote_content(&mxc, server_name, media_id.clone()).await?; - Ok(get_content::v3::Response { + Ok(get_content::v1::Response { content_disposition: remote_content_response.content_disposition, content_type: remote_content_response.content_type, file: remote_content_response.file, - cross_origin_resource_policy: Some("cross-origin".to_owned()), }) } else { Err(Error::BadRequest(ErrorKind::NotFound, "Media not found.")) @@ -137,35 +189,74 @@ pub async fn get_content_route( /// /// - Only allows federation if `allow_remote` is true pub async fn get_content_as_filename_route( - body: Ruma, -) -> Result { - let mxc = format!("mxc://{}/{}", body.server_name, body.media_id); + body: Ruma, +) -> Result { + let get_content_as_filename::v1::Response { + file, + content_type, + content_disposition, + } = get_content_as_filename( + &body.server_name, + body.media_id.clone(), + body.filename.clone(), + body.allow_remote, + ) + .await?; + + Ok(media::get_content_as_filename::v3::Response { + file, + content_type, + content_disposition, + cross_origin_resource_policy: Some("cross-origin".to_owned()), + }) +} + +/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}/{fileName}` +/// +/// Load media from our server or over federation, permitting desired filename. +pub async fn get_content_as_filename_auth_route( + body: Ruma, +) -> Result { + get_content_as_filename( + &body.server_name, + body.media_id.clone(), + body.filename.clone(), + true, + ) + .await +} + +async fn get_content_as_filename( + server_name: &ServerName, + media_id: String, + filename: String, + allow_remote: bool, +) -> Result { + let mxc = format!("mxc://{}/{}", server_name, media_id); if let Some(FileMeta { file, content_type, .. }) = services().media.get(mxc.clone()).await? { - Ok(get_content_as_filename::v3::Response { + Ok(get_content_as_filename::v1::Response { file, content_type, content_disposition: Some( ContentDisposition::new(ContentDispositionType::Inline) - .with_filename(Some(body.filename.clone())), + .with_filename(Some(filename.clone())), ), - cross_origin_resource_policy: Some("cross-origin".to_owned()), }) - } else if &*body.server_name != services().globals.server_name() && body.allow_remote { + } else if server_name != services().globals.server_name() && allow_remote { let remote_content_response = - get_remote_content(&mxc, &body.server_name, body.media_id.clone()).await?; + get_remote_content(&mxc, server_name, media_id.clone()).await?; - Ok(get_content_as_filename::v3::Response { + Ok(get_content_as_filename::v1::Response { content_disposition: Some( ContentDisposition::new(ContentDispositionType::Inline) - .with_filename(Some(body.filename.clone())), + .with_filename(Some(filename.clone())), ), content_type: remote_content_response.content_type, file: remote_content_response.file, - cross_origin_resource_policy: Some("cross-origin".to_owned()), }) } else { Err(Error::BadRequest(ErrorKind::NotFound, "Media not found.")) @@ -178,9 +269,54 @@ pub async fn get_content_as_filename_route( /// /// - Only allows federation if `allow_remote` is true pub async fn get_content_thumbnail_route( - body: Ruma, -) -> Result { - let mxc = format!("mxc://{}/{}", body.server_name, body.media_id); + body: Ruma, +) -> Result { + let get_content_thumbnail::v1::Response { file, content_type } = get_content_thumbnail( + &body.server_name, + body.media_id.clone(), + body.height, + body.width, + body.method.clone(), + body.animated, + body.allow_remote, + ) + .await?; + + Ok(media::get_content_thumbnail::v3::Response { + file, + content_type, + cross_origin_resource_policy: Some("cross-origin".to_owned()), + }) +} + +/// # `GET /_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}` +/// +/// Load media thumbnail from our server or over federation. +pub async fn get_content_thumbnail_auth_route( + body: Ruma, +) -> Result { + get_content_thumbnail( + &body.server_name, + body.media_id.clone(), + body.height, + body.width, + body.method.clone(), + body.animated, + true, + ) + .await +} + +async fn get_content_thumbnail( + server_name: &ServerName, + media_id: String, + height: UInt, + width: UInt, + method: Option, + animated: Option, + allow_remote: bool, +) -> Result { + let mxc = format!("mxc://{}/{}", server_name, media_id); if let Some(FileMeta { file, content_type, .. @@ -188,52 +324,74 @@ pub async fn get_content_thumbnail_route( .media .get_thumbnail( mxc.clone(), - body.width + width .try_into() .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?, - body.height + height .try_into() .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?, ) .await? { - Ok(get_content_thumbnail::v3::Response { - file, - content_type, - cross_origin_resource_policy: Some("cross-origin".to_owned()), - }) - } else if body.server_name != services().globals.server_name() && body.allow_remote { - let get_thumbnail_response = services() + Ok(get_content_thumbnail::v1::Response { file, content_type }) + } else if server_name != services().globals.server_name() && allow_remote { + let media::get_content_thumbnail::v3::Response { + file, content_type, .. + } = services() .sending .send_federation_request( - &body.server_name, - get_content_thumbnail::v3::Request { - allow_remote: false, - height: body.height, - width: body.width, - method: body.method.clone(), - server_name: body.server_name.clone(), - media_id: body.media_id.clone(), + server_name, + media::get_content_thumbnail::v3::Request { + height, + width, + method: method.clone(), + server_name: server_name.to_owned(), + media_id: media_id.clone(), timeout_ms: Duration::from_secs(20), allow_redirect: false, - animated: body.animated, + animated, + allow_remote: false, }, ) .await?; - services() .media .upload_thumbnail( mxc, - get_thumbnail_response.content_type.as_deref(), - body.width.try_into().expect("all UInts are valid u32s"), - body.height.try_into().expect("all UInts are valid u32s"), - &get_thumbnail_response.file, + content_type.as_deref(), + width.try_into().expect("all UInts are valid u32s"), + height.try_into().expect("all UInts are valid u32s"), + &file, ) .await?; - Ok(get_thumbnail_response) + Ok(get_content_thumbnail::v1::Response { file, content_type }) } else { Err(Error::BadRequest(ErrorKind::NotFound, "Media not found.")) } } + +async fn get_location_content(url: String) -> Result { + let client = services().globals.default_client(); + let response = client.get(url).send().await?; + let headers = response.headers(); + + let content_type = headers + .get(CONTENT_TYPE) + .and_then(|header| header.to_str().ok()) + .map(ToOwned::to_owned); + + let content_disposition = headers + .get(CONTENT_DISPOSITION) + .map(|header| header.as_bytes()) + .map(TryFrom::try_from) + .and_then(Result::ok); + + let file = response.bytes().await?.to_vec(); + + Ok(get_content::v1::Response { + file, + content_type, + content_disposition, + }) +} diff --git a/src/api/server_server.rs b/src/api/server_server.rs index 6cc29be4..56dd74d3 100644 --- a/src/api/server_server.rs +++ b/src/api/server_server.rs @@ -4,6 +4,7 @@ use crate::{ api::client_server::{self, claim_keys_helper, get_keys_helper}, service::{ globals::SigningKeys, + media::FileMeta, pdu::{gen_event_id_canonical_json, PduBuilder}, }, services, utils, Error, PduEvent, Result, Ruma, @@ -17,6 +18,9 @@ use ruma::{ api::{ client::error::{Error as RumaError, ErrorKind}, federation::{ + authenticated_media::{ + get_content, get_content_thumbnail, Content, ContentMetadata, FileOrLocation, + }, authorization::get_event_authorization, backfill::get_backfill, device::get_devices::{self, v1::UserDevice}, @@ -1891,6 +1895,90 @@ pub async fn create_invite_route( }) } +/// # `GET /_matrix/federation/v1/media/download/{serverName}/{mediaId}` +/// +/// Load media from our server. +pub async fn get_content_route( + body: Ruma, +) -> Result { + let mxc = format!( + "mxc://{}/{}", + services().globals.server_name(), + body.media_id + ); + + if let Some(FileMeta { + content_disposition, + content_type, + file, + }) = services().media.get(mxc.clone()).await? + { + Ok(get_content::v1::Response::new( + ContentMetadata::new(), + FileOrLocation::File(Content { + file, + content_type, + content_disposition: Some(content_disposition), + }), + )) + } else { + Err(Error::BadRequest(ErrorKind::NotFound, "Media not found.")) + } +} + +/// # `GET /_matrix/federation/v1/media/thumbnail/{serverName}/{mediaId}` +/// +/// Load media thumbnail from our server or over federation. +pub async fn get_content_thumbnail_route( + body: Ruma, +) -> Result { + let mxc = format!( + "mxc://{}/{}", + services().globals.server_name(), + body.media_id + ); + + let Some(FileMeta { + file, + content_type, + content_disposition, + }) = services() + .media + .get_thumbnail( + mxc.clone(), + body.width + .try_into() + .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?, + body.height + .try_into() + .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?, + ) + .await? + else { + return Err(Error::BadRequest(ErrorKind::NotFound, "Media not found.")); + }; + + services() + .media + .upload_thumbnail( + mxc, + content_type.as_deref(), + body.width.try_into().expect("all UInts are valid u32s"), + body.height.try_into().expect("all UInts are valid u32s"), + &file, + ) + .await?; + + Ok(get_content_thumbnail::v1::Response::new( + ContentMetadata::new(), + FileOrLocation::File(Content { + file, + content_type, + content_disposition: Some(content_disposition), + }), + )) +} + /// # `GET /_matrix/federation/v1/user/devices/{userId}` /// /// Gets information on all devices of the user. diff --git a/src/main.rs b/src/main.rs index 8d242c53..d0793f2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -379,10 +379,14 @@ fn routes(config: &Config) -> Router { .ruma_route(client_server::turn_server_route) .ruma_route(client_server::send_event_to_device_route) .ruma_route(client_server::get_media_config_route) + .ruma_route(client_server::get_media_config_auth_route) .ruma_route(client_server::create_content_route) .ruma_route(client_server::get_content_route) + .ruma_route(client_server::get_content_auth_route) .ruma_route(client_server::get_content_as_filename_route) + .ruma_route(client_server::get_content_as_filename_auth_route) .ruma_route(client_server::get_content_thumbnail_route) + .ruma_route(client_server::get_content_thumbnail_auth_route) .ruma_route(client_server::get_devices_route) .ruma_route(client_server::get_device_route) .ruma_route(client_server::update_device_route) @@ -440,6 +444,8 @@ fn routes(config: &Config) -> Router { .ruma_route(server_server::create_join_event_v2_route) .ruma_route(server_server::create_invite_route) .ruma_route(server_server::get_devices_route) + .ruma_route(server_server::get_content_route) + .ruma_route(server_server::get_content_thumbnail_route) .ruma_route(server_server::get_room_information_route) .ruma_route(server_server::get_profile_information_route) .ruma_route(server_server::get_keys_route)