Diff

mod_anti_spam/mod_anti_spam.lua @ 6208:e20901443eae draft

Merge
author Trần H. Trung <xmpp:trần.h.trung@trung.fun>
date Mon, 17 Mar 2025 23:42:11 +0700
parent 5883:259ffdbf8906
child 6211:750d64c47ec6
line wrap: on
line diff
--- a/mod_anti_spam/mod_anti_spam.lua	Wed Feb 26 19:36:35 2025 +0700
+++ b/mod_anti_spam/mod_anti_spam.lua	Mon Mar 17 23:42:11 2025 +0700
@@ -4,7 +4,7 @@
 local set = require "util.set";
 local sha256 = require "util.hashes".sha256;
 local st = require"util.stanza";
-local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
+local rm = require "core.rostermanager";
 local full_sessions = prosody.full_sessions;
 
 local user_exists = require "core.usermanager".user_exists;
@@ -15,10 +15,30 @@
 local spam_source_domains = set.new();
 local spam_source_ips = trie.new();
 local spam_source_jids = set.new();
+local default_spam_action = module:get_option("anti_spam_default_action", "bounce");
+local custom_spam_actions = module:get_option("anti_spam_actions", {});
+
+local spam_actions = setmetatable({}, {
+	__index = function (t, reason)
+		local action = rawget(custom_spam_actions, reason) or default_spam_action;
+		rawset(t, reason, action);
+		return action;
+	end;
+});
 
 local count_spam_blocked = module:metric("counter", "anti_spam_blocked", "stanzas", "Stanzas blocked as spam", {"reason"});
 
+local hosts = prosody.hosts;
+
+local reason_messages = {
+	default = "Rejected as spam";
+	["known-spam-source"] = "Rejected as spam. Your server is listed as a known source of spam. Please contact your server operator.";
+};
+
 function block_spam(event, reason, action)
+	if not action then
+		action = spam_actions[reason];
+	end
 	event.spam_reason = reason;
 	event.spam_action = action;
 	if module:fire_event("spam-blocked", event) == false then
@@ -30,7 +50,7 @@
 
 	if action == "bounce" then
 		module:log("debug", "Bouncing likely spam %s from %s (%s)", event.stanza.name, event.stanza.attr.from, reason);
-		event.origin.send(st.error_reply("cancel", "policy-violation", "Rejected as spam"));
+		event.origin.send(st.error_reply(event.stanza, "cancel", "policy-violation", reason_messages[reason] or reason_messages.default));
 	else
 		module:log("debug", "Discarding likely spam %s from %s (%s)", event.stanza.name, event.stanza.attr.from, reason);
 	end
@@ -47,13 +67,25 @@
 	local to_session = full_sessions[stanza.attr.to];
 	if to_session then return false; end
 
-	if not is_contact_subscribed(to_user, to_host, from_jid) then
+	if not (
+		rm.is_contact_subscribed(to_user, to_host, from_jid) or
+		rm.is_user_subscribed(to_user, to_host, from_jid) or
+		rm.is_contact_pending_out(to_user, to_host, from_jid) or
+		rm.is_contact_preapproved(to_user, to_host, from_jid)
+	) then
+		local from_user, from_host = jid_split(from_jid);
+
 		-- Allow all messages from your own jid
-		if from_jid == to_user.."@"..to_host then
+		if from_user == to_user and from_host == to_host then
 			return false; -- Pass through
 		end
 		if to_resource and stanza.attr.type == "groupchat" then
-			return false; -- Pass through
+			return false; -- Pass through group chat messages
+		end
+		if rm.is_contact_subscribed(to_user, to_host, from_host) then
+			-- If you have the sending domain in your roster,
+			-- allow through (probably a gateway)
+			return false;
 		end
 		return true; -- Stranger danger
 	end
@@ -63,8 +95,11 @@
 	if spam_source_domains:contains(session.from_host) then
 		return true;
 	end
-	local origin_ip = ip.new(session.ip);
-	if spam_source_ips:contains_ip(origin_ip) then
+	local raw_ip = session.ip;
+	local parsed_ip = raw_ip and ip.new_ip(session.ip);
+	-- Not every session has an ip - for example, stanzas sent from a
+	-- local host session
+	if parsed_ip and spam_source_ips:contains_ip(parsed_ip) then
 		return true;
 	end
 end
