mirror of
https://gitlab.computer.surgery/matrix/grapevine.git
synced 2025-12-17 15:51:23 +01:00
client_server: use and provide authenticated media API
This commit is contained in:
parent
7f6ab63752
commit
79053ad052
4 changed files with 568 additions and 101 deletions
|
|
@ -3,17 +3,21 @@ use std::time::Duration;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use http::{
|
use http::{
|
||||||
header::{CONTENT_DISPOSITION, CONTENT_SECURITY_POLICY, CONTENT_TYPE},
|
header::{CONTENT_DISPOSITION, CONTENT_SECURITY_POLICY, CONTENT_TYPE},
|
||||||
HeaderName, HeaderValue,
|
HeaderName, HeaderValue, Method,
|
||||||
};
|
};
|
||||||
use phf::{phf_set, Set};
|
use phf::{phf_set, Set};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
api::client::{
|
api::{
|
||||||
error::ErrorKind,
|
client::{
|
||||||
media::{self as legacy_media, create_content},
|
authenticated_media as authenticated_media_client,
|
||||||
|
error::ErrorKind,
|
||||||
|
media::{self as legacy_media, create_content},
|
||||||
|
},
|
||||||
|
federation::authenticated_media as authenticated_media_fed,
|
||||||
},
|
},
|
||||||
http_headers::{ContentDisposition, ContentDispositionType},
|
http_headers::{ContentDisposition, ContentDispositionType},
|
||||||
};
|
};
|
||||||
use tracing::error;
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
service::media::FileMeta,
|
service::media::FileMeta,
|
||||||
|
|
@ -121,7 +125,7 @@ fn set_header_or_panic(
|
||||||
///
|
///
|
||||||
/// Returns max upload size.
|
/// Returns max upload size.
|
||||||
#[allow(deprecated)] // unauthenticated media
|
#[allow(deprecated)] // unauthenticated media
|
||||||
pub(crate) async fn get_media_config_route(
|
pub(crate) async fn get_media_config_legacy_route(
|
||||||
_body: Ar<legacy_media::get_media_config::v3::Request>,
|
_body: Ar<legacy_media::get_media_config::v3::Request>,
|
||||||
) -> Result<Ra<legacy_media::get_media_config::v3::Response>> {
|
) -> Result<Ra<legacy_media::get_media_config::v3::Response>> {
|
||||||
Ok(Ra(legacy_media::get_media_config::v3::Response {
|
Ok(Ra(legacy_media::get_media_config::v3::Response {
|
||||||
|
|
@ -129,6 +133,17 @@ pub(crate) async fn get_media_config_route(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # `GET /_matrix/client/v1/media/config`
|
||||||
|
///
|
||||||
|
/// Returns max upload size.
|
||||||
|
pub(crate) async fn get_media_config_route(
|
||||||
|
_body: Ar<authenticated_media_client::get_media_config::v1::Request>,
|
||||||
|
) -> Result<Ra<authenticated_media_client::get_media_config::v1::Response>> {
|
||||||
|
Ok(Ra(authenticated_media_client::get_media_config::v1::Response {
|
||||||
|
upload_size: services().globals.max_request_size().into(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
/// # `POST /_matrix/media/r0/upload`
|
/// # `POST /_matrix/media/r0/upload`
|
||||||
///
|
///
|
||||||
/// Permanently save media in the server.
|
/// Permanently save media in the server.
|
||||||
|
|
@ -163,10 +178,106 @@ pub(crate) async fn create_content_route(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(deprecated)] // unauthenticated media
|
struct RemoteResponse {
|
||||||
pub(crate) async fn get_remote_content(
|
#[allow(unused)]
|
||||||
|
metadata: authenticated_media_fed::ContentMetadata,
|
||||||
|
content: authenticated_media_fed::Content,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches remote media content from a URL specified in a
|
||||||
|
/// `/_matrix/federation/v1/media/*/{mediaId}` `Location` header
|
||||||
|
#[tracing::instrument]
|
||||||
|
async fn get_redirected_content(
|
||||||
|
location: String,
|
||||||
|
) -> Result<authenticated_media_fed::Content> {
|
||||||
|
let location = location.parse().map_err(|error| {
|
||||||
|
warn!(location, %error, "Invalid redirect location");
|
||||||
|
Error::BadServerResponse("Invalid redirect location")
|
||||||
|
})?;
|
||||||
|
let response = services()
|
||||||
|
.globals
|
||||||
|
.federation_client()
|
||||||
|
.execute(reqwest::Request::new(Method::GET, location))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let content_type = response
|
||||||
|
.headers()
|
||||||
|
.get(CONTENT_TYPE)
|
||||||
|
.map(|value| {
|
||||||
|
value.to_str().map_err(|error| {
|
||||||
|
error!(
|
||||||
|
?value,
|
||||||
|
%error,
|
||||||
|
"Invalid Content-Type header"
|
||||||
|
);
|
||||||
|
Error::BadServerResponse("Invalid Content-Type header")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?
|
||||||
|
.map(str::to_owned);
|
||||||
|
|
||||||
|
let content_disposition = response
|
||||||
|
.headers()
|
||||||
|
.get(CONTENT_DISPOSITION)
|
||||||
|
.map(|value| {
|
||||||
|
ContentDisposition::try_from(value.as_bytes()).map_err(|error| {
|
||||||
|
error!(
|
||||||
|
?value,
|
||||||
|
%error,
|
||||||
|
"Invalid Content-Disposition header"
|
||||||
|
);
|
||||||
|
Error::BadServerResponse("Invalid Content-Disposition header")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
Ok(authenticated_media_fed::Content {
|
||||||
|
file: response.bytes().await?.to_vec(),
|
||||||
|
content_type,
|
||||||
|
content_disposition,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn get_remote_content_via_federation_api(
|
||||||
mxc: &MxcData<'_>,
|
mxc: &MxcData<'_>,
|
||||||
) -> Result<legacy_media::get_content::v3::Response, Error> {
|
) -> Result<RemoteResponse, Error> {
|
||||||
|
let authenticated_media_fed::get_content::v1::Response {
|
||||||
|
metadata,
|
||||||
|
content,
|
||||||
|
} = services()
|
||||||
|
.sending
|
||||||
|
.send_federation_request(
|
||||||
|
mxc.server_name,
|
||||||
|
authenticated_media_fed::get_content::v1::Request {
|
||||||
|
media_id: mxc.media_id.to_owned(),
|
||||||
|
timeout_ms: Duration::from_secs(20),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let content = match content {
|
||||||
|
authenticated_media_fed::FileOrLocation::File(content) => {
|
||||||
|
debug!("Got media from remote server");
|
||||||
|
content
|
||||||
|
}
|
||||||
|
authenticated_media_fed::FileOrLocation::Location(location) => {
|
||||||
|
debug!(location, "Following redirect");
|
||||||
|
get_redirected_content(location).await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(RemoteResponse {
|
||||||
|
metadata,
|
||||||
|
content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)] // unauthenticated media
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn get_remote_content_via_legacy_api(
|
||||||
|
mxc: &MxcData<'_>,
|
||||||
|
) -> Result<RemoteResponse, Error> {
|
||||||
let content_response = services()
|
let content_response = services()
|
||||||
.sending
|
.sending
|
||||||
.send_federation_request(
|
.send_federation_request(
|
||||||
|
|
@ -181,22 +292,53 @@ pub(crate) async fn get_remote_content(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
Ok(RemoteResponse {
|
||||||
|
metadata: authenticated_media_fed::ContentMetadata {},
|
||||||
|
content: authenticated_media_fed::Content {
|
||||||
|
file: content_response.file,
|
||||||
|
content_disposition: content_response.content_disposition,
|
||||||
|
content_type: content_response.content_type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub(crate) async fn get_remote_content(
|
||||||
|
mxc: &MxcData<'_>,
|
||||||
|
) -> Result<RemoteResponse, Error> {
|
||||||
|
let fed_result = get_remote_content_via_federation_api(mxc).await;
|
||||||
|
|
||||||
|
let response = match fed_result {
|
||||||
|
Ok(response) => {
|
||||||
|
debug!("Got remote content via authenticated media API");
|
||||||
|
response
|
||||||
|
}
|
||||||
|
Err(Error::Federation(_, error))
|
||||||
|
if error.error_kind() == Some(&ErrorKind::Unrecognized) =>
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
"Remote server does not support authenticated media, falling \
|
||||||
|
back to deprecated API"
|
||||||
|
);
|
||||||
|
|
||||||
|
get_remote_content_via_legacy_api(mxc).await?
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
services()
|
services()
|
||||||
.media
|
.media
|
||||||
.create(
|
.create(
|
||||||
mxc.to_string(),
|
mxc.to_string(),
|
||||||
content_response.content_disposition.as_ref(),
|
response.content.content_disposition.as_ref(),
|
||||||
content_response.content_type.as_deref(),
|
response.content.content_type.as_deref(),
|
||||||
&content_response.file,
|
&response.content.file,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(legacy_media::get_content::v3::Response {
|
Ok(response)
|
||||||
file: content_response.file,
|
|
||||||
content_disposition: content_response.content_disposition,
|
|
||||||
content_type: content_response.content_type,
|
|
||||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # `GET /_matrix/media/r0/download/{serverName}/{mediaId}`
|
/// # `GET /_matrix/media/r0/download/{serverName}/{mediaId}`
|
||||||
|
|
@ -205,10 +347,71 @@ pub(crate) async fn get_remote_content(
|
||||||
///
|
///
|
||||||
/// - Only allows federation if `allow_remote` is true
|
/// - Only allows federation if `allow_remote` is true
|
||||||
#[allow(deprecated)] // unauthenticated media
|
#[allow(deprecated)] // unauthenticated media
|
||||||
pub(crate) async fn get_content_route(
|
pub(crate) async fn get_content_legacy_route(
|
||||||
body: Ar<legacy_media::get_content::v3::Request>,
|
body: Ar<legacy_media::get_content::v3::Request>,
|
||||||
) -> Result<axum::response::Response> {
|
) -> Result<axum::response::Response> {
|
||||||
get_content_route_ruma(body).await.map(|x| {
|
use authenticated_media_client::get_content::v1::{
|
||||||
|
Request as AmRequest, Response as AmResponse,
|
||||||
|
};
|
||||||
|
use legacy_media::get_content::v3::{
|
||||||
|
Request as LegacyRequest, Response as LegacyResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn convert_request(
|
||||||
|
LegacyRequest {
|
||||||
|
server_name,
|
||||||
|
media_id,
|
||||||
|
timeout_ms,
|
||||||
|
..
|
||||||
|
}: LegacyRequest,
|
||||||
|
) -> AmRequest {
|
||||||
|
AmRequest {
|
||||||
|
server_name,
|
||||||
|
media_id,
|
||||||
|
timeout_ms,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_response(
|
||||||
|
AmResponse {
|
||||||
|
file,
|
||||||
|
content_type,
|
||||||
|
content_disposition,
|
||||||
|
}: AmResponse,
|
||||||
|
) -> LegacyResponse {
|
||||||
|
LegacyResponse {
|
||||||
|
file,
|
||||||
|
content_type,
|
||||||
|
content_disposition,
|
||||||
|
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let allow_remote = body.allow_remote;
|
||||||
|
|
||||||
|
get_content_route_ruma(body.map_body(convert_request), allow_remote)
|
||||||
|
.await
|
||||||
|
.map(|response| {
|
||||||
|
let response = convert_response(response);
|
||||||
|
let mut r = Ra(response).into_response();
|
||||||
|
|
||||||
|
set_header_or_panic(
|
||||||
|
&mut r,
|
||||||
|
CONTENT_SECURITY_POLICY,
|
||||||
|
content_security_policy(),
|
||||||
|
);
|
||||||
|
|
||||||
|
r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}`
|
||||||
|
///
|
||||||
|
/// Load media from our server or over federation.
|
||||||
|
pub(crate) async fn get_content_route(
|
||||||
|
body: Ar<authenticated_media_client::get_content::v1::Request>,
|
||||||
|
) -> Result<axum::response::Response> {
|
||||||
|
get_content_route_ruma(body, true).await.map(|x| {
|
||||||
let mut r = Ra(x).into_response();
|
let mut r = Ra(x).into_response();
|
||||||
|
|
||||||
set_header_or_panic(
|
set_header_or_panic(
|
||||||
|
|
@ -221,10 +424,10 @@ pub(crate) async fn get_content_route(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(deprecated)] // unauthenticated media
|
|
||||||
async fn get_content_route_ruma(
|
async fn get_content_route_ruma(
|
||||||
body: Ar<legacy_media::get_content::v3::Request>,
|
body: Ar<authenticated_media_client::get_content::v1::Request>,
|
||||||
) -> Result<legacy_media::get_content::v3::Response> {
|
allow_remote: bool,
|
||||||
|
) -> Result<authenticated_media_client::get_content::v1::Response> {
|
||||||
let mxc = MxcData::new(&body.server_name, &body.media_id)?;
|
let mxc = MxcData::new(&body.server_name, &body.media_id)?;
|
||||||
|
|
||||||
if let Some(FileMeta {
|
if let Some(FileMeta {
|
||||||
|
|
@ -233,27 +436,25 @@ async fn get_content_route_ruma(
|
||||||
..
|
..
|
||||||
}) = services().media.get(mxc.to_string()).await?
|
}) = services().media.get(mxc.to_string()).await?
|
||||||
{
|
{
|
||||||
Ok(legacy_media::get_content::v3::Response {
|
Ok(authenticated_media_client::get_content::v1::Response {
|
||||||
file,
|
file,
|
||||||
content_disposition: Some(content_disposition_for(
|
content_disposition: Some(content_disposition_for(
|
||||||
content_type.as_deref(),
|
content_type.as_deref(),
|
||||||
None,
|
None,
|
||||||
)),
|
)),
|
||||||
content_type,
|
content_type,
|
||||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
|
||||||
})
|
})
|
||||||
} else if &*body.server_name != services().globals.server_name()
|
} else if &*body.server_name != services().globals.server_name()
|
||||||
&& body.allow_remote
|
&& allow_remote
|
||||||
{
|
{
|
||||||
let remote_content_response = get_remote_content(&mxc).await?;
|
let remote_response = get_remote_content(&mxc).await?;
|
||||||
Ok(legacy_media::get_content::v3::Response {
|
Ok(authenticated_media_client::get_content::v1::Response {
|
||||||
file: remote_content_response.file,
|
file: remote_response.content.file,
|
||||||
content_disposition: Some(content_disposition_for(
|
content_disposition: Some(content_disposition_for(
|
||||||
remote_content_response.content_type.as_deref(),
|
remote_response.content.content_type.as_deref(),
|
||||||
None,
|
None,
|
||||||
)),
|
)),
|
||||||
content_type: remote_content_response.content_type,
|
content_type: remote_response.content.content_type,
|
||||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Error::BadRequest(ErrorKind::NotYetUploaded, "Media not found."))
|
Err(Error::BadRequest(ErrorKind::NotYetUploaded, "Media not found."))
|
||||||
|
|
@ -266,10 +467,75 @@ async fn get_content_route_ruma(
|
||||||
///
|
///
|
||||||
/// - Only allows federation if `allow_remote` is true
|
/// - Only allows federation if `allow_remote` is true
|
||||||
#[allow(deprecated)] // unauthenticated media
|
#[allow(deprecated)] // unauthenticated media
|
||||||
pub(crate) async fn get_content_as_filename_route(
|
pub(crate) async fn get_content_as_filename_legacy_route(
|
||||||
body: Ar<legacy_media::get_content_as_filename::v3::Request>,
|
body: Ar<legacy_media::get_content_as_filename::v3::Request>,
|
||||||
) -> Result<axum::response::Response> {
|
) -> Result<axum::response::Response> {
|
||||||
get_content_as_filename_route_ruma(body).await.map(|x| {
|
use authenticated_media_client::get_content_as_filename::v1::{
|
||||||
|
Request as AmRequest, Response as AmResponse,
|
||||||
|
};
|
||||||
|
use legacy_media::get_content_as_filename::v3::{
|
||||||
|
Request as LegacyRequest, Response as LegacyResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn convert_request(
|
||||||
|
LegacyRequest {
|
||||||
|
server_name,
|
||||||
|
media_id,
|
||||||
|
filename,
|
||||||
|
timeout_ms,
|
||||||
|
..
|
||||||
|
}: LegacyRequest,
|
||||||
|
) -> AmRequest {
|
||||||
|
AmRequest {
|
||||||
|
server_name,
|
||||||
|
media_id,
|
||||||
|
filename,
|
||||||
|
timeout_ms,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_response(
|
||||||
|
AmResponse {
|
||||||
|
file,
|
||||||
|
content_type,
|
||||||
|
content_disposition,
|
||||||
|
}: AmResponse,
|
||||||
|
) -> LegacyResponse {
|
||||||
|
LegacyResponse {
|
||||||
|
file,
|
||||||
|
content_type,
|
||||||
|
content_disposition,
|
||||||
|
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let allow_remote = body.allow_remote;
|
||||||
|
get_content_as_filename_route_ruma(
|
||||||
|
body.map_body(convert_request),
|
||||||
|
allow_remote,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|response| {
|
||||||
|
let response = convert_response(response);
|
||||||
|
let mut r = Ra(response).into_response();
|
||||||
|
|
||||||
|
set_header_or_panic(
|
||||||
|
&mut r,
|
||||||
|
CONTENT_SECURITY_POLICY,
|
||||||
|
content_security_policy(),
|
||||||
|
);
|
||||||
|
|
||||||
|
r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}/{fileName}`
|
||||||
|
///
|
||||||
|
/// Load media from our server or over federation, permitting desired filename.
|
||||||
|
pub(crate) async fn get_content_as_filename_route(
|
||||||
|
body: Ar<authenticated_media_client::get_content_as_filename::v1::Request>,
|
||||||
|
) -> Result<axum::response::Response> {
|
||||||
|
get_content_as_filename_route_ruma(body, true).await.map(|x| {
|
||||||
let mut r = Ra(x).into_response();
|
let mut r = Ra(x).into_response();
|
||||||
|
|
||||||
set_header_or_panic(
|
set_header_or_panic(
|
||||||
|
|
@ -282,10 +548,10 @@ pub(crate) async fn get_content_as_filename_route(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(deprecated)] // unauthenticated media
|
|
||||||
pub(crate) async fn get_content_as_filename_route_ruma(
|
pub(crate) async fn get_content_as_filename_route_ruma(
|
||||||
body: Ar<legacy_media::get_content_as_filename::v3::Request>,
|
body: Ar<authenticated_media_client::get_content_as_filename::v1::Request>,
|
||||||
) -> Result<legacy_media::get_content_as_filename::v3::Response> {
|
allow_remote: bool,
|
||||||
|
) -> Result<authenticated_media_client::get_content_as_filename::v1::Response> {
|
||||||
let mxc = MxcData::new(&body.server_name, &body.media_id)?;
|
let mxc = MxcData::new(&body.server_name, &body.media_id)?;
|
||||||
|
|
||||||
if let Some(FileMeta {
|
if let Some(FileMeta {
|
||||||
|
|
@ -294,74 +560,242 @@ pub(crate) async fn get_content_as_filename_route_ruma(
|
||||||
..
|
..
|
||||||
}) = services().media.get(mxc.to_string()).await?
|
}) = services().media.get(mxc.to_string()).await?
|
||||||
{
|
{
|
||||||
Ok(legacy_media::get_content_as_filename::v3::Response {
|
Ok(authenticated_media_client::get_content_as_filename::v1::Response {
|
||||||
file,
|
file,
|
||||||
content_disposition: Some(content_disposition_for(
|
content_disposition: Some(content_disposition_for(
|
||||||
content_type.as_deref(),
|
content_type.as_deref(),
|
||||||
Some(body.filename.clone()),
|
Some(body.filename.clone()),
|
||||||
)),
|
)),
|
||||||
content_type,
|
content_type,
|
||||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
|
||||||
})
|
})
|
||||||
} else if &*body.server_name != services().globals.server_name()
|
} else if &*body.server_name != services().globals.server_name()
|
||||||
&& body.allow_remote
|
&& allow_remote
|
||||||
{
|
{
|
||||||
let remote_content_response = get_remote_content(&mxc).await?;
|
let remote_response = get_remote_content(&mxc).await?;
|
||||||
|
|
||||||
Ok(legacy_media::get_content_as_filename::v3::Response {
|
Ok(authenticated_media_client::get_content_as_filename::v1::Response {
|
||||||
content_disposition: Some(content_disposition_for(
|
content_disposition: Some(content_disposition_for(
|
||||||
remote_content_response.content_type.as_deref(),
|
remote_response.content.content_type.as_deref(),
|
||||||
Some(body.filename.clone()),
|
Some(body.filename.clone()),
|
||||||
)),
|
)),
|
||||||
content_type: remote_content_response.content_type,
|
content_type: remote_response.content.content_type,
|
||||||
file: remote_content_response.file,
|
file: remote_response.content.file,
|
||||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fix_thumbnail_headers(r: &mut axum::response::Response) {
|
||||||
|
let content_type = r
|
||||||
|
.headers()
|
||||||
|
.get(CONTENT_TYPE)
|
||||||
|
.and_then(|x| std::str::from_utf8(x.as_ref()).ok())
|
||||||
|
.map(ToOwned::to_owned);
|
||||||
|
|
||||||
|
set_header_or_panic(r, CONTENT_SECURITY_POLICY, content_security_policy());
|
||||||
|
set_header_or_panic(
|
||||||
|
r,
|
||||||
|
CONTENT_DISPOSITION,
|
||||||
|
content_disposition_for(content_type.as_deref(), None)
|
||||||
|
.to_string()
|
||||||
|
.try_into()
|
||||||
|
.expect("generated header value should be valid"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// # `GET /_matrix/media/r0/thumbnail/{serverName}/{mediaId}`
|
/// # `GET /_matrix/media/r0/thumbnail/{serverName}/{mediaId}`
|
||||||
///
|
///
|
||||||
/// Load media thumbnail from our server or over federation.
|
/// Load media thumbnail from our server or over federation.
|
||||||
///
|
///
|
||||||
/// - Only allows federation if `allow_remote` is true
|
/// - Only allows federation if `allow_remote` is true
|
||||||
#[allow(deprecated)] // unauthenticated media
|
#[allow(deprecated)] // unauthenticated media
|
||||||
pub(crate) async fn get_content_thumbnail_route(
|
pub(crate) async fn get_content_thumbnail_legacy_route(
|
||||||
body: Ar<legacy_media::get_content_thumbnail::v3::Request>,
|
body: Ar<legacy_media::get_content_thumbnail::v3::Request>,
|
||||||
) -> Result<axum::response::Response> {
|
) -> Result<axum::response::Response> {
|
||||||
get_content_thumbnail_route_ruma(body).await.map(|x| {
|
use authenticated_media_client::get_content_thumbnail::v1::{
|
||||||
let mut r = Ra(x).into_response();
|
Request as AmRequest, Response as AmResponse,
|
||||||
|
};
|
||||||
|
use legacy_media::get_content_thumbnail::v3::{
|
||||||
|
Request as LegacyRequest, Response as LegacyResponse,
|
||||||
|
};
|
||||||
|
|
||||||
let content_type = r
|
fn convert_request(
|
||||||
.headers()
|
LegacyRequest {
|
||||||
.get(CONTENT_TYPE)
|
server_name,
|
||||||
.and_then(|x| std::str::from_utf8(x.as_ref()).ok())
|
media_id,
|
||||||
.map(ToOwned::to_owned);
|
method,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
timeout_ms,
|
||||||
|
animated,
|
||||||
|
..
|
||||||
|
}: LegacyRequest,
|
||||||
|
) -> AmRequest {
|
||||||
|
AmRequest {
|
||||||
|
server_name,
|
||||||
|
media_id,
|
||||||
|
method,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
timeout_ms,
|
||||||
|
animated,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
set_header_or_panic(
|
fn convert_response(
|
||||||
&mut r,
|
AmResponse {
|
||||||
CONTENT_SECURITY_POLICY,
|
file,
|
||||||
content_security_policy(),
|
content_type,
|
||||||
);
|
}: AmResponse,
|
||||||
set_header_or_panic(
|
) -> LegacyResponse {
|
||||||
&mut r,
|
LegacyResponse {
|
||||||
CONTENT_DISPOSITION,
|
file,
|
||||||
content_disposition_for(content_type.as_deref(), None)
|
content_type,
|
||||||
.to_string()
|
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||||
.try_into()
|
}
|
||||||
.expect("generated header value should be valid"),
|
}
|
||||||
);
|
|
||||||
|
let allow_remote = body.allow_remote;
|
||||||
|
|
||||||
|
get_content_thumbnail_route_ruma(
|
||||||
|
body.map_body(convert_request),
|
||||||
|
allow_remote,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|response| {
|
||||||
|
let response = convert_response(response);
|
||||||
|
let mut r = Ra(response).into_response();
|
||||||
|
|
||||||
|
fix_thumbnail_headers(&mut r);
|
||||||
|
|
||||||
r
|
r
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # `GET /_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}`
|
||||||
|
///
|
||||||
|
/// Load media thumbnail from our server or over federation.
|
||||||
|
pub(crate) async fn get_content_thumbnail_route(
|
||||||
|
body: Ar<authenticated_media_client::get_content_thumbnail::v1::Request>,
|
||||||
|
) -> Result<axum::response::Response> {
|
||||||
|
get_content_thumbnail_route_ruma(body, true).await.map(|x| {
|
||||||
|
let mut r = Ra(x).into_response();
|
||||||
|
|
||||||
|
fix_thumbnail_headers(&mut r);
|
||||||
|
|
||||||
|
r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn get_remote_thumbnail_via_federation_api(
|
||||||
|
server_name: &ruma::ServerName,
|
||||||
|
request: authenticated_media_fed::get_content_thumbnail::v1::Request,
|
||||||
|
) -> Result<RemoteResponse, Error> {
|
||||||
|
let authenticated_media_fed::get_content_thumbnail::v1::Response {
|
||||||
|
metadata,
|
||||||
|
content,
|
||||||
|
} = services()
|
||||||
|
.sending
|
||||||
|
.send_federation_request(server_name, request)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let content = match content {
|
||||||
|
authenticated_media_fed::FileOrLocation::File(content) => {
|
||||||
|
debug!("Got thumbnail from remote server");
|
||||||
|
content
|
||||||
|
}
|
||||||
|
authenticated_media_fed::FileOrLocation::Location(location) => {
|
||||||
|
debug!(location, "Following redirect");
|
||||||
|
get_redirected_content(location).await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(RemoteResponse {
|
||||||
|
metadata,
|
||||||
|
content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(deprecated)] // unauthenticated media
|
#[allow(deprecated)] // unauthenticated media
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn get_remote_thumbnail_via_legacy_api(
|
||||||
|
server_name: &ruma::ServerName,
|
||||||
|
authenticated_media_fed::get_content_thumbnail::v1::Request {
|
||||||
|
media_id,
|
||||||
|
method,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
timeout_ms,
|
||||||
|
animated,
|
||||||
|
}: authenticated_media_fed::get_content_thumbnail::v1::Request,
|
||||||
|
) -> Result<RemoteResponse, Error> {
|
||||||
|
let content_response = services()
|
||||||
|
.sending
|
||||||
|
.send_federation_request(
|
||||||
|
server_name,
|
||||||
|
legacy_media::get_content_thumbnail::v3::Request {
|
||||||
|
server_name: server_name.to_owned(),
|
||||||
|
allow_remote: false,
|
||||||
|
allow_redirect: false,
|
||||||
|
media_id,
|
||||||
|
method,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
timeout_ms,
|
||||||
|
animated,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(RemoteResponse {
|
||||||
|
metadata: authenticated_media_fed::ContentMetadata {},
|
||||||
|
content: authenticated_media_fed::Content {
|
||||||
|
file: content_response.file,
|
||||||
|
content_disposition: None,
|
||||||
|
content_type: content_response.content_type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub(crate) async fn get_remote_thumbnail(
|
||||||
|
server_name: &ruma::ServerName,
|
||||||
|
request: authenticated_media_fed::get_content_thumbnail::v1::Request,
|
||||||
|
) -> Result<RemoteResponse, Error> {
|
||||||
|
let fed_result =
|
||||||
|
get_remote_thumbnail_via_federation_api(server_name, request.clone())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let response = match fed_result {
|
||||||
|
Ok(response) => {
|
||||||
|
debug!("Got remote content via authenticated media API");
|
||||||
|
response
|
||||||
|
}
|
||||||
|
Err(Error::Federation(_, error))
|
||||||
|
if error.error_kind() == Some(&ErrorKind::Unrecognized) =>
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
"Remote server does not support authenticated media, falling \
|
||||||
|
back to deprecated API"
|
||||||
|
);
|
||||||
|
|
||||||
|
get_remote_thumbnail_via_legacy_api(server_name, request.clone())
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_content_thumbnail_route_ruma(
|
async fn get_content_thumbnail_route_ruma(
|
||||||
body: Ar<legacy_media::get_content_thumbnail::v3::Request>,
|
body: Ar<authenticated_media_client::get_content_thumbnail::v1::Request>,
|
||||||
) -> Result<legacy_media::get_content_thumbnail::v3::Response> {
|
allow_remote: bool,
|
||||||
|
) -> Result<authenticated_media_client::get_content_thumbnail::v1::Response> {
|
||||||
let mxc = MxcData::new(&body.server_name, &body.media_id)?;
|
let mxc = MxcData::new(&body.server_name, &body.media_id)?;
|
||||||
let width = body.width.try_into().map_err(|_| {
|
let width = body.width.try_into().map_err(|_| {
|
||||||
Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid.")
|
Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid.")
|
||||||
|
|
@ -377,48 +811,44 @@ async fn get_content_thumbnail_route_ruma(
|
||||||
}) =
|
}) =
|
||||||
services().media.get_thumbnail(mxc.to_string(), width, height).await?
|
services().media.get_thumbnail(mxc.to_string(), width, height).await?
|
||||||
{
|
{
|
||||||
Ok(legacy_media::get_content_thumbnail::v3::Response {
|
Ok(authenticated_media_client::get_content_thumbnail::v1::Response {
|
||||||
file,
|
file,
|
||||||
content_type,
|
content_type,
|
||||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
|
||||||
})
|
})
|
||||||
} else if &*body.server_name != services().globals.server_name()
|
} else if &*body.server_name != services().globals.server_name()
|
||||||
&& body.allow_remote
|
&& allow_remote
|
||||||
{
|
{
|
||||||
let get_thumbnail_response = services()
|
let get_thumbnail_response = get_remote_thumbnail(
|
||||||
.sending
|
&body.server_name,
|
||||||
.send_federation_request(
|
authenticated_media_fed::get_content_thumbnail::v1::Request {
|
||||||
&body.server_name,
|
height: body.height,
|
||||||
legacy_media::get_content_thumbnail::v3::Request {
|
width: body.width,
|
||||||
allow_remote: false,
|
method: body.method.clone(),
|
||||||
height: body.height,
|
media_id: body.media_id.clone(),
|
||||||
width: body.width,
|
timeout_ms: Duration::from_secs(20),
|
||||||
method: body.method.clone(),
|
// we don't support animated thumbnails, so don't try requesting
|
||||||
server_name: body.server_name.clone(),
|
// one - we're allowed to ignore the client's request for an
|
||||||
media_id: body.media_id.clone(),
|
// animated thumbnail
|
||||||
timeout_ms: Duration::from_secs(20),
|
animated: Some(false),
|
||||||
allow_redirect: false,
|
},
|
||||||
animated: Some(false),
|
)
|
||||||
},
|
.await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
services()
|
services()
|
||||||
.media
|
.media
|
||||||
.upload_thumbnail(
|
.upload_thumbnail(
|
||||||
mxc.to_string(),
|
mxc.to_string(),
|
||||||
None,
|
None,
|
||||||
get_thumbnail_response.content_type.as_deref(),
|
get_thumbnail_response.content.content_type.as_deref(),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
&get_thumbnail_response.file,
|
&get_thumbnail_response.content.file,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(legacy_media::get_content_thumbnail::v3::Response {
|
Ok(authenticated_media_client::get_content_thumbnail::v1::Response {
|
||||||
file: get_thumbnail_response.file,
|
file: get_thumbnail_response.content.file,
|
||||||
content_type: get_thumbnail_response.content_type,
|
content_type: get_thumbnail_response.content.content_type,
|
||||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Error::BadRequest(ErrorKind::NotYetUploaded, "Media not found."))
|
Err(Error::BadRequest(ErrorKind::NotYetUploaded, "Media not found."))
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,10 @@ pub(crate) async fn get_supported_versions_route(
|
||||||
"v1.4".to_owned(),
|
"v1.4".to_owned(),
|
||||||
"v1.5".to_owned(),
|
"v1.5".to_owned(),
|
||||||
],
|
],
|
||||||
unstable_features: BTreeMap::from_iter([(
|
unstable_features: BTreeMap::from_iter([
|
||||||
"org.matrix.e2e_cross_signing".to_owned(),
|
("org.matrix.e2e_cross_signing".to_owned(), true),
|
||||||
true,
|
("org.matrix.msc3916.stable".to_owned(), true),
|
||||||
)]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Ra(resp))
|
Ok(Ra(resp))
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,31 @@ pub(crate) struct Ar<T> {
|
||||||
pub(crate) appservice_info: Option<RegistrationInfo>,
|
pub(crate) appservice_info: Option<RegistrationInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Ar<T> {
|
||||||
|
pub(crate) fn map_body<F, U>(self, f: F) -> Ar<U>
|
||||||
|
where
|
||||||
|
F: FnOnce(T) -> U,
|
||||||
|
{
|
||||||
|
let Ar {
|
||||||
|
body,
|
||||||
|
sender_user,
|
||||||
|
sender_device,
|
||||||
|
sender_servername,
|
||||||
|
json_body,
|
||||||
|
appservice_info,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
Ar {
|
||||||
|
body: f(body),
|
||||||
|
sender_user,
|
||||||
|
sender_device,
|
||||||
|
sender_servername,
|
||||||
|
json_body,
|
||||||
|
appservice_info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Deref for Ar<T> {
|
impl<T> Deref for Ar<T> {
|
||||||
type Target = T;
|
type Target = T;
|
||||||
|
|
||||||
|
|
|
||||||
16
src/main.rs
16
src/main.rs
|
|
@ -390,12 +390,24 @@ fn routes(config: &Config) -> Router {
|
||||||
.ruma_route(c2s::get_message_events_route)
|
.ruma_route(c2s::get_message_events_route)
|
||||||
.ruma_route(c2s::search_events_route)
|
.ruma_route(c2s::search_events_route)
|
||||||
.ruma_route(c2s::turn_server_route)
|
.ruma_route(c2s::turn_server_route)
|
||||||
.ruma_route(c2s::send_event_to_device_route)
|
.ruma_route(c2s::send_event_to_device_route);
|
||||||
|
|
||||||
|
// unauthenticated (legacy) media
|
||||||
|
let router = router
|
||||||
|
.ruma_route(c2s::get_media_config_legacy_route)
|
||||||
|
.ruma_route(c2s::get_content_legacy_route)
|
||||||
|
.ruma_route(c2s::get_content_as_filename_legacy_route)
|
||||||
|
.ruma_route(c2s::get_content_thumbnail_legacy_route);
|
||||||
|
|
||||||
|
// authenticated media
|
||||||
|
let router = router
|
||||||
.ruma_route(c2s::get_media_config_route)
|
.ruma_route(c2s::get_media_config_route)
|
||||||
.ruma_route(c2s::create_content_route)
|
.ruma_route(c2s::create_content_route)
|
||||||
.ruma_route(c2s::get_content_route)
|
.ruma_route(c2s::get_content_route)
|
||||||
.ruma_route(c2s::get_content_as_filename_route)
|
.ruma_route(c2s::get_content_as_filename_route)
|
||||||
.ruma_route(c2s::get_content_thumbnail_route)
|
.ruma_route(c2s::get_content_thumbnail_route);
|
||||||
|
|
||||||
|
let router = router
|
||||||
.ruma_route(c2s::get_devices_route)
|
.ruma_route(c2s::get_devices_route)
|
||||||
.ruma_route(c2s::get_device_route)
|
.ruma_route(c2s::get_device_route)
|
||||||
.ruma_route(c2s::update_device_route)
|
.ruma_route(c2s::update_device_route)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue