mirror of
https://gitlab.computer.surgery/matrix/grapevine.git
synced 2025-12-16 23:31:24 +01:00
<https://github.com/ruma/ruma/pull/2045> Co-authored-by: Jonas Platte <jplatte+git@posteo.de>
2215 lines
72 KiB
Rust
2215 lines
72 KiB
Rust
use std::{
|
|
collections::BTreeMap,
|
|
fmt::Debug,
|
|
mem,
|
|
net::{IpAddr, SocketAddr},
|
|
sync::Arc,
|
|
time::Instant,
|
|
};
|
|
|
|
use axum::{response::IntoResponse, Json};
|
|
use axum_extra::headers::{Authorization, HeaderMapExt};
|
|
use base64::Engine as _;
|
|
use get_profile_information::v1::ProfileField;
|
|
use ruma::{
|
|
api::{
|
|
client::error::{Error as RumaError, ErrorKind},
|
|
federation::{
|
|
authenticated_media,
|
|
authentication::XMatrix,
|
|
authorization::get_event_authorization,
|
|
backfill::get_backfill,
|
|
device::get_devices::{self, v1::UserDevice},
|
|
directory::{get_public_rooms, get_public_rooms_filtered},
|
|
discovery::{
|
|
get_server_keys, get_server_version, ServerSigningKeys,
|
|
},
|
|
event::{
|
|
get_event, get_missing_events, get_room_state,
|
|
get_room_state_ids,
|
|
},
|
|
keys::{claim_keys, get_keys},
|
|
membership::{
|
|
create_invite, create_join_event, prepare_join_event,
|
|
},
|
|
query::{get_profile_information, get_room_information},
|
|
transactions::{
|
|
edu::{
|
|
DeviceListUpdateContent, DirectDeviceContent, Edu,
|
|
SigningKeyUpdateContent,
|
|
},
|
|
send_transaction_message,
|
|
},
|
|
},
|
|
EndpointError, IncomingResponse, MatrixVersion, OutgoingRequest,
|
|
OutgoingResponse, SendAccessToken,
|
|
},
|
|
directory::{Filter, RoomNetwork},
|
|
events::{
|
|
receipt::{ReceiptEvent, ReceiptEventContent, ReceiptType},
|
|
room::{
|
|
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
|
member::{MembershipState, RoomMemberEventContent},
|
|
},
|
|
StateEventType, TimelineEventType,
|
|
},
|
|
serde::{Base64, JsonObject, Raw},
|
|
state_res::Event,
|
|
to_device::DeviceIdOrAllDevices,
|
|
uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId,
|
|
MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedServerName,
|
|
OwnedServerSigningKeyId, OwnedSigningKeyId, OwnedUserId, RoomId,
|
|
ServerName, Signatures,
|
|
};
|
|
use serde_json::value::{to_raw_value, RawValue as RawJsonValue};
|
|
use tokio::sync::RwLock;
|
|
use tracing::{debug, error, field, trace, trace_span, warn};
|
|
|
|
use super::appservice_server;
|
|
use crate::{
|
|
api::client_server::{self, claim_keys_helper, get_keys_helper},
|
|
observability::{FoundIn, Lookup, METRICS},
|
|
service::{
|
|
globals::SigningKeys,
|
|
pdu::{gen_event_id_canonical_json, PduBuilder},
|
|
rooms::state::ExtractVersion,
|
|
},
|
|
services,
|
|
utils::{self, dbg_truncate_str, MxcData},
|
|
Ar, Error, PduEvent, Ra, Result,
|
|
};
|
|
|
|
/// Wraps either an literal IP address plus port, or a hostname plus complement
|
|
/// (colon-plus-port if it was specified).
|
|
///
|
|
/// Note: A [`FedDest::Named`] might contain an IP address in string form if
|
|
/// there was no port specified to construct a [`SocketAddr`] with.
|
|
///
|
|
/// # Examples:
|
|
/// ```rust
|
|
/// # use grapevine::api::server_server::FedDest;
|
|
/// # fn main() -> Result<(), std::net::AddrParseError> {
|
|
/// FedDest::Literal("198.51.100.3:8448".parse()?);
|
|
/// FedDest::Literal("[2001:db8::4:5]:443".parse()?);
|
|
/// FedDest::Named("matrix.example.org".to_owned(), "".to_owned());
|
|
/// FedDest::Named("matrix.example.org".to_owned(), ":8448".to_owned());
|
|
/// FedDest::Named("198.51.100.5".to_owned(), "".to_owned());
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(crate) enum FedDest {
|
|
Literal(SocketAddr),
|
|
Named(String, String),
|
|
}
|
|
|
|
impl FedDest {
|
|
fn to_https_string(&self) -> String {
|
|
match self {
|
|
Self::Literal(addr) => format!("https://{addr}"),
|
|
Self::Named(host, port) => format!("https://{host}{port}"),
|
|
}
|
|
}
|
|
|
|
fn to_uri_string(&self) -> String {
|
|
match self {
|
|
Self::Literal(addr) => addr.to_string(),
|
|
Self::Named(host, port) => format!("{host}{port}"),
|
|
}
|
|
}
|
|
|
|
fn hostname(&self) -> String {
|
|
match &self {
|
|
Self::Literal(addr) => addr.ip().to_string(),
|
|
Self::Named(host, _) => host.clone(),
|
|
}
|
|
}
|
|
|
|
fn port(&self) -> Option<u16> {
|
|
match &self {
|
|
Self::Literal(addr) => Some(addr.port()),
|
|
Self::Named(_, port) => {
|
|
port.strip_prefix(':').and_then(|x| x.parse().ok())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) enum LogRequestError {
|
|
Yes,
|
|
No,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) enum AllowLoopbackRequests {
|
|
Yes,
|
|
No,
|
|
}
|
|
|
|
#[allow(clippy::too_many_lines)]
|
|
#[tracing::instrument(skip(request, log_error, allow_loopback), fields(url))]
|
|
pub(crate) async fn send_request<T>(
|
|
destination: &ServerName,
|
|
request: T,
|
|
log_error: LogRequestError,
|
|
allow_loopback: AllowLoopbackRequests,
|
|
) -> Result<T::IncomingResponse>
|
|
where
|
|
T: OutgoingRequest + Debug,
|
|
{
|
|
if !services().globals.allow_federation() {
|
|
return Err(Error::BadConfig("Federation is disabled."));
|
|
}
|
|
|
|
if destination == services().globals.server_name()
|
|
&& allow_loopback == AllowLoopbackRequests::No
|
|
{
|
|
return Err(Error::bad_config(
|
|
"Won't send federation request to ourselves",
|
|
));
|
|
}
|
|
|
|
debug!("Preparing to send request");
|
|
|
|
let mut write_destination_to_cache = false;
|
|
|
|
let cached_result = services()
|
|
.globals
|
|
.actual_destination_cache
|
|
.read()
|
|
.await
|
|
.get(destination)
|
|
.cloned();
|
|
|
|
let (actual_destination, host) = if let Some(result) = cached_result {
|
|
METRICS.record_lookup(Lookup::FederationDestination, FoundIn::Cache);
|
|
result
|
|
} else {
|
|
write_destination_to_cache = true;
|
|
|
|
let result = find_actual_destination(destination).await;
|
|
|
|
(result.0, result.1.to_uri_string())
|
|
};
|
|
|
|
let actual_destination_str = actual_destination.to_https_string();
|
|
|
|
let mut http_request = request
|
|
.try_into_http_request::<Vec<u8>>(
|
|
&actual_destination_str,
|
|
SendAccessToken::IfRequired(""),
|
|
&[MatrixVersion::V1_11],
|
|
)
|
|
.map_err(|error| {
|
|
warn!(
|
|
%error,
|
|
actual_destination = actual_destination_str,
|
|
"Failed to find destination",
|
|
);
|
|
Error::BadServerResponse("Invalid destination")
|
|
})?;
|
|
|
|
let mut request_map = serde_json::Map::new();
|
|
|
|
if !http_request.body().is_empty() {
|
|
request_map.insert(
|
|
"content".to_owned(),
|
|
serde_json::from_slice(http_request.body())
|
|
.expect("body is valid json, we just created it"),
|
|
);
|
|
}
|
|
|
|
request_map
|
|
.insert("method".to_owned(), T::METADATA.method.to_string().into());
|
|
request_map.insert(
|
|
"uri".to_owned(),
|
|
http_request
|
|
.uri()
|
|
.path_and_query()
|
|
.expect("all requests have a path")
|
|
.to_string()
|
|
.into(),
|
|
);
|
|
request_map.insert(
|
|
"origin".to_owned(),
|
|
services().globals.server_name().as_str().into(),
|
|
);
|
|
request_map.insert("destination".to_owned(), destination.as_str().into());
|
|
|
|
let mut request_json = serde_json::from_value(request_map.into())
|
|
.expect("valid JSON is valid BTreeMap");
|
|
|
|
ruma::signatures::sign_json(
|
|
services().globals.server_name().as_str(),
|
|
services().globals.keypair(),
|
|
&mut request_json,
|
|
)
|
|
.expect("our request json is what ruma expects");
|
|
|
|
let request_json: serde_json::Map<String, serde_json::Value> =
|
|
serde_json::from_slice(&serde_json::to_vec(&request_json).unwrap())
|
|
.unwrap();
|
|
|
|
// There's exactly the one signature we just created, fish it back out again
|
|
let (key_id, signature) = request_json["signatures"]
|
|
.get(services().globals.server_name().as_str())
|
|
.unwrap()
|
|
.as_object()
|
|
.unwrap()
|
|
.iter()
|
|
.next()
|
|
.unwrap();
|
|
|
|
let key_id = OwnedSigningKeyId::try_from(key_id.clone()).unwrap();
|
|
let signature = Base64::parse(signature.as_str().unwrap())
|
|
.expect("generated signature should be valid base64");
|
|
|
|
http_request.headers_mut().typed_insert(Authorization(XMatrix::new(
|
|
services().globals.server_name().to_owned(),
|
|
destination.to_owned(),
|
|
key_id,
|
|
signature,
|
|
)));
|
|
|
|
// can be enabled selectively using `filter =
|
|
// grapevine[outgoing_request_curl]=trace` in config
|
|
trace_span!("outgoing_request_curl").in_scope(|| {
|
|
trace!(
|
|
cmd = utils::curlify(&http_request),
|
|
"curl command line for outgoing request"
|
|
);
|
|
});
|
|
let reqwest_request = reqwest::Request::try_from(http_request)?;
|
|
|
|
let url = reqwest_request.url().clone();
|
|
tracing::Span::current().record("url", field::display(url));
|
|
|
|
debug!("Sending request");
|
|
let response =
|
|
services().globals.federation_client().execute(reqwest_request).await;
|
|
|
|
let mut response = response.inspect_err(|error| {
|
|
if log_error == LogRequestError::Yes {
|
|
warn!(%error, "Could not send request");
|
|
}
|
|
})?;
|
|
|
|
// reqwest::Response -> http::Response conversion
|
|
let status = response.status();
|
|
debug!(status = u16::from(status), "Received response");
|
|
let mut http_response_builder =
|
|
http::Response::builder().status(status).version(response.version());
|
|
mem::swap(
|
|
response.headers_mut(),
|
|
http_response_builder
|
|
.headers_mut()
|
|
.expect("http::response::Builder is usable"),
|
|
);
|
|
|
|
debug!("Getting response bytes");
|
|
// TODO: handle timeout
|
|
let body = response.bytes().await.unwrap_or_else(|error| {
|
|
warn!(%error, "Server error");
|
|
Vec::new().into()
|
|
});
|
|
debug!("Got response bytes");
|
|
|
|
if status != 200 {
|
|
warn!(
|
|
status = u16::from(status),
|
|
response =
|
|
dbg_truncate_str(String::from_utf8_lossy(&body).as_ref(), 100)
|
|
.into_owned(),
|
|
"Received error over federation",
|
|
);
|
|
}
|
|
|
|
let http_response = http_response_builder
|
|
.body(body)
|
|
.expect("reqwest body is valid http body");
|
|
|
|
if status != 200 {
|
|
return Err(Error::Federation(
|
|
destination.to_owned(),
|
|
RumaError::from_http_response(http_response),
|
|
));
|
|
}
|
|
|
|
debug!("Parsing response bytes");
|
|
let response = T::IncomingResponse::try_from_http_response(http_response);
|
|
if response.is_ok() && write_destination_to_cache {
|
|
METRICS.record_lookup(Lookup::FederationDestination, FoundIn::Remote);
|
|
services().globals.actual_destination_cache.write().await.insert(
|
|
OwnedServerName::from(destination),
|
|
(actual_destination, host),
|
|
);
|
|
}
|
|
|
|
response.map_err(|e| {
|
|
warn!(error = %e, "Invalid 200 response");
|
|
Error::BadServerResponse("Server returned bad 200 response.")
|
|
})
|
|
}
|
|
|
|
fn get_ip_with_port(destination_str: &str) -> Option<FedDest> {
|
|
if let Ok(destination) = destination_str.parse::<SocketAddr>() {
|
|
Some(FedDest::Literal(destination))
|
|
} else if let Ok(ip_addr) = destination_str.parse::<IpAddr>() {
|
|
Some(FedDest::Literal(SocketAddr::new(ip_addr, 8448)))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn add_port_to_hostname(destination_str: &str) -> FedDest {
|
|
let (host, port) = match destination_str.find(':') {
|
|
None => (destination_str, ":8448"),
|
|
Some(pos) => destination_str.split_at(pos),
|
|
};
|
|
FedDest::Named(host.to_owned(), port.to_owned())
|
|
}
|
|
|
|
/// Returns: `actual_destination`, `Host` header
|
|
/// Implemented according to the specification at <https://matrix.org/docs/spec/server_server/r0.1.4#resolving-server-names>
|
|
/// Numbers in comments below refer to bullet points in linked section of
|
|
/// specification
|
|
#[allow(clippy::too_many_lines)]
|
|
#[tracing::instrument(ret(level = "debug"))]
|
|
async fn find_actual_destination(
|
|
destination: &'_ ServerName,
|
|
) -> (FedDest, FedDest) {
|
|
debug!("Finding actual destination");
|
|
let destination_str = destination.as_str().to_owned();
|
|
let mut hostname = destination_str.clone();
|
|
let actual_destination = match get_ip_with_port(&destination_str) {
|
|
Some(host_port) => {
|
|
debug!("1: IP literal with provided or default port");
|
|
host_port
|
|
}
|
|
None => {
|
|
if let Some(pos) = destination_str.find(':') {
|
|
debug!("2: Hostname with included port");
|
|
let (host, port) = destination_str.split_at(pos);
|
|
FedDest::Named(host.to_owned(), port.to_owned())
|
|
} else {
|
|
debug!(%destination, "Requesting well known");
|
|
if let Some(delegated_hostname) =
|
|
request_well_known(destination.as_str()).await
|
|
{
|
|
debug!("3: A .well-known file is available");
|
|
hostname = add_port_to_hostname(&delegated_hostname)
|
|
.to_uri_string();
|
|
if let Some(host_and_port) =
|
|
get_ip_with_port(&delegated_hostname)
|
|
{
|
|
host_and_port
|
|
} else if let Some(pos) = delegated_hostname.find(':') {
|
|
debug!("3.2: Hostname with port in .well-known file");
|
|
let (host, port) = delegated_hostname.split_at(pos);
|
|
FedDest::Named(host.to_owned(), port.to_owned())
|
|
} else {
|
|
debug!("Delegated hostname has no port in this branch");
|
|
if let Some(hostname_override) =
|
|
query_srv_record(&delegated_hostname).await
|
|
{
|
|
debug!("3.3: SRV lookup successful");
|
|
let force_port = hostname_override.port();
|
|
|
|
if let Ok(override_ip) = services()
|
|
.globals
|
|
.dns_resolver()
|
|
.lookup_ip(hostname_override.hostname())
|
|
.await
|
|
{
|
|
services()
|
|
.globals
|
|
.tls_name_override
|
|
.write()
|
|
.unwrap()
|
|
.insert(
|
|
delegated_hostname.clone(),
|
|
(
|
|
override_ip.iter().collect(),
|
|
force_port.unwrap_or(8448),
|
|
),
|
|
);
|
|
} else {
|
|
warn!(
|
|
"Using SRV record, but could not resolve \
|
|
to IP"
|
|
);
|
|
}
|
|
|
|
if let Some(port) = force_port {
|
|
FedDest::Named(
|
|
delegated_hostname,
|
|
format!(":{port}"),
|
|
)
|
|
} else {
|
|
add_port_to_hostname(&delegated_hostname)
|
|
}
|
|
} else {
|
|
debug!(
|
|
"3.4: No SRV records, just use the hostname \
|
|
from .well-known"
|
|
);
|
|
add_port_to_hostname(&delegated_hostname)
|
|
}
|
|
}
|
|
} else {
|
|
debug!("4: No .well-known or an error occured");
|
|
if let Some(hostname_override) =
|
|
query_srv_record(&destination_str).await
|
|
{
|
|
debug!("4: SRV record found");
|
|
let force_port = hostname_override.port();
|
|
|
|
if let Ok(override_ip) = services()
|
|
.globals
|
|
.dns_resolver()
|
|
.lookup_ip(hostname_override.hostname())
|
|
.await
|
|
{
|
|
services()
|
|
.globals
|
|
.tls_name_override
|
|
.write()
|
|
.unwrap()
|
|
.insert(
|
|
hostname.clone(),
|
|
(
|
|
override_ip.iter().collect(),
|
|
force_port.unwrap_or(8448),
|
|
),
|
|
);
|
|
} else {
|
|
warn!(
|
|
"Using SRV record, but could not resolve to IP"
|
|
);
|
|
}
|
|
|
|
if let Some(port) = force_port {
|
|
FedDest::Named(hostname.clone(), format!(":{port}"))
|
|
} else {
|
|
add_port_to_hostname(&hostname)
|
|
}
|
|
} else {
|
|
debug!("5: No SRV record found");
|
|
add_port_to_hostname(&destination_str)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
debug!(?actual_destination, "Resolved actual destination");
|
|
|
|
// Can't use get_ip_with_port here because we don't want to add a port
|
|
// to an IP address if it wasn't specified
|
|
let hostname = if let Ok(addr) = hostname.parse::<SocketAddr>() {
|
|
FedDest::Literal(addr)
|
|
} else if let Ok(addr) = hostname.parse::<IpAddr>() {
|
|
FedDest::Named(addr.to_string(), ":8448".to_owned())
|
|
} else if let Some(pos) = hostname.find(':') {
|
|
let (host, port) = hostname.split_at(pos);
|
|
FedDest::Named(host.to_owned(), port.to_owned())
|
|
} else {
|
|
FedDest::Named(hostname, ":8448".to_owned())
|
|
};
|
|
(actual_destination, hostname)
|
|
}
|
|
|
|
#[tracing::instrument(ret(level = "debug"))]
|
|
async fn query_given_srv_record(record: &str) -> Option<FedDest> {
|
|
services()
|
|
.globals
|
|
.dns_resolver()
|
|
.srv_lookup(record)
|
|
.await
|
|
.map(|srv| {
|
|
srv.iter().next().map(|result| {
|
|
FedDest::Named(
|
|
result
|
|
.target()
|
|
.to_string()
|
|
.trim_end_matches('.')
|
|
.to_owned(),
|
|
format!(":{}", result.port()),
|
|
)
|
|
})
|
|
})
|
|
.unwrap_or(None)
|
|
}
|
|
|
|
#[tracing::instrument(ret(level = "debug"))]
|
|
async fn query_srv_record(hostname: &'_ str) -> Option<FedDest> {
|
|
let hostname = hostname.trim_end_matches('.');
|
|
|
|
if let Some(host_port) =
|
|
query_given_srv_record(&format!("_matrix-fed._tcp.{hostname}.")).await
|
|
{
|
|
Some(host_port)
|
|
} else {
|
|
query_given_srv_record(&format!("_matrix._tcp.{hostname}.")).await
|
|
}
|
|
}
|
|
|
|
#[tracing::instrument(ret(level = "debug"))]
|
|
async fn request_well_known(destination: &str) -> Option<String> {
|
|
let response = services()
|
|
.globals
|
|
.default_client()
|
|
.get(format!("https://{destination}/.well-known/matrix/server"))
|
|
.send()
|
|
.await;
|
|
debug!("Got well known response");
|
|
if let Err(error) = &response {
|
|
debug!(%error, "Failed to request .well-known");
|
|
return None;
|
|
}
|
|
let text = response.ok()?.text().await;
|
|
debug!("Got well known response text");
|
|
let body: serde_json::Value = serde_json::from_str(&text.ok()?).ok()?;
|
|
Some(body.get("m.server")?.as_str()?.to_owned())
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/version`
|
|
///
|
|
/// Get version information on this server.
|
|
pub(crate) async fn get_server_version_route(
|
|
_body: Ar<get_server_version::v1::Request>,
|
|
) -> Result<Ra<get_server_version::v1::Response>> {
|
|
Ok(Ra(get_server_version::v1::Response {
|
|
server: Some(get_server_version::v1::Server {
|
|
name: Some(env!("CARGO_PKG_NAME").to_owned()),
|
|
version: Some(crate::version()),
|
|
}),
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/key/v2/server`
|
|
///
|
|
/// Gets the public signing keys of this server.
|
|
///
|
|
/// - Matrix does not support invalidating public keys, so the key returned by
|
|
/// this will be valid forever.
|
|
// Response type for this endpoint is Json because we need to calculate a
|
|
// signature for the response
|
|
pub(crate) async fn get_server_keys_route() -> Result<impl IntoResponse> {
|
|
fn convert_key_ids<K>(
|
|
keys: BTreeMap<String, K>,
|
|
) -> BTreeMap<OwnedServerSigningKeyId, K> {
|
|
keys.into_iter()
|
|
.map(|(id, key)| {
|
|
let id = id
|
|
.try_into()
|
|
.expect("found invalid server signing keys in DB");
|
|
(id, key)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
let keys = SigningKeys::load_own_keys();
|
|
let verify_keys = convert_key_ids(keys.verify_keys);
|
|
let old_verify_keys = convert_key_ids(keys.old_verify_keys);
|
|
|
|
let mut response = serde_json::from_slice(
|
|
get_server_keys::v2::Response {
|
|
server_key: Raw::new(&ServerSigningKeys {
|
|
server_name: services().globals.server_name().to_owned(),
|
|
verify_keys,
|
|
old_verify_keys,
|
|
signatures: Signatures::new(),
|
|
valid_until_ts: keys.valid_until_ts,
|
|
})
|
|
.expect("static conversion, no errors"),
|
|
}
|
|
.try_into_http_response::<Vec<u8>>()
|
|
.unwrap()
|
|
.body(),
|
|
)
|
|
.unwrap();
|
|
|
|
ruma::signatures::sign_json(
|
|
services().globals.server_name().as_str(),
|
|
services().globals.keypair(),
|
|
&mut response,
|
|
)
|
|
.unwrap();
|
|
|
|
Ok(Json(response))
|
|
}
|
|
|
|
/// # `GET /_matrix/key/v2/server/{keyId}`
|
|
///
|
|
/// Gets the public signing keys of this server.
|
|
///
|
|
/// - Matrix does not support invalidating public keys, so the key returned by
|
|
/// this will be valid forever.
|
|
pub(crate) async fn get_server_keys_deprecated_route() -> impl IntoResponse {
|
|
get_server_keys_route().await
|
|
}
|
|
|
|
/// # `POST /_matrix/federation/v1/publicRooms`
|
|
///
|
|
/// Lists the public rooms on this server.
|
|
pub(crate) async fn get_public_rooms_filtered_route(
|
|
body: Ar<get_public_rooms_filtered::v1::Request>,
|
|
) -> Result<Ra<get_public_rooms_filtered::v1::Response>> {
|
|
let response = client_server::get_public_rooms_filtered_helper(
|
|
None,
|
|
body.limit,
|
|
body.since.as_deref(),
|
|
&body.filter,
|
|
&body.room_network,
|
|
)
|
|
.await?;
|
|
|
|
Ok(Ra(get_public_rooms_filtered::v1::Response {
|
|
chunk: response.chunk,
|
|
prev_batch: response.prev_batch,
|
|
next_batch: response.next_batch,
|
|
total_room_count_estimate: response.total_room_count_estimate,
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/publicRooms`
|
|
///
|
|
/// Lists the public rooms on this server.
|
|
pub(crate) async fn get_public_rooms_route(
|
|
body: Ar<get_public_rooms::v1::Request>,
|
|
) -> Result<Ra<get_public_rooms::v1::Response>> {
|
|
let response = client_server::get_public_rooms_filtered_helper(
|
|
None,
|
|
body.limit,
|
|
body.since.as_deref(),
|
|
&Filter::default(),
|
|
&RoomNetwork::Matrix,
|
|
)
|
|
.await?;
|
|
|
|
Ok(Ra(get_public_rooms::v1::Response {
|
|
chunk: response.chunk,
|
|
prev_batch: response.prev_batch,
|
|
next_batch: response.next_batch,
|
|
total_room_count_estimate: response.total_room_count_estimate,
|
|
}))
|
|
}
|
|
|
|
#[tracing::instrument(skip(pdu))]
|
|
pub(crate) fn parse_incoming_pdu(
|
|
pdu: &RawJsonValue,
|
|
) -> Result<(OwnedEventId, CanonicalJsonObject, OwnedRoomId)> {
|
|
let value: CanonicalJsonObject =
|
|
serde_json::from_str(pdu.get()).map_err(|error| {
|
|
warn!(%error, object = ?pdu, "Error parsing incoming event");
|
|
Error::BadServerResponse("Invalid PDU in server response")
|
|
})?;
|
|
|
|
let room_id: OwnedRoomId = value
|
|
.get("room_id")
|
|
.and_then(|id| RoomId::parse(id.as_str()?).ok())
|
|
.ok_or(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Invalid room id in pdu",
|
|
))?;
|
|
|
|
let room_version_id = services()
|
|
.rooms
|
|
.state
|
|
.get_create_content::<ExtractVersion>(&room_id)?;
|
|
let Some(room_version_rules) = room_version_id.rules() else {
|
|
return Err(Error::UnsupportedRoomVersion(room_version_id));
|
|
};
|
|
|
|
let Ok((event_id, value)) =
|
|
gen_event_id_canonical_json(pdu, &room_version_rules)
|
|
else {
|
|
// Event could not be converted to canonical json
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Could not convert event to canonical json.",
|
|
));
|
|
};
|
|
Ok((event_id, value, room_id))
|
|
}
|
|
|
|
/// # `PUT /_matrix/federation/v1/send/{txnId}`
|
|
///
|
|
/// Push EDUs and PDUs to this server.
|
|
#[allow(clippy::too_many_lines)]
|
|
pub(crate) async fn send_transaction_message_route(
|
|
body: Ar<send_transaction_message::v1::Request>,
|
|
) -> Result<Ra<send_transaction_message::v1::Response>> {
|
|
let sender_servername =
|
|
body.sender_servername.as_ref().expect("server is authenticated");
|
|
|
|
let mut resolved_map = BTreeMap::new();
|
|
|
|
let pub_key_map = RwLock::new(BTreeMap::new());
|
|
|
|
for pdu in &body.pdus {
|
|
let value: CanonicalJsonObject = serde_json::from_str(pdu.get())
|
|
.map_err(|error| {
|
|
warn!(%error, object = ?pdu, "Error parsing incoming event");
|
|
Error::BadServerResponse("Invalid PDU in server response")
|
|
})?;
|
|
let room_id: OwnedRoomId = value
|
|
.get("room_id")
|
|
.and_then(|id| RoomId::parse(id.as_str()?).ok())
|
|
.ok_or(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Invalid room id in pdu",
|
|
))?;
|
|
|
|
if services()
|
|
.rooms
|
|
.state
|
|
.get_create_content::<ExtractVersion>(&room_id)
|
|
.is_err()
|
|
{
|
|
debug!(%room_id, "This server is not in the room");
|
|
continue;
|
|
}
|
|
|
|
let r = parse_incoming_pdu(pdu);
|
|
let (event_id, value, room_id) = match r {
|
|
Ok(t) => t,
|
|
Err(error) => {
|
|
warn!(%error, object = ?pdu, "Error parsing incoming event");
|
|
continue;
|
|
}
|
|
};
|
|
// We do not add the event_id field to the pdu here because of signature
|
|
// and hashes checks
|
|
|
|
let federation_token = services()
|
|
.globals
|
|
.roomid_mutex_federation
|
|
.lock_key(room_id.clone())
|
|
.await;
|
|
let start_time = Instant::now();
|
|
resolved_map.insert(
|
|
event_id.clone(),
|
|
services()
|
|
.rooms
|
|
.event_handler
|
|
.handle_incoming_pdu(
|
|
sender_servername,
|
|
&event_id,
|
|
&room_id,
|
|
value,
|
|
true,
|
|
&pub_key_map,
|
|
)
|
|
.await
|
|
.map(|_| ()),
|
|
);
|
|
drop(federation_token);
|
|
|
|
debug!(
|
|
%event_id,
|
|
elapsed = ?start_time.elapsed(),
|
|
"Finished handling event",
|
|
);
|
|
}
|
|
|
|
for pdu in &resolved_map {
|
|
if let (event_id, Err(error)) = pdu {
|
|
if matches!(error, Error::BadRequest(ErrorKind::NotFound, _)) {
|
|
warn!(%error, %event_id, "Incoming PDU failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
for edu in body
|
|
.edus
|
|
.iter()
|
|
.filter_map(|edu| serde_json::from_str::<Edu>(edu.json().get()).ok())
|
|
{
|
|
match edu {
|
|
Edu::Receipt(receipt) => {
|
|
for (room_id, room_updates) in receipt.receipts {
|
|
for (user_id, user_updates) in room_updates.read {
|
|
if user_id.server_name() != sender_servername {
|
|
warn!(
|
|
%user_id,
|
|
%sender_servername,
|
|
"Got receipt EDU from incorrect homeserver, \
|
|
ignoring",
|
|
);
|
|
continue;
|
|
}
|
|
if let Some((event_id, _)) = user_updates
|
|
.event_ids
|
|
.iter()
|
|
.filter_map(|id| {
|
|
services()
|
|
.rooms
|
|
.timeline
|
|
.get_pdu_count(id)
|
|
.ok()
|
|
.flatten()
|
|
.map(|r| (id, r))
|
|
})
|
|
.max_by_key(|(_, count)| *count)
|
|
{
|
|
let mut user_receipts = BTreeMap::new();
|
|
user_receipts
|
|
.insert(user_id.clone(), user_updates.data);
|
|
|
|
let mut receipts = BTreeMap::new();
|
|
receipts.insert(ReceiptType::Read, user_receipts);
|
|
|
|
let mut receipt_content = BTreeMap::new();
|
|
receipt_content
|
|
.insert(event_id.to_owned(), receipts);
|
|
|
|
let event = ReceiptEvent {
|
|
content: ReceiptEventContent(receipt_content),
|
|
room_id: room_id.clone(),
|
|
};
|
|
services()
|
|
.rooms
|
|
.edus
|
|
.read_receipt
|
|
.readreceipt_update(
|
|
&user_id, &room_id, event,
|
|
)?;
|
|
} else {
|
|
// TODO fetch missing events
|
|
debug!(
|
|
?user_updates,
|
|
"No known event ids in read receipt",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Edu::Typing(typing) => {
|
|
if typing.user_id.server_name() != sender_servername {
|
|
warn!(
|
|
user_id = %typing.user_id,
|
|
%sender_servername,
|
|
"Got typing EDU from incorrect homeserver, ignoring",
|
|
);
|
|
continue;
|
|
}
|
|
if services()
|
|
.rooms
|
|
.state_cache
|
|
.is_joined(&typing.user_id, &typing.room_id)?
|
|
{
|
|
if typing.typing {
|
|
services()
|
|
.rooms
|
|
.edus
|
|
.typing
|
|
.typing_add(
|
|
&typing.user_id,
|
|
&typing.room_id,
|
|
3000 + utils::millis_since_unix_epoch(),
|
|
)
|
|
.await?;
|
|
} else {
|
|
services()
|
|
.rooms
|
|
.edus
|
|
.typing
|
|
.typing_remove(&typing.user_id, &typing.room_id)
|
|
.await?;
|
|
}
|
|
}
|
|
}
|
|
Edu::DeviceListUpdate(DeviceListUpdateContent {
|
|
user_id,
|
|
..
|
|
}) => {
|
|
if user_id.server_name() != sender_servername {
|
|
warn!(
|
|
%user_id,
|
|
%sender_servername,
|
|
"Got device list update EDU from incorrect homeserver, \
|
|
ignoring",
|
|
);
|
|
continue;
|
|
}
|
|
services().users.mark_device_key_update(&user_id)?;
|
|
}
|
|
Edu::DirectToDevice(DirectDeviceContent {
|
|
sender,
|
|
ev_type,
|
|
message_id,
|
|
messages,
|
|
}) => {
|
|
if sender.server_name() != sender_servername {
|
|
warn!(
|
|
user_id = %sender,
|
|
%sender_servername,
|
|
"Got direct-to-device EDU from incorrect homeserver, \
|
|
ignoring",
|
|
);
|
|
continue;
|
|
}
|
|
// Check if this is a new transaction id
|
|
if services()
|
|
.transaction_ids
|
|
.existing_txnid(&sender, None, &message_id)?
|
|
.is_none()
|
|
{
|
|
for (target_user_id, map) in &messages {
|
|
for (target_device_id_maybe, event) in map {
|
|
match target_device_id_maybe {
|
|
DeviceIdOrAllDevices::DeviceId(
|
|
target_device_id,
|
|
) => services().users.add_to_device_event(
|
|
&sender,
|
|
target_user_id,
|
|
target_device_id,
|
|
&ev_type.to_string(),
|
|
event.deserialize_as().map_err(
|
|
|error| {
|
|
warn!(
|
|
%error,
|
|
object = ?event.json(),
|
|
"To-Device event is invalid",
|
|
);
|
|
Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Event is invalid",
|
|
)
|
|
},
|
|
)?,
|
|
)?,
|
|
|
|
DeviceIdOrAllDevices::AllDevices => {
|
|
for target_device_id in services()
|
|
.users
|
|
.all_device_ids(target_user_id)
|
|
{
|
|
services().users.add_to_device_event(
|
|
&sender,
|
|
target_user_id,
|
|
&target_device_id?,
|
|
&ev_type.to_string(),
|
|
event.deserialize_as().map_err(
|
|
|_| {
|
|
Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Event is invalid",
|
|
)
|
|
},
|
|
)?,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save transaction id with empty data
|
|
services().transaction_ids.add_txnid(
|
|
&sender,
|
|
None,
|
|
&message_id,
|
|
&[],
|
|
)?;
|
|
}
|
|
}
|
|
Edu::SigningKeyUpdate(SigningKeyUpdateContent {
|
|
user_id,
|
|
master_key,
|
|
self_signing_key,
|
|
}) => {
|
|
if user_id.server_name() != sender_servername {
|
|
warn!(
|
|
%user_id,
|
|
%sender_servername,
|
|
"Got signing key update from incorrect homeserver, \
|
|
ignoring",
|
|
);
|
|
continue;
|
|
}
|
|
if let Some(master_key) = master_key {
|
|
services().users.add_cross_signing_keys(
|
|
&user_id,
|
|
&master_key,
|
|
self_signing_key.as_ref(),
|
|
None,
|
|
true,
|
|
)?;
|
|
}
|
|
}
|
|
Edu::_Custom(_) | Edu::Presence(_) => {}
|
|
}
|
|
}
|
|
|
|
Ok(Ra(send_transaction_message::v1::Response {
|
|
pdus: resolved_map
|
|
.into_iter()
|
|
.map(|(e, r)| (e, r.map_err(|e| e.sanitized_error())))
|
|
.collect(),
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/event/{eventId}`
|
|
///
|
|
/// Retrieves a single event from the server.
|
|
///
|
|
/// - Only works if a user of this server is currently invited or joined the
|
|
/// room
|
|
pub(crate) async fn get_event_route(
|
|
body: Ar<get_event::v1::Request>,
|
|
) -> Result<Ra<get_event::v1::Response>> {
|
|
let sender_servername =
|
|
body.sender_servername.as_ref().expect("server is authenticated");
|
|
|
|
let event =
|
|
services().rooms.timeline.get_pdu_json(&body.event_id)?.ok_or_else(
|
|
|| {
|
|
warn!(event_id = %body.event_id, "Event not found");
|
|
Error::BadRequest(ErrorKind::NotFound, "Event not found.")
|
|
},
|
|
)?;
|
|
|
|
let room_id_str = event
|
|
.get("room_id")
|
|
.and_then(|val| val.as_str())
|
|
.ok_or_else(|| Error::bad_database("Invalid event in database"))?;
|
|
|
|
let room_id = <&RoomId>::try_from(room_id_str).map_err(|_| {
|
|
Error::bad_database("Invalid room id field in event in database")
|
|
})?;
|
|
|
|
if !services()
|
|
.rooms
|
|
.state_cache
|
|
.server_in_room(sender_servername, room_id)?
|
|
{
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::forbidden(),
|
|
"Server is not in room",
|
|
));
|
|
}
|
|
|
|
if !services().rooms.state_accessor.server_can_see_event(
|
|
sender_servername,
|
|
room_id,
|
|
&body.event_id,
|
|
)? {
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::forbidden(),
|
|
"Server is not allowed to see event.",
|
|
));
|
|
}
|
|
|
|
Ok(Ra(get_event::v1::Response {
|
|
origin: services().globals.server_name().to_owned(),
|
|
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
|
|
pdu: PduEvent::convert_to_outgoing_federation_event(event),
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/backfill/<room_id>`
|
|
///
|
|
/// Retrieves events from before the sender joined the room, if the room's
|
|
/// history visibility allows.
|
|
pub(crate) async fn get_backfill_route(
|
|
body: Ar<get_backfill::v1::Request>,
|
|
) -> Result<Ra<get_backfill::v1::Response>> {
|
|
let sender_servername =
|
|
body.sender_servername.as_ref().expect("server is authenticated");
|
|
|
|
debug!(server = %sender_servername, "Got backfill request");
|
|
|
|
if !services()
|
|
.rooms
|
|
.state_cache
|
|
.server_in_room(sender_servername, &body.room_id)?
|
|
{
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::forbidden(),
|
|
"Server is not in room.",
|
|
));
|
|
}
|
|
|
|
services()
|
|
.rooms
|
|
.event_handler
|
|
.acl_check(sender_servername, &body.room_id)?;
|
|
|
|
let until = body
|
|
.v
|
|
.iter()
|
|
.map(|eventid| services().rooms.timeline.get_pdu_count(eventid))
|
|
.filter_map(|r| r.ok().flatten())
|
|
.max()
|
|
.ok_or(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"No known eventid in v",
|
|
))?;
|
|
|
|
let limit = body.limit.min(uint!(100));
|
|
|
|
let all_events = services()
|
|
.rooms
|
|
.timeline
|
|
.pdus_until(user_id!("@doesntmatter:grapevine"), &body.room_id, until)?
|
|
.take(limit.try_into().unwrap());
|
|
|
|
let events = all_events
|
|
.filter_map(Result::ok)
|
|
.filter(|(_, e)| {
|
|
matches!(
|
|
services().rooms.state_accessor.server_can_see_event(
|
|
sender_servername,
|
|
&e.room_id,
|
|
&e.event_id,
|
|
),
|
|
Ok(true),
|
|
)
|
|
})
|
|
.map(|(_, pdu)| services().rooms.timeline.get_pdu_json(&pdu.event_id))
|
|
.filter_map(|r| r.ok().flatten())
|
|
.map(PduEvent::convert_to_outgoing_federation_event)
|
|
.collect();
|
|
|
|
Ok(Ra(get_backfill::v1::Response {
|
|
origin: services().globals.server_name().to_owned(),
|
|
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
|
|
pdus: events,
|
|
}))
|
|
}
|
|
|
|
/// # `POST /_matrix/federation/v1/get_missing_events/{roomId}`
|
|
///
|
|
/// Retrieves events that the sender is missing.
|
|
pub(crate) async fn get_missing_events_route(
|
|
body: Ar<get_missing_events::v1::Request>,
|
|
) -> Result<Ra<get_missing_events::v1::Response>> {
|
|
let sender_servername =
|
|
body.sender_servername.as_ref().expect("server is authenticated");
|
|
|
|
if !services()
|
|
.rooms
|
|
.state_cache
|
|
.server_in_room(sender_servername, &body.room_id)?
|
|
{
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::forbidden(),
|
|
"Server is not in room",
|
|
));
|
|
}
|
|
|
|
services()
|
|
.rooms
|
|
.event_handler
|
|
.acl_check(sender_servername, &body.room_id)?;
|
|
|
|
let mut queued_events = body.latest_events.clone();
|
|
let mut events = Vec::new();
|
|
|
|
let mut i = 0;
|
|
while i < queued_events.len()
|
|
&& events.len() < body.limit.try_into().unwrap_or(usize::MAX)
|
|
{
|
|
if let Some(pdu) =
|
|
services().rooms.timeline.get_pdu_json(&queued_events[i])?
|
|
{
|
|
let room_id_str =
|
|
pdu.get("room_id").and_then(|val| val.as_str()).ok_or_else(
|
|
|| Error::bad_database("Invalid event in database"),
|
|
)?;
|
|
|
|
let event_room_id =
|
|
<&RoomId>::try_from(room_id_str).map_err(|_| {
|
|
Error::bad_database(
|
|
"Invalid room id field in event in database",
|
|
)
|
|
})?;
|
|
|
|
if event_room_id != body.room_id {
|
|
warn!(
|
|
event_id = %queued_events[i],
|
|
expected_room_id = %body.room_id,
|
|
actual_room_id = %event_room_id,
|
|
"Evil event detected"
|
|
);
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Evil event detected",
|
|
));
|
|
}
|
|
|
|
if body.earliest_events.contains(&queued_events[i]) {
|
|
i += 1;
|
|
continue;
|
|
}
|
|
|
|
if !services().rooms.state_accessor.server_can_see_event(
|
|
sender_servername,
|
|
&body.room_id,
|
|
&queued_events[i],
|
|
)? {
|
|
i += 1;
|
|
continue;
|
|
}
|
|
|
|
queued_events.extend_from_slice(
|
|
&serde_json::from_value::<Vec<OwnedEventId>>(
|
|
serde_json::to_value(
|
|
pdu.get("prev_events").cloned().ok_or_else(|| {
|
|
Error::bad_database(
|
|
"Event in db has no prev_events field.",
|
|
)
|
|
})?,
|
|
)
|
|
.expect("canonical json is valid json value"),
|
|
)
|
|
.map_err(|_| {
|
|
Error::bad_database(
|
|
"Invalid prev_events content in pdu in db.",
|
|
)
|
|
})?,
|
|
);
|
|
events.push(PduEvent::convert_to_outgoing_federation_event(pdu));
|
|
}
|
|
i += 1;
|
|
}
|
|
|
|
Ok(Ra(get_missing_events::v1::Response {
|
|
events,
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/event_auth/{roomId}/{eventId}`
|
|
///
|
|
/// Retrieves the auth chain for a given event.
|
|
///
|
|
/// - This does not include the event itself
|
|
pub(crate) async fn get_event_authorization_route(
|
|
body: Ar<get_event_authorization::v1::Request>,
|
|
) -> Result<Ra<get_event_authorization::v1::Response>> {
|
|
let sender_servername =
|
|
body.sender_servername.as_ref().expect("server is authenticated");
|
|
|
|
if !services()
|
|
.rooms
|
|
.state_cache
|
|
.server_in_room(sender_servername, &body.room_id)?
|
|
{
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::forbidden(),
|
|
"Server is not in room.",
|
|
));
|
|
}
|
|
|
|
services()
|
|
.rooms
|
|
.event_handler
|
|
.acl_check(sender_servername, &body.room_id)?;
|
|
|
|
let event =
|
|
services().rooms.timeline.get_pdu_json(&body.event_id)?.ok_or_else(
|
|
|| {
|
|
warn!(event_id = %body.event_id, "Event not found");
|
|
Error::BadRequest(ErrorKind::NotFound, "Event not found.")
|
|
},
|
|
)?;
|
|
|
|
let room_id_str = event
|
|
.get("room_id")
|
|
.and_then(|val| val.as_str())
|
|
.ok_or_else(|| Error::bad_database("Invalid event in database"))?;
|
|
|
|
let room_id = <&RoomId>::try_from(room_id_str).map_err(|_| {
|
|
Error::bad_database("Invalid room id field in event in database")
|
|
})?;
|
|
|
|
let auth_chain_ids = services()
|
|
.rooms
|
|
.auth_chain
|
|
.get_auth_chain(room_id, vec![Arc::from(&*body.event_id)])
|
|
.await?;
|
|
|
|
Ok(Ra(get_event_authorization::v1::Response {
|
|
auth_chain: auth_chain_ids
|
|
.filter_map(|id| {
|
|
services().rooms.timeline.get_pdu_json(&id).ok()?
|
|
})
|
|
.map(PduEvent::convert_to_outgoing_federation_event)
|
|
.collect(),
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/state/{roomId}`
|
|
///
|
|
/// Retrieves the current state of the room.
|
|
pub(crate) async fn get_room_state_route(
|
|
body: Ar<get_room_state::v1::Request>,
|
|
) -> Result<Ra<get_room_state::v1::Response>> {
|
|
let sender_servername =
|
|
body.sender_servername.as_ref().expect("server is authenticated");
|
|
|
|
if !services()
|
|
.rooms
|
|
.state_cache
|
|
.server_in_room(sender_servername, &body.room_id)?
|
|
{
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::forbidden(),
|
|
"Server is not in room.",
|
|
));
|
|
}
|
|
|
|
services()
|
|
.rooms
|
|
.event_handler
|
|
.acl_check(sender_servername, &body.room_id)?;
|
|
|
|
let shortstatehash = services()
|
|
.rooms
|
|
.state_accessor
|
|
.pdu_shortstatehash(&body.event_id)?
|
|
.ok_or(Error::BadRequest(
|
|
ErrorKind::NotFound,
|
|
"Pdu state not found.",
|
|
))?;
|
|
|
|
let pdus = services()
|
|
.rooms
|
|
.state_accessor
|
|
.state_full_ids(shortstatehash)
|
|
.await?
|
|
.into_values()
|
|
.map(|id| {
|
|
PduEvent::convert_to_outgoing_federation_event(
|
|
services().rooms.timeline.get_pdu_json(&id).unwrap().unwrap(),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
let auth_chain_ids = services()
|
|
.rooms
|
|
.auth_chain
|
|
.get_auth_chain(&body.room_id, vec![Arc::from(&*body.event_id)])
|
|
.await?;
|
|
|
|
Ok(Ra(get_room_state::v1::Response {
|
|
auth_chain: auth_chain_ids
|
|
.filter_map(|event_id| {
|
|
if let Some(json) =
|
|
services().rooms.timeline.get_pdu_json(&event_id).ok()?
|
|
{
|
|
Some(PduEvent::convert_to_outgoing_federation_event(json))
|
|
} else {
|
|
error!(%event_id, "Could not find event JSON for event");
|
|
None
|
|
}
|
|
})
|
|
.collect(),
|
|
pdus,
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/state_ids/{roomId}`
|
|
///
|
|
/// Retrieves the current state of the room.
|
|
pub(crate) async fn get_room_state_ids_route(
|
|
body: Ar<get_room_state_ids::v1::Request>,
|
|
) -> Result<Ra<get_room_state_ids::v1::Response>> {
|
|
let sender_servername =
|
|
body.sender_servername.as_ref().expect("server is authenticated");
|
|
|
|
if !services()
|
|
.rooms
|
|
.state_cache
|
|
.server_in_room(sender_servername, &body.room_id)?
|
|
{
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::forbidden(),
|
|
"Server is not in room.",
|
|
));
|
|
}
|
|
|
|
services()
|
|
.rooms
|
|
.event_handler
|
|
.acl_check(sender_servername, &body.room_id)?;
|
|
|
|
let shortstatehash = services()
|
|
.rooms
|
|
.state_accessor
|
|
.pdu_shortstatehash(&body.event_id)?
|
|
.ok_or(Error::BadRequest(
|
|
ErrorKind::NotFound,
|
|
"Pdu state not found.",
|
|
))?;
|
|
|
|
let pdu_ids = services()
|
|
.rooms
|
|
.state_accessor
|
|
.state_full_ids(shortstatehash)
|
|
.await?
|
|
.into_values()
|
|
.map(|id| (*id).to_owned())
|
|
.collect();
|
|
|
|
let auth_chain_ids = services()
|
|
.rooms
|
|
.auth_chain
|
|
.get_auth_chain(&body.room_id, vec![Arc::from(&*body.event_id)])
|
|
.await?;
|
|
|
|
Ok(Ra(get_room_state_ids::v1::Response {
|
|
auth_chain_ids: auth_chain_ids.map(|id| (*id).to_owned()).collect(),
|
|
pdu_ids,
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/make_join/{roomId}/{userId}`
|
|
///
|
|
/// Creates a join template.
|
|
pub(crate) async fn create_join_event_template_route(
|
|
body: Ar<prepare_join_event::v1::Request>,
|
|
) -> Result<Ra<prepare_join_event::v1::Response>> {
|
|
if !services().rooms.metadata.exists(&body.room_id)? {
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::NotFound,
|
|
"Room is unknown to this server.",
|
|
));
|
|
}
|
|
|
|
let sender_servername =
|
|
body.sender_servername.as_ref().expect("server is authenticated");
|
|
|
|
services()
|
|
.rooms
|
|
.event_handler
|
|
.acl_check(sender_servername, &body.room_id)?;
|
|
|
|
let room_token = services()
|
|
.globals
|
|
.roomid_mutex_state
|
|
.lock_key(body.room_id.clone())
|
|
.await;
|
|
|
|
// TODO: Grapevine does not implement restricted join rules yet, we always
|
|
// reject
|
|
let join_rules_event = services().rooms.state_accessor.room_state_get(
|
|
&body.room_id,
|
|
&StateEventType::RoomJoinRules,
|
|
"",
|
|
)?;
|
|
|
|
let join_rules_event_content: Option<RoomJoinRulesEventContent> =
|
|
join_rules_event
|
|
.as_ref()
|
|
.map(|join_rules_event| {
|
|
serde_json::from_str(join_rules_event.content.get()).map_err(
|
|
|error| {
|
|
warn!(%error, "Invalid join rules event");
|
|
Error::bad_database("Invalid join rules event in db.")
|
|
},
|
|
)
|
|
})
|
|
.transpose()?;
|
|
|
|
if let Some(join_rules_event_content) = join_rules_event_content {
|
|
if matches!(
|
|
join_rules_event_content.join_rule,
|
|
JoinRule::Restricted { .. } | JoinRule::KnockRestricted { .. }
|
|
) {
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::UnableToAuthorizeJoin,
|
|
"Grapevine does not support restricted rooms yet.",
|
|
));
|
|
}
|
|
}
|
|
|
|
let room_version_id = services()
|
|
.rooms
|
|
.state
|
|
.get_create_content::<ExtractVersion>(&body.room_id)?;
|
|
if !body.ver.contains(&room_version_id) {
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::IncompatibleRoomVersion {
|
|
room_version: room_version_id,
|
|
},
|
|
"Room version not supported.",
|
|
));
|
|
}
|
|
|
|
let content = to_raw_value(&RoomMemberEventContent {
|
|
avatar_url: None,
|
|
blurhash: None,
|
|
displayname: None,
|
|
is_direct: None,
|
|
membership: MembershipState::Join,
|
|
third_party_invite: None,
|
|
reason: None,
|
|
join_authorized_via_users_server: None,
|
|
})
|
|
.expect("member event is valid value");
|
|
|
|
let (_pdu, mut pdu_json) =
|
|
services().rooms.timeline.create_hash_and_sign_event(
|
|
PduBuilder {
|
|
event_type: TimelineEventType::RoomMember,
|
|
content,
|
|
unsigned: None,
|
|
state_key: Some(body.user_id.to_string()),
|
|
redacts: None,
|
|
},
|
|
&body.user_id,
|
|
&room_token,
|
|
)?;
|
|
|
|
drop(room_token);
|
|
|
|
pdu_json.remove("event_id");
|
|
|
|
Ok(Ra(prepare_join_event::v1::Response {
|
|
room_version: Some(room_version_id),
|
|
event: to_raw_value(&pdu_json)
|
|
.expect("CanonicalJson can be serialized to JSON"),
|
|
}))
|
|
}
|
|
|
|
#[allow(clippy::too_many_lines)]
|
|
async fn create_join_event(
|
|
sender_servername: &ServerName,
|
|
room_id: &RoomId,
|
|
pdu: &RawJsonValue,
|
|
) -> Result<create_join_event::v2::RoomState> {
|
|
if !services().rooms.metadata.exists(room_id)? {
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::NotFound,
|
|
"Room is unknown to this server.",
|
|
));
|
|
}
|
|
|
|
services().rooms.event_handler.acl_check(sender_servername, room_id)?;
|
|
|
|
// TODO: Grapevine does not implement restricted join rules yet, we always
|
|
// reject
|
|
let join_rules_event = services().rooms.state_accessor.room_state_get(
|
|
room_id,
|
|
&StateEventType::RoomJoinRules,
|
|
"",
|
|
)?;
|
|
|
|
let join_rules_event_content: Option<RoomJoinRulesEventContent> =
|
|
join_rules_event
|
|
.as_ref()
|
|
.map(|join_rules_event| {
|
|
serde_json::from_str(join_rules_event.content.get()).map_err(
|
|
|error| {
|
|
warn!(%error, "Invalid join rules event");
|
|
Error::bad_database("Invalid join rules event in db.")
|
|
},
|
|
)
|
|
})
|
|
.transpose()?;
|
|
|
|
if let Some(join_rules_event_content) = join_rules_event_content {
|
|
if matches!(
|
|
join_rules_event_content.join_rule,
|
|
JoinRule::Restricted { .. } | JoinRule::KnockRestricted { .. }
|
|
) {
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::UnableToAuthorizeJoin,
|
|
"Grapevine does not support restricted rooms yet.",
|
|
));
|
|
}
|
|
}
|
|
|
|
// We need to return the state prior to joining, let's keep a reference to
|
|
// that here
|
|
let shortstatehash =
|
|
services().rooms.state.get_room_shortstatehash(room_id)?.ok_or(
|
|
Error::BadRequest(ErrorKind::NotFound, "Pdu state not found."),
|
|
)?;
|
|
|
|
let pub_key_map = RwLock::new(BTreeMap::new());
|
|
|
|
// We do not add the event_id field to the pdu here because of signature and
|
|
// hashes checks
|
|
let room_version_id =
|
|
services().rooms.state.get_create_content::<ExtractVersion>(room_id)?;
|
|
let Some(room_version_rules) = room_version_id.rules() else {
|
|
return Err(Error::UnsupportedRoomVersion(room_version_id));
|
|
};
|
|
|
|
let Ok((event_id, value)) =
|
|
gen_event_id_canonical_json(pdu, &room_version_rules)
|
|
else {
|
|
// Event could not be converted to canonical json
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Could not convert event to canonical json.",
|
|
));
|
|
};
|
|
|
|
let origin: OwnedServerName = serde_json::from_value(
|
|
serde_json::to_value(value.get("origin").ok_or(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Event needs an origin field.",
|
|
))?)
|
|
.expect("CanonicalJson is valid json value"),
|
|
)
|
|
.map_err(|_| {
|
|
Error::BadRequest(ErrorKind::InvalidParam, "Origin field is invalid.")
|
|
})?;
|
|
|
|
let federation_token = services()
|
|
.globals
|
|
.roomid_mutex_federation
|
|
.lock_key(room_id.to_owned())
|
|
.await;
|
|
let pdu_id = services()
|
|
.rooms
|
|
.event_handler
|
|
.handle_incoming_pdu(
|
|
&origin,
|
|
&event_id,
|
|
room_id,
|
|
value,
|
|
true,
|
|
&pub_key_map,
|
|
)
|
|
.await?
|
|
.ok_or(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Could not accept incoming PDU as timeline event.",
|
|
))?;
|
|
drop(federation_token);
|
|
|
|
let state_ids =
|
|
services().rooms.state_accessor.state_full_ids(shortstatehash).await?;
|
|
let auth_chain_ids = services()
|
|
.rooms
|
|
.auth_chain
|
|
.get_auth_chain(room_id, state_ids.values().cloned().collect())
|
|
.await?;
|
|
|
|
let servers = services()
|
|
.rooms
|
|
.state_cache
|
|
.room_servers(room_id)
|
|
.filter_map(Result::ok)
|
|
.filter(|server| &**server != services().globals.server_name());
|
|
|
|
services().sending.send_pdu(servers, &pdu_id)?;
|
|
|
|
Ok(create_join_event::v2::RoomState {
|
|
auth_chain: auth_chain_ids
|
|
.filter_map(|id| {
|
|
services().rooms.timeline.get_pdu_json(&id).ok().flatten()
|
|
})
|
|
.map(PduEvent::convert_to_outgoing_federation_event)
|
|
.collect(),
|
|
state: state_ids
|
|
.iter()
|
|
.filter_map(|(_, id)| {
|
|
services().rooms.timeline.get_pdu_json(id).ok().flatten()
|
|
})
|
|
.map(PduEvent::convert_to_outgoing_federation_event)
|
|
.collect(),
|
|
// TODO: handle restricted joins
|
|
event: None,
|
|
members_omitted: false,
|
|
servers_in_room: None,
|
|
})
|
|
}
|
|
|
|
/// # `PUT /_matrix/federation/v1/send_join/{roomId}/{eventId}`
|
|
///
|
|
/// Submits a signed join event.
|
|
#[allow(deprecated)]
|
|
pub(crate) async fn create_join_event_v1_route(
|
|
body: Ar<create_join_event::v1::Request>,
|
|
) -> Result<Ra<create_join_event::v1::Response>> {
|
|
let sender_servername =
|
|
body.sender_servername.as_ref().expect("server is authenticated");
|
|
|
|
let create_join_event::v2::RoomState {
|
|
auth_chain,
|
|
state,
|
|
event,
|
|
..
|
|
} = create_join_event(sender_servername, &body.room_id, &body.pdu).await?;
|
|
let room_state = create_join_event::v1::RoomState {
|
|
auth_chain,
|
|
state,
|
|
event,
|
|
};
|
|
|
|
Ok(Ra(create_join_event::v1::Response {
|
|
room_state,
|
|
}))
|
|
}
|
|
|
|
/// # `PUT /_matrix/federation/v2/send_join/{roomId}/{eventId}`
|
|
///
|
|
/// Submits a signed join event.
|
|
pub(crate) async fn create_join_event_v2_route(
|
|
body: Ar<create_join_event::v2::Request>,
|
|
) -> Result<Ra<create_join_event::v2::Response>> {
|
|
let sender_servername =
|
|
body.sender_servername.as_ref().expect("server is authenticated");
|
|
|
|
let room_state =
|
|
create_join_event(sender_servername, &body.room_id, &body.pdu).await?;
|
|
|
|
Ok(Ra(create_join_event::v2::Response {
|
|
room_state,
|
|
}))
|
|
}
|
|
|
|
/// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}`
|
|
///
|
|
/// Invites a remote user to a room.
|
|
#[allow(clippy::too_many_lines)]
|
|
pub(crate) async fn create_invite_route(
|
|
body: Ar<create_invite::v2::Request>,
|
|
) -> Result<Ra<create_invite::v2::Response>> {
|
|
let sender_servername =
|
|
body.sender_servername.as_ref().expect("server is authenticated");
|
|
|
|
services()
|
|
.rooms
|
|
.event_handler
|
|
.acl_check(sender_servername, &body.room_id)?;
|
|
|
|
if !services()
|
|
.globals
|
|
.supported_room_versions()
|
|
.contains(&body.room_version)
|
|
{
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::IncompatibleRoomVersion {
|
|
room_version: body.room_version.clone(),
|
|
},
|
|
"Server does not support this room version.",
|
|
));
|
|
}
|
|
let room_version_rules = body
|
|
.room_version
|
|
.rules()
|
|
.expect("ruma should support all room versions we advertise");
|
|
|
|
let mut signed_event =
|
|
utils::to_canonical_object(&body.event).map_err(|_| {
|
|
Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Invite event is invalid.",
|
|
)
|
|
})?;
|
|
|
|
ruma::signatures::hash_and_sign_event(
|
|
services().globals.server_name().as_str(),
|
|
services().globals.keypair(),
|
|
&mut signed_event,
|
|
&room_version_rules.redaction,
|
|
)
|
|
.map_err(|_| {
|
|
Error::BadRequest(ErrorKind::InvalidParam, "Failed to sign event.")
|
|
})?;
|
|
|
|
// Generate event id
|
|
let event_id = EventId::parse(format!(
|
|
"${}",
|
|
ruma::signatures::reference_hash(&signed_event, &room_version_rules)
|
|
.expect("ruma can calculate reference hashes")
|
|
))
|
|
.expect("ruma's reference hashes are valid event ids");
|
|
|
|
// Add event_id back
|
|
signed_event.insert(
|
|
"event_id".to_owned(),
|
|
CanonicalJsonValue::String(event_id.to_string()),
|
|
);
|
|
|
|
let sender: OwnedUserId = serde_json::from_value(
|
|
signed_event
|
|
.get("sender")
|
|
.ok_or(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Event had no sender field.",
|
|
))?
|
|
.clone()
|
|
.into(),
|
|
)
|
|
.map_err(|_| {
|
|
Error::BadRequest(ErrorKind::InvalidParam, "sender is not a user id.")
|
|
})?;
|
|
|
|
let invited_user: Box<_> = serde_json::from_value(
|
|
signed_event
|
|
.get("state_key")
|
|
.ok_or(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Event had no state_key field.",
|
|
))?
|
|
.clone()
|
|
.into(),
|
|
)
|
|
.map_err(|_| {
|
|
Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"state_key is not a user id.",
|
|
)
|
|
})?;
|
|
|
|
let mut invite_state = body.invite_room_state.clone();
|
|
|
|
let mut event: JsonObject = serde_json::from_str(body.event.get())
|
|
.map_err(|_| {
|
|
Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Invalid invite event bytes.",
|
|
)
|
|
})?;
|
|
|
|
event.insert("event_id".to_owned(), "$dummy".into());
|
|
|
|
let pdu: PduEvent =
|
|
serde_json::from_value(event.into()).map_err(|error| {
|
|
warn!(%error, "Invalid invite event");
|
|
Error::BadRequest(ErrorKind::InvalidParam, "Invalid invite event.")
|
|
})?;
|
|
|
|
invite_state.push(pdu.to_stripped_state_event());
|
|
|
|
// If we are active in the room, the remote server will notify us about the
|
|
// invite via m.room.member through /send. If we are not in the room, we
|
|
// need to manually record the invited state for clients' /sync through
|
|
// update_membership(), and send the invite pseudo-PDU to the affected
|
|
// appservices.
|
|
if !services()
|
|
.rooms
|
|
.state_cache
|
|
.server_in_room(services().globals.server_name(), &body.room_id)?
|
|
{
|
|
services().rooms.state_cache.update_membership(
|
|
&body.room_id,
|
|
&invited_user,
|
|
&MembershipState::Invite,
|
|
&sender,
|
|
Some(invite_state),
|
|
true,
|
|
)?;
|
|
|
|
for appservice in services().appservice.read().await.values() {
|
|
if appservice.is_user_match(&invited_user) {
|
|
appservice_server::send_request(
|
|
appservice.registration.clone(),
|
|
ruma::api::appservice::event::push_events::v1::Request {
|
|
events: vec![pdu.to_room_event()],
|
|
txn_id:
|
|
base64::engine::general_purpose::URL_SAFE_NO_PAD
|
|
.encode(utils::calculate_hash([pdu
|
|
.event_id()
|
|
.as_bytes()]))
|
|
.into(),
|
|
ephemeral: Vec::new(),
|
|
},
|
|
)
|
|
.await?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Ra(create_invite::v2::Response {
|
|
event: PduEvent::convert_to_outgoing_federation_event(signed_event),
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/user/devices/{userId}`
|
|
///
|
|
/// Gets information on all devices of the user.
|
|
pub(crate) async fn get_devices_route(
|
|
body: Ar<get_devices::v1::Request>,
|
|
) -> Result<Ra<get_devices::v1::Response>> {
|
|
if body.user_id.server_name() != services().globals.server_name() {
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Tried to access user from other server.",
|
|
));
|
|
}
|
|
|
|
let sender_servername =
|
|
body.sender_servername.as_ref().expect("server is authenticated");
|
|
|
|
Ok(Ra(get_devices::v1::Response {
|
|
user_id: body.user_id.clone(),
|
|
stream_id: services()
|
|
.users
|
|
.get_devicelist_version(&body.user_id)?
|
|
.unwrap_or(0)
|
|
.try_into()
|
|
.expect("version will not grow that large"),
|
|
devices: services()
|
|
.users
|
|
.all_devices_metadata(&body.user_id)
|
|
.filter_map(Result::ok)
|
|
.filter_map(|metadata| {
|
|
Some(UserDevice {
|
|
keys: services()
|
|
.users
|
|
.get_device_keys(&body.user_id, &metadata.device_id)
|
|
.ok()??,
|
|
device_id: metadata.device_id,
|
|
device_display_name: metadata.display_name,
|
|
})
|
|
})
|
|
.collect(),
|
|
master_key: services().users.get_master_key(
|
|
None,
|
|
&body.user_id,
|
|
&|u| u.server_name() == sender_servername,
|
|
)?,
|
|
self_signing_key: services().users.get_self_signing_key(
|
|
None,
|
|
&body.user_id,
|
|
&|u| u.server_name() == sender_servername,
|
|
)?,
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/query/directory`
|
|
///
|
|
/// Resolve a room alias to a room id.
|
|
pub(crate) async fn get_room_information_route(
|
|
body: Ar<get_room_information::v1::Request>,
|
|
) -> Result<Ra<get_room_information::v1::Response>> {
|
|
let room_id =
|
|
services().rooms.alias.resolve_local_alias(&body.room_alias)?.ok_or(
|
|
Error::BadRequest(ErrorKind::NotFound, "Room alias not found."),
|
|
)?;
|
|
|
|
Ok(Ra(get_room_information::v1::Response {
|
|
room_id,
|
|
servers: vec![services().globals.server_name().to_owned()],
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/query/profile`
|
|
///
|
|
/// Gets information on a profile.
|
|
pub(crate) async fn get_profile_information_route(
|
|
body: Ar<get_profile_information::v1::Request>,
|
|
) -> Result<Ra<get_profile_information::v1::Response>> {
|
|
if body.user_id.server_name() != services().globals.server_name() {
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Tried to access user from other server.",
|
|
));
|
|
}
|
|
|
|
let mut displayname = None;
|
|
let mut avatar_url = None;
|
|
let mut blurhash = None;
|
|
|
|
match &body.field {
|
|
Some(ProfileField::DisplayName) => {
|
|
displayname = services().users.displayname(&body.user_id)?;
|
|
}
|
|
Some(ProfileField::AvatarUrl) => {
|
|
avatar_url = services().users.avatar_url(&body.user_id)?;
|
|
blurhash = services().users.blurhash(&body.user_id)?;
|
|
}
|
|
// TODO: what to do with custom
|
|
Some(_) => {}
|
|
None => {
|
|
displayname = services().users.displayname(&body.user_id)?;
|
|
avatar_url = services().users.avatar_url(&body.user_id)?;
|
|
blurhash = services().users.blurhash(&body.user_id)?;
|
|
}
|
|
}
|
|
|
|
Ok(Ra(get_profile_information::v1::Response {
|
|
displayname,
|
|
avatar_url,
|
|
blurhash,
|
|
}))
|
|
}
|
|
|
|
/// # `POST /_matrix/federation/v1/user/keys/query`
|
|
///
|
|
/// Gets devices and identity keys for the given users.
|
|
pub(crate) async fn get_keys_route(
|
|
body: Ar<get_keys::v1::Request>,
|
|
) -> Result<Ra<get_keys::v1::Response>> {
|
|
if body
|
|
.device_keys
|
|
.iter()
|
|
.any(|(u, _)| u.server_name() != services().globals.server_name())
|
|
{
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Tried to access user from other server.",
|
|
));
|
|
}
|
|
|
|
let result = get_keys_helper(None, &body.device_keys, |u| {
|
|
Some(u.server_name()) == body.sender_servername.as_deref()
|
|
})
|
|
.await?;
|
|
|
|
Ok(Ra(get_keys::v1::Response {
|
|
device_keys: result.device_keys,
|
|
master_keys: result.master_keys,
|
|
self_signing_keys: result.self_signing_keys,
|
|
}))
|
|
}
|
|
|
|
/// # `POST /_matrix/federation/v1/user/keys/claim`
|
|
///
|
|
/// Claims one-time keys.
|
|
pub(crate) async fn claim_keys_route(
|
|
body: Ar<claim_keys::v1::Request>,
|
|
) -> Result<Ra<claim_keys::v1::Response>> {
|
|
if body
|
|
.one_time_keys
|
|
.iter()
|
|
.any(|(u, _)| u.server_name() != services().globals.server_name())
|
|
{
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::InvalidParam,
|
|
"Tried to access user from other server.",
|
|
));
|
|
}
|
|
|
|
let result = claim_keys_helper(&body.one_time_keys).await?;
|
|
|
|
Ok(Ra(claim_keys::v1::Response {
|
|
one_time_keys: result.one_time_keys,
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/media/download/{mediaId}`
|
|
///
|
|
/// Downloads media owned by a remote homeserver.
|
|
pub(crate) async fn media_download_route(
|
|
body: Ar<authenticated_media::get_content::v1::Request>,
|
|
) -> Result<Ra<authenticated_media::get_content::v1::Response>> {
|
|
let mxc = MxcData::new(services().globals.server_name(), &body.media_id)?;
|
|
let Some((
|
|
crate::service::media::FileMeta {
|
|
content_disposition,
|
|
content_type,
|
|
},
|
|
file,
|
|
)) = services().media.get(mxc.into()).await?
|
|
else {
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::NotYetUploaded,
|
|
"Media not found",
|
|
));
|
|
};
|
|
|
|
let content_disposition = content_disposition.and_then(|s| {
|
|
s.parse().inspect_err(
|
|
|error| warn!(%error, "Invalid Content-Disposition in database"),
|
|
)
|
|
.ok()
|
|
});
|
|
|
|
Ok(Ra(authenticated_media::get_content::v1::Response {
|
|
metadata: authenticated_media::ContentMetadata {},
|
|
content: authenticated_media::FileOrLocation::File(
|
|
authenticated_media::Content {
|
|
file,
|
|
content_type,
|
|
content_disposition,
|
|
},
|
|
),
|
|
}))
|
|
}
|
|
|
|
/// # `GET /_matrix/federation/v1/media/thumbnail/{mediaId}`
|
|
///
|
|
/// Downloads a thumbnail from a remote homeserver.
|
|
pub(crate) async fn media_thumbnail_route(
|
|
body: Ar<authenticated_media::get_content_thumbnail::v1::Request>,
|
|
) -> Result<Ra<authenticated_media::get_content_thumbnail::v1::Response>> {
|
|
let mxc = MxcData::new(services().globals.server_name(), &body.media_id)?;
|
|
let width = body.width.try_into().map_err(|_| {
|
|
Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid.")
|
|
})?;
|
|
let height = body.height.try_into().map_err(|_| {
|
|
Error::BadRequest(ErrorKind::InvalidParam, "Height is invalid.")
|
|
})?;
|
|
|
|
let Some((
|
|
crate::service::media::FileMeta {
|
|
content_type,
|
|
..
|
|
},
|
|
file,
|
|
)) = services().media.get_thumbnail(mxc.into(), width, height).await?
|
|
else {
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::NotYetUploaded,
|
|
"Media not found",
|
|
));
|
|
};
|
|
|
|
Ok(Ra(authenticated_media::get_content_thumbnail::v1::Response {
|
|
metadata: authenticated_media::ContentMetadata {},
|
|
content: authenticated_media::FileOrLocation::File(
|
|
authenticated_media::Content {
|
|
file,
|
|
content_type,
|
|
content_disposition: None,
|
|
},
|
|
),
|
|
}))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{add_port_to_hostname, get_ip_with_port, FedDest};
|
|
|
|
#[test]
|
|
fn ips_get_default_ports() {
|
|
assert_eq!(
|
|
get_ip_with_port("1.1.1.1"),
|
|
Some(FedDest::Literal("1.1.1.1:8448".parse().unwrap()))
|
|
);
|
|
assert_eq!(
|
|
get_ip_with_port("dead:beef::"),
|
|
Some(FedDest::Literal("[dead:beef::]:8448".parse().unwrap()))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ips_keep_custom_ports() {
|
|
assert_eq!(
|
|
get_ip_with_port("1.1.1.1:1234"),
|
|
Some(FedDest::Literal("1.1.1.1:1234".parse().unwrap()))
|
|
);
|
|
assert_eq!(
|
|
get_ip_with_port("[dead::beef]:8933"),
|
|
Some(FedDest::Literal("[dead::beef]:8933".parse().unwrap()))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hostnames_get_default_ports() {
|
|
assert_eq!(
|
|
add_port_to_hostname("example.com"),
|
|
FedDest::Named(String::from("example.com"), String::from(":8448"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hostnames_keep_custom_ports() {
|
|
assert_eq!(
|
|
add_port_to_hostname("example.com:1337"),
|
|
FedDest::Named(String::from("example.com"), String::from(":1337"))
|
|
);
|
|
}
|
|
}
|