@@ -82,6 +117,8 @@
 	if not (spammy_strings or spammy_patterns) then return; end
 
 	local body = stanza:get_child_text("body");
+	if not body then return; end
+
 	if spammy_strings then
 		for _, s in ipairs(spammy_strings) do
 			if body:find(s, 1, true) then
@@ -100,7 +137,7 @@
 
 -- Set up RTBLs
 
-local anti_spam_services = module:get_option_array("anti_spam_services");
+local anti_spam_services = module:get_option_array("anti_spam_services", {});
 
 for _, rtbl_service_jid in ipairs(anti_spam_services) do
 	new_rtbl_subscription(rtbl_service_jid, "spam_source_domains", {
@@ -113,10 +150,18 @@
 	});
 	new_rtbl_subscription(rtbl_service_jid, "spam_source_ips", {
 		added = function (item)
-			spam_source_ips:add_subnet(ip.parse_cidr(item));
+			local subnet_ip, subnet_bits = ip.parse_cidr(item);
+			if not subnet_ip then
+				return;
+			end
+			spam_source_ips:add_subnet(subnet_ip, subnet_bits);
 		end;
 		removed = function (item)
-			spam_source_ips:remove_subnet(ip.parse_cidr(item));
+			local subnet_ip, subnet_bits = ip.parse_cidr(item);
+			if not subnet_ip then
+				return;
+			end
+			spam_source_ips:remove_subnet(subnet_ip, subnet_bits);
 		end;
 	});
 	new_rtbl_subscription(rtbl_service_jid, "spam_source_jids_sha256", {
@@ -130,36 +175,65 @@
 end
 
 module:hook("message/bare", function (event)
-	local to_bare = jid_bare(event.stanza.attr.to);
+	local to_user, to_host = jid_split(event.stanza.attr.to);
 
-	if not user_exists(to_bare) then return; end
+	if not hosts[to_host] then
+		module:log("warn", "Skipping filtering of message to unknown host <%s>", to_host);
+		return;
+	end
 
 	local from_bare = jid_bare(event.stanza.attr.from);
-	if not is_from_stranger(from_bare, event) then return; end
+	if user_exists(to_user, to_host) then
+		if not is_from_stranger(from_bare, event) then
+			return;
+		end
+	end
+
+	module:log("debug", "Processing message from stranger...");
 
 	if is_spammy_server(event.origin) then
-		return block_spam(event, "known-spam-source", "drop");
+		return block_spam(event, "known-spam-source");
 	end
 
 	if is_spammy_sender(from_bare) then
-		return block_spam(event, "known-spam-jid", "drop");
+		return block_spam(event, "known-spam-jid");
 	end
 
 	if is_spammy_content(event.stanza) then
-		return block_spam(event, "spam-content", "drop");
+		return block_spam(event, "spam-content");
 	end
+
+	module:log("debug", "Allowing message through");
 end, 500);
 
 module:hook("presence/bare", function (event)
-	if event.stanza.type ~= "subscribe" then
+	if event.stanza.attr.type ~= "subscribe" then
 		return;
 	end
 
-	if is_spammy_server(event.origin) then
-		return block_spam(event, "known-spam-source", "drop");
+
+	local to_user, to_host = jid_split(event.stanza.attr.to);
+	local from_bare = jid_bare(event.stanza.attr.from);
+
+	if user_exists(to_user, to_host) then
+		if not is_from_stranger(from_bare, event) then
+			return;
+		end
 	end
 
-	if is_spammy_sender(event.stanza) then
-		return block_spam(event, "known-spam-jid", "drop");
+	module:log("debug", "Processing subscription request from stranger...");
+
+	if is_spammy_server(event.origin) then
+		return block_spam(event, "known-spam-source");
 	end
+
+	module:log("debug", "Not from known spam source server");
+
+	if is_spammy_sender(jid_bare(event.stanza.attr.from)) then
+		return block_spam(event, "known-spam-jid");
+	end
+
+	module:log("debug", "Not from known spam source JID");
+
+	module:log("debug", "Allowing subscription request through");
 end, 500);