Mercurial > prosody-modules
view mod_firewall/definitions.lib.lua @ 5193:2bb29ece216b
mod_http_oauth2: Implement stateless dynamic client registration
Replaces previous explicit registration that required either the
additional module mod_adhoc_oauth2_client or manually editing the
database. That method was enough to have something to test with, but
would not probably not scale easily.
Dynamic client registration allows creating clients on the fly, which
may be even easier in theory.
In order to not allow basically unauthenticated writes to the database,
we implement a stateless model here.
per_host_key := HMAC(config -> oauth2_registration_key, hostname)
client_id := JWT { client metadata } signed with per_host_key
client_secret := HMAC(per_host_key, client_id)
This should ensure everything we need to know is part of the client_id,
allowing redirects etc to be validated, and the client_secret can be
validated with only the client_id and the per_host_key.
A nonce injected into the client_id JWT should ensure nobody can submit
the same client metadata and retrieve the same client_secret
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Fri, 03 Mar 2023 21:14:19 +0100 |
parents | b88f05c878ac |
children | 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;