mod_anti_spam/mod_anti_spam.lua
changeset 5863 259ffdbf8906
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_anti_spam/mod_anti_spam.lua	Tue Mar 05 18:26:29 2024 +0000
@@ -0,0 +1,165 @@
+local ip = require "util.ip";
+local jid_bare = require "util.jid".bare;
+local jid_split = require "util.jid".split;
+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 full_sessions = prosody.full_sessions;
+
+local user_exists = require "core.usermanager".user_exists;
+
+local new_rtbl_subscription = module:require("rtbl").new_rtbl_subscription;
+local trie = module:require("trie");
+
+local spam_source_domains = set.new();
+local spam_source_ips = trie.new();
+local spam_source_jids = set.new();
+
+local count_spam_blocked = module:metric("counter", "anti_spam_blocked", "stanzas", "Stanzas blocked as spam", {"reason"});
+
+function block_spam(event, reason, action)
+	event.spam_reason = reason;
+	event.spam_action = action;
+	if module:fire_event("spam-blocked", event) == false then
+		module:log("debug", "Spam allowed by another module");
+		return;
+	end
+
+	count_spam_blocked:with_labels(reason):add(1);
+
+	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"));
+	else
+		module:log("debug", "Discarding likely spam %s from %s (%s)", event.stanza.name, event.stanza.attr.from, reason);
+	end
+
+	return true;
+end
+
+function is_from_stranger(from_jid, event)
+	local stanza = event.stanza;
+	local to_user, to_host, to_resource = jid_split(stanza.attr.to);
+
+	if not to_user then return false; end
+
+	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
+		-- Allow all messages from your own jid
+		if from_jid == to_user.."@"..to_host then
+			return false; -- Pass through
+		end
+		if to_resource and stanza.attr.type == "groupchat" then
+			return false; -- Pass through
+		end
+		return true; -- Stranger danger
+	end
+end
+
+function is_spammy_server(session)
+	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
+		return true;
+	end
+end
+
+function is_spammy_sender(sender_jid)
+	return spam_source_jids:contains(sha256(sender_jid, true));
+end
+
+local spammy_strings = module:get_option_array("anti_spam_block_strings");
+local spammy_patterns = module:get_option_array("anti_spam_block_patterns");
+
+function is_spammy_content(stanza)
+	-- Only support message content
+	if stanza.name ~= "message" then return; end
+	if not (spammy_strings or spammy_patterns) then return; end
+
+	local body = stanza:get_child_text("body");
+	if spammy_strings then
+		for _, s in ipairs(spammy_strings) do
+			if body:find(s, 1, true) then
+				return true;
+			end
+		end
+	end
+	if spammy_patterns then
+		for _, s in ipairs(spammy_patterns) do
+			if body:find(s) then
+				return true;
+			end
+		end
+	end
+end
+
+-- Set up RTBLs
+
+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", {
+		added = function (item)
+			spam_source_domains:add(item);
+		end;
+		removed = function (item)
+			spam_source_domains:remove(item);
+		end;
+	});
+	new_rtbl_subscription(rtbl_service_jid, "spam_source_ips", {
+		added = function (item)
+			spam_source_ips:add_subnet(ip.parse_cidr(item));
+		end;
+		removed = function (item)
+			spam_source_ips:remove_subnet(ip.parse_cidr(item));
+		end;
+	});
+	new_rtbl_subscription(rtbl_service_jid, "spam_source_jids_sha256", {
+		added = function (item)
+			spam_source_jids:add(item);
+		end;
+		removed = function (item)
+			spam_source_jids:remove(item);
+		end;
+	});
+end
+
+module:hook("message/bare", function (event)
+	local to_bare = jid_bare(event.stanza.attr.to);
+
+	if not user_exists(to_bare) then return; end
+
+	local from_bare = jid_bare(event.stanza.attr.from);
+	if not is_from_stranger(from_bare, event) then return; end
+
+	if is_spammy_server(event.origin) then
+		return block_spam(event, "known-spam-source", "drop");
+	end
+
+	if is_spammy_sender(from_bare) then
+		return block_spam(event, "known-spam-jid", "drop");
+	end
+
+	if is_spammy_content(event.stanza) then
+		return block_spam(event, "spam-content", "drop");
+	end
+end, 500);
+
+module:hook("presence/bare", function (event)
+	if event.stanza.type ~= "subscribe" then
+		return;
+	end
+
+	if is_spammy_server(event.origin) then
+		return block_spam(event, "known-spam-source", "drop");
+	end
+
+	if is_spammy_sender(event.stanza) then
+		return block_spam(event, "known-spam-jid", "drop");
+	end
+end, 500);