File

mod_anti_spam/mod_anti_spam.lua @ 6055:23c4c61a1068

mod_muc_gateway_optimize: New module to optimize muc presence to remote gateways Some gateways are happy to receive presence for each participant in MUCs that they are in only once, to any one of their joined JIDs.
author Stephen Paul Weber <singpolyma@singpolyma.net>
date Sun, 17 Nov 2024 22:32:52 -0500
parent 5883:259ffdbf8906
child 6115:b644832a8290
child 6208:e20901443eae
line wrap: on
line source

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);