Mercurial > prosody-modules
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 |
