comparison mod_muc_rtbl/mod_muc_rtbl.lua @ 6304:6e2458b69e33

mod_muc_rtbl: Use mod_rtbl, adds support for multiple list subscriptions
author Matthew Wild <mwild1@gmail.com>
date Wed, 30 Jul 2025 14:16:55 +0100
parents ba71fdc8ea73
children 093c47ee2041
comparison
equal deleted inserted replaced
6303:d1d774f79ea8 6304:6e2458b69e33
1 local array = require "util.array";
2 local it = require "util.iterators";
3 local jid = require "util.jid"; 1 local jid = require "util.jid";
4 local sha256 = require "util.hashes".sha256; 2 local sha256 = require "util.hashes".sha256;
5 local set = require "util.set";
6 local st = require "util.stanza"; 3 local st = require "util.stanza";
7 4
8 local rtbl_service_jid = assert(module:get_option_string("muc_rtbl_jid"), "No RTBL JID supplied"); 5 local url = require "socket.url";
6
7 -- Legacy config (single list only)
8 local rtbl_service_jid = module:get_option_string("muc_rtbl_jid");
9 local rtbl_node = module:get_option_string("muc_rtbl_node", "muc_bans_sha256"); 9 local rtbl_node = module:get_option_string("muc_rtbl_node", "muc_bans_sha256");
10 10
11 local banned_hashes = module:shared("banned_hashes"); 11 -- Multi-list config
12 12 local rtbl_config = module:get_option_array("muc_rtbls", {
13 module:depends("pubsub_subscription"); 13 rtbl_service_jid and rtbl_node and ("xmpp:"..rtbl_service_jid.."?;node="..rtbl_node) or nil;
14
15 module:add_item("pubsub-subscription", {
16 service = rtbl_service_jid;
17 node = rtbl_node;
18
19 -- Callbacks:
20 on_subscribed = function()
21 module:log("info", "RTBL active");
22 end;
23
24 on_error = function(err)
25 module:log("error", "Failed to subscribe to RTBL: %s::%s: %s", err.type, err.condition, err.text);
26 end;
27
28 on_item = function(event)
29 local hash = event.item.attr.id;
30 if not hash then return; end
31 module:log("debug", "Received new hash: %s", hash);
32 banned_hashes[hash] = true;
33 end;
34
35 on_retract = function (event)
36 local hash = event.item.attr.id;
37 if not hash then return; end
38 module:log("debug", "Retracted hash: %s", hash);
39 banned_hashes[hash] = nil;
40 end;
41
42 purge = function()
43 module:log("debug", "Purge all hashes");
44 for hash in pairs(banned_hashes) do
45 banned_hashes[hash] = nil;
46 end
47 end;
48 }); 14 });
49 15
50 function request_list() 16 if #rtbl_config == 0 then
51 local items_request = st.iq({ to = rtbl_service_jid, from = module.host, type = "get", id = "rtbl-request" }) 17 return error("No RTBLs configured");
52 :tag("pubsub", { xmlns = "http://jabber.org/protocol/pubsub" })
53 :tag("items", { node = rtbl_node }):up()
54 :up();
55
56 module:send(items_request);
57 end 18 end
58 19
59 function update_list(event) 20 local mod_rtbl = module:depends("rtbl");
60 local from_jid = event.stanza.attr.from; 21
61 if from_jid ~= rtbl_service_jid then 22 local lists = {};
62 module:log("debug", "Ignoring RTBL response from unknown sender"); 23
63 return; 24 local function parse_xmpp_uri(uri)
64 end 25 local parsed = url.parse(uri);
65 local items_el = event.stanza:find("{http://jabber.org/protocol/pubsub}pubsub/items"); 26 if parsed.scheme ~= "xmpp" then
66 if not items_el then 27 return nil, "unexpected-scheme";
67 module:log("warn", "Invalid items response from RTBL service");
68 return;
69 end 28 end
70 29
71 local old_entries = set.new(array.collect(it.keys(banned_hashes))); 30 local parsed_query = {};
72 31 for kv in (parsed.query or ""):gmatch("[^?;]+") do
73 local n_added, n_removed, n_total = 0, 0, 0; 32 local k, v = kv:match("^([^=]+)=?(.*)$");
74 for item in items_el:childtags("item") do 33 parsed_query[k] = v;
75 local hash = item.attr.id;
76 if hash then
77 n_total = n_total + 1;
78 if not old_entries:contains(hash) then
79 -- New entry
80 n_added = n_added + 1;
81 banned_hashes[hash] = true;
82 else
83 -- Entry already existed
84 old_entries:remove(hash);
85 end
86 end
87 end 34 end
88 35
89 -- Remove old entries that weren't in the received list 36 return {
90 for hash in old_entries do 37 jid = jid.prep(parsed.path);
91 n_removed = n_removed + 1; 38 params = parsed_query;
92 banned_hashes[hash] = nil; 39 };
93 end
94
95 module:log("info", "%d RTBL entries received from %s (%d added, %d removed)", n_total, from_jid, n_added, n_removed);
96 return true;
97 end 40 end
98 41
99 module:hook("iq-result/host/rtbl-request", update_list); 42 for _, rtbl_uri in ipairs(rtbl_config) do
43 local uri = parse_xmpp_uri(rtbl_uri);
44 module:log("debug", "Subscribing to %q node %q", uri.jid, uri.params.node);
45 local rtbl = mod_rtbl.new_rtbl_subscription(uri.jid, uri.params.node, {});
46 table.insert(lists, rtbl);
47 end
100 48
101 function update_hashes(occupant) 49 local function update_occupant_hashes(occupant)
102 local bare_hash, host_hash; 50 local bare_hash, host_hash;
103 if not occupant.mod_muc_rtbl_bare_hash then 51 if not occupant.mod_muc_rtbl_bare_hash then
104 bare_hash = sha256(jid.bare(occupant.bare_jid), true); 52 bare_hash = sha256(jid.bare(occupant.bare_jid), true);
105 occupant.mod_muc_rtbl_bare_hash = bare_hash; 53 occupant.mod_muc_rtbl_bare_hash = bare_hash;
106 else 54 else
110 host_hash = sha256(jid.host(occupant.bare_jid), true); 58 host_hash = sha256(jid.host(occupant.bare_jid), true);
111 occupant.mod_muc_rtbl_host_hash = host_hash; 59 occupant.mod_muc_rtbl_host_hash = host_hash;
112 else 60 else
113 host_hash = occupant.mod_muc_rtbl_host_hash; 61 host_hash = occupant.mod_muc_rtbl_host_hash;
114 end 62 end
115 return bare_hash, host_hash 63 return bare_hash, host_hash;
64 end
65
66 local function is_banned_occupant(occupant)
67 local bare_hash, host_hash = update_occupant_hashes(occupant);
68 for _, list in ipairs(lists) do
69 local items = list.items;
70 if items[bare_hash] or items[host_hash] then
71 return true;
72 end
73 end
116 end 74 end
117 75
118 module:hook("muc-occupant-pre-join", function (event) 76 module:hook("muc-occupant-pre-join", function (event)
119 if next(banned_hashes) == nil then return end
120
121 local from_bare = jid.bare(event.stanza.attr.from); 77 local from_bare = jid.bare(event.stanza.attr.from);
122 78
123 local affiliation = event.room:get_affiliation(from_bare); 79 local affiliation = event.room:get_affiliation(from_bare);
124 if affiliation and affiliation ~= "none" then 80 if affiliation and affiliation ~= "none" then
125 -- Skip check for affiliated users 81 -- Skip check for affiliated users
126 return; 82 return;
127 end 83 end
128 84
129 local bare_hash, host_hash = update_hashes(event.occupant); 85 if is_banned_occupant(event.occupant) then
130 if banned_hashes[bare_hash] or banned_hashes[host_hash] then
131 module:log("info", "Blocked user <%s> from room <%s> due to RTBL match", from_bare, event.stanza.attr.to); 86 module:log("info", "Blocked user <%s> from room <%s> due to RTBL match", from_bare, event.stanza.attr.to);
132 local error_reply = st.error_reply(event.stanza, "cancel", "forbidden", "You are banned from this service", event.room.jid); 87 local error_reply = st.error_reply(event.stanza, "cancel", "forbidden", "You are banned from this service", event.room.jid);
133 event.origin.send(error_reply); 88 event.origin.send(error_reply);
134 return true; 89 return true;
135 end 90 end
136 end); 91 end);
137 92
138 module:hook("muc-occupant-groupchat", function(event) 93 module:hook("muc-occupant-groupchat", function(event)
139 local affiliation = event.room:get_affiliation(event.occupant.bare_jid); 94 local occupant = event.occupant;
95 local affiliation = event.room:get_affiliation(occupant.bare_jid);
140 if affiliation and affiliation ~= "none" then 96 if affiliation and affiliation ~= "none" then
141 -- Skip check for affiliated users 97 -- Skip check for affiliated users
142 return; 98 return;
143 end 99 end
144 100
145 local bare_hash, host_hash = update_hashes(event.occupant); 101 if is_banned_occupant(occupant) then
146 if banned_hashes[bare_hash] or banned_hashes[host_hash] then
147 module:log("debug", "Blocked message from user <%s> to room <%s> due to RTBL match", event.stanza.attr.from, event.stanza.attr.to); 102 module:log("debug", "Blocked message from user <%s> to room <%s> due to RTBL match", event.stanza.attr.from, event.stanza.attr.to);
148 local error_reply = st.error_reply(event.stanza, "cancel", "forbidden", "You are banned from this service", event.room.jid); 103 local error_reply = st.error_reply(event.stanza, "cancel", "forbidden", "You are banned from this service", event.room.jid);
149 event.origin.send(error_reply); 104 event.origin.send(error_reply);
150 return true; 105 return true;
151 end 106 end
157 if affiliation and affiliation ~= "none" then 112 if affiliation and affiliation ~= "none" then
158 -- Skip check for affiliated users 113 -- Skip check for affiliated users
159 return; 114 return;
160 end 115 end
161 116
162 local bare_hash, host_hash = update_hashes(occupant); 117 if is_banned_occupant(occupant) then
163 if banned_hashes[bare_hash] or banned_hashes[host_hash] then
164 module:log("debug", "Blocked private message from user <%s> from room <%s> due to RTBL match", occupant.bare_jid, event.stanza.attr.to); 118 module:log("debug", "Blocked private message from user <%s> from room <%s> due to RTBL match", occupant.bare_jid, event.stanza.attr.to);
165 local error_reply = st.error_reply(event.stanza, "cancel", "forbidden", "You are banned from this service", event.room.jid); 119 local error_reply = st.error_reply(event.stanza, "cancel", "forbidden", "You are banned from this service", event.room.jid);
166 event.origin.send(error_reply); 120 event.origin.send(error_reply);
167 return false; -- Don't route it 121 return false; -- Don't route it
168 end 122 end
169 end); 123 end);
170 124
171 if prosody.start_time then 125 module.environment.lists = lists
172 request_list();
173 else
174 module:hook_global("server-started", function ()
175 request_list();
176 end);
177 end