State-reset and security mitigations.
Upgrade Ruma to present. The following are intentionally benign for activation in a later commit: - Hydra backports not default. - Room version 12 not default. - Room version 12 not listed as stable. Do not enable them manually or you can brick your database. Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
@@ -11,10 +11,7 @@ use ruma::{
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
},
|
||||
events::{
|
||||
StateEventType,
|
||||
room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
events::{StateEventType, room::power_levels::RoomPowerLevelsEventContent},
|
||||
};
|
||||
use tuwunel_core::{
|
||||
Err, Error, Result, err, info,
|
||||
@@ -60,10 +57,7 @@ pub(crate) async fn change_password_route(
|
||||
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
@@ -189,10 +183,7 @@ pub(crate) async fn deactivate_route(
|
||||
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
@@ -331,21 +322,18 @@ pub async fn full_user_deactivate(
|
||||
let room_power_levels = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<RoomPowerLevelsEventContent>(
|
||||
room_id,
|
||||
&StateEventType::RoomPowerLevels,
|
||||
"",
|
||||
)
|
||||
.get_power_levels(room_id)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let user_can_demote_self =
|
||||
room_power_levels
|
||||
.as_ref()
|
||||
.is_some_and(|power_levels_content| {
|
||||
RoomPowerLevels::from(power_levels_content.clone())
|
||||
.user_can_change_user_power_level(user_id, user_id)
|
||||
}) || services
|
||||
let user_can_change_self = room_power_levels
|
||||
.as_ref()
|
||||
.is_some_and(|power_levels| {
|
||||
power_levels.user_can_change_user_power_level(user_id, user_id)
|
||||
});
|
||||
|
||||
let user_can_demote_self = user_can_change_self
|
||||
|| services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||
@@ -353,7 +341,11 @@ pub async fn full_user_deactivate(
|
||||
.is_ok_and(|event| event.sender() == user_id);
|
||||
|
||||
if user_can_demote_self {
|
||||
let mut power_levels_content = room_power_levels.unwrap_or_default();
|
||||
let mut power_levels_content: RoomPowerLevelsEventContent = room_power_levels
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
power_levels_content.users.remove(user_id);
|
||||
|
||||
// ignore errors so deactivation doesn't fail
|
||||
|
||||
@@ -3,9 +3,12 @@ use std::collections::BTreeMap;
|
||||
use axum::extract::State;
|
||||
use ruma::{
|
||||
RoomVersionId,
|
||||
api::client::discovery::get_capabilities::{
|
||||
self, Capabilities, GetLoginTokenCapability, RoomVersionStability,
|
||||
RoomVersionsCapability, ThirdPartyIdChangesCapability,
|
||||
api::client::discovery::{
|
||||
get_capabilities,
|
||||
get_capabilities::v3::{
|
||||
Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability,
|
||||
ThirdPartyIdChangesCapability,
|
||||
},
|
||||
},
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -145,10 +145,7 @@ pub(crate) async fn delete_device_route(
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
@@ -224,10 +221,7 @@ pub(crate) async fn delete_devices_route(
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
|
||||
@@ -16,18 +16,15 @@ use ruma::{
|
||||
},
|
||||
federation,
|
||||
},
|
||||
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
|
||||
directory::{Filter, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
},
|
||||
uint,
|
||||
};
|
||||
use tuwunel_core::{
|
||||
Err, Result, err, info,
|
||||
Err, Result, err, info, is_true,
|
||||
matrix::Event,
|
||||
utils::{
|
||||
TryFutureExtExt,
|
||||
@@ -51,7 +48,7 @@ pub(crate) async fn get_public_rooms_filtered_route(
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_public_rooms_filtered::v3::Request>,
|
||||
) -> Result<get_public_rooms_filtered::v3::Response> {
|
||||
check_banned(&services, body.server.as_deref())?;
|
||||
check_server_banned(&services, body.server.as_deref())?;
|
||||
|
||||
let response = get_public_rooms_filtered_helper(
|
||||
&services,
|
||||
@@ -80,7 +77,7 @@ pub(crate) async fn get_public_rooms_route(
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_public_rooms::v3::Request>,
|
||||
) -> Result<get_public_rooms::v3::Response> {
|
||||
check_banned(&services, body.server.as_deref())?;
|
||||
check_server_banned(&services, body.server.as_deref())?;
|
||||
|
||||
let response = get_public_rooms_filtered_helper(
|
||||
&services,
|
||||
@@ -393,15 +390,11 @@ async fn user_can_publish_room(
|
||||
match services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
|
||||
.get_power_levels(room_id)
|
||||
.await
|
||||
{
|
||||
| Ok(event) => serde_json::from_str(event.content().get())
|
||||
.map_err(|_| err!(Database("Invalid event content for m.room.power_levels")))
|
||||
.map(|content: RoomPowerLevelsEventContent| {
|
||||
RoomPowerLevels::from(content)
|
||||
.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)
|
||||
}),
|
||||
| Ok(power_levels) =>
|
||||
Ok(power_levels.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)),
|
||||
| _ => {
|
||||
match services
|
||||
.rooms
|
||||
@@ -453,7 +446,7 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
|
||||
.state_accessor
|
||||
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
|
||||
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
|
||||
| JoinRule::Public => PublicRoomJoinRule::Public,
|
||||
| JoinRule::Public => "public".into(),
|
||||
| JoinRule::Knock => "knock".into(),
|
||||
| JoinRule::KnockRestricted(_) => "knock_restricted".into(),
|
||||
| _ => "invite".into(),
|
||||
@@ -497,24 +490,25 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
|
||||
}
|
||||
}
|
||||
|
||||
fn check_banned(services: &Services, server: Option<&ServerName>) -> Result {
|
||||
fn check_server_banned(services: &Services, server: Option<&ServerName>) -> Result {
|
||||
let Some(server) = server else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let forbidden_remote_directory = services
|
||||
.config
|
||||
.forbidden_remote_room_directory_server_names
|
||||
.is_match(server.host());
|
||||
let conditions = [
|
||||
services
|
||||
.config
|
||||
.forbidden_remote_room_directory_server_names
|
||||
.is_match(server.host()),
|
||||
services
|
||||
.config
|
||||
.forbidden_remote_server_names
|
||||
.is_match(server.host()),
|
||||
];
|
||||
|
||||
let forbidden_remote_server = services
|
||||
.config
|
||||
.forbidden_remote_server_names
|
||||
.is_match(server.host());
|
||||
|
||||
if forbidden_remote_directory || forbidden_remote_server {
|
||||
Err!(Request(Forbidden("Server is banned on this homeserver.")))
|
||||
} else {
|
||||
Ok(())
|
||||
if conditions.iter().any(is_true!()) {
|
||||
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use axum::extract::State;
|
||||
use futures::{StreamExt, stream::FuturesUnordered};
|
||||
use ruma::{
|
||||
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
|
||||
CanonicalJsonObject, CanonicalJsonValue, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId,
|
||||
UserId,
|
||||
api::{
|
||||
client::{
|
||||
error::ErrorKind,
|
||||
@@ -162,10 +163,7 @@ pub(crate) async fn upload_signing_keys_route(
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match check_for_new_keys(
|
||||
@@ -599,18 +597,19 @@ fn add_unsigned_device_display_name(
|
||||
include_display_names: bool,
|
||||
) -> Result {
|
||||
if let Some(display_name) = metadata.display_name {
|
||||
let mut object = keys.deserialize_as::<serde_json::Map<String, serde_json::Value>>()?;
|
||||
let mut object = keys.deserialize_as_unchecked::<CanonicalJsonObject>()?;
|
||||
|
||||
let unsigned = object
|
||||
.entry("unsigned")
|
||||
.or_insert_with(|| json!({}));
|
||||
if let serde_json::Value::Object(unsigned_object) = unsigned {
|
||||
.entry("unsigned".into())
|
||||
.or_insert_with(CanonicalJsonValue::default);
|
||||
|
||||
if let CanonicalJsonValue::Object(unsigned_object) = unsigned {
|
||||
if include_display_names {
|
||||
unsigned_object.insert("device_display_name".to_owned(), display_name.into());
|
||||
} else {
|
||||
unsigned_object.insert(
|
||||
"device_display_name".to_owned(),
|
||||
Some(metadata.device_id.as_str().to_owned()).into(),
|
||||
CanonicalJsonValue::String(metadata.device_id.as_str().to_owned()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ use ruma::{
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
};
|
||||
use tuwunel_core::{
|
||||
Err, Result, debug_error, err, info,
|
||||
Err, Result, err,
|
||||
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
|
||||
warn,
|
||||
};
|
||||
use tuwunel_service::Services;
|
||||
|
||||
@@ -27,8 +28,8 @@ pub(crate) async fn invite_user_route(
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
|
||||
debug_error!(
|
||||
"User {sender_user} is not an admin and attempted to send an invite to room {}",
|
||||
warn!(
|
||||
"{sender_user} is not an admin and attempted to send an invite to {}",
|
||||
&body.room_id
|
||||
);
|
||||
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
|
||||
@@ -104,10 +105,7 @@ pub(crate) async fn invite_helper(
|
||||
is_direct: bool,
|
||||
) -> Result {
|
||||
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
|
||||
info!(
|
||||
"User {sender_user} is not an admin and attempted to send an invite to room \
|
||||
{room_id}"
|
||||
);
|
||||
warn!("{sender_user} is not an admin and attempted to send an invite to {room_id}");
|
||||
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
|
||||
}
|
||||
|
||||
@@ -156,7 +154,10 @@ pub(crate) async fn invite_helper(
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(pdu_json.clone())
|
||||
.await,
|
||||
invite_room_state,
|
||||
invite_room_state: invite_room_state
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
via: services
|
||||
.rooms
|
||||
.state_cache
|
||||
|
||||
@@ -25,10 +25,9 @@ use ruma::{
|
||||
use tuwunel_core::{
|
||||
Err, Result, debug, debug_info, debug_warn, err, error, info,
|
||||
matrix::{
|
||||
StateKey,
|
||||
event::{gen_event_id, gen_event_id_canonical_json},
|
||||
pdu::{PduBuilder, PduEvent},
|
||||
state_res,
|
||||
room_version, state_res,
|
||||
},
|
||||
result::FlatOk,
|
||||
trace,
|
||||
@@ -551,7 +550,13 @@ async fn join_room_by_id_helper_remote(
|
||||
})
|
||||
.ready_filter_map(Result::ok)
|
||||
.fold(HashMap::new(), async |mut state, (event_id, value)| {
|
||||
let pdu = match PduEvent::from_id_val(&event_id, value.clone()) {
|
||||
let pdu = if value["type"] == "m.room.create" {
|
||||
PduEvent::from_rid_val(room_id, &event_id, value.clone())
|
||||
} else {
|
||||
PduEvent::from_id_val(&event_id, value.clone())
|
||||
};
|
||||
|
||||
let pdu = match pdu {
|
||||
| Ok(pdu) => pdu,
|
||||
| Err(e) => {
|
||||
debug_warn!("Invalid PDU in send_join response: {e:?}: {value:#?}");
|
||||
@@ -604,36 +609,26 @@ async fn join_room_by_id_helper_remote(
|
||||
drop(cork);
|
||||
|
||||
debug!("Running send_join auth check");
|
||||
let fetch_state = &state;
|
||||
let state_fetch = |k: StateEventType, s: StateKey| async move {
|
||||
let shortstatekey = services
|
||||
.rooms
|
||||
.short
|
||||
.get_shortstatekey(&k, &s)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let event_id = fetch_state.get(&shortstatekey)?;
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu(event_id)
|
||||
.await
|
||||
.ok()
|
||||
};
|
||||
|
||||
let auth_check = state_res::event_auth::auth_check(
|
||||
&state_res::RoomVersion::new(&room_version_id)?,
|
||||
state_res::auth_check(
|
||||
&room_version::rules(&room_version_id)?,
|
||||
&parsed_join_pdu,
|
||||
None, // TODO: third party invite
|
||||
|k, s| state_fetch(k.clone(), s.into()),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?;
|
||||
&async |event_id| services.rooms.timeline.get_pdu(&event_id).await,
|
||||
&async |event_type, state_key| {
|
||||
let shortstatekey = services
|
||||
.rooms
|
||||
.short
|
||||
.get_shortstatekey(&event_type, state_key.as_str())
|
||||
.await?;
|
||||
|
||||
if !auth_check {
|
||||
return Err!(Request(Forbidden("Auth check failed")));
|
||||
}
|
||||
let event_id = state.get(&shortstatekey).ok_or_else(|| {
|
||||
err!(Request(NotFound("Missing fetch_state {shortstatekey:?}")))
|
||||
})?;
|
||||
|
||||
services.rooms.timeline.get_pdu(event_id).await
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
info!("Compressing state from send_join");
|
||||
let compressed: CompressedState = services
|
||||
|
||||
@@ -8,7 +8,10 @@ use ruma::{
|
||||
RoomVersionId, UserId,
|
||||
api::{
|
||||
client::knock::knock_room,
|
||||
federation::{self},
|
||||
federation::{
|
||||
membership::RawStrippedState,
|
||||
{self},
|
||||
},
|
||||
},
|
||||
canonical_json::to_canonical_value,
|
||||
events::{
|
||||
@@ -17,7 +20,7 @@ use ruma::{
|
||||
},
|
||||
};
|
||||
use tuwunel_core::{
|
||||
Err, Result, debug, debug_info, debug_warn, err, info,
|
||||
Err, Result, debug, debug_info, debug_warn, err, extract_variant, info,
|
||||
matrix::{
|
||||
event::{Event, gen_event_id},
|
||||
pdu::{PduBuilder, PduEvent},
|
||||
@@ -346,7 +349,7 @@ async fn knock_room_helper_local(
|
||||
let knock_event = knock_event_stub;
|
||||
|
||||
info!("Asking {remote_server} for send_knock in room {room_id}");
|
||||
let send_knock_request = federation::knock::send_knock::v1::Request {
|
||||
let send_knock_request = federation::membership::create_knock_event::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: event_id.clone(),
|
||||
pdu: services
|
||||
@@ -384,7 +387,13 @@ async fn knock_room_helper_local(
|
||||
.get_content::<RoomMemberEventContent>()
|
||||
.expect("we just created this"),
|
||||
sender_user,
|
||||
Some(send_knock_response.knock_room_state),
|
||||
Some(
|
||||
send_knock_response
|
||||
.knock_room_state
|
||||
.into_iter()
|
||||
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
|
||||
.collect(),
|
||||
),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
@@ -477,7 +486,7 @@ async fn knock_room_helper_remote(
|
||||
let knock_event = knock_event_stub;
|
||||
|
||||
info!("Asking {remote_server} for send_knock in room {room_id}");
|
||||
let send_knock_request = federation::knock::send_knock::v1::Request {
|
||||
let send_knock_request = federation::membership::create_knock_event::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: event_id.clone(),
|
||||
pdu: services
|
||||
@@ -507,7 +516,14 @@ async fn knock_room_helper_remote(
|
||||
let state = send_knock_response
|
||||
.knock_room_state
|
||||
.iter()
|
||||
.map(|event| serde_json::from_str::<CanonicalJsonObject>(event.clone().into_json().get()))
|
||||
.map(|event| {
|
||||
serde_json::from_str::<CanonicalJsonObject>(
|
||||
extract_variant!(event.clone(), RawStrippedState::Stripped)
|
||||
.expect("Raw<AnyStrippedStateEvent>")
|
||||
.json()
|
||||
.get(),
|
||||
)
|
||||
})
|
||||
.filter_map(Result::ok);
|
||||
|
||||
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
|
||||
@@ -594,7 +610,13 @@ async fn knock_room_helper_remote(
|
||||
.get_content::<RoomMemberEventContent>()
|
||||
.expect("we just created this"),
|
||||
sender_user,
|
||||
Some(send_knock_response.knock_room_state),
|
||||
Some(
|
||||
send_knock_response
|
||||
.knock_room_state
|
||||
.into_iter()
|
||||
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
|
||||
.collect(),
|
||||
),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
@@ -628,7 +650,7 @@ async fn make_knock_request(
|
||||
sender_user: &UserId,
|
||||
room_id: &RoomId,
|
||||
servers: &[OwnedServerName],
|
||||
) -> Result<(federation::knock::create_knock_event_template::v1::Response, OwnedServerName)> {
|
||||
) -> Result<(federation::membership::prepare_knock_event::v1::Response, OwnedServerName)> {
|
||||
let mut make_knock_response_and_server =
|
||||
Err!(BadServerResponse("No server available to assist in knocking."));
|
||||
|
||||
@@ -645,7 +667,7 @@ async fn make_knock_request(
|
||||
.sending
|
||||
.send_federation_request(
|
||||
remote_server,
|
||||
federation::knock::create_knock_event_template::v1::Request {
|
||||
federation::membership::prepare_knock_event::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
user_id: sender_user.to_owned(),
|
||||
ver: services
|
||||
|
||||
@@ -70,15 +70,16 @@ pub(crate) async fn banned_room_check(
|
||||
|
||||
if let Some(room_id) = room_id {
|
||||
if services.rooms.metadata.is_banned(room_id).await
|
||||
|| services
|
||||
.config
|
||||
.forbidden_remote_server_names
|
||||
.is_match(
|
||||
room_id
|
||||
.server_name()
|
||||
.expect("legacy room mxid")
|
||||
.host(),
|
||||
) {
|
||||
|| (room_id.server_name().is_some()
|
||||
&& services
|
||||
.config
|
||||
.forbidden_remote_server_names
|
||||
.is_match(
|
||||
room_id
|
||||
.server_name()
|
||||
.expect("legacy room mxid")
|
||||
.host(),
|
||||
)) {
|
||||
warn!(
|
||||
"User {user_id} who is not an admin attempted to send an invite for or \
|
||||
attempted to join a banned room or banned room server name: {room_id}"
|
||||
|
||||
@@ -60,7 +60,7 @@ pub(super) use message::*;
|
||||
pub(super) use openid::*;
|
||||
pub(super) use presence::*;
|
||||
pub(super) use profile::*;
|
||||
pub use profile::{update_all_rooms, update_avatar_url, update_displayname};
|
||||
pub use profile::{update_avatar_url, update_displayname};
|
||||
pub(super) use push::*;
|
||||
pub(super) use read_marker::*;
|
||||
pub(super) use redact::*;
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
||||
|
||||
use axum::extract::State;
|
||||
use futures::{
|
||||
StreamExt, TryStreamExt,
|
||||
FutureExt, StreamExt, TryStreamExt,
|
||||
future::{join, join3, join4},
|
||||
};
|
||||
use ruma::{
|
||||
@@ -368,7 +368,9 @@ pub async fn update_displayname(
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
update_all_rooms(services, all_joined_rooms, user_id).await;
|
||||
update_all_rooms(services, all_joined_rooms, user_id)
|
||||
.boxed()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn update_avatar_url(
|
||||
@@ -421,10 +423,12 @@ pub async fn update_avatar_url(
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
update_all_rooms(services, all_joined_rooms, user_id).await;
|
||||
update_all_rooms(services, all_joined_rooms, user_id)
|
||||
.boxed()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn update_all_rooms(
|
||||
async fn update_all_rooms(
|
||||
services: &Services,
|
||||
all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>,
|
||||
user_id: &UserId,
|
||||
|
||||
@@ -316,7 +316,7 @@ pub(crate) async fn register_route(
|
||||
stages: vec![AuthType::RegistrationToken],
|
||||
}],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
params: Default::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
@@ -326,7 +326,7 @@ pub(crate) async fn register_route(
|
||||
uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
params: Default::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
@@ -523,7 +523,11 @@ pub(crate) async fn register_route(
|
||||
.await
|
||||
.is_ok_and(is_equal_to!(1))
|
||||
{
|
||||
services.admin.make_user_admin(&user_id).await?;
|
||||
services
|
||||
.admin
|
||||
.make_user_admin(&user_id)
|
||||
.boxed()
|
||||
.await?;
|
||||
warn!("Granting {user_id} admin privileges as the first user");
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ use tuwunel_service::Services;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
const REASON_MAX_LEN: usize = 750;
|
||||
|
||||
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
|
||||
///
|
||||
/// Reports an abusive room to homeserver admins
|
||||
@@ -29,18 +31,13 @@ pub(crate) async fn report_room_route(
|
||||
|
||||
info!(
|
||||
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
|
||||
body.room_id,
|
||||
body.reason.as_deref().unwrap_or("")
|
||||
body.room_id, body.reason,
|
||||
);
|
||||
|
||||
if body
|
||||
.reason
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.len() > 750)
|
||||
{
|
||||
return Err!(Request(
|
||||
InvalidParam("Reason too long, should be 750 characters or fewer",)
|
||||
));
|
||||
if body.reason.len().gt(&REASON_MAX_LEN) {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Reason too long, should be {REASON_MAX_LEN} characters or fewer"
|
||||
)));
|
||||
}
|
||||
|
||||
delay_response().await;
|
||||
@@ -64,7 +61,7 @@ pub(crate) async fn report_room_route(
|
||||
"@room Room report received from {} -\n\nRoom ID: {}\n\nReport Reason: {}",
|
||||
sender_user.to_owned(),
|
||||
body.room_id,
|
||||
body.reason.as_deref().unwrap_or("")
|
||||
body.reason,
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::extract::State;
|
||||
use futures::FutureExt;
|
||||
use futures::{FutureExt, future::OptionFuture};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
|
||||
api::client::room::{self, create_room},
|
||||
api::client::room::{
|
||||
self, create_room,
|
||||
create_room::v3::{CreationContent, RoomPreset},
|
||||
},
|
||||
events::{
|
||||
TimelineEventType,
|
||||
room::{
|
||||
@@ -16,19 +19,21 @@ use ruma::{
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
name::RoomNameEventContent,
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
topic::RoomTopicEventContent,
|
||||
topic::{RoomTopicEventContent, TopicContentBlock},
|
||||
},
|
||||
},
|
||||
int,
|
||||
room_version_rules::{RoomIdFormatVersion, RoomVersionRules},
|
||||
serde::{JsonObject, Raw},
|
||||
};
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
use tuwunel_core::{
|
||||
Err, Result, debug_info, debug_warn, err, info,
|
||||
matrix::{StateKey, pdu::PduBuilder},
|
||||
matrix::{StateKey, pdu::PduBuilder, room_version},
|
||||
utils::BoolExt,
|
||||
warn,
|
||||
};
|
||||
use tuwunel_service::{Services, appservice::RegistrationInfo};
|
||||
use tuwunel_service::{Services, appservice::RegistrationInfo, rooms::state::RoomMutexGuard};
|
||||
|
||||
use crate::{Ruma, client::invite_helper};
|
||||
|
||||
@@ -53,164 +58,61 @@ pub(crate) async fn create_room_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<create_room::v3::Request>,
|
||||
) -> Result<create_room::v3::Response> {
|
||||
use create_room::v3::RoomPreset;
|
||||
can_create_room_check(&services, &body).await?;
|
||||
can_publish_directory_check(&services, &body).await?;
|
||||
|
||||
let sender_user = body.sender_user();
|
||||
// Figure out preset. We need it for preset specific events
|
||||
let preset = body
|
||||
.preset
|
||||
.clone()
|
||||
.unwrap_or(match &body.visibility {
|
||||
| room::Visibility::Public => RoomPreset::PublicChat,
|
||||
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
|
||||
});
|
||||
|
||||
if !services.globals.allow_room_creation()
|
||||
&& body.appservice_info.is_none()
|
||||
&& !services.users.is_admin(sender_user).await
|
||||
{
|
||||
return Err!(Request(Forbidden("Room creation has been disabled.",)));
|
||||
}
|
||||
let alias: OptionFuture<_> = body
|
||||
.room_alias_name
|
||||
.as_ref()
|
||||
.map(|alias| room_alias_check(&services, alias, body.appservice_info.as_ref()))
|
||||
.into();
|
||||
|
||||
let room_id: OwnedRoomId = match &body.room_id {
|
||||
| Some(custom_room_id) => custom_room_id_check(&services, custom_room_id)?,
|
||||
| _ => RoomId::new(&services.server.name),
|
||||
};
|
||||
|
||||
// check if room ID doesn't already exist instead of erroring on auth check
|
||||
if services
|
||||
.rooms
|
||||
.short
|
||||
.get_shortroomid(&room_id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
|
||||
}
|
||||
|
||||
if body.visibility == room::Visibility::Public
|
||||
&& services
|
||||
.server
|
||||
.config
|
||||
.lockdown_public_room_directory
|
||||
&& !services.users.is_admin(sender_user).await
|
||||
&& body.appservice_info.is_none()
|
||||
{
|
||||
warn!(
|
||||
"Non-admin user {sender_user} tried to publish {room_id} to the room directory \
|
||||
while \"lockdown_public_room_directory\" is enabled"
|
||||
);
|
||||
|
||||
if services.server.config.admin_room_notices {
|
||||
// Determine room version
|
||||
let (room_version, version_rules) = body
|
||||
.room_version
|
||||
.as_ref()
|
||||
.map_or(Ok(&services.server.config.default_room_version), |version| {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Non-admin user {sender_user} tried to publish {room_id} to the room \
|
||||
directory while \"lockdown_public_room_directory\" is enabled"
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")));
|
||||
}
|
||||
|
||||
let _short_id = services
|
||||
.rooms
|
||||
.short
|
||||
.get_or_create_shortroomid(&room_id)
|
||||
.await;
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
|
||||
|
||||
let alias: Option<OwnedRoomAliasId> = match body.room_alias_name.as_ref() {
|
||||
| Some(alias) =>
|
||||
Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?),
|
||||
| _ => None,
|
||||
};
|
||||
|
||||
let room_version = match body.room_version.clone() {
|
||||
| Some(room_version) =>
|
||||
if services
|
||||
.server
|
||||
.supported_room_version(&room_version)
|
||||
{
|
||||
room_version
|
||||
} else {
|
||||
return Err!(Request(UnsupportedRoomVersion(
|
||||
"This server does not support that room version."
|
||||
)));
|
||||
},
|
||||
| None => services
|
||||
.server
|
||||
.config
|
||||
.default_room_version
|
||||
.clone(),
|
||||
};
|
||||
.supported_room_version(version)
|
||||
.then_ok_or_else(version, || {
|
||||
err!(Request(UnsupportedRoomVersion(
|
||||
"This server does not support room version {version:?}"
|
||||
)))
|
||||
})
|
||||
})
|
||||
.and_then(|version| Ok((version, room_version::rules(version)?)))?;
|
||||
|
||||
let create_content = match &body.creation_content {
|
||||
| Some(content) => {
|
||||
use RoomVersionId::*;
|
||||
|
||||
let mut content = content
|
||||
.deserialize_as::<CanonicalJsonObject>()
|
||||
.map_err(|e| {
|
||||
err!(Request(BadJson(error!(
|
||||
"Failed to deserialise content as canonical JSON: {e}"
|
||||
))))
|
||||
})?;
|
||||
|
||||
match room_version {
|
||||
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
|
||||
content.insert(
|
||||
"creator".into(),
|
||||
json!(&sender_user).try_into().map_err(|e| {
|
||||
err!(Request(BadJson(debug_error!("Invalid creation content: {e}"))))
|
||||
})?,
|
||||
);
|
||||
},
|
||||
| _ => {
|
||||
// V11+ removed the "creator" key
|
||||
},
|
||||
}
|
||||
content.insert(
|
||||
"room_version".into(),
|
||||
json!(room_version.as_str())
|
||||
.try_into()
|
||||
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
|
||||
);
|
||||
content
|
||||
},
|
||||
| None => {
|
||||
use RoomVersionId::*;
|
||||
|
||||
let content = match room_version {
|
||||
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
|
||||
RoomCreateEventContent::new_v1(sender_user.to_owned()),
|
||||
| _ => RoomCreateEventContent::new_v11(),
|
||||
};
|
||||
let mut content =
|
||||
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())
|
||||
.unwrap();
|
||||
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
|
||||
content
|
||||
},
|
||||
};
|
||||
// Error on existing alias before committing to creation.
|
||||
let alias = alias.await.transpose()?;
|
||||
|
||||
// Increment and hold the counter; the room will sync atomically to clients
|
||||
// which is preferable.
|
||||
let next_count = services.globals.next_count();
|
||||
|
||||
// 1. The room create event
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&create_content)?,
|
||||
state_key: Some(StateKey::new()),
|
||||
..Default::default()
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
// 1. Create the create event.
|
||||
let (room_id, state_lock) = match version_rules.room_id_format {
|
||||
| RoomIdFormatVersion::V1 =>
|
||||
create_create_event_legacy(&services, &body, room_version, &version_rules).await?,
|
||||
| RoomIdFormatVersion::V2 =>
|
||||
create_create_event(&services, &body, &preset, room_version, &version_rules)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
err!(Request(InvalidParam("Error while creating m.room.create event: {e}")))
|
||||
})?,
|
||||
};
|
||||
|
||||
// 2. Let the room creator join
|
||||
let sender_user = body.sender_user();
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
@@ -230,17 +132,14 @@ pub(crate) async fn create_room_route(
|
||||
.await?;
|
||||
|
||||
// 3. Power levels
|
||||
|
||||
// Figure out preset. We need it for preset specific events
|
||||
let preset = body
|
||||
.preset
|
||||
.clone()
|
||||
.unwrap_or(match &body.visibility {
|
||||
| room::Visibility::Public => RoomPreset::PublicChat,
|
||||
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
|
||||
});
|
||||
|
||||
let mut users = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
|
||||
let mut users = if !version_rules
|
||||
.authorization
|
||||
.explicitly_privilege_room_creators
|
||||
{
|
||||
BTreeMap::from_iter([(sender_user.to_owned(), int!(100))])
|
||||
} else {
|
||||
BTreeMap::new()
|
||||
};
|
||||
|
||||
if preset == RoomPreset::TrustedPrivateChat {
|
||||
for invite in &body.invite {
|
||||
@@ -260,11 +159,17 @@ pub(crate) async fn create_room_route(
|
||||
continue;
|
||||
}
|
||||
|
||||
users.insert(invite.clone(), int!(100));
|
||||
if !version_rules
|
||||
.authorization
|
||||
.additional_room_creators
|
||||
{
|
||||
users.insert(invite.clone(), int!(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let power_levels_content = default_power_levels_content(
|
||||
&version_rules,
|
||||
body.power_level_content_override.as_ref(),
|
||||
&body.visibility,
|
||||
users,
|
||||
@@ -365,7 +270,7 @@ pub(crate) async fn create_room_route(
|
||||
// 6. Events listed in initial_state
|
||||
for event in &body.initial_state {
|
||||
let mut pdu_builder = event
|
||||
.deserialize_as::<PduBuilder>()
|
||||
.deserialize_as_unchecked::<PduBuilder>()
|
||||
.map_err(|e| {
|
||||
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
|
||||
})?;
|
||||
@@ -421,7 +326,10 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }),
|
||||
PduBuilder::state(String::new(), &RoomTopicEventContent {
|
||||
topic: topic.clone(),
|
||||
topic_block: TopicContentBlock::default(),
|
||||
}),
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
@@ -488,25 +396,226 @@ pub(crate) async fn create_room_route(
|
||||
Ok(create_room::v3::Response::new(room_id))
|
||||
}
|
||||
|
||||
async fn create_create_event(
|
||||
services: &Services,
|
||||
body: &Ruma<create_room::v3::Request>,
|
||||
preset: &RoomPreset,
|
||||
room_version: &RoomVersionId,
|
||||
version_rules: &RoomVersionRules,
|
||||
) -> Result<(OwnedRoomId, RoomMutexGuard)> {
|
||||
let _sender_user = body.sender_user();
|
||||
|
||||
let mut create_content = match &body.creation_content {
|
||||
| Some(content) => {
|
||||
let mut content = content
|
||||
.deserialize_as_unchecked::<CanonicalJsonObject>()
|
||||
.map_err(|e| {
|
||||
err!(Request(BadJson(error!(
|
||||
"Failed to deserialise content as canonical JSON: {e}"
|
||||
))))
|
||||
})?;
|
||||
|
||||
content.insert(
|
||||
"room_version".into(),
|
||||
json!(room_version.as_str())
|
||||
.try_into()
|
||||
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
|
||||
);
|
||||
|
||||
content
|
||||
},
|
||||
| None => {
|
||||
let content = RoomCreateEventContent::new_v11();
|
||||
|
||||
let mut content =
|
||||
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
|
||||
|
||||
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
|
||||
content
|
||||
},
|
||||
};
|
||||
|
||||
if version_rules
|
||||
.authorization
|
||||
.additional_room_creators
|
||||
{
|
||||
let mut additional_creators = body
|
||||
.creation_content
|
||||
.as_ref()
|
||||
.and_then(|c| {
|
||||
c.deserialize_as_unchecked::<CreationContent>()
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.additional_creators;
|
||||
|
||||
if *preset == RoomPreset::TrustedPrivateChat {
|
||||
additional_creators.extend(body.invite.clone());
|
||||
}
|
||||
|
||||
additional_creators.sort();
|
||||
additional_creators.dedup();
|
||||
if !additional_creators.is_empty() {
|
||||
create_content
|
||||
.insert("additional_creators".into(), json!(additional_creators).try_into()?);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. The room create event, using a placeholder room_id
|
||||
let room_id = ruma::room_id!("!thiswillbereplaced").to_owned();
|
||||
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
|
||||
let create_event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&create_content)?,
|
||||
state_key: Some(StateKey::new()),
|
||||
..Default::default()
|
||||
},
|
||||
body.sender_user(),
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
// The real room_id is now the event_id.
|
||||
let room_id = OwnedRoomId::from_parts('!', create_event_id.localpart(), None)?;
|
||||
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
|
||||
|
||||
Ok((room_id, state_lock))
|
||||
}
|
||||
|
||||
async fn create_create_event_legacy(
|
||||
services: &Services,
|
||||
body: &Ruma<create_room::v3::Request>,
|
||||
room_version: &RoomVersionId,
|
||||
_version_rules: &RoomVersionRules,
|
||||
) -> Result<(OwnedRoomId, RoomMutexGuard)> {
|
||||
let room_id: OwnedRoomId = match &body.room_id {
|
||||
| None => RoomId::new_v1(&services.server.name),
|
||||
| Some(custom_id) => custom_room_id_check(services, custom_id).await?,
|
||||
};
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
|
||||
|
||||
let _short_id = services
|
||||
.rooms
|
||||
.short
|
||||
.get_or_create_shortroomid(&room_id)
|
||||
.await;
|
||||
|
||||
let create_content = match &body.creation_content {
|
||||
| Some(content) => {
|
||||
use RoomVersionId::*;
|
||||
|
||||
let mut content = content
|
||||
.deserialize_as_unchecked::<CanonicalJsonObject>()
|
||||
.map_err(|e| {
|
||||
err!(Request(BadJson(error!(
|
||||
"Failed to deserialise content as canonical JSON: {e}"
|
||||
))))
|
||||
})?;
|
||||
|
||||
match room_version {
|
||||
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
|
||||
content.insert(
|
||||
"creator".into(),
|
||||
json!(body.sender_user())
|
||||
.try_into()
|
||||
.map_err(|e| {
|
||||
err!(Request(BadJson(debug_error!(
|
||||
"Invalid creation content: {e}"
|
||||
))))
|
||||
})?,
|
||||
);
|
||||
},
|
||||
| _ => {
|
||||
// V11+ removed the "creator" key
|
||||
},
|
||||
}
|
||||
|
||||
content.insert(
|
||||
"room_version".into(),
|
||||
json!(room_version.as_str())
|
||||
.try_into()
|
||||
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
|
||||
);
|
||||
|
||||
content
|
||||
},
|
||||
| None => {
|
||||
use RoomVersionId::*;
|
||||
|
||||
let content = match room_version {
|
||||
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
|
||||
RoomCreateEventContent::new_v1(body.sender_user().to_owned()),
|
||||
| _ => RoomCreateEventContent::new_v11(),
|
||||
};
|
||||
|
||||
let mut content =
|
||||
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
|
||||
|
||||
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
|
||||
content
|
||||
},
|
||||
};
|
||||
|
||||
// 1. The room create event
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&create_content)?,
|
||||
state_key: Some(StateKey::new()),
|
||||
..Default::default()
|
||||
},
|
||||
body.sender_user(),
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
Ok((room_id, state_lock))
|
||||
}
|
||||
|
||||
/// creates the power_levels_content for the PDU builder
|
||||
fn default_power_levels_content(
|
||||
version_rules: &RoomVersionRules,
|
||||
power_level_content_override: Option<&Raw<RoomPowerLevelsEventContent>>,
|
||||
visibility: &room::Visibility,
|
||||
users: BTreeMap<OwnedUserId, Int>,
|
||||
) -> Result<serde_json::Value> {
|
||||
use serde_json::to_value;
|
||||
|
||||
let mut power_levels_content =
|
||||
to_value(RoomPowerLevelsEventContent { users, ..Default::default() })?;
|
||||
let mut power_levels_content = RoomPowerLevelsEventContent::new(&version_rules.authorization);
|
||||
power_levels_content.users = users;
|
||||
|
||||
let mut power_levels_content = to_value(power_levels_content)?;
|
||||
|
||||
// secure proper defaults of sensitive/dangerous permissions that moderators
|
||||
// (power level 50) should not have easy access to
|
||||
power_levels_content["events"]["m.room.power_levels"] = to_value(100)?;
|
||||
power_levels_content["events"]["m.room.server_acl"] = to_value(100)?;
|
||||
power_levels_content["events"]["m.room.tombstone"] = to_value(100)?;
|
||||
power_levels_content["events"]["m.room.encryption"] = to_value(100)?;
|
||||
power_levels_content["events"]["m.room.history_visibility"] = to_value(100)?;
|
||||
|
||||
if version_rules
|
||||
.authorization
|
||||
.explicitly_privilege_room_creators
|
||||
{
|
||||
power_levels_content["events"]["m.room.tombstone"] = to_value(150)?;
|
||||
} else {
|
||||
power_levels_content["events"]["m.room.tombstone"] = to_value(100)?;
|
||||
}
|
||||
|
||||
// always allow users to respond (not post new) to polls. this is primarily
|
||||
// useful in read-only announcement rooms that post a public poll.
|
||||
power_levels_content["events"]["org.matrix.msc3381.poll.response"] = to_value(0)?;
|
||||
@@ -599,7 +708,7 @@ async fn room_alias_check(
|
||||
}
|
||||
|
||||
/// if a room is being created with a custom room ID, run our checks against it
|
||||
fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
|
||||
async fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
|
||||
// apply forbidden room alias checks to custom room IDs too
|
||||
if services
|
||||
.globals
|
||||
@@ -623,8 +732,65 @@ fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<Own
|
||||
let server_name = services.globals.server_name();
|
||||
let full_room_id = format!("!{custom_room_id}:{server_name}");
|
||||
|
||||
OwnedRoomId::parse(full_room_id)
|
||||
.map_err(Into::into)
|
||||
let room_id = OwnedRoomId::parse(full_room_id)
|
||||
.inspect(|full_room_id| debug_info!(?full_room_id, "Full custom room ID"))
|
||||
.inspect_err(|e| warn!(?e, ?custom_room_id, "Failed to create room with custom room ID",))
|
||||
.inspect_err(|e| {
|
||||
warn!(?e, ?custom_room_id, "Failed to create room with custom room ID");
|
||||
})?;
|
||||
|
||||
// check if room ID doesn't already exist instead of erroring on auth check
|
||||
if services
|
||||
.rooms
|
||||
.short
|
||||
.get_shortroomid(&room_id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
|
||||
}
|
||||
|
||||
Ok(room_id)
|
||||
}
|
||||
|
||||
async fn can_publish_directory_check(
|
||||
services: &Services,
|
||||
body: &Ruma<create_room::v3::Request>,
|
||||
) -> Result {
|
||||
if !services
|
||||
.server
|
||||
.config
|
||||
.lockdown_public_room_directory
|
||||
|| body.appservice_info.is_some()
|
||||
|| body.visibility != room::Visibility::Public
|
||||
|| services.users.is_admin(body.sender_user()).await
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let msg = format!(
|
||||
"Non-admin user {} tried to publish new to the directory while \
|
||||
lockdown_public_room_directory is enabled",
|
||||
body.sender_user(),
|
||||
);
|
||||
|
||||
warn!("{msg}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(&msg).await;
|
||||
}
|
||||
|
||||
Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")))
|
||||
}
|
||||
|
||||
async fn can_create_room_check(
|
||||
services: &Services,
|
||||
body: &Ruma<create_room::v3::Request>,
|
||||
) -> Result {
|
||||
if !services.globals.allow_room_creation()
|
||||
&& body.appservice_info.is_none()
|
||||
&& !services.users.is_admin(body.sender_user()).await
|
||||
{
|
||||
return Err!(Request(Forbidden("Room creation has been disabled.",)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use ruma::{
|
||||
federation::space::{SpaceHierarchyParentSummary, get_hierarchy},
|
||||
},
|
||||
events::room::member::MembershipState,
|
||||
space::SpaceRoomJoinRule::{self, *},
|
||||
room::{JoinRuleSummary, RoomSummary},
|
||||
};
|
||||
use tuwunel_core::{
|
||||
Err, Result, debug_warn, trace,
|
||||
@@ -34,8 +34,8 @@ use crate::{Ruma, RumaResponse};
|
||||
pub(crate) async fn get_room_summary_legacy(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_summary::msc3266::Request>,
|
||||
) -> Result<RumaResponse<get_summary::msc3266::Response>> {
|
||||
body: Ruma<get_summary::v1::Request>,
|
||||
) -> Result<RumaResponse<get_summary::v1::Response>> {
|
||||
get_room_summary(State(services), InsecureClientIp(client), body)
|
||||
.boxed()
|
||||
.await
|
||||
@@ -51,8 +51,8 @@ pub(crate) async fn get_room_summary_legacy(
|
||||
pub(crate) async fn get_room_summary(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_summary::msc3266::Request>,
|
||||
) -> Result<get_summary::msc3266::Response> {
|
||||
body: Ruma<get_summary::v1::Request>,
|
||||
) -> Result<get_summary::v1::Response> {
|
||||
let (room_id, servers) = services
|
||||
.rooms
|
||||
.alias
|
||||
@@ -73,7 +73,7 @@ async fn room_summary_response(
|
||||
room_id: &RoomId,
|
||||
servers: &[OwnedServerName],
|
||||
sender_user: Option<&UserId>,
|
||||
) -> Result<get_summary::msc3266::Response> {
|
||||
) -> Result<get_summary::v1::Response> {
|
||||
if services
|
||||
.rooms
|
||||
.state_cache
|
||||
@@ -85,23 +85,12 @@ async fn room_summary_response(
|
||||
.await;
|
||||
}
|
||||
|
||||
let room =
|
||||
remote_room_summary_hierarchy_response(services, room_id, servers, sender_user).await?;
|
||||
let summary = remote_room_summary_hierarchy_response(services, room_id, servers, sender_user)
|
||||
.await?
|
||||
.summary;
|
||||
|
||||
Ok(get_summary::msc3266::Response {
|
||||
room_id: room_id.to_owned(),
|
||||
canonical_alias: room.canonical_alias,
|
||||
avatar_url: room.avatar_url,
|
||||
guest_can_join: room.guest_can_join,
|
||||
name: room.name,
|
||||
num_joined_members: room.num_joined_members,
|
||||
topic: room.topic,
|
||||
world_readable: room.world_readable,
|
||||
join_rule: room.join_rule,
|
||||
room_type: room.room_type,
|
||||
room_version: room.room_version,
|
||||
encryption: room.encryption,
|
||||
allowed_room_ids: room.allowed_room_ids,
|
||||
Ok(get_summary::v1::Response {
|
||||
summary,
|
||||
membership: sender_user
|
||||
.is_some()
|
||||
.then_some(MembershipState::Leave),
|
||||
@@ -112,7 +101,7 @@ async fn local_room_summary_response(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
sender_user: Option<&UserId>,
|
||||
) -> Result<get_summary::msc3266::Response> {
|
||||
) -> Result<get_summary::v1::Response> {
|
||||
trace!(?sender_user, "Sending local room summary response for {room_id:?}");
|
||||
let join_rule = services
|
||||
.rooms
|
||||
@@ -139,7 +128,7 @@ async fn local_room_summary_response(
|
||||
&join_rule.clone().into(),
|
||||
guest_can_join,
|
||||
world_readable,
|
||||
join_rule.allowed_rooms(),
|
||||
join_rule.allowed_room_ids(),
|
||||
sender_user,
|
||||
)
|
||||
.await?;
|
||||
@@ -224,24 +213,22 @@ async fn local_room_summary_response(
|
||||
membership,
|
||||
);
|
||||
|
||||
Ok(get_summary::msc3266::Response {
|
||||
room_id: room_id.to_owned(),
|
||||
canonical_alias,
|
||||
avatar_url,
|
||||
guest_can_join,
|
||||
name,
|
||||
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
|
||||
topic,
|
||||
world_readable,
|
||||
room_type,
|
||||
room_version,
|
||||
encryption,
|
||||
Ok(get_summary::v1::Response {
|
||||
summary: RoomSummary {
|
||||
room_id: room_id.to_owned(),
|
||||
canonical_alias,
|
||||
avatar_url,
|
||||
guest_can_join,
|
||||
name,
|
||||
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
|
||||
topic,
|
||||
world_readable,
|
||||
room_type,
|
||||
room_version,
|
||||
encryption,
|
||||
join_rule: join_rule.into(),
|
||||
},
|
||||
membership,
|
||||
allowed_room_ids: join_rule
|
||||
.allowed_rooms()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
join_rule: join_rule.into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -277,10 +264,11 @@ async fn remote_room_summary_hierarchy_response(
|
||||
while let Some(Ok(response)) = requests.next().await {
|
||||
trace!("{response:?}");
|
||||
let room = response.room.clone();
|
||||
if room.room_id != room_id {
|
||||
let summary = &room.summary;
|
||||
if summary.room_id != room_id {
|
||||
debug_warn!(
|
||||
"Room ID {} returned does not belong to the requested room ID {}",
|
||||
room.room_id,
|
||||
summary.room_id,
|
||||
room_id
|
||||
);
|
||||
continue;
|
||||
@@ -289,10 +277,10 @@ async fn remote_room_summary_hierarchy_response(
|
||||
return user_can_see_summary(
|
||||
services,
|
||||
room_id,
|
||||
&room.join_rule,
|
||||
room.guest_can_join,
|
||||
room.world_readable,
|
||||
room.allowed_room_ids.iter().map(AsRef::as_ref),
|
||||
&summary.join_rule,
|
||||
summary.guest_can_join,
|
||||
summary.world_readable,
|
||||
summary.join_rule.allowed_room_ids(),
|
||||
sender_user,
|
||||
)
|
||||
.await
|
||||
@@ -308,7 +296,7 @@ async fn remote_room_summary_hierarchy_response(
|
||||
async fn user_can_see_summary<'a, I>(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
join_rule: &SpaceRoomJoinRule,
|
||||
join_rule: &JoinRuleSummary,
|
||||
guest_can_join: bool,
|
||||
world_readable: bool,
|
||||
allowed_room_ids: I,
|
||||
@@ -317,17 +305,23 @@ async fn user_can_see_summary<'a, I>(
|
||||
where
|
||||
I: Iterator<Item = &'a RoomId> + Send,
|
||||
{
|
||||
let is_public_room = matches!(join_rule, Public | Knock | KnockRestricted);
|
||||
let is_public_room = matches!(
|
||||
join_rule,
|
||||
JoinRuleSummary::Public | JoinRuleSummary::Knock | JoinRuleSummary::KnockRestricted(_)
|
||||
);
|
||||
|
||||
match sender_user {
|
||||
| Some(sender_user) => {
|
||||
let user_can_see_state_events = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, room_id);
|
||||
|
||||
let is_guest = services
|
||||
.users
|
||||
.is_deactivated(sender_user)
|
||||
.unwrap_or(false);
|
||||
|
||||
let user_in_allowed_restricted_room = allowed_room_ids.stream().any(|room| {
|
||||
services
|
||||
.rooms
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::extract::State;
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
CanonicalJsonObject, RoomId, RoomVersionId,
|
||||
api::client::{error::ErrorKind, room::upgrade_room},
|
||||
api::client::room::upgrade_room,
|
||||
events::{
|
||||
StateEventType, TimelineEventType,
|
||||
room::{
|
||||
@@ -14,11 +14,12 @@ use ruma::{
|
||||
},
|
||||
},
|
||||
int,
|
||||
room_version_rules::RoomIdFormatVersion,
|
||||
};
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
use tuwunel_core::{
|
||||
Err, Error, Result, err,
|
||||
matrix::{Event, StateKey, pdu::PduBuilder},
|
||||
Err, Result, err,
|
||||
matrix::{Event, StateKey, pdu::PduBuilder, room_version},
|
||||
};
|
||||
|
||||
use crate::Ruma;
|
||||
@@ -61,14 +62,23 @@ pub(crate) async fn upgrade_room_route(
|
||||
.server
|
||||
.supported_room_version(&body.new_version)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::UnsupportedRoomVersion,
|
||||
return Err!(Request(UnsupportedRoomVersion(
|
||||
"This server does not support that room version.",
|
||||
));
|
||||
)));
|
||||
}
|
||||
|
||||
if matches!(body.new_version, RoomVersionId::V12) {
|
||||
return Err!(Request(UnsupportedRoomVersion(
|
||||
"Upgrading to version 12 is still under development.",
|
||||
)));
|
||||
}
|
||||
|
||||
let room_version_rules = room_version::rules(&body.new_version)?;
|
||||
let room_id_format = &room_version_rules.room_id_format;
|
||||
assert!(*room_id_format == RoomIdFormatVersion::V1, "TODO");
|
||||
|
||||
// Create a replacement room
|
||||
let replacement_room = RoomId::new(services.globals.server_name());
|
||||
let replacement_room = RoomId::new_v1(services.globals.server_name());
|
||||
|
||||
let _short_id = services
|
||||
.rooms
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use futures::FutureExt;
|
||||
use ruma::{OwnedUserId, UserId};
|
||||
use tuwunel_core::{Err, Result, debug};
|
||||
use tuwunel_service::Services;
|
||||
@@ -63,6 +64,7 @@ pub(super) async fn ldap_login(
|
||||
services
|
||||
.admin
|
||||
.make_user_admin(lowercased_user_id)
|
||||
.boxed()
|
||||
.await?;
|
||||
} else if !is_ldap_admin && is_tuwunel_admin {
|
||||
services
|
||||
|
||||
@@ -56,10 +56,7 @@ pub(crate) async fn login_token_route(
|
||||
|
||||
let mut uiaainfo = uiaa::UiaaInfo {
|
||||
flows: vec![password_flow],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
|
||||
@@ -2,7 +2,7 @@ use axum::extract::State;
|
||||
use futures::{FutureExt, TryFutureExt, TryStreamExt};
|
||||
use ruma::{
|
||||
OwnedEventId, RoomId, UserId,
|
||||
api::client::state::{get_state_events, get_state_events_for_key, send_state_event},
|
||||
api::client::state::{get_state_event_for_key, get_state_events, send_state_event},
|
||||
events::{
|
||||
AnyStateEventContent, StateEventType,
|
||||
room::{
|
||||
@@ -107,8 +107,8 @@ pub(crate) async fn get_state_events_route(
|
||||
/// readable
|
||||
pub(crate) async fn get_state_events_for_key_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_state_events_for_key::v3::Request>,
|
||||
) -> Result<get_state_events_for_key::v3::Response> {
|
||||
body: Ruma<get_state_event_for_key::v3::Request>,
|
||||
) -> Result<get_state_event_for_key::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
if !services
|
||||
@@ -140,7 +140,7 @@ pub(crate) async fn get_state_events_for_key_route(
|
||||
.as_ref()
|
||||
.is_some_and(|f| f.to_lowercase().eq("event"));
|
||||
|
||||
Ok(get_state_events_for_key::v3::Response {
|
||||
Ok(get_state_event_for_key::v3::Response {
|
||||
content: event_format.or(|| event.get_content_as_value()),
|
||||
event: event_format.then(|| {
|
||||
json!({
|
||||
@@ -167,8 +167,8 @@ pub(crate) async fn get_state_events_for_key_route(
|
||||
/// readable
|
||||
pub(crate) async fn get_state_events_for_empty_key_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_state_events_for_key::v3::Request>,
|
||||
) -> Result<RumaResponse<get_state_events_for_key::v3::Response>> {
|
||||
body: Ruma<get_state_event_for_key::v3::Request>,
|
||||
) -> Result<RumaResponse<get_state_event_for_key::v3::Response>> {
|
||||
get_state_events_for_key_route(State(services), body)
|
||||
.await
|
||||
.map(RumaResponse)
|
||||
@@ -222,7 +222,7 @@ async fn allowed_to_send_state_event(
|
||||
| StateEventType::RoomServerAcl => {
|
||||
// prevents common ACL paw-guns as ACL management is difficult and prone to
|
||||
// irreversible mistakes
|
||||
match json.deserialize_as::<RoomServerAclEventContent>() {
|
||||
match json.deserialize_as_unchecked::<RoomServerAclEventContent>() {
|
||||
| Ok(acl_content) => {
|
||||
if acl_content.allow_is_empty() {
|
||||
return Err!(Request(BadJson(debug_warn!(
|
||||
@@ -282,7 +282,7 @@ async fn allowed_to_send_state_event(
|
||||
// admin room is a sensitive room, it should not ever be made public
|
||||
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
|
||||
if admin_room_id == room_id {
|
||||
match json.deserialize_as::<RoomJoinRulesEventContent>() {
|
||||
match json.deserialize_as_unchecked::<RoomJoinRulesEventContent>() {
|
||||
| Ok(join_rule) =>
|
||||
if join_rule.join_rule == JoinRule::Public {
|
||||
return Err!(Request(Forbidden(
|
||||
@@ -301,7 +301,7 @@ async fn allowed_to_send_state_event(
|
||||
| StateEventType::RoomHistoryVisibility => {
|
||||
// admin room is a sensitive room, it should not ever be made world readable
|
||||
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
|
||||
match json.deserialize_as::<RoomHistoryVisibilityEventContent>() {
|
||||
match json.deserialize_as_unchecked::<RoomHistoryVisibilityEventContent>() {
|
||||
| Ok(visibility_content) => {
|
||||
if admin_room_id == room_id
|
||||
&& visibility_content.history_visibility
|
||||
@@ -322,7 +322,7 @@ async fn allowed_to_send_state_event(
|
||||
}
|
||||
},
|
||||
| StateEventType::RoomCanonicalAlias => {
|
||||
match json.deserialize_as::<RoomCanonicalAliasEventContent>() {
|
||||
match json.deserialize_as_unchecked::<RoomCanonicalAliasEventContent>() {
|
||||
| Ok(canonical_alias_content) => {
|
||||
let mut aliases = canonical_alias_content.alt_aliases.clone();
|
||||
|
||||
@@ -354,52 +354,53 @@ async fn allowed_to_send_state_event(
|
||||
},
|
||||
}
|
||||
},
|
||||
| StateEventType::RoomMember => match json.deserialize_as::<RoomMemberEventContent>() {
|
||||
| Ok(membership_content) => {
|
||||
let Ok(_state_key) = UserId::parse(state_key) else {
|
||||
return Err!(Request(BadJson(
|
||||
"Membership event has invalid or non-existent state key"
|
||||
)));
|
||||
};
|
||||
|
||||
if let Some(authorising_user) =
|
||||
membership_content.join_authorized_via_users_server
|
||||
{
|
||||
if membership_content.membership != MembershipState::Join {
|
||||
| StateEventType::RoomMember =>
|
||||
match json.deserialize_as_unchecked::<RoomMemberEventContent>() {
|
||||
| Ok(membership_content) => {
|
||||
let Ok(_state_key) = UserId::parse(state_key) else {
|
||||
return Err!(Request(BadJson(
|
||||
"join_authorised_via_users_server is only for member joins"
|
||||
"Membership event has invalid or non-existent state key"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
if !services.globals.user_is_local(&authorising_user) {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Authorising user {authorising_user} does not belong to this \
|
||||
homeserver"
|
||||
)));
|
||||
}
|
||||
if let Some(authorising_user) =
|
||||
membership_content.join_authorized_via_users_server
|
||||
{
|
||||
if membership_content.membership != MembershipState::Join {
|
||||
return Err!(Request(BadJson(
|
||||
"join_authorised_via_users_server is only for member joins"
|
||||
)));
|
||||
}
|
||||
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(&authorising_user, room_id)
|
||||
.map(is_false!())
|
||||
.map(BoolExt::into_result)
|
||||
.map_err(|()| {
|
||||
err!(Request(InvalidParam(
|
||||
"Authorising user {authorising_user} is not in the room. They \
|
||||
cannot authorise the join."
|
||||
)))
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
if !services.globals.user_is_local(&authorising_user) {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Authorising user {authorising_user} does not belong to this \
|
||||
homeserver"
|
||||
)));
|
||||
}
|
||||
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(&authorising_user, room_id)
|
||||
.map(is_false!())
|
||||
.map(BoolExt::into_result)
|
||||
.map_err(|()| {
|
||||
err!(Request(InvalidParam(
|
||||
"Authorising user {authorising_user} is not in the room. \
|
||||
They cannot authorise the join."
|
||||
)))
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(BadJson(
|
||||
"Membership content must have a valid JSON body with at least a valid \
|
||||
membership state: {e}"
|
||||
)));
|
||||
},
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(BadJson(
|
||||
"Membership content must have a valid JSON body with at least a valid \
|
||||
membership state: {e}"
|
||||
)));
|
||||
},
|
||||
},
|
||||
| _ => (),
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ use ruma::{
|
||||
api::client::{
|
||||
filter::FilterDefinition,
|
||||
sync::sync_events::{
|
||||
self, DeviceLists, UnreadNotificationsCount,
|
||||
self, DeviceLists, StrippedState, UnreadNotificationsCount,
|
||||
v3::{
|
||||
Ephemeral, Filter, GlobalAccountData, InviteState, InvitedRoom, JoinedRoom,
|
||||
KnockState, KnockedRoom, LeftRoom, Presence, RoomAccountData, RoomSummary, Rooms,
|
||||
State as RoomState, Timeline, ToDevice,
|
||||
State as RoomState, StateEvents, Timeline, ToDevice,
|
||||
},
|
||||
},
|
||||
uiaa::UiaaResponse,
|
||||
@@ -295,7 +295,12 @@ async fn build_sync_events(
|
||||
}
|
||||
|
||||
let invited_room = InvitedRoom {
|
||||
invite_state: InviteState { events: invite_state },
|
||||
invite_state: InviteState {
|
||||
events: invite_state
|
||||
.into_iter()
|
||||
.map(Raw::cast::<StrippedState>)
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
|
||||
invited_rooms.insert(room_id, invited_room);
|
||||
@@ -320,7 +325,12 @@ async fn build_sync_events(
|
||||
}
|
||||
|
||||
let knocked_room = KnockedRoom {
|
||||
knock_state: KnockState { events: knock_state },
|
||||
knock_state: KnockState {
|
||||
events: knock_state
|
||||
.into_iter()
|
||||
.map(Raw::cast::<StrippedState>)
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
|
||||
knocked_rooms.insert(room_id, knocked_room);
|
||||
@@ -540,7 +550,7 @@ async fn handle_left_room(
|
||||
prev_batch: Some(next_batch.to_string()),
|
||||
events: Vec::new(),
|
||||
},
|
||||
state: RoomState { events: vec![event.into_format()] },
|
||||
state: RoomState::Before(StateEvents { events: vec![event.into_format()] }),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -635,7 +645,7 @@ async fn handle_left_room(
|
||||
prev_batch: Some(next_batch.to_string()),
|
||||
events: Vec::new(), // and so we dont need to set this to empty vec
|
||||
},
|
||||
state: RoomState { events: left_state_events },
|
||||
state: RoomState::Before(StateEvents { events: left_state_events }),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1042,7 +1052,7 @@ async fn load_joined_room(
|
||||
let joined_room = JoinedRoom {
|
||||
account_data: RoomAccountData { events: account_data_events },
|
||||
ephemeral: Ephemeral { events: edus },
|
||||
state: RoomState { events: state_events },
|
||||
state: RoomState::Before(StateEvents { events: state_events }),
|
||||
summary: RoomSummary {
|
||||
joined_member_count: joined_member_count.map(ruma_from_u64),
|
||||
invited_member_count: invited_member_count.map(ruma_from_u64),
|
||||
|
||||
@@ -13,7 +13,10 @@ use futures::{
|
||||
};
|
||||
use ruma::{
|
||||
DeviceId, OwnedEventId, OwnedRoomId, RoomId, UInt, UserId,
|
||||
api::client::sync::sync_events::{self, DeviceLists, UnreadNotificationsCount},
|
||||
api::client::sync::sync_events::{
|
||||
self, DeviceLists, StrippedState, UnreadNotificationsCount,
|
||||
v5::request::ExtensionRoomConfig,
|
||||
},
|
||||
directory::RoomTypeFilter,
|
||||
events::{
|
||||
AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, StateEventType, TimelineEventType,
|
||||
@@ -647,7 +650,11 @@ where
|
||||
name: room_name.or(hero_name),
|
||||
initial: Some(roomsince == &0),
|
||||
is_dm: None,
|
||||
invite_state,
|
||||
invite_state: invite_state.map(|s| {
|
||||
s.into_iter()
|
||||
.map(Raw::cast::<StrippedState>)
|
||||
.collect()
|
||||
}),
|
||||
unread_notifications: UnreadNotificationsCount {
|
||||
highlight_count: Some(
|
||||
services
|
||||
@@ -727,7 +734,10 @@ async fn collect_account_data(
|
||||
.await;
|
||||
|
||||
if let Some(rooms) = &body.extensions.account_data.rooms {
|
||||
for room in rooms {
|
||||
for room in rooms
|
||||
.iter()
|
||||
.filter_map(|erc| extract_variant!(erc, ExtensionRoomConfig::Room))
|
||||
{
|
||||
account_data.rooms.insert(
|
||||
room.clone(),
|
||||
services
|
||||
|
||||
@@ -146,7 +146,7 @@ pub(crate) async fn set_profile_key_route(
|
||||
)));
|
||||
}
|
||||
|
||||
let Some(profile_key_value) = body.kv_pair.get(&body.key_name) else {
|
||||
let Some(profile_key_value) = body.kv_pair.get(&body.key) else {
|
||||
return Err!(Request(BadJson(
|
||||
"The key does not match the URL field key, or JSON body is empty (use DELETE)"
|
||||
)));
|
||||
@@ -164,7 +164,7 @@ pub(crate) async fn set_profile_key_route(
|
||||
return Err!(Request(BadJson("Key names cannot be longer than 128 bytes")));
|
||||
}
|
||||
|
||||
if body.key_name == "displayname" {
|
||||
if body.key == "displayname" {
|
||||
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
@@ -180,7 +180,7 @@ pub(crate) async fn set_profile_key_route(
|
||||
&all_joined_rooms,
|
||||
)
|
||||
.await;
|
||||
} else if body.key_name == "avatar_url" {
|
||||
} else if body.key == "avatar_url" {
|
||||
let mxc = ruma::OwnedMxcUri::from(profile_key_value.to_string());
|
||||
|
||||
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||
@@ -193,11 +193,9 @@ pub(crate) async fn set_profile_key_route(
|
||||
|
||||
update_avatar_url(&services, &body.user_id, Some(mxc), None, &all_joined_rooms).await;
|
||||
} else {
|
||||
services.users.set_profile_key(
|
||||
&body.user_id,
|
||||
&body.key_name,
|
||||
Some(profile_key_value.clone()),
|
||||
);
|
||||
services
|
||||
.users
|
||||
.set_profile_key(&body.user_id, &body.key, Some(profile_key_value.clone()));
|
||||
}
|
||||
|
||||
if services.config.allow_local_presence {
|
||||
@@ -233,7 +231,7 @@ pub(crate) async fn delete_profile_key_route(
|
||||
)));
|
||||
}
|
||||
|
||||
if body.key_name == "displayname" {
|
||||
if body.key == "displayname" {
|
||||
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
@@ -243,7 +241,7 @@ pub(crate) async fn delete_profile_key_route(
|
||||
.await;
|
||||
|
||||
update_displayname(&services, &body.user_id, None, &all_joined_rooms).await;
|
||||
} else if body.key_name == "avatar_url" {
|
||||
} else if body.key == "avatar_url" {
|
||||
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
@@ -256,7 +254,7 @@ pub(crate) async fn delete_profile_key_route(
|
||||
} else {
|
||||
services
|
||||
.users
|
||||
.set_profile_key(&body.user_id, &body.key_name, None);
|
||||
.set_profile_key(&body.user_id, &body.key, None);
|
||||
}
|
||||
|
||||
if services.config.allow_local_presence {
|
||||
@@ -379,14 +377,12 @@ pub(crate) async fn get_profile_key_route(
|
||||
.users
|
||||
.set_timezone(&body.user_id, response.tz.clone());
|
||||
|
||||
match response.custom_profile_fields.get(&body.key_name) {
|
||||
match response.custom_profile_fields.get(&body.key) {
|
||||
| Some(value) => {
|
||||
profile_key_value.insert(body.key_name.clone(), value.clone());
|
||||
services.users.set_profile_key(
|
||||
&body.user_id,
|
||||
&body.key_name,
|
||||
Some(value.clone()),
|
||||
);
|
||||
profile_key_value.insert(body.key.clone(), value.clone());
|
||||
services
|
||||
.users
|
||||
.set_profile_key(&body.user_id, &body.key, Some(value.clone()));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotFound("The requested profile key does not exist.")));
|
||||
@@ -409,11 +405,11 @@ pub(crate) async fn get_profile_key_route(
|
||||
|
||||
match services
|
||||
.users
|
||||
.profile_key(&body.user_id, &body.key_name)
|
||||
.profile_key(&body.user_id, &body.key)
|
||||
.await
|
||||
{
|
||||
| Ok(value) => {
|
||||
profile_key_value.insert(body.key_name.clone(), value);
|
||||
profile_key_value.insert(body.key.clone(), value);
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotFound("The requested profile key does not exist.")));
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use axum::{Json, extract::State, response::IntoResponse};
|
||||
use ruma::api::client::{
|
||||
discovery::{
|
||||
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
|
||||
discover_support::{self, Contact},
|
||||
},
|
||||
error::ErrorKind,
|
||||
use ruma::api::client::discovery::{
|
||||
discover_homeserver::{self, HomeserverInfo},
|
||||
discover_support::{self, Contact},
|
||||
};
|
||||
use tuwunel_core::{Error, Result};
|
||||
use tuwunel_core::{Err, Result};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -19,13 +16,12 @@ pub(crate) async fn well_known_client(
|
||||
) -> Result<discover_homeserver::Response> {
|
||||
let client_url = match services.server.config.well_known.client.as_ref() {
|
||||
| Some(url) => url.to_string(),
|
||||
| None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
|
||||
| None => return Err!(Request(NotFound("Not found."))),
|
||||
};
|
||||
|
||||
Ok(discover_homeserver::Response {
|
||||
homeserver: HomeserverInfo { base_url: client_url.clone() },
|
||||
homeserver: HomeserverInfo { base_url: client_url },
|
||||
identity_server: None,
|
||||
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
|
||||
tile_server: None,
|
||||
})
|
||||
}
|
||||
@@ -54,7 +50,7 @@ pub(crate) async fn well_known_support(
|
||||
|
||||
// support page or role must be either defined for this to be valid
|
||||
if support_page.is_none() && role.is_none() {
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
|
||||
return Err!(Request(NotFound("Not found.")));
|
||||
}
|
||||
|
||||
let email_address = services
|
||||
@@ -63,6 +59,7 @@ pub(crate) async fn well_known_support(
|
||||
.well_known
|
||||
.support_email
|
||||
.clone();
|
||||
|
||||
let matrix_id = services
|
||||
.server
|
||||
.config
|
||||
@@ -72,7 +69,7 @@ pub(crate) async fn well_known_support(
|
||||
|
||||
// if a role is specified, an email address or matrix id is required
|
||||
if role.is_some() && (email_address.is_none() && matrix_id.is_none()) {
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
|
||||
return Err!(Request(NotFound("Not found.")));
|
||||
}
|
||||
|
||||
// TODO: support defining multiple contacts in the config
|
||||
@@ -86,7 +83,7 @@ pub(crate) async fn well_known_support(
|
||||
|
||||
// support page or role+contacts must be either defined for this to be valid
|
||||
if contacts.is_empty() && support_page.is_none() {
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
|
||||
return Err!(Request(NotFound("Not found.")));
|
||||
}
|
||||
|
||||
Ok(discover_support::Response { contacts, support_page })
|
||||
@@ -103,7 +100,7 @@ pub(crate) async fn syncv3_client_server_json(
|
||||
| Some(url) => url.to_string(),
|
||||
| None => match services.server.config.well_known.server.as_ref() {
|
||||
| Some(url) => url.to_string(),
|
||||
| None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
|
||||
| None => return Err!(Request(NotFound("Not found."))),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![type_length_limit = "65536"] //TODO: reduce me
|
||||
#![type_length_limit = "262144"] //TODO: REDUCE ME
|
||||
#![allow(clippy::toplevel_ref_arg)]
|
||||
|
||||
pub mod client;
|
||||
|
||||
@@ -69,7 +69,10 @@ pub(super) async fn auth(
|
||||
json_body: Option<&CanonicalJsonValue>,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Auth> {
|
||||
use AuthScheme::{AccessToken, AccessTokenOptional, AppserviceToken, ServerSignatures};
|
||||
use AuthScheme::{
|
||||
AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional,
|
||||
ServerSignatures,
|
||||
};
|
||||
use Error::BadRequest;
|
||||
use ErrorKind::UnknownToken;
|
||||
use Token::{Appservice, Expired, Invalid, User};
|
||||
@@ -129,7 +132,7 @@ pub(super) async fn auth(
|
||||
|
||||
| (AccessToken, Appservice(info)) => Ok(auth_appservice(services, request, info).await?),
|
||||
|
||||
| (AccessToken, Token::None) => match metadata {
|
||||
| (AccessToken | AppserviceToken, Token::None) => match metadata {
|
||||
| &get_turn_server_info::v3::Request::METADATA
|
||||
if services.server.config.turn_allow_guests =>
|
||||
Ok(Auth::default()),
|
||||
@@ -137,22 +140,25 @@ pub(super) async fn auth(
|
||||
| _ => Err!(Request(MissingToken("Missing access token."))),
|
||||
},
|
||||
|
||||
| (AccessToken | AccessTokenOptional | AuthScheme::None, User(user)) => Ok(Auth {
|
||||
| (
|
||||
AccessToken | AccessTokenOptional | AppserviceTokenOptional | AuthScheme::None,
|
||||
User(user),
|
||||
) => Ok(Auth {
|
||||
sender_user: Some(user.0),
|
||||
sender_device: Some(user.1),
|
||||
_expires_at: user.2,
|
||||
..Auth::default()
|
||||
}),
|
||||
|
||||
//TODO: add AppserviceTokenOptional
|
||||
| (AccessTokenOptional | AppserviceToken | AuthScheme::None, Appservice(info)) =>
|
||||
Ok(Auth {
|
||||
appservice_info: Some(*info),
|
||||
..Auth::default()
|
||||
}),
|
||||
| (
|
||||
AccessTokenOptional | AppserviceTokenOptional | AppserviceToken | AuthScheme::None,
|
||||
Appservice(info),
|
||||
) => Ok(Auth {
|
||||
appservice_info: Some(*info),
|
||||
..Auth::default()
|
||||
}),
|
||||
|
||||
//TODO: add AppserviceTokenOptional
|
||||
| (AccessTokenOptional | AppserviceToken | AuthScheme::None, Token::None) =>
|
||||
| (AccessTokenOptional | AppserviceTokenOptional | AuthScheme::None, Token::None) =>
|
||||
Ok(Auth::default()),
|
||||
}
|
||||
}
|
||||
@@ -306,7 +312,7 @@ async fn auth_server(
|
||||
|
||||
let keys: PubKeys = [(x_matrix.key.to_string(), key.key)].into();
|
||||
let keys: PubKeyMap = [(origin.as_str().into(), keys)].into();
|
||||
if let Err(e) = ruma::signatures::verify_json(&keys, authorization) {
|
||||
if let Err(e) = ruma::signatures::verify_json(&keys, &authorization) {
|
||||
debug_error!("Failed to verify federation request from {origin}: {e}");
|
||||
if request.parts.uri.to_string().contains('@') {
|
||||
warn!(
|
||||
|
||||
@@ -64,17 +64,18 @@ pub(crate) async fn get_hierarchy_route(
|
||||
})
|
||||
.unzip()
|
||||
.map(|(children, inaccessible_children): (Vec<_>, Vec<_>)| {
|
||||
(
|
||||
children
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
inaccessible_children
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
)
|
||||
let children = children
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|parent| parent.summary)
|
||||
.collect();
|
||||
|
||||
let inaccessible_children = inaccessible_children
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
(children, inaccessible_children)
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -3,12 +3,15 @@ use axum_client_ip::InsecureClientIp;
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use ruma::{
|
||||
CanonicalJsonValue, OwnedUserId, UserId,
|
||||
api::{client::error::ErrorKind, federation::membership::create_invite},
|
||||
api::{
|
||||
client::error::ErrorKind,
|
||||
federation::membership::{RawStrippedState, create_invite},
|
||||
},
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
serde::JsonObject,
|
||||
};
|
||||
use tuwunel_core::{
|
||||
Err, Error, Result, err,
|
||||
Err, Error, Result, err, extract_variant,
|
||||
matrix::{Event, PduEvent, event::gen_event_id},
|
||||
utils,
|
||||
utils::hash::sha256,
|
||||
@@ -119,7 +122,12 @@ pub(crate) async fn create_invite_route(
|
||||
return Err!(Request(Forbidden("This server does not allow room invites.")));
|
||||
}
|
||||
|
||||
let mut invite_state = body.invite_room_state.clone();
|
||||
let mut invite_state: Vec<_> = body
|
||||
.invite_room_state
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
|
||||
.collect();
|
||||
|
||||
let mut event: JsonObject = serde_json::from_str(body.event.get())
|
||||
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
|
||||
|
||||
@@ -2,7 +2,7 @@ use RoomVersionId::*;
|
||||
use axum::extract::State;
|
||||
use ruma::{
|
||||
RoomVersionId,
|
||||
api::{client::error::ErrorKind, federation::knock::create_knock_event_template},
|
||||
api::{client::error::ErrorKind, federation::membership::prepare_knock_event},
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
@@ -15,8 +15,8 @@ use crate::Ruma;
|
||||
/// Creates a knock template.
|
||||
pub(crate) async fn create_knock_event_template_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<create_knock_event_template::v1::Request>,
|
||||
) -> Result<create_knock_event_template::v1::Response> {
|
||||
body: Ruma<prepare_knock_event::v1::Request>,
|
||||
) -> Result<prepare_knock_event::v1::Response> {
|
||||
if !services
|
||||
.rooms
|
||||
.metadata
|
||||
@@ -124,7 +124,7 @@ pub(crate) async fn create_knock_event_template_route(
|
||||
// room v3 and above removed the "event_id" field from remote PDU format
|
||||
super::maybe_strip_event_id(&mut pdu_json, &room_version_id)?;
|
||||
|
||||
Ok(create_knock_event_template::v1::Response {
|
||||
Ok(prepare_knock_event::v1::Response {
|
||||
room_version: room_version_id,
|
||||
event: to_raw_value(&pdu_json).expect("CanonicalJson can be serialized to JSON"),
|
||||
})
|
||||
|
||||
@@ -354,6 +354,7 @@ pub(crate) async fn create_join_event_v2_route(
|
||||
create_join_event(&services, body.origin(), &body.room_id, &body.pdu)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
let room_state = create_join_event::v2::RoomState {
|
||||
members_omitted: false,
|
||||
auth_chain,
|
||||
|
||||
@@ -3,7 +3,7 @@ use futures::FutureExt;
|
||||
use ruma::{
|
||||
OwnedServerName, OwnedUserId,
|
||||
RoomVersionId::*,
|
||||
api::federation::knock::send_knock,
|
||||
api::federation::membership::create_knock_event,
|
||||
events::{
|
||||
StateEventType,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
@@ -23,8 +23,8 @@ use crate::Ruma;
|
||||
/// Submits a signed knock event.
|
||||
pub(crate) async fn create_knock_event_v1_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<send_knock::v1::Request>,
|
||||
) -> Result<send_knock::v1::Response> {
|
||||
body: Ruma<create_knock_event::v1::Request>,
|
||||
) -> Result<create_knock_event::v1::Response> {
|
||||
if services
|
||||
.config
|
||||
.forbidden_remote_server_names
|
||||
@@ -189,7 +189,14 @@ pub(crate) async fn create_knock_event_v1_route(
|
||||
.send_pdu_room(&body.room_id, &pdu_id)
|
||||
.await?;
|
||||
|
||||
let knock_room_state = services.rooms.state.summary_stripped(&pdu).await;
|
||||
|
||||
Ok(send_knock::v1::Response { knock_room_state })
|
||||
Ok(create_knock_event::v1::Response {
|
||||
knock_room_state: services
|
||||
.rooms
|
||||
.state
|
||||
.summary_stripped(&pdu)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user