From 7881c5d0935b643dc18428a190d50d863b7bed51 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 17 Mar 2022 10:22:47 +0000 Subject: mod_admin_shell: Add session.write() method to write data to client with no \n --- plugins/mod_admin_shell.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 35124e79..c0aed977 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -83,8 +83,8 @@ function runner_callbacks:error(err) self.data.print("Error: "..tostring(err)); end -local function send_repl_output(session, line) - return session.send(st.stanza("repl-output"):text(tostring(line))); +local function send_repl_output(session, line, attr) + return session.send(st.stanza("repl-output", attr):text(tostring(line))); end function console:new_session(admin_session) @@ -99,6 +99,9 @@ function console:new_session(admin_session) end return send_repl_output(admin_session, table.concat(t, "\t")); end; + write = function (t) + return send_repl_output(admin_session, t, { eol = "0" }); + end; serialize = tostring; disconnect = function () admin_session:close(); end; }; -- cgit v1.2.3 From 119725bbe3a4182fc573a32805a46069d2affa0b Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 17 Mar 2022 10:23:12 +0000 Subject: mod_admin_shell: Add session.is_connected() method --- plugins/mod_admin_shell.lua | 3 +++ 1 file changed, 3 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index c0aed977..28536c53 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -104,6 +104,9 @@ function console:new_session(admin_session) end; serialize = tostring; disconnect = function () admin_session:close(); end; + is_connected = function () + return not not admin_session.conn; + end }; session.env = setmetatable({}, default_env_mt); -- cgit v1.2.3 From 36fbcd47721f51abfd59ec3a58286c3c6aedd7e0 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 17 Mar 2022 10:24:38 +0000 Subject: mod_admin_shell: Add watch:log() command to tail logs in realtime --- plugins/mod_admin_shell.lua | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 28536c53..9af77676 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -36,6 +36,7 @@ local serialization = require "util.serialization"; local serialize_config = serialization.new ({ fatal = false, unquoted = true}); local time = require "util.time"; local promise = require "util.promise"; +local logger = require "util.logger"; local t_insert = table.insert; local t_concat = table.concat; @@ -1589,6 +1590,26 @@ function def_env.http:list(hosts) return true; end +def_env.watch = {}; + +function def_env.watch:log() + local writing = false; + local sink = logger.add_simple_sink(function (source, level, message) + if writing then return; end + writing = true; + self.session.print(source, level, message); + writing = false; + end); + + while self.session.is_connected() do + async.sleep(3); + end + if not logger.remove_sink(sink) then + module:log("warn", "Unable to remove watch:log() sink"); + end +end + + def_env.debug = {}; function def_env.debug:logevents(host) -- cgit v1.2.3 From 6fea6634dd465da3ac1de24c83d2cd1021009404 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 6 Mar 2017 15:19:35 +0100 Subject: adhoc: Include stanza and origin in adhoc event data This allows easier access to these, which could be useful for all sorts of reasons --- plugins/adhoc/adhoc.lib.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'plugins') diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua index 4cf6911d..eb91f252 100644 --- a/plugins/adhoc/adhoc.lib.lua +++ b/plugins/adhoc/adhoc.lib.lua @@ -34,6 +34,8 @@ function _M.handle_cmd(command, origin, stanza) local cmdtag = stanza.tags[1] local sessionid = cmdtag.attr.sessionid or uuid.generate(); local dataIn = { + origin = origin; + stanza = stanza; to = stanza.attr.to; from = stanza.attr.from; action = cmdtag.attr.action or "execute"; -- cgit v1.2.3 From 4dc57d01f124c16e4536bdc12dda77c563b00816 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 22 Mar 2022 18:07:11 +0100 Subject: mod_adhoc: Simplify variable references Since commands[node] was already stored in a local a few lines up --- plugins/adhoc/mod_adhoc.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua index 09a72075..9d6ff77a 100644 --- a/plugins/adhoc/mod_adhoc.lua +++ b/plugins/adhoc/mod_adhoc.lua @@ -79,12 +79,12 @@ module:hook("iq-set/host/"..xmlns_cmd..":command", function (event) or (command.permission == "global_admin" and not global_admin) or (command.permission == "local_user" and hostname ~= module.host) then origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up() - :add_child(commands[node]:cmdtag("canceled") + :add_child(command:cmdtag("canceled") :tag("note", {type="error"}):text("You don't have permission to execute this command"))); return true end -- User has permission now execute the command - adhoc_handle_cmd(commands[node], origin, stanza); + adhoc_handle_cmd(command, origin, stanza); return true; end end, 500); -- cgit v1.2.3 From 259df23ffb16efde4653a33557c95c3f045f15d4 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 23 Mar 2022 15:25:22 +0000 Subject: mod_s2s: Store real stanzas in session.sendq, rather than strings This is the "right" thing to do. Strings were more memory-efficient, but e.g. bypassed stanza filters at reconnection time. Also not being stanzas prevents us from potential future work, such as merging sendq with mod_smacks. Regarding performance: we should counter the probable negative effect of this change with other positive changes that are desired anyway - e.g. a limit on the size of the sendq, improved in-memory representation of stanzas, s2s backoff (e.g. if a remote server is persistently unreachable, cache this failure for a while and don't just keep forever queuing stanzas for it). --- plugins/mod_s2s.lua | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index 3ad0f521..b9cd5fcd 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -146,16 +146,14 @@ local function bounce_sendq(session, reason) elseif type(reason) == "string" then reason_text = reason; end - for i, data in ipairs(sendq) do - local reply = data[2]; - if reply and not(reply.attr.xmlns) and bouncy_stanzas[reply.name] then - reply.attr.type = "error"; - reply:tag("error", {type = error_type, by = session.from_host}) - :tag(condition, {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up(); - if reason_text then - reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}) - :text("Server-to-server connection failed: "..reason_text):up(); - end + for i, stanza in ipairs(sendq) do + if not stanza.attr.xmlns and bouncy_stanzas[stanza.name] then + local reply = st.error_reply( + stanza, + error_type, + condition, + reason_text and ("Server-to-server connection failed: "..reason_text) or nil + ); core_process_stanza(dummy, reply); end sendq[i] = nil; @@ -182,15 +180,11 @@ function route_to_existing_session(event) (host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host); -- Queue stanza until we are able to send it - local queued_item = { - tostring(stanza), - stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza); - }; if host.sendq then - t_insert(host.sendq, queued_item); + t_insert(host.sendq, st.clone(stanza)); else -- luacheck: ignore 122 - host.sendq = { queued_item }; + host.sendq = { st.clone(stanza) }; end host.log("debug", "stanza [%s] queued ", stanza.name); return true; @@ -215,7 +209,7 @@ function route_to_new_session(event) -- Store in buffer host_session.bounce_sendq = bounce_sendq; - host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} }; + host_session.sendq = { st.clone(stanza) }; log("debug", "stanza [%s] queued until connection complete", stanza.name); -- FIXME Cleaner solution to passing extra data from resolvers to net.server -- This mt-clone allows resolvers to add extra data, currently used for DANE TLSA records @@ -324,8 +318,8 @@ function mark_connected(session) if sendq then session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host); local send = session.sends2s; - for i, data in ipairs(sendq) do - send(data[1]); + for i, stanza in ipairs(sendq) do + send(stanza); sendq[i] = nil; end session.sendq = nil; -- cgit v1.2.3 From 611a10346961579f4c73fd57c12ebefaf955e891 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 23 Mar 2022 13:42:44 +0000 Subject: mod_debug_stanzas/watcher: New module library to dynamically 'watch' for stanzas --- plugins/mod_debug_stanzas/watcher.lib.lua | 220 ++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 plugins/mod_debug_stanzas/watcher.lib.lua (limited to 'plugins') diff --git a/plugins/mod_debug_stanzas/watcher.lib.lua b/plugins/mod_debug_stanzas/watcher.lib.lua new file mode 100644 index 00000000..e21fc946 --- /dev/null +++ b/plugins/mod_debug_stanzas/watcher.lib.lua @@ -0,0 +1,220 @@ +local filters = require "util.filters"; +local jid = require "util.jid"; +local set = require "util.set"; + +local client_watchers = {}; + +-- active_filters[session] = { +-- filter_func = filter_func; +-- downstream = { cb1, cb2, ... }; +-- } +local active_filters = {}; + +local function subscribe_session_stanzas(session, handler, reason) + if active_filters[session] then + table.insert(active_filters[session].downstream, handler); + if reason then + handler(reason, nil, session); + end + return; + end + local downstream = { handler }; + active_filters[session] = { + filter_in = function (stanza) + module:log("debug", "NOTIFY WATCHER %d", #downstream); + for i = 1, #downstream do + downstream[i]("received", stanza, session); + end + return stanza; + end; + filter_out = function (stanza) + module:log("debug", "NOTIFY WATCHER %d", #downstream); + for i = 1, #downstream do + downstream[i]("sent", stanza, session); + end + return stanza; + end; + downstream = downstream; + }; + filters.add_filter(session, "stanzas/in", active_filters[session].filter_in); + filters.add_filter(session, "stanzas/out", active_filters[session].filter_out); + if reason then + handler(reason, nil, session); + end +end + +local function unsubscribe_session_stanzas(session, handler, reason) + local active_filter = active_filters[session]; + if not active_filter then + return; + end + for i = #active_filter.downstream, 1, -1 do + if active_filter.downstream[i] == handler then + table.remove(active_filter.downstream, i); + if reason then + handler(reason, nil, session); + end + end + end + if #active_filter.downstream == 0 then + filters.remove_filter(session, "stanzas/in", active_filter.filter_in); + filters.remove_filter(session, "stanzas/out", active_filter.filter_out); + end + active_filters[session] = nil; +end + +local function unsubscribe_all_from_session(session, reason) + local active_filter = active_filters[session]; + if not active_filter then + return; + end + for i = #active_filter.downstream, 1, -1 do + local handler = table.remove(active_filter.downstream, i); + if reason then + handler(reason, nil, session); + end + end + filters.remove_filter(session, "stanzas/in", active_filter.filter_in); + filters.remove_filter(session, "stanzas/out", active_filter.filter_out); + active_filters[session] = nil; +end + +local function unsubscribe_handler_from_all(handler, reason) + for session in pairs(active_filters) do + unsubscribe_session_stanzas(session, handler, reason); + end +end + +local s2s_watchers = {}; + +module:hook("s2sin-established", function (event) + for _, watcher in ipairs(s2s_watchers) do + if watcher.target_spec == event.session.from_host then + subscribe_session_stanzas(event.session, watcher.handler, "opened"); + end + end +end); + +module:hook("s2sout-established", function (event) + for _, watcher in ipairs(s2s_watchers) do + if watcher.target_spec == event.session.to_host then + subscribe_session_stanzas(event.session, watcher.handler, "opened"); + end + end +end); + +module:hook("s2s-closed", function (event) + unsubscribe_all_from_session(event.session, "closed"); +end); + +local watched_hosts = set.new(); + +local handler_map = setmetatable({}, { __mode = "kv" }); + +local function add_stanza_watcher(spec, orig_handler) + local function filtering_handler(event_type, stanza, session) + if stanza and spec.filter_spec then + if spec.filter_spec.with_jid then + if event_type == "sent" and (not stanza.attr.from or not jid.compare(stanza.attr.from, spec.filter_spec.with_jid)) then + return; + elseif event_type == "received" and (not stanza.attr.to or not jid.compare(stanza.attr.to, spec.filter_spec.with_jid)) then + return; + end + end + end + return orig_handler(event_type, stanza, session); + end + handler_map[orig_handler] = filtering_handler; + if spec.target_spec.jid then + local target_is_remote_host = not jid.node(spec.target_spec.jid) and not prosody.hosts[spec.target_spec.jid]; + + if target_is_remote_host then + -- Watch s2s sessions + table.insert(s2s_watchers, { + target_spec = spec.target_spec.jid; + handler = filtering_handler; + orig_handler = orig_handler; + }); + + -- Scan existing s2sin for matches + for session in pairs(prosody.incoming_s2s) do + if spec.target_spec.jid == session.from_host then + subscribe_session_stanzas(session, filtering_handler, "attached"); + end + end + -- Scan existing s2sout for matches + for local_host, local_session in pairs(prosody.hosts) do --luacheck: ignore 213/local_host + for remote_host, remote_session in pairs(local_session.s2sout) do + if spec.target_spec.jid == remote_host then + subscribe_session_stanzas(remote_session, filtering_handler, "attached"); + end + end + end + else + table.insert(client_watchers, { + target_spec = spec.target_spec.jid; + handler = filtering_handler; + orig_handler = orig_handler; + }); + local host = jid.host(spec.target_spec.jid); + if not watched_hosts:contains(host) and prosody.hosts[host] then + module:context(host):hook("resource-bind", function (event) + for _, watcher in ipairs(client_watchers) do + module:log("debug", "NEW CLIENT: %s vs %s", event.session.full_jid, watcher.target_spec); + if jid.compare(event.session.full_jid, watcher.target_spec) then + module:log("debug", "MATCH"); + subscribe_session_stanzas(event.session, watcher.handler, "opened"); + else + module:log("debug", "NO MATCH"); + end + end + end); + + module:context(host):hook("resource-unbind", function (event) + unsubscribe_all_from_session(event.session, "closed"); + end); + + watched_hosts:add(host); + end + for full_jid, session in pairs(prosody.full_sessions) do + if jid.compare(full_jid, spec.target_spec.jid) then + subscribe_session_stanzas(session, filtering_handler, "attached"); + end + end + end + else + error("No recognized target selector"); + end +end + +local function remove_stanza_watcher(orig_handler) + local handler = handler_map[orig_handler]; + unsubscribe_handler_from_all(handler, "detached"); + handler_map[orig_handler] = nil; + + for i = #client_watchers, 1, -1 do + if client_watchers[i].orig_handler == orig_handler then + table.remove(client_watchers, i); + end + end + + for i = #s2s_watchers, 1, -1 do + if s2s_watchers[i].orig_handler == orig_handler then + table.remove(s2s_watchers, i); + end + end +end + +local function cleanup(reason) + client_watchers = {}; + s2s_watchers = {}; + for session in pairs(active_filters) do + unsubscribe_all_from_session(session, reason or "cancelled"); + end +end + +return { + add = add_stanza_watcher; + remove = remove_stanza_watcher; + cleanup = cleanup; +}; -- cgit v1.2.3 From 1ef09e4285fafaffe133fcaab95d790865cd5ad9 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 23 Mar 2022 13:43:08 +0000 Subject: mod_admin_shell: Add watch:stanzas() command --- plugins/mod_admin_shell.lua | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 9af77676..ae7e3c7c 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -1609,6 +1609,40 @@ function def_env.watch:log() end end +local stanza_watchers = module:require("mod_debug_stanzas/watcher"); +function def_env.watch:stanzas(target_spec, filter_spec) + local function handler(event_type, stanza, session) + if stanza then + if event_type == "sent" then + self.session.print(("\n"):format(session.id)); + elseif event_type == "received" then + self.session.print(("\n"):format(session.id)); + else + self.session.print(("\n"):format(event_type, session.id)); + end + self.session.print(stanza); + elseif session then + self.session.print("\n"); + elseif event_type then + self.session.print("\n"); + end + end + + stanza_watchers.add({ + target_spec = { + jid = target_spec; + }; + filter_spec = filter_spec and { + with_jid = filter_spec; + }; + }, handler); + + while self.session.is_connected() do + async.sleep(3); + end + + stanza_watchers.remove(handler); +end def_env.debug = {}; @@ -1953,6 +1987,10 @@ function def_env.stats:show(name_filter) end +function module.unload() + stanza_watchers.cleanup(); +end + ------------- -- cgit v1.2.3 From 5db031e07065a5cbeded76b7b3971a089f62903f Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 24 Apr 2022 16:17:32 +0200 Subject: mod_smacks: Improve activation of smacks on outgoing s2s Using a timer was a hack to get around that stream features are not available at the right time and sendq stanzas were stored as strings so could not be counted properly. The later has now been fixed and the former is fixed by recording the relevant stream feature on the session so that the correct version of XEP-0198 can be activated once the connection has been authenticated and is ready to start. --- plugins/mod_smacks.lua | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index ce59248e..0d2016fc 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -2,7 +2,7 @@ -- -- Copyright (C) 2010-2015 Matthew Wild -- Copyright (C) 2010 Waqas Hussain --- Copyright (C) 2012-2021 Kim Alvefur +-- Copyright (C) 2012-2022 Kim Alvefur -- Copyright (C) 2012 Thijs Alkemade -- Copyright (C) 2014 Florian Zeitz -- Copyright (C) 2016-2020 Thilo Molitor @@ -10,6 +10,7 @@ -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- +-- TODO unify sendq and smqueue local tonumber = tonumber; local tostring = tostring; @@ -322,26 +323,20 @@ end module:hook_tag(xmlns_sm2, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm2); end, 100); module:hook_tag(xmlns_sm3, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm3); end, 100); -module:hook_tag("http://etherx.jabber.org/streams", "features", - function (session, stanza) - -- Needs to be done after flushing sendq since those aren't stored as - -- stanzas and counting them is weird. - -- TODO unify sendq and smqueue - timer.add_task(1e-6, function () - if can_do_smacks(session) then - if stanza:get_child("sm", xmlns_sm3) then - session.sends2s(st.stanza("enable", sm3_attr)); - session.smacks = xmlns_sm3; - elseif stanza:get_child("sm", xmlns_sm2) then - session.sends2s(st.stanza("enable", sm2_attr)); - session.smacks = xmlns_sm2; - else - return; - end - wrap_session_out(session, false); - end - end); - end); +module:hook_tag("http://etherx.jabber.org/streams", "features", function(session, stanza) + if can_do_smacks(session) then + session.smacks_feature = stanza:get_child("sm", xmlns_sm3) or stanza:get_child("sm", xmlns_sm2); + end +end); + +module:hook("s2sout-established", function (event) + local session = event.session; + if not session.smacks_feature then return end + + session.smacks = session.smacks_feature.attr.xmlns; + session.sends2s(st.stanza("enable", { xmlns = session.smacks })); + wrap_session_out(session, false); +end); function handle_enabled(session, stanza, xmlns_sm) -- luacheck: ignore 212/stanza module:log("debug", "Enabling stream management"); -- cgit v1.2.3 From 192e0081ce78d4bbd10b9e65d0b69ffaa9ce9117 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 25 Apr 2022 14:36:56 +0200 Subject: mod_s2s: Recognise and report errors with CA or intermediate certs Should be invoked for cases such as when the Let's Encrypt intermediate certificate expired not too long ago. --- plugins/mod_s2s.lua | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index b9cd5fcd..3e86e94c 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -918,6 +918,14 @@ local function friendly_cert_error(session) --> string elseif cert_errors:contains("self signed certificate") then return "is self-signed"; end + + local chain_errors = set.new(session.cert_chain_errors[2]); + for i, e in pairs(session.cert_chain_errors) do + if i > 2 then chain_errors:add_list(e); end + end + if chain_errors:contains("certificate has expired") then + return "has an expired certificate chain"; + end end return "is not trusted"; -- for some other reason elseif session.cert_identity_status == "invalid" then -- cgit v1.2.3 From f40337890ee71a17a80f86af3e2789f3a508b94d Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 25 Apr 2022 14:41:54 +0200 Subject: mod_s2s: Distinguish DANE TLSA errors from generic cert chain errors Otherwise it would just report "is not trusted" unless you inspect the logs. This message is sent to to the remote server, and will hopefully show up in their logs, allowing the admin to fix their DANE setup. --- plugins/mod_s2s.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index 3e86e94c..5f60e01c 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -925,6 +925,8 @@ local function friendly_cert_error(session) --> string end if chain_errors:contains("certificate has expired") then return "has an expired certificate chain"; + elseif chain_errors:contains("No matching DANE TLSA records") then + return "does not match any DANE TLSA records"; end end return "is not trusted"; -- for some other reason -- cgit v1.2.3 From 38346dd6f1dcd963e17722bf175445465d7683f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Wed, 27 Apr 2022 17:44:14 +0200 Subject: net: isolate LuaSec-specifics For this, various accessor functions are now provided directly on the sockets, which reach down into the LuaSec implementation to obtain the information. While this may seem of little gain at first, it hides the implementation detail of the LuaSec+LuaSocket combination that the actual socket and the TLS layer are separate objects. The net gain here is that an alternative implementation does not have to emulate that specific implementation detail and "only" has to expose LuaSec-compatible data structures on the new functions. --- plugins/mod_admin_shell.lua | 7 ++----- plugins/mod_c2s.lua | 6 ++---- plugins/mod_s2s.lua | 9 ++++----- plugins/mod_s2s_auth_certs.lua | 6 +++--- plugins/mod_saslauth.lua | 11 +++++------ 5 files changed, 16 insertions(+), 23 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index ae7e3c7c..c60cc75b 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -807,9 +807,7 @@ available_columns = { mapper = function(conn, session) if not session.secure then return "insecure"; end if not conn or not conn:ssl() then return "secure" end - local sock = conn and conn:socket(); - if not sock then return "secure"; end - local tls_info = sock.info and sock:info(); + local tls_info = conn.ssl_info and conn:ssl_info(); return tls_info and tls_info.protocol or "secure"; end; }; @@ -819,8 +817,7 @@ available_columns = { width = 30; key = "conn"; mapper = function(conn) - local sock = conn:socket(); - local info = sock and sock.info and sock:info(); + local info = conn and conn.ssl_info and conn:ssl_info(); if info then return info.cipher end end; }; diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua index c8f54fa7..8c0844ae 100644 --- a/plugins/mod_c2s.lua +++ b/plugins/mod_c2s.lua @@ -117,8 +117,7 @@ function stream_callbacks._streamopened(session, attr) session.secure = true; session.encrypted = true; - local sock = session.conn:socket(); - local info = sock.info and sock:info(); + local info = session.conn:ssl_info(); if type(info) == "table" then (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); session.compressed = info.compression; @@ -295,8 +294,7 @@ function listener.onconnect(conn) session.encrypted = true; -- Check if TLS compression is used - local sock = conn:socket(); - local info = sock.info and sock:info(); + local info = conn:ssl_info(); if type(info) == "table" then (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); session.compressed = info.compression; diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index 2f3815c4..3afb73eb 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -383,10 +383,10 @@ end --- Helper to check that a session peer's certificate is valid local function check_cert_status(session) local host = session.direction == "outgoing" and session.to_host or session.from_host - local conn = session.conn:socket() + local conn = session.conn local cert - if conn.getpeercertificate then - cert = conn:getpeercertificate() + if conn.ssl_peercertificate then + cert = conn:ssl_peercertificate() end return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert }); @@ -398,8 +398,7 @@ local function session_secure(session) session.secure = true; session.encrypted = true; - local sock = session.conn:socket(); - local info = sock.info and sock:info(); + local info = session.conn:ssl_info(); if type(info) == "table" then (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); session.compressed = info.compression; diff --git a/plugins/mod_s2s_auth_certs.lua b/plugins/mod_s2s_auth_certs.lua index 992ee934..bde3cb82 100644 --- a/plugins/mod_s2s_auth_certs.lua +++ b/plugins/mod_s2s_auth_certs.lua @@ -9,7 +9,7 @@ local measure_cert_statuses = module:metric("counter", "checked", "", "Certifica module:hook("s2s-check-certificate", function(event) local session, host, cert = event.session, event.host, event.cert; - local conn = session.conn:socket(); + local conn = session.conn; local log = session.log or log; if not cert then @@ -18,8 +18,8 @@ module:hook("s2s-check-certificate", function(event) end local chain_valid, errors; - if conn.getpeerverification then - chain_valid, errors = conn:getpeerverification(); + if conn.ssl_peerverification then + chain_valid, errors = conn:ssl_peerverification(); else chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } }; end diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index ab863aa3..649f9ba6 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -242,7 +242,7 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event) end); local function tls_unique(self) - return self.userdata["tls-unique"]:getpeerfinished(); + return self.userdata["tls-unique"]:ssl_peerfinished(); end local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' }; @@ -262,18 +262,17 @@ module:hook("stream-features", function(event) -- check whether LuaSec has the nifty binding to the function needed for tls-unique -- FIXME: would be nice to have this check only once and not for every socket if sasl_handler.add_cb_handler then - local socket = origin.conn:socket(); - local info = socket.info and socket:info(); - if info.protocol == "TLSv1.3" then + local info = origin.conn:ssl_info(); + if info and info.protocol == "TLSv1.3" then log("debug", "Channel binding 'tls-unique' undefined in context of TLS 1.3"); - elseif socket.getpeerfinished and socket:getpeerfinished() then + elseif origin.conn.ssl_peerfinished and origin.conn:ssl_peerfinished() then log("debug", "Channel binding 'tls-unique' supported"); sasl_handler:add_cb_handler("tls-unique", tls_unique); else log("debug", "Channel binding 'tls-unique' not supported (by LuaSec?)"); end sasl_handler["userdata"] = { - ["tls-unique"] = socket; + ["tls-unique"] = origin.conn; }; else log("debug", "Channel binding not supported by SASL handler"); -- cgit v1.2.3 From 85abab1dfd0e1f919db3130bfd08226184099ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Fri, 17 Sep 2021 21:18:30 +0200 Subject: mod_tls: Do not offer TLS if the connection is considered secure This may be necessary if the session.conn object is not exchanged by the network backend when establishing TLS. In that case, the starttls method will always exist and thus that is not a good indicator for offering TLS. However, the secure bit already tells us that TLS has been established or is not to be established on the connection, so we use that instead. --- plugins/mod_tls.lua | 3 +++ 1 file changed, 3 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index afc1653a..76964082 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -80,6 +80,9 @@ end module:hook_global("config-reloaded", module.load); local function can_do_tls(session) + if session.secure then + return false; + end if session.conn and not session.conn.starttls then if not session.secure then session.log("debug", "Underlying connection does not support STARTTLS"); -- cgit v1.2.3 From 87d3cb2f3348edf3753c487effd04e1af213f668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sat, 2 Apr 2022 11:18:57 +0200 Subject: mod_tls: tell network backend to stop reading while preparing TLS --- plugins/mod_tls.lua | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index 76964082..dcb2da21 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -129,6 +129,13 @@ end); module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event) local origin = event.origin; if can_do_tls(origin) then + if origin.conn.block_reads then + -- we need to ensure that no data is read anymore, otherwise we could end up in a situation where + -- is sent and the socket receives the TLS handshake (and passes the data to lua) before + -- it is asked to initiate TLS + -- (not with the classical single-threaded server backends) + origin.conn:block_reads() + end (origin.sends2s or origin.send)(starttls_proceed); if origin.destroyed then return end origin:reset_stream(); -- cgit v1.2.3 From 84d748f94d6a94e9caf165661692770ea11e0e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Fri, 17 Sep 2021 21:43:54 +0200 Subject: mod_tls: pass target hostname to starttls In case the network backend needs it for outgoing SNI or something. --- plugins/mod_tls.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index dcb2da21..fc35b1d0 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -193,7 +193,7 @@ module:hook_tag(xmlns_starttls, "proceed", function (session, stanza) -- luachec if session.type == "s2sout_unauthed" and can_do_tls(session) then module:log("debug", "Proceeding with TLS on s2sout..."); session:reset_stream(); - session.conn:starttls(session.ssl_ctx); + session.conn:starttls(session.ssl_ctx, session.to_host); session.secure = false; return true; end -- cgit v1.2.3 From 3c35d94011af3c46666c676b7098a30f4e427168 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 7 May 2022 13:01:49 +0200 Subject: mod_s2s: Don't bounce queued error stanzas (thanks Martin) The check for the type attr was lost in 11765f0605ec leading to attempts to create error replies for error stanzas, which util.stanza rejects. Tested by sending which produced a traceback previously. --- plugins/mod_s2s.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index 3afb73eb..7662be57 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -147,7 +147,7 @@ local function bounce_sendq(session, reason) reason_text = reason; end for i, stanza in ipairs(sendq) do - if not stanza.attr.xmlns and bouncy_stanzas[stanza.name] then + if not stanza.attr.xmlns and bouncy_stanzas[stanza.name] and stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then local reply = st.error_reply( stanza, error_type, -- cgit v1.2.3 From 71dc755f0f20c5dd2ad6ed76a8ae791bae16a8b1 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 7 May 2022 13:10:27 +0200 Subject: mod_s2s: Log queued stanzas for which no error reply is produced This would mainly be error stanzas. Good to have some trace of when handling of them are finished. --- plugins/mod_s2s.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index 7662be57..dd585ac7 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -155,6 +155,8 @@ local function bounce_sendq(session, reason) reason_text and ("Server-to-server connection failed: "..reason_text) or nil ); core_process_stanza(dummy, reply); + else + (session.log or log)("debug", "Not eligible for bouncing, discarding %s", stanza:top_tag()); end sendq[i] = nil; end -- cgit v1.2.3 From 18baeca54aa5ad8b2f439dee7f17b810cff35bca Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 15 May 2022 16:12:34 +0200 Subject: mod_smacks: Initialize queue before sending Setting the .smacks field enables code paths that expects the queue to be present. The queue is initialized in wrap_session_out(). With opportunistic writes enabled this happens immediately on .sends2s(), so the sending must happen before OR after these two lines, not in the middle. --- plugins/mod_smacks.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 0d2016fc..841e1208 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -334,8 +334,8 @@ module:hook("s2sout-established", function (event) if not session.smacks_feature then return end session.smacks = session.smacks_feature.attr.xmlns; - session.sends2s(st.stanza("enable", { xmlns = session.smacks })); wrap_session_out(session, false); + session.sends2s(st.stanza("enable", { xmlns = session.smacks })); end); function handle_enabled(session, stanza, xmlns_sm) -- luacheck: ignore 212/stanza -- cgit v1.2.3 From a350ea4f1ce48d4ba56f5c8f169906f12fac24db Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 11 Feb 2022 16:09:42 +0100 Subject: mod_smacks: Factor out some convenience functions Those lines are long and the risk of mistakes if another one needs to be added seems high, but lower when factored out like this. --- plugins/mod_smacks.lua | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 841e1208..06bfb53f 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -85,6 +85,22 @@ local all_old_sessions = module:open_store("smacks_h"); local old_session_registry = module:open_store("smacks_h", "map"); local session_registry = module:shared "/*/smacks/resumption-tokens"; -- > user@host/resumption-token --> resource +local function track_session(session, id) + session_registry[jid.join(session.username, session.host, id or session.resumption_token)] = session; + session.resumption_token = id; +end + +local function save_old_session(session) + session_registry[jid.join(session.username, session.host, session.resumption_token)] = nil; + return old_session_registry:set(session.username, session.resumption_token, + { h = session.handled_stanza_count; t = os.time() }) +end + +local function clear_old_session(session, id) + session_registry[jid.join(session.username, session.host, id or session.resumption_token)] = nil; + return old_session_registry:set(session.username, id or session.resumption_token, nil) +end + local ack_errors = require"util.error".init("mod_smacks", xmlns_sm3, { head = { condition = "undefined-condition"; text = "Client acknowledged more stanzas than sent by server" }; tail = { condition = "undefined-condition"; text = "Client acknowledged less stanzas than already acknowledged" }; @@ -237,8 +253,7 @@ module:hook("pre-session-close", function(event) if session.smacks == nil then return end if session.resumption_token then session.log("debug", "Revoking resumption token"); - session_registry[jid.join(session.username, session.host, session.resumption_token)] = nil; - old_session_registry:set(session.username, session.resumption_token, nil); + clear_old_session(session); session.resumption_token = nil; else session.log("debug", "Session not resumable"); @@ -313,8 +328,7 @@ function handle_enable(session, stanza, xmlns_sm) local resume = stanza.attr.resume; if resume == "true" or resume == "1" then resume_token = new_id(); - session_registry[jid.join(session.username, session.host, resume_token)] = session; - session.resumption_token = resume_token; + track_session(session, resume_token); resume_max = tostring(resume_timeout); end (session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm, id = resume_token, resume = resume, max = resume_max })); @@ -482,9 +496,7 @@ module:hook("pre-resource-unbind", function (event) end session.log("debug", "Destroying session for hibernating too long"); - session_registry[jid.join(session.username, session.host, session.resumption_token)] = nil; - old_session_registry:set(session.username, session.resumption_token, - { h = session.handled_stanza_count; t = os.time() }); + save_old_session(session); session.resumption_token = nil; session.resending_unacked = true; -- stop outgoing_stanza_filter from re-queueing anything anymore sessionmanager.destroy_session(session, "Hibernating too long"); @@ -541,7 +553,7 @@ function handle_resume(session, stanza, xmlns_sm) session.send(st.stanza("failed", { xmlns = xmlns_sm, h = format_h(old_session.h) }) :tag("item-not-found", { xmlns = xmlns_errors }) ); - old_session_registry:set(session.username, id, nil); + clear_old_session(session, id); resumption_expired(1); else session.log("debug", "Tried to resume non-existent session with id %s", id); @@ -698,8 +710,7 @@ module:hook_global("server-stopping", function(event) for _, user in pairs(local_sessions) do for _, session in pairs(user.sessions) do if session.resumption_token then - if old_session_registry:set(session.username, session.resumption_token, - { h = session.handled_stanza_count; t = os.time() }) then + if save_old_session(session) then session.resumption_token = nil; -- Deal with unacked stanzas -- cgit v1.2.3 From 1a87a2d40cf4812b57ea2db92ba4dd4364512e62 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 21 May 2022 13:11:25 +0200 Subject: mod_smacks: Use session logging for remaining log messages For consistency and easier correlation of session events. --- plugins/mod_smacks.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 06bfb53f..57d412ad 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -302,7 +302,7 @@ function handle_enable(session, stanza, xmlns_sm) if session.username then local old_sessions, err = all_old_sessions:get(session.username); - module:log("debug", "Old sessions: %q", old_sessions) + session.log("debug", "Old sessions: %q", old_sessions) if old_sessions then local keep, count = {}, 0; for token, info in it.sorted_pairs(old_sessions, function(a, b) @@ -314,11 +314,11 @@ function handle_enable(session, stanza, xmlns_sm) end all_old_sessions:set(session.username, keep); elseif err then - module:log("error", "Unable to retrieve old resumption counters: %s", err); + session.log("error", "Unable to retrieve old resumption counters: %s", err); end end - module:log("debug", "Enabling stream management"); + session.log("debug", "Enabling stream management"); session.smacks = xmlns_sm; wrap_session(session, false); @@ -353,7 +353,7 @@ module:hook("s2sout-established", function (event) end); function handle_enabled(session, stanza, xmlns_sm) -- luacheck: ignore 212/stanza - module:log("debug", "Enabling stream management"); + session.log("debug", "Enabling stream management"); session.smacks = xmlns_sm; wrap_session_in(session, false); @@ -367,10 +367,10 @@ module:hook_tag(xmlns_sm3, "enabled", function (session, stanza) return handle_e function handle_r(origin, stanza, xmlns_sm) -- luacheck: ignore 212/stanza if not origin.smacks then - module:log("debug", "Received ack request from non-smack-enabled session"); + origin.log("debug", "Received ack request from non-smack-enabled session"); return; end - module:log("debug", "Received ack request, acking for %d", origin.handled_stanza_count); + origin.log("debug", "Received ack request, acking for %d", origin.handled_stanza_count); -- Reply with (origin.sends2s or origin.send)(st.stanza("a", { xmlns = xmlns_sm, h = format_h(origin.handled_stanza_count) })); -- piggyback our own ack request if needed (see request_ack_if_needed() for explanation of last_requested_h) -- cgit v1.2.3 From ce636e5b331afa27f69b32ba040dc6a51e23893e Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 26 May 2022 19:24:01 +0200 Subject: mod_smacks: Remove debug log references to timer (not used anymore) Cuts down on noise as well --- plugins/mod_smacks.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 84231e26..2172cd5c 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -173,13 +173,12 @@ end local function request_ack(session, reason) local queue = session.outgoing_stanza_queue; - session.log("debug", "Sending (inside timer, before send) from %s - #queue=%d", reason, queue:count_unacked()); + session.log("debug", "Sending from %s - #queue=%d", reason, queue:count_unacked()); (session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks })) if session.destroyed then return end -- sending something can trigger destruction session.awaiting_ack = true; -- expected_h could be lower than this expression e.g. more stanzas added to the queue meanwhile) session.last_requested_h = queue:count_acked() + queue:count_unacked(); - session.log("debug", "Sending (inside timer, after send) from %s - #queue=%d", reason, queue:count_unacked()); if not session.delayed_ack_timer then session.delayed_ack_timer = timer.add_task(delayed_ack_timeout, function() ack_delayed(session, nil); -- we don't know if this is the only new stanza in the queue -- cgit v1.2.3 From 553620127a0a5edc437d8a1c970ca0cb13ab98ec Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 27 May 2022 14:54:32 +0200 Subject: mod_smacks: Indicate that bounces are generated by the server Could arguably be implied by 'recipient-unavailable' since if it was available, this error wouldn't happen. --- plugins/mod_smacks.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 79f824f6..3baf30c2 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -420,13 +420,14 @@ local function handle_unacked_stanzas(session) local queue = session.outgoing_stanza_queue; local unacked = queue:count_unacked() if unacked > 0 then + local error_from = jid.join(session.username, session.host or module.host); tx_dropped_stanzas:sample(unacked); session.smacks = false; -- Disable queueing session.outgoing_stanza_queue = nil; for stanza in queue._queue:consume() do if not module:fire_event("delivery/failure", { session = session, stanza = stanza }) then if stanza.attr.type ~= "error" and stanza.attr.from ~= session.full_jid then - local reply = st.error_reply(stanza, "cancel", "recipient-unavailable"); + local reply = st.error_reply(stanza, "cancel", "recipient-unavailable", nil, error_from); module:send(reply); end end -- cgit v1.2.3 From 0267554c8e91555c658241861943b684e5f98000 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 30 May 2022 15:28:44 +0200 Subject: prosodyctl shell: Communicate width of terminal to mod_admin_shell This lets it adjust the width of tables to the actual terminal width. --- plugins/mod_admin_shell.lua | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 363ad5c6..02f9a894 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -133,6 +133,11 @@ local function handle_line(event) session = console:new_session(event.origin); event.origin.shell_session = session; end + + local default_width = 132; -- The common default of 80 is a bit too narrow for e.g. s2s:show(), 132 was another common width for hardware terminals + local margin = 2; -- To account for '| ' when lines are printed + session.width = (tonumber(event.stanza.attr.width) or default_width)-margin; + local line = event.stanza:get_text(); local useglobalenv; @@ -219,7 +224,7 @@ function commands.help(session, data) print [[Commands are divided into multiple sections. For help on a particular section, ]] print [[type: help SECTION (for example, 'help c2s'). Sections are: ]] print [[]] - local row = format_table({ { title = "Section"; width = 7 }; { title = "Description"; width = "100%" } }) + local row = format_table({ { title = "Section", width = 7 }, { title = "Description", width = "100%" } }, session.width) print(row()) print(row { "c2s"; "Commands to manage local client-to-server sessions" }) print(row { "s2s"; "Commands to manage sessions between this server and others" }) @@ -341,7 +346,7 @@ function commands.help(session, data) meta_columns[2].width = math.max(meta_columns[2].width or 0, #(spec.title or "")); meta_columns[3].width = math.max(meta_columns[3].width or 0, #(spec.description or "")); end - local row = format_table(meta_columns, 120) + local row = format_table(meta_columns, session.width) print(row()); for column, spec in iterators.sorted_pairs(available_columns) do print(row({ column, spec.title, spec.description })); @@ -935,7 +940,7 @@ end function def_env.c2s:show(match_jid, colspec) local print = self.session.print; local columns = get_colspec(colspec, { "id"; "jid"; "ipv"; "status"; "secure"; "smacks"; "csi" }); - local row = format_table(columns, 120); + local row = format_table(columns, self.session.width); local function match(session) local jid = get_jid(session) @@ -1018,7 +1023,7 @@ end function def_env.s2s:show(match_jid, colspec) local print = self.session.print; local columns = get_colspec(colspec, { "id"; "host"; "dir"; "remote"; "ipv"; "secure"; "s2s_sasl"; "dialback" }); - local row = format_table(columns, 132); + local row = format_table(columns, self.session.width); local function match(session) local host, remote = get_s2s_hosts(session); @@ -1556,7 +1561,7 @@ function def_env.http:list(hosts) local output = format_table({ { title = "Module", width = "20%" }, { title = "URL", width = "80%" }, - }, 132); + }, self.session.width); for _, host in ipairs(hosts) do local http_apps = modulemanager.get_items("http-provider", host); -- cgit v1.2.3 From 8ba6d3a2b1e2eb5dee1ceb25bde0f799fb4b793c Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 30 May 2022 14:54:10 +0200 Subject: mod_admin_shell: Document the 'watch' section in the built-in help --- plugins/mod_admin_shell.lua | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 02f9a894..ca9cb0d5 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -240,6 +240,7 @@ function commands.help(session, data) print(row { "dns"; "Commands to manage and inspect the internal DNS resolver" }) print(row { "xmpp"; "Commands for sending XMPP stanzas" }) print(row { "debug"; "Commands for debugging the server" }) + print(row { "watch"; "Commands for watching live logs from the server" }) print(row { "config"; "Reloading the configuration, etc." }) print(row { "columns"; "Information about customizing session listings" }) print(row { "console"; "Help regarding the console itself" }) @@ -316,6 +317,9 @@ function commands.help(session, data) print [[debug:logevents(host) - Enable logging of fired events on host]] print [[debug:events(host, event) - Show registered event handlers]] print [[debug:timers() - Show information about scheduled timers]] + elseif section == "watch" then + print [[watch:log() - Follow debug logs]] + print [[watch:stanzas(target, filter) - Watch live stanzas matching the specified target and filter]] elseif section == "console" then print [[Hey! Welcome to Prosody's admin console.]] print [[First thing, if you're ever wondering how to get out, simply type 'quit'.]] -- cgit v1.2.3 From b7bd70874a1dd468bc9acdffeebe9b240ba9d04f Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 31 May 2022 00:31:56 +0200 Subject: mod_admin_shell: Show bound ports in module:info I.e. the subset of port:list() relevant to the specified module. --- plugins/mod_admin_shell.lua | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index ca9cb0d5..1ced2e49 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -513,7 +513,17 @@ function def_env.module:info(name, hosts) ["auth-provider"] = item_name, ["storage-provider"] = item_name, ["http-provider"] = function(item, mod) return mod:http_url(item.name, item.default_path); end, - ["net-provider"] = item_name, + ["net-provider"] = function(item, mod) + local service_name = item.name; + local ports_list = {}; + for _, interface, port in portmanager.get_active_services():iter(service_name, nil, nil) do + table.insert(ports_list, "["..interface.."]:"..port); + end + if not ports_list[1] then + return service_name..": not listening on any ports"; + end + return service_name..": "..table.concat(ports_list, ", "); + end, ["measure"] = function(item) return item.name .. " (" .. suf(item.conf and item.conf.unit, " ") .. item.type .. ")"; end, ["metric"] = function(item) return ("%s (%s%s)%s"):format(item.name, suf(item.mf.unit, " "), item.mf.type_, pre(": ", item.mf.description)); -- cgit v1.2.3 From 3717f5872a5abe08be4be2e0de8a2f3fd0666410 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 31 May 2022 13:26:44 +0200 Subject: mod_admin_shell: Drop unused argument [luacheck] --- plugins/mod_admin_shell.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 1ced2e49..afe91011 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -513,7 +513,7 @@ function def_env.module:info(name, hosts) ["auth-provider"] = item_name, ["storage-provider"] = item_name, ["http-provider"] = function(item, mod) return mod:http_url(item.name, item.default_path); end, - ["net-provider"] = function(item, mod) + ["net-provider"] = function(item) local service_name = item.name; local ports_list = {}; for _, interface, port in portmanager.get_active_services():iter(service_name, nil, nil) do -- cgit v1.2.3 From b0c116f47b96bb5d9be4646d1031cc6606a81405 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 1 Jun 2022 17:27:17 +0200 Subject: mod_admin_shell: Include last (mod_cron) task run time in module:info() Don't think this is otherwise shown anywhere outside of debug logs --- plugins/mod_admin_shell.lua | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index afe91011..c498072b 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -496,6 +496,16 @@ function def_env.module:info(name, hosts) local function item_name(item) return item.name; end + local function task_timefmt(t) + if not t then + return "no last run time" + elseif os.difftime(os.time(), t) < 86400 then + return os.date("last run today at %H:%M", t); + else + return os.date("last run %A at %H:%M", t); + end + end + local friendly_descriptions = { ["adhoc-provider"] = "Ad-hoc commands", ["auth-provider"] = "Authentication provider", @@ -528,7 +538,7 @@ function def_env.module:info(name, hosts) ["metric"] = function(item) return ("%s (%s%s)%s"):format(item.name, suf(item.mf.unit, " "), item.mf.type_, pre(": ", item.mf.description)); end, - ["task"] = function (item) return string.format("%s (%s)", item.name or item.id, item.when); end + ["task"] = function (item) return string.format("%s (%s, %s)", item.name or item.id, item.when, task_timefmt(item.last)); end }; for host in hosts do -- cgit v1.2.3 From 2048a7a762e619974557c4015429626443835b4c Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 6 Dec 2020 22:04:43 +0100 Subject: mod_saslauth: Advertise channel bindings via XEP-0440 This is useful when there's more than one channel binding in circulation, since perhaps there will be varying support for them. --- plugins/mod_saslauth.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index 649f9ba6..0b350c74 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -258,6 +258,7 @@ module:hook("stream-features", function(event) end local sasl_handler = usermanager_get_sasl_handler(module.host, origin) origin.sasl_handler = sasl_handler; + local channel_bindings = set.new() if origin.encrypted then -- check whether LuaSec has the nifty binding to the function needed for tls-unique -- FIXME: would be nice to have this check only once and not for every socket @@ -268,6 +269,7 @@ module:hook("stream-features", function(event) elseif origin.conn.ssl_peerfinished and origin.conn:ssl_peerfinished() then log("debug", "Channel binding 'tls-unique' supported"); sasl_handler:add_cb_handler("tls-unique", tls_unique); + channel_bindings:add("tls-unique"); else log("debug", "Channel binding 'tls-unique' not supported (by LuaSec?)"); end @@ -304,6 +306,14 @@ module:hook("stream-features", function(event) for mechanism in usable_mechanisms do mechanisms:tag("mechanism"):text(mechanism):up(); end + if not channel_bindings:empty() then + -- XXX XEP-0440 is Experimental + mechanisms:tag("sasl-channel-binding", {xmlns='urn:xmpp:sasl-cb:0'}) + for channel_binding in channel_bindings do + mechanisms:tag("channel-binding", {type=channel_binding}):up() + end + mechanisms:up(); + end features:add_child(mechanisms); return; end -- cgit v1.2.3 From d0ab468f40151e5e728ed9590313cea1820b8ff7 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 13 Jun 2022 21:25:42 +0200 Subject: mod_admin_shell: Show session id ping reply came To point out which one when more than one connection was established, or if it's an existing connection, allows correlation with s2s:show() or with logs. --- plugins/mod_admin_shell.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index c498072b..4033d868 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -1533,7 +1533,7 @@ function def_env.xmpp:ping(localhost, remotehost, timeout) module:unhook("s2sin-established", onestablished); module:unhook("s2s-destroyed", ondestroyed); end):next(function(pong) - return ("pong from %s in %gs"):format(pong.stanza.attr.from, time.now() - time_start); + return ("pong from %s on %s in %gs"):format(pong.stanza.attr.from, pong.origin.id, time.now() - time_start); end); end -- cgit v1.2.3 From 07660a908817500a6f17dd2fad8cc38f373d82f1 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 14 Jun 2022 03:31:30 +0200 Subject: mod_csi_simple: Collect stats on number of stanzas per flush Because interesting, gives some idea about the efficiency. --- plugins/mod_csi_simple.lua | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_csi_simple.lua b/plugins/mod_csi_simple.lua index 569916b0..b9a470f5 100644 --- a/plugins/mod_csi_simple.lua +++ b/plugins/mod_csi_simple.lua @@ -116,6 +116,9 @@ local flush_reasons = module:metric( { "reason" } ); +local flush_sizes = module:metric("histogram", "flush_stanza_count", "", "Number of stanzas flushed at once", {}, + { buckets = { 0, 1, 2, 4, 8, 16, 32, 64, 128, 256 } }):with_labels(); + local function manage_buffer(stanza, session) local ctr = session.csi_counter or 0; if session.state ~= "inactive" then @@ -129,6 +132,7 @@ local function manage_buffer(stanza, session) session.csi_measure_buffer_hold = nil; end flush_reasons:with_labels(why or "important"):add(1); + flush_sizes:sample(ctr); session.log("debug", "Flushing buffer (%s; queue size is %d)", why or "important", session.csi_counter); session.state = "flushing"; module:fire_event("csi-flushing", { session = session }); @@ -147,6 +151,7 @@ local function flush_buffer(data, session) session.log("debug", "Flushing buffer (%s; queue size is %d)", "client activity", session.csi_counter); session.state = "flushing"; module:fire_event("csi-flushing", { session = session }); + flush_sizes:sample(ctr); flush_reasons:with_labels("client activity"):add(1); if session.csi_measure_buffer_hold then session.csi_measure_buffer_hold(); -- cgit v1.2.3 From 9efbb2d015aef48737ebd9dd39b830bbecff002f Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 14 Jun 2022 22:03:02 +0200 Subject: mod_mam: Clarify comment (thanks chili-b) This was slightly inaccurate since 6e1af07921d1 because the conditions are more complicated now. --- plugins/mod_mam/mod_mam.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua index 50095e2f..229cdfb3 100644 --- a/plugins/mod_mam/mod_mam.lua +++ b/plugins/mod_mam/mod_mam.lua @@ -53,8 +53,12 @@ if not archive.find then end local use_total = module:get_option_boolean("mam_include_total", true); -function schedule_cleanup() - -- replaced later if cleanup is enabled +function schedule_cleanup(_username, _date) + -- Called to make a note of which users have messages on which days, which in + -- turn is used to optimize the message expiry routine. + -- + -- This noop is conditionally replaced later depending on retention settings + -- and storage backend capabilities. end -- Handle prefs. -- cgit v1.2.3 From 7215ba25ff882523d2c1d7b8c8168b218dd38ff9 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 14 Jun 2022 22:06:21 +0200 Subject: mod_mam: Silence luacheck (yay warnings in CI but not locally) --- plugins/mod_mam/mod_mam.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua index 229cdfb3..083ae90d 100644 --- a/plugins/mod_mam/mod_mam.lua +++ b/plugins/mod_mam/mod_mam.lua @@ -53,7 +53,7 @@ if not archive.find then end local use_total = module:get_option_boolean("mam_include_total", true); -function schedule_cleanup(_username, _date) +function schedule_cleanup(_username, _date) -- luacheck: ignore 212 -- Called to make a note of which users have messages on which days, which in -- turn is used to optimize the message expiry routine. -- -- cgit v1.2.3 From 2ec8fbe7e5f571727a14205a7389f6528112a133 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 2 Jul 2022 17:30:06 +0200 Subject: mod_storage_sql: Remove Lua 5.1 compatibility hack Part of #1600 --- plugins/mod_storage_sql.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua index 3bfe1739..8749bcc4 100644 --- a/plugins/mod_storage_sql.lua +++ b/plugins/mod_storage_sql.lua @@ -382,8 +382,7 @@ local function archive_where(query, args, where) -- Set of ids if query.ids then local nids, nargs = #query.ids, #args; - -- COMPAT Lua 5.1: No separator argument to string.rep - where[#where + 1] = "\"key\" IN (" .. string.rep("?,", nids):sub(1,-2) .. ")"; + where[#where + 1] = "\"key\" IN (" .. string.rep("?", nids, ",") .. ")"; for i, id in ipairs(query.ids) do args[nargs+i] = id; end -- cgit v1.2.3 From 5251c9b686fc7885c1213cc2580d66ebda2dda9b Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 11 Jul 2022 19:07:38 +0200 Subject: compat: Remove handling of Lua 5.1 location of 'unpack' function --- plugins/mod_admin_shell.lua | 2 +- plugins/mod_pep_simple.lua | 2 +- plugins/mod_pubsub/pubsub.lib.lua | 2 +- plugins/mod_storage_sql.lua | 2 +- plugins/mod_storage_xep0227.lua | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 4033d868..7a69e3a4 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -22,7 +22,7 @@ local _G = _G; local prosody = _G.prosody; -local unpack = table.unpack or unpack; -- luacheck: ignore 113 +local unpack = table.unpack; local iterators = require "util.iterators"; local keys, values = iterators.keys, iterators.values; local jid_bare, jid_split, jid_join = import("util.jid", "bare", "prepped_split", "join"); diff --git a/plugins/mod_pep_simple.lua b/plugins/mod_pep_simple.lua index e686b99b..1314aece 100644 --- a/plugins/mod_pep_simple.lua +++ b/plugins/mod_pep_simple.lua @@ -14,7 +14,7 @@ local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed local pairs = pairs; local next = next; local type = type; -local unpack = table.unpack or unpack; -- luacheck: ignore 113 +local unpack = table.unpack; local calculate_hash = require "util.caps".calculate_hash; local core_post_stanza = prosody.core_post_stanza; local bare_sessions = prosody.bare_sessions; diff --git a/plugins/mod_pubsub/pubsub.lib.lua b/plugins/mod_pubsub/pubsub.lib.lua index 83cef808..661bb65b 100644 --- a/plugins/mod_pubsub/pubsub.lib.lua +++ b/plugins/mod_pubsub/pubsub.lib.lua @@ -1,4 +1,4 @@ -local t_unpack = table.unpack or unpack; -- luacheck: ignore 113 +local t_unpack = table.unpack; local time_now = os.time; local jid_prep = require "util.jid".prep; diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua index 8749bcc4..b1e90752 100644 --- a/plugins/mod_storage_sql.lua +++ b/plugins/mod_storage_sql.lua @@ -13,7 +13,7 @@ local is_stanza = require"util.stanza".is_stanza; local t_concat = table.concat; local noop = function() end -local unpack = table.unpack or unpack; -- luacheck: ignore 113 +local unpack = table.unpack; local function iterator(result) return function(result_) local row = result_(); diff --git a/plugins/mod_storage_xep0227.lua b/plugins/mod_storage_xep0227.lua index 5c3cf7f6..079e49d8 100644 --- a/plugins/mod_storage_xep0227.lua +++ b/plugins/mod_storage_xep0227.lua @@ -2,7 +2,7 @@ local ipairs, pairs = ipairs, pairs; local setmetatable = setmetatable; local tostring = tostring; -local next, unpack = next, table.unpack or unpack; --luacheck: ignore 113/unpack +local next, unpack = next, table.unpack; local os_remove = os.remove; local io_open = io.open; local jid_bare = require "util.jid".bare; -- cgit v1.2.3 From 8db7cdc71321f4a367338df87a7e90629e741ff9 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 1 Jun 2022 15:06:59 +0200 Subject: mod_saslauth: Implement RFC 9266 'tls-exporter' channel binding (#1760) Brings back SCRAM-SHA-*-PLUS from its hiatus brought on by the earlier channel binding method being undefined for TLS 1.3, and the increasing deployment of TLS 1.3. See 1bfd238e05ad and #1542 Requires future version of LuaSec, once support for this key material export method is merged. See https://github.com/brunoos/luasec/pull/187 --- plugins/mod_saslauth.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index 0b350c74..e94b2d78 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -245,6 +245,15 @@ local function tls_unique(self) return self.userdata["tls-unique"]:ssl_peerfinished(); end +local function tls_exporter(conn) + if not conn.ssl_exportkeyingmaterial then return end + return conn:ssl_exportkeyingmaterial("EXPORTER-Channel-Binding", 32, ""); +end + +local function sasl_tls_exporter(self) + return tls_exporter(self.userdata["tls-exporter"]); +end + local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' }; local bind_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-bind' }; local xmpp_session_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-session' }; @@ -266,6 +275,11 @@ module:hook("stream-features", function(event) local info = origin.conn:ssl_info(); if info and info.protocol == "TLSv1.3" then log("debug", "Channel binding 'tls-unique' undefined in context of TLS 1.3"); + if tls_exporter(origin.conn) then + log("debug", "Channel binding 'tls-exporter' supported"); + sasl_handler:add_cb_handler("tls-exporter", sasl_tls_exporter); + channel_bindings:add("tls-exporter"); + end elseif origin.conn.ssl_peerfinished and origin.conn:ssl_peerfinished() then log("debug", "Channel binding 'tls-unique' supported"); sasl_handler:add_cb_handler("tls-unique", tls_unique); @@ -275,6 +289,7 @@ module:hook("stream-features", function(event) end sasl_handler["userdata"] = { ["tls-unique"] = origin.conn; + ["tls-exporter"] = origin.conn; }; else log("debug", "Channel binding not supported by SASL handler"); -- cgit v1.2.3 From a2f8218a63e2ebbe04d1a0fc2a235af59c34f515 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 2 Aug 2022 19:26:26 +0200 Subject: mod_tls: Record STARTTLS state so it can be shown in Shell This field can be viewed using s2s:show(nil, "... starttls") even without any special support in mod_admin_shell, which can be added later to make it nicer. One can then assume that a TLS connection with an empty / nil starttls field means Direct TLS. --- plugins/mod_s2s.lua | 4 +++- plugins/mod_tls.lua | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index dd585ac7..ac3c8d4f 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -429,7 +429,8 @@ function stream_callbacks._streamopened(session, attr) session.had_stream = true; -- Had a stream opened at least once -- TODO: Rename session.secure to session.encrypted - if session.secure == false then + if session.secure == false then -- Set by mod_tls during STARTTLS handshake + session.starttls = "completed"; session_secure(session); end @@ -750,6 +751,7 @@ local function initialize_session(session) local w = conn.write; if conn:ssl() then + -- Direct TLS was used session_secure(session); end diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index fc35b1d0..380effe3 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -128,6 +128,7 @@ end); -- Hook module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event) local origin = event.origin; + origin.starttls = "requested"; if can_do_tls(origin) then if origin.conn.block_reads then -- we need to ensure that no data is read anymore, otherwise we could end up in a situation where @@ -176,6 +177,7 @@ module:hook_tag("http://etherx.jabber.org/streams", "features", function (sessio module:log("debug", "%s is not offering TLS", session.to_host); return; end + session.starttls = "initiated"; session.sends2s(starttls_initiate); return true; end @@ -193,6 +195,7 @@ module:hook_tag(xmlns_starttls, "proceed", function (session, stanza) -- luachec if session.type == "s2sout_unauthed" and can_do_tls(session) then module:log("debug", "Proceeding with TLS on s2sout..."); session:reset_stream(); + session.starttls = "proceeding" session.conn:starttls(session.ssl_ctx, session.to_host); session.secure = false; return true; -- cgit v1.2.3 From 0e7e43f62ff33e3d4cdb4f4d079c5d2c1b07aca9 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 5 Aug 2022 14:41:13 +0200 Subject: mod_admin_shell: Remove obsolete module:load() argument from 0.8 time This 'config' argument was removed without explanation in d8dbf569766c --- plugins/mod_admin_shell.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 7a69e3a4..45bc1d48 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -575,14 +575,14 @@ function def_env.module:info(name, hosts) return true; end -function def_env.module:load(name, hosts, config) +function def_env.module:load(name, hosts) hosts = get_hosts_with_module(hosts); -- Load the module for each host local ok, err, count, mod = true, nil, 0; for host in hosts do if (not modulemanager.is_loaded(host, name)) then - mod, err = modulemanager.load(host, name, config); + mod, err = modulemanager.load(host, name); if not mod then ok = false; if err == "global-module-already-loaded" then -- cgit v1.2.3 From e2807b035edf7d5588dbe8359fc6c1e6b9adf178 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 14 Aug 2022 17:28:31 +0200 Subject: mod_mam: Store archives with sub-second precision timestamps Changes sub-second part of example timestamp to .5 in order to avoid floating point issues. Some clients use timestamps when ordering messages which can lead to messages having the same timestamp ending up in the wrong order. It would be better to preserve the order messages are sent in, which is the order they were stored in. --- plugins/mod_mam/mod_mam.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua index 083ae90d..4d830332 100644 --- a/plugins/mod_mam/mod_mam.lua +++ b/plugins/mod_mam/mod_mam.lua @@ -34,9 +34,9 @@ local rm_load_roster = require "core.rostermanager".load_roster; local is_stanza = st.is_stanza; local tostring = tostring; -local time_now = os.time; +local time_now = require "util.time".now; local m_min = math.min; -local timestamp, datestamp = import( "util.datetime", "datetime", "date"); +local timestamp, datestamp = import("util.datetime", "datetime", "date"); local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50); local strip_tags = module:get_option_set("dont_archive_namespaces", { "http://jabber.org/protocol/chatstates" }); -- cgit v1.2.3 From 50304f83fdb05d1fa56cd46215a248924b8642be Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 14 Aug 2022 17:29:39 +0200 Subject: mod_storage_sql: Drop archive timestamp precision pending schema update The "when" column is an INTEGER which will probably be unhappy about storing higher precision timestamps, so we keep the older behavior for now. --- plugins/mod_storage_sql.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua index 90a9316e..ca8b51ac 100644 --- a/plugins/mod_storage_sql.lua +++ b/plugins/mod_storage_sql.lua @@ -321,7 +321,8 @@ function archive_store:append(username, key, value, when, with) end end - when = when or os.time(); + -- FIXME update the schema to allow precision timestamps + when = when and math.floor(when) or os.time(); with = with or ""; local ok, ret = engine:transaction(function() local delete_sql = [[ -- cgit v1.2.3 From 16331082a5d2de99ac6ded166cba305da9167454 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 14 Aug 2022 17:47:13 +0200 Subject: mod_time: Return sub-second precision timestamps Because why not? Who even has this module enabled? --- plugins/mod_time.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_time.lua b/plugins/mod_time.lua index 0cd5a4ea..72421f85 100644 --- a/plugins/mod_time.lua +++ b/plugins/mod_time.lua @@ -8,6 +8,7 @@ local st = require "util.stanza"; local datetime = require "util.datetime".datetime; +local now = require "util.time".now; local legacy = require "util.datetime".legacy; -- XEP-0202: Entity Time @@ -18,7 +19,7 @@ local function time_handler(event) local origin, stanza = event.origin, event.stanza; origin.send(st.reply(stanza):tag("time", {xmlns="urn:xmpp:time"}) :tag("tzo"):text("+00:00"):up() -- TODO get the timezone in a platform independent fashion - :tag("utc"):text(datetime())); + :tag("utc"):text(datetime(now()))); return true; end -- cgit v1.2.3 From 2294d8b8e08fbf75985bd976bc32c79293a478ef Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 15 Aug 2022 16:35:14 +0200 Subject: mod_time: Remove obsolete XEP-0090 support Deprecated even before Prosody even started, obsolete for over a decade. --- plugins/mod_time.lua | 14 -------------- 1 file changed, 14 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_time.lua b/plugins/mod_time.lua index 72421f85..c9197799 100644 --- a/plugins/mod_time.lua +++ b/plugins/mod_time.lua @@ -9,7 +9,6 @@ local st = require "util.stanza"; local datetime = require "util.datetime".datetime; local now = require "util.time".now; -local legacy = require "util.datetime".legacy; -- XEP-0202: Entity Time @@ -26,16 +25,3 @@ end module:hook("iq-get/bare/urn:xmpp:time:time", time_handler); module:hook("iq-get/host/urn:xmpp:time:time", time_handler); --- XEP-0090: Entity Time (deprecated) - -module:add_feature("jabber:iq:time"); - -local function legacy_time_handler(event) - local origin, stanza = event.origin, event.stanza; - origin.send(st.reply(stanza):tag("query", {xmlns="jabber:iq:time"}) - :tag("utc"):text(legacy())); - return true; -end - -module:hook("iq-get/bare/jabber:iq:time:query", legacy_time_handler); -module:hook("iq-get/host/jabber:iq:time:query", legacy_time_handler); -- cgit v1.2.3 From 8959868a28fb74e55ce41a9cb93d7e3524c7dc2b Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 17 Aug 2022 19:04:30 +0200 Subject: util.stanza: Add method for extracting a single attribute value Sometimes you only care about a single attribute, but the child tag itself may be optional, leading to needing `tag and tag.attr.foo` or `stanza:find("tag@foo")`. The `:find()` method is fairly complex, so avoiding it for this kind of simpler use case is a win. --- plugins/mod_pubsub/pubsub.lib.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_pubsub/pubsub.lib.lua b/plugins/mod_pubsub/pubsub.lib.lua index 661bb65b..cd3efb09 100644 --- a/plugins/mod_pubsub/pubsub.lib.lua +++ b/plugins/mod_pubsub/pubsub.lib.lua @@ -678,8 +678,7 @@ end function handlers.set_retract(origin, stanza, retract, service) local node, notify = retract.attr.node, retract.attr.notify; notify = (notify == "1") or (notify == "true"); - local item = retract:get_child("item"); - local id = item and item.attr.id + local id = retract:get_child_attr("item", nil, "id"); if not (node and id) then origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required")); return true; -- cgit v1.2.3 From 8b384dc77fe54fc6a6c228dd5b6208d403f74216 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 15 Jun 2022 11:47:39 +0100 Subject: mod_saslauth: Rename field from 'scope'->'role' The 'scope' term derives from OAuth, and represents a bundle of permissions. We're now setting on the term 'role' for a bundle of permissions. This change does not affect any public modules I'm aware of. --- plugins/mod_saslauth.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index e94b2d78..c7228b10 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -52,7 +52,7 @@ local function handle_status(session, status, ret, err_msg) module:fire_event("authentication-failure", { session = session, condition = ret, text = err_msg }); session.sasl_handler = session.sasl_handler:clean_clone(); elseif status == "success" then - local ok, err = sm_make_authenticated(session, session.sasl_handler.username, session.sasl_handler.scope); + local ok, err = sm_make_authenticated(session, session.sasl_handler.username, session.sasl_handler.role); if ok then module:fire_event("authentication-success", { session = session }); session.sasl_handler = nil; -- cgit v1.2.3 From d73714b4f426da4f9c79d5ddf0b8cb11d09e9f3f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 15 Jun 2022 12:15:01 +0100 Subject: Switch to a new role-based authorization framework, removing is_admin() We began moving away from simple "is this user an admin?" permission checks before 0.12, with the introduction of mod_authz_internal and the ability to dynamically change the roles of individual users. The approach in 0.12 still had various limitations however, and apart from the introduction of roles other than "admin" and the ability to pull that info from storage, not much actually changed. This new framework shakes things up a lot, though aims to maintain the same functionality and behaviour on the surface for a default Prosody configuration. That is, if you don't take advantage of any of the new features, you shouldn't notice any change. The biggest change visible to developers is that usermanager.is_admin() (and the auth provider is_admin() method) have been removed. Gone. Completely. Permission checks should now be performed using a new module API method: module:may(action_name, context) This method accepts an action name, followed by either a JID (string) or (preferably) a table containing 'origin'/'session' and 'stanza' fields (e.g. the standard object passed to most events). It will return true if the action should be permitted, or false/nil otherwise. Modules should no longer perform permission checks based on the role name. E.g. a lot of code previously checked if the user's role was prosody:admin before permitting some action. Since many roles might now exist with similar permissions, and the permissions of prosody:admin may be redefined dynamically, it is no longer suitable to use this method for permission checks. Use module:may(). If you start an action name with ':' (recommended) then the current module's name will automatically be used as a prefix. To define a new permission, use the new module API: module:default_permission(role_name, action_name) module:default_permissions(role_name, { action_name[, action_name...] }) This grants the specified role permission to execute the named action(s) by default. This may be overridden via other mechanisms external to your module. The built-in roles that developers should use are: - prosody:user (normal user) - prosody:admin (host admin) - prosody:operator (global admin) The new prosody:operator role is intended for server-wide actions (such as shutting down Prosody). Finally, all usage of is_admin() in modules has been fixed by this commit. Some of these changes were trickier than others, but no change is expected to break existing deployments. EXCEPT: mod_auth_ldap no longer supports the ldap_admin_filter option. It's very possible nobody is using this, but if someone is then we can later update it to pull roles from LDAP somehow. --- plugins/adhoc/adhoc.lib.lua | 10 ++- plugins/adhoc/mod_adhoc.lua | 37 +++------- plugins/mod_announce.lua | 6 +- plugins/mod_auth_ldap.lua | 26 ++----- plugins/mod_authz_internal.lua | 145 +++++++++++++++++++++++++++++++++----- plugins/mod_disco.lua | 9 +-- plugins/mod_invites_adhoc.lua | 38 ++-------- plugins/mod_pubsub/mod_pubsub.lua | 4 +- plugins/muc/hidden.lib.lua | 8 +-- plugins/muc/mod_muc.lua | 19 ++--- plugins/muc/persistent.lib.lua | 11 +-- 11 files changed, 191 insertions(+), 122 deletions(-) (limited to 'plugins') diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua index eb91f252..9f091e3b 100644 --- a/plugins/adhoc/adhoc.lib.lua +++ b/plugins/adhoc/adhoc.lib.lua @@ -23,10 +23,16 @@ end function _M.new(name, node, handler, permission) if not permission then error "adhoc.new() expects a permission argument, none given" - end - if permission == "user" then + elseif permission == "user" then error "the permission mode 'user' has been renamed 'any', please update your code" end + if permission == "admin" then + module:default_permission("prosody:admin", "mod_adhoc:"..node); + permission = "check"; + elseif permission == "global_admin" then + module:default_permission("prosody:operator", "mod_adhoc:"..node); + permission = "check"; + end return { name = name, node = node, handler = handler, cmdtag = _cmdtag, permission = permission }; end diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua index 9d6ff77a..c94ff24f 100644 --- a/plugins/adhoc/mod_adhoc.lua +++ b/plugins/adhoc/mod_adhoc.lua @@ -7,7 +7,6 @@ local it = require "util.iterators"; local st = require "util.stanza"; -local is_admin = require "core.usermanager".is_admin; local jid_host = require "util.jid".host; local adhoc_handle_cmd = module:require "adhoc".handle_cmd; local xmlns_cmd = "http://jabber.org/protocol/commands"; @@ -15,18 +14,17 @@ local commands = {}; module:add_feature(xmlns_cmd); +local function check_permissions(event, node, command) + return (command.permission == "check" and module:may("mod_adhoc:"..node, event)) + or (command.permission == "local_user" and jid_host(event.stanza.attr.from) == module.host) + or (command.permission == "any"); +end + module:hook("host-disco-info-node", function (event) local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; if commands[node] then - local from = stanza.attr.from; - local privileged = is_admin(from, stanza.attr.to); - local global_admin = is_admin(from); - local hostname = jid_host(from); local command = commands[node]; - if (command.permission == "admin" and privileged) - or (command.permission == "global_admin" and global_admin) - or (command.permission == "local_user" and hostname == module.host) - or (command.permission == "any") then + if check_permissions(event, node, command) then reply:tag("identity", { name = command.name, category = "automation", type = "command-node" }):up(); reply:tag("feature", { var = xmlns_cmd }):up(); @@ -44,20 +42,13 @@ module:hook("host-disco-info-node", function (event) end); module:hook("host-disco-items-node", function (event) - local stanza, reply, disco_node = event.stanza, event.reply, event.node; + local reply, disco_node = event.reply, event.node; if disco_node ~= xmlns_cmd then return; end - local from = stanza.attr.from; - local admin = is_admin(from, stanza.attr.to); - local global_admin = is_admin(from); - local hostname = jid_host(from); for node, command in it.sorted_pairs(commands) do - if (command.permission == "admin" and admin) - or (command.permission == "global_admin" and global_admin) - or (command.permission == "local_user" and hostname == module.host) - or (command.permission == "any") then + if check_permissions(event, node, command) then reply:tag("item", { name = command.name, node = node, jid = module:get_host() }); reply:up(); @@ -71,15 +62,9 @@ module:hook("iq-set/host/"..xmlns_cmd..":command", function (event) local node = stanza.tags[1].attr.node local command = commands[node]; if command then - local from = stanza.attr.from; - local admin = is_admin(from, stanza.attr.to); - local global_admin = is_admin(from); - local hostname = jid_host(from); - if (command.permission == "admin" and not admin) - or (command.permission == "global_admin" and not global_admin) - or (command.permission == "local_user" and hostname ~= module.host) then + if not check_permissions(event, node, command) then origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up() - :add_child(command:cmdtag("canceled") + :add_child(command:cmdtag("canceled") :tag("note", {type="error"}):text("You don't have permission to execute this command"))); return true end diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua index c742ebb8..8161d4ba 100644 --- a/plugins/mod_announce.lua +++ b/plugins/mod_announce.lua @@ -9,7 +9,6 @@ local st, jid = require "util.stanza", require "util.jid"; local hosts = prosody.hosts; -local is_admin = require "core.usermanager".is_admin; function send_to_online(message, host) local sessions; @@ -34,6 +33,7 @@ function send_to_online(message, host) return c; end +module:default_permission("prosody:admin", ":send-announcement"); -- Old -based jabberd-style announcement sending function handle_announcement(event) @@ -45,8 +45,8 @@ function handle_announcement(event) return; -- Not an announcement end - if not is_admin(stanza.attr.from, host) then - -- Not an admin? Not allowed! + if not module:may(":send-announcement", event) then + -- Not allowed! module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from); return; end diff --git a/plugins/mod_auth_ldap.lua b/plugins/mod_auth_ldap.lua index 4d484aaa..a3ea880c 100644 --- a/plugins/mod_auth_ldap.lua +++ b/plugins/mod_auth_ldap.lua @@ -1,6 +1,5 @@ -- mod_auth_ldap -local jid_split = require "util.jid".split; local new_sasl = require "util.sasl".new; local lualdap = require "lualdap"; @@ -21,6 +20,13 @@ local ldap_admins = module:get_option_string("ldap_admin_filter", module:get_option_string("ldap_admins")); -- COMPAT with mistake in documentation local host = ldap_filter_escape(module:get_option_string("realm", module.host)); +if ldap_admins then + module:log("error", "The 'ldap_admin_filter' option has been deprecated, ".. + "and will be ignored. Equivalent functionality may be added in ".. + "the future if there is demand." + ); +end + -- Initiate connection local ld = nil; module.unload = function() if ld then pcall(ld, ld.close); end end @@ -133,22 +139,4 @@ else module:log("error", "Unsupported ldap_mode %s", tostring(ldap_mode)); end -if ldap_admins then - function provider.is_admin(jid) - local username, user_host = jid_split(jid); - if user_host ~= module.host then - return false; - end - return ldap_do("search", 2, { - base = ldap_base; - scope = ldap_scope; - sizelimit = 1; - filter = ldap_admins:gsub("%$(%a+)", { - user = ldap_filter_escape(username); - host = host; - }); - }); - end -end - module:provides("auth", provider); diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index 17687959..35bc3929 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -1,20 +1,89 @@ local array = require "util.array"; local it = require "util.iterators"; local set = require "util.set"; -local jid_split = require "util.jid".split; +local jid_split, jid_bare = require "util.jid".split, require "util.jid".bare; local normalize = require "util.jid".prep; +local config_global_admin_jids = module:context("*"):get_option_set("admins", {}) / normalize; local config_admin_jids = module:get_option_inherited_set("admins", {}) / normalize; local host = module.host; local role_store = module:open_store("roles"); local role_map_store = module:open_store("roles", "map"); -local admin_role = { ["prosody:admin"] = true }; +local role_methods = {}; +local role_mt = { __index = role_methods }; + +local role_registry = { + ["prosody:operator"] = { + default = true; + priority = 75; + includes = { "prosody:admin" }; + }; + ["prosody:admin"] = { + default = true; + priority = 50; + includes = { "prosody:user" }; + }; + ["prosody:user"] = { + default = true; + priority = 25; + includes = { "prosody:restricted" }; + }; + ["prosody:restricted"] = { + default = true; + priority = 15; + }; +}; + +-- Some processing on the role registry +for role_name, role_info in pairs(role_registry) do + role_info.name = role_name; + role_info.includes = set.new(role_info.includes) / function (included_role_name) + return role_registry[included_role_name]; + end; + if not role_info.permissions then + role_info.permissions = {}; + end + setmetatable(role_info, role_mt); +end + +function role_methods:may(action, context) + local policy = self.permissions[action]; + if policy ~= nil then + return policy; + end + for inherited_role in self.includes do + module:log("debug", "Checking included role '%s' for %s", inherited_role.name, action); + policy = inherited_role:may(action, context); + if policy ~= nil then + return policy; + end + end + return false; +end + +-- Public API + +local config_operator_role_set = { + ["prosody:operator"] = role_registry["prosody:operator"]; +}; +local config_admin_role_set = { + ["prosody:admin"] = role_registry["prosody:admin"]; +}; function get_user_roles(user) - if config_admin_jids:contains(user.."@"..host) then - return admin_role; + local bare_jid = user.."@"..host; + if config_global_admin_jids:contains(bare_jid) then + return config_operator_role_set; + elseif config_admin_jids:contains(bare_jid) then + return config_admin_role_set; + end + local role_names = role_store:get(user); + if not role_names then return {}; end + local roles = {}; + for role_name in pairs(role_names) do + roles[role_name] = role_registry[role_name]; end - return role_store:get(user); + return roles; end function set_user_roles(user, roles) @@ -22,10 +91,29 @@ function set_user_roles(user, roles) return true; end -function get_users_with_role(role) - local storage_role_users = it.to_array(it.keys(role_map_store:get_all(role) or {})); - if role == "prosody:admin" then - local config_admin_users = config_admin_jids / function (admin_jid) +function get_user_default_role(user) + local roles = get_user_roles(user); + if not roles then return nil; end + local default_role; + for role_name, role_info in pairs(roles) do --luacheck: ignore 213/role_name + if role_info.default and (not default_role or role_info.priority > default_role.priority) then + default_role = role_info; + end + end + if not default_role then return nil; end + return default_role; +end + +function get_users_with_role(role_name) + local storage_role_users = it.to_array(it.keys(role_map_store:get_all(role_name) or {})); + local config_set; + if role_name == "prosody:admin" then + config_set = config_admin_jids; + elseif role_name == "prosody:operator" then + config_set = config_global_admin_jids; + end + if config_set then + local config_admin_users = config_set / function (admin_jid) local j_node, j_host = jid_split(admin_jid); if j_host == host then return j_node; @@ -36,24 +124,49 @@ function get_users_with_role(role) return storage_role_users; end -function get_jid_roles(jid) - if config_admin_jids:contains(jid) then - return admin_role; +function get_jid_role(jid) + local bare_jid = jid_bare(jid); + if config_global_admin_jids:contains(bare_jid) then + return role_registry["prosody:operator"]; + elseif config_admin_jids:contains(bare_jid) then + return role_registry["prosody:admin"]; end return nil; end -function set_jid_roles(jid) -- luacheck: ignore 212 +function set_jid_role(jid) -- luacheck: ignore 212 return false; end -function get_jids_with_role(role) +function get_jids_with_role(role_name) -- Fetch role users from storage - local storage_role_jids = array.map(get_users_with_role(role), function (username) + local storage_role_jids = array.map(get_users_with_role(role_name), function (username) return username.."@"..host; end); - if role == "prosody:admin" then + if role_name == "prosody:admin" then return it.to_array(config_admin_jids + set.new(storage_role_jids)); + elseif role_name == "prosody:operator" then + return it.to_array(config_global_admin_jids + set.new(storage_role_jids)); end return storage_role_jids; end + +function add_default_permission(role_name, action, policy) + local role = role_registry[role_name]; + if not role then + module:log("warn", "Attempt to add default permission for unknown role: %s", role_name); + return nil, "no-such-role"; + end + if role.permissions[action] == nil then + if policy == nil then + policy = true; + end + module:log("debug", "Adding permission, role '%s' may '%s': %s", role_name, action, policy and "allow" or "deny"); + role.permissions[action] = policy; + end + return true; +end + +function get_role_info(role_name) + return role_registry[role_name]; +end diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua index 79249c52..7b3e5caf 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -8,7 +8,6 @@ local get_children = require "core.hostmanager".get_children; local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; -local um_is_admin = require "core.usermanager".is_admin; local jid_split = require "util.jid".split; local jid_bare = require "util.jid".bare; local st = require "util.stanza" @@ -162,14 +161,16 @@ module:hook("s2s-stream-features", function (event) end end); +module:default_permission("prosody:admin", ":be-discovered-admin"); + -- Handle disco requests to user accounts if module:get_host_type() ~= "local" then return end -- skip for components module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function(event) local origin, stanza = event.origin, event.stanza; local node = stanza.tags[1].attr.node; local username = jid_split(stanza.attr.to) or origin.username; - local is_admin = um_is_admin(stanza.attr.to or origin.full_jid, module.host) - if not stanza.attr.to or (expose_admins and is_admin) or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then + local target_is_admin = module:may(":be-discovered-admin", stanza.attr.to or origin.full_jid); + if not stanza.attr.to or (expose_admins and target_is_admin) or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then if node and node ~= "" then local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account @@ -185,7 +186,7 @@ module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function( end local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account - if is_admin then + if target_is_admin then reply:tag('identity', {category='account', type='admin'}):up(); elseif prosody.hosts[module.host].users.name == "anonymous" then reply:tag('identity', {category='account', type='anonymous'}):up(); diff --git a/plugins/mod_invites_adhoc.lua b/plugins/mod_invites_adhoc.lua index bd6f0c2e..04c74461 100644 --- a/plugins/mod_invites_adhoc.lua +++ b/plugins/mod_invites_adhoc.lua @@ -2,7 +2,6 @@ local dataforms = require "util.dataforms"; local datetime = require "util.datetime"; local split_jid = require "util.jid".split; -local usermanager = require "core.usermanager"; local new_adhoc = module:require("adhoc").new; @@ -13,8 +12,7 @@ local allow_user_invites = module:get_option_boolean("allow_user_invites", false -- on the server, use the option above instead. local allow_contact_invites = module:get_option_boolean("allow_contact_invites", true); -local allow_user_invite_roles = module:get_option_set("allow_user_invites_by_roles"); -local deny_user_invite_roles = module:get_option_set("deny_user_invites_by_roles"); +module:default_permission(allow_user_invites and "prosody:user" or "prosody:admin", ":invite-users"); local invites; if prosody.shutdown then -- COMPAT hack to detect prosodyctl @@ -42,36 +40,8 @@ local invite_result_form = dataforms.new({ -- This is for checking if the specified JID may create invites -- that allow people to register accounts on this host. -local function may_invite_new_users(jid) - if usermanager.get_roles then - local user_roles = usermanager.get_roles(jid, module.host); - if not user_roles then - -- User has no roles we can check, just return default - return allow_user_invites; - end - - if user_roles["prosody:admin"] then - return true; - end - if allow_user_invite_roles then - for allowed_role in allow_user_invite_roles do - if user_roles[allowed_role] then - return true; - end - end - end - if deny_user_invite_roles then - for denied_role in deny_user_invite_roles do - if user_roles[denied_role] then - return false; - end - end - end - elseif usermanager.is_admin(jid, module.host) then -- COMPAT w/0.11 - return true; -- Admins may always create invitations - end - -- No role matches, so whatever the default is - return allow_user_invites; +local function may_invite_new_users(context) + return module:may(":invite-users", context); end module:depends("adhoc"); @@ -91,7 +61,7 @@ module:provides("adhoc", new_adhoc("Create new contact invite", "urn:xmpp:invite }; }; end - local invite = invites.create_contact(username, may_invite_new_users(data.from), { + local invite = invites.create_contact(username, may_invite_new_users(data), { source = data.from }); --TODO: check errors diff --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua index ef31f326..f51e8fe4 100644 --- a/plugins/mod_pubsub/mod_pubsub.lua +++ b/plugins/mod_pubsub/mod_pubsub.lua @@ -1,7 +1,6 @@ local pubsub = require "util.pubsub"; local st = require "util.stanza"; local jid_bare = require "util.jid".bare; -local usermanager = require "core.usermanager"; local new_id = require "util.id".medium; local storagemanager = require "core.storagemanager"; local xtemplate = require "util.xtemplate"; @@ -177,9 +176,10 @@ module:hook("host-disco-items", function (event) end); local admin_aff = module:get_option_string("default_admin_affiliation", "owner"); +module:default_permission("prosody:admin", ":service-admin"); local function get_affiliation(jid) local bare_jid = jid_bare(jid); - if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then + if bare_jid == module.host or module:may(":service-admin", bare_jid) then return admin_aff; end end diff --git a/plugins/muc/hidden.lib.lua b/plugins/muc/hidden.lib.lua index 153df21a..087fa102 100644 --- a/plugins/muc/hidden.lib.lua +++ b/plugins/muc/hidden.lib.lua @@ -8,7 +8,7 @@ -- local restrict_public = not module:get_option_boolean("muc_room_allow_public", true); -local um_is_admin = require "core.usermanager".is_admin; +module:default_permission(restrict_public and "prosody:admin" or "prosody:user", ":create-public-room"); local function get_hidden(room) return room._data.hidden; @@ -22,8 +22,8 @@ local function set_hidden(room, hidden) end module:hook("muc-config-form", function(event) - if restrict_public and not um_is_admin(event.actor, module.host) then - -- Don't show option if public rooms are restricted and user is not admin of this host + if not module:may(":create-public-room", event.actor) then + -- Hide config option if this user is not allowed to create public rooms return; end table.insert(event.form, { @@ -36,7 +36,7 @@ module:hook("muc-config-form", function(event) end, 100-9); module:hook("muc-config-submitted/muc#roomconfig_publicroom", function(event) - if restrict_public and not um_is_admin(event.actor, module.host) then + if not module:may(":create-public-room", event.actor) then return; -- Not allowed end if set_hidden(event.room, not event.value) then diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua index 5873b1a2..08be3586 100644 --- a/plugins/muc/mod_muc.lua +++ b/plugins/muc/mod_muc.lua @@ -100,7 +100,6 @@ local jid_prep = require "util.jid".prep; local jid_bare = require "util.jid".bare; local st = require "util.stanza"; local cache = require "util.cache"; -local um_is_admin = require "core.usermanager".is_admin; module:require "muc/config_form_sections"; @@ -111,21 +110,23 @@ module:depends "muc_unique" module:require "muc/hats"; module:require "muc/lock"; -local function is_admin(jid) - return um_is_admin(jid, module.host); -end +module:default_permissions("prosody:admin", { + ":automatic-ownership"; + ":create-room"; + ":recreate-destroyed-room"; +}); if module:get_option_boolean("component_admins_as_room_owners", true) then -- Monkey patch to make server admins room owners local _get_affiliation = room_mt.get_affiliation; function room_mt:get_affiliation(jid) - if is_admin(jid) then return "owner"; end + if module:may(":automatic-ownership", jid) then return "owner"; end return _get_affiliation(self, jid); end local _set_affiliation = room_mt.set_affiliation; function room_mt:set_affiliation(actor, jid, affiliation, reason, data) - if affiliation ~= "owner" and is_admin(jid) then return nil, "modify", "not-acceptable"; end + if affiliation ~= "owner" and module:may(":automatic-ownership", jid) then return nil, "modify", "not-acceptable"; end return _set_affiliation(self, actor, jid, affiliation, reason, data); end end @@ -412,6 +413,8 @@ if module:get_option_boolean("muc_tombstones", true) then end, -10); end +module:default_permission("prosody:admin", ":create-room"); + do local restrict_room_creation = module:get_option("restrict_room_creation"); if restrict_room_creation == true then @@ -422,7 +425,7 @@ do module:hook("muc-room-pre-create", function(event) local origin, stanza = event.origin, event.stanza; local user_jid = stanza.attr.from; - if not is_admin(user_jid) and not ( + if not module:may(":create-room", event) and not ( restrict_room_creation == "local" and select(2, jid_split(user_jid)) == host_suffix ) then @@ -465,7 +468,7 @@ for event_name, method in pairs { if room and room._data.destroyed then if room._data.locked < os.time() - or (is_admin(stanza.attr.from) and stanza.name == "presence" and stanza.attr.type == nil) then + or (module:may(":recreate-destroyed-room", event) and stanza.name == "presence" and stanza.attr.type == nil) then -- Allow the room to be recreated by admin or after time has passed delete_room(room); room = nil; diff --git a/plugins/muc/persistent.lib.lua b/plugins/muc/persistent.lib.lua index c3b16ea4..4c753921 100644 --- a/plugins/muc/persistent.lib.lua +++ b/plugins/muc/persistent.lib.lua @@ -8,7 +8,10 @@ -- local restrict_persistent = not module:get_option_boolean("muc_room_allow_persistent", true); -local um_is_admin = require "core.usermanager".is_admin; +module:default_permission( + restrict_persistent and "prosody:admin" or "prosody:user", + ":create-persistent-room" +); local function get_persistent(room) return room._data.persistent; @@ -22,8 +25,8 @@ local function set_persistent(room, persistent) end module:hook("muc-config-form", function(event) - if restrict_persistent and not um_is_admin(event.actor, module.host) then - -- Don't show option if hidden rooms are restricted and user is not admin of this host + if not module:may(":create-persistent-room", event.actor) then + -- Hide config option if this user is not allowed to create persistent rooms return; end table.insert(event.form, { @@ -36,7 +39,7 @@ module:hook("muc-config-form", function(event) end, 100-5); module:hook("muc-config-submitted/muc#roomconfig_persistentroom", function(event) - if restrict_persistent and not um_is_admin(event.actor, module.host) then + if not module:may(":create-persistent-room", event.actor) then return; -- Not allowed end if set_persistent(event.room, event.value) then -- cgit v1.2.3 From 4db3d1572390ce5b615282cb1112358d9e3ba892 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 12 Jul 2022 13:14:47 +0100 Subject: usermanager, mod_auth_*: Add get_account_info() returning creation/update time This is useful for a number of things. For example, listing users that need to rotate their passwords after some event. It also provides a safer way for code to determine that a user password has changed without needing to set a handler for the password change event (which is a more fragile approach). --- plugins/mod_auth_internal_hashed.lua | 14 +++++++++++++- plugins/mod_auth_internal_plain.lua | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua index cf851eef..397d82e9 100644 --- a/plugins/mod_auth_internal_hashed.lua +++ b/plugins/mod_auth_internal_hashed.lua @@ -86,11 +86,21 @@ function provider.set_password(username, password) account.server_key = server_key_hex account.password = nil; + account.updated = os.time(); return accounts:set(username, account); end return nil, "Account not available."; end +function provider.get_account_info(username) + local account = accounts:get(username); + if not account then return nil, "Account not available"; end + return { + created = account.created; + password_updated = account.updated; + }; +end + function provider.user_exists(username) local account = accounts:get(username); if not account then @@ -115,9 +125,11 @@ function provider.create_user(username, password) end local stored_key_hex = to_hex(stored_key); local server_key_hex = to_hex(server_key); + local now = os.time(); return accounts:set(username, { stored_key = stored_key_hex, server_key = server_key_hex, - salt = salt, iteration_count = default_iteration_count + salt = salt, iteration_count = default_iteration_count, + created = now, updated = now; }); end diff --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua index 8a50e820..0f65323c 100644 --- a/plugins/mod_auth_internal_plain.lua +++ b/plugins/mod_auth_internal_plain.lua @@ -48,11 +48,21 @@ function provider.set_password(username, password) local account = accounts:get(username); if account then account.password = password; + account.updated = os.time(); return accounts:set(username, account); end return nil, "Account not available."; end +function provider.get_account_info(username) + local account = accounts:get(username); + if not account then return nil, "Account not available"; end + return { + created = account.created; + password_updated = account.updated; + }; +end + function provider.user_exists(username) local account = accounts:get(username); if not account then @@ -71,7 +81,11 @@ function provider.create_user(username, password) if not password then return nil, "Password fails SASLprep."; end - return accounts:set(username, {password = password}); + local now = os.time(); + return accounts:set(username, { + password = password; + created = now, updated = now; + }); end function provider.delete_user(username) -- cgit v1.2.3 From c0b857e5fb2a670d0a7a6ef29977ee58528e842f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 19 Jul 2022 18:02:02 +0100 Subject: mod_authz_internal: Use util.roles, some API changes and config support This commit was too awkward to split (hg record didn't like it), so: - Switch to the new util.roles lib to provide a consistent representation of a role object. - Change API method from get_role_info() to get_role_by_name() (touches sessionmanager and usermanager) - Change get_roles() to get_user_roles(), take a username instead of a JID This is more consistent with all other usermanager API methods. - Support configuration of custom roles and permissions via the config file (to be documented). --- plugins/mod_authz_internal.lua | 159 +++++++++++++++++++++++++---------------- 1 file changed, 96 insertions(+), 63 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index 35bc3929..135c7e61 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -3,62 +3,97 @@ local it = require "util.iterators"; local set = require "util.set"; local jid_split, jid_bare = require "util.jid".split, require "util.jid".bare; local normalize = require "util.jid".prep; +local roles = require "util.roles"; + local config_global_admin_jids = module:context("*"):get_option_set("admins", {}) / normalize; local config_admin_jids = module:get_option_inherited_set("admins", {}) / normalize; local host = module.host; local role_store = module:open_store("roles"); local role_map_store = module:open_store("roles", "map"); -local role_methods = {}; -local role_mt = { __index = role_methods }; - -local role_registry = { - ["prosody:operator"] = { - default = true; - priority = 75; - includes = { "prosody:admin" }; - }; - ["prosody:admin"] = { - default = true; - priority = 50; - includes = { "prosody:user" }; - }; - ["prosody:user"] = { - default = true; - priority = 25; - includes = { "prosody:restricted" }; - }; - ["prosody:restricted"] = { - default = true; - priority = 15; - }; +local role_registry = {}; + +function register_role(role) + if role_registry[role.name] ~= nil then + return error("A role '"..role.name.."' is already registered"); + end + if not roles.is_role(role) then + -- Convert table syntax to real role object + for i, inherited_role in ipairs(role.inherits or {}) do + if type(inherited_role) == "string" then + role.inherits[i] = assert(role_registry[inherited_role], "The named role '"..inherited_role.."' is not registered"); + end + end + if not role.permissions then role.permissions = {}; end + for _, allow_permission in ipairs(role.allow or {}) do + role.permissions[allow_permission] = true; + end + for _, deny_permission in ipairs(role.deny or {}) do + role.permissions[deny_permission] = false; + end + role = roles.new(role); + end + role_registry[role.name] = role; +end + +-- Default roles +register_role { + name = "prosody:restricted"; + priority = 15; +}; + +register_role { + name = "prosody:user"; + priority = 25; + inherits = { "prosody:restricted" }; +}; + +register_role { + name = "prosody:admin"; + priority = 50; + inherits = { "prosody:user" }; }; --- Some processing on the role registry -for role_name, role_info in pairs(role_registry) do - role_info.name = role_name; - role_info.includes = set.new(role_info.includes) / function (included_role_name) - return role_registry[included_role_name]; - end; - if not role_info.permissions then - role_info.permissions = {}; +register_role { + name = "prosody:operator"; + priority = 75; + inherits = { "prosody:admin" }; +}; + + +-- Process custom roles from config + +local custom_roles = module:get_option("custom_roles", {}); +for n, role_config in ipairs(custom_roles) do + local ok, err = pcall(register_role, role_config); + if not ok then + module:log("error", "Error registering custom role %s: %s", role_config.name or tostring(n), err); end - setmetatable(role_info, role_mt); end -function role_methods:may(action, context) - local policy = self.permissions[action]; - if policy ~= nil then - return policy; +-- Process custom permissions from config + +local config_add_perms = module:get_option("add_permissions", {}); +local config_remove_perms = module:get_option("remove_permissions", {}); + +for role_name, added_permissions in pairs(config_add_perms) do + if not role_registry[role_name] then + module:log("error", "Cannot add permissions to unknown role '%s'", role_name); + else + for _, permission in ipairs(added_permissions) do + role_registry[role_name]:set_permission(permission, true, true); + end end - for inherited_role in self.includes do - module:log("debug", "Checking included role '%s' for %s", inherited_role.name, action); - policy = inherited_role:may(action, context); - if policy ~= nil then - return policy; +end + +for role_name, removed_permissions in pairs(config_remove_perms) do + if not role_registry[role_name] then + module:log("error", "Cannot remove permissions from unknown role '%s'", role_name); + else + for _, permission in ipairs(removed_permissions) do + role_registry[role_name]:set_permission(permission, false, true); end end - return false; end -- Public API @@ -69,6 +104,9 @@ local config_operator_role_set = { local config_admin_role_set = { ["prosody:admin"] = role_registry["prosody:admin"]; }; +local default_role_set = { + ["prosody:user"] = role_registry["prosody:user"]; +}; function get_user_roles(user) local bare_jid = user.."@"..host; @@ -78,25 +116,25 @@ function get_user_roles(user) return config_admin_role_set; end local role_names = role_store:get(user); - if not role_names then return {}; end - local roles = {}; + if not role_names then return default_role_set; end + local user_roles = {}; for role_name in pairs(role_names) do - roles[role_name] = role_registry[role_name]; + user_roles[role_name] = role_registry[role_name]; end - return roles; + return user_roles; end -function set_user_roles(user, roles) - role_store:set(user, roles) +function set_user_roles(user, user_roles) + role_store:set(user, user_roles) return true; end function get_user_default_role(user) - local roles = get_user_roles(user); - if not roles then return nil; end + local user_roles = get_user_roles(user); + if not user_roles then return nil; end local default_role; - for role_name, role_info in pairs(roles) do --luacheck: ignore 213/role_name - if role_info.default and (not default_role or role_info.priority > default_role.priority) then + for role_name, role_info in pairs(user_roles) do --luacheck: ignore 213/role_name + if role_info.default ~= false and (not default_role or role_info.priority > default_role.priority) then default_role = role_info; end end @@ -134,7 +172,7 @@ function get_jid_role(jid) return nil; end -function set_jid_role(jid) -- luacheck: ignore 212 +function set_jid_role(jid, role_name) -- luacheck: ignore 212 return false; end @@ -157,16 +195,11 @@ function add_default_permission(role_name, action, policy) module:log("warn", "Attempt to add default permission for unknown role: %s", role_name); return nil, "no-such-role"; end - if role.permissions[action] == nil then - if policy == nil then - policy = true; - end - module:log("debug", "Adding permission, role '%s' may '%s': %s", role_name, action, policy and "allow" or "deny"); - role.permissions[action] = policy; - end - return true; + if policy == nil then policy = true; end + module:log("debug", "Adding policy %s for permission %s on role %s", policy, action, role_name); + return role:set_permission(action, policy); end -function get_role_info(role_name) - return role_registry[role_name]; +function get_role_by_name(role_name) + return assert(role_registry[role_name], role_name); end -- cgit v1.2.3 From a0f2f9ee193826cfb595bf93e237e33a926214f0 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 20 Jul 2022 10:52:17 +0100 Subject: mod_tokenauth: New API that better fits how modules are using token auth This also updates the module to the new role API, and improves support for scope/role selection (currently treated as the same thing, which they almost are). --- plugins/mod_tokenauth.lua | 52 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 12 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_tokenauth.lua b/plugins/mod_tokenauth.lua index c04a1aa4..6610036c 100644 --- a/plugins/mod_tokenauth.lua +++ b/plugins/mod_tokenauth.lua @@ -1,10 +1,19 @@ local id = require "util.id"; local jid = require "util.jid"; local base64 = require "util.encodings".base64; +local usermanager = require "core.usermanager"; +local generate_identifier = require "util.id".short; local token_store = module:open_store("auth_tokens", "map"); -function create_jid_token(actor_jid, token_jid, token_scope, token_ttl) +local function select_role(username, host, role) + if role then + return prosody.hosts[host].authz.get_role_by_name(role); + end + return usermanager.get_user_default_role(username, host); +end + +function create_jid_token(actor_jid, token_jid, token_role, token_ttl) token_jid = jid.prep(token_jid); if not actor_jid or token_jid ~= actor_jid and not jid.compare(token_jid, actor_jid) then return nil, "not-authorized"; @@ -21,13 +30,9 @@ function create_jid_token(actor_jid, token_jid, token_scope, token_ttl) created = os.time(); expires = token_ttl and (os.time() + token_ttl) or nil; jid = token_jid; - session = { - username = token_username; - host = token_host; - resource = token_resource; - auth_scope = token_scope; - }; + resource = token_resource; + role = token_role; }; local token_id = id.long(); @@ -46,11 +51,7 @@ local function parse_token(encoded_token) return token_id, token_user, token_host; end -function get_token_info(token) - local token_id, token_user, token_host = parse_token(token); - if not token_id then - return nil, "invalid-token-format"; - end +local function _get_parsed_token_info(token_id, token_user, token_host) if token_host ~= module.host then return nil, "invalid-host"; end @@ -70,6 +71,33 @@ function get_token_info(token) return token_info end +function get_token_info(token) + local token_id, token_user, token_host = parse_token(token); + if not token_id then + return nil, "invalid-token-format"; + end + return _get_parsed_token_info(token_id, token_user, token_host); +end + +function get_token_session(token, resource) + local token_id, token_user, token_host = parse_token(token); + if not token_id then + return nil, "invalid-token-format"; + end + + local token_info, err = _get_parsed_token_info(token_id, token_user, token_host); + if not token_info then return nil, err; end + + return { + username = token_user; + host = token_host; + resource = token_info.resource or resource or generate_identifier(); + + role = select_role(token_user, token_host, token_info.role); + }; +end + + function revoke_token(token) local token_id, token_user, token_host = parse_token(token); if not token_id then -- cgit v1.2.3 From 1fac00b2affd58bcfbe47347280a406eccefb805 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 15 Aug 2022 16:36:00 +0200 Subject: mod_admin_shell: Show session role in c2s:show --- plugins/mod_admin_shell.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 84ae0f72..bf682979 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -943,6 +943,15 @@ available_columns = { end end }; + role = { + title = "Role"; + description = "Session role"; + width = 20; + key = "role"; + mapper = function(role) + return role.name; + end; + } }; local function get_colspec(colspec, default) @@ -963,7 +972,7 @@ end function def_env.c2s:show(match_jid, colspec) local print = self.session.print; - local columns = get_colspec(colspec, { "id"; "jid"; "ipv"; "status"; "secure"; "smacks"; "csi" }); + local columns = get_colspec(colspec, { "id"; "jid"; "role"; "ipv"; "status"; "secure"; "smacks"; "csi" }); local row = format_table(columns, self.session.width); local function match(session) -- cgit v1.2.3 From f5768f63c993cee9f7f8e3c89db7e4e3080beab5 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 17 Aug 2022 16:38:53 +0100 Subject: mod_authz_internal, and more: New iteration of role API These changes to the API (hopefully the last) introduce a cleaner separation between the user's primary (default) role, and their secondary (optional) roles. To keep the code sane and reduce complexity, a data migration is needed for people using stored roles in 0.12. This can be performed with prosodyctl mod_authz_internal migrate --- plugins/mod_authz_internal.lua | 166 ++++++++++++++++++++++++++++++++--------- plugins/mod_c2s.lua | 2 +- plugins/mod_tokenauth.lua | 2 +- 3 files changed, 132 insertions(+), 38 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index 135c7e61..af402d3e 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -8,8 +8,9 @@ local roles = require "util.roles"; local config_global_admin_jids = module:context("*"):get_option_set("admins", {}) / normalize; local config_admin_jids = module:get_option_inherited_set("admins", {}) / normalize; local host = module.host; -local role_store = module:open_store("roles"); -local role_map_store = module:open_store("roles", "map"); + +local role_store = module:open_store("account_roles"); +local role_map_store = module:open_store("account_roles", "map"); local role_registry = {}; @@ -98,52 +99,96 @@ end -- Public API -local config_operator_role_set = { - ["prosody:operator"] = role_registry["prosody:operator"]; -}; -local config_admin_role_set = { - ["prosody:admin"] = role_registry["prosody:admin"]; -}; -local default_role_set = { - ["prosody:user"] = role_registry["prosody:user"]; -}; - -function get_user_roles(user) +-- Get the primary role of a user +function get_user_role(user) local bare_jid = user.."@"..host; + + -- Check config first if config_global_admin_jids:contains(bare_jid) then - return config_operator_role_set; + return role_registry["prosody:operator"]; elseif config_admin_jids:contains(bare_jid) then - return config_admin_role_set; + return role_registry["prosody:admin"]; + end + + -- Check storage + local stored_roles, err = role_store:get(user); + if not stored_roles then + if err then + -- Unable to fetch role, fail + return nil, err; + end + -- No role set, use default role + return role_registry["prosody:user"]; + end + if stored_roles._default == nil then + -- No primary role explicitly set, return default + return role_registry["prosody:user"]; + end + local primary_stored_role = role_registry[stored_roles._default]; + if not primary_stored_role then + return nil, "unknown-role"; + end + return primary_stored_role; +end + +-- Set the primary role of a user +function set_user_role(user, role_name) + local role = role_registry[role_name]; + if not role then + return error("Cannot assign default user an unknown role: "..tostring(role_name)); + end + local keys_update = { + _default = role_name; + -- Primary role cannot be secondary role + [role_name] = role_map_store.remove; + }; + if role_name == "prosody:user" then + -- Don't store default + keys_update._default = role_map_store.remove; + end + local ok, err = role_map_store:set_keys(user, keys_update); + if not ok then + return nil, err; end - local role_names = role_store:get(user); - if not role_names then return default_role_set; end - local user_roles = {}; - for role_name in pairs(role_names) do - user_roles[role_name] = role_registry[role_name]; + return role; +end + +function add_user_secondary_role(user, role_name) + if not role_registry[role_name] then + return error("Cannot assign default user an unknown role: "..tostring(role_name)); end - return user_roles; + role_map_store:set(user, role_name, true); end -function set_user_roles(user, user_roles) - role_store:set(user, user_roles) - return true; +function remove_user_secondary_role(user, role_name) + role_map_store:set(user, role_name, nil); end -function get_user_default_role(user) - local user_roles = get_user_roles(user); - if not user_roles then return nil; end - local default_role; - for role_name, role_info in pairs(user_roles) do --luacheck: ignore 213/role_name - if role_info.default ~= false and (not default_role or role_info.priority > default_role.priority) then - default_role = role_info; +function get_user_secondary_roles(user) + local stored_roles, err = role_store:get(user); + if not stored_roles then + if err then + -- Unable to fetch role, fail + return nil, err; end + -- No role set + return {}; + end + stored_roles._default = nil; + for role_name in pairs(stored_roles) do + stored_roles[role_name] = role_registry[role_name]; end - if not default_role then return nil; end - return default_role; + return stored_roles; end +-- This function is *expensive* function get_users_with_role(role_name) - local storage_role_users = it.to_array(it.keys(role_map_store:get_all(role_name) or {})); + local function role_filter(username, default_role) --luacheck: ignore 212/username + return default_role == role_name; + end + local primary_role_users = set.new(it.to_array(it.filter(role_filter, pairs(role_map_store:get_all("_default") or {})))); + local secondary_role_users = set.new(it.to_array(it.keys(role_map_store:get_all(role_name) or {}))); + local config_set; if role_name == "prosody:admin" then config_set = config_admin_jids; @@ -157,9 +202,9 @@ function get_users_with_role(role_name) return j_node; end end; - return it.to_array(config_admin_users + set.new(storage_role_users)); + return it.to_array(config_admin_users + primary_role_users + secondary_role_users); end - return storage_role_users; + return it.to_array(primary_role_users + secondary_role_users); end function get_jid_role(jid) @@ -203,3 +248,52 @@ end function get_role_by_name(role_name) return assert(role_registry[role_name], role_name); end + +-- COMPAT: Migrate from 0.12 role storage +local function do_migration(migrate_host) + local old_role_store = assert(module:context(migrate_host):open_store("roles")); + local new_role_store = assert(module:context(migrate_host):open_store("account_roles")); + + local migrated, failed, skipped = 0, 0, 0; + -- Iterate all users + for username in assert(old_role_store:users()) do + local old_roles = it.to_array(it.filter(function (k) return k:sub(1,1) ~= "_"; end, it.keys(old_role_store:get(username)))); + if #old_roles == 1 then + local ok, err = new_role_store:set(username, { + _default = old_roles[1]; + }); + if ok then + migrated = migrated + 1; + else + failed = failed + 1; + print("EE: Failed to store new role info for '"..username.."': "..err); + end + else + print("WW: User '"..username.."' has multiple roles and cannot be automatically migrated"); + skipped = skipped + 1; + end + end + return migrated, failed, skipped; +end + +function module.command(arg) + if arg[1] == "migrate" then + table.remove(arg, 1); + local migrate_host = arg[1]; + if not migrate_host or not prosody.hosts[migrate_host] then + print("EE: Please supply a valid host to migrate to the new role storage"); + return 1; + end + + -- Initialize storage layer + require "core.storagemanager".initialize_host(migrate_host); + + print("II: Migrating roles..."); + local migrated, failed, skipped = do_migration(migrate_host); + print(("II: %d migrated, %d failed, %d skipped"):format(migrated, failed, skipped)); + return (failed + skipped == 0) and 0 or 1; + else + print("EE: Unknown command: "..(arg[1] or "")); + print(" Hint: try 'migrate'?"); + end +end diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua index 8c0844ae..e8241687 100644 --- a/plugins/mod_c2s.lua +++ b/plugins/mod_c2s.lua @@ -259,7 +259,7 @@ local function disconnect_user_sessions(reason, leave_resource) end module:hook_global("user-password-changed", disconnect_user_sessions({ condition = "reset", text = "Password changed" }, true), 200); -module:hook_global("user-roles-changed", disconnect_user_sessions({ condition = "reset", text = "Roles changed" }), 200); +module:hook_global("user-role-changed", disconnect_user_sessions({ condition = "reset", text = "Role changed" }), 200); module:hook_global("user-deleted", disconnect_user_sessions({ condition = "not-authorized", text = "Account deleted" }), 200); function runner_callbacks:ready() diff --git a/plugins/mod_tokenauth.lua b/plugins/mod_tokenauth.lua index 6610036c..85602747 100644 --- a/plugins/mod_tokenauth.lua +++ b/plugins/mod_tokenauth.lua @@ -10,7 +10,7 @@ local function select_role(username, host, role) if role then return prosody.hosts[host].authz.get_role_by_name(role); end - return usermanager.get_user_default_role(username, host); + return usermanager.get_user_role(username, host); end function create_jid_token(actor_jid, token_jid, token_role, token_ttl) -- cgit v1.2.3 From f75ac951b518b04fb6b5f425950cfb2a8c8bb67b Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 18 Aug 2022 10:37:59 +0100 Subject: mod_authz_internal: Expose convenience method to test if user can assume role --- plugins/mod_authz_internal.lua | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index af402d3e..4f88b176 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -181,6 +181,18 @@ function get_user_secondary_roles(user) return stored_roles; end +function user_can_assume_role(user, role_name) + local primary_role = get_user_role(user); + if primary_role and primary_role.role_name == role_name then + return true; + end + local secondary_roles = get_user_secondary_roles(user); + if secondary_roles and secondary_roles[role_name] then + return true; + end + return false; +end + -- This function is *expensive* function get_users_with_role(role_name) local function role_filter(username, default_role) --luacheck: ignore 212/username -- cgit v1.2.3 From 4db3f8cf46824bd682cbf764369ed474d804f96b Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 18 Aug 2022 16:46:07 +0100 Subject: mod_admin_shell: Update with new role management commands and help text --- plugins/mod_admin_shell.lua | 91 ++++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 39 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index bf682979..dcbb4d09 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -271,20 +271,19 @@ function commands.help(session, data) print [[user:create(jid, password, roles) - Create the specified user account]] print [[user:password(jid, password) - Set the password for the specified user account]] print [[user:roles(jid, host) - Show current roles for an user]] - print [[user:setroles(jid, host, roles) - Set roles for an user (see 'help roles')]] + print [[user:setrole(jid, host, role) - Set primary role of a user (see 'help roles')]] + print [[user:addrole(jid, host, role) - Add a secondary role to a user]] + print [[user:delrole(jid, host, role) - Remove a secondary role from a user]] print [[user:delete(jid) - Permanently remove the specified user account]] print [[user:list(hostname, pattern) - List users on the specified host, optionally filtering with a pattern]] elseif section == "roles" then print [[Roles may grant access or restrict users from certain operations]] print [[Built-in roles are:]] - print [[ prosody:admin - Administrator]] - print [[ (empty set) - Normal user]] + print [[ prosody:user - Normal user (default)]] + print [[ prosody:admin - Host administrator]] + print [[ prosody:operator - Server administrator]] print [[]] - print [[The canonical role format looks like: { ["example:role"] = true }]] - print [[For convenience, the following formats are also accepted:]] - print [["admin" - short for "prosody:admin", the normal admin status (like the admins config option)]] - print [["example:role" - short for {["example:role"]=true}]] - print [[{"example:role"} - short for {["example:role"]=true}]] + print [[Roles can be assigned using the user management commands (see 'help user').]] elseif section == "muc" then -- TODO `muc:room():foo()` commands print [[muc:create(roomjid, { config }) - Create the specified MUC room with the given config]] @@ -1383,15 +1382,8 @@ end local um = require"core.usermanager"; -local function coerce_roles(roles) - if roles == "admin" then roles = "prosody:admin"; end - if type(roles) == "string" then roles = { [roles] = true }; end - if roles[1] then for i, role in ipairs(roles) do roles[role], roles[i] = true, nil; end end - return roles; -end - def_env.user = {}; -function def_env.user:create(jid, password, roles) +function def_env.user:create(jid, password, role) local username, host = jid_split(jid); if not prosody.hosts[host] then return nil, "No such host: "..host; @@ -1400,10 +1392,9 @@ function def_env.user:create(jid, password, roles) end local ok, err = um.create_user(username, password, host); if ok then - if ok and roles then - roles = coerce_roles(roles); - local roles_ok, rerr = um.set_roles(jid, host, roles); - if not roles_ok then return nil, "User created, but could not set roles: " .. tostring(rerr); end + if ok and role then + local role_ok, rerr = um.set_user_role(jid, host, role); + if not role_ok then return nil, "User created, but could not set role: " .. tostring(rerr); end end return true, "User created"; else @@ -1441,41 +1432,63 @@ function def_env.user:password(jid, password) end end -function def_env.user:roles(jid, host, new_roles) - if new_roles or type(host) == "table" then - return nil, "Use user:setroles(jid, host, roles) to change user roles"; - end +function def_env.user:role(jid, host) local username, userhost = jid_split(jid); if host == nil then host = userhost; end - if host ~= "*" and not prosody.hosts[host] then + if not prosody.hosts[host] then return nil, "No such host: "..host; elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then return nil, "No such user"; end - local roles = um.get_roles(jid, host); - if not roles then return true, "No roles"; end - local count = 0; - local print = self.session.print; - for role in pairs(roles) do + + local primary_role = um.get_user_role(username, host); + local secondary_roles = um.get_user_secondary_roles(username, host); + + print(primary_role and primary_role.name or ""); + + local count = primary_role and 1 or 0; + for role_name in pairs(secondary_roles or {}) do count = count + 1; - print(role); + print(role_name.." (secondary)"); end + return true, count == 1 and "1 role" or count.." roles"; end -def_env.user.showroles = def_env.user.roles; -- COMPAT +def_env.user.roles = def_env.user.role; --- user:roles("someone@example.com", "example.com", {"prosody:admin"}) --- user:roles("someone@example.com", {"prosody:admin"}) -function def_env.user:setroles(jid, host, new_roles) +-- user:setrole("someone@example.com", "example.com", "prosody:admin") +-- user:setrole("someone@example.com", "prosody:admin") +function def_env.user:setrole(jid, host, new_role) local username, userhost = jid_split(jid); - if new_roles == nil then host, new_roles = userhost, host; end - if host ~= "*" and not prosody.hosts[host] then + if new_role == nil then host, new_role = userhost, host; end + if not prosody.hosts[host] then + return nil, "No such host: "..host; + elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then + return nil, "No such user"; + end + return um.set_user_role(username, host, new_role); +end + +function def_env.user:addrole(jid, host, new_role) + local username, userhost = jid_split(jid); + if new_role == nil then host, new_role = userhost, host; end + if not prosody.hosts[host] then + return nil, "No such host: "..host; + elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then + return nil, "No such user"; + end + return um.add_user_secondary_role(username, host, new_role); +end + +function def_env.user:delrole(jid, host, role_name) + local username, userhost = jid_split(jid); + if role_name == nil then host, role_name = userhost, host; end + if not prosody.hosts[host] then return nil, "No such host: "..host; elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then return nil, "No such user"; end - if host == "*" then host = nil; end - return um.set_roles(jid, host, coerce_roles(new_roles)); + return um.remove_user_secondary_role(username, host, role_name); end -- TODO switch to table view, include roles -- cgit v1.2.3 From 8ff2f04e4ce842ae70b0edfaef1d237dc69d6dec Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 17:50:56 +0200 Subject: mod_auth_internal_hashed: Allow creating disabled account without password Otherwise, create_user(username, nil) leads to the account being deleted. --- plugins/mod_auth_internal_hashed.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua index 397d82e9..ddff31e9 100644 --- a/plugins/mod_auth_internal_hashed.lua +++ b/plugins/mod_auth_internal_hashed.lua @@ -115,8 +115,9 @@ function provider.users() end function provider.create_user(username, password) + local now = os.time(); if password == nil then - return accounts:set(username, {}); + return accounts:set(username, { created = now; updated = now; disabled = true }); end local salt = generate_uuid(); local valid, stored_key, server_key = get_auth_db(password, salt, default_iteration_count); @@ -125,7 +126,6 @@ function provider.create_user(username, password) end local stored_key_hex = to_hex(stored_key); local server_key_hex = to_hex(server_key); - local now = os.time(); return accounts:set(username, { stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = default_iteration_count, -- cgit v1.2.3 From 6f11c198b30fc581a2de25cdb3fe0b29c1d48eda Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 18:10:18 +0200 Subject: mod_admin_shell: Update help for user:create to reflect singular role argument --- plugins/mod_admin_shell.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index dcbb4d09..087b8768 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -268,7 +268,7 @@ function commands.help(session, data) print [[host:deactivate(hostname) - Disconnects all clients on this host and deactivates]] print [[host:list() - List the currently-activated hosts]] elseif section == "user" then - print [[user:create(jid, password, roles) - Create the specified user account]] + print [[user:create(jid, password, role) - Create the specified user account]] print [[user:password(jid, password) - Set the password for the specified user account]] print [[user:roles(jid, host) - Show current roles for an user]] print [[user:setrole(jid, host, role) - Set primary role of a user (see 'help roles')]] -- cgit v1.2.3 From 742153c55540bd9de365e775bd71c5c4544d88f8 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 18:10:44 +0200 Subject: mod_auth_insecure: Store creation and update timestamps on account This ensures that the store is not empty in case no password is provided, so the underlying data storage won't consider the store empty. --- plugins/mod_auth_insecure.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_auth_insecure.lua b/plugins/mod_auth_insecure.lua index dc5ee616..5428d1fa 100644 --- a/plugins/mod_auth_insecure.lua +++ b/plugins/mod_auth_insecure.lua @@ -27,6 +27,7 @@ function provider.set_password(username, password) return nil, "Password fails SASLprep."; end if account then + account.updated = os.time(); account.password = password; return datamanager.store(username, host, "accounts", account); end @@ -38,7 +39,8 @@ function provider.user_exists(username) end function provider.create_user(username, password) - return datamanager.store(username, host, "accounts", {password = password}); + local now = os.time(); + return datamanager.store(username, host, "accounts", { created = now; updated = now; password = password }); end function provider.delete_user(username) -- cgit v1.2.3 From 96e172167d9b0d135d2937a83b252700f458f4fe Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 19:00:01 +0200 Subject: mod_admin_shell: Ensure account has role before it is usable By creating the account first without a password it can't be used until the role has set. This is most important for restricted accounts, as a failure to set the role would lead to the account having more privileges than indented. --- plugins/mod_admin_shell.lua | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 087b8768..49e07dae 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -1390,16 +1390,24 @@ function def_env.user:create(jid, password, role) elseif um.user_exists(username, host) then return nil, "User exists"; end - local ok, err = um.create_user(username, password, host); - if ok then - if ok and role then - local role_ok, rerr = um.set_user_role(jid, host, role); - if not role_ok then return nil, "User created, but could not set role: " .. tostring(rerr); end - end - return true, "User created"; - else + local ok, err = um.create_user(username, nil, host); + if not ok then return nil, "Could not create user: "..err; end + + if role then + local role_ok, rerr = um.set_user_role(jid, host, role); + if not role_ok then + return nil, "Could not set role: " .. tostring(rerr); + end + end + + local ok, err = um.set_password(username, password, host, nil); + if not ok then + return nil, "Could not set password for user: "..err; + end + + return true, "User created"; end function def_env.user:delete(jid) -- cgit v1.2.3 From b2921275006698b2dee48de667db068c861fa620 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 25 Aug 2022 22:40:41 +0200 Subject: mod_admin_shell: Fix output from user:roles() It used _G.print instead of the shell session print, which would silently write to stdout --- plugins/mod_admin_shell.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 49e07dae..042d6cff 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -1441,6 +1441,7 @@ function def_env.user:password(jid, password) end function def_env.user:role(jid, host) + local print = self.session.print; local username, userhost = jid_split(jid); if host == nil then host = userhost; end if not prosody.hosts[host] then -- cgit v1.2.3 From 95bba786f1fb0e15b98fa2b475d0ad585ad359a3 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 25 Aug 2022 22:42:41 +0200 Subject: mod_admin_shell: Rename variable to avoid confusion with global function For luacheck, but it doesn't actually complain about this right now --- plugins/mod_admin_shell.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 042d6cff..14fab8ad 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -1265,18 +1265,18 @@ end function def_env.host:list() local print = self.session.print; local i = 0; - local type; + local host_type; for host, host_session in iterators.sorted_pairs(prosody.hosts, _sort_hosts) do i = i + 1; - type = host_session.type; - if type == "local" then + host_type = host_session.type; + if host_type == "local" then print(host); else - type = module:context(host):get_option_string("component_module", type); - if type ~= "component" then - type = type .. " component"; + host_type = module:context(host):get_option_string("component_module", host_type); + if host_type ~= "component" then + host_type = host_type .. " component"; end - print(("%s (%s)"):format(host, type)); + print(("%s (%s)"):format(host, host_type)); end end return true, i.." hosts"; -- cgit v1.2.3 From 54fcc029c8fafd6e9fd9a38e279e5557177c1370 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 26 Aug 2022 17:04:15 +0100 Subject: mod_smacks: Long overdue cleanup of resumption code, fixes some old TODOs --- plugins/mod_c2s.lua | 8 +++++++ plugins/mod_smacks.lua | 64 ++++++++++---------------------------------------- 2 files changed, 21 insertions(+), 51 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua index e8241687..2d4186d0 100644 --- a/plugins/mod_c2s.lua +++ b/plugins/mod_c2s.lua @@ -262,6 +262,14 @@ module:hook_global("user-password-changed", disconnect_user_sessions({ condition module:hook_global("user-role-changed", disconnect_user_sessions({ condition = "reset", text = "Role changed" }), 200); module:hook_global("user-deleted", disconnect_user_sessions({ condition = "not-authorized", text = "Account deleted" }), 200); +module:hook_global("c2s-session-updated", function (event) + sessions[event.session.conn] = event.session; + local replaced_conn = event.replaced_conn; + if replaced_conn then + sessions[replaced_conn] = nil; + end +end); + function runner_callbacks:ready() if self.data.conn then self.data.conn:resume(); diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index e2bbff9c..a1435829 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -196,7 +196,6 @@ local function outgoing_stanza_filter(stanza, session) -- supposed to be nil. -- However, when using mod_smacks with mod_websocket, then mod_websocket's -- stanzas/out filter can get called before this one and adds the xmlns. - if session.resending_unacked then return stanza end if not session.smacks then return stanza end local is_stanza = st.is_stanza(stanza) and (not stanza.attr.xmlns or stanza.attr.xmlns == 'jabber:client') @@ -496,7 +495,6 @@ module:hook("pre-resource-unbind", function (event) session.log("debug", "Destroying session for hibernating too long"); save_old_session(session); session.resumption_token = nil; - session.resending_unacked = true; -- stop outgoing_stanza_filter from re-queueing anything anymore sessionmanager.destroy_session(session, "Hibernating too long"); sessions_expired(1); end); @@ -529,10 +527,6 @@ end module:hook("s2sout-destroyed", handle_s2s_destroyed); module:hook("s2sin-destroyed", handle_s2s_destroyed); -local function get_session_id(session) - return session.id or (tostring(session):match("[a-f0-9]+$")); -end - function handle_resume(session, stanza, xmlns_sm) if session.full_jid then session.log("warn", "Tried to resume after resource binding"); @@ -573,40 +567,11 @@ function handle_resume(session, stanza, xmlns_sm) local now = os_time(); age = now - original_session.hibernating; end - session.log("debug", "mod_smacks resuming existing session %s...", get_session_id(original_session)); - original_session.log("debug", "mod_smacks session resumed from %s...", get_session_id(session)); - -- TODO: All this should move to sessionmanager (e.g. session:replace(new_session)) - if original_session.conn then - original_session.log("debug", "mod_smacks closing an old connection for this session"); - local conn = original_session.conn; - c2s_sessions[conn] = nil; - conn:close(); - end - local migrated_session_log = session.log; - original_session.ip = session.ip; - original_session.conn = session.conn; - original_session.rawsend = session.rawsend; - original_session.rawsend.session = original_session; - original_session.rawsend.conn = original_session.conn; - original_session.send = session.send; - original_session.send.session = original_session; - original_session.close = session.close; - original_session.filter = session.filter; - original_session.filter.session = original_session; - original_session.filters = session.filters; - original_session.send.filter = original_session.filter; - original_session.stream = session.stream; - original_session.secure = session.secure; - original_session.hibernating = nil; - original_session.resumption_counter = (original_session.resumption_counter or 0) + 1; - session.log = original_session.log; - session.type = original_session.type; - wrap_session(original_session, true); - -- Inform xmppstream of the new session (passed to its callbacks) - original_session.stream:set_session(original_session); - -- Similar for connlisteners - c2s_sessions[session.conn] = original_session; + session.log("debug", "mod_smacks resuming existing session %s...", original_session.id); + + -- Update original_session with the parameters (connection, etc.) from the new session + sessionmanager.update_session(original_session, session); local queue = original_session.outgoing_stanza_queue; local h = tonumber(stanza.attr.h); @@ -633,20 +598,16 @@ function handle_resume(session, stanza, xmlns_sm) -- We have to use the send of "session" because we don't want to add our resent stanzas -- to the outgoing queue again - session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", queue:count_unacked()); - -- FIXME Which session is it that the queue filter sees? - session.resending_unacked = true; - original_session.resending_unacked = true; + original_session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", queue:count_unacked()); for _, queued_stanza in queue:resume() do - session.send(queued_stanza); - end - session.resending_unacked = nil; - original_session.resending_unacked = nil; - session.log("debug", "all stanzas resent, now disabling send() in this migrated session, #queue = %d", queue:count_unacked()); - function session.send(stanza) -- luacheck: ignore 432 - migrated_session_log("error", "Tried to send stanza on old session migrated by smacks resume (maybe there is a bug?): %s", tostring(stanza)); - return false; + original_session.send(queued_stanza); end + session.log("debug", "all stanzas resent, enabling stream management on resumed stream, #queue = %d", queue:count_unacked()); + + -- Add our own handlers to the resumed session (filters have been reset in the update) + wrap_session(original_session, true); + + -- Let everyone know that we are no longer hibernating module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue:table()}); original_session.awaiting_ack = nil; -- Don't wait for acks from before the resumption request_ack_now_if_needed(original_session, true, "handle_resume", nil); @@ -654,6 +615,7 @@ function handle_resume(session, stanza, xmlns_sm) end return true; end + module:hook_tag(xmlns_sm2, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm2); end); module:hook_tag(xmlns_sm3, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm3); end); -- cgit v1.2.3 From 9c5aefe7e7a884a3749133e85b3725e1472b471f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 26 Aug 2022 17:28:06 +0100 Subject: mod_smacks: Don't close resuming session when failed due to overflow --- plugins/mod_smacks.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index a1435829..42a903f6 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -570,9 +570,6 @@ function handle_resume(session, stanza, xmlns_sm) session.log("debug", "mod_smacks resuming existing session %s...", original_session.id); - -- Update original_session with the parameters (connection, etc.) from the new session - sessionmanager.update_session(original_session, session); - local queue = original_session.outgoing_stanza_queue; local h = tonumber(stanza.attr.h); @@ -583,13 +580,17 @@ function handle_resume(session, stanza, xmlns_sm) err = ack_errors.new("overflow"); end - if err or not queue:resumable() then - original_session.send(st.stanza("failed", + if err then + session.send(st.stanza("failed", { xmlns = xmlns_sm; h = format_h(original_session.handled_stanza_count); previd = id })); - original_session:close(err); - return false; + session.log("debug", "Resumption failed: %s", err); + return true; end + -- Update original_session with the parameters (connection, etc.) from the new session + sessionmanager.update_session(original_session, session); + + -- Inform client of successful resumption original_session.send(st.stanza("resumed", { xmlns = xmlns_sm, h = format_h(original_session.handled_stanza_count), previd = id })); -- cgit v1.2.3 From a018497a27dea6d379d7772b36c4ba5593d67ecb Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 26 Aug 2022 19:10:15 +0200 Subject: mod_s2s: Simplify conditionals since all sessions should have .host now --- plugins/mod_s2s.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index 1df3d5ad..7eae9043 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -275,7 +275,7 @@ function module.add_host(module) function module.unload() if module.reloading then return end for _, session in pairs(sessions) do - if session.to_host == module.host or session.from_host == module.host then + if session.host == module.host then session:close("host-gone"); end end -- cgit v1.2.3 From 6926340d75370f106ad8694041d9b3f4e0078bdb Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 26 Aug 2022 19:07:36 +0100 Subject: mod_smacks: Split resumption into multiple stages, to simplify ISR integration This will allow us to return the success/failed as part of the SASL2 response, and *then* perform the stanza sync as a second step. --- plugins/mod_smacks.lua | 145 ++++++++++++++++++++++++++++--------------------- 1 file changed, 82 insertions(+), 63 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 42a903f6..7aa8687a 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -107,6 +107,12 @@ local ack_errors = require"util.error".init("mod_smacks", xmlns_sm3, { overflow = { condition = "resource-constraint", text = "Too many unacked stanzas remaining, session can't be resumed" } }); +local resume_errors = require "util.error".init("mod_smacks", xmlns_sm3, { + expired = { condition = "item-not-found", text = "Session expired, and cannot be resumed" }; + already_bound = { condition = "unexpected-request", text = "Cannot resume another session after a resource is bound" }; + unknown_session = { condition = "item-not-found", text = "Unknown session" }; +}); + -- COMPAT note the use of compatibility wrapper in events (queue:table()) local function ack_delayed(session, stanza) @@ -527,13 +533,10 @@ end module:hook("s2sout-destroyed", handle_s2s_destroyed); module:hook("s2sin-destroyed", handle_s2s_destroyed); -function handle_resume(session, stanza, xmlns_sm) +function do_resume(session, stanza) if session.full_jid then session.log("warn", "Tried to resume after resource binding"); - session.send(st.stanza("failed", { xmlns = xmlns_sm }) - :tag("unexpected-request", { xmlns = xmlns_errors }) - ); - return true; + return nil, resume_errors.new("already_bound"); end local id = stanza.attr.previd; @@ -542,78 +545,94 @@ function handle_resume(session, stanza, xmlns_sm) local old_session = old_session_registry:get(session.username, id); if old_session then session.log("debug", "Tried to resume old expired session with id %s", id); - session.send(st.stanza("failed", { xmlns = xmlns_sm, h = format_h(old_session.h) }) - :tag("item-not-found", { xmlns = xmlns_errors }) - ); clear_old_session(session, id); resumption_expired(1); - else - session.log("debug", "Tried to resume non-existent session with id %s", id); - session.send(st.stanza("failed", { xmlns = xmlns_sm }) - :tag("item-not-found", { xmlns = xmlns_errors }) - ); - end; - else - if original_session.hibernating_watchdog then - original_session.log("debug", "Letting the watchdog go"); - original_session.hibernating_watchdog:cancel(); - original_session.hibernating_watchdog = nil; - elseif session.hibernating then - original_session.log("error", "Hibernating session has no watchdog!") - end - -- zero age = was not hibernating yet - local age = 0; - if original_session.hibernating then - local now = os_time(); - age = now - original_session.hibernating; + return nil, resume_errors.new("expired", { h = old_session.h }); end + session.log("debug", "Tried to resume non-existent session with id %s", id); + return nil, resume_errors.new("unknown_session"); + end - session.log("debug", "mod_smacks resuming existing session %s...", original_session.id); - - local queue = original_session.outgoing_stanza_queue; - local h = tonumber(stanza.attr.h); + if original_session.hibernating_watchdog then + original_session.log("debug", "Letting the watchdog go"); + original_session.hibernating_watchdog:cancel(); + original_session.hibernating_watchdog = nil; + elseif session.hibernating then + original_session.log("error", "Hibernating session has no watchdog!") + end + -- zero age = was not hibernating yet + local age = 0; + if original_session.hibernating then + local now = os_time(); + age = now - original_session.hibernating; + end - original_session.log("debug", "Pre-resumption #queue = %d", queue:count_unacked()) - local acked, err = ack_errors.coerce(queue:ack(h)); -- luacheck: ignore 211/acked + session.log("debug", "mod_smacks resuming existing session %s...", original_session.id); - if not err and not queue:resumable() then - err = ack_errors.new("overflow"); - end + local queue = original_session.outgoing_stanza_queue; + local h = tonumber(stanza.attr.h); - if err then - session.send(st.stanza("failed", - { xmlns = xmlns_sm; h = format_h(original_session.handled_stanza_count); previd = id })); - session.log("debug", "Resumption failed: %s", err); - return true; - end + original_session.log("debug", "Pre-resumption #queue = %d", queue:count_unacked()) + local acked, err = ack_errors.coerce(queue:ack(h)); -- luacheck: ignore 211/acked - -- Update original_session with the parameters (connection, etc.) from the new session - sessionmanager.update_session(original_session, session); + if not err and not queue:resumable() then + err = ack_errors.new("overflow"); + end - -- Inform client of successful resumption - original_session.send(st.stanza("resumed", { xmlns = xmlns_sm, - h = format_h(original_session.handled_stanza_count), previd = id })); + if err then + session.log("debug", "Resumption failed: %s", err); + return nil, err; + end - -- Ok, we need to re-send any stanzas that the client didn't see - -- ...they are what is now left in the outgoing stanza queue - -- We have to use the send of "session" because we don't want to add our resent stanzas - -- to the outgoing queue again + -- Update original_session with the parameters (connection, etc.) from the new session + sessionmanager.update_session(original_session, session); + + return { + session = original_session; + id = id; + -- Return function to complete the resumption and resync unacked stanzas + -- This is two steps so we can support SASL2/ISR + finish = function () + -- Ok, we need to re-send any stanzas that the client didn't see + -- ...they are what is now left in the outgoing stanza queue + -- We have to use the send of "session" because we don't want to add our resent stanzas + -- to the outgoing queue again + + original_session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", queue:count_unacked()); + for _, queued_stanza in queue:resume() do + original_session.send(queued_stanza); + end + original_session.log("debug", "all stanzas resent, enabling stream management on resumed stream, #queue = %d", queue:count_unacked()); - original_session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", queue:count_unacked()); - for _, queued_stanza in queue:resume() do - original_session.send(queued_stanza); - end - session.log("debug", "all stanzas resent, enabling stream management on resumed stream, #queue = %d", queue:count_unacked()); + -- Add our own handlers to the resumed session (filters have been reset in the update) + wrap_session(original_session, true); - -- Add our own handlers to the resumed session (filters have been reset in the update) - wrap_session(original_session, true); + -- Let everyone know that we are no longer hibernating + module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue:table()}); + original_session.awaiting_ack = nil; -- Don't wait for acks from before the resumption + request_ack_now_if_needed(original_session, true, "handle_resume", nil); + resumption_age:sample(age); + end; + }; +end - -- Let everyone know that we are no longer hibernating - module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue:table()}); - original_session.awaiting_ack = nil; -- Don't wait for acks from before the resumption - request_ack_now_if_needed(original_session, true, "handle_resume", nil); - resumption_age:sample(age); +function handle_resume(session, stanza, xmlns_sm) + local resumed, err = do_resume(session, stanza); + if not resumed then + session.send(st.stanza("failed", { xmlns = xmlns_sm, h = format_h(err.context.h) }) + :tag(err.condition, { xmlns = xmlns_errors })); + return true; end + + session = resumed.session; + + -- Inform client of successful resumption + session.send(st.stanza("resumed", { xmlns = xmlns_sm, + h = format_h(session.handled_stanza_count), previd = resumed.id })); + + -- Complete resume (sync stanzas, etc.) + resumed.finish(); + return true; end -- cgit v1.2.3 From 2b397d14525079172b2c5aaf2b184a7fa66f6d61 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 27 Aug 2022 17:19:13 +0200 Subject: mod_external_services: Update tools.ietf.org URL See bd9e006a7a74 --- plugins/mod_external_services.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_external_services.lua b/plugins/mod_external_services.lua index ae418fd8..6a76b922 100644 --- a/plugins/mod_external_services.lua +++ b/plugins/mod_external_services.lua @@ -16,7 +16,7 @@ local configured_services = module:get_option_array("external_services", {}); local access = module:get_option_set("external_service_access", {}); --- https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 +-- https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00 local function behave_turn_rest_credentials(srv, item, secret) local ttl = default_ttl; if type(item.ttl) == "number" then -- cgit v1.2.3 From a2cabe641854d887bd5814ddf3037a3c4cd7440d Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sun, 28 Aug 2022 07:51:50 +0100 Subject: mod_component: Require 'from' attribute on stanzas by default The old behaviour of falling back to the component domain when it is missing has been merged into the logic for the existing "validate_from_addresses" option (which is strict by default). ejabberd already rejects component stanzas with no 'from' (as the XEP requires), and this has led to compatibility issues for components that were seemingly working fine with Prosody. --- plugins/mod_component.lua | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua index f57c4381..c1c29b5e 100644 --- a/plugins/mod_component.lua +++ b/plugins/mod_component.lua @@ -17,7 +17,7 @@ local logger = require "util.logger"; local sha1 = require "util.hashes".sha1; local st = require "util.stanza"; -local jid_split = require "util.jid".split; +local jid_host = require "util.jid".host; local new_xmpp_stream = require "util.xmppstream".new; local uuid_gen = require "util.uuid".generate; @@ -222,22 +222,19 @@ function stream_callbacks.handlestanza(session, stanza) end if not stanza.attr.xmlns or stanza.attr.xmlns == "jabber:client" then local from = stanza.attr.from; - if from then - if session.component_validate_from then - local _, domain = jid_split(stanza.attr.from); - if domain ~= session.host then - -- Return error - session.log("warn", "Component sent stanza with missing or invalid 'from' address"); - session:close{ - condition = "invalid-from"; - text = "Component tried to send from address <"..tostring(from) - .."> which is not in domain <"..tostring(session.host)..">"; - }; - return; - end + if session.component_validate_from then + if not from or (jid_host(from) ~= session.host) then + -- Return error + session.log("warn", "Component sent stanza with missing or invalid 'from' address"); + session:close{ + condition = "invalid-from"; + text = "Component tried to send from address <"..(from or "< [missing 'from' attribute] >") + .."> which is not in domain <"..tostring(session.host)..">"; + }; + return; end - else - stanza.attr.from = session.host; -- COMPAT: Strictly we shouldn't allow this + elseif not from then + stanza.attr.from = session.host; end if not stanza.attr.to then session.log("warn", "Rejecting stanza with no 'to' address"); -- cgit v1.2.3 From 79ac7f6b74e25eb62f4590cb76f424507954085c Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 29 Aug 2022 15:45:52 +0100 Subject: mod_smacks: Split enable handling to stages, to allow easier SASL2 integration --- plugins/mod_smacks.lua | 69 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 21 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 7aa8687a..2ffbc45a 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -107,7 +107,11 @@ local ack_errors = require"util.error".init("mod_smacks", xmlns_sm3, { overflow = { condition = "resource-constraint", text = "Too many unacked stanzas remaining, session can't be resumed" } }); -local resume_errors = require "util.error".init("mod_smacks", xmlns_sm3, { +local enable_errors = require "util.error".init("mod_smacks", xmlns_sm3, { + already_enabled = { condition = "unexpected-request", text = "Stream management is already enabled" }; + bind_required = { condition = "unexpected-request", text = "Client must bind a resource before enabling stream management" }; + unavailable = { condition = "service-unavailable", text = "Stream management is not available for this stream" }; + -- Resumption expired = { condition = "item-not-found", text = "Session expired, and cannot be resumed" }; already_bound = { condition = "unexpected-request", text = "Cannot resume another session after a resource is bound" }; unknown_session = { condition = "item-not-found", text = "Unknown session" }; @@ -127,18 +131,18 @@ local function ack_delayed(session, stanza) end local function can_do_smacks(session, advertise_only) - if session.smacks then return false, "unexpected-request", "Stream management is already enabled"; end + if session.smacks then return false, enable_errors.new("already_enabled"); end local session_type = session.type; if session.username then if not(advertise_only) and not(session.resource) then -- Fail unless we're only advertising sm - return false, "unexpected-request", "Client must bind a resource before enabling stream management"; + return false, enable_errors.new("bind_required"); end return true; elseif s2s_smacks and (session_type == "s2sin" or session_type == "s2sout") then return true; end - return false, "service-unavailable", "Stream management is not available for this stream"; + return false, enable_errors.new("unavailable"); end module:hook("stream-features", @@ -294,12 +298,11 @@ local function wrap_session(session, resume) return session; end -function handle_enable(session, stanza, xmlns_sm) - local ok, err, err_text = can_do_smacks(session); +function do_enable(session, stanza) + local ok, err = can_do_smacks(session); if not ok then - session.log("warn", "Failed to enable smacks: %s", err_text); -- TODO: XEP doesn't say we can send error text, should it? - (session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):tag(err, { xmlns = xmlns_errors})); - return true; + session.log("warn", "Failed to enable smacks: %s", err.text); -- TODO: XEP doesn't say we can send error text, should it? + return nil, err; end if session.username then @@ -320,20 +323,44 @@ function handle_enable(session, stanza, xmlns_sm) end end - session.log("debug", "Enabling stream management"); - session.smacks = xmlns_sm; - - wrap_session(session, false); - - local resume_max; local resume_token; local resume = stanza.attr.resume; if resume == "true" or resume == "1" then resume_token = new_id(); - track_session(session, resume_token); - resume_max = tostring(resume_timeout); end - (session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm, id = resume_token, resume = resume, max = resume_max })); + + return { + id = resume_token; + resume_max = resume_token and tostring(resume_timeout) or nil; + session = session; + finish = function () + session.log("debug", "Enabling stream management"); + + track_session(session, resume_token); + wrap_session(session, false); + + end; + }; +end + +function handle_enable(session, stanza, xmlns_sm) + local enabled, err = do_enable(session, stanza); + if not enabled then + (session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):add_error(err)); + return true; + end + + session.smacks = xmlns_sm; + + (session.sends2s or session.send)(st.stanza("enabled", { + xmlns = xmlns_sm; + id = enabled.id; + resume = enabled.id and "1" or nil; + max = enabled.resume_max; + })); + + enabled.finish(); + return true; end module:hook_tag(xmlns_sm2, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm2); end, 100); @@ -536,7 +563,7 @@ module:hook("s2sin-destroyed", handle_s2s_destroyed); function do_resume(session, stanza) if session.full_jid then session.log("warn", "Tried to resume after resource binding"); - return nil, resume_errors.new("already_bound"); + return nil, enable_errors.new("already_bound"); end local id = stanza.attr.previd; @@ -547,10 +574,10 @@ function do_resume(session, stanza) session.log("debug", "Tried to resume old expired session with id %s", id); clear_old_session(session, id); resumption_expired(1); - return nil, resume_errors.new("expired", { h = old_session.h }); + return nil, enable_errors.new("expired", { h = old_session.h }); end session.log("debug", "Tried to resume non-existent session with id %s", id); - return nil, resume_errors.new("unknown_session"); + return nil, enable_errors.new("unknown_session"); end if original_session.hibernating_watchdog then -- cgit v1.2.3 From 1254a0de555478847dcea21c3d3b519624a9e5a6 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 29 Aug 2022 15:48:07 +0100 Subject: mod_smacks: Use new :add_error() in last remaining error result construction --- plugins/mod_smacks.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 2ffbc45a..b3f6d189 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -57,7 +57,6 @@ local it = require"util.iterators"; local sessionmanager = require "core.sessionmanager"; -local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas"; local xmlns_delay = "urn:xmpp:delay"; local xmlns_mam2 = "urn:xmpp:mam:2"; local xmlns_sm2 = "urn:xmpp:sm:2"; @@ -647,7 +646,7 @@ function handle_resume(session, stanza, xmlns_sm) local resumed, err = do_resume(session, stanza); if not resumed then session.send(st.stanza("failed", { xmlns = xmlns_sm, h = format_h(err.context.h) }) - :tag(err.condition, { xmlns = xmlns_errors })); + :add_error(err)); return true; end -- cgit v1.2.3 From 78a197c25d6b0658f5745f3010190856fcf0ffb2 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 29 Aug 2022 15:58:51 +0100 Subject: mod_smacks: Set session.smacks after sending to fix traceback ...with opportunistic writes enabled. --- plugins/mod_smacks.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index b3f6d189..6f257ba5 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -349,8 +349,6 @@ function handle_enable(session, stanza, xmlns_sm) return true; end - session.smacks = xmlns_sm; - (session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm; id = enabled.id; @@ -358,6 +356,7 @@ function handle_enable(session, stanza, xmlns_sm) max = enabled.resume_max; })); + session.smacks = xmlns_sm; enabled.finish(); return true; -- cgit v1.2.3 From 7a36d5edcfce7c91f321783afee1cdf6aa151fa8 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 11 Jul 2022 13:49:47 +0100 Subject: mod_http_file_share: Switch to new util.jwt API Some changes/improvements in this commit: - Default token lifetime is now 3600s (from 300s) - Tokens are only validated once per upload - "iat"/"exp" are handled automatically by util.jwt --- plugins/mod_http_file_share.lua | 64 ++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 32 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_http_file_share.lua b/plugins/mod_http_file_share.lua index b6200628..7c615757 100644 --- a/plugins/mod_http_file_share.lua +++ b/plugins/mod_http_file_share.lua @@ -12,7 +12,6 @@ local jid = require "util.jid"; local st = require "util.stanza"; local url = require "socket.url"; local dm = require "core.storagemanager".olddm; -local jwt = require "util.jwt"; local errors = require "util.error"; local dataform = require "util.dataforms".new; local urlencode = require "util.http".urlencode; @@ -44,6 +43,8 @@ local expiry = module:get_option_number(module.name .. "_expires_after", 7 * 864 local daily_quota = module:get_option_number(module.name .. "_daily_quota", file_size_limit*10); -- 100 MB / day local total_storage_limit = module:get_option_number(module.name.."_global_quota", unlimited); +local create_jwt, verify_jwt = require "util.jwt".init("HS256", secret); + local access = module:get_option_set(module.name .. "_access", {}); if not external_base_url then @@ -169,16 +170,13 @@ function may_upload(uploader, filename, filesize, filetype) -- > boolean, error end function get_authz(slot, uploader, filename, filesize, filetype) -local now = os.time(); - return jwt.sign(secret, { + return create_jwt({ -- token properties sub = uploader; - iat = now; - exp = now+300; -- slot properties slot = slot; - expires = expiry >= 0 and (now+expiry) or nil; + expires = expiry >= 0 and (os.time()+expiry) or nil; -- file properties filename = filename; filesize = filesize; @@ -249,32 +247,34 @@ end function handle_upload(event, path) -- PUT /upload/:slot local request = event.request; - local authz = request.headers.authorization; - if authz then - authz = authz:match("^Bearer (.*)") - end - if not authz then - module:log("debug", "Missing or malformed Authorization header"); - event.response.headers.www_authenticate = "Bearer"; - return 401; - end - local authed, upload_info = jwt.verify(secret, authz); - if not (authed and type(upload_info) == "table" and type(upload_info.exp) == "number") then - module:log("debug", "Unauthorized or invalid token: %s, %q", authed, upload_info); - return 401; - end - if not request.body_sink and upload_info.exp < os.time() then - module:log("debug", "Authorization token expired on %s", dt.datetime(upload_info.exp)); - return 410; - end - if not path or upload_info.slot ~= path:match("^[^/]+") then - module:log("debug", "Invalid upload slot: %q, path: %q", upload_info.slot, path); - return 400; - end - if request.headers.content_length and tonumber(request.headers.content_length) ~= upload_info.filesize then - return 413; - -- Note: We don't know the size if the upload is streamed in chunked encoding, - -- so we also check the final file size on completion. + local upload_info = request.http_file_share_upload_info; + + if not upload_info then -- Initial handling of request + local authz = request.headers.authorization; + if authz then + authz = authz:match("^Bearer (.*)") + end + if not authz then + module:log("debug", "Missing or malformed Authorization header"); + event.response.headers.www_authenticate = "Bearer"; + return 401; + end + local authed, authed_upload_info = verify_jwt(authz); + if not authed then + module:log("debug", "Unauthorized or invalid token: %s, %q", authz, authed_upload_info); + return 401; + end + if not path or upload_info.slot ~= path:match("^[^/]+") then + module:log("debug", "Invalid upload slot: %q, path: %q", upload_info.slot, path); + return 400; + end + if request.headers.content_length and tonumber(request.headers.content_length) ~= upload_info.filesize then + return 413; + -- Note: We don't know the size if the upload is streamed in chunked encoding, + -- so we also check the final file size on completion. + end + upload_info = authed_upload_info; + request.http_file_share_upload_info = upload_info; end local filename = get_filename(upload_info.slot, true); -- cgit v1.2.3 From 5eef82fdcfe3163b1d23b04058dc41b7848b7976 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 3 Sep 2022 21:19:00 +0100 Subject: mod_saslauth: Only announce bind feature if no resource yet bound It's now possible to bind during SASL2 negotiation. --- plugins/mod_saslauth.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index c7228b10..2552a253 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -352,7 +352,7 @@ module:hook("stream-features", function(event) authmod, available_disabled); end - else + elseif not session.full_jid then features:tag("bind", bind_attr):tag("required"):up():up(); features:tag("session", xmpp_session_attr):tag("optional"):up():up(); end -- cgit v1.2.3 From a061b11f327222d20f4ab85e874805da261f49f8 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 3 Sep 2022 21:20:07 +0100 Subject: mod_smacks: Add type field to results so actions can be later distinguished --- plugins/mod_smacks.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 6f257ba5..7a07a9da 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -329,6 +329,7 @@ function do_enable(session, stanza) end return { + type = "enabled"; id = resume_token; resume_max = resume_token and tostring(resume_timeout) or nil; session = session; @@ -337,7 +338,6 @@ function do_enable(session, stanza) track_session(session, resume_token); wrap_session(session, false); - end; }; end @@ -613,6 +613,7 @@ function do_resume(session, stanza) sessionmanager.update_session(original_session, session); return { + type = "resumed"; session = original_session; id = id; -- Return function to complete the resumption and resync unacked stanzas -- cgit v1.2.3 From 5f2c086da7bd3db208dd79d64bdc1af38592fc16 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 3 Sep 2022 21:20:29 +0100 Subject: mod_smacks: Set session flag during successful enable --- plugins/mod_smacks.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 7a07a9da..eaba198d 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -336,6 +336,7 @@ function do_enable(session, stanza) finish = function () session.log("debug", "Enabling stream management"); + session.smacks = stanza.attr.xmlns; track_session(session, resume_token); wrap_session(session, false); end; -- cgit v1.2.3 From af0f1947cf22896d962b93ab93a83747414558d4 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 3 Sep 2022 21:25:51 +0100 Subject: mod_saslauth: Fix incorrect variable name introduced in 27a4a7e64831 --- plugins/mod_saslauth.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index 2552a253..ddef3f17 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -352,7 +352,7 @@ module:hook("stream-features", function(event) authmod, available_disabled); end - elseif not session.full_jid then + elseif not origin.full_jid then features:tag("bind", bind_attr):tag("required"):up():up(); features:tag("session", xmpp_session_attr):tag("optional"):up():up(); end -- cgit v1.2.3 From fd637bf6bede43756abd33ec84593b3091d1283d Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sun, 4 Sep 2022 10:01:57 +0100 Subject: mod_http_file_share: Use correct variable name (thanks riau.sni) --- plugins/mod_http_file_share.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_http_file_share.lua b/plugins/mod_http_file_share.lua index 7c615757..c45e7732 100644 --- a/plugins/mod_http_file_share.lua +++ b/plugins/mod_http_file_share.lua @@ -264,11 +264,11 @@ function handle_upload(event, path) -- PUT /upload/:slot module:log("debug", "Unauthorized or invalid token: %s, %q", authz, authed_upload_info); return 401; end - if not path or upload_info.slot ~= path:match("^[^/]+") then - module:log("debug", "Invalid upload slot: %q, path: %q", upload_info.slot, path); + if not path or authed_upload_info.slot ~= path:match("^[^/]+") then + module:log("debug", "Invalid upload slot: %q, path: %q", authed_upload_info.slot, path); return 400; end - if request.headers.content_length and tonumber(request.headers.content_length) ~= upload_info.filesize then + if request.headers.content_length and tonumber(request.headers.content_length) ~= authed_upload_info.filesize then return 413; -- Note: We don't know the size if the upload is streamed in chunked encoding, -- so we also check the final file size on completion. -- cgit v1.2.3 From 1dd9e547ced3f6ce998b0e722dd4e4f9e69d5b07 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 7 Sep 2022 12:27:12 +0200 Subject: mod_storage_sql: Strip timestamp precision in queries to fix error (thanks muppeth) Fixes Error in SQL transaction: Error executing statement parameters: ERROR: invalid input syntax for integer This was handled for INSERT in 9524bb7f3944 but not SELECT. --- plugins/mod_storage_sql.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua index ca8b51ac..eb335d6b 100644 --- a/plugins/mod_storage_sql.lua +++ b/plugins/mod_storage_sql.lua @@ -355,12 +355,12 @@ end local function archive_where(query, args, where) -- Time range, inclusive if query.start then - args[#args+1] = query.start + args[#args+1] = math.floor(query.start); where[#where+1] = "\"when\" >= ?" end if query["end"] then - args[#args+1] = query["end"]; + args[#args+1] = math.floor(query["end"]); if query.start then where[#where] = "\"when\" BETWEEN ? AND ?" -- is this inclusive? else -- cgit v1.2.3 From afa583dfcdcf1e3a53ccb88691985a1c691046d0 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 21 Sep 2022 15:00:06 +0100 Subject: mod_saslauth: Put in stream:features per XEP-0440 0.4.0 --- plugins/mod_saslauth.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'plugins') diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index ddef3f17..4c8858cb 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -321,15 +321,15 @@ module:hook("stream-features", function(event) for mechanism in usable_mechanisms do mechanisms:tag("mechanism"):text(mechanism):up(); end + features:add_child(mechanisms); if not channel_bindings:empty() then -- XXX XEP-0440 is Experimental - mechanisms:tag("sasl-channel-binding", {xmlns='urn:xmpp:sasl-cb:0'}) + features:tag("sasl-channel-binding", {xmlns='urn:xmpp:sasl-cb:0'}) for channel_binding in channel_bindings do - mechanisms:tag("channel-binding", {type=channel_binding}):up() + features:tag("channel-binding", {type=channel_binding}):up() end - mechanisms:up(); + features:up(); end - features:add_child(mechanisms); return; end -- cgit v1.2.3 From 8b82dc338c5e4a6a08cc945d2b65466743f4e352 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 23 Sep 2022 11:58:15 +0200 Subject: mod_admin_shell: Fix display of session without role (thanks Link Mauve) This can happen to sessions before they are assigned a role --- plugins/mod_admin_shell.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 14fab8ad..97464579 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -948,7 +948,7 @@ available_columns = { width = 20; key = "role"; mapper = function(role) - return role.name; + return role and role.name; end; } }; -- cgit v1.2.3 From 4dc941fa5370d86a0338587feb321693bcdabc9b Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 28 Sep 2022 17:47:00 +0100 Subject: muc: Re-allow non-admins to configure persistence (thanks Meaz) Non-admins don't have a role on MUC services by default. Not even prosody:user. This meant they had no :create-persistent-room permission, even if muc_room_allow_persistent was true (the default). Now we only check the role permissions if persistent room creation is restricted, otherwise we skip any permission checks, just like previous versions. --- plugins/muc/persistent.lib.lua | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'plugins') diff --git a/plugins/muc/persistent.lib.lua b/plugins/muc/persistent.lib.lua index 4c753921..fc0f16db 100644 --- a/plugins/muc/persistent.lib.lua +++ b/plugins/muc/persistent.lib.lua @@ -8,10 +8,7 @@ -- local restrict_persistent = not module:get_option_boolean("muc_room_allow_persistent", true); -module:default_permission( - restrict_persistent and "prosody:admin" or "prosody:user", - ":create-persistent-room" -); +module:default_permission("prosody:admin", ":create-persistent-room"); -- Admins can always create, by default local function get_persistent(room) return room._data.persistent; @@ -25,7 +22,7 @@ local function set_persistent(room, persistent) end module:hook("muc-config-form", function(event) - if not module:may(":create-persistent-room", event.actor) then + if restrict_persistent and not module:may(":create-persistent-room", event.actor) then -- Hide config option if this user is not allowed to create persistent rooms return; end @@ -39,7 +36,7 @@ module:hook("muc-config-form", function(event) end, 100-5); module:hook("muc-config-submitted/muc#roomconfig_persistentroom", function(event) - if not module:may(":create-persistent-room", event.actor) then + if restrict_persistent and not module:may(":create-persistent-room", event.actor) then return; -- Not allowed end if set_persistent(event.room, event.value) then -- cgit v1.2.3 From 9b8c2cd1c980d5c4a9ec1c4b170e486d16ef1a15 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 29 Sep 2022 12:10:14 +0100 Subject: mod_authz_internal: Allow configuring role of local-server/parent-host users 'host_user_role' is the default role of users who have JIDs on the "parent" host (i.e. jabber.org users on conference.jabber.org). Defaults to 'prosody:user'. 'server_user_roles' is the default role of users who have JIDs on any active host on the current Prosody instance. Default to nil (no role). This finally allows better permissions splitting between host and server users, which has previously been done (e.g. in MUC) with options like 'restrict_room_creation' and 'muc_room_allow_persistent'. Using roles makes these permissions a lot more flexible, and easier for developers to integrate. --- plugins/mod_authz_internal.lua | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index 4f88b176..f7995cdd 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -1,13 +1,22 @@ local array = require "util.array"; local it = require "util.iterators"; local set = require "util.set"; -local jid_split, jid_bare = require "util.jid".split, require "util.jid".bare; +local jid_split, jid_bare, jid_host = import("util.jid", "split", "bare", "host"); local normalize = require "util.jid".prep; local roles = require "util.roles"; local config_global_admin_jids = module:context("*"):get_option_set("admins", {}) / normalize; local config_admin_jids = module:get_option_inherited_set("admins", {}) / normalize; local host = module.host; +local host_suffix = host:gsub("^[^%.]+%.", ""); + +local hosts = prosody.hosts; +local is_component = hosts[host].type == "component"; +local host_user_role, server_user_role; +if is_component then + host_user_role = module:get_option_string("host_user_role", "prosody:user"); + server_user_role = module:get_option_string("server_user_role"); +end local role_store = module:open_store("account_roles"); local role_map_store = module:open_store("account_roles", "map"); @@ -225,6 +234,13 @@ function get_jid_role(jid) return role_registry["prosody:operator"]; elseif config_admin_jids:contains(bare_jid) then return role_registry["prosody:admin"]; + elseif is_component then + local user_host = jid_host(bare_jid); + if host_user_role and user_host == host_suffix then + return role_registry[host_user_role]; + elseif server_user_role and hosts[user_host] then + return role_registry[server_user_role]; + end end return nil; end -- cgit v1.2.3 From 856a482013e3729f9d2b6b76cc8b061d327391d5 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 29 Sep 2022 12:30:52 +0100 Subject: mod_muc: Better map restrict_room_creation to role permissions (behaviour change) With this change and 427dd01f0864, room creation is now effectively restricted to parent-host users by default. This is a better default than previous Prosody versions (where room creation was not restricted). The "local" option for restrict_room_creation is no longer used (any value other than true/false won't change the default behaviour). restrict_room_creation = true will grant prosody:admin the ability to create rooms. restrict_room_creation = false disables all permission checks. Anything between these two can be achieved using custom roles and permissions. --- plugins/muc/mod_muc.lua | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) (limited to 'plugins') diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua index 08be3586..ab042d99 100644 --- a/plugins/muc/mod_muc.lua +++ b/plugins/muc/mod_muc.lua @@ -413,28 +413,15 @@ if module:get_option_boolean("muc_tombstones", true) then end, -10); end -module:default_permission("prosody:admin", ":create-room"); - -do - local restrict_room_creation = module:get_option("restrict_room_creation"); - if restrict_room_creation == true then - restrict_room_creation = "admin"; - end - if restrict_room_creation then - local host_suffix = module.host:gsub("^[^%.]+%.", ""); - module:hook("muc-room-pre-create", function(event) - local origin, stanza = event.origin, event.stanza; - local user_jid = stanza.attr.from; - if not module:may(":create-room", event) and not ( - restrict_room_creation == "local" and - select(2, jid_split(user_jid)) == host_suffix - ) then - origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Room creation is restricted", module.host)); - return true; - end - end); +local restrict_room_creation = module:get_option("restrict_room_creation"); +module:default_permission(restrict_room_creation == true and "prosody:admin" or "prosody:user", ":create-room"); +module:hook("muc-room-pre-create", function(event) + local origin, stanza = event.origin, event.stanza; + if restrict_room_creation ~= false and not module:may(":create-room", event) then + origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Room creation is restricted", module.host)); + return true; end -end +end); for event_name, method in pairs { -- Normal room interactions -- cgit v1.2.3 From 53ccf68cdf32f6cc26417305c54246bdd60ec49b Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 29 Sep 2022 12:43:09 +0100 Subject: Backed out changeset 73a45ba6e3f1 in favour of 427dd01f0864 New behaviour (muc_room_allow_persistent = true, the default): - Parent host users are not restricted by default (prosody:user) - Users without roles (by default that is non-admins, non-parent-host users, and users on other servers) can no longer configure persistence by default. muc_room_allow_persistent = false will restrict persistence to prosody:admin. Parent-host users should not be restricted by default, and this can be configured via the new roles/permissions options. --- plugins/muc/persistent.lib.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'plugins') diff --git a/plugins/muc/persistent.lib.lua b/plugins/muc/persistent.lib.lua index fc0f16db..4c753921 100644 --- a/plugins/muc/persistent.lib.lua +++ b/plugins/muc/persistent.lib.lua @@ -8,7 +8,10 @@ -- local restrict_persistent = not module:get_option_boolean("muc_room_allow_persistent", true); -module:default_permission("prosody:admin", ":create-persistent-room"); -- Admins can always create, by default +module:default_permission( + restrict_persistent and "prosody:admin" or "prosody:user", + ":create-persistent-room" +); local function get_persistent(room) return room._data.persistent; @@ -22,7 +25,7 @@ local function set_persistent(room, persistent) end module:hook("muc-config-form", function(event) - if restrict_persistent and not module:may(":create-persistent-room", event.actor) then + if not module:may(":create-persistent-room", event.actor) then -- Hide config option if this user is not allowed to create persistent rooms return; end @@ -36,7 +39,7 @@ module:hook("muc-config-form", function(event) end, 100-5); module:hook("muc-config-submitted/muc#roomconfig_persistentroom", function(event) - if restrict_persistent and not module:may(":create-persistent-room", event.actor) then + if not module:may(":create-persistent-room", event.actor) then return; -- Not allowed end if set_persistent(event.room, event.value) then -- cgit v1.2.3 From 5ed7f1638c35b0a77f6a6a015326846a8aa0fac2 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 29 Sep 2022 12:46:02 +0100 Subject: mod_authz_internal: Allow specifying default role for public (remote) users --- plugins/mod_authz_internal.lua | 3 +++ 1 file changed, 3 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index f7995cdd..66aa02c1 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -16,6 +16,7 @@ local host_user_role, server_user_role; if is_component then host_user_role = module:get_option_string("host_user_role", "prosody:user"); server_user_role = module:get_option_string("server_user_role"); + public_user_role = module:get_option_string("public_user_role"); end local role_store = module:open_store("account_roles"); @@ -240,6 +241,8 @@ function get_jid_role(jid) return role_registry[host_user_role]; elseif server_user_role and hosts[user_host] then return role_registry[server_user_role]; + elseif public_user_role then + return role_registry[public_user_role]; end end return nil; -- cgit v1.2.3 From 865b6da69d39c716c9fb073d4ac79d662c901f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Mon, 3 Oct 2022 12:55:11 +0200 Subject: Backed out changeset 1bc2220cd6ec The use of the error helpers creates an `` child element containing the error condition. This is however not allowed as per XEP-0198, which specifies that the error condition is to be a direct child of the `` stream management element. This has triggered a fun reconnect loop in aioxmpp where it was reported by a user [1]. [1]: https://github.com/horazont/aioxmpp/issues/382 --- plugins/mod_smacks.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index eaba198d..08ebcfec 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -57,6 +57,7 @@ local it = require"util.iterators"; local sessionmanager = require "core.sessionmanager"; +local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas"; local xmlns_delay = "urn:xmpp:delay"; local xmlns_mam2 = "urn:xmpp:mam:2"; local xmlns_sm2 = "urn:xmpp:sm:2"; @@ -647,7 +648,7 @@ function handle_resume(session, stanza, xmlns_sm) local resumed, err = do_resume(session, stanza); if not resumed then session.send(st.stanza("failed", { xmlns = xmlns_sm, h = format_h(err.context.h) }) - :add_error(err)); + :tag(err.condition, { xmlns = xmlns_errors })); return true; end -- cgit v1.2.3 From 988e651630d980249c8ae6ee698527094fc4d4b4 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 4 Oct 2022 12:04:43 +0200 Subject: mod_authz_internal: Fix warning due to global use Thanks Menel and Martin --- plugins/mod_authz_internal.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index 66aa02c1..c2895613 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -12,7 +12,7 @@ local host_suffix = host:gsub("^[^%.]+%.", ""); local hosts = prosody.hosts; local is_component = hosts[host].type == "component"; -local host_user_role, server_user_role; +local host_user_role, server_user_role, public_user_role; if is_component then host_user_role = module:get_option_string("host_user_role", "prosody:user"); server_user_role = module:get_option_string("server_user_role"); -- cgit v1.2.3 From 36afd999b1042b997ae59c81dce755fb01ee2b11 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 6 Oct 2022 15:59:07 +0100 Subject: mod_tokenauth: Invalidate tokens issued before most recent password change This is a security improvement, to ensure that sessions authenticated using a token (note: not currently possible in stock Prosody) are invalidated just like password-authenticated sessions are. --- plugins/mod_tokenauth.lua | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_tokenauth.lua b/plugins/mod_tokenauth.lua index 85602747..450e2f0a 100644 --- a/plugins/mod_tokenauth.lua +++ b/plugins/mod_tokenauth.lua @@ -68,6 +68,12 @@ local function _get_parsed_token_info(token_id, token_user, token_host) return nil, "not-authorized"; end + local account_info = usermanager.get_account_info(token_user, module.host); + local password_updated_at = account_info and account_info.password_updated; + if password_updated_at and password_updated_at > token_info.created then + return nil, "not-authorized"; + end + return token_info end -- cgit v1.2.3 From 12fc0febf172dd25d403af6365f3e5042b06d6dd Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 6 Oct 2022 16:00:39 +0100 Subject: mod_tokenauth: Remove expired tokens from storage --- plugins/mod_tokenauth.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_tokenauth.lua b/plugins/mod_tokenauth.lua index 450e2f0a..22d2199a 100644 --- a/plugins/mod_tokenauth.lua +++ b/plugins/mod_tokenauth.lua @@ -65,12 +65,14 @@ local function _get_parsed_token_info(token_id, token_user, token_host) end if token_info.expires and token_info.expires < os.time() then + token_store:set(token_user, token_id, nil); return nil, "not-authorized"; end local account_info = usermanager.get_account_info(token_user, module.host); local password_updated_at = account_info and account_info.password_updated; if password_updated_at and password_updated_at > token_info.created then + token_store:set(token_user, token_id, nil); return nil, "not-authorized"; end -- cgit v1.2.3 From 01e03f483760c6f0ff7c6f15fe9cd6b6a54ee97b Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 7 Oct 2022 11:35:56 +0100 Subject: mod_smacks: Change boolean attribute from '1' to 'true' for compatibility Conversations 2.10.10 and earlier expect this to be literally 'true' and don't recognise '1'. This leads to it not attempting resumption with Prosody at all since this change was introduced in 36ba170c4fd0. Thanks to Zash for noticing, debugging and diagnosing this issue. This issue is fixed in Conversations commit 052c58f3 (unreleased at the time of writing). --- plugins/mod_smacks.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 08ebcfec..ee537b0d 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -354,7 +354,7 @@ function handle_enable(session, stanza, xmlns_sm) (session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm; id = enabled.id; - resume = enabled.id and "1" or nil; + resume = enabled.id and "true" or nil; -- COMPAT w/ Conversations 2.10.10 requires 'true' not '1' max = enabled.resume_max; })); -- cgit v1.2.3 From 32c38bc24958581e0c21e4143fbd94e02dde2c22 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 13 Oct 2022 22:46:19 +0100 Subject: mod_tokenauth: Allow attaching an arbitrary data table to a token --- plugins/mod_tokenauth.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_tokenauth.lua b/plugins/mod_tokenauth.lua index 22d2199a..9cd73570 100644 --- a/plugins/mod_tokenauth.lua +++ b/plugins/mod_tokenauth.lua @@ -13,7 +13,7 @@ local function select_role(username, host, role) return usermanager.get_user_role(username, host); end -function create_jid_token(actor_jid, token_jid, token_role, token_ttl) +function create_jid_token(actor_jid, token_jid, token_role, token_ttl, token_data) token_jid = jid.prep(token_jid); if not actor_jid or token_jid ~= actor_jid and not jid.compare(token_jid, actor_jid) then return nil, "not-authorized"; @@ -33,6 +33,7 @@ function create_jid_token(actor_jid, token_jid, token_role, token_ttl) resource = token_resource; role = token_role; + data = token_data; }; local token_id = id.long(); -- cgit v1.2.3 From fdf61a38cfa273c4781dbe578647638106cb7b52 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 14 Oct 2022 11:21:16 +0100 Subject: mod_c2s: Include stream attributes in stream-features event We need this to access 'from' in SASL2/FAST. --- plugins/mod_c2s.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua index 2d4186d0..c5a9b42a 100644 --- a/plugins/mod_c2s.lua +++ b/plugins/mod_c2s.lua @@ -128,7 +128,7 @@ function stream_callbacks._streamopened(session, attr) end local features = st.stanza("stream:features"); - hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); + hosts[session.host].events.fire_event("stream-features", { origin = session, features = features, stream = attr }); if features.tags[1] or session.full_jid then send(features); else -- cgit v1.2.3 From 2269035c0dd4831975fa40ffeb0848a1ada478b5 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 17 Oct 2022 15:20:06 +0200 Subject: mod_blocklist: Add option 'migrate_legacy_blocking' to disable migration from mod_privacy Tiny performance improvement for new users by skipping this check. Most servers should have gone trough the migration for all active users long ago. As a suitable first step of phasing out this code, we make it possible to disable it first. Later it can be disabled by default, before finally the code is deleted. --- plugins/mod_blocklist.lua | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'plugins') diff --git a/plugins/mod_blocklist.lua b/plugins/mod_blocklist.lua index dad06b62..13e98e00 100644 --- a/plugins/mod_blocklist.lua +++ b/plugins/mod_blocklist.lua @@ -54,6 +54,7 @@ local function set_blocklist(username, blocklist) end -- Migrates from the old mod_privacy storage +-- TODO mod_privacy was removed in 0.10.0, this should be phased out local function migrate_privacy_list(username) local legacy_data = module:open_store("privacy"):get(username); if not legacy_data or not legacy_data.lists or not legacy_data.default then return; end @@ -77,6 +78,13 @@ local function migrate_privacy_list(username) return migrated_data; end +if not module:get_option_boolean("migrate_legacy_blocking", true) then + migrate_privacy_list = function (username) + module:log("debug", "Migrating from mod_privacy disabled, user '%s' will start with a fresh blocklist", username); + return nil; + end +end + local function get_blocklist(username) local blocklist = cache2:get(username); if not blocklist then -- cgit v1.2.3 From c916ce76ee89dca32e7e653dff1ade4732462efc Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 18 Nov 2022 16:18:47 +0100 Subject: mod_smacks: Only track resumable sessions Required due to track_session() having moved here --- plugins/mod_smacks.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'plugins') diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index ee537b0d..4657fc53 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -338,7 +338,9 @@ function do_enable(session, stanza) session.log("debug", "Enabling stream management"); session.smacks = stanza.attr.xmlns; - track_session(session, resume_token); + if resume_token then + track_session(session, resume_token); + end wrap_session(session, false); end; }; -- cgit v1.2.3