From 0eb9e4f3d2284a9c96b4c781e25328f1a6e9f9e2 Mon Sep 17 00:00:00 2001
From: Jade Ellis <jade@ellis.link>
Date: Sat, 19 Apr 2025 23:02:43 +0100
Subject: [PATCH] refactor: Centralize server forbidden checks into moderation
 module

This moves all checks related to `forbidden_remote_server_names`,
`forbidden_remote_room_directory_server_names` and
`prevent_media_downloads_from` to a new `moderation` module.
This is useful for implementing more complicated logic globally.
Mostly the changes from #673, but is also relevant for #750
---
 conduwuit-example.toml            |  3 ++
 src/api/client/directory.rs       | 19 ++--------
 src/api/client/membership.rs      |  5 +--
 src/api/client/message.rs         |  5 +--
 src/api/router/auth.rs            |  8 +---
 src/api/server/invite.rs          | 11 ++----
 src/api/server/make_join.rs       | 11 ++----
 src/api/server/make_knock.rs      | 11 ++----
 src/api/server/send_join.rs       | 22 +++--------
 src/api/server/send_knock.rs      | 11 ++----
 src/core/config/mod.rs            |  5 ++-
 src/service/federation/execute.rs |  8 +---
 src/service/federation/mod.rs     |  4 +-
 src/service/media/mod.rs          |  4 +-
 src/service/media/remote.rs       | 12 +-----
 src/service/mod.rs                |  1 +
 src/service/moderation.rs         | 62 +++++++++++++++++++++++++++++++
 src/service/services.rs           |  4 +-
 18 files changed, 109 insertions(+), 97 deletions(-)
 create mode 100644 src/service/moderation.rs

diff --git a/conduwuit-example.toml b/conduwuit-example.toml
index af8da6bb..5a4b7b3f 100644
--- a/conduwuit-example.toml
+++ b/conduwuit-example.toml
@@ -1197,6 +1197,9 @@
 # incoming AND outgoing federation with, and block client room joins /
 # remote user invites.
 #
+# Additionally, it will hide messages from these servers for all users
+# on this server.
+#
 # This check is applied on the room ID, room alias, sender server name,
 # sender user's server name, inbound federation X-Matrix origin, and
 # outbound federation handler.
