Software /
code /
prosody-modules
File
mod_firewall/definitions.lib.lua @ 5173:460f78654864
mod_muc_rtbl: also filter messages
This was a bit tricky because we don't want to run the JIDs
through SHA256 on each message. Took a while to come up with this
simple plan of just caching the SHA256 of the JIDs on the
occupants.
This will leave some dirt in the occupants after unloading the
module, but that should be ok; once they cycle the room, the
hashes will be gone.
This is direly needed, otherwise, there is a tight race between
the moderation activities and the actors joining the room.
author | Jonas Schäfer <jonas@wielicki.name> |
---|---|
date | Tue, 21 Feb 2023 21:37:27 +0100 |
parent | 4512:b88f05c878ac |
child | 5232:0f943619e01a |
line wrap: on
line source
-- Name arguments are unused here -- luacheck: ignore 212 local definition_handlers = {}; local http = require "net.http"; local timer = require "util.timer"; local set = require"util.set"; local new_throttle = require "util.throttle".create; local hashes = require "util.hashes"; local jid = require "util.jid"; local lfs = require "lfs"; local multirate_cache_size = module:get_option_number("firewall_multirate_cache_limit", 1000); function definition_handlers.ZONE(zone_name, zone_members) local zone_member_list = {}; for member in zone_members:gmatch("[^, ]+") do zone_member_list[#zone_member_list+1] = member; end return set.new(zone_member_list)._items; end -- Helper function used by RATE handler local function evict_only_unthrottled(name, throttle) throttle:update(); -- Check whether the throttle is at max balance (i.e. totally safe to forget about it) if throttle.balance < throttle.max then -- Not safe to forget return false; end end function definition_handlers.RATE(name, line) local rate = assert(tonumber(line:match("([%d.]+)")), "Unable to parse rate"); local burst = tonumber(line:match("%(%s*burst%s+([%d.]+)%s*%)")) or 1; local max_throttles = tonumber(line:match("%(%s*entries%s+([%d]+)%s*%)")) or multirate_cache_size; local deny_when_full = not line:match("%(allow overflow%)"); return { single = function () return new_throttle(rate*burst, burst); end; multi = function () local cache = require "util.cache".new(max_throttles, deny_when_full and evict_only_unthrottled or nil); return { poll_on = function (_, key, amount) assert(key, "no key"); local throttle = cache:get(key); if not throttle then throttle = new_throttle(rate*burst, burst); if not cache:set(key, throttle) then module:log("warn", "Multirate '%s' has hit its maximum number of active throttles (%d), denying new events", name, max_throttles); return false; end end return throttle:poll(amount); end; } end; }; end local list_backends = { -- %LIST name: memory (limit: number) memory = { init = function (self, type, opts) if opts.limit then local have_cache_lib, cache_lib = pcall(require, "util.cache"); if not have_cache_lib then error("In-memory lists with a size limit require Prosody 0.10"); end self.cache = cache_lib.new((assert(tonumber(opts.limit), "Invalid list limit"))); if not self.cache.table then error("In-memory lists with a size limit require a newer version of Prosody 0.10"); end self.items = self.cache:table(); else self.items = {}; end end; add = function (self, item) self.items[item] = true; end; remove = function (self, item) self.items[item] = nil; end; contains = function (self, item) return self.items[item] == true; end; }; -- %LIST name: http://example.com/ (ttl: number, pattern: pat, hash: sha1) http = { init = function (self, url, opts) local poll_interval = assert(tonumber(opts.ttl or "3600"), "invalid ttl for <"..url.."> (expected number of seconds)"); local pattern = opts.pattern or "([^\r\n]+)\r?\n"; assert(pcall(string.match, "", pattern), "invalid pattern for <"..url..">"); if opts.hash then assert(opts.hash:match("^%w+$") and type(hashes[opts.hash]) == "function", "invalid hash function: "..opts.hash); self.hash_function = hashes[opts.hash]; end local etag; local failure_count = 0; local retry_intervals = { 60, 120, 300 }; -- By default only check the certificate if net.http supports SNI local sni_supported = http.feature and http.features.sni; local insecure = false; if opts.checkcert == "never" then insecure = true; elseif (opts.checkcert == nil or opts.checkcert == "when-sni") and not sni_supported then insecure = false; end local function update_list() http.request(url, { insecure = insecure; headers = { ["If-None-Match"] = etag; }; }, function (body, code, response) local next_poll = poll_interval; if code == 200 and body then etag = response.headers.etag; local items = {}; for entry in body:gmatch(pattern) do items[entry] = true; end self.items = items; module:log("debug", "Fetched updated list from <%s>", url); elseif code == 304 then module:log("debug", "List at <%s> is unchanged", url); elseif code == 0 or (code >= 400 and code <=599) then module:log("warn", "Failed to fetch list from <%s>: %d %s", url, code, tostring(body)); failure_count = failure_count + 1; next_poll = retry_intervals[failure_count] or retry_intervals[#retry_intervals]; end if next_poll > 0 then timer.add_task(next_poll+math.random(0, 60), update_list); end end); end update_list(); end; add = function () end; remove = function () end; contains = function (self, item) if self.hash_function then item = self.hash_function(item); end return self.items and self.items[item] == true; end; }; -- %LIST: file:/path/to/file file = { init = function (self, file_spec, opts) local n, items = 0, {}; self.items = items; local filename = file_spec:gsub("^file:", ""); if opts.missing == "ignore" and not lfs.attributes(filename, "mode") then module:log("debug", "Ignoring missing list file: %s", filename); return; end local file, err = io.open(filename); if not file then module:log("warn", "Failed to open list from %s: %s", filename, err); return; else for line in file:lines() do if not items[line] then n = n + 1; items[line] = true; end end end module:log("debug", "Loaded %d items from %s", n, filename); end; add = function (self, item) self.items[item] = true; end; remove = function (self, item) self.items[item] = nil; end; contains = function (self, item) return self.items and self.items[item] == true; end; }; -- %LIST: pubsub:pubsub.example.com/node -- TODO or the actual URI scheme? Bit overkill maybe? -- TODO Publish items back to the service? -- Step 1: Receiving pubsub events and storing them in the list -- We'll start by using only the item id. -- TODO Invent some custom schema for this? Needed for just a set of strings? pubsubitemid = { init = function(self, pubsub_spec, opts) local service_addr, node = pubsub_spec:match("^([^/]*)/(.*)"); module:depends("pubsub_subscription"); module:add_item("pubsub-subscription", { service = service_addr; node = node; on_subscribed = function () self.items = {}; end; on_item = function (event) self:add(event.item.attr.id); end; on_retract = function (event) self:remove(event.item.attr.id); end; on_purge = function () self.items = {}; end; on_unsubscribed = function () self.items = nil; end; on_delete= function () self.items = nil; end; }); -- TODO Initial fetch? Or should mod_pubsub_subscription do this? end; add = function (self, item) if self.items then self.items[item] = true; end end; remove = function (self, item) if self.items then self.items[item] = nil; end end; contains = function (self, item) return self.items and self.items[item] == true; end; }; }; list_backends.https = list_backends.http; local normalize_functions = { upper = string.upper, lower = string.lower; md5 = hashes.md5, sha1 = hashes.sha1, sha256 = hashes.sha256; prep = jid.prep, bare = jid.bare; }; local function wrap_list_method(list_method, filter) return function (self, item) return list_method(self, filter(item)); end end local function create_list(list_backend, list_def, opts) if not list_backends[list_backend] then error("Unknown list type '"..list_backend.."'", 0); end local list = setmetatable({}, { __index = list_backends[list_backend] }); if list.init then list:init(list_def, opts); end if opts.filter then local filters = {}; for func_name in opts.filter:gmatch("[%w_]+") do if func_name == "log" then table.insert(filters, function (s) --print("&&&&&", s); module:log("debug", "Checking list <%s> for: %s", list_def, s); return s; end); else assert(normalize_functions[func_name], "Unknown list filter: "..func_name); table.insert(filters, normalize_functions[func_name]); end end local filter; local n = #filters; if n == 1 then filter = filters[1]; else function filter(s) for i = 1, n do s = filters[i](s or ""); end return s; end end list.add = wrap_list_method(list.add, filter); list.remove = wrap_list_method(list.remove, filter); list.contains = wrap_list_method(list.contains, filter); end return list; end --[[ %LIST spammers: memory (source: /etc/spammers.txt) %LIST spammers: memory (source: /etc/spammers.txt) %LIST spammers: http://example.com/blacklist.txt ]] function definition_handlers.LIST(list_name, list_definition) local list_backend = list_definition:match("^%w+"); local opts = {}; local opt_string = list_definition:match("^%S+%s+%((.+)%)"); if opt_string then for opt_k, opt_v in opt_string:gmatch("(%w+): ?([^,]+)") do opts[opt_k] = opt_v; end end return create_list(list_backend, list_definition:match("^%S+"), opts); end function definition_handlers.PATTERN(name, pattern) local ok, err = pcall(string.match, "", pattern); if not ok then error("Invalid pattern '"..name.."': "..err); end return pattern; end function definition_handlers.SEARCH(name, pattern) return pattern; end return definition_handlers;