view mod_muc_rtbl/mod_muc_rtbl.lua @ 6549:d2e50b587721

mod_firewall: marks: switch to keyval+ store API
author Matthew Wild <mwild1@gmail.com>
date Sun, 24 May 2026 10:49:09 +0100
parents 093c47ee2041
children
line wrap: on
line source

local jid = require "util.jid";
local sha256 = require "util.hashes".sha256;
local st = require "util.stanza";

local url = require "socket.url";

-- Legacy config (single list only)
local rtbl_service_jid = module:get_option_string("muc_rtbl_jid");
local rtbl_node = module:get_option_string("muc_rtbl_node", "muc_bans_sha256");

-- Multi-list config
local rtbl_config = module:get_option_array("muc_rtbls", {
	rtbl_service_jid and rtbl_node and ("xmpp:"..rtbl_service_jid.."?;node="..rtbl_node) or nil;
});

if #rtbl_config == 0 then
	return error("No RTBLs configured");
end

local mod_rtbl = module:depends("rtbl");

local lists = {};

local function parse_xmpp_uri(uri)
	local parsed = url.parse(uri);
	if parsed.scheme and parsed.scheme ~= "xmpp" then
		return nil, "unexpected-scheme";
	end

	local parsed_query = {};
	for kv in (parsed.query or ""):gmatch("[^?;]+") do
		local k, v = kv:match("^([^=]+)=?(.*)$");
		parsed_query[k] = v;
	end

	return {
		jid = jid.prep(parsed.path);
		params = parsed_query;
	};
end

for _, rtbl_uri in ipairs(rtbl_config) do
	local uri = parse_xmpp_uri(rtbl_uri);
	module:log("debug", "Subscribing to %q node %q", uri.jid, uri.params.node);
	local rtbl = mod_rtbl.new_rtbl_subscription(uri.jid, uri.params.node or "muc_bans_sha256", {});
	table.insert(lists, rtbl);
end

local function update_occupant_hashes(occupant)
	local bare_hash, host_hash;
	if not occupant.mod_muc_rtbl_bare_hash then
		bare_hash = sha256(jid.bare(occupant.bare_jid), true);
		occupant.mod_muc_rtbl_bare_hash = bare_hash;
	else
		bare_hash = occupant.mod_muc_rtbl_bare_hash;
	end
	if not occupant.mod_muc_rtbl_host_hash then
		host_hash = sha256(jid.host(occupant.bare_jid), true);
		occupant.mod_muc_rtbl_host_hash = host_hash;
	else
		host_hash = occupant.mod_muc_rtbl_host_hash;
	end
	return bare_hash, host_hash;
end

local function is_banned_occupant(occupant)
	local bare_hash, host_hash = update_occupant_hashes(occupant);
	for _, list in ipairs(lists) do
		local items = list.items;
		if items[bare_hash] or items[host_hash] then
			return true;
		end
	end
end

module:hook("muc-occupant-pre-join", function (event)
	local from_bare = jid.bare(event.stanza.attr.from);

	local affiliation = event.room:get_affiliation(from_bare);
	if affiliation and affiliation ~= "none" then
		-- Skip check for affiliated users
		return;
	end

	if is_banned_occupant(event.occupant) then
		module:log("info", "Blocked user <%s> from room <%s> due to RTBL match", from_bare, event.stanza.attr.to);
		local error_reply = st.error_reply(event.stanza, "cancel", "forbidden", "You are banned from this service", event.room.jid);
		event.origin.send(error_reply);
		return true;
	end
end);

module:hook("muc-occupant-groupchat", function(event)
	local occupant = event.occupant;
	local affiliation = event.room:get_affiliation(occupant.bare_jid);
	if affiliation and affiliation ~= "none" then
		-- Skip check for affiliated users
		return;
	end

	if is_banned_occupant(occupant) then
		module:log("debug", "Blocked message from user <%s> to room <%s> due to RTBL match", event.stanza.attr.from, event.stanza.attr.to);
		local error_reply = st.error_reply(event.stanza, "cancel", "forbidden", "You are banned from this service", event.room.jid);
		event.origin.send(error_reply);
		return true;
	end
end);

module:hook("muc-private-message", function(event)
	local occupant = event.room:get_occupant_by_nick(event.stanza.attr.from);
	local affiliation = event.room:get_affiliation(occupant.bare_jid);
	if affiliation and affiliation ~= "none" then
		-- Skip check for affiliated users
		return;
	end

	if is_banned_occupant(occupant) then
		module:log("debug", "Blocked private message from user <%s> from room <%s> due to RTBL match", occupant.bare_jid, event.stanza.attr.to);
		local error_reply = st.error_reply(event.stanza, "cancel", "forbidden", "You are banned from this service", event.room.jid);
		event.origin.send(error_reply);
		return false; -- Don't route it
	end
end);

module.environment.lists = lists