# HG changeset patch # User Stephen Paul Weber # Date 1695043459 18000 # Node ID 62c6e17a5e9d8ae684984f2efc0a62100aeade6c # Parent eade7ff9f52cfc63fbced072292ce7c12e37aa2f# Parent c217f4edfc4fbd718000bdab3d7dd158cf543cfd Merge diff -r eade7ff9f52c -r 62c6e17a5e9d .editorconfig --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.editorconfig Mon Sep 18 08:24:19 2023 -0500 @@ -0,0 +1,34 @@ +# https://editorconfig.org/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 150 + +[*.json] +# json_pp -json_opt canonical,pretty +indent_size = 3 +indent_style = space + +[{README,COPYING,CONTRIBUTING,TODO}{,.markdown,.md}] +# pandoc -s -t markdown +indent_size = 4 +indent_style = space + +[*.py] +indent_size = 4 +indent_style = space + +[*.{xml,svg}] +# xmllint --nsclean --encode UTF-8 --noent --format - +indent_size = 2 +indent_style = space + +[*.yaml] +indent_size = 2 +indent_style = space diff -r eade7ff9f52c -r 62c6e17a5e9d misc/lnav/README.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/lnav/README.md Mon Sep 18 08:24:19 2023 -0500 @@ -0,0 +1,6 @@ +% Prosody log format for lnav + +This is a format definition that allows to better +handle Prosody logs. + +Install it using `lnav -i ./prosody.json` diff -r eade7ff9f52c -r 62c6e17a5e9d misc/lnav/prosody.json --- a/misc/lnav/prosody.json Mon Sep 18 08:22:07 2023 -0500 +++ b/misc/lnav/prosody.json Mon Sep 18 08:24:19 2023 -0500 @@ -14,7 +14,7 @@ "ordered-by-time" : true, "regex" : { "standard" : { - "pattern" : "^(?\\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2})\\s+(?\\S+)\\s+(?debug|info|warn|error)\\s+(?.+)$" + "pattern" : "^(?\\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2}\\s+)(?\\S+)\\s+(?debug|info|warn|error)\\s+(?.+)$" } }, "sample" : [ @@ -23,7 +23,9 @@ } ], "timestamp-field" : "timestamp", - "timestamp-format" : "%b %d %H:%M:%S ", + "timestamp-format" : [ + "%b %d %H:%M:%S " + ], "title" : "Prosody log", "url" : "https://prosody.im/doc/logging", "value" : { diff -r eade7ff9f52c -r 62c6e17a5e9d misc/mtail/prosody.mtail --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/mtail/prosody.mtail Mon Sep 18 08:24:19 2023 -0500 @@ -0,0 +1,13 @@ +counter prosody_log_messages by level + +/^(?P(?P\w+\s+\d+\s+\d+:\d+:\d+)|(?P\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+[+-]\d{2}:\d{2})) (?P\S+)\s(?P\w+)\s(?P.*)/ { + len($legacy_date) > 0 { + strptime($2, "Jan _2 15:04:05") + } + len($rfc3339_date) > 0 { + strptime($rfc3339_date, "2006-01-02T03:04:05-0700") + } + $loglevel != "" { + prosody_log_messages[$loglevel]++ + } +} diff -r eade7ff9f52c -r 62c6e17a5e9d mod_auth_oauth_external/README.md --- a/mod_auth_oauth_external/README.md Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_auth_oauth_external/README.md Mon Sep 18 08:24:19 2023 -0500 @@ -50,6 +50,8 @@ logging in the field specified by `oauth_external_username_field`. Commonly the [OpenID `UserInfo` endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) + If left unset, only `SASL PLAIN` is supported and the username + provided there is assumed correct. `oauth_external_username_field` : String. Default is `"preferred_username"`. Field in the JSON @@ -72,21 +74,30 @@ : String. Client ID used to identify Prosody during the resource owner password grant. +`oauth_external_client_secret` +: String. Client secret used to identify Prosody during the resource + owner password grant. + +`oauth_external_scope` +: String. Defaults to `"openid"`. Included in request for resource + owner password grant. + # Compatibility ## Prosody Version Status - --------- --------------- + --------- ----------------------------------------------- trunk works - 0.12.x does not work - 0.11.x does not work + 0.12.x OAUTHBEARER will not work, otherwise untested + 0.11.x OAUTHBEARER will not work, otherwise untested ## Identity Provider Tested with - [KeyCloak](https://www.keycloak.org/) +- [Mastodon](https://joinmastodon.org/) # Future work diff -r eade7ff9f52c -r 62c6e17a5e9d mod_auth_oauth_external/mod_auth_oauth_external.lua --- a/mod_auth_oauth_external/mod_auth_oauth_external.lua Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_auth_oauth_external/mod_auth_oauth_external.lua Mon Sep 18 08:24:19 2023 -0500 @@ -1,5 +1,6 @@ local http = require "net.http"; local async = require "util.async"; +local jid = require "util.jid"; local json = require "util.json"; local sasl = require "util.sasl"; @@ -15,7 +16,8 @@ -- XXX Hold up, does whatever done here even need any of these things? Are we -- the OAuth client? Is the XMPP client the OAuth client? What are we??? local client_id = module:get_option_string("oauth_external_client_id"); --- TODO -- local client_secret = module:get_option_string("oauth_external_client_secret"); +local client_secret = module:get_option_string("oauth_external_client_secret"); +local scope = module:get_option_string("oauth_external_scope", "openid"); --[[ More or less required endpoints digraph "oauth endpoints" { @@ -28,6 +30,32 @@ local host = module.host; local provider = {}; +local function not_implemented() + return nil, "method not implemented" +end + +-- With proper OAuth 2, most of these should be handled at the atuhorization +-- server, no there. +provider.test_password = not_implemented; +provider.get_password = not_implemented; +provider.set_password = not_implemented; +provider.create_user = not_implemented; +provider.delete_user = not_implemented; + +function provider.user_exists(_username) + -- Can this even be done in a generic way in OAuth 2? + -- OIDC and WebFinger perhaps? + return true; +end + +function provider.users() + -- TODO this could be done by recording known users locally + return function () + module:log("debug", "User iteration not supported"); + return nil; + end +end + function provider.get_sasl_handler() local profile = {}; profile.http_client = http.default; -- TODO configurable @@ -35,14 +63,16 @@ if token_endpoint and allow_plain then local map_username = function (username, _realm) return username; end; --jid.join; -- TODO configurable function profile:plain_test(username, password, realm) + username = jid.unescape(username); -- COMPAT Mastodon local tok, err = async.wait_for(self.profile.http_client:request(token_endpoint, { headers = { ["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"; ["Accept"] = "application/json" }; body = http.formencode({ grant_type = "password"; client_id = client_id; + client_secret = client_secret; username = map_username(username, realm); password = password; - scope = "openid"; + scope = scope; }); })) if err or not (tok.code >= 200 and tok.code < 300) then @@ -52,6 +82,12 @@ if not token_resp or string.lower(token_resp.token_type or "") ~= "bearer" then return false, nil; end + if not validation_endpoint then + -- We're not going to get more info, only the username + self.username = jid.escape(username); + self.token_info = token_resp; + return true, true; + end local ret, err = async.wait_for(self.profile.http_client:request(validation_endpoint, { headers = { ["Authorization"] = "Bearer " .. token_resp.access_token; ["Accept"] = "application/json" } })); if err then @@ -61,36 +97,38 @@ return false, nil; end local response = json.decode(ret.body); - if type(response) ~= "table" or (response[username_field]) ~= username then + if type(response) ~= "table" then + return false, nil, nil; + elseif type(response[username_field]) ~= "string" then return false, nil, nil; end - if response.jid then - self.username, self.realm, self.resource = jid.prepped_split(response.jid, true); - end - self.role = response.role; + self.username = jid.escape(response[username_field]); self.token_info = response; return true, true; end end - function profile:oauthbearer(token) - if token == "" then - return false, nil, extra; - end + if validation_endpoint then + function profile:oauthbearer(token) + if token == "" then + return false, nil, extra; + end - local ret, err = async.wait_for(self.profile.http_client:request(validation_endpoint, - { headers = { ["Authorization"] = "Bearer " .. token; ["Accept"] = "application/json" } })); - if err then - return false, nil, extra; + local ret, err = async.wait_for(self.profile.http_client:request(validation_endpoint, { + headers = { ["Authorization"] = "Bearer " .. token; ["Accept"] = "application/json" }; + })); + if err then + return false, nil, extra; + end + local response = ret and json.decode(ret.body); + if not (ret.code >= 200 and ret.code < 300) then + return false, nil, response or extra; + end + if type(response) ~= "table" or type(response[username_field]) ~= "string" then + return false, nil, nil; + end + + return jid.escape(response[username_field]), true, response; end - local response = ret and json.decode(ret.body); - if not (ret.code >= 200 and ret.code < 300) then - return false, nil, response or extra; - end - if type(response) ~= "table" or type(response[username_field]) ~= "string" then - return false, nil, nil; - end - - return response[username_field], true, response; end return sasl.new(host, profile); end diff -r eade7ff9f52c -r 62c6e17a5e9d mod_bidi/README.markdown --- a/mod_bidi/README.markdown Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_bidi/README.markdown Mon Sep 18 08:24:19 2023 -0500 @@ -1,11 +1,15 @@ --- labels: -- 'Stage-Stable' -summary: 'XEP-0288: Bidirectional Server-to-Server Connections' -... +- Stage-Stable +summary: "XEP-0288: Bidirectional Server-to-Server Connections" +--- -Introduction -============ +::: {.alert .alert-warning} +This module is unreliable when used with Prosody 0.12, switch to +[mod_s2s_bidi][doc:modules:mod_s2s_bidi] +::: + +# Introduction This module implements [XEP-0288: Bidirectional Server-to-Server Connections](http://xmpp.org/extensions/xep-0288.html). It allows @@ -14,13 +18,9 @@ Install and enable it like any other module. It has no configuration. -Compatibility -============= +# Compatibility - ------- -------------------------- - trunk Bidi available natively with [mod_s2s_bidi][doc:modules:mod_s2s_bidi] - 0.11 Works - 0.10 Works - 0.9 Works - 0.8 Works (use the 0.8 repo) - ------- -------------------------- + ------ ------------------------------------------- + 0.12 Bidi available natively with [mod_s2s_bidi][doc:modules:mod_s2s_bidi] + 0.11 Works + ------ ------------------------------------------- diff -r eade7ff9f52c -r 62c6e17a5e9d mod_client_management/README.md --- a/mod_client_management/README.md Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_client_management/README.md Mon Sep 18 08:24:19 2023 -0500 @@ -35,6 +35,12 @@ prosodyctl shell user clients user@example.com ``` +To revoke access from particular client: + +```shell +prosodyctl shell user revoke_client user@example.com grant/xxxxx +``` + ## Compatibility Requires Prosody trunk (as of 2023-03-29). Not compatible with Prosody 0.12 diff -r eade7ff9f52c -r 62c6e17a5e9d mod_client_management/mod_client_management.lua --- a/mod_client_management/mod_client_management.lua Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_client_management/mod_client_management.lua Mon Sep 18 08:24:19 2023 -0500 @@ -10,8 +10,8 @@ local strict = module:get_option_boolean("enforce_client_ids", false); -module:default_permission("prosody:user", ":list-clients"); -module:default_permission("prosody:user", ":manage-clients"); +module:default_permission("prosody:registered", ":list-clients"); +module:default_permission("prosody:registered", ":manage-clients"); local tokenauth = module:depends("tokenauth"); local mod_fast = module:depends("sasl2_fast"); @@ -35,6 +35,8 @@ if not (sasl_agent or token_agent) then return; end return { software = sasl_agent and sasl_agent.software or token_agent and token_agent.name or nil; + software_id = token_agent and token_agent.id or nil; + software_version = token_agent and token_agent.version or nil; uri = token_agent and token_agent.uri or nil; device = sasl_agent and sasl_agent.device or nil; }; @@ -250,6 +252,7 @@ type = "access"; first_seen = grant.created; last_seen = grant.accessed; + expires = grant.expires; active = { grant = grant; }; @@ -276,6 +279,17 @@ return active_clients; end +local function user_agent_tostring(user_agent) + if user_agent then + if user_agent.software then + if user_agent.software_version then + return user_agent.software .. "/" .. user_agent.software_version; + end + return user_agent.software; + end + end +end + function revoke_client_access(username, client_selector) if client_selector then local c_type, c_id = client_selector:match("^(%w+)/(.+)$"); @@ -309,6 +323,13 @@ local ok = tokenauth.revoke_grant(username, c_id); if not ok then return nil, "internal-server-error"; end return true; + elseif c_type == "software" then + local active_clients = get_active_clients(username); + for _, client in ipairs(active_clients) do + if client.user_agent and client.user_agent.software == c_id or user_agent_tostring(client.user_agent) == c_id then + return revoke_client_access(username, client.id); + end + end end end @@ -348,7 +369,7 @@ local user_agent = st.stanza("user-agent"); if client.user_agent then if client.user_agent.software then - user_agent:text_tag("software", client.user_agent.software); + user_agent:text_tag("software", client.user_agent.software, { id = client.user_agent.software_id; version = client.user_agent.software_version }); end if client.user_agent.device then user_agent:text_tag("device", client.user_agent.device); @@ -417,23 +438,40 @@ return true, "No clients associated with this account"; end + local function date_or_time(last_seen) + return last_seen and os.date(math.abs(os.difftime(os.time(), last_seen)) >= 86400 and "%Y-%m-%d" or "%H:%M:%S", last_seen); + end + + local date_or_time_width = math.max(#os.date("%Y-%m-%d"), #os.date("%H:%M:%S")); + local colspec = { + { title = "ID"; key = "id"; width = "1p" }; { title = "Software"; key = "user_agent"; width = "1p"; - mapper = function(user_agent) - return user_agent and user_agent.software; - end; + mapper = user_agent_tostring; + }; + { + title = "First seen"; + key = "first_seen"; + width = date_or_time_width; + align = "right"; + mapper = date_or_time; }; { title = "Last seen"; key = "last_seen"; - width = math.max(#os.date("%Y-%m-%d"), #os.date("%H:%M:%S")); + width = date_or_time_width; align = "right"; - mapper = function(last_seen) - return os.date(os.difftime(os.time(), last_seen) >= 86400 and "%Y-%m-%d" or "%H:%M:%S", last_seen); - end; + mapper = date_or_time; + }; + { + title = "Expires"; + key = "expires"; + width = date_or_time_width; + align = "right"; + mapper = date_or_time; }; { title = "Authentication"; @@ -456,4 +494,18 @@ print(string.rep("-", self.session.width)); return true, ("%d clients"):format(#clients); end + + function console_env.user:revoke_client(user_jid, selector) -- luacheck: ignore 212/self + local username, host = jid.split(user_jid); + local mod = prosody.hosts[host] and prosody.hosts[host].modules.client_management; + if not mod then + return false, ("Host does not exist on this server, or does not have mod_client_management loaded"); + end + + local revoked, err = revocation_errors.coerce(mod.revoke_client_access(username, selector)); + if not revoked then + return false, err.text or err; + end + return true, "Client access revoked"; + end end); diff -r eade7ff9f52c -r 62c6e17a5e9d mod_cloud_notify_extensions/README.markdown --- a/mod_cloud_notify_extensions/README.markdown Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_cloud_notify_extensions/README.markdown Mon Sep 18 08:24:19 2023 -0500 @@ -38,13 +38,10 @@ There is no configuration for this module, just add it to modules\_enabled as normal. -Compatibility -============= +# Compatibility - ----- ------- - 0.12 Works - ----- ------- - 0.11 Should work - ----- ------- - trunk Works - ----- ------- + ------- ------------- + 0.12 Works + 0.11 Should work + trunk Works + ------- ------------- diff -r eade7ff9f52c -r 62c6e17a5e9d mod_compat_roles/mod_compat_roles.lua --- a/mod_compat_roles/mod_compat_roles.lua Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_compat_roles/mod_compat_roles.lua Mon Sep 18 08:24:19 2023 -0500 @@ -33,8 +33,12 @@ local role_inheritance = { ["prosody:operator"] = "prosody:admin"; - ["prosody:admin"] = "prosody:user"; - ["prosody:user"] = "prosody:restricted"; + ["prosody:admin"] = "prosody:member"; + ["prosody:member"] = "prosody:registered"; + ["prosody:registered"] = "prosody:guest"; + + -- COMPAT + ["prosody:user"] = "prosody:registered"; }; local function role_may(host, role_name, permission) diff -r eade7ff9f52c -r 62c6e17a5e9d mod_default_bookmarks/README.markdown --- a/mod_default_bookmarks/README.markdown Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_default_bookmarks/README.markdown Mon Sep 18 08:24:19 2023 -0500 @@ -31,13 +31,15 @@ Then add a list of the default rooms you want: - default_bookmarks = { - { jid = "room@conference.example.com", name = "The Room" }; - -- Specifying a password is supported: - { jid = "secret-room@conference.example.com", name = "A Secret Room", password = "secret" }; - -- You can also use this compact syntax: - "yetanother@conference.example.com"; -- this will get "yetanother" as name - }; +``` lua +default_bookmarks = { + { jid = "room@conference.example.com"; name = "The Room"; autojoin = true }; + -- Specifying a password is supported: + { jid = "secret-room@conference.example.com"; name = "A Secret Room"; password = "secret"; autojoin = true }; + -- You can also use this compact syntax: + "yetanother@conference.example.com"; -- this will get "yetanother" as name +}; +``` Compatibility ------------- diff -r eade7ff9f52c -r 62c6e17a5e9d mod_firewall/README.markdown --- a/mod_firewall/README.markdown Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_firewall/README.markdown Mon Sep 18 08:24:19 2023 -0500 @@ -10,6 +10,8 @@ mod_firewall.definitions: definitions.lib.lua mod_firewall.marks: marks.lib.lua mod_firewall.test: test.lib.lua + copy_directories: + - scripts --- ------------------------------------------------------------------------ @@ -253,12 +255,13 @@ ### Sender/recipient matching - Condition Matches - ------------- ------------------------------------------------------- - `FROM` The JID in the 'from' attribute matches the given JID. - `TO` The JID in the 'to' attribute matches the given JID. - `TO SELF` The stanza is sent by any of a user's resources to their own bare JID. - `TO FULL JID` The stanza is addressed to a valid full JID on the local server (full JIDs include a resource at the end, and only exist for the lifetime of a single session, therefore the recipient must be online, or this check will not match). + Condition Matches + --------------- ------------------------------------------------------- + `FROM` The JID in the 'from' attribute matches the given JID. + `TO` The JID in the 'to' attribute matches the given JID. + `TO SELF` The stanza is sent by any of a user's resources to their own bare JID. + `TO FULL JID` The stanza is addressed to a **valid** full JID on the local server (full JIDs include a resource at the end, and only exist for the lifetime of a single session, therefore the recipient **must be online**, or this check will not match). + `FROM FULL JID` The stanza is from a full JID (unlike `TO FULL JID` this check is on the format of the JID only). The TO and FROM conditions both accept wildcards in the JID when it is enclosed in angle brackets ('\<...\>'). For example: diff -r eade7ff9f52c -r 62c6e17a5e9d mod_firewall/actions.lib.lua --- a/mod_firewall/actions.lib.lua Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_firewall/actions.lib.lua Mon Sep 18 08:24:19 2023 -0500 @@ -220,11 +220,29 @@ end function action_handlers.MARK_USER(name) - return [[if session.firewall_marks then session.firewall_marks.]]..idsafe(name)..[[ = current_timestamp; end]], { "timestamp" }; + return ([[if session.username and session.host == current_host then + fire_event("firewall/marked/user", { + username = session.username; + mark = %q; + timestamp = current_timestamp; + }); + else + log("warn", "Attempt to MARK a remote user - only local users may be marked"); + end]]):format(assert(idsafe(name), "Invalid characters in mark name: "..name)), { + "current_host"; + "timestamp"; + }; end function action_handlers.UNMARK_USER(name) - return [[if session.firewall_marks then session.firewall_marks.]]..idsafe(name)..[[ = nil; end]], { "timestamp" }; + return ([[if session.username and session.host == current_host then + fire_event("firewall/unmarked/user", { + username = session.username; + mark = %q; + }); + else + log("warn", "Attempt to UNMARK a remote user - only local users may be marked"); + end]]):format(assert(idsafe(name), "Invalid characters in mark name: "..name)); end function action_handlers.ADD_TO(spec) diff -r eade7ff9f52c -r 62c6e17a5e9d mod_firewall/conditions.lib.lua --- a/mod_firewall/conditions.lib.lua Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_firewall/conditions.lib.lua Mon Sep 18 08:24:19 2023 -0500 @@ -67,6 +67,10 @@ return compile_jid_match("from", from), { "split_from" }; end +function condition_handlers.FROM_FULL_JID() + return "not "..compile_jid_match_part("from_resource", nil), { "split_from" }; +end + function condition_handlers.FROM_EXACTLY(from) local metadeps = {}; return ("from == %s"):format(metaq(from, metadeps)), { "from", unpack(metadeps) }; @@ -310,7 +314,9 @@ error("Error parsing mark name, see documentation for usage examples"); end if time then - return ("(current_timestamp - (session.firewall_marks and session.firewall_marks.%s or 0)) < %d"):format(idsafe(name), tonumber(time)), { "timestamp" }; + return ([[( + current_timestamp - (session.firewall_marks and session.firewall_marks.%s or 0) + ) < %d]]):format(idsafe(name), tonumber(time)), { "timestamp" }; end return ("not not (session.firewall_marks and session.firewall_marks."..idsafe(name)..")"); end @@ -341,7 +347,13 @@ if not (search_name) then error("Error parsing SCAN expression, syntax: SEARCH for PATTERN in LIST"); end - return ("scan_list(list_%s, %s)"):format(list_name, "tokens_"..search_name.."_"..pattern_name), { "scan_list", "tokens:"..search_name.."-"..pattern_name, "list:"..list_name }; + return ("scan_list(list_%s, %s)"):format( + list_name, + "tokens_"..search_name.."_"..pattern_name + ), { + "scan_list", + "tokens:"..search_name.."-"..pattern_name, "list:"..list_name + }; end -- COUNT: lines in body < 10 @@ -361,7 +373,12 @@ end local comp_op = comparator_expression:gsub("%s+", ""); assert(valid_comp_ops[comp_op], "Error parsing COUNT expression, unknown comparison operator: "..comp_op); - return ("it_count(search_%s:gmatch(pattern_%s)) %s %d"):format(search_name, pattern_name, comp_op, value), { "it_count", "search:"..search_name, "pattern:"..pattern_name }; + return ("it_count(search_%s:gmatch(pattern_%s)) %s %d"):format( + search_name, pattern_name, comp_op, value + ), { + "it_count", + "search:"..search_name, "pattern:"..pattern_name + }; end return condition_handlers; diff -r eade7ff9f52c -r 62c6e17a5e9d mod_firewall/marks.lib.lua --- a/mod_firewall/marks.lib.lua Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_firewall/marks.lib.lua Mon Sep 18 08:24:19 2023 -0500 @@ -1,23 +1,35 @@ local mark_storage = module:open_store("firewall_marks"); +local mark_map_storage = module:open_store("firewall_marks", "map"); local user_sessions = prosody.hosts[module.host].sessions; -module:hook("resource-bind", function (event) - local session = event.session; - local username = session.username; - local user = user_sessions[username]; - local marks = user.firewall_marks; - if not marks then - marks = mark_storage:get(username) or {}; - user.firewall_marks = marks; -- luacheck: ignore 122 +module:hook("firewall/marked/user", function (event) + local user = user_sessions[event.username]; + local marks = user and user.firewall_marks; + if user and not marks then + -- Load marks from storage to cache on the user object + marks = mark_storage:get(event.username) or {}; + user.firewall_marks = marks; --luacheck: ignore 122 + end + if marks then + marks[event.mark] = event.timestamp; + end + local ok, err = mark_map_storage:set(event.username, event.mark, event.timestamp); + if not ok then + module:log("error", "Failed to mark user %q with %q: %s", event.username, event.mark, err); end - session.firewall_marks = marks; -end); + return true; +end, -1); -module:hook("resource-unbind", function (event) - local session = event.session; - local username = session.username; - local marks = session.firewall_marks; - mark_storage:set(username, marks); -end); - +module:hook("firewall/unmarked/user", function (event) + local user = user_sessions[event.username]; + local marks = user and user.firewall_marks; + if marks then + marks[event.mark] = nil; + end + local ok, err = mark_map_storage:set(event.username, event.mark, nil); + if not ok then + module:log("error", "Failed to unmark user %q with %q: %s", event.username, event.mark, err); + end + return true; +end, -1); diff -r eade7ff9f52c -r 62c6e17a5e9d mod_firewall/mod_firewall.lua --- a/mod_firewall/mod_firewall.lua Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_firewall/mod_firewall.lua Mon Sep 18 08:24:19 2023 -0500 @@ -316,7 +316,7 @@ local condition_handlers = module:require("conditions"); local action_handlers = module:require("actions"); -if module:get_option_boolean("firewall_experimental_user_marks", false) then +if module:get_option_boolean("firewall_experimental_user_marks", true) then module:require"marks"; end @@ -742,3 +742,43 @@ print("end -- End of file "..filename); end end + + +-- Console + +local console_env = module:shared("/*/admin_shell/env"); + +console_env.firewall = {}; + +function console_env.firewall:mark(user_jid, mark_name) + local username, host = jid.split(user_jid); + if not username or not hosts[host] then + return nil, "Invalid JID supplied"; + elseif not idsafe(mark_name) then + return nil, "Invalid characters in mark name"; + end + if not module:context(host):fire_event("firewall/marked/user", { + username = session.username; + mark = mark_name; + timestamp = os.time(); + }) then + return nil, "Mark not set - is mod_firewall loaded on that host?"; + end + return true, "User marked"; +end + +function console_env.firewall:unmark(jid, mark_name) + local username, host = jid.split(user_jid); + if not username or not hosts[host] then + return nil, "Invalid JID supplied"; + elseif not idsafe(mark_name) then + return nil, "Invalid characters in mark name"; + end + if not module:context(host):fire_event("firewall/unmarked/user", { + username = session.username; + mark = mark_name; + }) then + return nil, "Mark not removed - is mod_firewall loaded on that host?"; + end + return true, "User unmarked"; +end diff -r eade7ff9f52c -r 62c6e17a5e9d mod_firewall/scripts/spam-blocking.pfw --- a/mod_firewall/scripts/spam-blocking.pfw Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_firewall/scripts/spam-blocking.pfw Mon Sep 18 08:24:19 2023 -0500 @@ -97,6 +97,12 @@ TYPE: groupchat PASS. +# Mediated MUC invitations are naturally from 'strangers' and have special +# handling. We lean towards accepting them, unless overridden by custom rules. +NOT FROM FULL JID? +INSPECT: {http://jabber.org/protocol/muc#user}x/invite +JUMP CHAIN=user/spam_check_muc_invite + # Non-chat message types often generate pop-ups in clients, # so we won't accept them from strangers NOT TYPE: chat @@ -138,6 +144,18 @@ ################################################################## +#### Rules for MUC invitations ################################### + +::user/spam_check_muc_invite + +# This chain can be used to inspect the invitation and determine +# the appropriate action. Otherwise, we proceed with the default +# action below. +JUMP CHAIN=user/spam_check_muc_invite_custom + +# Allow mediated MUC invitations by default +PASS. + #### Stanzas reaching this chain will be rejected ################ ::user/spam_reject @@ -151,7 +169,7 @@ ################################################################## -#### Stanzas that may be spam, but we're not sure either way###### +#### Stanzas that may be spam, but we're not sure either way ##### ::user/spam_handle_unknown # This chain can be used by other scripts diff -r eade7ff9f52c -r 62c6e17a5e9d mod_firewall/scripts/spam-blocklists.pfw --- a/mod_firewall/scripts/spam-blocklists.pfw Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_firewall/scripts/spam-blocklists.pfw Mon Sep 18 08:24:19 2023 -0500 @@ -8,3 +8,13 @@ CHECK LIST: blocklist contains $<@from|host> BOUNCE=policy-violation (Your server is blocked due to spam) + +::user/spam_check_muc_invite_custom + +# Check the server we received the invitation from +CHECK LIST: blocklist contains $<@from|host> +BOUNCE=policy-violation (Your server is blocked due to spam) + +# Check the inviter's JID against the blocklist, too +CHECK LIST: blocklist contains $<{http://jabber.org/protocol/muc#user}x/invite@from|host> +BOUNCE=policy-violation (Your server is blocked due to spam) diff -r eade7ff9f52c -r 62c6e17a5e9d mod_groups_oidc/README.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_groups_oidc/README.md Mon Sep 18 08:24:19 2023 -0500 @@ -0,0 +1,12 @@ +--- +summary: OIDC group membership in UserInfo +labels: +- Stage-Alpha +rockspec: + dependencies: + - mod_http_oauth2 >= 200 + - mod_groups_internal +--- + +This module exposes [mod_groups_internal] groups to +[OAuth 2.0][mod_http_oauth2] clients via a `groups` scope/claim. diff -r eade7ff9f52c -r 62c6e17a5e9d mod_groups_oidc/mod_groups_oidc.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_groups_oidc/mod_groups_oidc.lua Mon Sep 18 08:24:19 2023 -0500 @@ -0,0 +1,15 @@ +local array = require "util.array"; + +module:add_item("openid-claim", "groups"); + +local group_memberships = module:open_store("groups", "map"); +local function user_groups(username) + return pairs(group_memberships:get_all(username) or {}); +end + +module:hook("token/userinfo", function(event) + local userinfo = event.userinfo; + if event.claims:contains("groups") then + userinfo.groups = array(user_groups(event.username)); + end +end); diff -r eade7ff9f52c -r 62c6e17a5e9d mod_http_debug/README.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_http_debug/README.md Mon Sep 18 08:24:19 2023 -0500 @@ -0,0 +1,40 @@ +--- +summary: HTTP module returning info about requests for debugging +--- + +This module returns some info about HTTP requests as Prosody sees them +from an endpoint like `http://xmpp.example.net:5281/debug`. This can be +used to validate [reverse-proxy configuration][doc:http] and similar use +cases. + +# Example + +``` +$ curl -sSf https://xmpp.example.net:5281/debug | json_pp +{ + "body" : "", + "headers" : { + "accept" : "*/*", + "host" : "xmpp.example.net:5281", + "user_agent" : "curl/7.74.0" + }, + "httpversion" : "1.1", + "id" : "jmFROQKoduU3", + "ip" : "127.0.0.1", + "method" : "GET", + "path" : "/debug", + "secure" : true, + "url" : { + "path" : "/debug" + } +} +``` + +# Configuration + +HTTP Methods handled can be configured via the `http_debug_methods` +setting. By default, the most common methods are already enabled. + +```lua +http_debug_methods = { "GET"; "HEAD"; "DELETE"; "OPTIONS"; "PATCH"; "POST"; "PUT" }; +``` diff -r eade7ff9f52c -r 62c6e17a5e9d mod_http_debug/mod_http_debug.lua --- a/mod_http_debug/mod_http_debug.lua Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_http_debug/mod_http_debug.lua Mon Sep 18 08:24:19 2023 -0500 @@ -1,26 +1,34 @@ local json = require "util.json" module:depends("http") +local function handle_request(event) + local request = event.request; + (request.log or module._log)("debug", "%s -- %s %q HTTP/%s -- %q -- %s", request.ip, request.method, request.url, request.httpversion, request.headers, request.body); + return { + status_code = 200; + headers = { content_type = "application/json" }; + host = module.host; + body = json.encode { + body = request.body; + headers = request.headers; + httpversion = request.httpversion; + id = request.id; + ip = request.ip; + method = request.method; + path = request.path; + secure = request.secure; + url = request.url; + }; + } +end + +local methods = module:get_option_set("http_debug_methods", { "GET"; "HEAD"; "DELETE"; "OPTIONS"; "PATCH"; "POST"; "PUT" }); +local route = {}; +for method in methods do + route[method] = handle_request; + route[method .. " /*"] = handle_request; +end + module:provides("http", { - route = { - GET = function(event) - local request = event.request; - return { - status_code = 200; - headers = { - content_type = "application/json", - }, - body = json.encode { - body = request.body; - headers = request.headers; - httpversion = request.httpversion; - ip = request.ip; - method = request.method; - path = request.path; - secure = request.secure; - url = request.url; - } - } - end; - } - }) + route = route; +}) diff -r eade7ff9f52c -r 62c6e17a5e9d mod_http_dir_listing/README.markdown --- a/mod_http_dir_listing/README.markdown Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_http_dir_listing/README.markdown Mon Sep 18 08:24:19 2023 -0500 @@ -2,9 +2,9 @@ rockspec: build: copy_directories: - - mod_http_dir_listing/http_dir_listing/resources + - http_dir_listing/resources modules: - mod_http_dir_listing: mod_http_dir_listing/http_dir_listing/mod_http_dir_listing.lua + mod_http_dir_listing: http_dir_listing/mod_http_dir_listing.lua summary: HTTP directory listing ... diff -r eade7ff9f52c -r 62c6e17a5e9d mod_http_dir_listing2/README.markdown --- a/mod_http_dir_listing2/README.markdown Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_http_dir_listing2/README.markdown Mon Sep 18 08:24:19 2023 -0500 @@ -1,6 +1,10 @@ --- summary: HTTP directory listing -... +rockspec: + build: + copy_directories: + - resources +--- Introduction ============ diff -r eade7ff9f52c -r 62c6e17a5e9d mod_http_muc_log/mod_http_muc_log.lua --- a/mod_http_muc_log/mod_http_muc_log.lua Mon Sep 18 08:22:07 2023 -0500 +++ b/mod_http_muc_log/mod_http_muc_log.lua Mon Sep 18 08:24:19 2023 -0500 @@ -128,17 +128,42 @@ local presence_logged = module:get_option_boolean("muc_log_presences", false); -local function hide_presence(request) +local function show_presence(request) --> boolean|nil + -- boolean -> yes or no + -- nil -> dunno if not presence_logged then - return false; + -- No presence stored, skip + return nil; end if request.url.query then local data = httplib.formdecode(request.url.query); - if data then - return data.p == "h" + if type(data) == "table" then + if data.p == "s" or data.p == "h" then + return data.p == "s"; + end end end - return false; +end + +local function presence_with(request) + local show = show_presence(request); + if show == true then + return nil; -- no filter, everything + elseif show == false or show == nil then + -- only messages + return "message ?p=[sh] + local show = show_presence(request); + if show == true then + return { p = "s" } + elseif show == false then + return { p = "h" } + else + return nil; + end end local function get_dates(room) --> { integer, ... } @@ -254,7 +279,8 @@ room = room_obj._data; jid = room_obj.jid; jid_node = jid_split(room_obj.jid); - hide_presence = hide_presence(request); + q = presence_query(request); + show_presence = show_presence(request); presence_available = presence_logged; dates = date_list; links = { @@ -268,10 +294,16 @@ local function logs_page(event, path) local request, response = event.request, event.response; - local room, date = path:match("^([^/]+)/([^/]*)/?$"); - if not room then + -- /room --> 303 /room/ + -- /room/ --> calendar view + -- /room/yyyy-mm-dd --> logs view + -- /room/yyyy-mm-dd/* --> 404 + local room, date = path:match("^([^/]+)/([^/]*)$"); + if not room and not path:find"/" then response.headers.location = url.build({ path = path .. "/" }); return 303; + elseif not room then + return 404; end room = nodeprep(room); if not room then @@ -300,7 +332,7 @@ local iter, err = archive:find(room, { ["start"] = day_start; ["end"] = day_start + 86399; - ["with"] = hide_presence(request) and "messageJoin via web } {links# -
  • {item.text}
  • } +
  • {item.text}
  • } @@ -28,7 +28,7 @@