diff --git a/src/api/client/directory.rs b/src/api/client/directory.rs
index b44b9f64..aa6ae168 100644
--- a/src/api/client/directory.rs
+++ b/src/api/client/directory.rs
@@ -52,13 +52,8 @@ pub(crate) async fn get_public_rooms_filtered_route(
 ) -> Result<get_public_rooms_filtered::v3::Response> {
 	if let Some(server) = &body.server {
 		if services
-			.config
-			.forbidden_remote_room_directory_server_names
-			.is_match(server.host())
-			|| services
-				.config
-				.forbidden_remote_server_names
-				.is_match(server.host())
+			.moderation
+			.is_remote_server_room_directory_forbidden(server)
 		{
 			return Err!(Request(Forbidden("Server is banned on this homeserver.")));
 		}
@@ -92,15 +87,7 @@ pub(crate) async fn get_public_rooms_route(
 	body: Ruma<get_public_rooms::v3::Request>,
 ) -> Result<get_public_rooms::v3::Response> {
 	if let Some(server) = &body.server {
-		if services
-			.config
-			.forbidden_remote_room_directory_server_names
-			.is_match(server.host())
-			|| services
-				.config
-				.forbidden_remote_server_names
-				.is_match(server.host())
-		{
+		if services.moderation.is_remote_server_forbidden(server) {
 			return Err!(Request(Forbidden("Server is banned on this homeserver.")));
 		}
 	}
diff --git a/src/api/client/membership.rs b/src/api/client/membership.rs
index 18a1c741..b1b85b81 100644
--- a/src/api/client/membership.rs
+++ b/src/api/client/membership.rs
@@ -83,9 +83,8 @@ 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())
+				.moderation
+				.is_remote_server_forbidden(room_id.server_name().expect("legacy room mxid"))
 		{
 			warn!(
 				"User {user_id} who is not an admin attempted to send an invite for or \
diff --git a/src/api/client/message.rs b/src/api/client/message.rs
index 9c2c4057..08887e18 100644
--- a/src/api/client/message.rs
+++ b/src/api/client/message.rs
@@ -274,9 +274,8 @@ pub(crate) async fn is_ignored_pdu(
 	let ignored_type = IGNORED_MESSAGE_TYPES.binary_search(&pdu.kind).is_ok();
 
 	let ignored_server = services
-		.config
-		.forbidden_remote_server_names
-		.is_match(pdu.sender().server_name().host());
+		.moderation
+		.is_remote_server_forbidden(pdu.sender().server_name());
 
 	if ignored_type
 		&& (ignored_server || services.users.user_is_ignored(&pdu.sender, user_id).await)
diff --git a/src/api/router/auth.rs b/src/api/router/auth.rs
index 0eb61ca6..01254c32 100644
--- a/src/api/router/auth.rs
+++ b/src/api/router/auth.rs
@@ -306,7 +306,7 @@ async fn auth_server(
 }
 
 fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result<()> {
-	if !services.server.config.allow_federation {
+	if !services.config.allow_federation {
 		return Err!(Config("allow_federation", "Federation is disabled."));
 	}
 
@@ -316,11 +316,7 @@ fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result<()> {
 	}
 
 	let origin = &x_matrix.origin;
-	if services
-		.config
-		.forbidden_remote_server_names
-		.is_match(origin.host())
-	{
+	if services.moderation.is_remote_server_forbidden(origin) {
 		return Err!(Request(Forbidden(debug_warn!(
 			"Federation requests from {origin} denied."
 		))));
diff --git a/src/api/server/invite.rs b/src/api/server/invite.rs
index edd6ac16..f53e1a15 100644
--- a/src/api/server/invite.rs
+++ b/src/api/server/invite.rs
@@ -37,19 +37,14 @@ pub(crate) async fn create_invite_route(
 	}
 
 	if let Some(server) = body.room_id.server_name() {
-		if services
-			.config
-			.forbidden_remote_server_names
-			.is_match(server.host())
-		{
+		if services.moderation.is_remote_server_forbidden(server) {
 			return Err!(Request(Forbidden("Server is banned on this homeserver.")));
 		}
 	}
 
 	if services
-		.config
-		.forbidden_remote_server_names
-		.is_match(body.origin().host())
+		.moderation
+		.is_remote_server_forbidden(body.origin())
 	{
 		warn!(
 			"Received federated/remote invite from banned server {} for room ID {}. Rejecting.",
diff --git a/src/api/server/make_join.rs b/src/api/server/make_join.rs
index ac2c5485..3204c30c 100644
--- a/src/api/server/make_join.rs
+++ b/src/api/server/make_join.rs
@@ -42,9 +42,8 @@ pub(crate) async fn create_join_event_template_route(
 		.await?;
 
 	if services
-		.config
-		.forbidden_remote_server_names
-		.is_match(body.origin().host())
+		.moderation
+		.is_remote_server_forbidden(body.origin())
 	{
 		warn!(
 			"Server {} for remote user {} tried joining room ID {} which has a server name that \
@@ -57,11 +56,7 @@ pub(crate) async fn create_join_event_template_route(
 	}
 
 	if let Some(server) = body.room_id.server_name() {
-		if services
-			.config
-			.forbidden_remote_server_names
-			.is_match(server.host())
-		{
+		if services.moderation.is_remote_server_forbidden(server) {
 			return Err!(Request(Forbidden(warn!(
 				"Room ID server name {server} is banned on this homeserver."
 			))));
diff --git a/src/api/server/make_knock.rs b/src/api/server/make_knock.rs
index 511c13b2..423c8e81 100644
--- a/src/api/server/make_knock.rs
+++ b/src/api/server/make_knock.rs
@@ -33,9 +33,8 @@ pub(crate) async fn create_knock_event_template_route(
 		.await?;
 
 	if services
-		.config
-		.forbidden_remote_server_names
-		.is_match(body.origin().host())
+		.moderation
+		.is_remote_server_forbidden(body.origin())
 	{
 		warn!(
 			"Server {} for remote user {} tried knocking room ID {} which has a server name \
@@ -48,11 +47,7 @@ pub(crate) async fn create_knock_event_template_route(
 	}
 
 	if let Some(server) = body.room_id.server_name() {
-		if services
-			.config
-			.forbidden_remote_server_names
-			.is_match(server.host())
-		{
+		if services.moderation.is_remote_server_forbidden(server) {
 			return Err!(Request(Forbidden("Server is banned on this homeserver.")));
 		}
 	}
diff --git a/src/api/server/send_join.rs b/src/api/server/send_join.rs
index a66d8890..895eca81 100644
--- a/src/api/server/send_join.rs
+++ b/src/api/server/send_join.rs
@@ -268,9 +268,8 @@ pub(crate) async fn create_join_event_v1_route(
 	body: Ruma<create_join_event::v1::Request>,
 ) -> Result<create_join_event::v1::Response> {
 	if services
-		.config
-		.forbidden_remote_server_names
-		.is_match(body.origin().host())
+		.moderation
+		.is_remote_server_forbidden(body.origin())
 	{
 		warn!(
 			"Server {} tried joining room ID {} through us who has a server name that is \
@@ -282,11 +281,7 @@ pub(crate) async fn create_join_event_v1_route(
 	}
 
 	if let Some(server) = body.room_id.server_name() {
-		if services
-			.config
-			.forbidden_remote_server_names
-			.is_match(server.host())
-		{
+		if services.moderation.is_remote_server_forbidden(server) {
 			warn!(
 				"Server {} tried joining room ID {} through us which has a server name that is \
 				 globally forbidden. Rejecting.",
@@ -314,19 +309,14 @@ pub(crate) async fn create_join_event_v2_route(
 	body: Ruma<create_join_event::v2::Request>,
 ) -> Result<create_join_event::v2::Response> {
 	if services
-		.config
-		.forbidden_remote_server_names
-		.is_match(body.origin().host())
+		.moderation
+		.is_remote_server_forbidden(body.origin())
 	{
 		return Err!(Request(Forbidden("Server is banned on this homeserver.")));
 	}
 
 	if let Some(server) = body.room_id.server_name() {
-		if services
-			.config
-			.forbidden_remote_server_names
-			.is_match(server.host())
-		{
+		if services.moderation.is_remote_server_forbidden(server) {
 			warn!(
 				"Server {} tried joining room ID {} through us which has a server name that is \
 				 globally forbidden. Rejecting.",
diff --git a/src/api/server/send_knock.rs b/src/api/server/send_knock.rs
index ee7b6cba..8d3697d2 100644
--- a/src/api/server/send_knock.rs
+++ b/src/api/server/send_knock.rs
@@ -26,9 +26,8 @@ pub(crate) async fn create_knock_event_v1_route(
 	body: Ruma<send_knock::v1::Request>,
 ) -> Result<send_knock::v1::Response> {
 	if services
-		.config
-		.forbidden_remote_server_names
-		.is_match(body.origin().host())
+		.moderation
+		.is_remote_server_forbidden(body.origin())
 	{
 		warn!(
 			"Server {} tried knocking room ID {} who has a server name that is globally \
@@ -40,11 +39,7 @@ pub(crate) async fn create_knock_event_v1_route(
 	}
 
 	if let Some(server) = body.room_id.server_name() {
-		if services
-			.config
-			.forbidden_remote_server_names
-			.is_match(server.host())
-		{
+		if services.moderation.is_remote_server_forbidden(server) {
 			warn!(
 				"Server {} tried knocking room ID {} which has a server name that is globally \
 				 forbidden. Rejecting.",
diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs
index a7205423..2de3b710 100644
--- a/src/core/config/mod.rs
+++ b/src/core/config/mod.rs
@@ -1374,6 +1374,9 @@ pub struct Config {
 	/// incoming AND outgoing federation with, and block client room joins /
 	/// remote user invites.
 	///
+	/// Additionally, it will hide messages from these servers for all users
+	/// on this server.
+	///
 	/// This check is applied on the room ID, room alias, sender server name,
 	/// sender user's server name, inbound federation X-Matrix origin, and
 	/// outbound federation handler.
@@ -1954,7 +1957,7 @@ impl Config {
 		let mut addrs = Vec::with_capacity(
 			self.get_bind_hosts()
 				.len()
-				.saturating_add(self.get_bind_ports().len()),
+				.saturating_mul(self.get_bind_ports().len()),
 		);
 		for host in &self.get_bind_hosts() {
 			for port in &self.get_bind_ports() {
diff --git a/src/service/federation/execute.rs b/src/service/federation/execute.rs
index 97314ffb..1d1d1154 100644
--- a/src/service/federation/execute.rs
+++ b/src/service/federation/execute.rs
@@ -64,13 +64,7 @@ where
 		return Err!(Config("allow_federation", "Federation is disabled."));
 	}
 
-	if self
-		.services
-		.server
-		.config
-		.forbidden_remote_server_names
-		.is_match(dest.host())
-	{
+	if self.services.moderation.is_remote_server_forbidden(dest) {
 		return Err!(Request(Forbidden(debug_warn!("Federation with {dest} is not allowed."))));
 	}
 
diff --git a/src/service/federation/mod.rs b/src/service/federation/mod.rs
index ce7765ee..15521875 100644
--- a/src/service/federation/mod.rs
+++ b/src/service/federation/mod.rs
@@ -4,7 +4,7 @@ use std::sync::Arc;
 
 use conduwuit::{Result, Server};
 
-use crate::{Dep, client, resolver, server_keys};
+use crate::{Dep, client, moderation, resolver, server_keys};
 
 pub struct Service {
 	services: Services,
@@ -15,6 +15,7 @@ struct Services {
 	client: Dep<client::Service>,
 	resolver: Dep<resolver::Service>,
 	server_keys: Dep<server_keys::Service>,
+	moderation: Dep<moderation::Service>,
 }
 
 impl crate::Service for Service {
@@ -25,6 +26,7 @@ impl crate::Service for Service {
 				client: args.depend::<client::Service>("client"),
 				resolver: args.depend::<resolver::Service>("resolver"),
 				server_keys: args.depend::<server_keys::Service>("server_keys"),
+				moderation: args.depend::<moderation::Service>("moderation"),
 			},
 		}))
 	}
diff --git a/src/service/media/mod.rs b/src/service/media/mod.rs
index 5c26efe8..d053ba54 100644
--- a/src/service/media/mod.rs
+++ b/src/service/media/mod.rs
@@ -22,7 +22,7 @@ use tokio::{
 
 use self::data::{Data, Metadata};
 pub use self::thumbnail::Dim;
-use crate::{Dep, client, globals, sending};
+use crate::{Dep, client, globals, moderation, sending};
 
 #[derive(Debug)]
 pub struct FileMeta {
@@ -42,6 +42,7 @@ struct Services {
 	client: Dep<client::Service>,
 	globals: Dep<globals::Service>,
 	sending: Dep<sending::Service>,
+	moderation: Dep<moderation::Service>,
 }
 
 /// generated MXC ID (`media-id`) length
@@ -64,6 +65,7 @@ impl crate::Service for Service {
 				client: args.depend::<client::Service>("client"),
 				globals: args.depend::<globals::Service>("globals"),
 				sending: args.depend::<sending::Service>("sending"),
+				moderation: args.depend::<moderation::Service>("moderation"),
 			},
 		}))
 	}
diff --git a/src/service/media/remote.rs b/src/service/media/remote.rs
index cdcb429e..a1e874d8 100644
--- a/src/service/media/remote.rs
+++ b/src/service/media/remote.rs
@@ -423,16 +423,8 @@ pub async fn fetch_remote_content_legacy(
 fn check_fetch_authorized(&self, mxc: &Mxc<'_>) -> Result<()> {
 	if self
 		.services
-		.server
-		.config
-		.prevent_media_downloads_from
-		.is_match(mxc.server_name.host())
-		|| self
-			.services
-			.server
-			.config
-			.forbidden_remote_server_names
-			.is_match(mxc.server_name.host())
+		.moderation
+		.is_remote_server_media_downloads_forbidden(mxc.server_name)
 	{
 		// we'll lie to the client and say the blocked server's media was not found and
 		// log. the client has no way of telling anyways so this is a security bonus.
diff --git a/src/service/mod.rs b/src/service/mod.rs
index 2be16f79..a3214408 100644
--- a/src/service/mod.rs
+++ b/src/service/mod.rs
@@ -16,6 +16,7 @@ pub mod federation;
 pub mod globals;
 pub mod key_backups;
 pub mod media;
+pub mod moderation;
 pub mod presence;
 pub mod pusher;
 pub mod resolver;
diff --git a/src/service/moderation.rs b/src/service/moderation.rs
new file mode 100644
index 00000000..bd2616f6
--- /dev/null
+++ b/src/service/moderation.rs
@@ -0,0 +1,62 @@
+use std::sync::Arc;
+
+use conduwuit::{Result, Server, implement};
+use ruma::ServerName;
+
+pub struct Service {
+	services: Services,
+}
+
+struct Services {
+	pub server: Arc<Server>,
+}
+
+impl crate::Service for Service {
+	fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
+		Ok(Arc::new(Self {
+			services: Services { server: args.server.clone() },
+		}))
+	}
+
+	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
+}
+
+#[implement(Service)]
+#[must_use]
+pub fn is_remote_server_forbidden(&self, server_name: &ServerName) -> bool {
+	// Forbidden if NOT (allowed is empty OR allowed contains server OR is self)
+	// OR forbidden contains server
+	self.services
+		.server
+		.config
+		.forbidden_remote_server_names
+		.is_match(server_name.host())
+}
+
+#[implement(Service)]
+#[must_use]
+pub fn is_remote_server_room_directory_forbidden(&self, server_name: &ServerName) -> bool {
+	// Forbidden if NOT (allowed is empty OR allowed contains server OR is self)
+	// OR forbidden contains server
+	self.is_remote_server_forbidden(server_name)
+		|| self
+			.services
+			.server
+			.config
+			.forbidden_remote_room_directory_server_names
+			.is_match(server_name.host())
+}
+
+#[implement(Service)]
+#[must_use]
+pub fn is_remote_server_media_downloads_forbidden(&self, server_name: &ServerName) -> bool {
+	// Forbidden if NOT (allowed is empty OR allowed contains server OR is self)
+	// OR forbidden contains server
+	self.is_remote_server_forbidden(server_name)
+		|| self
+			.services
+			.server
+			.config
+			.prevent_media_downloads_from
+			.is_match(server_name.host())
+}
diff --git a/src/service/services.rs b/src/service/services.rs
index dc390054..5dcc120e 100644
--- a/src/service/services.rs
+++ b/src/service/services.rs
@@ -12,7 +12,7 @@ use tokio::sync::Mutex;
 use crate::{
 	account_data, admin, appservice, client, config, emergency, federation, globals, key_backups,
 	manager::Manager,
-	media, presence, pusher, resolver, rooms, sending, server_keys, service,
+	media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service,
 	service::{Args, Map, Service},
 	sync, transaction_ids, uiaa, updates, users,
 };
@@ -39,6 +39,7 @@ pub struct Services {
 	pub uiaa: Arc<uiaa::Service>,
 	pub updates: Arc<updates::Service>,
 	pub users: Arc<users::Service>,
+	pub moderation: Arc<moderation::Service>,
 
 	manager: Mutex<Option<Arc<Manager>>>,
 	pub(crate) service: Arc<Map>,
@@ -106,6 +107,7 @@ impl Services {
 			uiaa: build!(uiaa::Service),
 			updates: build!(updates::Service),
 			users: build!(users::Service),
+			moderation: build!(moderation::Service),
 
 			manager: Mutex::new(None),
 			service,