Compare commits

..

2 commits

Author SHA1 Message Date
528e75cf95
Main: Fix support for authenticated media
This patch greately improves the authenticated media support
by checking for advertised support first, then trying
unauthenticated first and correctly interpreting M_NOT_FOUND
errors to then request a token.
2024-09-20 20:06:05 +02:00
82a76a17af
Main: Add debug output for resolving server names 2024-09-20 20:05:14 +02:00

View file

@ -1,4 +1,4 @@
use std::fs;
use std::{collections::HashSet, fs};
use std::process::exit;
use std::io::Read;
@ -9,28 +9,47 @@ use log::{info, debug, error};
use serde::Deserialize;
use rustyline_async::{Readline, ReadlineEvent};
use matrix_sdk::{matrix_auth::{MatrixSession, MatrixSessionTokens}, ruma::{api::client::{authenticated_media, media}, device_id, user_id, OwnedMxcUri}, Client, SessionMeta};
use matrix_sdk::{matrix_auth::{MatrixSession, MatrixSessionTokens}, Client, SessionMeta};
use matrix_sdk::ruma::events::room::{MediaSource, message::{MessageType, RoomMessageEventContent}};
use matrix_sdk::ruma::api::client::error::{ErrorBody, ErrorKind};
use matrix_sdk::ruma::api::client::{authenticated_media, media};
use matrix_sdk::ruma::api::{MatrixVersion, OutgoingRequest};
use matrix_sdk::ruma::{device_id, user_id, OwnedMxcUri};
use matrix_sdk::reqwest::StatusCode;
// NOTE: This has been stolen from the matrix-rust-sdk to patch out checks for authenticated media
async fn get_media_content(
client: &Client,
source: &MediaSource,
use_auth: bool
use_authenticated_media_endpoints: bool
) -> matrix_sdk::Result<Vec<u8>> {
#[allow(deprecated)]
type DeprecatedMediaRequest = media::get_content::v3::Request;
type AuthenticatedMediaRequest = authenticated_media::get_content::v1::Request;
let deprecated_media_request_path = DeprecatedMediaRequest::METADATA.history.stable_endpoint_for(&[MatrixVersion::V1_1]).unwrap();
let authenticated_media_request_path = AuthenticatedMediaRequest::METADATA.history.stable_endpoint_for(&[MatrixVersion::V1_11]).unwrap();
let homeserver_base_url = client.homeserver().to_string();
let homeserver_base_url = homeserver_base_url.trim_end_matches("/");
let content: Vec<u8> = match &source {
MediaSource::Encrypted(file) => {
let content = if use_auth {
let request = authenticated_media::get_content::v1::Request::from_uri(&file.url)?;
let content = if use_authenticated_media_endpoints {
debug!("Attempting to download {} from {}{}", file.url, homeserver_base_url, authenticated_media_request_path);
let request = AuthenticatedMediaRequest::from_uri(&file.url)?;
client.send(request, None).await?.file
} else {
debug!("Attempting to download {} from {}{}", file.url, homeserver_base_url, deprecated_media_request_path);
#[allow(deprecated)]
let request = media::get_content::v3::Request::from_url(&file.url)?;
let request = DeprecatedMediaRequest::from_url(&file.url)?;
client.send(request, None).await?.file
};
let content = {
debug!("Decrypting file");
debug!("Decrypting file using {}", file.key.alg);
let content_len = content.len();
let mut cursor = std::io::Cursor::new(content);
@ -57,14 +76,18 @@ async fn get_media_content(
content
}
MediaSource::Plain(uri) => {
debug!("File doesn't appear to be encrypted");
debug!("No encryption details provided, will not attempt decryption after download");
if use_auth {
let request = authenticated_media::get_content::v1::Request::from_uri(uri)?;
if use_authenticated_media_endpoints {
debug!("Attempting to download {} from {}{}", uri, homeserver_base_url, authenticated_media_request_path);
let request = AuthenticatedMediaRequest::from_uri(uri)?;
client.send(request, None).await?.file
} else {
debug!("Attempting to download {} from {}{}", uri, homeserver_base_url, deprecated_media_request_path);
#[allow(deprecated)]
let request = media::get_content::v3::Request::from_url(uri)?;
let request = DeprecatedMediaRequest::from_url(uri)?;
client.send(request, None).await?.file
}
}
@ -73,6 +96,49 @@ async fn get_media_content(
Ok(content)
}
async fn supports_authenticated_media(client: &Client) -> Result<bool> {
debug!("Discovering supported versions and features");
let supported_versions_response = client.send(
matrix_sdk::ruma::api::client::discovery::get_supported_versions::Request::new(),
None
).await?;
let known_supported_versions = supported_versions_response.known_versions().collect::<Vec<_>>();
let supported_unstable_features = supported_versions_response.unstable_features
.into_iter()
.filter(|(_, supported)| *supported)
.map(|(feature, _)| feature)
.collect::<HashSet<_>>();
const AUTHENTICATED_MEDIA_STABLE_FEATURE: &str = "org.matrix.msc3916.stable";
Ok(match (
known_supported_versions.contains(&MatrixVersion::V1_11),
supported_unstable_features.contains(AUTHENTICATED_MEDIA_STABLE_FEATURE)
) {
(false, false) => {
debug!("Authenticated Media *is not* supported (the server neither advertises Matrix 1.11 nor org.matrix.msc3916.stable)");
false
}
(true, false) => {
debug!("Authenticated Media *is* supported (the server advertises support for Matrix 1.11 but not org.matrix.msc3916.stable)");
true
}
(false, true) => {
debug!("Authenticated Media *is* supported (the server doesn't advertise support for Matrix 1.11 but does for org.matrix.msc3916.stable)");
true
}
(true, true) => {
debug!("Authenticated Media *is* supported (the server advertises support for both Matrix 1.11 and org.matrix.msc3916.stable)");
true
}
})
}
async fn process_media(source: MediaSource, body: Option<String>, server: Option<String>) -> Result<()> {
let uri = match source.clone() {
MediaSource::Plain(uri) => uri,
@ -83,51 +149,68 @@ async fn process_media(source: MediaSource, body: Option<String>, server: Option
let media_id = uri.media_id()?;
let client = match server {
Some(ref url) => Client::builder().server_name_or_homeserver_url(url),
Some(ref url) => {
debug!("Resolving server name or homeserver url '{url}' from command line");
Client::builder().server_name_or_homeserver_url(url)
},
// NOTE: We know that server_name is not a URL at this point, however if resolving .well-known
// delegation fails falling back to interpreting the server name as the base URL is somewhat
// more resilient. Just had this now.
None => Client::builder().server_name_or_homeserver_url(server_name),
None => {
debug!("Resolving server name or homeserver url '{server_name}' mxc uri '{uri}'");
Client::builder().server_name_or_homeserver_url(server_name)
},
}.build().await?;
debug!("Downloading {} from {}", uri, client.homeserver());
let supports_authenticated_media = supports_authenticated_media(&client).await?;
let content = match get_media_content(&client, &source, false).await {
Ok(content) => content,
Err(error) => {
let error_is_needs_authentication = match &error {
matrix_sdk::Error::Http(http) => match http {
matrix_sdk::HttpError::IntoHttp(into_http) => {
match into_http {
matrix_sdk::ruma::api::error::IntoHttpError::NeedsAuthentication => true,
_ => false,
}
},
_ => false
},
_ => false,
};
let content = match supports_authenticated_media {
false => get_media_content(&client, &source, false).await
.map_err(|error| anyhow::Error::msg(error.to_string()))?,
if error_is_needs_authentication {
info!("The server did not allow downloading without authentication, please enter an access token:");
true => match get_media_content(&client, &source, false).await {
Ok(content) => content,
Err(error) => {
let is_probably_new_media = matches!(
error,
matrix_sdk::Error::Http(
matrix_sdk::HttpError::Api(
matrix_sdk::ruma::api::error::FromHttpResponseError::Server(
matrix_sdk::RumaApiError::ClientApi(
matrix_sdk::ruma::api::client::Error {
status_code: StatusCode::NOT_FOUND,
body: ErrorBody::Standard { kind: ErrorKind::NotFound, message: _ },
..
}
)
)
)
)
);
let token = rpassword::prompt_password("> ")?;
if is_probably_new_media {
info!("The server returned 404 M_NOT_FOUND. Assuming the media is new, please enter an access token:");
client.restore_session(MatrixSession {
meta: SessionMeta {
user_id: user_id!("@unused:example.com").to_owned(),
device_id: device_id!("UNUSED").to_owned(),
},
tokens: MatrixSessionTokens {
access_token: token,
refresh_token: None,
},
}).await?;
let token = rpassword::prompt_password("> ")?;
get_media_content(&client, &source, false).await
.map_err(|error| anyhow::Error::msg(error.to_string()))?
} else {
return Err(anyhow::Error::msg(error.to_string()));
client.restore_session(MatrixSession {
meta: SessionMeta {
user_id: user_id!("@unused:example.com").to_owned(),
device_id: device_id!("UNUSED").to_owned(),
},
tokens: MatrixSessionTokens {
access_token: token,
refresh_token: None,
},
}).await?;
get_media_content(&client, &source, true).await
.map_err(|error| anyhow::Error::msg(error.to_string()))?
} else {
return Err(anyhow::Error::msg(error.to_string()));
}
}
}
};