diff --git a/src/api/client_server/membership.rs b/src/api/client_server/membership.rs index 054d151b..bb9d9a31 100644 --- a/src/api/client_server/membership.rs +++ b/src/api/client_server/membership.rs @@ -49,6 +49,13 @@ pub async fn join_room_by_id_route( ) -> Result { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); + if services().rooms.metadata.is_banned(&body.room_id)? { + return Err(Error::BadRequest( + ErrorKind::Forbidden, + "This room is banned on this homeserver.", + )); + } + let mut servers = Vec::new(); // There is no body.server_name for /roomId/join servers.extend( services() @@ -90,6 +97,13 @@ pub async fn join_room_by_id_or_alias_route( let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias) { Ok(room_id) => { + if services().rooms.metadata.is_banned(&room_id)? { + return Err(Error::BadRequest( + ErrorKind::Forbidden, + "This room is banned on this homeserver.", + )); + } + let mut servers = body.server_name.clone(); servers.extend( services() @@ -112,6 +126,13 @@ pub async fn join_room_by_id_or_alias_route( Err(room_alias) => { let response = get_alias_helper(room_alias).await?; + if services().rooms.metadata.is_banned(&response.room_id)? { + return Err(Error::BadRequest( + ErrorKind::Forbidden, + "This room is banned on this homeserver.", + )); + } + (response.servers, response.room_id) } }; diff --git a/src/database/key_value/rooms/metadata.rs b/src/database/key_value/rooms/metadata.rs index 57540c40..a97878e1 100644 --- a/src/database/key_value/rooms/metadata.rs +++ b/src/database/key_value/rooms/metadata.rs @@ -1,4 +1,5 @@ use ruma::{OwnedRoomId, RoomId}; +use tracing::error; use crate::{database::KeyValueDatabase, service, services, utils, Error, Result}; @@ -42,4 +43,37 @@ impl service::rooms::metadata::Data for KeyValueDatabase { Ok(()) } + + fn is_banned(&self, room_id: &RoomId) -> Result { + Ok(self.bannedroomids.get(room_id.as_bytes())?.is_some()) + } + + fn ban_room(&self, room_id: &RoomId, banned: bool) -> Result<()> { + if banned { + self.bannedroomids.insert(room_id.as_bytes(), &[])?; + } else { + self.bannedroomids.remove(room_id.as_bytes())?; + } + + Ok(()) + } + + fn list_banned_rooms<'a>(&'a self) -> Box> + 'a> { + Box::new(self.bannedroomids.iter().map( + |(room_id_bytes, _ /* non-banned rooms should not be in this table */)| { + let room_id = utils::string_from_bytes(&room_id_bytes) + .map_err(|e| { + error!("Invalid room_id bytes in bannedroomids: {e}"); + Error::bad_database("Invalid room_id in bannedroomids.") + })? + .try_into() + .map_err(|e| { + error!("Invalid room_id in bannedroomids: {e}"); + Error::bad_database("Invalid room_id in bannedroomids") + })?; + + Ok(room_id) + }, + )) + } } diff --git a/src/database/mod.rs b/src/database/mod.rs index d3edf133..26e9ea31 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -105,6 +105,8 @@ pub struct KeyValueDatabase { pub(super) disabledroomids: Arc, // Rooms where incoming federation handling is disabled + pub(super) bannedroomids: Arc, // Rooms where local users are not allowed to join + pub(super) lazyloadedids: Arc, // LazyLoadedIds = UserId + DeviceId + RoomId + LazyLoadedUserId pub(super) userroomid_notificationcount: Arc, // NotifyCount = u64 @@ -301,6 +303,8 @@ impl KeyValueDatabase { disabledroomids: builder.open_tree("disabledroomids")?, + bannedroomids: builder.open_tree("bannedroomids")?, + lazyloadedids: builder.open_tree("lazyloadedids")?, userroomid_notificationcount: builder.open_tree("userroomid_notificationcount")?, diff --git a/src/service/admin/mod.rs b/src/service/admin/mod.rs index 7983cd36..84fb80a8 100644 --- a/src/service/admin/mod.rs +++ b/src/service/admin/mod.rs @@ -27,14 +27,15 @@ use ruma::{ }, TimelineEventType, }, - EventId, OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId, RoomVersionId, ServerName, UserId, + EventId, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, + ServerName, UserId, }; use serde_json::value::to_raw_value; use tokio::sync::{mpsc, Mutex}; -use tracing::warn; +use tracing::{debug, error, warn}; use crate::{ - api::client_server::{leave_all_rooms, AUTO_GEN_PASSWORD_LENGTH}, + api::client_server::{leave_all_rooms, leave_room, AUTO_GEN_PASSWORD_LENGTH}, services, utils::{self, HtmlEscape}, Error, PduEvent, Result, @@ -165,6 +166,20 @@ enum RoomCommand { /// - List all rooms the server knows about List { page: Option }, + /// - Bans a room ID from local users joining and evicts all our local users from the room + BanRoomId { + #[arg(short, long)] + force: bool, + + room_id: Box, + }, + + /// - Unbans a room ID to allow local users to join again + UnbanRoomId { room_id: Box }, + + /// - List of all rooms we have banned + ListBannedRooms, + #[command(subcommand)] /// - Manage rooms' aliases Alias(RoomAliasCommand), @@ -774,6 +789,103 @@ impl Service { } }, AdminCommand::Rooms(command) => match command { + RoomCommand::BanRoomId { force, room_id } => { + // basic syntax checks on room ID + if !&room_id.to_string().starts_with('!') + || !&room_id.to_string().contains(':') + || room_id.to_string().contains(char::is_whitespace) + { + return Ok(RoomMessageEventContent::text_plain("Invalid room ID specified. Please note that this requires a full room ID e.g. `!awIh6gGInaS5wLQJwa:example.com`")); + } + + services().rooms.metadata.ban_room(&room_id, true)?; + + debug!("Making all users leave the room {}", &room_id); + if force { + for local_user in services() + .rooms + .state_cache + .room_members(&room_id) + .filter_map(|user| { + user.ok().filter(|local_user| { + local_user.server_name() == services().globals.server_name() + }) + }) + .collect::>() + { + debug!( + "Attempting leave for user {} in room {} (forced, ignoring all errors)", + &local_user, &room_id + ); + let _ = leave_room(&local_user, &room_id, None).await; + } + } else { + for local_user in services() + .rooms + .state_cache + .room_members(&room_id) + .filter_map(|user| { + user.ok().filter(|local_user| { + local_user.server_name() == services().globals.server_name() + }) + }) + .collect::>() + { + debug!( + "Attempting leave for user {} in room {}", + &local_user, &room_id + ); + if let Err(e) = leave_room(&local_user, &room_id, None).await { + error!("Error attempting to make local user {} leave room {} during room banning: {}", &local_user, &room_id, e); + return Ok(RoomMessageEventContent::text_plain(format!("Error attempting to make local user {} leave room {} during room banning (room is still banned but not removing any more users): {}\nIf you would like to ignore errors, use --force", &local_user, &room_id, e))); + } + } + } + + RoomMessageEventContent::text_plain("Room banned and removed all our local users, use disable-room to stop receiving new inbound federation events as well if needed.") + } + RoomCommand::UnbanRoomId { room_id } => { + services().rooms.metadata.ban_room(&room_id, false)?; + RoomMessageEventContent::text_plain("Room unbanned, you may need to re-enable federation with the room using enable-room if this is a remote room to make it fully functional.") + } + RoomCommand::ListBannedRooms => { + let rooms: Result, _> = + services().rooms.metadata.list_banned_rooms().collect(); + + match rooms { + Ok(room_ids) => { + // TODO: add room name from our state cache if available, default to the room ID as the room name if we dont have it + // TODO: do same if we have a room alias for this + let plain_list = + room_ids.iter().fold(String::new(), |mut output, room_id| { + writeln!(output, "- `{}`", room_id).unwrap(); + output + }); + + let html_list = + room_ids.iter().fold(String::new(), |mut output, room_id| { + writeln!( + output, + "
  • {}
  • ", + escape_html(room_id.as_ref()) + ) + .unwrap(); + output + }); + + let plain = format!("Rooms:\n{}", plain_list); + let html = format!("Rooms:\n
      {}
    ", html_list); + RoomMessageEventContent::text_html(plain, html) + } + Err(e) => { + error!("Failed to list banned rooms: {}", e); + RoomMessageEventContent::text_plain(format!( + "Unable to list room aliases: {}", + e + )) + } + } + } RoomCommand::List { page } => { // TODO: i know there's a way to do this with clap, but i can't seem to find it let page = page.unwrap_or(1); diff --git a/src/service/rooms/metadata/data.rs b/src/service/rooms/metadata/data.rs index 339db573..7c7e10be 100644 --- a/src/service/rooms/metadata/data.rs +++ b/src/service/rooms/metadata/data.rs @@ -6,4 +6,7 @@ pub trait Data: Send + Sync { fn iter_ids<'a>(&'a self) -> Box> + 'a>; fn is_disabled(&self, room_id: &RoomId) -> Result; fn disable_room(&self, room_id: &RoomId, disabled: bool) -> Result<()>; + fn is_banned(&self, room_id: &RoomId) -> Result; + fn ban_room(&self, room_id: &RoomId, banned: bool) -> Result<()>; + fn list_banned_rooms<'a>(&'a self) -> Box> + 'a>; } diff --git a/src/service/rooms/metadata/mod.rs b/src/service/rooms/metadata/mod.rs index d1884691..69ae6dbc 100644 --- a/src/service/rooms/metadata/mod.rs +++ b/src/service/rooms/metadata/mod.rs @@ -27,4 +27,16 @@ impl Service { pub fn disable_room(&self, room_id: &RoomId, disabled: bool) -> Result<()> { self.db.disable_room(room_id, disabled) } + + pub fn is_banned(&self, room_id: &RoomId) -> Result { + self.db.is_banned(room_id) + } + + pub fn ban_room(&self, room_id: &RoomId, banned: bool) -> Result<()> { + self.db.ban_room(room_id, banned) + } + + pub fn list_banned_rooms<'a>(&'a self) -> Box> + 'a> { + self.db.list_banned_rooms() + } }