initial implementation of banning room IDs
takes a full room ID, evicts all our users from that room, adds room ID to banned room IDs metadata db table, and forbids any new local users from attempting to join it. Signed-off-by: strawberry <strawberry@puppygock.gay>
This commit is contained in:
parent
a92f291bbf
commit
ed0c8e86f7
6 changed files with 189 additions and 3 deletions
|
@ -49,6 +49,13 @@ pub async fn join_room_by_id_route(
|
||||||
) -> Result<join_room_by_id::v3::Response> {
|
) -> Result<join_room_by_id::v3::Response> {
|
||||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
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
|
let mut servers = Vec::new(); // There is no body.server_name for /roomId/join
|
||||||
servers.extend(
|
servers.extend(
|
||||||
services()
|
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) {
|
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias) {
|
||||||
Ok(room_id) => {
|
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();
|
let mut servers = body.server_name.clone();
|
||||||
servers.extend(
|
servers.extend(
|
||||||
services()
|
services()
|
||||||
|
@ -112,6 +126,13 @@ pub async fn join_room_by_id_or_alias_route(
|
||||||
Err(room_alias) => {
|
Err(room_alias) => {
|
||||||
let response = get_alias_helper(room_alias).await?;
|
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)
|
(response.servers, response.room_id)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use ruma::{OwnedRoomId, RoomId};
|
use ruma::{OwnedRoomId, RoomId};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{database::KeyValueDatabase, service, services, utils, Error, Result};
|
use crate::{database::KeyValueDatabase, service, services, utils, Error, Result};
|
||||||
|
|
||||||
|
@ -42,4 +43,37 @@ impl service::rooms::metadata::Data for KeyValueDatabase {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_banned(&self, room_id: &RoomId) -> Result<bool> {
|
||||||
|
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<dyn Iterator<Item = Result<OwnedRoomId>> + '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)
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,8 @@ pub struct KeyValueDatabase {
|
||||||
|
|
||||||
pub(super) disabledroomids: Arc<dyn KvTree>, // Rooms where incoming federation handling is disabled
|
pub(super) disabledroomids: Arc<dyn KvTree>, // Rooms where incoming federation handling is disabled
|
||||||
|
|
||||||
|
pub(super) bannedroomids: Arc<dyn KvTree>, // Rooms where local users are not allowed to join
|
||||||
|
|
||||||
pub(super) lazyloadedids: Arc<dyn KvTree>, // LazyLoadedIds = UserId + DeviceId + RoomId + LazyLoadedUserId
|
pub(super) lazyloadedids: Arc<dyn KvTree>, // LazyLoadedIds = UserId + DeviceId + RoomId + LazyLoadedUserId
|
||||||
|
|
||||||
pub(super) userroomid_notificationcount: Arc<dyn KvTree>, // NotifyCount = u64
|
pub(super) userroomid_notificationcount: Arc<dyn KvTree>, // NotifyCount = u64
|
||||||
|
@ -301,6 +303,8 @@ impl KeyValueDatabase {
|
||||||
|
|
||||||
disabledroomids: builder.open_tree("disabledroomids")?,
|
disabledroomids: builder.open_tree("disabledroomids")?,
|
||||||
|
|
||||||
|
bannedroomids: builder.open_tree("bannedroomids")?,
|
||||||
|
|
||||||
lazyloadedids: builder.open_tree("lazyloadedids")?,
|
lazyloadedids: builder.open_tree("lazyloadedids")?,
|
||||||
|
|
||||||
userroomid_notificationcount: builder.open_tree("userroomid_notificationcount")?,
|
userroomid_notificationcount: builder.open_tree("userroomid_notificationcount")?,
|
||||||
|
|
|
@ -27,14 +27,15 @@ use ruma::{
|
||||||
},
|
},
|
||||||
TimelineEventType,
|
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 serde_json::value::to_raw_value;
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
use tracing::warn;
|
use tracing::{debug, error, warn};
|
||||||
|
|
||||||
use crate::{
|
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,
|
services,
|
||||||
utils::{self, HtmlEscape},
|
utils::{self, HtmlEscape},
|
||||||
Error, PduEvent, Result,
|
Error, PduEvent, Result,
|
||||||
|
@ -165,6 +166,20 @@ enum RoomCommand {
|
||||||
/// - List all rooms the server knows about
|
/// - List all rooms the server knows about
|
||||||
List { page: Option<usize> },
|
List { page: Option<usize> },
|
||||||
|
|
||||||
|
/// - 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<RoomId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - Unbans a room ID to allow local users to join again
|
||||||
|
UnbanRoomId { room_id: Box<RoomId> },
|
||||||
|
|
||||||
|
/// - List of all rooms we have banned
|
||||||
|
ListBannedRooms,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
/// - Manage rooms' aliases
|
/// - Manage rooms' aliases
|
||||||
Alias(RoomAliasCommand),
|
Alias(RoomAliasCommand),
|
||||||
|
@ -774,6 +789,103 @@ impl Service {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
AdminCommand::Rooms(command) => match command {
|
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::<Vec<OwnedUserId>>()
|
||||||
|
{
|
||||||
|
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::<Vec<OwnedUserId>>()
|
||||||
|
{
|
||||||
|
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<Vec<_>, _> =
|
||||||
|
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,
|
||||||
|
"<li><code>{}</code></li>",
|
||||||
|
escape_html(room_id.as_ref())
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
output
|
||||||
|
});
|
||||||
|
|
||||||
|
let plain = format!("Rooms:\n{}", plain_list);
|
||||||
|
let html = format!("Rooms:\n<ul>{}</ul>", 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 } => {
|
RoomCommand::List { page } => {
|
||||||
// TODO: i know there's a way to do this with clap, but i can't seem to find it
|
// 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);
|
let page = page.unwrap_or(1);
|
||||||
|
|
|
@ -6,4 +6,7 @@ pub trait Data: Send + Sync {
|
||||||
fn iter_ids<'a>(&'a self) -> Box<dyn Iterator<Item = Result<OwnedRoomId>> + 'a>;
|
fn iter_ids<'a>(&'a self) -> Box<dyn Iterator<Item = Result<OwnedRoomId>> + 'a>;
|
||||||
fn is_disabled(&self, room_id: &RoomId) -> Result<bool>;
|
fn is_disabled(&self, room_id: &RoomId) -> Result<bool>;
|
||||||
fn disable_room(&self, room_id: &RoomId, disabled: bool) -> Result<()>;
|
fn disable_room(&self, room_id: &RoomId, disabled: bool) -> Result<()>;
|
||||||
|
fn is_banned(&self, room_id: &RoomId) -> Result<bool>;
|
||||||
|
fn ban_room(&self, room_id: &RoomId, banned: bool) -> Result<()>;
|
||||||
|
fn list_banned_rooms<'a>(&'a self) -> Box<dyn Iterator<Item = Result<OwnedRoomId>> + 'a>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,4 +27,16 @@ impl Service {
|
||||||
pub fn disable_room(&self, room_id: &RoomId, disabled: bool) -> Result<()> {
|
pub fn disable_room(&self, room_id: &RoomId, disabled: bool) -> Result<()> {
|
||||||
self.db.disable_room(room_id, disabled)
|
self.db.disable_room(room_id, disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_banned(&self, room_id: &RoomId) -> Result<bool> {
|
||||||
|
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<dyn Iterator<Item = Result<OwnedRoomId>> + 'a> {
|
||||||
|
self.db.list_banned_rooms()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue