From 9f34801aba7249016e5471ecdeb1b4823ad9d38e Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 17 Mar 2022 10:19:35 +0000 Subject: util.logger: Support for removing individual log sinks without a full reset --- util/logger.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/util/logger.lua b/util/logger.lua index 20a5cef2..a94a1ee1 100644 --- a/util/logger.lua +++ b/util/logger.lua @@ -10,6 +10,7 @@ local pairs = pairs; local ipairs = ipairs; local require = require; +local t_remove = table.remove; local _ENV = nil; -- luacheck: std none @@ -80,6 +81,19 @@ local function add_simple_sink(simple_sink_function, levels) end end +local function remove_sink(sink_function) + local removed; + for level, sinks in pairs(level_sinks) do + for i = #sinks, 1, -1 do + if sinks[i] == sink_function then + t_remove(sinks, i); + removed = true; + end + end + end + return removed; +end + return { init = init; make_logger = make_logger; @@ -87,4 +101,5 @@ return { add_level_sink = add_level_sink; add_simple_sink = add_simple_sink; new = make_logger; + remove_sink = remove_sink; }; -- cgit v1.2.3 From a946bdf4ae58e6aa2dea7915a5da67099a6e5c63 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 17 Mar 2022 10:20:23 +0000 Subject: util.logger: Return sink_function from add_simple_sink() This allows a simple sink to be later removed via remove_sink() --- util/logger.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/util/logger.lua b/util/logger.lua index a94a1ee1..148b98dc 100644 --- a/util/logger.lua +++ b/util/logger.lua @@ -79,6 +79,7 @@ local function add_simple_sink(simple_sink_function, levels) for _, level in ipairs(levels or {"debug", "info", "warn", "error"}) do add_level_sink(level, sink_function); end + return sink_function; end local function remove_sink(sink_function) -- cgit v1.2.3 From b617c24af4e1cc79fd27ed3dc852d6b9e53be543 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 17 Mar 2022 10:21:43 +0000 Subject: util.prosodyctl.shell: Support for receiving partial lines (no automatic \n) --- util/prosodyctl/shell.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/util/prosodyctl/shell.lua b/util/prosodyctl/shell.lua index bce27b94..0b1dd3f9 100644 --- a/util/prosodyctl/shell.lua +++ b/util/prosodyctl/shell.lua @@ -89,11 +89,15 @@ local function start(arg) --luacheck: ignore 212/arg local errors = 0; -- TODO This is weird, but works for now. client.events.add_handler("received", function(stanza) if stanza.name == "repl-output" or stanza.name == "repl-result" then + local dest = io.stdout; if stanza.attr.type == "error" then errors = errors + 1; - io.stderr:write(stanza:get_text(), "\n"); + dest = io.stderr; + end + if stanza.attr.eol == "0" then + dest:write(stanza:get_text()); else - print(stanza:get_text()); + dest:write(stanza:get_text(), "\n"); end end if stanza.name == "repl-result" then -- cgit v1.2.3 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(-) 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(+) 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(+) 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 d1c2e34e610477b901998f7230631f6c99166f9b Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 17 Mar 2022 13:15:50 +0100 Subject: util.poll: Expand stub tests Because tests good. --- spec/util_poll_spec.lua | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/spec/util_poll_spec.lua b/spec/util_poll_spec.lua index a763be90..05318453 100644 --- a/spec/util_poll_spec.lua +++ b/spec/util_poll_spec.lua @@ -1,6 +1,35 @@ -describe("util.poll", function () - it("loads", function () - require "util.poll" +describe("util.poll", function() + local poll; + setup(function() + poll = require "util.poll"; end); + it("loads", function() + assert.is_table(poll); + assert.is_function(poll.new); + assert.is_string(poll.api); + end); + describe("new", function() + local p; + setup(function() + p = poll.new(); + end) + it("times out", function () + local fd, err = p:wait(0); + assert.falsy(fd); + assert.equal("timeout", err); + end); + it("works", function() + -- stdout should be writable, right? + assert.truthy(p:add(1, false, true)); + local fd, r, w = p:wait(1); + assert.is_number(fd); + assert.is_boolean(r); + assert.is_boolean(w); + assert.equal(1, fd); + assert.falsy(r); + assert.truthy(w); + assert.truthy(p:del(1)); + end); + end) end); -- cgit v1.2.3 From d26811f5e570192b2dd5fb66d3d789b66362508c Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 17 Mar 2022 18:20:26 +0000 Subject: net.resolvers.service: Honour record 'weight' when picking SRV targets #NotHappyEyeballs --- net/resolvers/service.lua | 81 ++++++++++-- spec/net_resolvers_service_spec.lua | 241 ++++++++++++++++++++++++++++++++++++ 2 files changed, 309 insertions(+), 13 deletions(-) create mode 100644 spec/net_resolvers_service_spec.lua diff --git a/net/resolvers/service.lua b/net/resolvers/service.lua index 3810cac8..a7ce76a3 100644 --- a/net/resolvers/service.lua +++ b/net/resolvers/service.lua @@ -2,23 +2,78 @@ local adns = require "net.adns"; local basic = require "net.resolvers.basic"; local inet_pton = require "util.net".pton; local idna_to_ascii = require "util.encodings".idna.to_ascii; -local unpack = table.unpack or unpack; -- luacheck: ignore 113 local methods = {}; local resolver_mt = { __index = methods }; +local function new_target_selector(rrset) + local rr_count = rrset and #rrset; + if not rr_count or rr_count == 0 then + rrset = nil; + else + table.sort(rrset, function (a, b) return a.srv.priority < b.srv.priority end); + end + local rrset_pos = 1; + local priority_bucket, bucket_total_weight, bucket_len, bucket_used; + return function () + if not rrset then return; end + + if not priority_bucket or bucket_used >= bucket_len then + if rrset_pos > rr_count then return; end -- Used up all records + + -- Going to start on a new priority now. Gather up all the next + -- records with the same priority and add them to priority_bucket + priority_bucket, bucket_total_weight, bucket_len, bucket_used = {}, 0, 0, 0; + local current_priority; + repeat + local curr_record = rrset[rrset_pos].srv; + if not current_priority then + current_priority = curr_record.priority; + elseif current_priority ~= curr_record.priority then + break; + end + table.insert(priority_bucket, curr_record); + bucket_total_weight = bucket_total_weight + curr_record.weight; + bucket_len = bucket_len + 1; + rrset_pos = rrset_pos + 1; + until rrset_pos > rr_count; + end + + bucket_used = bucket_used + 1; + local n, running_total = math.random(0, bucket_total_weight), 0; + local target_record; + for i = 1, bucket_len do + local candidate = priority_bucket[i]; + if candidate then + running_total = running_total + candidate.weight; + if running_total >= n then + target_record = candidate; + bucket_total_weight = bucket_total_weight - candidate.weight; + priority_bucket[i] = nil; + break; + end + end + end + return target_record; + end; +end + -- Find the next target to connect to, and -- pass it to cb() function methods:next(cb) - if self.targets then - if not self.resolver then - if #self.targets == 0 then + if self.resolver or self._get_next_target then + if not self.resolver then -- Do we have a basic resolver currently? + -- We don't, so fetch a new SRV target, create a new basic resolver for it + local next_srv_target = self._get_next_target and self._get_next_target(); + if not next_srv_target then + -- No more SRV targets left cb(nil); return; end - local next_target = table.remove(self.targets, 1); - self.resolver = basic.new(unpack(next_target, 1, 4)); + -- Create a new basic resolver for this SRV target + self.resolver = basic.new(next_srv_target.target, next_srv_target.port, self.conn_type, self.extra); end + -- Look up the next (basic) target from the current target's resolver self.resolver:next(function (...) if self.resolver then self.last_error = self.resolver.last_error; @@ -31,6 +86,9 @@ function methods:next(cb) end end); return; + elseif self.in_progress then + cb(nil); + return; end if not self.hostname then @@ -39,9 +97,9 @@ function methods:next(cb) return; end - local targets = {}; + self.in_progress = true; + local function ready() - self.targets = targets; self:next(cb); end @@ -63,7 +121,7 @@ function methods:next(cb) if #answer == 0 then if self.extra and self.extra.default_port then - table.insert(targets, { self.hostname, self.extra.default_port, self.conn_type, self.extra }); + self.resolver = basic.new(self.hostname, self.extra.default_port, self.conn_type, self.extra); else self.last_error = "zero SRV records found"; end @@ -77,10 +135,7 @@ function methods:next(cb) return; end - table.sort(answer, function (a, b) return a.srv.priority < b.srv.priority end); - for _, record in ipairs(answer) do - table.insert(targets, { record.srv.target, record.srv.port, self.conn_type, self.extra }); - end + self._get_next_target = new_target_selector(answer); else self.last_error = err; end diff --git a/spec/net_resolvers_service_spec.lua b/spec/net_resolvers_service_spec.lua new file mode 100644 index 00000000..53ce4754 --- /dev/null +++ b/spec/net_resolvers_service_spec.lua @@ -0,0 +1,241 @@ +local set = require "util.set"; + +insulate("net.resolvers.service", function () + local adns = { + resolver = function () + return { + lookup = function (_, cb, qname, qtype, qclass) + if qname == "_xmpp-server._tcp.example.com" + and (qtype or "SRV") == "SRV" + and (qclass or "IN") == "IN" then + cb({ + { -- 60+35+60 + srv = { target = "xmpp0-a.example.com", port = 5228, priority = 0, weight = 60 }; + }; + { + srv = { target = "xmpp0-b.example.com", port = 5216, priority = 0, weight = 35 }; + }; + { + srv = { target = "xmpp0-c.example.com", port = 5200, priority = 0, weight = 0 }; + }; + { + srv = { target = "xmpp0-d.example.com", port = 5256, priority = 0, weight = 120 }; + }; + + { + srv = { target = "xmpp1-a.example.com", port = 5273, priority = 1, weight = 30 }; + }; + { + srv = { target = "xmpp1-b.example.com", port = 5274, priority = 1, weight = 30 }; + }; + + { + srv = { target = "xmpp2.example.com", port = 5275, priority = 2, weight = 0 }; + }; + }); + elseif qname == "_xmpp-server._tcp.single.example.com" + and (qtype or "SRV") == "SRV" + and (qclass or "IN") == "IN" then + cb({ + { + srv = { target = "xmpp0-a.example.com", port = 5269, priority = 0, weight = 0 }; + }; + }); + elseif qname == "_xmpp-server._tcp.half.example.com" + and (qtype or "SRV") == "SRV" + and (qclass or "IN") == "IN" then + cb({ + { + srv = { target = "xmpp0-a.example.com", port = 5269, priority = 0, weight = 0 }; + }; + { + srv = { target = "xmpp0-b.example.com", port = 5270, priority = 0, weight = 1 }; + }; + }); + elseif qtype == "A" then + local l = qname:match("%-(%a)%.example.com$") or "1"; + local d = ("%d"):format(l:byte()) + cb({ + { + a = "127.0.0."..d; + }; + }); + elseif qtype == "AAAA" then + local l = qname:match("%-(%a)%.example.com$") or "1"; + local d = ("%04d"):format(l:byte()) + cb({ + { + aaaa = "fdeb:9619:649e:c7d9::"..d; + }; + }); + else + cb(nil); + end + end; + }; + end; + }; + package.loaded["net.adns"] = mock(adns); + local resolver = require "net.resolvers.service"; + math.randomseed(os.time()); + it("works for 99% of deployments", function () + -- Most deployments only have a single SRV record, let's make + -- sure that works okay + + local expected_targets = set.new({ + -- xmpp0-a + "tcp4 127.0.0.97 5269"; + "tcp6 fdeb:9619:649e:c7d9::0097 5269"; + }); + local received_targets = set.new({}); + + local r = resolver.new("single.example.com", "xmpp-server"); + local done = false; + local function handle_target(...) + if ... == nil then + done = true; + -- No more targets + return; + end + received_targets:add(table.concat({ ... }, " ", 1, 3)); + end + r:next(handle_target); + while not done do + r:next(handle_target); + end + + -- We should have received all expected targets, and no unexpected + -- ones: + assert.truthy(set.xor(received_targets, expected_targets):empty()); + end); + + it("supports A/AAAA fallback", function () + -- Many deployments don't have any SRV records, so we should + -- fall back to A/AAAA records instead when that is the case + + local expected_targets = set.new({ + -- xmpp0-a + "tcp4 127.0.0.97 5269"; + "tcp6 fdeb:9619:649e:c7d9::0097 5269"; + }); + local received_targets = set.new({}); + + local r = resolver.new("xmpp0-a.example.com", "xmpp-server", "tcp", { default_port = 5269 }); + local done = false; + local function handle_target(...) + if ... == nil then + done = true; + -- No more targets + return; + end + received_targets:add(table.concat({ ... }, " ", 1, 3)); + end + r:next(handle_target); + while not done do + r:next(handle_target); + end + + -- We should have received all expected targets, and no unexpected + -- ones: + assert.truthy(set.xor(received_targets, expected_targets):empty()); + end); + + + it("works", function () + local expected_targets = set.new({ + -- xmpp0-a + "tcp4 127.0.0.97 5228"; + "tcp6 fdeb:9619:649e:c7d9::0097 5228"; + "tcp4 127.0.0.97 5273"; + "tcp6 fdeb:9619:649e:c7d9::0097 5273"; + + -- xmpp0-b + "tcp4 127.0.0.98 5274"; + "tcp6 fdeb:9619:649e:c7d9::0098 5274"; + "tcp4 127.0.0.98 5216"; + "tcp6 fdeb:9619:649e:c7d9::0098 5216"; + + -- xmpp0-c + "tcp4 127.0.0.99 5200"; + "tcp6 fdeb:9619:649e:c7d9::0099 5200"; + + -- xmpp0-d + "tcp4 127.0.0.100 5256"; + "tcp6 fdeb:9619:649e:c7d9::0100 5256"; + + -- xmpp2 + "tcp4 127.0.0.49 5275"; + "tcp6 fdeb:9619:649e:c7d9::0049 5275"; + + }); + local received_targets = set.new({}); + + local r = resolver.new("example.com", "xmpp-server"); + local done = false; + local function handle_target(...) + if ... == nil then + done = true; + -- No more targets + return; + end + received_targets:add(table.concat({ ... }, " ", 1, 3)); + end + r:next(handle_target); + while not done do + r:next(handle_target); + end + + -- We should have received all expected targets, and no unexpected + -- ones: + assert.truthy(set.xor(received_targets, expected_targets):empty()); + end); + + it("balances across weights correctly #slow", function () + -- This mimics many repeated connections to 'example.com' (mock + -- records defined above), and records the port number of the + -- first target. Therefore it (should) only return priority + -- 0 records, and the input data is constructed such that the + -- last two digits of the port number represent the percentage + -- of times that record should (on average) be picked first. + + -- To prevent random test failures, we test across a handful + -- of fixed (randomly selected) seeds. + for _, seed in ipairs({ 8401877, 3943829, 7830992 }) do + math.randomseed(seed); + + local results = {}; + local function run() + local run_results = {}; + local r = resolver.new("example.com", "xmpp-server"); + local function record_target(...) + if ... == nil then + -- No more targets + return; + end + run_results = { ... }; + end + r:next(record_target); + return run_results[3]; + end + + for _ = 1, 1000 do + local port = run(); + results[port] = (results[port] or 0) + 1; + end + + local ports = {}; + for port in pairs(results) do + table.insert(ports, port); + end + table.sort(ports); + for _, port in ipairs(ports) do + --print("PORT", port, tostring((results[port]/1000) * 100).."% hits (expected "..tostring(port-5200).."%)"); + local hit_pct = (results[port]/1000) * 100; + local expected_pct = port - 5200; + --print(hit_pct, expected_pct, math.abs(hit_pct - expected_pct)); + assert.is_true(math.abs(hit_pct - expected_pct) < 5); + end + --print("---"); + end + end); +end); -- cgit v1.2.3 From 26605b51971d2f899ce55214a86d799a310fd010 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 18 Mar 2022 15:21:25 +0000 Subject: util.table: Backport table.move() from Lua 5.4 One difference is that 5.4 accepts "table-like" values (for this and other table.*() functions), but that would require additional backporting work. --- util-src/table.c | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/util-src/table.c b/util-src/table.c index 9a9553fc..0756a256 100644 --- a/util-src/table.c +++ b/util-src/table.c @@ -1,11 +1,17 @@ #include #include +#ifndef LUA_MAXINTEGER +#include +#define LUA_MAXINTEGER PTRDIFF_MAX +#endif + static int Lcreate_table(lua_State *L) { lua_createtable(L, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2)); return 1; } +/* COMPAT: w/ Lua pre-5.4 */ static int Lpack(lua_State *L) { unsigned int n_args = lua_gettop(L); lua_createtable(L, n_args, 1); @@ -20,6 +26,40 @@ static int Lpack(lua_State *L) { return 1; } +/* COMPAT: w/ Lua pre-5.4 */ +static int Lmove (lua_State *L) { + lua_Integer f = luaL_checkinteger(L, 2); + lua_Integer e = luaL_checkinteger(L, 3); + lua_Integer t = luaL_checkinteger(L, 4); + + int tt = !lua_isnoneornil(L, 5) ? 5 : 1; /* destination table */ + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checktype(L, tt, LUA_TTABLE); + + if (e >= f) { /* otherwise, nothing to move */ + lua_Integer n, i; + luaL_argcheck(L, f > 0 || e < LUA_MAXINTEGER + f, 3, + "too many elements to move"); + n = e - f + 1; /* number of elements to move */ + luaL_argcheck(L, t <= LUA_MAXINTEGER - n + 1, 4, + "destination wrap around"); + if (t > e || t <= f || (tt != 1 && !lua_compare(L, 1, tt, LUA_OPEQ))) { + for (i = 0; i < n; i++) { + lua_rawgeti(L, 1, f + i); + lua_rawseti(L, tt, t + i); + } + } else { + for (i = n - 1; i >= 0; i--) { + lua_rawgeti(L, 1, f + i); + lua_rawseti(L, tt, t + i); + } + } + } + + lua_pushvalue(L, tt); /* return destination table */ + return 1; +} + int luaopen_util_table(lua_State *L) { #if (LUA_VERSION_NUM > 501) luaL_checkversion(L); @@ -29,5 +69,7 @@ int luaopen_util_table(lua_State *L) { lua_setfield(L, -2, "create"); lua_pushcfunction(L, Lpack); lua_setfield(L, -2, "pack"); + lua_pushcfunction(L, Lmove); + lua_setfield(L, -2, "move"); return 1; } -- cgit v1.2.3 From 22cc92f4376b5a641cdbb337d34b40312972b1e8 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 18 Mar 2022 15:22:00 +0000 Subject: util.array: Take advantage of table.move() --- util/array.lua | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/util/array.lua b/util/array.lua index c33a5ef1..9d438940 100644 --- a/util/array.lua +++ b/util/array.lua @@ -8,6 +8,7 @@ local t_insert, t_sort, t_remove, t_concat = table.insert, table.sort, table.remove, table.concat; +local t_move = require "util.table".move; local setmetatable = setmetatable; local getmetatable = getmetatable; @@ -137,13 +138,11 @@ function array_base.slice(outa, ina, i, j) return outa; end - for idx = 1, 1+j-i do - outa[idx] = ina[i+(idx-1)]; - end + + t_move(ina, i, j, 1, outa); if ina == outa then - for idx = 2+j-i, #outa do - outa[idx] = nil; - end + -- Clear (nil) remainder of range + t_move(ina, #outa+1, #outa*2, 2+j-i, ina); end return outa; end @@ -209,10 +208,7 @@ function array_methods:shuffle() end function array_methods:append(ina) - local len, len2 = #self, #ina; - for i = 1, len2 do - self[len+i] = ina[i]; - end + t_move(ina, 1, #ina, #self+1, self); return self; end -- cgit v1.2.3 From 9a1b7e5627a5b5e85ed5c50c18932a9808fd676a Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 18 Mar 2022 15:24:12 +0000 Subject: util.table: tests: Tests for table.move() --- spec/util_table_spec.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spec/util_table_spec.lua b/spec/util_table_spec.lua index 76f54b69..a0535c08 100644 --- a/spec/util_table_spec.lua +++ b/spec/util_table_spec.lua @@ -12,6 +12,17 @@ describe("util.table", function () assert.same({ "lorem", "ipsum", "dolor", "sit", "amet", n = 5 }, u_table.pack("lorem", "ipsum", "dolor", "sit", "amet")); end); end); + + describe("move()", function () + it("works", function () + local t1 = { "apple", "banana", "carrot" }; + local t2 = { "cat", "donkey", "elephant" }; + local t3 = {}; + u_table.move(t1, 1, 3, 1, t3); + u_table.move(t2, 1, 3, 3, t3); + assert.same({ "apple", "banana", "cat", "donkey", "elephant" }, t3); + end); + end); end); -- cgit v1.2.3 From ca3d1e1958ed89da5147eee42096baae35c744da Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 18 Mar 2022 15:29:05 +0000 Subject: util.table: Compatibility with Lua 5.1 lua_equals --- util-src/table.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/util-src/table.c b/util-src/table.c index 0756a256..4bbceedb 100644 --- a/util-src/table.c +++ b/util-src/table.c @@ -6,6 +6,10 @@ #define LUA_MAXINTEGER PTRDIFF_MAX #endif +#if (LUA_VERSION_NUM > 501) +#define lua_equal(L, A, B) lua_compare(L, A, B, LUA_OPEQ) +#endif + static int Lcreate_table(lua_State *L) { lua_createtable(L, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2)); return 1; @@ -43,7 +47,7 @@ static int Lmove (lua_State *L) { n = e - f + 1; /* number of elements to move */ luaL_argcheck(L, t <= LUA_MAXINTEGER - n + 1, 4, "destination wrap around"); - if (t > e || t <= f || (tt != 1 && !lua_compare(L, 1, tt, LUA_OPEQ))) { + if (t > e || t <= f || (tt != 1 && !lua_equal(L, 1, tt))) { for (i = 0; i < n; i++) { lua_rawgeti(L, 1, f + i); lua_rawseti(L, tt, t + i); -- cgit v1.2.3 From ceea9b1788ea9f06314c6aeb493c3b63c7ca6c5e Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 18 Mar 2022 16:39:48 +0100 Subject: util.stanza: Use table.move in clone Code reduction, potentially a performance gain. --- util/stanza.lua | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/util/stanza.lua b/util/stanza.lua index a38f80b3..9e249059 100644 --- a/util/stanza.lua +++ b/util/stanza.lua @@ -21,6 +21,7 @@ local type = type; local s_gsub = string.gsub; local s_sub = string.sub; local s_find = string.find; +local t_move = table.move or require "util.table".move; local valid_utf8 = require "util.encodings".utf8.valid; @@ -283,17 +284,13 @@ local function _clone(stanza, only_top) for k,v in pairs(old_namespaces) do namespaces[k] = v; end end local new = { name = stanza.name, attr = attr, namespaces = namespaces, tags = tags }; + setmetatable(new, stanza_mt); if not only_top then - for i=1,#stanza do - local child = stanza[i]; - if child.name then - child = _clone(child); - t_insert(tags, child); - end - t_insert(new, child); - end + t_move(stanza, 1, #stanza, 1, new); + t_move(stanza.tags, 1, #stanza.tags, 1, tags); + new:maptags(_clone); end - return setmetatable(new, stanza_mt); + return new; end local function clone(stanza, only_top) -- cgit v1.2.3 From 47dbb928887f1ece83ca7005eb714a4e937113e5 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 18 Mar 2022 16:43:06 +0100 Subject: util.stanza: Create tables with correct size to avoid reallocations Potential performance gain since the tables don't need to be resized as they grow to the final size. --- util/stanza.lua | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/util/stanza.lua b/util/stanza.lua index 9e249059..a14be5f0 100644 --- a/util/stanza.lua +++ b/util/stanza.lua @@ -22,6 +22,7 @@ local s_gsub = string.gsub; local s_sub = string.sub; local s_find = string.find; local t_move = table.move or require "util.table".move; +local t_create = require"util.table".create; local valid_utf8 = require "util.encodings".utf8.valid; @@ -276,14 +277,26 @@ function stanza_mt:find(path) end local function _clone(stanza, only_top) - local attr, tags = {}, {}; + local attr = {}; for k,v in pairs(stanza.attr) do attr[k] = v; end local old_namespaces, namespaces = stanza.namespaces; if old_namespaces then namespaces = {}; for k,v in pairs(old_namespaces) do namespaces[k] = v; end end - local new = { name = stanza.name, attr = attr, namespaces = namespaces, tags = tags }; + local tags, new; + if only_top then + tags = {}; + new = { name = stanza.name, attr = attr, namespaces = namespaces, tags = tags }; + else + tags = t_create(#stanza.tags, 0); + new = t_create(#stanza, 4); + new.name = stanza.name; + new.attr = attr; + new.namespaces = namespaces; + new.tags = tags; + end + setmetatable(new, stanza_mt); if not only_top then t_move(stanza, 1, #stanza, 1, new); -- cgit v1.2.3 From 668bd38c71bacc78bbd44154f3305be6450e0729 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 18 Mar 2022 16:09:22 +0000 Subject: net.resolvers.basic: Refactor to remove code duplication ...and prepare for Happy Eyeballs --- net/resolvers/basic.lua | 152 +++++++++++++++++++++++------------------------- 1 file changed, 72 insertions(+), 80 deletions(-) diff --git a/net/resolvers/basic.lua b/net/resolvers/basic.lua index 305bce76..a5ea5dee 100644 --- a/net/resolvers/basic.lua +++ b/net/resolvers/basic.lua @@ -2,13 +2,51 @@ local adns = require "net.adns"; local inet_pton = require "util.net".pton; local inet_ntop = require "util.net".ntop; local idna_to_ascii = require "util.encodings".idna.to_ascii; -local unpack = table.unpack or unpack; -- luacheck: ignore 113 +local promise = require "util.promise"; +local t_move = require "util.table".move; local methods = {}; local resolver_mt = { __index = methods }; -- FIXME RFC 6724 +local function do_dns_lookup(self, dns_resolver, record_type, name) + return promise.new(function (resolve, reject) + local ipv = (record_type == "A" and "4") or (record_type == "AAAA" and "6") or nil; + if ipv and self.extra["use_ipv"..ipv] == false then + return reject(("IPv%s disabled - %s lookup skipped"):format(ipv, record_type)); + elseif record_type == "TLSA" and self.extra.use_dane ~= true then + return reject("DANE disabled - TLSA lookup skipped"); + end + dns_resolver:lookup(function (answer, err) + if not answer then + return reject(err); + elseif answer.bogus then + return reject(("Validation error in %s lookup"):format(record_type)); + elseif answer.status and #answer == 0 then + return reject(("%s in %s lookup"):format(answer.status, record_type)); + end + + local targets = { secure = answer.secure }; + for _, record in ipairs(answer) do + if ipv then + table.insert(targets, { self.conn_type..ipv, record[record_type:lower()], self.port, self.extra }); + else + table.insert(targets, record[record_type:lower()]); + end + end + return resolve(targets); + end, name, record_type, "IN"); + end); +end + +local function merge_targets(ipv4_targets, ipv6_targets) + local result = { secure = ipv4_targets.secure and ipv6_targets.secure }; + t_move(ipv6_targets, 1, #ipv6_targets, 1, result); + t_move(ipv4_targets, 1, #ipv4_targets, #result+1, result); + return result; +end + -- Find the next target to connect to, and -- pass it to cb() function methods:next(cb) @@ -18,7 +56,7 @@ function methods:next(cb) return; end local next_target = table.remove(self.targets, 1); - cb(unpack(next_target, 1, 4)); + cb(next_target[1], next_target[2], next_target[3], next_target[4]); return; end @@ -28,91 +66,45 @@ function methods:next(cb) return; end - local secure = true; - local tlsa = {}; - local targets = {}; - local n = 3; - local function ready() - n = n - 1; - if n > 0 then return; end - self.targets = targets; + -- Resolve DNS to target list + local dns_resolver = adns.resolver(); + + local dns_lookups = { + ipv4 = do_dns_lookup(self, dns_resolver, "A", self.hostname); + ipv6 = do_dns_lookup(self, dns_resolver, "AAAA", self.hostname); + tlsa = do_dns_lookup(self, dns_resolver, "TLSA", ("_%d._%s.%s"):format(self.port, self.conntype, self.hostname)); + }; + + promise.all_settled(dns_lookups):next(function (dns_results) + -- Combine targets, assign to self.targets, self:next(cb) + local have_ipv4 = dns_results.ipv4.status == "fulfilled"; + local have_ipv6 = dns_results.ipv6.status == "fulfilled"; + + if have_ipv4 and have_ipv6 then + self.targets = merge_targets(dns_results.ipv4.value, dns_results.ipv6.value); + elseif have_ipv4 then + self.targets = dns_results.ipv4.value; + elseif have_ipv6 then + self.targets = dns_results.ipv6.value; + else + self.targets = {}; + end + if self.extra and self.extra.use_dane then - if secure and tlsa[1] then - self.extra.tlsa = tlsa; + if self.targets.secure and dns_results.tlsa.status == "fulfilled" then + self.extra.tlsa = dns_results.tlsa.value; self.extra.dane_hostname = self.hostname; else self.extra.tlsa = nil; self.extra.dane_hostname = nil; end end - self:next(cb); - end - - -- Resolve DNS to target list - local dns_resolver = adns.resolver(); - if not self.extra or self.extra.use_ipv4 ~= false then - dns_resolver:lookup(function (answer, err) - if answer then - secure = secure and answer.secure; - for _, record in ipairs(answer) do - table.insert(targets, { self.conn_type.."4", record.a, self.port, self.extra }); - end - if answer.bogus then - self.last_error = "Validation error in A lookup"; - elseif answer.status then - self.last_error = answer.status .. " in A lookup"; - end - else - self.last_error = err; - end - ready(); - end, self.hostname, "A", "IN"); - else - ready(); - end - - if not self.extra or self.extra.use_ipv6 ~= false then - dns_resolver:lookup(function (answer, err) - if answer then - secure = secure and answer.secure; - for _, record in ipairs(answer) do - table.insert(targets, { self.conn_type.."6", record.aaaa, self.port, self.extra }); - end - if answer.bogus then - self.last_error = "Validation error in AAAA lookup"; - elseif answer.status then - self.last_error = answer.status .. " in AAAA lookup"; - end - else - self.last_error = err; - end - ready(); - end, self.hostname, "AAAA", "IN"); - else - ready(); - end - - if self.extra and self.extra.use_dane == true then - dns_resolver:lookup(function (answer, err) - if answer then - secure = secure and answer.secure; - for _, record in ipairs(answer) do - table.insert(tlsa, record.tlsa); - end - if answer.bogus then - self.last_error = "Validation error in TLSA lookup"; - elseif answer.status then - self.last_error = answer.status .. " in TLSA lookup"; - end - else - self.last_error = err; - end - ready(); - end, ("_%d._tcp.%s"):format(self.port, self.hostname), "TLSA", "IN"); - else - ready(); - end + self:next(cb); + end):catch(function (err) + self.last_error = err; + self.targets = {}; + end); end local function new(hostname, port, conn_type, extra) @@ -137,7 +129,7 @@ local function new(hostname, port, conn_type, extra) hostname = ascii_host; port = port; conn_type = conn_type; - extra = extra; + extra = extra or {}; targets = targets; }, resolver_mt); end -- cgit v1.2.3 From 3c5d7eaef1d3b6d9c93c6ae510317f671da33ceb Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 18 Mar 2022 16:10:26 +0000 Subject: net.resolvers.basic: Alternate IP address family targets, per RFC 8305 --- net/resolvers/basic.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/net/resolvers/basic.lua b/net/resolvers/basic.lua index a5ea5dee..667015a0 100644 --- a/net/resolvers/basic.lua +++ b/net/resolvers/basic.lua @@ -42,8 +42,16 @@ end local function merge_targets(ipv4_targets, ipv6_targets) local result = { secure = ipv4_targets.secure and ipv6_targets.secure }; - t_move(ipv6_targets, 1, #ipv6_targets, 1, result); - t_move(ipv4_targets, 1, #ipv4_targets, #result+1, result); + local common_length = math.min(#ipv4_targets, #ipv6_targets); + for i = 1, common_length do + table.insert(result, ipv6_targets[i]); + table.insert(result, ipv4_targets[i]); + end + if common_length < #ipv4_targets then + t_move(ipv4_targets, common_length+1, #ipv4_targets, common_length+1, result); + elseif common_length < #ipv6_targets then + t_move(ipv6_targets, common_length+1, #ipv6_targets, common_length+1, result); + end return result; end -- cgit v1.2.3 From 6050cd0da5a2463f2d19466adc5f87357c65a0dd Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 18 Mar 2022 16:13:09 +0000 Subject: net.resolvers.basic: Indicate to callback if we have more targets available --- net/resolvers/basic.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net/resolvers/basic.lua b/net/resolvers/basic.lua index 667015a0..9eb7091c 100644 --- a/net/resolvers/basic.lua +++ b/net/resolvers/basic.lua @@ -64,7 +64,7 @@ function methods:next(cb) return; end local next_target = table.remove(self.targets, 1); - cb(next_target[1], next_target[2], next_target[3], next_target[4]); + cb(next_target[1], next_target[2], next_target[3], next_target[4], not not self.targets[1]); return; end -- cgit v1.2.3 From f278c021e68f508383e8a7b67e5e491614c61def Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 18 Mar 2022 16:16:01 +0000 Subject: net.connect: Support for multiple pending connection attempts --- net/connect.lua | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/net/connect.lua b/net/connect.lua index 4b602be4..6bc5e6b5 100644 --- a/net/connect.lua +++ b/net/connect.lua @@ -28,10 +28,6 @@ local pending_connection_listeners = {}; local function attempt_connection(p) p:log("debug", "Checking for targets..."); - if p.conn then - pending_connections_map[p.conn] = nil; - p.conn = nil; - end p.target_resolver:next(function (conn_type, ip, port, extra) if not conn_type then -- No more targets to try @@ -49,7 +45,7 @@ local function attempt_connection(p) p.last_error = err or "unknown reason"; return attempt_connection(p); end - p.conn = conn; + p.conns[conn] = true; pending_connections_map[conn] = p; end); end @@ -62,6 +58,13 @@ function pending_connection_listeners.onconnect(conn) return; end pending_connections_map[conn] = nil; + if p.connected then + -- We already succeeded in connecting + p.conns[conn] = nil; + conn:close(); + return; + end + p.connected = true; p:log("debug", "Successfully connected"); conn:setlistener(p.listeners, p.data); return p.listeners.onconnect(conn); @@ -73,6 +76,7 @@ function pending_connection_listeners.ondisconnect(conn, reason) log("warn", "Failed connection, but unexpected!"); return; end + p.conns[conn] = nil; p.last_error = reason or "unknown reason"; p:log("debug", "Connection attempt failed: %s", p.last_error); attempt_connection(p); @@ -85,6 +89,7 @@ local function connect(target_resolver, listeners, options, data) listeners = assert(listeners); options = options or {}; data = data; + conns = {}; }, pending_connection_mt); p:log("debug", "Starting connection process"); -- cgit v1.2.3 From a188ece31ae32f89f945e1064c0a998454cdf936 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 18 Mar 2022 16:16:57 +0000 Subject: net.connect: When more targets are immediately available, try them after a delay RFC 8305 --- net/connect.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/net/connect.lua b/net/connect.lua index 6bc5e6b5..241cc65b 100644 --- a/net/connect.lua +++ b/net/connect.lua @@ -1,6 +1,7 @@ local server = require "net.server"; local log = require "util.logger".init("net.connect"); local new_id = require "util.id".short; +local timer = require "util.timer"; -- TODO #1246 Happy Eyeballs -- FIXME RFC 6724 @@ -28,7 +29,7 @@ local pending_connection_listeners = {}; local function attempt_connection(p) p:log("debug", "Checking for targets..."); - p.target_resolver:next(function (conn_type, ip, port, extra) + p.target_resolver:next(function (conn_type, ip, port, extra, more_targets_available) if not conn_type then -- No more targets to try p:log("debug", "No more connection targets to try", p.target_resolver.last_error); @@ -47,6 +48,14 @@ local function attempt_connection(p) end p.conns[conn] = true; pending_connections_map[conn] = p; + if more_targets_available then + timer.add_task(0.250, function () + if not p.connected then + p:log("debug", "Still not connected, making parallel connection attempt..."); + attempt_connection(p); + end + end); + end end); end -- cgit v1.2.3 From 860f7d6b7dfccfe3dee023d5cec8130379c5c85c Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 18 Mar 2022 20:14:20 +0000 Subject: net.resolvers.basic: Fix incorrect field name (thanks CI) --- net/resolvers/basic.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net/resolvers/basic.lua b/net/resolvers/basic.lua index 9eb7091c..15338ff4 100644 --- a/net/resolvers/basic.lua +++ b/net/resolvers/basic.lua @@ -80,7 +80,7 @@ function methods:next(cb) local dns_lookups = { ipv4 = do_dns_lookup(self, dns_resolver, "A", self.hostname); ipv6 = do_dns_lookup(self, dns_resolver, "AAAA", self.hostname); - tlsa = do_dns_lookup(self, dns_resolver, "TLSA", ("_%d._%s.%s"):format(self.port, self.conntype, self.hostname)); + tlsa = do_dns_lookup(self, dns_resolver, "TLSA", ("_%d._%s.%s"):format(self.port, self.conn_type, self.hostname)); }; promise.all_settled(dns_lookups):next(function (dns_results) -- cgit v1.2.3 From 5d0ae73a2a13ab9d6b19db62661d9227ed302f4d Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 19 Mar 2022 11:47:52 +0000 Subject: CHANGES: Update to add new changes in trunk --- CHANGES | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGES b/CHANGES index d963f310..5b37d729 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,17 @@ +TRUNK +===== + +## New + +### Administration + +- Add 'watch log' command to follow live debug logs at runtime (even if disabled) + +### Networking + +- Honour 'weight' parameter during SRV record selection +- Support for RFC 8305 "Happy Eyeballs" to improve IPv4/IPv6 connectivity + 0.12.0 ====== -- cgit v1.2.3 From b42fe8746c442351e0330c4570b0ae7eba102fe8 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 21 Mar 2022 11:01:58 +0000 Subject: net.connect: Improve handling of failure when attempts are still pending This could lead to failure being reported too early, even if some connections have not yet failed. --- net/connect.lua | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/net/connect.lua b/net/connect.lua index 241cc65b..0ae9c2b2 100644 --- a/net/connect.lua +++ b/net/connect.lua @@ -33,8 +33,13 @@ local function attempt_connection(p) if not conn_type then -- No more targets to try p:log("debug", "No more connection targets to try", p.target_resolver.last_error); - if p.listeners.onfail then - p.listeners.onfail(p.data, p.last_error or p.target_resolver.last_error or "unable to resolve service"); + if next(p.conns) == nil then + p:log("debug", "No more targets, no pending connections. Connection failed."); + if p.listeners.onfail then + p.listeners.onfail(p.data, p.last_error or p.target_resolver.last_error or "unable to resolve service"); + end + else + p:log("debug", "One or more connection attempts are still pending. Waiting for now."); end return; end @@ -88,7 +93,10 @@ function pending_connection_listeners.ondisconnect(conn, reason) p.conns[conn] = nil; p.last_error = reason or "unknown reason"; p:log("debug", "Connection attempt failed: %s", p.last_error); - attempt_connection(p); + if next(p.conns) == nil and not p.connected then + p:log("debug", "No pending connection attempts, and not yet connected"); + attempt_connection(p); + end end local function connect(target_resolver, listeners, options, data) -- cgit v1.2.3 From 28e919462b81de447b08c490f9879234642d2200 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 21 Mar 2022 11:15:30 +0000 Subject: net.connect: Improve logging on connection attempt failure --- net/connect.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/net/connect.lua b/net/connect.lua index 0ae9c2b2..f2c932f9 100644 --- a/net/connect.lua +++ b/net/connect.lua @@ -93,9 +93,13 @@ function pending_connection_listeners.ondisconnect(conn, reason) p.conns[conn] = nil; p.last_error = reason or "unknown reason"; p:log("debug", "Connection attempt failed: %s", p.last_error); - if next(p.conns) == nil and not p.connected then + if p.connected then + p:log("debug", "Connection already established, ignoring failure"); + elseif next(p.conns) == nil then p:log("debug", "No pending connection attempts, and not yet connected"); attempt_connection(p); + else + p:log("debug", "Other attempts are still pending, ignoring failure"); end end -- 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(+) 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(-) 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(-) 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 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(+) 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 a885cc53fda6f6f07ff69af0a7d01376223f2a0b Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 13 Apr 2022 16:53:19 +0200 Subject: make: Install stanza watcher library (thanks Menel) --- GNUmakefile | 3 ++- makefile | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index e9ec78c4..c8d2d3dd 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -71,12 +71,13 @@ install-util: util/encodings.so util/encodings.so util/pposix.so util/signal.so install-plugins: $(MKDIR) $(MODULES) - $(MKDIR) $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam + $(MKDIR) $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam $(MODULES)/mod_debug_stanzas $(INSTALL_DATA) plugins/*.lua $(MODULES) $(INSTALL_DATA) plugins/mod_pubsub/*.lua $(MODULES)/mod_pubsub $(INSTALL_DATA) plugins/adhoc/*.lua $(MODULES)/adhoc $(INSTALL_DATA) plugins/muc/*.lua $(MODULES)/muc $(INSTALL_DATA) plugins/mod_mam/*.lua $(MODULES)/mod_mam + $(INSTALL_DATA) plugins/mod_debug_stanzas/*.lua $(MODULES)/mod_debug_stanzas install-man: $(MKDIR) $(MAN)/man1 diff --git a/makefile b/makefile index cd6340b7..7f17de03 100644 --- a/makefile +++ b/makefile @@ -73,12 +73,13 @@ install-util: util/encodings.so util/encodings.so util/pposix.so util/signal.so install-plugins: $(MKDIR) $(MODULES) - $(MKDIR) $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam + $(MKDIR) $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam $(MODULES)/mod_debug_stanzas $(INSTALL_DATA) plugins/*.lua $(MODULES) $(INSTALL_DATA) plugins/mod_pubsub/*.lua $(MODULES)/mod_pubsub $(INSTALL_DATA) plugins/adhoc/*.lua $(MODULES)/adhoc $(INSTALL_DATA) plugins/muc/*.lua $(MODULES)/muc $(INSTALL_DATA) plugins/mod_mam/*.lua $(MODULES)/mod_mam + $(INSTALL_DATA) plugins/mod_debug_stanzas/*.lua $(MODULES)/mod_debug_stanzas install-man: $(MKDIR) $(MAN)/man1 -- cgit v1.2.3 From 9e8e910f1e7be52db27ec26aeb9f39171a6946b5 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 20 Apr 2022 22:41:54 +0200 Subject: net.connect: Fix accumulation of connection attempt references Connection attempts that failed the Happy Eyeballs race were not unreferenced and would accumulate. Tested by inspecting the 'pending_connections_map' after establishing s2s with a s2s target where the IPv6 port has a -j DROP rule causing it to time out and the IPv4 attempt wins the race. Expected is that the losing connection stays around until net.server timeouts kick in where it should be removed. The map table should tend towards being empty during idle times. --- net/connect.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/net/connect.lua b/net/connect.lua index f2c932f9..d85afcff 100644 --- a/net/connect.lua +++ b/net/connect.lua @@ -91,6 +91,7 @@ function pending_connection_listeners.ondisconnect(conn, reason) return; end p.conns[conn] = nil; + pending_connections_map[conn] = nil; p.last_error = reason or "unknown reason"; p:log("debug", "Connection attempt failed: %s", p.last_error); if p.connected then -- cgit v1.2.3 From 787835d693df993e8dbf4164d85b7939856e1aea Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 23 Apr 2022 14:29:43 +0200 Subject: util.strbitop: Reduce scope of functions Equivalent to 'local' in Lua, these functions are exported via the luaopen_ function, which is the only one needing to be visible outside of the file. Pointed out by Link Mauve at some point, but there wasn't really any rush here. --- util-src/strbitop.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util-src/strbitop.c b/util-src/strbitop.c index 89fce661..fda8917a 100644 --- a/util-src/strbitop.c +++ b/util-src/strbitop.c @@ -14,7 +14,7 @@ /* TODO Deduplicate code somehow */ -int strop_and(lua_State *L) { +static int strop_and(lua_State *L) { luaL_Buffer buf; size_t a, b, i; const char *str_a = luaL_checklstring(L, 1, &a); @@ -35,7 +35,7 @@ int strop_and(lua_State *L) { return 1; } -int strop_or(lua_State *L) { +static int strop_or(lua_State *L) { luaL_Buffer buf; size_t a, b, i; const char *str_a = luaL_checklstring(L, 1, &a); @@ -56,7 +56,7 @@ int strop_or(lua_State *L) { return 1; } -int strop_xor(lua_State *L) { +static int strop_xor(lua_State *L) { luaL_Buffer buf; size_t a, b, i; const char *str_a = luaL_checklstring(L, 1, &a); -- cgit v1.2.3 From 300813b68b95bd897585cd8728b862c849e1a78d Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 23 Apr 2022 14:37:43 +0200 Subject: util.crand: Reduce scope here too Same as previous commit --- util-src/crand.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util-src/crand.c b/util-src/crand.c index 160ac1f6..e4104787 100644 --- a/util-src/crand.c +++ b/util-src/crand.c @@ -45,7 +45,7 @@ #endif /* This wasn't present before glibc 2.25 */ -int getrandom(void *buf, size_t buflen, unsigned int flags) { +static int getrandom(void *buf, size_t buflen, unsigned int flags) { return syscall(SYS_getrandom, buf, buflen, flags); } #else @@ -66,7 +66,7 @@ int getrandom(void *buf, size_t buflen, unsigned int flags) { #define SMALLBUFSIZ 32 #endif -int Lrandom(lua_State *L) { +static int Lrandom(lua_State *L) { char smallbuf[SMALLBUFSIZ]; char *buf = &smallbuf[0]; const lua_Integer l = luaL_checkinteger(L, 1); -- 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(-) 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(+) 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(+) 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. --- core/certmanager.lua | 34 +--------------- core/portmanager.lua | 21 +++++----- net/server_epoll.lua | 35 ++++++++++++---- net/server_event.lua | 28 +++++++++++-- net/server_select.lua | 18 +++++++-- net/tls_luasec.lua | 90 ++++++++++++++++++++++++++++++++++++++++++ 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 +++--- util/sslconfig.lua | 54 ++++++++++++++++++++++++- 12 files changed, 237 insertions(+), 82 deletions(-) create mode 100644 net/tls_luasec.lua diff --git a/core/certmanager.lua b/core/certmanager.lua index c193e824..6013c633 100644 --- a/core/certmanager.lua +++ b/core/certmanager.lua @@ -9,7 +9,6 @@ local ssl = require "ssl"; local configmanager = require "core.configmanager"; local log = require "util.logger".init("certmanager"); -local ssl_context = ssl.context or require "ssl.context"; local ssl_newcontext = ssl.newcontext; local new_config = require"util.sslconfig".new; local stat = require "lfs".attributes; @@ -313,10 +312,6 @@ else core_defaults.curveslist = nil; end -local path_options = { -- These we pass through resolve_path() - key = true, certificate = true, cafile = true, capath = true, dhparam = true -} - local function create_context(host, mode, ...) local cfg = new_config(); cfg:apply(core_defaults); @@ -352,34 +347,7 @@ local function create_context(host, mode, ...) if user_ssl_config.certificate and not user_ssl_config.key then return nil, "No key present in SSL/TLS configuration for "..host; end end - for option in pairs(path_options) do - if type(user_ssl_config[option]) == "string" then - user_ssl_config[option] = resolve_path(config_path, user_ssl_config[option]); - else - user_ssl_config[option] = nil; - end - end - - -- LuaSec expects dhparam to be a callback that takes two arguments. - -- We ignore those because it is mostly used for having a separate - -- set of params for EXPORT ciphers, which we don't have by default. - if type(user_ssl_config.dhparam) == "string" then - local f, err = io_open(user_ssl_config.dhparam); - if not f then return nil, "Could not open DH parameters: "..err end - local dhparam = f:read("*a"); - f:close(); - user_ssl_config.dhparam = function() return dhparam; end - end - - local ctx, err = ssl_newcontext(user_ssl_config); - - -- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care - -- of it ourselves (W/A for #x) - if ctx and user_ssl_config.ciphers then - local success; - success, err = ssl_context.setcipher(ctx, user_ssl_config.ciphers); - if not success then ctx = nil; end - end + local ctx, err = cfg:build(); if not ctx then err = err or "invalid ssl config" diff --git a/core/portmanager.lua b/core/portmanager.lua index 38c74b66..8c7dfddb 100644 --- a/core/portmanager.lua +++ b/core/portmanager.lua @@ -240,21 +240,22 @@ local function add_sni_host(host, service) log("debug", "Gathering certificates for SNI for host %s, %s service", host, service or "default"); for name, interface, port, n, active_service --luacheck: ignore 213 in active_services:iter(service, nil, nil, nil) do - if active_service.server.hosts and active_service.tls_cfg then - local config_prefix = (active_service.config_prefix or name).."_"; - if config_prefix == "_" then config_prefix = ""; end - local prefix_ssl_config = config.get(host, config_prefix.."ssl"); + if active_service.server and active_service.tls_cfg then local alternate_host = name and config.get(host, name.."_host"); if not alternate_host and name == "https" then -- TODO should this be some generic thing? e.g. in the service definition alternate_host = config.get(host, "http_host"); end local autocert = certmanager.find_host_cert(alternate_host or host); - -- luacheck: ignore 211/cfg - local ssl, err, cfg = certmanager.create_context(host, "server", prefix_ssl_config, autocert, active_service.tls_cfg); - if ssl then - active_service.server.hosts[alternate_host or host] = ssl; - else + local manualcert = active_service.tls_cfg; + local certificate = (autocert and autocert.certificate) or manualcert.certificate; + local key = (autocert and autocert.key) or manualcert.key; + local ok, err = active_service.server:sslctx():set_sni_host( + host, + certificate, + key + ); + if not ok then log("error", "Error creating TLS context for SNI host %s: %s", host, err); end end @@ -277,7 +278,7 @@ prosody.events.add_handler("host-deactivated", function (host) for name, interface, port, n, active_service --luacheck: ignore 213 in active_services:iter(nil, nil, nil, nil) do if active_service.tls_cfg then - active_service.server.hosts[host] = nil; + active_service.server:sslctx():remove_sni_host(host) end end end); diff --git a/net/server_epoll.lua b/net/server_epoll.lua index fa275d71..f8bab56c 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -18,7 +18,6 @@ local traceback = debug.traceback; local logger = require "util.logger"; local log = logger.init("server_epoll"); local socket = require "socket"; -local luasec = require "ssl"; local realtime = require "util.time".now; local monotonic = require "util.time".monotonic; local indexedbheap = require "util.indexedbheap"; @@ -614,6 +613,30 @@ function interface:set_sslctx(sslctx) self._sslctx = sslctx; end +function interface:sslctx() + return self.tls_ctx +end + +function interface:ssl_info() + local sock = self.conn; + return sock.info and sock:info(); +end + +function interface:ssl_peercertificate() + local sock = self.conn; + return sock.getpeercertificate and sock:getpeercertificate(); +end + +function interface:ssl_peerverification() + local sock = self.conn; + return sock.getpeerverification and sock:getpeerverification(); +end + +function interface:ssl_peerfinished() + local sock = self.conn; + return sock.getpeerfinished and sock:getpeerfinished(); +end + function interface:starttls(tls_ctx) if tls_ctx then self.tls_ctx = tls_ctx; end self.starttls = false; @@ -641,11 +664,7 @@ function interface:inittls(tls_ctx, now) self.starttls = false; self:debug("Starting TLS now"); self:updatenames(); -- Can't getpeer/sockname after wrap() - local ok, conn, err = pcall(luasec.wrap, self.conn, self.tls_ctx); - if not ok then - conn, err = ok, conn; - self:debug("Failed to initialize TLS: %s", err); - end + local conn, err = self.tls_ctx:wrap(self.conn); if not conn then self:on("disconnect", err); self:destroy(); @@ -656,8 +675,8 @@ function interface:inittls(tls_ctx, now) if conn.sni then if self.servername then conn:sni(self.servername); - elseif self._server and type(self._server.hosts) == "table" and next(self._server.hosts) ~= nil then - conn:sni(self._server.hosts, true); + elseif next(self.tls_ctx._sni_contexts) ~= nil then + conn:sni(self.tls_ctx._sni_contexts, true); end end if self.extra and self.extra.tlsa and conn.settlsa then diff --git a/net/server_event.lua b/net/server_event.lua index c30181b8..dfd94db4 100644 --- a/net/server_event.lua +++ b/net/server_event.lua @@ -47,7 +47,7 @@ local s_sub = string.sub local coroutine_wrap = coroutine.wrap local coroutine_yield = coroutine.yield -local has_luasec, ssl = pcall ( require , "ssl" ) +local has_luasec = pcall ( require , "ssl" ) local socket = require "socket" local levent = require "luaevent.core" local inet = require "util.net"; @@ -153,7 +153,7 @@ function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed _ = self.eventwrite and self.eventwrite:close( ) self.eventread, self.eventwrite = nil, nil local err - self.conn, err = ssl.wrap( self.conn, self._sslctx ) + self.conn, err = self._sslctx:wrap(self.conn) if err then self.fatalerror = err self.conn = nil -- cannot be used anymore @@ -168,8 +168,8 @@ function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed if self.conn.sni then if self.servername then self.conn:sni(self.servername); - elseif self._server and type(self._server.hosts) == "table" and next(self._server.hosts) ~= nil then - self.conn:sni(self._server.hosts, true); + elseif next(self._sslctx._sni_contexts) ~= nil then + self.conn:sni(self._sslctx._sni_contexts, true); end end @@ -274,6 +274,26 @@ function interface_mt:pause() return self:_lock(self.nointerface, true, self.nowriting); end +function interface_mt:sslctx() + return self._sslctx +end + +function interface_mt:ssl_info() + return self.conn.info and self.conn:info() +end + +function interface_mt:ssl_peercertificate() + return self.conn.getpeercertificate and self.conn:getpeercertificate() +end + +function interface_mt:ssl_peerverification() + return self.conn.getpeerverification and self.conn:getpeerverification() +end + +function interface_mt:ssl_peerfinished() + return self.conn.getpeerfinished and self.conn:getpeerfinished() +end + function interface_mt:resume() self:_lock(self.nointerface, false, self.nowriting); if self.readcallback and not self.eventread then diff --git a/net/server_select.lua b/net/server_select.lua index eea850ce..51439fca 100644 --- a/net/server_select.lua +++ b/net/server_select.lua @@ -359,6 +359,18 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport handler.sslctx = function ( ) return sslctx end + handler.ssl_info = function( ) + return socket.info and socket:info() + end + handler.ssl_peercertificate = function( ) + return socket.getpeercertificate and socket:getpeercertificate() + end + handler.ssl_peerverification = function( ) + return socket.getpeerverification and socket:getpeerverification() + end + handler.ssl_peerfinished = function( ) + return socket.getpeerfinished and socket:getpeerfinished() + end handler.send = function( _, data, i, j ) return send( socket, data, i, j ) end @@ -652,7 +664,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport end out_put( "server.lua: attempting to start tls on " .. tostring( socket ) ) local oldsocket, err = socket - socket, err = ssl_wrap( socket, sslctx ) -- wrap socket + socket, err = sslctx:wrap(socket) -- wrap socket if not socket then out_put( "server.lua: error while starting tls on client: ", tostring(err or "unknown error") ) @@ -662,8 +674,8 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport if socket.sni then if self.servername then socket:sni(self.servername); - elseif self._server and type(self._server.hosts) == "table" and next(self._server.hosts) ~= nil then - socket:sni(self.server().hosts, true); + elseif next(sslctx._sni_contexts) ~= nil then + socket:sni(sslctx._sni_contexts, true); end end diff --git a/net/tls_luasec.lua b/net/tls_luasec.lua new file mode 100644 index 00000000..680b455e --- /dev/null +++ b/net/tls_luasec.lua @@ -0,0 +1,90 @@ +-- Prosody IM +-- Copyright (C) 2021 Prosody folks +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +--[[ +This file provides a shim abstraction over LuaSec, consolidating some code +which was previously spread between net.server backends, portmanager and +certmanager. + +The goal is to provide a more or less well-defined API on top of LuaSec which +abstracts away some of the things which are not needed and simplifies usage of +commonly used things (such as SNI contexts). Eventually, network backends +which do not rely on LuaSocket+LuaSec should be able to provide *this* API +instead of having to mimic LuaSec. +]] +local softreq = require"util.dependencies".softreq; +local ssl = softreq"ssl"; +local ssl_newcontext = ssl.newcontext; +local ssl_context = ssl.context or softreq"ssl.context"; +local io_open = io.open; + +local context_api = {}; +local context_mt = {__index = context_api}; + +function context_api:set_sni_host(host, cert, key) + local ctx, err = self._builder:clone():apply({ + certificate = cert, + key = key, + }):build(); + if not ctx then + return false, err + end + + self._sni_contexts[host] = ctx._inner + + return true, nil +end + +function context_api:remove_sni_host(host) + self._sni_contexts[host] = nil +end + +function context_api:wrap(sock) + local ok, conn, err = pcall(ssl.wrap, sock, self._inner); + if not ok then + return nil, err + end + return conn, nil +end + +local function new_context(cfg, builder) + -- LuaSec expects dhparam to be a callback that takes two arguments. + -- We ignore those because it is mostly used for having a separate + -- set of params for EXPORT ciphers, which we don't have by default. + if type(cfg.dhparam) == "string" then + local f, err = io_open(cfg.dhparam); + if not f then return nil, "Could not open DH parameters: "..err end + local dhparam = f:read("*a"); + f:close(); + cfg.dhparam = function() return dhparam; end + end + + local inner, err = ssl_newcontext(cfg); + if not inner then + return nil, err + end + + -- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care + -- of it ourselves (W/A for #x) + if inner and cfg.ciphers then + local success; + success, err = ssl_context.setcipher(inner, cfg.ciphers); + if not success then + return nil, err + end + end + + return setmetatable({ + _inner = inner, + _builder = builder, + _sni_contexts = {}, + }, context_mt), nil +end + +return { + new_context = new_context, +}; 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"); diff --git a/util/sslconfig.lua b/util/sslconfig.lua index 6074a1fb..23fb934c 100644 --- a/util/sslconfig.lua +++ b/util/sslconfig.lua @@ -3,9 +3,16 @@ local type = type; local pairs = pairs; local rawset = rawset; +local rawget = rawget; +local error = error; local t_concat = table.concat; local t_insert = table.insert; local setmetatable = setmetatable; +local config_path = prosody.paths.config or "."; +local resolve_path = require"util.paths".resolve_relative_path; + +-- TODO: use net.server directly here +local tls_impl = require"net.tls_luasec"; local _ENV = nil; -- luacheck: std none @@ -34,7 +41,7 @@ function handlers.options(config, field, new) options[value] = true; end end - config[field] = options; + rawset(config, field, options) end handlers.verifyext = handlers.options; @@ -70,6 +77,20 @@ finalisers.curveslist = finalisers.ciphers; -- TLS 1.3 ciphers finalisers.ciphersuites = finalisers.ciphers; +-- Path expansion +function finalisers.key(path) + if type(path) == "string" then + return resolve_path(config_path, path); + else + return nil + end +end +finalisers.certificate = finalisers.key; +finalisers.cafile = finalisers.key; +finalisers.capath = finalisers.key; +-- XXX: copied from core/certmanager.lua, but this seems odd, because it would remove a dhparam function from the config +finalisers.dhparam = finalisers.key; + -- protocol = "x" should enable only that protocol -- protocol = "x+" should enable x and later versions @@ -89,11 +110,14 @@ end -- Merge options from 'new' config into 'config' local function apply(config, new) + -- 0 == cache + rawset(config, 0, nil); if type(new) == "table" then for field, value in pairs(new) do (handlers[field] or rawset)(config, field, value); end end + return config end -- Finalize the config into the form LuaSec expects @@ -107,17 +131,45 @@ local function final(config) return output; end +local function build(config) + local cached = rawget(config, 0); + if cached then + return cached, nil + end + + local ctx, err = tls_impl.new_context(config:final(), config); + if ctx then + rawset(config, 0, ctx); + end + return ctx, err +end + local sslopts_mt = { __index = { apply = apply; final = final; + build = build; }; + __newindex = function() + error("SSL config objects cannot be modified directly. Use :apply()") + end; }; + local function new() return setmetatable({options={}}, sslopts_mt); end +local function clone(config) + local result = new(); + for k, v in pairs(config) do + rawset(result, k, v); + end + return result +end + +sslopts_mt.__index.clone = clone; + return { apply = apply; final = final; -- cgit v1.2.3 From 9f7c3b9ba6c2fde4431cd6f3a12072518b478d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sat, 2 Apr 2022 11:15:33 +0200 Subject: net: refactor sslconfig to not depend on LuaSec This now requires that the network backend exposes a tls_builder function, which essentially wraps the former util.sslconfig.new() function, passing a factory to create the eventual SSL context. That allows a net.server backend to pick whatever it likes as SSL context factory, as long as it understands the config table passed by the SSL config builder. Heck, a backend could even mock and replace the entire SSL config builder API. --- core/certmanager.lua | 2 +- net/server.lua | 7 +++++++ net/server_epoll.lua | 6 ++++++ net/server_event.lua | 6 ++++++ net/server_select.lua | 6 ++++++ util/sslconfig.lua | 41 +++++++++++++++++++++++++---------------- 6 files changed, 51 insertions(+), 17 deletions(-) diff --git a/core/certmanager.lua b/core/certmanager.lua index 6013c633..7958e8a9 100644 --- a/core/certmanager.lua +++ b/core/certmanager.lua @@ -10,7 +10,7 @@ local ssl = require "ssl"; local configmanager = require "core.configmanager"; local log = require "util.logger".init("certmanager"); local ssl_newcontext = ssl.newcontext; -local new_config = require"util.sslconfig".new; +local new_config = require"net.server".tls_builder; local stat = require "lfs".attributes; local x509 = require "util.x509"; diff --git a/net/server.lua b/net/server.lua index 0696fd52..72272bef 100644 --- a/net/server.lua +++ b/net/server.lua @@ -118,6 +118,13 @@ if prosody and set_config then prosody.events.add_handler("config-reloaded", load_config); end +local tls_builder = server.tls_builder; +-- resolving the basedir here avoids util.sslconfig depending on +-- prosody.paths.config +function server.tls_builder() + return tls_builder(prosody.paths.config or "") +end + -- require "net.server" shall now forever return this, -- ie. server_select or server_event as chosen above. return server; diff --git a/net/server_epoll.lua b/net/server_epoll.lua index f8bab56c..8e75e072 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -27,6 +27,8 @@ local inet_pton = inet.pton; local _SOCKETINVALID = socket._SOCKETINVALID or -1; local new_id = require "util.id".short; local xpcall = require "util.xpcall".xpcall; +local sslconfig = require "util.sslconfig"; +local tls_impl = require "net.tls_luasec"; local poller = require "util.poll" local EEXIST = poller.EEXIST; @@ -1104,6 +1106,10 @@ return { cfg = setmetatable(newconfig, default_config); end; + tls_builder = function(basedir) + return sslconfig._new(tls_impl.new_context, basedir) + end, + -- libevent emulation event = { EV_READ = "r", EV_WRITE = "w", EV_READWRITE = "rw", EV_LEAVE = -1 }; addevent = function (fd, mode, callback) diff --git a/net/server_event.lua b/net/server_event.lua index dfd94db4..313ba981 100644 --- a/net/server_event.lua +++ b/net/server_event.lua @@ -52,6 +52,8 @@ local socket = require "socket" local levent = require "luaevent.core" local inet = require "util.net"; local inet_pton = inet.pton; +local sslconfig = require "util.sslconfig"; +local tls_impl = require "net.tls_luasec"; local socket_gettime = socket.gettime @@ -944,6 +946,10 @@ return { add_task = add_task, watchfd = watchfd, + tls_builder = function(basedir) + return sslconfig._new(tls_impl.new_context, basedir) + end, + __NAME = SCRIPT_NAME, __DATE = LAST_MODIFIED, __AUTHOR = SCRIPT_AUTHOR, diff --git a/net/server_select.lua b/net/server_select.lua index 51439fca..80754e1a 100644 --- a/net/server_select.lua +++ b/net/server_select.lua @@ -52,6 +52,8 @@ local luasocket = use "socket" or require "socket" local luasocket_gettime = luasocket.gettime local inet = require "util.net"; local inet_pton = inet.pton; +local sslconfig = require "util.sslconfig"; +local tls_impl = require "net.tls_luasec"; --// extern lib methods //-- @@ -1181,4 +1183,8 @@ return { removeserver = removeserver, get_backend = get_backend, changesettings = changesettings, + + tls_builder = function(basedir) + return sslconfig._new(tls_impl.new_context, basedir) + end, } diff --git a/util/sslconfig.lua b/util/sslconfig.lua index 23fb934c..0078365b 100644 --- a/util/sslconfig.lua +++ b/util/sslconfig.lua @@ -8,12 +8,8 @@ local error = error; local t_concat = table.concat; local t_insert = table.insert; local setmetatable = setmetatable; -local config_path = prosody.paths.config or "."; local resolve_path = require"util.paths".resolve_relative_path; --- TODO: use net.server directly here -local tls_impl = require"net.tls_luasec"; - local _ENV = nil; -- luacheck: std none @@ -78,9 +74,9 @@ finalisers.curveslist = finalisers.ciphers; finalisers.ciphersuites = finalisers.ciphers; -- Path expansion -function finalisers.key(path) +function finalisers.key(path, config) if type(path) == "string" then - return resolve_path(config_path, path); + return resolve_path(config._basedir, path); else return nil end @@ -110,11 +106,13 @@ end -- Merge options from 'new' config into 'config' local function apply(config, new) - -- 0 == cache - rawset(config, 0, nil); + rawset(config, "_cache", nil); if type(new) == "table" then for field, value in pairs(new) do - (handlers[field] or rawset)(config, field, value); + -- exclude keys which are internal to the config builder + if field:sub(1, 1) ~= "_" then + (handlers[field] or rawset)(config, field, value); + end end end return config @@ -124,7 +122,10 @@ end local function final(config) local output = { }; for field, value in pairs(config) do - output[field] = (finalisers[field] or id)(value); + -- exclude keys which are internal to the config builder + if field:sub(1, 1) ~= "_" then + output[field] = (finalisers[field] or id)(value, config); + end end -- Need to handle protocols last because it adds to the options list protocol(output); @@ -132,14 +133,14 @@ local function final(config) end local function build(config) - local cached = rawget(config, 0); + local cached = rawget(config, "_cache"); if cached then return cached, nil end - local ctx, err = tls_impl.new_context(config:final(), config); + local ctx, err = rawget(config, "_context_factory")(config:final(), config); if ctx then - rawset(config, 0, ctx); + rawset(config, "_cache", ctx); end return ctx, err end @@ -156,13 +157,21 @@ local sslopts_mt = { }; -local function new() - return setmetatable({options={}}, sslopts_mt); +-- passing basedir through everything is required to avoid sslconfig depending +-- on prosody.paths.config +local function new(context_factory, basedir) + return setmetatable({ + _context_factory = context_factory, + _basedir = basedir, + options={}, + }, sslopts_mt); end local function clone(config) local result = new(); for k, v in pairs(config) do + -- note that we *do* copy the internal keys on clone -- we have to carry + -- both the factory and the cache with us rawset(result, k, v); end return result @@ -173,5 +182,5 @@ sslopts_mt.__index.clone = clone; return { apply = apply; final = final; - new = new; + _new = new; }; -- 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(+) 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(+) 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(-) 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 c90b2eca9da711e26a608720f2c7d80fce4e26b1 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 27 Apr 2022 21:34:35 +0200 Subject: net.tls_luasec: Harden dependency on LuaSec We at some point decided that it was okay to have a hard dependency the TLS library. Especially here since this module is meant to contain all LuaSec specifics. --- net/tls_luasec.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/net/tls_luasec.lua b/net/tls_luasec.lua index 680b455e..2bedb5ab 100644 --- a/net/tls_luasec.lua +++ b/net/tls_luasec.lua @@ -16,10 +16,9 @@ commonly used things (such as SNI contexts). Eventually, network backends which do not rely on LuaSocket+LuaSec should be able to provide *this* API instead of having to mimic LuaSec. ]] -local softreq = require"util.dependencies".softreq; -local ssl = softreq"ssl"; +local ssl = require "ssl"; local ssl_newcontext = ssl.newcontext; -local ssl_context = ssl.context or softreq"ssl.context"; +local ssl_context = ssl.context or require "ssl.context"; local io_open = io.open; local context_api = {}; -- cgit v1.2.3 From 75a15e67f60fcce8df742e68fc5ca988313f47a5 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 27 Apr 2022 21:45:36 +0200 Subject: net.server_select: Restore dependency on LuaSec to soft for tests server_select is used in e.g. storagemanager tests, and some of the CI runners are lacking LuaSec, which resulted in failures. --- net/server_select.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/net/server_select.lua b/net/server_select.lua index 80754e1a..80f5f590 100644 --- a/net/server_select.lua +++ b/net/server_select.lua @@ -47,17 +47,15 @@ local coroutine_yield = coroutine.yield --// extern libs //-- -local has_luasec, luasec = pcall ( require , "ssl" ) local luasocket = use "socket" or require "socket" local luasocket_gettime = luasocket.gettime local inet = require "util.net"; local inet_pton = inet.pton; local sslconfig = require "util.sslconfig"; -local tls_impl = require "net.tls_luasec"; +local has_luasec, tls_impl = pcall(require, "net.tls_luasec"); --// extern lib methods //-- -local ssl_wrap = ( has_luasec and luasec.wrap ) local socket_bind = luasocket.bind local socket_select = luasocket.select -- 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(-) 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(+) 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 117c340edbb7512b911038e40201e64135b966f8 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 8 May 2022 15:21:35 +0200 Subject: util.async: Add Teal description file --- teal-src/plugins/mod_cron.tl | 2 +- teal-src/util/async.d.tl | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 teal-src/util/async.d.tl diff --git a/teal-src/plugins/mod_cron.tl b/teal-src/plugins/mod_cron.tl index f3b8f62f..303ecbf0 100644 --- a/teal-src/plugins/mod_cron.tl +++ b/teal-src/plugins/mod_cron.tl @@ -88,7 +88,7 @@ local function run_task(task : task_spec) task:save(started_at); end -local task_runner = async.runner(run_task); +local task_runner : async.runner_t = async.runner(run_task); module:add_timer(1, function() : integer module:log("info", "Running periodic tasks"); local delay = 3600; diff --git a/teal-src/util/async.d.tl b/teal-src/util/async.d.tl new file mode 100644 index 00000000..a2e41cd6 --- /dev/null +++ b/teal-src/util/async.d.tl @@ -0,0 +1,42 @@ +local record lib + ready : function () : boolean + waiter : function (num : integer, allow_many : boolean) : function (), function () + guarder : function () : function (id : function ()) : function () | nil + record runner_t + func : function (T) + thread : thread + enum state_e + -- from Lua manual + "running" + "suspended" + "normal" + "dead" + + -- from util.async + "ready" + "error" + end + state : state_e + notified_state : state_e + queue : { T } + type watcher_t = function (runner_t, ... : any) + type watchers_t = { state_e : watcher_t } + data : any + id : string + + run : function (runner_t, T) : boolean, state_e, integer + enqueue : function (runner_t, T) : runner_t + log : function (runner_t, string, string, ... : any) + onready : function (runner_t, function) : runner_t + onready : function (runner_t, function) : runner_t + onwaiting : function (runner_t, function) : runner_t + onerror : function (runner_t, function) : runner_t + end + runner : function (function (T), runner_t.watchers_t, any) : runner_t + wait_for : function (any) : any, any + sleep : function (t:number) + + -- set_nexttick = function(new_next_tick) next_tick = new_next_tick; end; + -- set_schedule_function = function (new_schedule_function) schedule_task = new_schedule_function; end; +end +return lib -- cgit v1.2.3 From 04c6caba502115d0611b4313c3228865c48f4285 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 15 May 2022 15:27:35 +0200 Subject: teal/moduleapi: Describe timer wrapper Since it's used in mod_cron --- teal-src/module.d.tl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/teal-src/module.d.tl b/teal-src/module.d.tl index 67b2437c..cb7771e2 100644 --- a/teal-src/module.d.tl +++ b/teal-src/module.d.tl @@ -62,7 +62,12 @@ global record moduleapi send_iq : function (moduleapi, st.stanza_t, util_session, number) broadcast : function (moduleapi, { string }, st.stanza_t, function) type timer_callback = function (number, ... : any) : number - add_timer : function (moduleapi, number, timer_callback, ... : any) + record timer_wrapper + stop : function (timer_wrapper) + disarm : function (timer_wrapper) + reschedule : function (timer_wrapper, number) + end + add_timer : function (moduleapi, number, timer_callback, ... : any) : timer_wrapper get_directory : function (moduleapi) : string enum file_mode "r" "w" "a" "r+" "w+" "a+" -- cgit v1.2.3 From 54d1ff8cfd2e51de040a11522f31870b9033dd67 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 15 May 2022 15:29:02 +0200 Subject: mod_cron: Remove difference between teal version This previously was considered an error because the module API Teal spec did not document a return value from module:add_timer() --- teal-src/plugins/mod_cron.tl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teal-src/plugins/mod_cron.tl b/teal-src/plugins/mod_cron.tl index 303ecbf0..7fa2a36b 100644 --- a/teal-src/plugins/mod_cron.tl +++ b/teal-src/plugins/mod_cron.tl @@ -89,7 +89,7 @@ local function run_task(task : task_spec) end local task_runner : async.runner_t = async.runner(run_task); -module:add_timer(1, function() : integer +scheduled = module:add_timer(1, function() : integer module:log("info", "Running periodic tasks"); local delay = 3600; for host in pairs(active_hosts) do -- 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(-) 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 575b997d1dd13d4295e5bc2f612f8f639c5d9a60 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 8 Jul 2021 12:29:50 +0200 Subject: net.server_epoll: Add support for TCP Fast Open Requires a patch to LuaSocket adding this socket option, https://github.com/lunarmodules/luasocket/pull/378 sysctl tweaks net.ipv4.tcp_fastopen=3 net.ipv4.tcp_fastopen_blackhole_timeout_sec = 0 net.ipv4.tcp_fastopen_key=$( Date: Fri, 16 Jul 2021 00:56:45 +0200 Subject: net.server_epoll: Move call to refresh remote IP address out of wrapper Reduces the side effects of wrapsocket() --- net/server_epoll.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/net/server_epoll.lua b/net/server_epoll.lua index 07fb6dd9..d6b09de3 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -765,7 +765,6 @@ local function wrapsocket(client, server, read_size, listeners, tls_ctx, extra) end end - conn:updatenames(); return conn; end @@ -791,6 +790,7 @@ function interface:onacceptable() return; end local client = wrapsocket(conn, self, nil, self.listeners); + client:updatenames(); client:debug("New connection %s on server %s", client, self); client:defaultoptions(); client._writable = cfg.opportunistic_writes; @@ -935,6 +935,7 @@ end -- COMPAT local function wrapclient(conn, addr, port, listeners, read_size, tls_ctx, extra) local client = wrapsocket(conn, nil, read_size, listeners, tls_ctx, extra); + client:updatenames(); if not client.peername then client.peername, client.peerport = addr, port; end @@ -974,6 +975,7 @@ local function addclient(addr, port, listeners, read_size, tls_ctx, typ, extra) local ok, err = conn:setpeername(addr, port); if not ok and err ~= "timeout" then return ok, err; end local client = wrapsocket(conn, nil, read_size, listeners, tls_ctx, extra) + client:updatenames(); local ok, err = client:init(); if not client.peername then -- otherwise not set until connected -- cgit v1.2.3 From 3b6565c77ba7bb4107bc00d1004bf8db6eaa1fe4 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 16 Jul 2021 00:57:42 +0200 Subject: net.server_epoll: Wrap LuaSocket object earlier to reuse option setting method Since it provides some protection and error handling in the form of logging. --- net/server_epoll.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/net/server_epoll.lua b/net/server_epoll.lua index d6b09de3..0d04f6ee 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -969,12 +969,12 @@ local function addclient(addr, port, listeners, read_size, tls_ctx, typ, extra) if not conn then return conn, err; end local ok, err = conn:settimeout(0); if not ok then return ok, err; end + local client = wrapsocket(conn, nil, read_size, listeners, tls_ctx, extra) if cfg.tcp_fastopen then - pcall(conn.setoption, conn, "tcp-fastopen-connect", 1); + client:setoption("tcp-fastopen-connect", 1); end local ok, err = conn:setpeername(addr, port); if not ok and err ~= "timeout" then return ok, err; end - local client = wrapsocket(conn, nil, read_size, listeners, tls_ctx, extra) client:updatenames(); local ok, err = client:init(); if not client.peername then -- cgit v1.2.3 From d33b858436ddfce5a3364ee35debb7988e6dda0c Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 15 May 2022 22:41:17 +0200 Subject: net.server_epoll: Add option to defer accept() until data available This is a Linux(?) socket option that delays the accept signal until there is data available to read. E.g. with HTTP this might mean that a whole request can be handled without going back trough another turn of the main loop, and an initial client can be responded to. This may have effects on latency and resource use, as the server does not need to allocate resources until really needed. --- CHANGES | 1 + net/server_epoll.lua | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGES b/CHANGES index f47dc1a9..e34ea88e 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,7 @@ TRUNK - Honour 'weight' parameter during SRV record selection - Support for RFC 8305 "Happy Eyeballs" to improve IPv4/IPv6 connectivity - Support for TCP Fast Open in server_epoll (pending LuaSocket support) +- Support for deferred accept in server_epoll (pending LuaSocket support) 0.12.0 ====== diff --git a/net/server_epoll.lua b/net/server_epoll.lua index 0d04f6ee..d0ca3125 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -95,6 +95,9 @@ local default_config = { __index = { -- TCP Fast Open tcp_fastopen = false; + + -- Defer accept until incoming data is available + tcp_defer_accept = false; }}; local cfg = default_config.__index; @@ -912,6 +915,9 @@ local function wrapserver(conn, addr, port, listeners, config) if cfg.tcp_fastopen then server:setoption("tcp-fastopen", cfg.tcp_fastopen); end + if type(cfg.tcp_defer_accept) == "number" then + server:setoption("tcp-defer-accept", cfg.tcp_defer_accept); + end server:add(true, false); return server; end -- cgit v1.2.3 From 977373c9887a1087bd64c76a9bb936078994f8b6 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 17 May 2022 17:05:15 +0200 Subject: util.stanza: Document error conditions in Teal definition Gets you nice error messages if you make a typo in with error_reply() --- teal-src/util/stanza.d.tl | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/teal-src/util/stanza.d.tl b/teal-src/util/stanza.d.tl index a358248a..a04deee0 100644 --- a/teal-src/util/stanza.d.tl +++ b/teal-src/util/stanza.d.tl @@ -4,6 +4,39 @@ local record lib type childtags_iter = function () : stanza_t type maptags_cb = function ( stanza_t ) : stanza_t + + enum stanza_error_type + "auth" + "cancel" + "continue" + "modify" + "wait" + end + enum stanza_error_condition + "bad-request" + "conflict" + "feature-not-implemented" + "forbidden" + "gone" + "internal-server-error" + "item-not-found" + "jid-malformed" + "not-acceptable" + "not-allowed" + "not-authorized" + "policy-violation" + "recipient-unavailable" + "redirect" + "registration-required" + "remote-server-not-found" + "remote-server-timeout" + "resource-constraint" + "service-unavailable" + "subscription-required" + "undefined-condition" + "unexpected-request" + end + record stanza_t name : string attr : { string : string } @@ -35,7 +68,7 @@ local record lib pretty_print : function ( stanza_t ) : string pretty_top_tag : function ( stanza_t ) : string - get_error : function ( stanza_t ) : string, string, string, stanza_t + get_error : function ( stanza_t ) : stanza_error_type, stanza_error_condition, string, stanza_t indent : function ( stanza_t, integer, string ) : stanza_t end @@ -53,7 +86,7 @@ local record lib message : function ( { string : string }, string ) : stanza_t iq : function ( { string : string } ) : stanza_t reply : function ( stanza_t ) : stanza_t - error_reply : function ( stanza_t, string, string, string, string ) + error_reply : function ( stanza_t, stanza_error_type, stanza_error_condition, string, string ) : stanza_t presence : function ( { string : string } ) : stanza_t xml_escape : function ( string ) : string pretty_print : function ( string ) : string -- cgit v1.2.3 From 413b9d70aa337f4ab07fb3cf8adefbe5b411f480 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 17 May 2022 17:06:36 +0200 Subject: util.stanza: Document top level attributes in Teal definition Tells you if you make a typo like "gropchat" or so --- teal-src/util/stanza.d.tl | 51 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/teal-src/util/stanza.d.tl b/teal-src/util/stanza.d.tl index a04deee0..1f565b88 100644 --- a/teal-src/util/stanza.d.tl +++ b/teal-src/util/stanza.d.tl @@ -78,16 +78,61 @@ local record lib { serialized_stanza_t | string } end + record message_attr + ["xml:lang"] : string + from : string + id : string + to : string + type : message_type + enum message_type + "chat" + "error" + "groupchat" + "headline" + "normal" + end + end + + record presence_attr + ["xml:lang"] : string + from : string + id : string + to : string + type : presence_type + enum presence_type + "error" + "probe" + "subscribe" + "subscribed" + "unsubscribe" + "unsubscribed" + end + end + + record iq_attr + ["xml:lang"] : string + from : string + id : string + to : string + type : iq_type + enum iq_type + "error" + "get" + "result" + "set" + end + end + stanza : function ( string, { string : string } ) : stanza_t is_stanza : function ( any ) : boolean preserialize : function ( stanza_t ) : serialized_stanza_t deserialize : function ( serialized_stanza_t ) : stanza_t clone : function ( stanza_t, boolean ) : stanza_t - message : function ( { string : string }, string ) : stanza_t - iq : function ( { string : string } ) : stanza_t + message : function ( message_attr, string ) : stanza_t + iq : function ( iq_attr ) : stanza_t reply : function ( stanza_t ) : stanza_t error_reply : function ( stanza_t, stanza_error_type, stanza_error_condition, string, string ) : stanza_t - presence : function ( { string : string } ) : stanza_t + presence : function ( presence_attr ) : stanza_t xml_escape : function ( string ) : string pretty_print : function ( string ) : string end -- 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(-) 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(-) 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(-) 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(-) 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 ++++++++++----- util/prosodyctl/shell.lua | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) 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); diff --git a/util/prosodyctl/shell.lua b/util/prosodyctl/shell.lua index 0b1dd3f9..cad9ac00 100644 --- a/util/prosodyctl/shell.lua +++ b/util/prosodyctl/shell.lua @@ -27,7 +27,7 @@ local function read_line(prompt_string) end local function send_line(client, line) - client.send(st.stanza("repl-input"):text(line)); + client.send(st.stanza("repl-input", { width = os.getenv "COLUMNS" }):text(line)); end local function repl(client) -- cgit v1.2.3 From fb0299bbf610cf757f9821c358e5c003016643c7 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 30 May 2022 16:25:35 +0200 Subject: util.vcard: Delete since nothing uses it --- util/vcard.lua | 574 --------------------------------------------------------- 1 file changed, 574 deletions(-) delete mode 100644 util/vcard.lua diff --git a/util/vcard.lua b/util/vcard.lua deleted file mode 100644 index e311f73f..00000000 --- a/util/vcard.lua +++ /dev/null @@ -1,574 +0,0 @@ --- Copyright (C) 2011-2014 Kim Alvefur --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - --- TODO --- Fix folding. - -local st = require "util.stanza"; -local t_insert, t_concat = table.insert, table.concat; -local type = type; -local pairs, ipairs = pairs, ipairs; - -local from_text, to_text, from_xep54, to_xep54; - -local line_sep = "\n"; - -local vCard_dtd; -- See end of file -local vCard4_dtd; - -local function vCard_esc(s) - return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n"); -end - -local function vCard_unesc(s) - return s:gsub("\\?[\\nt:;,]", { - ["\\\\"] = "\\", - ["\\n"] = "\n", - ["\\r"] = "\r", - ["\\t"] = "\t", - ["\\:"] = ":", -- FIXME Shouldn't need to escape : in values, just params - ["\\;"] = ";", - ["\\,"] = ",", - [":"] = "\29", - [";"] = "\30", - [","] = "\31", - }); -end - -local function item_to_xep54(item) - local t = st.stanza(item.name, { xmlns = "vcard-temp" }); - - local prop_def = vCard_dtd[item.name]; - if prop_def == "text" then - t:text(item[1]); - elseif type(prop_def) == "table" then - if prop_def.types and item.TYPE then - if type(item.TYPE) == "table" then - for _,v in pairs(prop_def.types) do - for _,typ in pairs(item.TYPE) do - if typ:upper() == v then - t:tag(v):up(); - break; - end - end - end - else - t:tag(item.TYPE:upper()):up(); - end - end - - if prop_def.props then - for _,prop in pairs(prop_def.props) do - if item[prop] then - for _, v in ipairs(item[prop]) do - t:text_tag(prop, v); - end - end - end - end - - if prop_def.value then - t:text_tag(prop_def.value, item[1]); - elseif prop_def.values then - local prop_def_values = prop_def.values; - local repeat_last = prop_def_values.behaviour == "repeat-last" and prop_def_values[#prop_def_values]; - for i=1,#item do - t:text_tag(prop_def.values[i] or repeat_last, item[i]); - end - end - end - - return t; -end - -local function vcard_to_xep54(vCard) - local t = st.stanza("vCard", { xmlns = "vcard-temp" }); - for i=1,#vCard do - t:add_child(item_to_xep54(vCard[i])); - end - return t; -end - -function to_xep54(vCards) - if not vCards[1] or vCards[1].name then - return vcard_to_xep54(vCards) - else - local t = st.stanza("xCard", { xmlns = "vcard-temp" }); - for i=1,#vCards do - t:add_child(vcard_to_xep54(vCards[i])); - end - return t; - end -end - -function from_text(data) - data = data -- unfold and remove empty lines - :gsub("\r\n","\n") - :gsub("\n ", "") - :gsub("\n\n+","\n"); - local vCards = {}; - local current; - for line in data:gmatch("[^\n]+") do - line = vCard_unesc(line); - local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$"); - value = value:gsub("\29",":"); - if #params > 0 then - local _params = {}; - for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do - k = k:upper(); - local _vt = {}; - for _p in v:gmatch("[^\31]+") do - _vt[#_vt+1]=_p - _vt[_p]=true; - end - if isval == "=" then - _params[k]=_vt; - else - _params[k]=true; - end - end - params = _params; - end - if name == "BEGIN" and value == "VCARD" then - current = {}; - vCards[#vCards+1] = current; - elseif name == "END" and value == "VCARD" then - current = nil; - elseif current and vCard_dtd[name] then - local dtd = vCard_dtd[name]; - local item = { name = name }; - t_insert(current, item); - local up = current; - current = item; - if dtd.types then - for _, t in ipairs(dtd.types) do - t = t:lower(); - if ( params.TYPE and params.TYPE[t] == true) - or params[t] == true then - current.TYPE=t; - end - end - end - if dtd.props then - for _, p in ipairs(dtd.props) do - if params[p] then - if params[p] == true then - current[p]=true; - else - for _, prop in ipairs(params[p]) do - current[p]=prop; - end - end - end - end - end - if dtd == "text" or dtd.value then - t_insert(current, value); - elseif dtd.values then - for p in ("\30"..value):gmatch("\30([^\30]*)") do - t_insert(current, p); - end - end - current = up; - end - end - return vCards; -end - -local function item_to_text(item) - local value = {}; - for i=1,#item do - value[i] = vCard_esc(item[i]); - end - value = t_concat(value, ";"); - - local params = ""; - for k,v in pairs(item) do - if type(k) == "string" and k ~= "name" then - params = params .. (";%s=%s"):format(k, type(v) == "table" and t_concat(v,",") or v); - end - end - - return ("%s%s:%s"):format(item.name, params, value) -end - -local function vcard_to_text(vcard) - local t={}; - t_insert(t, "BEGIN:VCARD") - for i=1,#vcard do - t_insert(t, item_to_text(vcard[i])); - end - t_insert(t, "END:VCARD") - return t_concat(t, line_sep); -end - -function to_text(vCards) - if vCards[1] and vCards[1].name then - return vcard_to_text(vCards) - else - local t = {}; - for i=1,#vCards do - t[i]=vcard_to_text(vCards[i]); - end - return t_concat(t, line_sep); - end -end - -local function from_xep54_item(item) - local prop_name = item.name; - local prop_def = vCard_dtd[prop_name]; - - local prop = { name = prop_name }; - - if prop_def == "text" then - prop[1] = item:get_text(); - elseif type(prop_def) == "table" then - if prop_def.value then --single item - prop[1] = item:get_child_text(prop_def.value) or ""; - elseif prop_def.values then --array - local value_names = prop_def.values; - if value_names.behaviour == "repeat-last" then - for i=1,#item.tags do - t_insert(prop, item.tags[i]:get_text() or ""); - end - else - for i=1,#value_names do - t_insert(prop, item:get_child_text(value_names[i]) or ""); - end - end - elseif prop_def.names then - local names = prop_def.names; - for i=1,#names do - if item:get_child(names[i]) then - prop[1] = names[i]; - break; - end - end - end - - if prop_def.props_verbatim then - for k,v in pairs(prop_def.props_verbatim) do - prop[k] = v; - end - end - - if prop_def.types then - local types = prop_def.types; - prop.TYPE = {}; - for i=1,#types do - if item:get_child(types[i]) then - t_insert(prop.TYPE, types[i]:lower()); - end - end - if #prop.TYPE == 0 then - prop.TYPE = nil; - end - end - - -- A key-value pair, within a key-value pair? - if prop_def.props then - local params = prop_def.props; - for i=1,#params do - local name = params[i] - local data = item:get_child_text(name); - if data then - prop[name] = prop[name] or {}; - t_insert(prop[name], data); - end - end - end - else - return nil - end - - return prop; -end - -local function from_xep54_vCard(vCard) - local tags = vCard.tags; - local t = {}; - for i=1,#tags do - t_insert(t, from_xep54_item(tags[i])); - end - return t -end - -function from_xep54(vCard) - if vCard.attr.xmlns ~= "vcard-temp" then - return nil, "wrong-xmlns"; - end - if vCard.name == "xCard" then -- A collection of vCards - local t = {}; - local vCards = vCard.tags; - for i=1,#vCards do - t[i] = from_xep54_vCard(vCards[i]); - end - return t - elseif vCard.name == "vCard" then -- A single vCard - return from_xep54_vCard(vCard) - end -end - -local vcard4 = { } - -function vcard4:text(node, params, value) -- luacheck: ignore 212/params - self:tag(node:lower()) - -- FIXME params - if type(value) == "string" then - self:text_tag("text", value); - elseif vcard4[node] then - vcard4[node](value); - end - self:up(); -end - -function vcard4.N(value) - for i, k in ipairs(vCard_dtd.N.values) do - value:text_tag(k, value[i]); - end -end - -local xmlns_vcard4 = "urn:ietf:params:xml:ns:vcard-4.0" - -local function item_to_vcard4(item) - local typ = item.name:lower(); - local t = st.stanza(typ, { xmlns = xmlns_vcard4 }); - - local prop_def = vCard4_dtd[typ]; - if prop_def == "text" then - t:text_tag("text", item[1]); - elseif prop_def == "uri" then - if item.ENCODING and item.ENCODING[1] == 'b' then - t:text_tag("uri", "data:;base64," .. item[1]); - else - t:text_tag("uri", item[1]); - end - elseif type(prop_def) == "table" then - if prop_def.values then - for i, v in ipairs(prop_def.values) do - t:text_tag(v:lower(), item[i]); - end - else - t:tag("unsupported",{xmlns="http://zash.se/protocol/vcardlib"}) - end - else - t:tag("unsupported",{xmlns="http://zash.se/protocol/vcardlib"}) - end - return t; -end - -local function vcard_to_vcard4xml(vCard) - local t = st.stanza("vcard", { xmlns = xmlns_vcard4 }); - for i=1,#vCard do - t:add_child(item_to_vcard4(vCard[i])); - end - return t; -end - -local function vcards_to_vcard4xml(vCards) - if not vCards[1] or vCards[1].name then - return vcard_to_vcard4xml(vCards) - else - local t = st.stanza("vcards", { xmlns = xmlns_vcard4 }); - for i=1,#vCards do - t:add_child(vcard_to_vcard4xml(vCards[i])); - end - return t; - end -end - --- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd -vCard_dtd = { - VERSION = "text", --MUST be 3.0, so parsing is redundant - FN = "text", - N = { - values = { - "FAMILY", - "GIVEN", - "MIDDLE", - "PREFIX", - "SUFFIX", - }, - }, - NICKNAME = "text", - PHOTO = { - props_verbatim = { ENCODING = { "b" } }, - props = { "TYPE" }, - value = "BINVAL", --{ "EXTVAL", }, - }, - BDAY = "text", - ADR = { - types = { - "HOME", - "WORK", - "POSTAL", - "PARCEL", - "DOM", - "INTL", - "PREF", - }, - values = { - "POBOX", - "EXTADD", - "STREET", - "LOCALITY", - "REGION", - "PCODE", - "CTRY", - } - }, - LABEL = { - types = { - "HOME", - "WORK", - "POSTAL", - "PARCEL", - "DOM", - "INTL", - "PREF", - }, - value = "LINE", - }, - TEL = { - types = { - "HOME", - "WORK", - "VOICE", - "FAX", - "PAGER", - "MSG", - "CELL", - "VIDEO", - "BBS", - "MODEM", - "ISDN", - "PCS", - "PREF", - }, - value = "NUMBER", - }, - EMAIL = { - types = { - "HOME", - "WORK", - "INTERNET", - "PREF", - "X400", - }, - value = "USERID", - }, - JABBERID = "text", - MAILER = "text", - TZ = "text", - GEO = { - values = { - "LAT", - "LON", - }, - }, - TITLE = "text", - ROLE = "text", - LOGO = "copy of PHOTO", - AGENT = "text", - ORG = { - values = { - behaviour = "repeat-last", - "ORGNAME", - "ORGUNIT", - } - }, - CATEGORIES = { - values = "KEYWORD", - }, - NOTE = "text", - PRODID = "text", - REV = "text", - SORTSTRING = "text", - SOUND = "copy of PHOTO", - UID = "text", - URL = "text", - CLASS = { - names = { -- The item.name is the value if it's one of these. - "PUBLIC", - "PRIVATE", - "CONFIDENTIAL", - }, - }, - KEY = { - props = { "TYPE" }, - value = "CRED", - }, - DESC = "text", -}; -vCard_dtd.LOGO = vCard_dtd.PHOTO; -vCard_dtd.SOUND = vCard_dtd.PHOTO; - -vCard4_dtd = { - source = "uri", - kind = "text", - xml = "text", - fn = "text", - n = { - values = { - "family", - "given", - "middle", - "prefix", - "suffix", - }, - }, - nickname = "text", - photo = "uri", - bday = "date-and-or-time", - anniversary = "date-and-or-time", - gender = "text", - adr = { - values = { - "pobox", - "ext", - "street", - "locality", - "region", - "code", - "country", - } - }, - tel = "text", - email = "text", - impp = "uri", - lang = "language-tag", - tz = "text", - geo = "uri", - title = "text", - role = "text", - logo = "uri", - org = "text", - member = "uri", - related = "uri", - categories = "text", - note = "text", - prodid = "text", - rev = "timestamp", - sound = "uri", - uid = "uri", - clientpidmap = "number, uuid", - url = "uri", - version = "text", - key = "uri", - fburl = "uri", - caladruri = "uri", - caluri = "uri", -}; - -return { - from_text = from_text; - to_text = to_text; - - from_xep54 = from_xep54; - to_xep54 = to_xep54; - - to_vcard4 = vcards_to_vcard4xml; -}; -- cgit v1.2.3 From 67177ce287a7a35c93538d9b1c574e9ed6ca44d3 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 29 May 2022 16:06:42 +0200 Subject: core.configmanager: Remove COMPAT for old config format from 2013 --- core/configmanager.lua | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/core/configmanager.lua b/core/configmanager.lua index 092b3946..4b8df96e 100644 --- a/core/configmanager.lua +++ b/core/configmanager.lua @@ -40,16 +40,10 @@ function _M.getconfig() return config; end -function _M.get(host, key, _oldkey) - if key == "core" then - key = _oldkey; -- COMPAT with code that still uses "core" - end +function _M.get(host, key) return config[host][key]; end -function _M.rawget(host, key, _oldkey) - if key == "core" then - key = _oldkey; -- COMPAT with code that still uses "core" - end +function _M.rawget(host, key) local hostconfig = rawget(config, host); if hostconfig then return rawget(hostconfig, key); @@ -68,10 +62,7 @@ local function set(config_table, host, key, value) return false; end -function _M.set(host, key, value, _oldvalue) - if key == "core" then - key, value = value, _oldvalue; --COMPAT with code that still uses "core" - end +function _M.set(host, key, value) return set(config, host, key, value); end -- cgit v1.2.3 From 3721a65c808b8873280fb6303924743b3e4d4c83 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 30 May 2022 16:49:52 +0200 Subject: luacheck: Ignore new warning about using variables prefixed with '_' luacheck 0.26 considers the _ prefix as a hint that the variable or argument is unused, then warns if they are used despite this. We have several places where this prefix is used to avoid shadowing another similarly named variable, resulting in many instances of this warning. --- .luacheckrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.luacheckrc b/.luacheckrc index 0c934d90..3f8c6225 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,6 +1,6 @@ cache = true codes = true -ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV", "431/log", } +ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV", "431/log", "214", } std = "lua53c" max_line_length = 150 -- cgit v1.2.3 From afe8199ba90062bb215e3f154549e7e793db955a Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 30 May 2022 17:12:58 +0200 Subject: luacheck: Ignore new warning about uses of `not (x == y)` and similar Added in luacheck 0.26. --- .luacheckrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.luacheckrc b/.luacheckrc index 3f8c6225..d08dfa70 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,6 +1,6 @@ cache = true codes = true -ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV", "431/log", "214", } +ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV", "431/log", "214", "581" } std = "lua53c" max_line_length = 150 -- cgit v1.2.3 From b78c9e7f5f1ad2e696a76e01e8af70c0eda84246 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 30 May 2022 17:34:58 +0200 Subject: util.openmetrics: Set (previously unused, empty) metatable Silences luacheck warning about the metatable being unused. --- util/openmetrics.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util/openmetrics.lua b/util/openmetrics.lua index cb7791ec..c18e63e9 100644 --- a/util/openmetrics.lua +++ b/util/openmetrics.lua @@ -38,7 +38,7 @@ local metric_proxy_mt = {} metric_proxy_mt.__index = metric_proxy_mt local function new_metric_proxy(metric_family, with_labels_proxy_fun) - return { + return setmetatable({ _family = metric_family, with_labels = function(self, ...) return with_labels_proxy_fun(self._family, ...) @@ -48,7 +48,7 @@ local function new_metric_proxy(metric_family, with_labels_proxy_fun) return family:with_labels(label, ...) end) end - } + }, metric_proxy_mt); end -- END of Utility: "metric proxy" -- cgit v1.2.3 From 677d77e82e0bae7d8d94c910df07ad82fdbcab6d Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 30 May 2022 17:37:25 +0200 Subject: util.openmetrics: Set __name field on metatables to improve error messages Don't think we cause any such errors right now, but you never know! --- util/openmetrics.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/util/openmetrics.lua b/util/openmetrics.lua index c18e63e9..634f9de1 100644 --- a/util/openmetrics.lua +++ b/util/openmetrics.lua @@ -35,6 +35,7 @@ local t_pack, t_unpack = require "util.table".pack, table.unpack or unpack; --lu -- `with_partial_label` by the moduleapi in order to pre-set the `host` label -- on metrics created in non-global modules. local metric_proxy_mt = {} +metric_proxy_mt.__name = "metric_proxy" metric_proxy_mt.__index = metric_proxy_mt local function new_metric_proxy(metric_family, with_labels_proxy_fun) @@ -128,6 +129,7 @@ end -- BEGIN of generic MetricFamily implementation local metric_family_mt = {} +metric_family_mt.__name = "metric_family" metric_family_mt.__index = metric_family_mt local function histogram_metric_ctor(orig_ctor, buckets) @@ -278,6 +280,7 @@ local function compose_name(name, unit) end local metric_registry_mt = {} +metric_registry_mt.__name = "metric_registry" metric_registry_mt.__index = metric_registry_mt local function new_metric_registry(backend) -- 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(+) 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(-) 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(-) 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 02f67bb9171552b69088c77c3d722a53b83e8650 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 1 Jun 2022 13:59:00 +0200 Subject: util.prosodyctl.shell: Print errors in red to highlight them --- util/prosodyctl/shell.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/util/prosodyctl/shell.lua b/util/prosodyctl/shell.lua index cad9ac00..8f910301 100644 --- a/util/prosodyctl/shell.lua +++ b/util/prosodyctl/shell.lua @@ -4,6 +4,8 @@ local st = require "util.stanza"; local path = require "util.paths"; local parse_args = require "util.argparse".parse; local unpack = table.unpack or _G.unpack; +local tc = require "util.termcolours"; +local isatty = require "util.pposix".isatty; local have_readline, readline = pcall(require, "readline"); @@ -64,6 +66,7 @@ end local function start(arg) --luacheck: ignore 212/arg local client = adminstream.client(); local opts, err, where = parse_args(arg); + local ttyout = isatty(io.stdout); if not opts then if err == "param-not-found" then @@ -122,7 +125,11 @@ local function start(arg) --luacheck: ignore 212/arg client.events.add_handler("received", function (stanza) if stanza.name == "repl-output" or stanza.name == "repl-result" then local result_prefix = stanza.attr.type == "error" and "!" or "|"; - print(result_prefix.." "..stanza:get_text()); + local out = result_prefix.." "..stanza:get_text(); + if ttyout and stanza.attr.type == "error" then + out = tc.getstring(tc.getstyle("red"), out); + end + print(out); end if stanza.name == "repl-result" then repl(client); -- 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(-) 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. --- CHANGES | 4 ++++ doc/doap.xml | 8 ++++++++ plugins/mod_saslauth.lua | 10 ++++++++++ 3 files changed, 22 insertions(+) diff --git a/CHANGES b/CHANGES index e34ea88e..213be9da 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,10 @@ TRUNK - Support for TCP Fast Open in server_epoll (pending LuaSocket support) - Support for deferred accept in server_epoll (pending LuaSocket support) +### Security and authentication + +- Advertise supported SASL Channel-Binding types (XEP-0440) + 0.12.0 ====== diff --git a/doc/doap.xml b/doc/doap.xml index fa3893f8..e767115b 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -845,5 +845,13 @@ Broken out of XEP-0313 + + + + 0.2.0 + trunk + complete + + 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 88a2c1ffe029818288cc86afd48d791f2b86f5ad Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 3 Jun 2022 17:51:42 +0200 Subject: net.server: Fix multiple return values return foo and foo() crops multiple return values to a single one, so any second return values etc were last, mostly error details. Introduced in 7e9ebdc75ce4 --- net/server_epoll.lua | 12 ++++++++---- net/server_event.lua | 16 ++++++++++++---- net/server_select.lua | 9 ++++++--- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/net/server_epoll.lua b/net/server_epoll.lua index d0ca3125..a546e1e3 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -627,22 +627,26 @@ end function interface:ssl_info() local sock = self.conn; - return sock.info and sock:info(); + if not sock.info then return nil, "not-implemented"; end + return sock:info(); end function interface:ssl_peercertificate() local sock = self.conn; - return sock.getpeercertificate and sock:getpeercertificate(); + if not sock.getpeercertificate then return nil, "not-implemented"; end + return sock:getpeercertificate(); end function interface:ssl_peerverification() local sock = self.conn; - return sock.getpeerverification and sock:getpeerverification(); + if not sock.getpeerverification then return nil, { { "Chain verification not supported" } }; end + return sock:getpeerverification(); end function interface:ssl_peerfinished() local sock = self.conn; - return sock.getpeerfinished and sock:getpeerfinished(); + if not sock.getpeerfinished then return nil, "not-implemented"; end + return sock:getpeerfinished(); end function interface:starttls(tls_ctx) diff --git a/net/server_event.lua b/net/server_event.lua index 313ba981..d8f08c8d 100644 --- a/net/server_event.lua +++ b/net/server_event.lua @@ -281,19 +281,27 @@ function interface_mt:sslctx() end function interface_mt:ssl_info() - return self.conn.info and self.conn:info() + local sock = self.conn; + if not sock.info then return nil, "not-implemented"; end + return sock:info(); end function interface_mt:ssl_peercertificate() - return self.conn.getpeercertificate and self.conn:getpeercertificate() + local sock = self.conn; + if not sock.getpeercertificate then return nil, "not-implemented"; end + return sock:getpeercertificate(); end function interface_mt:ssl_peerverification() - return self.conn.getpeerverification and self.conn:getpeerverification() + local sock = self.conn; + if not sock.getpeerverification then return nil, { { "Chain verification not supported" } }; end + return sock:getpeerverification(); end function interface_mt:ssl_peerfinished() - return self.conn.getpeerfinished and self.conn:getpeerfinished() + local sock = self.conn; + if not sock.getpeerfinished then return nil, "not-implemented"; end + return sock:getpeerfinished(); end function interface_mt:resume() diff --git a/net/server_select.lua b/net/server_select.lua index 80f5f590..651bdfde 100644 --- a/net/server_select.lua +++ b/net/server_select.lua @@ -363,13 +363,16 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport return socket.info and socket:info() end handler.ssl_peercertificate = function( ) - return socket.getpeercertificate and socket:getpeercertificate() + if not socket.getpeercertificate then return nil, "not-implemented"; end + return socket:getpeercertificate() end handler.ssl_peerverification = function( ) - return socket.getpeerverification and socket:getpeerverification() + if not socket.getpeerverification then return nil, { { "Chain verification not supported" } }; end + return socket:getpeerverification(); end handler.ssl_peerfinished = function( ) - return socket.getpeerfinished and socket:getpeerfinished() + if not socket.getpeerfinished then return nil, "not-implemented"; end + return socket:getpeerfinished(); end handler.send = function( _, data, i, j ) return send( socket, data, i, j ) -- cgit v1.2.3 From 49a9d5e4274c26272761656d3fdaa6b2509c90f5 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 11 Jun 2022 21:11:01 +0100 Subject: util.watchdog: Update to use "new" util.timer API When this module was written, it wasn't possible to cancel or reschedule a timer. Times have changed, and we should take advantage of those new methods. This module becomes a very thin wrapper around util.timer now, but I'd argue it's still a very common and useful concept/abstraction to have around. Possible API change: this removes the 'last_reset' field of the watchdog. This was never really intended as a public thing, and I can't find any code that uses it, so I consider removal to be safe. --- util/watchdog.lua | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/util/watchdog.lua b/util/watchdog.lua index 516e60e4..1d30f22b 100644 --- a/util/watchdog.lua +++ b/util/watchdog.lua @@ -9,27 +9,30 @@ local watchdog_methods = {}; local watchdog_mt = { __index = watchdog_methods }; local function new(timeout, callback) - local watchdog = setmetatable({ timeout = timeout, last_reset = os_time(), callback = callback }, watchdog_mt); - timer.add_task(timeout+1, function (current_time) - local last_reset = watchdog.last_reset; - if not last_reset then - return; - end - local time_left = (last_reset + timeout) - current_time; - if time_left < 0 then - return watchdog:callback(); - end - return time_left + 1; + local watchdog = setmetatable({ + timeout = timeout; + callback = callback; + timer_id = nil; + }, watchdog_mt); + + watchdog.timer_id = timer.add_task(timeout+1, function () + return watchdog:callback(); end); + return watchdog; end function watchdog_methods:reset() - self.last_reset = os_time(); + if self.timer_id then + timer.reschedule(self.timer_id, self.timeout); + end end function watchdog_methods:cancel() - self.last_reset = nil; + if self.timer_id then + timer.stop(self.timer_id); + self.timer_id = nil; + end end return { -- cgit v1.2.3 From 029e0934783a60cf0be8c08e982440205fa9d337 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 11 Jun 2022 22:15:14 +0100 Subject: util.watchdog: Allow :reset() to restart a cancelled watchdog --- util/watchdog.lua | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/util/watchdog.lua b/util/watchdog.lua index 1d30f22b..6eb2e602 100644 --- a/util/watchdog.lua +++ b/util/watchdog.lua @@ -1,6 +1,5 @@ local timer = require "util.timer"; local setmetatable = setmetatable; -local os_time = os.time; local _ENV = nil; -- luacheck: std none @@ -15,16 +14,18 @@ local function new(timeout, callback) timer_id = nil; }, watchdog_mt); - watchdog.timer_id = timer.add_task(timeout+1, function () - return watchdog:callback(); - end); + watchdog:reset(); -- Kick things off return watchdog; end function watchdog_methods:reset() if self.timer_id then - timer.reschedule(self.timer_id, self.timeout); + timer.reschedule(self.timer_id, self.timeout+1); + else + self.timer_id = timer.add_task(self.timeout+1, function () + return self:callback(); + end); end end -- cgit v1.2.3 From 7532eac0e5e4cd51a765498bdcca2d52b0b3ca7a Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 11 Jun 2022 22:15:31 +0100 Subject: util.watchdog: Allow passing a new timeout to :reset() --- util/watchdog.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/util/watchdog.lua b/util/watchdog.lua index 6eb2e602..407028a5 100644 --- a/util/watchdog.lua +++ b/util/watchdog.lua @@ -19,7 +19,10 @@ local function new(timeout, callback) return watchdog; end -function watchdog_methods:reset() +function watchdog_methods:reset(new_timeout) + if new_timeout then + self.timeout = new_timeout; + end if self.timer_id then timer.reschedule(self.timer_id, self.timeout+1); else -- cgit v1.2.3 From 76da7a7387d879f01bbefbc1efb420c437b1eb85 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 13 Jun 2022 18:41:13 +0200 Subject: net.http.codes: Refresh from registry, many refs now point to RFC9110 --- net/http/codes.lua | 92 +++++++++++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/net/http/codes.lua b/net/http/codes.lua index 4327f151..b2949286 100644 --- a/net/http/codes.lua +++ b/net/http/codes.lua @@ -2,62 +2,62 @@ local response_codes = { -- Source: http://www.iana.org/assignments/http-status-codes - [100] = "Continue"; -- RFC7231, Section 6.2.1 - [101] = "Switching Protocols"; -- RFC7231, Section 6.2.2 + [100] = "Continue"; -- RFC9110, Section 15.2.1 + [101] = "Switching Protocols"; -- RFC9110, Section 15.2.2 [102] = "Processing"; [103] = "Early Hints"; -- [104-199] = "Unassigned"; - [200] = "OK"; -- RFC7231, Section 6.3.1 - [201] = "Created"; -- RFC7231, Section 6.3.2 - [202] = "Accepted"; -- RFC7231, Section 6.3.3 - [203] = "Non-Authoritative Information"; -- RFC7231, Section 6.3.4 - [204] = "No Content"; -- RFC7231, Section 6.3.5 - [205] = "Reset Content"; -- RFC7231, Section 6.3.6 - [206] = "Partial Content"; -- RFC7233, Section 4.1 + [200] = "OK"; -- RFC9110, Section 15.3.1 + [201] = "Created"; -- RFC9110, Section 15.3.2 + [202] = "Accepted"; -- RFC9110, Section 15.3.3 + [203] = "Non-Authoritative Information"; -- RFC9110, Section 15.3.4 + [204] = "No Content"; -- RFC9110, Section 15.3.5 + [205] = "Reset Content"; -- RFC9110, Section 15.3.6 + [206] = "Partial Content"; -- RFC9110, Section 15.3.7 [207] = "Multi-Status"; [208] = "Already Reported"; -- [209-225] = "Unassigned"; [226] = "IM Used"; -- [227-299] = "Unassigned"; - [300] = "Multiple Choices"; -- RFC7231, Section 6.4.1 - [301] = "Moved Permanently"; -- RFC7231, Section 6.4.2 - [302] = "Found"; -- RFC7231, Section 6.4.3 - [303] = "See Other"; -- RFC7231, Section 6.4.4 - [304] = "Not Modified"; -- RFC7232, Section 4.1 - [305] = "Use Proxy"; -- RFC7231, Section 6.4.5 - -- [306] = "(Unused)"; -- RFC7231, Section 6.4.6 - [307] = "Temporary Redirect"; -- RFC7231, Section 6.4.7 - [308] = "Permanent Redirect"; + [300] = "Multiple Choices"; -- RFC9110, Section 15.4.1 + [301] = "Moved Permanently"; -- RFC9110, Section 15.4.2 + [302] = "Found"; -- RFC9110, Section 15.4.3 + [303] = "See Other"; -- RFC9110, Section 15.4.4 + [304] = "Not Modified"; -- RFC9110, Section 15.4.5 + [305] = "Use Proxy"; -- RFC9110, Section 15.4.6 + -- [306] = "(Unused)"; -- RFC9110, Section 15.4.7 + [307] = "Temporary Redirect"; -- RFC9110, Section 15.4.8 + [308] = "Permanent Redirect"; -- RFC9110, Section 15.4.9 -- [309-399] = "Unassigned"; - [400] = "Bad Request"; -- RFC7231, Section 6.5.1 - [401] = "Unauthorized"; -- RFC7235, Section 3.1 - [402] = "Payment Required"; -- RFC7231, Section 6.5.2 - [403] = "Forbidden"; -- RFC7231, Section 6.5.3 - [404] = "Not Found"; -- RFC7231, Section 6.5.4 - [405] = "Method Not Allowed"; -- RFC7231, Section 6.5.5 - [406] = "Not Acceptable"; -- RFC7231, Section 6.5.6 - [407] = "Proxy Authentication Required"; -- RFC7235, Section 3.2 - [408] = "Request Timeout"; -- RFC7231, Section 6.5.7 - [409] = "Conflict"; -- RFC7231, Section 6.5.8 - [410] = "Gone"; -- RFC7231, Section 6.5.9 - [411] = "Length Required"; -- RFC7231, Section 6.5.10 - [412] = "Precondition Failed"; -- RFC7232, Section 4.2 - [413] = "Payload Too Large"; -- RFC7231, Section 6.5.11 - [414] = "URI Too Long"; -- RFC7231, Section 6.5.12 - [415] = "Unsupported Media Type"; -- RFC7231, Section 6.5.13 - [416] = "Range Not Satisfiable"; -- RFC7233, Section 4.4 - [417] = "Expectation Failed"; -- RFC7231, Section 6.5.14 + [400] = "Bad Request"; -- RFC9110, Section 15.5.1 + [401] = "Unauthorized"; -- RFC9110, Section 15.5.2 + [402] = "Payment Required"; -- RFC9110, Section 15.5.3 + [403] = "Forbidden"; -- RFC9110, Section 15.5.4 + [404] = "Not Found"; -- RFC9110, Section 15.5.5 + [405] = "Method Not Allowed"; -- RFC9110, Section 15.5.6 + [406] = "Not Acceptable"; -- RFC9110, Section 15.5.7 + [407] = "Proxy Authentication Required"; -- RFC9110, Section 15.5.8 + [408] = "Request Timeout"; -- RFC9110, Section 15.5.9 + [409] = "Conflict"; -- RFC9110, Section 15.5.10 + [410] = "Gone"; -- RFC9110, Section 15.5.11 + [411] = "Length Required"; -- RFC9110, Section 15.5.12 + [412] = "Precondition Failed"; -- RFC9110, Section 15.5.13 + [413] = "Content Too Large"; -- RFC9110, Section 15.5.14 + [414] = "URI Too Long"; -- RFC9110, Section 15.5.15 + [415] = "Unsupported Media Type"; -- RFC9110, Section 15.5.16 + [416] = "Range Not Satisfiable"; -- RFC9110, Section 15.5.17 + [417] = "Expectation Failed"; -- RFC9110, Section 15.5.18 [418] = "I'm a teapot"; -- RFC2324, Section 2.3.2 -- [419-420] = "Unassigned"; - [421] = "Misdirected Request"; -- RFC7540, Section 9.1.2 - [422] = "Unprocessable Entity"; + [421] = "Misdirected Request"; -- RFC9110, Section 15.5.20 + [422] = "Unprocessable Content"; -- RFC9110, Section 15.5.21 [423] = "Locked"; [424] = "Failed Dependency"; [425] = "Too Early"; - [426] = "Upgrade Required"; -- RFC7231, Section 6.5.15 + [426] = "Upgrade Required"; -- RFC9110, Section 15.5.22 -- [427] = "Unassigned"; [428] = "Precondition Required"; [429] = "Too Many Requests"; @@ -67,17 +67,17 @@ local response_codes = { [451] = "Unavailable For Legal Reasons"; -- [452-499] = "Unassigned"; - [500] = "Internal Server Error"; -- RFC7231, Section 6.6.1 - [501] = "Not Implemented"; -- RFC7231, Section 6.6.2 - [502] = "Bad Gateway"; -- RFC7231, Section 6.6.3 - [503] = "Service Unavailable"; -- RFC7231, Section 6.6.4 - [504] = "Gateway Timeout"; -- RFC7231, Section 6.6.5 - [505] = "HTTP Version Not Supported"; -- RFC7231, Section 6.6.6 + [500] = "Internal Server Error"; -- RFC9110, Section 15.6.1 + [501] = "Not Implemented"; -- RFC9110, Section 15.6.2 + [502] = "Bad Gateway"; -- RFC9110, Section 15.6.3 + [503] = "Service Unavailable"; -- RFC9110, Section 15.6.4 + [504] = "Gateway Timeout"; -- RFC9110, Section 15.6.5 + [505] = "HTTP Version Not Supported"; -- RFC9110, Section 15.6.6 [506] = "Variant Also Negotiates"; [507] = "Insufficient Storage"; [508] = "Loop Detected"; -- [509] = "Unassigned"; - [510] = "Not Extended"; + [510] = "Not Extended"; -- (OBSOLETED) [511] = "Network Authentication Required"; -- [512-599] = "Unassigned"; }; -- 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(-) 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(+) 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(-) 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(-) 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 d690f1502aafe5fbbe66114239e70b389315938e Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 10 Sep 2020 21:58:23 +0200 Subject: util.hashes: Refactor hash functions to use OpenSSL EVP methods (fix #1698) MD5() is deprecated, but EVP_md5() is not. Functions in macros like this make it awkward to apply static analysis and code formatting. --- util-src/hashes.c | 83 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/util-src/hashes.c b/util-src/hashes.c index 8eefcd6b..44194905 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -46,28 +46,71 @@ static void toHex(const unsigned char *in, int length, unsigned char *out) { } } -#define MAKE_HASH_FUNCTION(myFunc, func, size) \ -static int myFunc(lua_State *L) { \ - size_t len; \ - const char *s = luaL_checklstring(L, 1, &len); \ - int hex_out = lua_toboolean(L, 2); \ - unsigned char hash[size], result[size*2]; \ - func((const unsigned char*)s, len, hash); \ - if (hex_out) { \ - toHex(hash, size, result); \ - lua_pushlstring(L, (char*)result, size*2); \ - } else { \ - lua_pushlstring(L, (char*)hash, size);\ - } \ - return 1; \ +static int Levp_hash(lua_State *L, const EVP_MD *evp) { + size_t len; + unsigned int size = EVP_MAX_MD_SIZE; + const char *s = luaL_checklstring(L, 1, &len); + int hex_out = lua_toboolean(L, 2); + + unsigned char hash[EVP_MAX_MD_SIZE], result[EVP_MAX_MD_SIZE * 2]; + + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + + if(ctx == NULL) { + goto fail; + } + + if(!EVP_DigestInit_ex(ctx, evp, NULL)) { + goto fail; + } + + if(!EVP_DigestUpdate(ctx, s, len)) { + goto fail; + } + + if(!EVP_DigestFinal_ex(ctx, hash, &size)) { + goto fail; + } + + EVP_MD_CTX_free(ctx); + + if(hex_out) { + toHex(hash, size, result); + lua_pushlstring(L, (char *)result, size * 2); + } else { + lua_pushlstring(L, (char *)hash, size); + } + + return 1; + +fail: + EVP_MD_CTX_free(ctx); + return luaL_error(L, "hash function failed"); +} + +static int Lsha1(lua_State *L) { + return Levp_hash(L, EVP_sha1()); +} + +static int Lsha224(lua_State *L) { + return Levp_hash(L, EVP_sha224()); +} + +static int Lsha256(lua_State *L) { + return Levp_hash(L, EVP_sha256()); +} + +static int Lsha384(lua_State *L) { + return Levp_hash(L, EVP_sha384()); +} + +static int Lsha512(lua_State *L) { + return Levp_hash(L, EVP_sha512()); } -MAKE_HASH_FUNCTION(Lsha1, SHA1, SHA_DIGEST_LENGTH) -MAKE_HASH_FUNCTION(Lsha224, SHA224, SHA224_DIGEST_LENGTH) -MAKE_HASH_FUNCTION(Lsha256, SHA256, SHA256_DIGEST_LENGTH) -MAKE_HASH_FUNCTION(Lsha384, SHA384, SHA384_DIGEST_LENGTH) -MAKE_HASH_FUNCTION(Lsha512, SHA512, SHA512_DIGEST_LENGTH) -MAKE_HASH_FUNCTION(Lmd5, MD5, MD5_DIGEST_LENGTH) +static int Lmd5(lua_State *L) { + return Levp_hash(L, EVP_md5()); +} struct hash_desc { int (*Init)(void *); -- cgit v1.2.3 From 54f8ca81f427db3ae9624f9ab5ebd0a566f63013 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 29 Nov 2020 17:58:30 +0100 Subject: util.hashes: Refactor HMAC bindings (fixes #1589) HMAC() is deprecated As with the regular hash functions, macros like this make it awkward to apply static analysis and code formatting. --- util-src/hashes.c | 83 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 22 deletions(-) diff --git a/util-src/hashes.c b/util-src/hashes.c index 44194905..6b7a28d8 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -120,28 +120,67 @@ struct hash_desc { void *ctx, *ctxo; }; -#define MAKE_HMAC_FUNCTION(myFunc, evp, size, type) \ -static int myFunc(lua_State *L) { \ - unsigned char hash[size], result[2*size]; \ - size_t key_len, msg_len; \ - unsigned int out_len; \ - const char *key = luaL_checklstring(L, 1, &key_len); \ - const char *msg = luaL_checklstring(L, 2, &msg_len); \ - const int hex_out = lua_toboolean(L, 3); \ - HMAC(evp(), key, key_len, (const unsigned char*)msg, msg_len, (unsigned char*)hash, &out_len); \ - if (hex_out) { \ - toHex(hash, out_len, result); \ - lua_pushlstring(L, (char*)result, out_len*2); \ - } else { \ - lua_pushlstring(L, (char*)hash, out_len); \ - } \ - return 1; \ -} - -MAKE_HMAC_FUNCTION(Lhmac_sha1, EVP_sha1, SHA_DIGEST_LENGTH, SHA_CTX) -MAKE_HMAC_FUNCTION(Lhmac_sha256, EVP_sha256, SHA256_DIGEST_LENGTH, SHA256_CTX) -MAKE_HMAC_FUNCTION(Lhmac_sha512, EVP_sha512, SHA512_DIGEST_LENGTH, SHA512_CTX) -MAKE_HMAC_FUNCTION(Lhmac_md5, EVP_md5, MD5_DIGEST_LENGTH, MD5_CTX) +static int Levp_hmac(lua_State *L, const EVP_MD *evp) { + unsigned char hash[EVP_MAX_MD_SIZE], result[EVP_MAX_MD_SIZE * 2]; + size_t key_len, msg_len; + size_t out_len = EVP_MAX_MD_SIZE; + const char *key = luaL_checklstring(L, 1, &key_len); + const char *msg = luaL_checklstring(L, 2, &msg_len); + const int hex_out = lua_toboolean(L, 3); + + EVP_PKEY *pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, (unsigned char *)key, key_len); + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + + if(ctx == NULL || pkey == NULL) { + goto fail; + } + + if(!EVP_DigestSignInit(ctx, NULL, evp, NULL, pkey)) { + goto fail; + } + + if(!EVP_DigestSignUpdate(ctx, msg, msg_len)) { + goto fail; + } + + if(!EVP_DigestSignFinal(ctx, hash, &out_len)) { + goto fail; + } + + EVP_MD_CTX_free(ctx); + EVP_PKEY_free(pkey); + + if(hex_out) { + toHex(hash, out_len, result); + lua_pushlstring(L, (char *)result, out_len * 2); + } else { + lua_pushlstring(L, (char *)hash, out_len); + } + + return 1; + +fail: + EVP_MD_CTX_free(ctx); + EVP_PKEY_free(pkey); + return luaL_error(L, "hash function failed"); +} + +static int Lhmac_sha1(lua_State *L) { + return Levp_hmac(L, EVP_sha1()); +} + +static int Lhmac_sha256(lua_State *L) { + return Levp_hmac(L, EVP_sha256()); +} + +static int Lhmac_sha512(lua_State *L) { + return Levp_hmac(L, EVP_sha512()); +} + +static int Lhmac_md5(lua_State *L) { + return Levp_hmac(L, EVP_md5()); +} + static int Lpbkdf2_sha1(lua_State *L) { unsigned char out[SHA_DIGEST_LENGTH]; -- cgit v1.2.3 From ae14dc12208406f54c3ac260dc1dfe701e77f3a3 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 29 Nov 2020 17:58:45 +0100 Subject: util.hashes: Expose sha224 and sha384 HMAC functions For completeness and consistency with set of plain hash functions --- teal-src/util/hashes.d.tl | 2 ++ util-src/hashes.c | 10 ++++++++++ util/hmac.lua | 2 ++ 3 files changed, 14 insertions(+) diff --git a/teal-src/util/hashes.d.tl b/teal-src/util/hashes.d.tl index cbb06f8e..2784a59b 100644 --- a/teal-src/util/hashes.d.tl +++ b/teal-src/util/hashes.d.tl @@ -11,6 +11,8 @@ local record lib md5 : hash hmac_sha1 : hmac hmac_sha256 : hmac + hmac_sha224 : hmac + hmac_sha384 :hmac hmac_sha512 : hmac hmac_md5 : hmac scram_Hi_sha1 : kdf diff --git a/util-src/hashes.c b/util-src/hashes.c index 6b7a28d8..00661531 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -169,10 +169,18 @@ static int Lhmac_sha1(lua_State *L) { return Levp_hmac(L, EVP_sha1()); } +static int Lhmac_sha224(lua_State *L) { + return Levp_hmac(L, EVP_sha224()); +} + static int Lhmac_sha256(lua_State *L) { return Levp_hmac(L, EVP_sha256()); } +static int Lhmac_sha384(lua_State *L) { + return Levp_hmac(L, EVP_sha384()); +} + static int Lhmac_sha512(lua_State *L) { return Levp_hmac(L, EVP_sha512()); } @@ -236,7 +244,9 @@ static const luaL_Reg Reg[] = { { "sha512", Lsha512 }, { "md5", Lmd5 }, { "hmac_sha1", Lhmac_sha1 }, + { "hmac_sha224", Lhmac_sha224 }, { "hmac_sha256", Lhmac_sha256 }, + { "hmac_sha384", Lhmac_sha384 }, { "hmac_sha512", Lhmac_sha512 }, { "hmac_md5", Lhmac_md5 }, { "scram_Hi_sha1", Lpbkdf2_sha1 }, /* COMPAT */ diff --git a/util/hmac.lua b/util/hmac.lua index 4cad17cc..994ea93b 100644 --- a/util/hmac.lua +++ b/util/hmac.lua @@ -13,6 +13,8 @@ local hashes = require "util.hashes" return { md5 = hashes.hmac_md5, sha1 = hashes.hmac_sha1, + sha224 = hashes.hmac_sha224, sha256 = hashes.hmac_sha256, + sha384 = hashes.hmac_sha384, sha512 = hashes.hmac_sha512, }; -- cgit v1.2.3 From 9f932f75595640510f3d36ab6bfe10e673235d9f Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 10 Sep 2020 21:58:25 +0200 Subject: util.hashes: Refactor PBKDF2 to deduplicate code --- util-src/hashes.c | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/util-src/hashes.c b/util-src/hashes.c index 00661531..80740866 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -190,38 +190,29 @@ static int Lhmac_md5(lua_State *L) { } -static int Lpbkdf2_sha1(lua_State *L) { - unsigned char out[SHA_DIGEST_LENGTH]; +static int Levp_pbkdf2(lua_State *L, const EVP_MD *evp, size_t out_len) { + unsigned char out[EVP_MAX_MD_SIZE]; size_t pass_len, salt_len; const char *pass = luaL_checklstring(L, 1, &pass_len); const unsigned char *salt = (unsigned char *)luaL_checklstring(L, 2, &salt_len); const int iter = luaL_checkinteger(L, 3); - if(PKCS5_PBKDF2_HMAC(pass, pass_len, salt, salt_len, iter, EVP_sha1(), SHA_DIGEST_LENGTH, out) == 0) { + if(PKCS5_PBKDF2_HMAC(pass, pass_len, salt, salt_len, iter, evp, out_len, out) == 0) { return luaL_error(L, "PKCS5_PBKDF2_HMAC() failed"); } - lua_pushlstring(L, (char *)out, SHA_DIGEST_LENGTH); + lua_pushlstring(L, (char *)out, out_len); return 1; } +static int Lpbkdf2_sha1(lua_State *L) { + return Levp_pbkdf2(L, EVP_sha1(), SHA_DIGEST_LENGTH); +} static int Lpbkdf2_sha256(lua_State *L) { - unsigned char out[SHA256_DIGEST_LENGTH]; - - size_t pass_len, salt_len; - const char *pass = luaL_checklstring(L, 1, &pass_len); - const unsigned char *salt = (unsigned char *)luaL_checklstring(L, 2, &salt_len); - const int iter = luaL_checkinteger(L, 3); - - if(PKCS5_PBKDF2_HMAC(pass, pass_len, salt, salt_len, iter, EVP_sha256(), SHA256_DIGEST_LENGTH, out) == 0) { - return luaL_error(L, "PKCS5_PBKDF2_HMAC() failed"); - } - - lua_pushlstring(L, (char *)out, SHA256_DIGEST_LENGTH); - return 1; + return Levp_pbkdf2(L, EVP_sha256(), SHA256_DIGEST_LENGTH); } static int Lhash_equals(lua_State *L) { -- cgit v1.2.3 From f3d61e394501472062f359e02ce5d3bb10bc4bc8 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 10 Sep 2020 21:58:24 +0200 Subject: util.hashes: Bind BLAKE2 algoritms supported by OpenSSL --- teal-src/util/hashes.d.tl | 2 ++ util-src/hashes.c | 21 +++++++++++++++++++++ util/hmac.lua | 2 ++ 3 files changed, 25 insertions(+) diff --git a/teal-src/util/hashes.d.tl b/teal-src/util/hashes.d.tl index 2784a59b..6932ab72 100644 --- a/teal-src/util/hashes.d.tl +++ b/teal-src/util/hashes.d.tl @@ -9,6 +9,8 @@ local record lib sha384 : hash sha512 : hash md5 : hash + blake2s256 : hash + blake2b512 : hash hmac_sha1 : hmac hmac_sha256 : hmac hmac_sha224 : hmac diff --git a/util-src/hashes.c b/util-src/hashes.c index 80740866..2696f177 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -112,6 +112,15 @@ static int Lmd5(lua_State *L) { return Levp_hash(L, EVP_md5()); } +static int Lblake2s256(lua_State *L) { + return Levp_hash(L, EVP_blake2s256()); +} + +static int Lblake2b512(lua_State *L) { + return Levp_hash(L, EVP_blake2b512()); +} + + struct hash_desc { int (*Init)(void *); int (*Update)(void *, const void *, size_t); @@ -189,6 +198,14 @@ static int Lhmac_md5(lua_State *L) { return Levp_hmac(L, EVP_md5()); } +static int Lhmac_blake2s256(lua_State *L) { + return Levp_hmac(L, EVP_blake2s256()); +} + +static int Lhmac_blake2b512(lua_State *L) { + return Levp_hmac(L, EVP_blake2b512()); +} + static int Levp_pbkdf2(lua_State *L, const EVP_MD *evp, size_t out_len) { unsigned char out[EVP_MAX_MD_SIZE]; @@ -234,12 +251,16 @@ static const luaL_Reg Reg[] = { { "sha384", Lsha384 }, { "sha512", Lsha512 }, { "md5", Lmd5 }, + { "blake2s256", Lblake2s256 }, + { "blake2b512", Lblake2b512 }, { "hmac_sha1", Lhmac_sha1 }, { "hmac_sha224", Lhmac_sha224 }, { "hmac_sha256", Lhmac_sha256 }, { "hmac_sha384", Lhmac_sha384 }, { "hmac_sha512", Lhmac_sha512 }, { "hmac_md5", Lhmac_md5 }, + { "hmac_blake2s256", Lhmac_blake2s256 }, + { "hmac_blake2b512", Lhmac_blake2b512 }, { "scram_Hi_sha1", Lpbkdf2_sha1 }, /* COMPAT */ { "pbkdf2_hmac_sha1", Lpbkdf2_sha1 }, { "pbkdf2_hmac_sha256", Lpbkdf2_sha256 }, diff --git a/util/hmac.lua b/util/hmac.lua index 994ea93b..ca030259 100644 --- a/util/hmac.lua +++ b/util/hmac.lua @@ -17,4 +17,6 @@ return { sha256 = hashes.hmac_sha256, sha384 = hashes.hmac_sha384, sha512 = hashes.hmac_sha512, + blake2s256 = hashes.hmac_blake2s256, + blake2b512 = hashes.hmac_blake2b512, }; -- cgit v1.2.3 From a7567a9055c959c89d00424cc9c14f64365a8a35 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 10 Sep 2020 21:58:24 +0200 Subject: util.hashes: Add SHA3 bindings --- spec/util_hashes_spec.lua | 15 +++++++++++++++ teal-src/util/hashes.d.tl | 4 ++++ util-src/hashes.c | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/spec/util_hashes_spec.lua b/spec/util_hashes_spec.lua index 51a4a79c..b3b81467 100644 --- a/spec/util_hashes_spec.lua +++ b/spec/util_hashes_spec.lua @@ -53,3 +53,18 @@ describe("PBKDF2-HMAC-SHA256", function () end); +describe("SHA-3", function () + describe("256", function () + it("works", function () + local expected = "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a" + assert.equal(expected, hashes.sha3_256("", true)); + end); + end); + describe("512", function () + it("works", function () + local expected = "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26" + assert.equal(expected, hashes.sha3_512("", true)); + end); + end); +end); + diff --git a/teal-src/util/hashes.d.tl b/teal-src/util/hashes.d.tl index 6932ab72..5c249627 100644 --- a/teal-src/util/hashes.d.tl +++ b/teal-src/util/hashes.d.tl @@ -9,6 +9,8 @@ local record lib sha384 : hash sha512 : hash md5 : hash + sha3_256 : hash + sha3_512 : hash blake2s256 : hash blake2b512 : hash hmac_sha1 : hmac @@ -17,6 +19,8 @@ local record lib hmac_sha384 :hmac hmac_sha512 : hmac hmac_md5 : hmac + hmac_sha3_256 : hmac + hmac_sha3_512 : hmac scram_Hi_sha1 : kdf pbkdf2_hmac_sha1 : kdf pbkdf2_hmac_sha256 : kdf diff --git a/util-src/hashes.c b/util-src/hashes.c index 2696f177..1b3e157d 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -120,6 +120,13 @@ static int Lblake2b512(lua_State *L) { return Levp_hash(L, EVP_blake2b512()); } +static int Lsha3_256(lua_State *L) { + return Levp_hash(L, EVP_sha3_256()); +} + +static int Lsha3_512(lua_State *L) { + return Levp_hash(L, EVP_sha3_512()); +} struct hash_desc { int (*Init)(void *); @@ -198,6 +205,14 @@ static int Lhmac_md5(lua_State *L) { return Levp_hmac(L, EVP_md5()); } +static int Lhmac_sha3_256(lua_State *L) { + return Levp_hmac(L, EVP_sha3_256()); +} + +static int Lhmac_sha3_512(lua_State *L) { + return Levp_hmac(L, EVP_sha3_512()); +} + static int Lhmac_blake2s256(lua_State *L) { return Levp_hmac(L, EVP_blake2s256()); } @@ -251,6 +266,8 @@ static const luaL_Reg Reg[] = { { "sha384", Lsha384 }, { "sha512", Lsha512 }, { "md5", Lmd5 }, + { "sha3_256", Lsha3_256 }, + { "sha3_512", Lsha3_512 }, { "blake2s256", Lblake2s256 }, { "blake2b512", Lblake2b512 }, { "hmac_sha1", Lhmac_sha1 }, @@ -259,6 +276,8 @@ static const luaL_Reg Reg[] = { { "hmac_sha384", Lhmac_sha384 }, { "hmac_sha512", Lhmac_sha512 }, { "hmac_md5", Lhmac_md5 }, + { "hmac_sha3_256", Lhmac_sha3_256 }, + { "hmac_sha3_512", Lhmac_sha3_512 }, { "hmac_blake2s256", Lhmac_blake2s256 }, { "hmac_blake2b512", Lhmac_blake2b512 }, { "scram_Hi_sha1", Lpbkdf2_sha1 }, /* COMPAT */ -- cgit v1.2.3 From 9a960217066e2659bf9473220c977c9246bcf6fa Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 24 Jun 2022 15:33:04 +0200 Subject: util.hashes: Return OpenSSL error messages on failure With luck, might contain more details than just "failed" --- util-src/hashes.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/util-src/hashes.c b/util-src/hashes.c index 1b3e157d..de802db0 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -28,6 +28,7 @@ typedef unsigned __int32 uint32_t; #include #include #include +#include #if (LUA_VERSION_NUM == 501) #define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) @@ -85,7 +86,7 @@ static int Levp_hash(lua_State *L, const EVP_MD *evp) { fail: EVP_MD_CTX_free(ctx); - return luaL_error(L, "hash function failed"); + return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); } static int Lsha1(lua_State *L) { @@ -178,7 +179,7 @@ static int Levp_hmac(lua_State *L, const EVP_MD *evp) { fail: EVP_MD_CTX_free(ctx); EVP_PKEY_free(pkey); - return luaL_error(L, "hash function failed"); + return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); } static int Lhmac_sha1(lua_State *L) { @@ -231,7 +232,7 @@ static int Levp_pbkdf2(lua_State *L, const EVP_MD *evp, size_t out_len) { const int iter = luaL_checkinteger(L, 3); if(PKCS5_PBKDF2_HMAC(pass, pass_len, salt, salt_len, iter, evp, out_len, out) == 0) { - return luaL_error(L, "PKCS5_PBKDF2_HMAC() failed"); + return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); } lua_pushlstring(L, (char *)out, out_len); -- cgit v1.2.3 From 452f9e4e2b04982a5222312602f710e28e8f41fb Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 24 Jun 2022 16:12:11 +0200 Subject: util.hashes: Remove unused struct Unused since 9f1c5ae8d70b --- util-src/hashes.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/util-src/hashes.c b/util-src/hashes.c index de802db0..b302d8eb 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -129,14 +129,6 @@ static int Lsha3_512(lua_State *L) { return Levp_hash(L, EVP_sha3_512()); } -struct hash_desc { - int (*Init)(void *); - int (*Update)(void *, const void *, size_t); - int (*Final)(unsigned char *, void *); - size_t digestLength; - void *ctx, *ctxo; -}; - static int Levp_hmac(lua_State *L, const EVP_MD *evp) { unsigned char hash[EVP_MAX_MD_SIZE], result[EVP_MAX_MD_SIZE * 2]; size_t key_len, msg_len; -- cgit v1.2.3 From 8f3d837cd253a226a30fd05529ff7b8bf0d535fa Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 24 Jun 2022 16:49:03 +0200 Subject: util.hashes: Remove unused constants --- util-src/hashes.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/util-src/hashes.c b/util-src/hashes.c index b302d8eb..4ad786ae 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -34,9 +34,6 @@ typedef unsigned __int32 uint32_t; #define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) #endif -#define HMAC_IPAD 0x36363636 -#define HMAC_OPAD 0x5c5c5c5c - static const char *hex_tab = "0123456789abcdef"; static void toHex(const unsigned char *in, int length, unsigned char *out) { int i; -- cgit v1.2.3 From b1c7b93139f52acd11ac22494b06a3678135ffdc Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 24 Jun 2022 16:59:54 +0200 Subject: util.hashes: Revert to HMAC() convenience function Reverts some of 1e41dd0f8353 Seems HMAC() isn't deprecated after all? Must have been at some point according to #1589 Twice as fast for some reason. --- util-src/hashes.c | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/util-src/hashes.c b/util-src/hashes.c index 4ad786ae..fbc86081 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -129,33 +129,15 @@ static int Lsha3_512(lua_State *L) { static int Levp_hmac(lua_State *L, const EVP_MD *evp) { unsigned char hash[EVP_MAX_MD_SIZE], result[EVP_MAX_MD_SIZE * 2]; size_t key_len, msg_len; - size_t out_len = EVP_MAX_MD_SIZE; + unsigned int out_len = EVP_MAX_MD_SIZE; const char *key = luaL_checklstring(L, 1, &key_len); const char *msg = luaL_checklstring(L, 2, &msg_len); const int hex_out = lua_toboolean(L, 3); - EVP_PKEY *pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, (unsigned char *)key, key_len); - EVP_MD_CTX *ctx = EVP_MD_CTX_new(); - - if(ctx == NULL || pkey == NULL) { - goto fail; - } - - if(!EVP_DigestSignInit(ctx, NULL, evp, NULL, pkey)) { - goto fail; - } - - if(!EVP_DigestSignUpdate(ctx, msg, msg_len)) { - goto fail; - } - - if(!EVP_DigestSignFinal(ctx, hash, &out_len)) { + if(HMAC(evp, key, key_len, (const unsigned char*)msg, msg_len, (unsigned char*)hash, &out_len) == NULL) { goto fail; } - EVP_MD_CTX_free(ctx); - EVP_PKEY_free(pkey); - if(hex_out) { toHex(hash, out_len, result); lua_pushlstring(L, (char *)result, out_len * 2); @@ -166,8 +148,6 @@ static int Levp_hmac(lua_State *L, const EVP_MD *evp) { return 1; fail: - EVP_MD_CTX_free(ctx); - EVP_PKEY_free(pkey); return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); } -- cgit v1.2.3 From 5393bff075e8ff9edefe0752dffbc3e15a8bc773 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 30 Jun 2022 17:03:50 +0200 Subject: util.dependencies: Deprecate support for Lua 5.1, this is your final warning --- util/dependencies.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/dependencies.lua b/util/dependencies.lua index d7836404..7ecbf797 100644 --- a/util/dependencies.lua +++ b/util/dependencies.lua @@ -155,7 +155,7 @@ local function log_warnings() if _VERSION > "Lua 5.4" then prosody.log("warn", "Support for %s is experimental, please report any issues", _VERSION); elseif _VERSION < "Lua 5.2" then - prosody.log("warn", "%s has several issues and support is being phased out, consider upgrading", _VERSION); + prosody.log("warn", "%s support is deprecated, upgrade as soon as possible", _VERSION); end local ssl = softreq"ssl"; if ssl then -- cgit v1.2.3 From 084b949eca52eac6cbd43db7696b12b36d4fc423 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 30 Jun 2022 17:00:35 +0200 Subject: configure: No longer accept Lua 5.1 --- configure | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/configure b/configure index 784d7339..93d2fe7d 100755 --- a/configure +++ b/configure @@ -45,7 +45,7 @@ Configure $APP_NAME prior to building. Default is \$PREFIX/lib --datadir=DIR Location where the server data should be stored. Default is \$PREFIX/var/lib/$APP_DIRNAME ---lua-version=VERSION Use specific Lua version: 5.1, 5.2, or 5.3 +--lua-version=VERSION Use specific Lua version: 5.2, 5.3, or 5.4 Default is auto-detected. --lua-suffix=SUFFIX Versioning suffix to use in Lua filenames. Default is "$LUA_SUFFIX" (lua$LUA_SUFFIX...) @@ -173,7 +173,8 @@ do --lua-version|--with-lua-version) [ -n "$value" ] || die "Missing value in flag $key." LUA_VERSION="$value" - [ "$LUA_VERSION" = "5.1" ] || [ "$LUA_VERSION" = "5.2" ] || [ "$LUA_VERSION" = "5.3" ] || [ "$LUA_VERSION" = "5.4" ] || die "Invalid Lua version in flag $key." + [ "$LUA_VERSION" != "5.1" ] || die "Lua 5.1 is no longer supported" + [ "$LUA_VERSION" = "5.2" ] || [ "$LUA_VERSION" = "5.3" ] || [ "$LUA_VERSION" = "5.4" ] || die "Invalid Lua version in flag $key." LUA_VERSION_SET=yes ;; --with-lua) @@ -275,11 +276,11 @@ if [ "$OSPRESET_SET" = "yes" ]; then CFLAGS="$CFLAGS -ggdb" fi if [ "$OSPRESET" = "freebsd" ] || [ "$OSPRESET" = "openbsd" ]; then - LUA_INCDIR="/usr/local/include/lua51" + LUA_INCDIR="/usr/local/include/lua52" LUA_INCDIR_SET=yes CFLAGS="-Wall -fPIC -I/usr/local/include" LDFLAGS="-I/usr/local/include -L/usr/local/lib -shared" - LUA_SUFFIX="51" + LUA_SUFFIX="52" LUA_SUFFIX_SET=yes LUA_DIR=/usr/local LUA_DIR_SET=yes @@ -291,16 +292,16 @@ if [ "$OSPRESET_SET" = "yes" ]; then LUA_INCDIR_SET="yes" fi if [ "$OSPRESET" = "netbsd" ]; then - LUA_INCDIR="/usr/pkg/include/lua-5.1" + LUA_INCDIR="/usr/pkg/include/lua-5.2" LUA_INCDIR_SET=yes - LUA_LIBDIR="/usr/pkg/lib/lua/5.1" + LUA_LIBDIR="/usr/pkg/lib/lua/5.2" LUA_LIBDIR_SET=yes CFLAGS="-Wall -fPIC -I/usr/pkg/include" LDFLAGS="-L/usr/pkg/lib -Wl,-rpath,/usr/pkg/lib -shared" fi if [ "$OSPRESET" = "pkg-config" ]; then if [ "$LUA_SUFFIX_SET" != "yes" ]; then - LUA_SUFFIX="5.1"; + LUA_SUFFIX="5.4"; LUA_SUFFIX_SET=yes fi LUA_CF="$(pkg-config --cflags-only-I lua$LUA_SUFFIX)" @@ -335,7 +336,7 @@ then fi detect_lua_version() { - detected_lua=$("$1" -e 'print(_VERSION:match(" (5%.[1234])$"))' 2> /dev/null) + detected_lua=$("$1" -e 'print(_VERSION:match(" (5%.[234])$"))' 2> /dev/null) if [ "$detected_lua" != "nil" ] then if [ "$LUA_VERSION_SET" != "yes" ] @@ -389,10 +390,7 @@ search_interpreter() { lua_interp_found=no if [ "$LUA_SUFFIX_SET" != "yes" ] then - if [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.1" ] - then - suffixes="5.1 51 -5.1 -51" - elif [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.2" ] + if [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.2" ] then suffixes="5.2 52 -5.2 -52" elif [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.3" ] @@ -402,8 +400,7 @@ then then suffixes="5.4 54 -5.4 -54" else - suffixes="5.1 51 -5.1 -51" - suffixes="$suffixes 5.2 52 -5.2 -52" + suffixes="5.2 52 -5.2 -52" suffixes="$suffixes 5.3 53 -5.3 -53" suffixes="$suffixes 5.4 54 -5.4 -54" fi -- cgit v1.2.3 From 14fceee089e48f3326a49537af7d4ef0310d6215 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 2 Jul 2022 17:29:03 +0200 Subject: tests: Remove special-casing of Lua 5.1 Part of #1600 --- spec/util_cache_spec.lua | 2 +- spec/util_format_spec.lua | 44 +++++++++++++++++++++----------------------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/spec/util_cache_spec.lua b/spec/util_cache_spec.lua index d57e25ac..7a5522b8 100644 --- a/spec/util_cache_spec.lua +++ b/spec/util_cache_spec.lua @@ -314,7 +314,7 @@ describe("util.cache", function() end); - (_VERSION=="Lua 5.1" and pending or it)(":table works", function () + it(":table works", function () local t = cache.new(3):table(); assert.is.table(t); t["a"] = "1"; diff --git a/spec/util_format_spec.lua b/spec/util_format_spec.lua index cb473b47..1016a215 100644 --- a/spec/util_format_spec.lua +++ b/spec/util_format_spec.lua @@ -333,29 +333,27 @@ describe("util.format", function() end); end); - if _VERSION > "Lua 5.1" then -- COMPAT no %a or %A in Lua 5.1 - describe("to %a", function () - it("works", function () - assert.equal("0x1.84p+6", format("%a", 97)) - assert.equal("-0x1.81c8p+13", format("%a", -12345)) - assert.equal("0x1.8p+0", format("%a", 1.5)) - assert.equal("0x1p+66", format("%a", 73786976294838206464)) - assert.equal("inf", format("%a", math.huge)) - assert.equal("0x1.fffffffcp+30", format("%a", 2147483647)) - end); - end); - - describe("to %A", function () - it("works", function () - assert.equal("0X1.84P+6", format("%A", 97)) - assert.equal("-0X1.81C8P+13", format("%A", -12345)) - assert.equal("0X1.8P+0", format("%A", 1.5)) - assert.equal("0X1P+66", format("%A", 73786976294838206464)) - assert.equal("INF", format("%A", math.huge)) - assert.equal("0X1.FFFFFFFCP+30", format("%A", 2147483647)) - end); - end); - end + describe("to %a", function () + it("works", function () + assert.equal("0x1.84p+6", format("%a", 97)) + assert.equal("-0x1.81c8p+13", format("%a", -12345)) + assert.equal("0x1.8p+0", format("%a", 1.5)) + assert.equal("0x1p+66", format("%a", 73786976294838206464)) + assert.equal("inf", format("%a", math.huge)) + assert.equal("0x1.fffffffcp+30", format("%a", 2147483647)) + end); + end); + + describe("to %A", function () + it("works", function () + assert.equal("0X1.84P+6", format("%A", 97)) + assert.equal("-0X1.81C8P+13", format("%A", -12345)) + assert.equal("0X1.8P+0", format("%A", 1.5)) + assert.equal("0X1P+66", format("%A", 73786976294838206464)) + assert.equal("INF", format("%A", math.huge)) + assert.equal("0X1.FFFFFFFCP+30", format("%A", 2147483647)) + end); + end); describe("to %e", function () it("works", function () -- cgit v1.2.3 From ada68efcc9547519a636bc46fdd7a60216a578d5 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 2 Jul 2022 17:30:34 +0200 Subject: util.dependencies: Reject Lua 5.1, Lua 5.2 or later is now required (see #1600) --- util/dependencies.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util/dependencies.lua b/util/dependencies.lua index 7ecbf797..165468c5 100644 --- a/util/dependencies.lua +++ b/util/dependencies.lua @@ -32,10 +32,10 @@ local function missingdep(name, sources, msg, err) -- luacheck: ignore err end local function check_dependencies() - if _VERSION < "Lua 5.1" then + if _VERSION < "Lua 5.2" then print "***********************************" print("Unsupported Lua version: ".._VERSION); - print("At least Lua 5.1 is required."); + print("At least Lua 5.2 is required."); print "***********************************" return false; end -- cgit v1.2.3 From 49a9a1e76a34d67cfa2be24cbb2e2a9db545f969 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 2 Jul 2022 17:31:14 +0200 Subject: util: Remove various Lua 5.1 compatibility hacks Part of #1600 --- util/bitcompat.lua | 14 -------------- util/format.lua | 3 --- util/human/io.lua | 5 +---- util/human/units.lua | 8 -------- util/prosodyctl/shell.lua | 3 +-- 5 files changed, 2 insertions(+), 31 deletions(-) diff --git a/util/bitcompat.lua b/util/bitcompat.lua index 454181af..8f227354 100644 --- a/util/bitcompat.lua +++ b/util/bitcompat.lua @@ -5,12 +5,6 @@ -- Lua 5.2 has it by default if _G.bit32 then return _G.bit32; -else - -- Lua 5.1 may have it as a standalone module that can be installed - local ok, bitop = pcall(require, "bit32") - if ok then - return bitop; - end end do @@ -21,12 +15,4 @@ do end end -do - -- Lastly, try the LuaJIT bitop library - local ok, bitop = pcall(require, "bit") - if ok then - return bitop; - end -end - error "No bit module found. See https://prosody.im/doc/depends#bitop"; diff --git a/util/format.lua b/util/format.lua index d709aada..611068d7 100644 --- a/util/format.lua +++ b/util/format.lua @@ -35,7 +35,6 @@ local control_symbols = { ["\030"] = "\226\144\158", ["\031"] = "\226\144\159", ["\127"] = "\226\144\161", }; local supports_p = pcall(string.format, "%p", ""); -- >= Lua 5.4 -local supports_a = pcall(string.format, "%a", 0.0); -- > Lua 5.1 local function format(formatstring, ...) local args = pack(...); @@ -93,8 +92,6 @@ local function format(formatstring, ...) elseif expects_positive[option] and arg < 0 then args[i] = tostring(arg); return "[%s]"; - elseif (option == "a" or option == "A") and not supports_a then - return "%x"; else return -- acceptable number end diff --git a/util/human/io.lua b/util/human/io.lua index 7d7dea97..4fce0e94 100644 --- a/util/human/io.lua +++ b/util/human/io.lua @@ -30,10 +30,7 @@ local function getline() end local function getpass() - local stty_ret, _, status_code = os.execute("stty -echo 2>/dev/null"); - if status_code then -- COMPAT w/ Lua 5.1 - stty_ret = status_code; - end + local stty_ret = os.execute("stty -echo 2>/dev/null"); if stty_ret ~= 0 then io.write("\027[08m"); -- ANSI 'hidden' text attribute end diff --git a/util/human/units.lua b/util/human/units.lua index af233e98..23e3e579 100644 --- a/util/human/units.lua +++ b/util/human/units.lua @@ -6,14 +6,6 @@ local math_max = math.max; local math_min = math.min; local unpack = table.unpack or unpack; --luacheck: ignore 113 -if math_log(10, 10) ~= 1 then - -- Lua 5.1 COMPAT - local log10 = math.log10; - function math_log(n, base) - return log10(n) / log10(base); - end -end - local large = { "k", 1000, "M", 1000000, diff --git a/util/prosodyctl/shell.lua b/util/prosodyctl/shell.lua index 8f910301..5f99bec1 100644 --- a/util/prosodyctl/shell.lua +++ b/util/prosodyctl/shell.lua @@ -80,8 +80,7 @@ local function start(arg) --luacheck: ignore 212/arg if arg[1] then if arg[2] then -- prosodyctl shell module reload foo bar.com --> module:reload("foo", "bar.com") - -- COMPAT Lua 5.1 doesn't have the separator argument to string.rep - arg[1] = string.format("%s:%s("..string.rep("%q, ", #arg-2):sub(1, -3)..")", unpack(arg)); + arg[1] = string.format("%s:%s("..string.rep("%q", #arg-2,", ")..")", unpack(arg)); end client.events.add_handler("connected", function() -- 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(-) 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 dff4beae0278ae5b98c52b7e1162ceb318236256 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 1 Jul 2022 21:21:21 +0200 Subject: util-src: Remove Lua 5.1 compat macros Part of #1600 --- util-src/crand.c | 2 -- util-src/encodings.c | 5 ----- util-src/hashes.c | 5 ----- util-src/net.c | 5 ----- util-src/poll.c | 5 ----- util-src/pposix.c | 5 ----- util-src/ringbuffer.c | 2 -- util-src/signal.c | 5 ----- util-src/strbitop.c | 3 --- util-src/struct.c | 8 +------- util-src/table.c | 8 +------- util-src/windows.c | 5 ----- 12 files changed, 2 insertions(+), 56 deletions(-) diff --git a/util-src/crand.c b/util-src/crand.c index e4104787..c6f0a3ba 100644 --- a/util-src/crand.c +++ b/util-src/crand.c @@ -124,9 +124,7 @@ static int Lrandom(lua_State *L) { } int luaopen_util_crand(lua_State *L) { -#if (LUA_VERSION_NUM > 501) luaL_checkversion(L); -#endif lua_createtable(L, 0, 2); lua_pushcfunction(L, Lrandom); diff --git a/util-src/encodings.c b/util-src/encodings.c index 72264da8..157d8526 100644 --- a/util-src/encodings.c +++ b/util-src/encodings.c @@ -21,9 +21,6 @@ #include "lua.h" #include "lauxlib.h" -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif @@ -616,9 +613,7 @@ static const luaL_Reg Reg_idna[] = { /***************** end *****************/ LUALIB_API int luaopen_util_encodings(lua_State *L) { -#if (LUA_VERSION_NUM > 501) luaL_checkversion(L); -#endif #ifdef USE_STRINGPREP_ICU init_icu(); #endif diff --git a/util-src/hashes.c b/util-src/hashes.c index fbc86081..5d67ca63 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -30,9 +30,6 @@ typedef unsigned __int32 uint32_t; #include #include -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif static const char *hex_tab = "0123456789abcdef"; static void toHex(const unsigned char *in, int length, unsigned char *out) { @@ -258,9 +255,7 @@ static const luaL_Reg Reg[] = { }; LUALIB_API int luaopen_util_hashes(lua_State *L) { -#if (LUA_VERSION_NUM > 501) luaL_checkversion(L); -#endif lua_newtable(L); luaL_setfuncs(L, Reg, 0); lua_pushliteral(L, "-3.14"); diff --git a/util-src/net.c b/util-src/net.c index d786e885..96b50e7b 100644 --- a/util-src/net.c +++ b/util-src/net.c @@ -30,9 +30,6 @@ #include #include -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif @@ -193,9 +190,7 @@ static int lc_ntop(lua_State *L) { } int luaopen_util_net(lua_State *L) { -#if (LUA_VERSION_NUM > 501) luaL_checkversion(L); -#endif luaL_Reg exports[] = { { "local_addresses", lc_local_addresses }, { "pton", lc_pton }, diff --git a/util-src/poll.c b/util-src/poll.c index 81caa953..d7c1f9e1 100644 --- a/util-src/poll.c +++ b/util-src/poll.c @@ -44,9 +44,6 @@ #define STATE_MT "util.poll<" POLL_BACKEND ">" -#if (LUA_VERSION_NUM == 501) -#define luaL_setmetatable(L, tname) luaL_getmetatable(L, tname); lua_setmetatable(L, -2) -#endif #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif @@ -564,9 +561,7 @@ static int Lnew(lua_State *L) { * Open library */ int luaopen_util_poll(lua_State *L) { -#if (LUA_VERSION_NUM > 501) luaL_checkversion(L); -#endif luaL_newmetatable(L, STATE_MT); { diff --git a/util-src/pposix.c b/util-src/pposix.c index a8e0720f..aac27d35 100644 --- a/util-src/pposix.c +++ b/util-src/pposix.c @@ -58,9 +58,6 @@ #include "lualib.h" #include "lauxlib.h" -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif #if (LUA_VERSION_NUM < 503) #define lua_isinteger(L, n) lua_isnumber(L, n) #endif @@ -829,9 +826,7 @@ static int lc_isatty(lua_State *L) { /* Register functions */ int luaopen_util_pposix(lua_State *L) { -#if (LUA_VERSION_NUM > 501) luaL_checkversion(L); -#endif luaL_Reg exports[] = { { "abort", lc_abort }, diff --git a/util-src/ringbuffer.c b/util-src/ringbuffer.c index 0f250c12..95c62de9 100644 --- a/util-src/ringbuffer.c +++ b/util-src/ringbuffer.c @@ -314,9 +314,7 @@ static int rb_new(lua_State *L) { } int luaopen_util_ringbuffer(lua_State *L) { -#if (LUA_VERSION_NUM > 501) luaL_checkversion(L); -#endif if(luaL_newmetatable(L, "ringbuffer_mt")) { lua_pushcfunction(L, rb_tostring); diff --git a/util-src/signal.c b/util-src/signal.c index 1a398fa0..b5ba16a9 100644 --- a/util-src/signal.c +++ b/util-src/signal.c @@ -36,9 +36,6 @@ #include "lua.h" #include "lauxlib.h" -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif #if (LUA_VERSION_NUM < 503) #define lua_isinteger(L, n) lua_isnumber(L, n) #endif @@ -381,9 +378,7 @@ static const struct luaL_Reg lsignal_lib[] = { }; int luaopen_util_signal(lua_State *L) { -#if (LUA_VERSION_NUM > 501) luaL_checkversion(L); -#endif int i = 0; /* add the library */ diff --git a/util-src/strbitop.c b/util-src/strbitop.c index fda8917a..722f5a2d 100644 --- a/util-src/strbitop.c +++ b/util-src/strbitop.c @@ -8,9 +8,6 @@ #include #include -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif /* TODO Deduplicate code somehow */ diff --git a/util-src/struct.c b/util-src/struct.c index e80df4e6..6267358c 100644 --- a/util-src/struct.c +++ b/util-src/struct.c @@ -36,12 +36,6 @@ #include "lauxlib.h" -#if (LUA_VERSION_NUM >= 502) - -#define luaL_register(L,n,f) luaL_newlib(L,f) - -#endif - /* basic integer type */ #if !defined(STRUCT_INT) @@ -392,7 +386,7 @@ static const struct luaL_Reg thislib[] = { LUALIB_API int luaopen_util_struct (lua_State *L); LUALIB_API int luaopen_util_struct (lua_State *L) { - luaL_register(L, "struct", thislib); + luaL_newlib(L, thislib); return 1; } diff --git a/util-src/table.c b/util-src/table.c index 4bbceedb..ade019c4 100644 --- a/util-src/table.c +++ b/util-src/table.c @@ -6,10 +6,6 @@ #define LUA_MAXINTEGER PTRDIFF_MAX #endif -#if (LUA_VERSION_NUM > 501) -#define lua_equal(L, A, B) lua_compare(L, A, B, LUA_OPEQ) -#endif - static int Lcreate_table(lua_State *L) { lua_createtable(L, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2)); return 1; @@ -47,7 +43,7 @@ static int Lmove (lua_State *L) { n = e - f + 1; /* number of elements to move */ luaL_argcheck(L, t <= LUA_MAXINTEGER - n + 1, 4, "destination wrap around"); - if (t > e || t <= f || (tt != 1 && !lua_equal(L, 1, tt))) { + if (t > e || t <= f || (tt != 1 && !lua_compare(L, 1, tt, LUA_OPEQ))) { for (i = 0; i < n; i++) { lua_rawgeti(L, 1, f + i); lua_rawseti(L, tt, t + i); @@ -65,9 +61,7 @@ static int Lmove (lua_State *L) { } int luaopen_util_table(lua_State *L) { -#if (LUA_VERSION_NUM > 501) luaL_checkversion(L); -#endif lua_createtable(L, 0, 2); lua_pushcfunction(L, Lcreate_table); lua_setfield(L, -2, "create"); diff --git a/util-src/windows.c b/util-src/windows.c index 57af79d5..2adb85f5 100644 --- a/util-src/windows.c +++ b/util-src/windows.c @@ -19,9 +19,6 @@ #include "lua.h" #include "lauxlib.h" -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif @@ -106,9 +103,7 @@ static const luaL_Reg Reg[] = { }; LUALIB_API int luaopen_util_windows(lua_State *L) { -#if (LUA_VERSION_NUM > 501) luaL_checkversion(L); -#endif lua_newtable(L); luaL_setfuncs(L, Reg, 0); lua_pushliteral(L, "-3.14"); -- cgit v1.2.3 From bcc6efb2aa55b5f028a50773ffff50b60622ab71 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 5 Jul 2022 14:18:32 +0200 Subject: util.envload: Remove Lua 5.1 method Part of #1600 Is this module even needed anymore? --- util/envload.lua | 39 ++++++++++----------------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/util/envload.lua b/util/envload.lua index 6182a1f9..cf45b702 100644 --- a/util/envload.lua +++ b/util/envload.lua @@ -6,38 +6,19 @@ -- -- luacheck: ignore 113/setfenv 113/loadstring -local load, loadstring, setfenv = load, loadstring, setfenv; +local load = load; local io_open = io.open; -local envload; -local envloadfile; -if setfenv then - function envload(code, source, env) - local f, err = loadstring(code, source); - if f and env then setfenv(f, env); end - return f, err; - end - - function envloadfile(file, env) - local fh, err, errno = io_open(file); - if not fh then return fh, err, errno; end - local f, err = load(function () return fh:read(2048); end, "@"..file); - fh:close(); - if f and env then setfenv(f, env); end - return f, err; - end -else - function envload(code, source, env) - return load(code, source, nil, env); - end +local function envload(code, source, env) + return load(code, source, nil, env); +end - function envloadfile(file, env) - local fh, err, errno = io_open(file); - if not fh then return fh, err, errno; end - local f, err = load(fh:lines(2048), "@"..file, nil, env); - fh:close(); - return f, err; - end +local function envloadfile(file, env) + local fh, err, errno = io_open(file); + if not fh then return fh, err, errno; end + local f, err = load(fh:lines(2048), "@" .. file, nil, env); + fh:close(); + return f, err; end return { envload = envload, envloadfile = envloadfile }; -- cgit v1.2.3 From c8a49ad14411a858ceb3cc4c4df091d31cb90ed7 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 5 Jul 2022 14:59:47 +0200 Subject: CHANGES: Lua 5.1 support removed (closes #1600) --- CHANGES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index 213be9da..e1f0854a 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,10 @@ TRUNK - Advertise supported SASL Channel-Binding types (XEP-0440) +## Removed + +- Lua 5.1 support + 0.12.0 ====== -- cgit v1.2.3 From 8ee6441fc524b6ffc65fe327485cb8500b7a63f9 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 2 Jul 2022 17:27:39 +0200 Subject: executables: Reject Lua 5.1 early Prevents attempting to load libraries that may no longer be found and crashing with a traceback. Platforms like Debian where multiple Lua versions can be installed at the same time and 'lua' pointing to one of the installed interpreters via symlinks, there's the possibility that prosody/prosodyctl may be invoked with Lua 5.1, which will no longer have any of the rest of Prosody libraries available to be require(), and thus would immediately fail with an unfriendly traceback. Checking and aborting early with a friendlier message and reference to more information is better. Part of #1600 --- prosody | 6 ++++++ prosodyctl | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/prosody b/prosody index ac623cef..eaac3dff 100755 --- a/prosody +++ b/prosody @@ -44,6 +44,12 @@ if CFG_DATADIR then end +-- Check before first require, to preempt the probable failure +if _VERSION < "Lua 5.2" then + io.stderr:write("Prosody is no longer compatible with Lua 5.1\n") + io.stderr:write("See https://prosody.im/doc/depends#lua for more information\n") + return os.exit(1); +end local startup = require "util.startup"; local async = require "util.async"; diff --git a/prosodyctl b/prosodyctl index 4e4099d5..e7bff982 100755 --- a/prosodyctl +++ b/prosodyctl @@ -44,6 +44,13 @@ end ----------- +-- Check before first require, to preempt the probable failure +if _VERSION < "Lua 5.2" then + io.stderr:write("Prosody is no longer compatible with Lua 5.1\n") + io.stderr:write("See https://prosody.im/doc/depends#lua for more information\n") + return os.exit(1); +end + local startup = require "util.startup"; startup.prosodyctl(); -- cgit v1.2.3 From 561c588fd5e2624f8c2f3e6bfacad562a728f60f Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 10 Jul 2022 16:10:29 +0200 Subject: luacheck: Set expected globals to Lua 5.4 + compat Requires luacheck 0.25.0 --- .luacheckrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.luacheckrc b/.luacheckrc index d08dfa70..6c948b17 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -2,7 +2,7 @@ cache = true codes = true ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV", "431/log", "214", "581" } -std = "lua53c" +std = "lua54c" max_line_length = 150 read_globals = { -- 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 --- core/moduleapi.lua | 2 +- net/resolvers/manual.lua | 2 +- 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 +- prosodyctl | 2 +- spec/core_storagemanager_spec.lua | 2 +- tools/modtrace.lua | 2 +- util/format.lua | 2 +- util/human/units.lua | 2 +- util/import.lua | 2 +- util/iterators.lua | 2 +- util/multitable.lua | 2 +- util/openmetrics.lua | 2 +- util/promise.lua | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index 870a6a50..98474b27 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -27,7 +27,7 @@ local ipairs, pairs, select = ipairs, pairs, select; local tonumber, tostring = tonumber, tostring; local require = require; local pack = table.pack or require "util.table".pack; -- table.pack is only in 5.2 -local unpack = table.unpack or unpack; --luacheck: ignore 113 -- renamed in 5.2 +local unpack = table.unpack; local prosody = prosody; local hosts = prosody.hosts; diff --git a/net/resolvers/manual.lua b/net/resolvers/manual.lua index dbc40256..c766a11f 100644 --- a/net/resolvers/manual.lua +++ b/net/resolvers/manual.lua @@ -1,6 +1,6 @@ local methods = {}; local resolver_mt = { __index = methods }; -local unpack = table.unpack or unpack; -- luacheck: ignore 113 +local unpack = table.unpack; -- Find the next target to connect to, and -- pass it to cb() 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; diff --git a/prosodyctl b/prosodyctl index e7bff982..cb55858a 100755 --- a/prosodyctl +++ b/prosodyctl @@ -580,7 +580,7 @@ function commands.reload(arg) end -- ejabberdctl compatibility -local unpack = table.unpack or unpack; -- luacheck: ignore 113 +local unpack = table.unpack; function commands.register(arg) local user, host, password = unpack(arg); diff --git a/spec/core_storagemanager_spec.lua b/spec/core_storagemanager_spec.lua index e8d9fc97..2170420d 100644 --- a/spec/core_storagemanager_spec.lua +++ b/spec/core_storagemanager_spec.lua @@ -1,4 +1,4 @@ -local unpack = table.unpack or unpack; -- luacheck: ignore 113 +local unpack = table.unpack; local server = require "net.server_select"; package.loaded["net.server"] = server; diff --git a/tools/modtrace.lua b/tools/modtrace.lua index 45fa9f6a..f31ce2d1 100644 --- a/tools/modtrace.lua +++ b/tools/modtrace.lua @@ -10,7 +10,7 @@ local t_pack = require "util.table".pack; local serialize = require "util.serialization".serialize; -local unpack = table.unpack or unpack; --luacheck: ignore 113 +local unpack = table.unpack; local set = require "util.set"; local serialize_cfg = { diff --git a/util/format.lua b/util/format.lua index 611068d7..35694271 100644 --- a/util/format.lua +++ b/util/format.lua @@ -6,7 +6,7 @@ -- Provides some protection from e.g. CAPEC-135, CWE-117, CWE-134, CWE-93 local tostring = tostring; -local unpack = table.unpack or unpack; -- luacheck: ignore 113/unpack +local unpack = table.unpack; local pack = require "util.table".pack; -- TODO table.pack in 5.2+ local valid_utf8 = require "util.encodings".utf8.valid; local type = type; diff --git a/util/human/units.lua b/util/human/units.lua index 23e3e579..329c8518 100644 --- a/util/human/units.lua +++ b/util/human/units.lua @@ -4,7 +4,7 @@ local math_floor = math.floor; local math_log = math.log; local math_max = math.max; local math_min = math.min; -local unpack = table.unpack or unpack; --luacheck: ignore 113 +local unpack = table.unpack; local large = { "k", 1000, diff --git a/util/import.lua b/util/import.lua index 1007bc0a..0892e9b1 100644 --- a/util/import.lua +++ b/util/import.lua @@ -8,7 +8,7 @@ -local unpack = table.unpack or unpack; --luacheck: ignore 113 +local unpack = table.unpack; local t_insert = table.insert; function _G.import(module, ...) local m = package.loaded[module] or require(module); diff --git a/util/iterators.lua b/util/iterators.lua index c03c2fd6..e9f7c1b2 100644 --- a/util/iterators.lua +++ b/util/iterators.lua @@ -12,7 +12,7 @@ local it = {}; local t_insert = table.insert; local next = next; -local unpack = table.unpack or unpack; --luacheck: ignore 113 +local unpack = table.unpack; local pack = table.pack or require "util.table".pack; local type = type; local table, setmetatable = table, setmetatable; diff --git a/util/multitable.lua b/util/multitable.lua index 4f2cd972..0c292b45 100644 --- a/util/multitable.lua +++ b/util/multitable.lua @@ -9,7 +9,7 @@ local select = select; local t_insert = table.insert; local pairs, next, type = pairs, next, type; -local unpack = table.unpack or unpack; --luacheck: ignore 113 +local unpack = table.unpack; local _ENV = nil; -- luacheck: std none diff --git a/util/openmetrics.lua b/util/openmetrics.lua index 634f9de1..f451e504 100644 --- a/util/openmetrics.lua +++ b/util/openmetrics.lua @@ -26,7 +26,7 @@ local log = require "util.logger".init("util.openmetrics"); local new_multitable = require "util.multitable".new; local iter_multitable = require "util.multitable".iter; local t_concat, t_insert = table.concat, table.insert; -local t_pack, t_unpack = require "util.table".pack, table.unpack or unpack; --luacheck: ignore 113/unpack +local t_pack, t_unpack = require "util.table".pack, table.unpack; -- BEGIN of Utility: "metric proxy" -- This allows to wrap a MetricFamily in a proxy which only provides the diff --git a/util/promise.lua b/util/promise.lua index c4e166ed..1762d501 100644 --- a/util/promise.lua +++ b/util/promise.lua @@ -2,7 +2,7 @@ local promise_methods = {}; local promise_mt = { __name = "promise", __index = promise_methods }; local xpcall = require "util.xpcall".xpcall; -local unpack = table.unpack or unpack; --luacheck: ignore 113 +local unpack = table.unpack; function promise_mt:__tostring() return "promise (" .. (self._state or "invalid") .. ")"; -- cgit v1.2.3 From f8e73eba98a73e5d3dd14f73d7ce66e5503efbb4 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 11 Jul 2022 19:15:24 +0200 Subject: compat: Use table.pack (there since Lua 5.2) over our util.table Added in d278a770eddc avoid having to deal with its absence in Lua 5.1. No longer needed when Lua 5.1 support is dropped. --- core/moduleapi.lua | 2 +- tools/modtrace.lua | 2 +- util/format.lua | 2 +- util/iterators.lua | 2 +- util/openmetrics.lua | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index 98474b27..36d82193 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -26,7 +26,7 @@ local error, setmetatable, type = error, setmetatable, type; local ipairs, pairs, select = ipairs, pairs, select; local tonumber, tostring = tonumber, tostring; local require = require; -local pack = table.pack or require "util.table".pack; -- table.pack is only in 5.2 +local pack = table.pack; local unpack = table.unpack; local prosody = prosody; diff --git a/tools/modtrace.lua b/tools/modtrace.lua index f31ce2d1..f1927077 100644 --- a/tools/modtrace.lua +++ b/tools/modtrace.lua @@ -8,7 +8,7 @@ -- local dbuffer = require "tools.modtrace".trace("util.dbuffer"); -- -local t_pack = require "util.table".pack; +local t_pack = table.pack; local serialize = require "util.serialization".serialize; local unpack = table.unpack; local set = require "util.set"; diff --git a/util/format.lua b/util/format.lua index 35694271..203bdeab 100644 --- a/util/format.lua +++ b/util/format.lua @@ -7,7 +7,7 @@ local tostring = tostring; local unpack = table.unpack; -local pack = require "util.table".pack; -- TODO table.pack in 5.2+ +local pack = table.pack; local valid_utf8 = require "util.encodings".utf8.valid; local type = type; local dump = require "util.serialization".new("debug"); diff --git a/util/iterators.lua b/util/iterators.lua index e9f7c1b2..4529697a 100644 --- a/util/iterators.lua +++ b/util/iterators.lua @@ -13,7 +13,7 @@ local it = {}; local t_insert = table.insert; local next = next; local unpack = table.unpack; -local pack = table.pack or require "util.table".pack; +local pack = table.pack; local type = type; local table, setmetatable = table, setmetatable; diff --git a/util/openmetrics.lua b/util/openmetrics.lua index f451e504..8c77ffcd 100644 --- a/util/openmetrics.lua +++ b/util/openmetrics.lua @@ -26,7 +26,7 @@ local log = require "util.logger".init("util.openmetrics"); local new_multitable = require "util.multitable".new; local iter_multitable = require "util.multitable".iter; local t_concat, t_insert = table.concat, table.insert; -local t_pack, t_unpack = require "util.table".pack, table.unpack; +local t_pack, t_unpack = table.pack, table.unpack; -- BEGIN of Utility: "metric proxy" -- This allows to wrap a MetricFamily in a proxy which only provides the -- cgit v1.2.3 From dabdfc91be04257db5a5aae48cb03bcbfb274425 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 11 Jul 2022 20:02:10 +0200 Subject: util.table: Fix inaccurate comment Probably a duplicate of the comment next to Lmove, recorded by mistake Lpack can probably be removed at some point in the near future once we are confident it is not used anywhere. --- util-src/table.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util-src/table.c b/util-src/table.c index ade019c4..1cbb276d 100644 --- a/util-src/table.c +++ b/util-src/table.c @@ -11,7 +11,7 @@ static int Lcreate_table(lua_State *L) { return 1; } -/* COMPAT: w/ Lua pre-5.4 */ +/* COMPAT: w/ Lua pre-5.2 */ static int Lpack(lua_State *L) { unsigned int n_args = lua_gettop(L); lua_createtable(L, n_args, 1); -- 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 --- CHANGES | 1 + doc/doap.xml | 1 + net/server_epoll.lua | 8 ++++++++ plugins/mod_saslauth.lua | 15 +++++++++++++++ 4 files changed, 25 insertions(+) diff --git a/CHANGES b/CHANGES index e1f0854a..48fe37d5 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,7 @@ TRUNK ### Security and authentication - Advertise supported SASL Channel-Binding types (XEP-0440) +- Implement RFC 9266 'tls-exporter' channel binding with TLS 1.3 ## Removed diff --git a/doc/doap.xml b/doc/doap.xml index e767115b..530b9c2c 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -60,6 +60,7 @@ + diff --git a/net/server_epoll.lua b/net/server_epoll.lua index a546e1e3..b269bd9c 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -649,6 +649,14 @@ function interface:ssl_peerfinished() return sock:getpeerfinished(); end +function interface:ssl_exportkeyingmaterial(label, len, context) + local sock = self.conn; + if sock.exportkeyingmaterial then + return sock:exportkeyingmaterial(label, len, context); + end +end + + function interface:starttls(tls_ctx) if tls_ctx then self.tls_ctx = tls_ctx; end self.starttls = false; 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 c1a166daa79ec4fadd2083dd7097216068f74a28 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 27 Jul 2022 00:32:04 +0200 Subject: util.sasl.scram: Add 'tls-exporter' as recognised channel binding method The last missing piece of #1760, otherwise SCRAM-SHA-*-PLUS is not actually advertised. --- util/sasl/scram.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/sasl/scram.lua b/util/sasl/scram.lua index 37abf4a4..4606d1fd 100644 --- a/util/sasl/scram.lua +++ b/util/sasl/scram.lua @@ -240,7 +240,7 @@ local function init(registerMechanism) -- register channel binding equivalent registerMechanism("SCRAM-"..hash_name.."-PLUS", {"plain", "scram_"..(hashprep(hash_name))}, - scram_gen(hash_name:lower(), hash, hmac_hash, get_auth_db, true), {"tls-unique"}); + scram_gen(hash_name:lower(), hash, hmac_hash, get_auth_db, true), {"tls-unique", "tls-exporter"}); end registerSCRAMMechanism("SHA-1", hashes.sha1, hashes.hmac_sha1, hashes.pbkdf2_hmac_sha1); -- cgit v1.2.3 From 79f4868b63ff6385bbc2290ba31361bcb7ce63b5 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Tue, 2 Aug 2022 16:08:43 +0200 Subject: net.resolvers.basic: Add opt-out argument for DNSSEC security status This makes explicit which lookups can accept an unsigned response. Insecure (unsigned, as before DNSSEC) A and AAAA records can be used as security would come from TLS, but an insecure TLSA record is worthless. --- net/resolvers/basic.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/net/resolvers/basic.lua b/net/resolvers/basic.lua index 15338ff4..e58165ba 100644 --- a/net/resolvers/basic.lua +++ b/net/resolvers/basic.lua @@ -10,7 +10,7 @@ local resolver_mt = { __index = methods }; -- FIXME RFC 6724 -local function do_dns_lookup(self, dns_resolver, record_type, name) +local function do_dns_lookup(self, dns_resolver, record_type, name, allow_insecure) return promise.new(function (resolve, reject) local ipv = (record_type == "A" and "4") or (record_type == "AAAA" and "6") or nil; if ipv and self.extra["use_ipv"..ipv] == false then @@ -23,6 +23,8 @@ local function do_dns_lookup(self, dns_resolver, record_type, name) return reject(err); elseif answer.bogus then return reject(("Validation error in %s lookup"):format(record_type)); + elseif not (answer.secure or allow_insecure) then + return reject(("Insecure response in %s lookup"):format(record_type)); elseif answer.status and #answer == 0 then return reject(("%s in %s lookup"):format(answer.status, record_type)); end @@ -78,8 +80,8 @@ function methods:next(cb) local dns_resolver = adns.resolver(); local dns_lookups = { - ipv4 = do_dns_lookup(self, dns_resolver, "A", self.hostname); - ipv6 = do_dns_lookup(self, dns_resolver, "AAAA", self.hostname); + ipv4 = do_dns_lookup(self, dns_resolver, "A", self.hostname, true); + ipv6 = do_dns_lookup(self, dns_resolver, "AAAA", self.hostname, true); tlsa = do_dns_lookup(self, dns_resolver, "TLSA", ("_%d._%s.%s"):format(self.port, self.conn_type, self.hostname)); }; -- 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(-) 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(-) 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 93830ecf40f487100170e22782d1966aa325067c Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 5 Aug 2022 16:54:15 +0200 Subject: various: Update IETF RFC URLs for tools.ietf.org transition See https://www.ietf.org/blog/finalizing-ietf-tools-transition/ Already done in various other places. --- certs/openssl.cnf | 2 +- net/dns.lua | 4 ++-- spec/util_uuid_spec.lua | 2 +- util/x509.lua | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/certs/openssl.cnf b/certs/openssl.cnf index ee17b1cf..8a05ecc3 100644 --- a/certs/openssl.cnf +++ b/certs/openssl.cnf @@ -46,7 +46,7 @@ subjectAltName = @subject_alternative_name [ subject_alternative_name ] -# See http://tools.ietf.org/html/rfc6120#section-13.7.1.2 for more info. +# See https://www.rfc-editor.org/rfc/rfc6120.html#section-13.7.1.2 for more info. DNS.0 = example.com otherName.0 = xmppAddr;FORMAT:UTF8,UTF8:example.com diff --git a/net/dns.lua b/net/dns.lua index a9846e86..e6179637 100644 --- a/net/dns.lua +++ b/net/dns.lua @@ -8,8 +8,8 @@ -- todo: cache results of encodeName --- reference: http://tools.ietf.org/html/rfc1035 --- reference: http://tools.ietf.org/html/rfc1876 (LOC) +-- reference: https://www.rfc-editor.org/rfc/rfc1035.html +-- reference: https://www.rfc-editor.org/rfc/rfc1876.html (LOC) local socket = require "socket"; diff --git a/spec/util_uuid_spec.lua b/spec/util_uuid_spec.lua index 95ae0a20..46400d00 100644 --- a/spec/util_uuid_spec.lua +++ b/spec/util_uuid_spec.lua @@ -5,7 +5,7 @@ local uuid = require "util.uuid"; describe("util.uuid", function() describe("#generate()", function() it("should work follow the UUID pattern", function() - -- https://tools.ietf.org/html/rfc4122#section-4.4 + -- https://www.rfc-editor.org/rfc/rfc4122.html#section-4.4 local pattern = "^" .. table.concat({ string.rep("%x", 8), diff --git a/util/x509.lua b/util/x509.lua index 76b50076..51ca3c96 100644 --- a/util/x509.lua +++ b/util/x509.lua @@ -11,12 +11,12 @@ -- IDN libraries complicate that. --- [TLS-CERTS] - http://tools.ietf.org/html/rfc6125 --- [XMPP-CORE] - http://tools.ietf.org/html/rfc6120 --- [SRV-ID] - http://tools.ietf.org/html/rfc4985 --- [IDNA] - http://tools.ietf.org/html/rfc5890 --- [LDAP] - http://tools.ietf.org/html/rfc4519 --- [PKIX] - http://tools.ietf.org/html/rfc5280 +-- [TLS-CERTS] - https://www.rfc-editor.org/rfc/rfc6125.html +-- [XMPP-CORE] - https://www.rfc-editor.org/rfc/rfc6120.html +-- [SRV-ID] - https://www.rfc-editor.org/rfc/rfc4985.html +-- [IDNA] - https://www.rfc-editor.org/rfc/rfc5890.html +-- [LDAP] - https://www.rfc-editor.org/rfc/rfc4519.html +-- [PKIX] - https://www.rfc-editor.org/rfc/rfc5280.html local nameprep = require "util.encodings".stringprep.nameprep; local idna_to_ascii = require "util.encodings".idna.to_ascii; -- cgit v1.2.3 From 6619b204e37f7d0b7c18a1194688f3c85e8447fa Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 8 Aug 2022 20:33:44 +0200 Subject: doap: Update XEP versions for which no code changes appear needed XEP-0004: Partial forms are handled XEP-0045: We're already strict with GC 1.0 XEP-0060: Change in semantics wrt 'pubsub#type', but not in code XEP-0115: No protocol change XEP-0138: Specification moved to Obsolete XEP-0163: Editorial only change XEP-0215: Minor schema change XEP-0280: Editorial change XEP-0297: Had the wrong version number XEP-0106: Note missing piece for version 1.1 XEP-0313: Editorial change XEP-0363: Editorial clarification, no code change required XEP-0380: Registry additions, no code change needed XEP-0384: Not directly supported, only here because people will ask otherwise XEP-0445: Broken out of XEP-0401 --- doc/doap.xml | 37 +++++++++++++++++++++++-------------- util/jid.lua | 1 + 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/doc/doap.xml b/doc/doap.xml index 530b9c2c..93376268 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -68,7 +68,7 @@ - 2.12.1 + 2.13.0 0.4.0 partial no support for multiple items (reported tag) @@ -120,7 +120,7 @@ - 1.34.1 + 1.34.3 0.3.0 partial @@ -173,7 +173,7 @@ - 1.22.0 + 1.24.1 0.9.0 partial mod_pubsub @@ -269,6 +269,7 @@ 1.0 0.9.0 complete + util.jid.(un)escape, missing rejection of \20 at start or end per xep version 1.1 @@ -298,7 +299,7 @@ - 1.5.2 + 1.6.0 0.8.0 complete @@ -356,7 +357,7 @@ - 2.0 + 2.1 0.6.0 0.10.0 removed @@ -391,7 +392,7 @@ - 1.2.1 + 1.2.2 0.5.0 complete @@ -562,7 +563,7 @@ - 0.7.1 + 0.7.2 complete 0.12.0 mod_external_services @@ -624,7 +625,7 @@ - 1.0.0 + 1.0.1 complete 0.10.0 @@ -658,7 +659,7 @@ - 1.0.0 + 1.0 0.11.0 complete Used by XEP-0280, XEP-0313 @@ -684,7 +685,7 @@ - 1.0.0 + 1.0.1 complete 0.10.0 mod_mam, mod_muc_mam @@ -738,7 +739,7 @@ - 1.0.0 + 1.1.0 complete 0.12.0 mod_http_file_share @@ -764,7 +765,7 @@ - 0.3.0 + 0.4.0 0.11.0 complete Used in context of XEP-0352 @@ -773,7 +774,7 @@ - 0.8.1 + 0.8.3 complete via XEP-0163, XEP-0222 @@ -790,7 +791,7 @@ - 0.3.0 + 0.5.0 0.12.0 partial @@ -854,5 +855,13 @@ complete + + + + 0.2.0 + 0.12.0 + complete + + diff --git a/util/jid.lua b/util/jid.lua index 694a6b1f..759af746 100644 --- a/util/jid.lua +++ b/util/jid.lua @@ -111,6 +111,7 @@ local function resource(jid) return (select(3, split(jid))); end +-- TODO Forbid \20 at start and end of escaped output per XEP-0106 v1.1 local function escape(s) return s and (s:gsub("\\%x%x", backslash_escapes):gsub("[\"&'/:<>@ ]", escapes)); end local function unescape(s) return s and (s:gsub("\\%x%x", unescapes)); end -- cgit v1.2.3 From 8e3e3f3bcd5dab436ac15f55c4244b20027ebabc Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 10 Aug 2022 13:45:43 +0200 Subject: util.hex: Update Teal spec for function rename in a0ff5c438e9d --- teal-src/util/hex.d.tl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/teal-src/util/hex.d.tl b/teal-src/util/hex.d.tl index 3b216a88..9d84540b 100644 --- a/teal-src/util/hex.d.tl +++ b/teal-src/util/hex.d.tl @@ -2,5 +2,7 @@ local type s2s = function (s : string) : string local record lib to : s2s from : s2s + encode : s2s + decode : s2s end return lib -- cgit v1.2.3 From a8332d2427bf918336f3b0353e28c13280ddd49d Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 24 Mar 2021 20:23:38 +0100 Subject: core.storagemanager: Convert old Typed Lua description file into Teal Still only a type definition. Typed Lua is no longer maintained. Teal is currently an active project. --- doc/storage.tld | 68 ----------------------------------- teal-src/core/storagemanager.d.tl | 74 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 68 deletions(-) delete mode 100644 doc/storage.tld create mode 100644 teal-src/core/storagemanager.d.tl diff --git a/doc/storage.tld b/doc/storage.tld deleted file mode 100644 index 0fc8ff54..00000000 --- a/doc/storage.tld +++ /dev/null @@ -1,68 +0,0 @@ --- Storage Interface API Description --- --- This is written as a TypedLua description - --- Key-Value stores (the default) - -interface keyval_store - get : ( self, string? ) -> (any) | (nil, string) - set : ( self, string?, any ) -> (boolean) | (nil, string) -end - --- Map stores (key-key-value stores) - -interface map_store - get : ( self, string?, any ) -> (any) | (nil, string) - set : ( self, string?, any, any ) -> (boolean) | (nil, string) - set_keys : ( self, string?, { any : any }) -> (boolean) | (nil, string) - remove : {} -end - --- Archive stores - -typealias archive_query = { - "start" : number?, -- timestamp - "end" : number?, -- timestamp - "with" : string?, - "after" : string?, -- archive id - "before" : string?, -- archive id - "total" : boolean?, -} - -interface archive_store - -- Optional set of capabilities - caps : { - -- Optional total count of matching items returned as second return value from :find() - "total" : boolean?, - }? - - -- Add to the archive - append : ( self, string?, string?, any, number?, string? ) -> (string) | (nil, string) - - -- Iterate over archive - find : ( self, string?, archive_query? ) -> ( () -> ( string, any, number?, string? ), integer? ) - - -- Removal of items. API like find. Optional? - delete : ( self, string?, archive_query? ) -> (boolean) | (number) | (nil, string) - - -- Array of dates which do have messages (Optional?) - dates : ( self, string? ) -> ({ string }) | (nil, string) - - -- Map of counts per "with" field - summary : ( self, string?, archive_query? ) -> ( { string : integer } ) | (nil, string) - - -- Map-store API - get : ( self, string, string ) -> (stanza, number?, string?) | (nil, string) - set : ( self, string, string, stanza, number?, string? ) -> (boolean) | (nil, string) -end - --- This represents moduleapi -interface module - -- If the first string is omitted then the name of the module is used - -- The second string is one of "keyval" (default), "map" or "archive" - open_store : (self, string?, string?) -> (keyval_store) | (map_store) | (archive_store) | (nil, string) - - -- Other module methods omitted -end - -module : module diff --git a/teal-src/core/storagemanager.d.tl b/teal-src/core/storagemanager.d.tl new file mode 100644 index 00000000..3c8253b1 --- /dev/null +++ b/teal-src/core/storagemanager.d.tl @@ -0,0 +1,74 @@ +-- Storage local record API Description +-- +-- This is written as a TypedLua description + +-- Key-Value stores (the default) + +local stanza = require"util.stanza".stanza_t + +local record keyval_store + get : function ( keyval_store, string ) : any , string + set : function ( keyval_store, string, any ) : boolean, string +end + +-- Map stores (key-key-value stores) + +local record map_store + get : function ( map_store, string, any ) : any, string + set : function ( map_store, string, any, any ) : boolean, string + set_keys : function ( map_store, string, { any : any }) : boolean, string + remove : table +end + +-- Archive stores + +local record archive_query + start : number -- timestamp + ["end"]: number -- timestamp + with : string + after : string -- archive id + before : string -- archive id + total : boolean +end + +local record archive_store + -- Optional set of capabilities + caps : { + -- Optional total count of matching items returned as second return value from :find() + string : any + } + + -- Add to the archive + append : function ( archive_store, string, string, any, number, string ) : string, string + + -- Iterate over archive + type iterator = function () : string, any, number, string + find : function ( archive_store, string, archive_query ) : iterator, integer + + -- Removal of items. API like find. Optional + delete : function ( archive_store, string, archive_query ) : boolean | number, string + + -- Array of dates which do have messages (Optional) + dates : function ( archive_store, string ) : { string }, string + + -- Map of counts per "with" field + summary : function ( archive_store, string, archive_query ) : { string : integer }, string + + -- Map-store API + get : function ( archive_store, string, string ) : stanza, number, string + get : function ( archive_store, string, string ) : nil, string + set : function ( archive_store, string, string, stanza, number, string ) : boolean, string +end + +-- This represents moduleapi +local record coremodule + -- If the first string is omitted then the name of the module is used + -- The second string is one of "keyval" (default), "map" or "archive" + open_store : function (archive_store, string, string) : keyval_store, string + open_store : function (archive_store, string, string) : map_store, string + open_store : function (archive_store, string, string) : archive_store, string + + -- Other module methods omitted +end + +return coremodule -- cgit v1.2.3 From 185b679953985197b6c087487c01e269c749e5c4 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 5 Jul 2021 13:18:14 +0200 Subject: util.human.units: Specify enum argument to format() --- teal-src/util/human/units.d.tl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/teal-src/util/human/units.d.tl b/teal-src/util/human/units.d.tl index f6568d90..3db17c3a 100644 --- a/teal-src/util/human/units.d.tl +++ b/teal-src/util/human/units.d.tl @@ -1,5 +1,8 @@ local lib = record + enum logbase + "b" -- 1024 + end adjust : function (number, string) : number, string - format : function (number, string, string) : string + format : function (number, string, logbase) : string end return lib -- cgit v1.2.3 From d459ec2f5dcf600840c44b75ff175c349b105bef Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 24 Jul 2022 21:25:03 +0200 Subject: net.http: Add teal description files --- teal-src/net/http.d.tl | 88 +++++++++++++++++++++++++++++++++++++++++++ teal-src/net/http/codes.d.tl | 2 + teal-src/net/http/errors.d.tl | 22 +++++++++++ teal-src/net/http/files.d.tl | 14 +++++++ teal-src/net/http/parser.d.tl | 58 ++++++++++++++++++++++++++++ teal-src/net/http/server.d.tl | 6 +++ 6 files changed, 190 insertions(+) create mode 100644 teal-src/net/http.d.tl create mode 100644 teal-src/net/http/codes.d.tl create mode 100644 teal-src/net/http/errors.d.tl create mode 100644 teal-src/net/http/files.d.tl create mode 100644 teal-src/net/http/parser.d.tl create mode 100644 teal-src/net/http/server.d.tl diff --git a/teal-src/net/http.d.tl b/teal-src/net/http.d.tl new file mode 100644 index 00000000..38da56b6 --- /dev/null +++ b/teal-src/net/http.d.tl @@ -0,0 +1,88 @@ +local record Promise + -- util.promise +end + +local record sslctx -- from LuaSec +end + +local record lib + + enum http_method + "GET" + "HEAD" + "POST" + "PUT" + "OPTIONS" + "DELETE" + -- etc? + end + + record http_client_options + sslctx : sslctx + end + + record http_options + id : string + onlystatus : boolean + body : string + method : http_method + headers : { string : string } + insecure : boolean + suppress_errors : boolean + streaming_handler : function + suppress_url : boolean + sslctx : sslctx + end + + record http_request + host : string + port : string + enum scheme + "http" + "https" + end + scheme : scheme + url : string + userinfo : string + path : string + + method : http_method + headers : { string : string } + + insecure : boolean + suppress_errors : boolean + streaming_handler : function + http : http_client + time : integer + id : string + callback : http_callback + end + + record http_response + end + + type http_callback = function (string, number, http_response, http_request) + + record http_client + options : http_client_options + request : function (http_client, string, http_options, http_callback) + end + + request : function (string, http_options, http_callback) : Promise, string + default : http_client + new : function (http_client_options) : http_client + events : table + -- COMPAT + urlencode : function (string) : string + urldecode : function (string) : string + formencode : function ({ string : string }) : string + formdecode : function (string) : { string : string } + destroy_request : function (http_request) + + enum available_features + "sni" + end + features : { available_features : boolean } +end + +return lib diff --git a/teal-src/net/http/codes.d.tl b/teal-src/net/http/codes.d.tl new file mode 100644 index 00000000..65d004fc --- /dev/null +++ b/teal-src/net/http/codes.d.tl @@ -0,0 +1,2 @@ +local type response_codes = { integer : string } +return response_codes diff --git a/teal-src/net/http/errors.d.tl b/teal-src/net/http/errors.d.tl new file mode 100644 index 00000000..a9b6ea6c --- /dev/null +++ b/teal-src/net/http/errors.d.tl @@ -0,0 +1,22 @@ +local record http_errors + enum known_conditions + "cancelled" + "connection-closed" + "certificate-chain-invalid" + "certificate-verify-failed" + "connection failed" + "invalid-url" + "unable to resolve service" + end + type registry_keys = known_conditions | integer + record error + type : string + condition : string + code : integer + text : string + end + registry : { registry_keys : error } + new : function (integer, known_conditions, table) + new : function (integer, string, table) +end +return http_errors diff --git a/teal-src/net/http/files.d.tl b/teal-src/net/http/files.d.tl new file mode 100644 index 00000000..d0ba5c1c --- /dev/null +++ b/teal-src/net/http/files.d.tl @@ -0,0 +1,14 @@ +local record serve_options + path : string + mime_map : { string : string } + cache_size : integer + cache_max_file_size : integer + index_files : { string } + directory_index : boolean +end + +local record http_files + serve : function(serve_options|string) : function +end + +return http_files diff --git a/teal-src/net/http/parser.d.tl b/teal-src/net/http/parser.d.tl new file mode 100644 index 00000000..1cd6ccf4 --- /dev/null +++ b/teal-src/net/http/parser.d.tl @@ -0,0 +1,58 @@ +local record httpstream + feed : function(httpstream, string) +end + +local type sink_cb = function () + +local record httppacket + enum http_method + "HEAD" + "GET" + "POST" + "PUT" + "DELETE" + "OPTIONS" + -- etc + end + method : http_method + record url_details + path : string + query : string + end + url : url_details + path : string + enum http_version + "1.0" + "1.1" + end + httpversion : http_version + headers : { string : string } + body : string | boolean + body_sink : sink_cb + chunked : boolean + partial : boolean +end + +local enum error_conditions + "cancelled" + "connection-closed" + "certificate-chain-invalid" + "certificate-verify-failed" + "connection failed" + "invalid-url" + "unable to resolve service" +end + +local type success_cb = function (httppacket) +local type error_cb = function (error_conditions) + +local enum stream_mode + "client" + "server" +end + +local record lib + new : function (success_cb, error_cb, stream_mode) : httpstream +end + +return lib diff --git a/teal-src/net/http/server.d.tl b/teal-src/net/http/server.d.tl new file mode 100644 index 00000000..5a83af7e --- /dev/null +++ b/teal-src/net/http/server.d.tl @@ -0,0 +1,6 @@ + +local record http_server + -- TODO +end + +return http_server -- cgit v1.2.3 From 0ed1e69a4e78612fe27426f716ce8faa6042a24a Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 24 Jul 2022 21:25:17 +0200 Subject: net.server: Add teal description file --- teal-src/net/server.d.tl | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 teal-src/net/server.d.tl diff --git a/teal-src/net/server.d.tl b/teal-src/net/server.d.tl new file mode 100644 index 00000000..bb61f677 --- /dev/null +++ b/teal-src/net/server.d.tl @@ -0,0 +1,65 @@ +local record server + record LuaSocketTCP + end + record LuaSecCTX + end + + record extra_settings + end + + record interface + end + enum socket_type + "tcp" + "tcp6" + "tcp4" + end + + record listeners + onconnect : function (interface) + ondetach : function (interface) + onattach : function (interface, string) + onincoming : function (interface, string, string) + ondrain : function (interface) + onreadtimeout : function (interface) + onstarttls : function (interface) + onstatus : function (interface, string) + ondisconnect : function (interface, string) + end + + get_backend : function () : string + + type port = string | integer + enum read_mode + "*a" + "*l" + end + type read_size = read_mode | integer + addserver : function (string, port, listeners, read_size, LuaSecCTX) : interface + addclient : function (string, port, listeners, read_size, LuaSecCTX, socket_type, extra_settings) : interface + record listen_config + read_size : read_size + tls_ctx : LuaSecCTX + tls_direct : boolean + sni_hosts : { string : LuaSecCTX } + end + listen : function (string, port, listeners, listen_config) : interface + enum quitting + "quitting" + end + loop : function () : quitting + closeall : function () + setquitting : function (boolean | quitting) + + wrapclient : function (LuaSocketTCP, string, port, listeners, read_size, LuaSecCTX, extra_settings) : interface + wrapserver : function (LuaSocketTCP, string, port, listeners, listen_config) : interface + watchfd : function (integer | LuaSocketTCP, function (interface), function (interface)) : interface + link : function () + + record config + end + set_config : function (config) + +end + +return server -- cgit v1.2.3 From 53907b44cee9c5acfb6979d6f60b9f285a064bc7 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 24 Jul 2022 21:25:31 +0200 Subject: teal: add stub util.array teal defs --- teal-src/util/array.d.tl | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 teal-src/util/array.d.tl diff --git a/teal-src/util/array.d.tl b/teal-src/util/array.d.tl new file mode 100644 index 00000000..70bf2624 --- /dev/null +++ b/teal-src/util/array.d.tl @@ -0,0 +1,9 @@ +local record array_t + { T } +end + +local record lib + metamethod __call : function () : array_t +end + +return lib -- cgit v1.2.3 From 523d844c789556f259244c2139009eff924e91ae Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 24 Jul 2022 21:25:41 +0200 Subject: util.promise: Add Teal interface specification file --- teal-src/net/http.d.tl | 4 +--- teal-src/util/promise.d.tl | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 teal-src/util/promise.d.tl diff --git a/teal-src/net/http.d.tl b/teal-src/net/http.d.tl index 38da56b6..9135ec12 100644 --- a/teal-src/net/http.d.tl +++ b/teal-src/net/http.d.tl @@ -1,6 +1,4 @@ -local record Promise - -- util.promise -end +local Promise = require "util.promise".Promise; local record sslctx -- from LuaSec end diff --git a/teal-src/util/promise.d.tl b/teal-src/util/promise.d.tl new file mode 100644 index 00000000..d895a828 --- /dev/null +++ b/teal-src/util/promise.d.tl @@ -0,0 +1,22 @@ + +local record lib + type resolve_func = function (any) + type promise_body = function (resolve_func, resolve_func) + + record Promise + type on_resolved = function (A) : any + type on_rejected = function (B) : any + next : function (Promise, on_resolved, on_rejected) : Promise + end + + new : function (promise_body) : Promise + resolve : function (any) : Promise + reject : function (any) : Promise + all : function ({ Promise }) : Promise + all_settled : function ({ Promise }) : Promise + race : function ({ Promise }) : Promise + try : function + is_promise : function(any) : boolean +end + +return lib -- cgit v1.2.3 From 238c64e233938970224ad691831336cbfa08cdfd Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 10 Nov 2021 15:12:03 +0100 Subject: util.human.io: Add Teal interface definition --- teal-src/util/human/io.d.tl | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 teal-src/util/human/io.d.tl diff --git a/teal-src/util/human/io.d.tl b/teal-src/util/human/io.d.tl new file mode 100644 index 00000000..e4f64cd1 --- /dev/null +++ b/teal-src/util/human/io.d.tl @@ -0,0 +1,28 @@ +local record lib + getchar : function (n : integer) : string + getline : function () : string + getpass : function () : string + show_yesno : function (prompt : string) : boolean + read_password : function () : string + show_prompt : function (prompt : string) : boolean + printf : function (fmt : string, ... : any) + padleft : function (s : string, width : integer) : string + padright : function (s : string, width : integer) : string + + -- {K:V} vs T ? + record tablerow + width : integer | string -- generate an 1..100 % enum? + title : string + mapper : function (V, {K:V}) : string + key : K + enum alignments + "left" + "right" + end + align : alignments + end + type getrow = function ({ K : V }) : string + table : function ({ tablerow }, width : integer) : getrow +end + +return lib -- cgit v1.2.3 From dff6720958a1cb46f4ce5c2721c67bc94cb80450 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 15 Nov 2021 17:07:23 +0100 Subject: util.dataforms: Restructure Teal definition file The PR has been merged and there's no reason not to have nested records and other definitions. --- teal-src/util/dataforms.d.tl | 91 ++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/teal-src/util/dataforms.d.tl b/teal-src/util/dataforms.d.tl index 9e4170fa..eac55717 100644 --- a/teal-src/util/dataforms.d.tl +++ b/teal-src/util/dataforms.d.tl @@ -1,51 +1,52 @@ local stanza_t = require "util.stanza".stanza_t -local enum form_type - "form" - "submit" - "cancel" - "result" -end - -local enum field_type - "boolean" - "fixed" - "hidden" - "jid-multi" - "jid-single" - "list-multi" - "list-single" - "text-multi" - "text-private" - "text-single" -end - -local record form_field - - type : field_type - var : string -- protocol name - name : string -- internal name - - label : string - desc : string - - datatype : string - range_min : number - range_max : number - - value : any -- depends on field_type - options : table -end - -local record dataform - title : string - instructions : string - { form_field } -- XXX https://github.com/teal-language/tl/pull/415 - - form : function ( dataform, table, form_type ) : stanza_t -end - local record lib + record dataform + title : string + instructions : string + + record form_field + + enum field_type + "boolean" + "fixed" + "hidden" + "jid-multi" + "jid-single" + "list-multi" + "list-single" + "text-multi" + "text-private" + "text-single" + end + + type : field_type + var : string -- protocol name + name : string -- internal name + + label : string + desc : string + + datatype : string + range_min : number + range_max : number + + value : any -- depends on field_type + options : table + end + + { form_field } + + enum form_type + "form" + "submit" + "cancel" + "result" + end + + form : function ( dataform, { string : any }, form_type ) : stanza_t + end + new : function ( dataform ) : dataform end -- cgit v1.2.3 From 9dc7ac98fa5a71e1e2019dd1b6fefde9a6842b5a Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 15 Nov 2021 17:15:22 +0100 Subject: util.dataforms: Add missing :data() to Teal definition --- teal-src/util/dataforms.d.tl | 1 + 1 file changed, 1 insertion(+) diff --git a/teal-src/util/dataforms.d.tl b/teal-src/util/dataforms.d.tl index eac55717..0eddf98e 100644 --- a/teal-src/util/dataforms.d.tl +++ b/teal-src/util/dataforms.d.tl @@ -45,6 +45,7 @@ local record lib end form : function ( dataform, { string : any }, form_type ) : stanza_t + data : function ( dataform, stanza_t ) : { string : any } end new : function ( dataform ) : dataform -- cgit v1.2.3 From efb69fa99bc19dc1beab79829bf98987ab5a4a33 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 1 Dec 2021 15:05:06 +0100 Subject: util.serialization: Add Teal type specification --- teal-src/util/serialization.d.tl | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 teal-src/util/serialization.d.tl diff --git a/teal-src/util/serialization.d.tl b/teal-src/util/serialization.d.tl new file mode 100644 index 00000000..27f100c0 --- /dev/null +++ b/teal-src/util/serialization.d.tl @@ -0,0 +1,33 @@ +local record _M + enum preset + "debug" + "oneline" + "compact" + end + type fallback = function (any, string) : string + record config + preset : preset + fallback : fallback + fatal : boolean + keywords : { string : boolean } + indentwith : string + itemstart : string + itemsep : string + itemlast : string + tstart : string + tend : string + kstart : string + kend : string + equals : string + unquoted : boolean | string + hex : string + freeze : boolean + maxdepth : integer + multirefs : boolean + table_pairs : function + end + type serializer = function (any) : string + new : function (config|preset) : serializer + serialize : function (any, config|preset) : string +end +return _M -- cgit v1.2.3 From dfa35e5217b21c8ea8e10a1128c50d07e67c056b Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 16 Feb 2022 16:42:22 +0100 Subject: util.set: Add teal type declaration file --- teal-src/util/set.d.tl | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 teal-src/util/set.d.tl diff --git a/teal-src/util/set.d.tl b/teal-src/util/set.d.tl new file mode 100644 index 00000000..1631eec4 --- /dev/null +++ b/teal-src/util/set.d.tl @@ -0,0 +1,21 @@ +local record lib + record Set + add : function (Set, T) + contains : function (Set, T) : boolean + contains_set : function (Set, Set) : boolean + items : function (Set) : function (Set, T) : T + add_list : function (Set, { T }) + include : function (Set, Set) + exclude : function (Set, Set) + empty : function (Set) : boolean + end + + new : function ({ T }) : Set + is_set : function (any) : boolean + union : function (Set, Set) : Set + difference : function (Set, Set) : Set + intersection : function (Set, Set) : Set + xor : function (Set, Set) : Set +end + +return lib -- cgit v1.2.3 From 7ebea134e6fd45a524b8a924be04c3b51f8f7799 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 15 Jun 2022 11:37:09 +0100 Subject: util.session: Add role management methods --- util/session.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/util/session.lua b/util/session.lua index 25b22faf..d908476a 100644 --- a/util/session.lua +++ b/util/session.lua @@ -57,10 +57,16 @@ local function set_send(session) return session; end +local function set_role(session, role) + session.role = role; +end + return { new = new_session; + set_id = set_id; set_logger = set_logger; set_conn = set_conn; set_send = set_send; + set_role = set_role; } -- 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(-) 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. --- .luacheckrc | 3 + core/moduleapi.lua | 63 +++++++++++++++++ core/sessionmanager.lua | 15 +++- core/usermanager.lua | 6 -- 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 +-- 15 files changed, 269 insertions(+), 131 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 6c948b17..e1bb7134 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -62,6 +62,8 @@ files["plugins/"] = { "module.broadcast", "module.context", "module.depends", + "module.default_permission", + "module.default_permissions", "module.fire_event", "module.get_directory", "module.get_host", @@ -86,6 +88,7 @@ files["plugins/"] = { "module.load_resource", "module.log", "module.log_status", + "module.may", "module.measure", "module.metric", "module.open_store", diff --git a/core/moduleapi.lua b/core/moduleapi.lua index 36d82193..f0b412f3 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -19,6 +19,7 @@ local promise = require "util.promise"; local time_now = require "util.time".now; local format = require "util.format".format; local jid_node = require "util.jid".node; +local jid_split = require "util.jid".split; local jid_resource = require "util.jid".resource; local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat; @@ -601,4 +602,66 @@ function api:get_status() return self.status_type, self.status_message, self.status_time; end +function api:default_permission(role_name, permission) + permission = permission:gsub("^:", self.name..":"); + hosts[self.host].authz.add_default_permission(role_name, permission); +end + +function api:default_permissions(role_name, permissions) + for _, permission in ipairs(permissions) do + permission = permission:gsub("^:", self.name..":"); + self:default_permission(role_name, permission); + end +end + +function api:may(action, context) + if type(context) == "string" then -- check JID permissions + local role; + local node, host = jid_split(context); + if host == self.host then + role = hosts[host].authz.get_user_role(node); + else + role = hosts[self.host].authz.get_jid_role(context); + end + if not role then + self:log("debug", "Access denied: JID <%s> may not %s (no role found)", context, action); + return false; + end + local permit = role:may(action); + if not permit then + self:log("debug", "Access denied: JID <%s> may not %s (not permitted by role %s)", context, action, role.name); + end + return permit; + end + + local session = context.origin or context.session; + if not session then + error("Unable to identify actor session from context"); + end + if action:byte(1) == 58 then -- action begins with ':' + action = self.name..action; -- prepend module name + end + if session.type == "s2sin" or (session.type == "c2s" and session.host ~= self.host) then + local actor_jid = context.stanza.attr.from; + local role = hosts[self.host].authz.get_jid_role(actor_jid); + if not role then + self:log("debug", "Access denied: JID <%s> may not %s (no role found)", actor_jid, action); + return false; + end + local permit = role:may(action, context); + if not permit then + self:log("debug", "Access denied: JID <%s> may not %s (not permitted by role %s)", actor_jid, action, role.name); + end + return permit; + elseif session.role then + local permit = session.role:may(action, context); + if not permit then + self:log("debug", "Access denied: session %s (%s) may not %s (not permitted by role %s)", + session.id, session.full_jid, action, session.role.name + ); + end + return permit; + end +end + return api; diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index 7f296ff1..426afa7b 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -123,15 +123,24 @@ local function destroy_session(session, err) retire_session(session); end -local function make_authenticated(session, username, scope) +local function make_authenticated(session, username, role_name) username = nodeprep(username); if not username or #username == 0 then return nil, "Invalid username"; end session.username = username; if session.type == "c2s_unauthed" then session.type = "c2s_unbound"; end - session.auth_scope = scope; - session.log("info", "Authenticated as %s@%s", username, session.host or "(unknown)"); + + local role; + if role_name then + role = hosts[session.host].authz.get_role_info(role_name); + else + role = hosts[session.host].authz.get_user_default_role(username); + end + if role then + sessionlib.set_role(session, role); + end + session.log("info", "Authenticated as %s@%s [%s]", username, session.host or "(unknown)", role and role.name or "no role"); return true; end diff --git a/core/usermanager.lua b/core/usermanager.lua index 45f104fa..970140ef 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -188,11 +188,6 @@ local function set_roles(jid, host, roles) end end -local function is_admin(jid, host) - local roles = get_roles(jid, host); - return roles and roles["prosody:admin"]; -end - local function get_users_with_role(role, host) if not hosts[host] then return false; end if type(role) ~= "string" then return false; end @@ -224,7 +219,6 @@ return { get_provider = get_provider; get_roles = get_roles; set_roles = set_roles; - is_admin = is_admin; get_users_with_role = get_users_with_role; get_jids_with_role = get_jids_with_role; }; 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 57ed2e28a09933dd6220dd6ddd84f0f442e9b71f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 15 Jun 2022 14:22:26 +0100 Subject: teal-src: update module.d.tl with new access control methods --- teal-src/module.d.tl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/teal-src/module.d.tl b/teal-src/module.d.tl index cb7771e2..24eb9558 100644 --- a/teal-src/module.d.tl +++ b/teal-src/module.d.tl @@ -126,6 +126,11 @@ global record moduleapi path : string resource_path : string + -- access control + may : function (moduleapi, string, table|string) + default_permission : function (string, string) + default_permissions : function (string, { string }) + -- methods the module can add load : function () add_host : function (moduleapi) -- cgit v1.2.3 From a9c96e12c2b2cbe40604571e2d81027fa4139aa6 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 15 Jun 2022 15:40:03 +0200 Subject: util.table: Add move() to Teal interface description file --- teal-src/util/table.d.tl | 1 + 1 file changed, 1 insertion(+) diff --git a/teal-src/util/table.d.tl b/teal-src/util/table.d.tl index 0ff5ed95..67e5d0f0 100644 --- a/teal-src/util/table.d.tl +++ b/teal-src/util/table.d.tl @@ -1,6 +1,7 @@ local record lib create : function (narr:integer, nrec:integer):table pack : function (...:any):{any} + move : function (table, integer, integer, integer, table) : table end return lib -- cgit v1.2.3 From 64641500885e86a28f79c69dbeb85ba2fa2566a3 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 15 Jun 2022 15:51:57 +0200 Subject: util.struct: Add Teal interface description file --- teal-src/util/struct.d.tl | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 teal-src/util/struct.d.tl diff --git a/teal-src/util/struct.d.tl b/teal-src/util/struct.d.tl new file mode 100644 index 00000000..201aaa23 --- /dev/null +++ b/teal-src/util/struct.d.tl @@ -0,0 +1,6 @@ +local record lib + pack : function (string, ...:any) : string + unpack : function(string, string, integer) : any... + size : function(string) : integer +end +return lib -- cgit v1.2.3 From c776c71066e22eff9f8c5b0401946513bd22f103 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 15 Jun 2022 23:03:15 +0200 Subject: core.moduleapi: Fixup method name `get_user_role()` did not exist anywhere else. MattJ said `get_user_default_role()` was indented --- core/moduleapi.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index f0b412f3..0652e032 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -619,7 +619,7 @@ function api:may(action, context) local role; local node, host = jid_split(context); if host == self.host then - role = hosts[host].authz.get_user_role(node); + role = hosts[host].authz.get_user_default_role(node); else role = hosts[self.host].authz.get_jid_role(context); end -- cgit v1.2.3 From af339f0e66480da6825fd655a5bf35e2824cfc00 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 15 Jun 2022 23:04:17 +0200 Subject: core.moduleapi: Expand permission name ':' prefix earlier Ensures it applies to the context as string case Somehow this fixes everything --- core/moduleapi.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index 0652e032..fe248c20 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -615,6 +615,9 @@ function api:default_permissions(role_name, permissions) end function api:may(action, context) + if action:byte(1) == 58 then -- action begins with ':' + action = self.name..action; -- prepend module name + end if type(context) == "string" then -- check JID permissions local role; local node, host = jid_split(context); @@ -638,9 +641,6 @@ function api:may(action, context) if not session then error("Unable to identify actor session from context"); end - if action:byte(1) == 58 then -- action begins with ':' - action = self.name..action; -- prepend module name - end if session.type == "s2sin" or (session.type == "c2s" and session.host ~= self.host) then local actor_jid = context.stanza.attr.from; local role = hosts[self.host].authz.get_jid_role(actor_jid); -- cgit v1.2.3 From 6e1de3134c037a4308531c031f9eae401091ed76 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 30 Jun 2022 15:39:03 +0200 Subject: util.bitcompat: Add Teal type specification --- teal-src/util/bitcompat.d.tl | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 teal-src/util/bitcompat.d.tl diff --git a/teal-src/util/bitcompat.d.tl b/teal-src/util/bitcompat.d.tl new file mode 100644 index 00000000..18adf725 --- /dev/null +++ b/teal-src/util/bitcompat.d.tl @@ -0,0 +1,8 @@ +local record lib + band : function (integer, integer, ... : integer) : integer + bor : function (integer, integer, ... : integer) : integer + bxor : function (integer, integer, ... : integer) : integer + lshift : function (integer, integer) : integer + rshift : function (integer, integer) : integer +end +return lib -- 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). --- core/usermanager.lua | 7 +++++++ plugins/mod_auth_internal_hashed.lua | 14 +++++++++++++- plugins/mod_auth_internal_plain.lua | 16 +++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index 970140ef..23571fe7 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -116,6 +116,12 @@ local function set_password(username, password, host, resource) return ok, err; end +local function get_account_info(username, host) + local method = hosts[host].users.get_account_info; + if not method then return nil, "method-not-supported"; end + return method(username); +end + local function user_exists(username, host) if hosts[host].sessions[username] then return true; end return hosts[host].users.user_exists(username); @@ -211,6 +217,7 @@ return { test_password = test_password; get_password = get_password; set_password = set_password; + get_account_info = get_account_info; user_exists = user_exists; create_user = create_user; delete_user = delete_user; 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 77146aa4e7841eb5ca35cca28881d37eb53e5a96 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 19 Jul 2022 17:44:26 +0100 Subject: util.roles: Add new utility module to consolidate role objects and methods --- util/roles.lua | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 util/roles.lua diff --git a/util/roles.lua b/util/roles.lua new file mode 100644 index 00000000..831ce6ae --- /dev/null +++ b/util/roles.lua @@ -0,0 +1,100 @@ +local array = require "util.array"; +local it = require "util.iterators"; +local new_short_id = require "util.id".short; + +local role_methods = {}; +local role_mt = { + __index = role_methods; + __name = "role"; + __add = nil; +}; + +local function is_role(o) + local mt = getmetatable(o); + return mt == role_mt; +end + +local function _new_may(permissions, inherited_mays) + local n_inherited = inherited_mays and #inherited_mays; + return function (role, action, context) + -- Note: 'role' may be a descendent role, not only the one we're attached to + local policy = permissions[action]; + if policy ~= nil then + return policy; + end + if n_inherited then + for i = 1, n_inherited do + policy = inherited_mays[i](role, action, context); + if policy ~= nil then + return policy; + end + end + end + return false; + end +end + +local permissions_key = {}; + +-- { +-- Required: +-- name = "My fancy role"; +-- +-- Optional: +-- inherits = { role_obj... } +-- default = true +-- priority = 100 +-- permissions = { +-- ["foo"] = true; -- allow +-- ["bar"] = false; -- deny +-- } +-- } +local function new(base_config, overrides) + local config = setmetatable(overrides or {}, { __index = base_config }); + local permissions = {}; + local inherited_mays; + if config.inherits then + inherited_mays = array.pluck(config.inherits, "may"); + end + local new_role = { + id = new_short_id(); + name = config.name; + description = config.description; + default = config.default; + priority = config.priority; + may = _new_may(permissions, inherited_mays); + inherits = config.inherits; + [permissions_key] = permissions; + }; + local desired_permissions = config.permissions or config[permissions_key]; + for k, v in pairs(desired_permissions or {}) do + permissions[k] = v; + end + return setmetatable(new_role, role_mt); +end + +function role_methods:clone(overrides) + return new(self, overrides); +end + +function role_methods:set_permission(permission_name, policy, overwrite) + local permissions = self[permissions_key]; + if overwrite ~= true and permissions[permission_name] ~= nil and permissions[permission_name] ~= policy then + return false, "policy-already-exists"; + end + permissions[permission_name] = policy; + return true; +end + +function role_mt.__tostring(self) + return ("role<[%s] %s>"):format(self.id or "nil", self.name or "[no name]"); +end + +function role_mt.__pairs(self) + return it.filter(permissions_key, next, self); +end + +return { + is_role = is_role; + new = new; +}; -- 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). --- core/sessionmanager.lua | 2 +- core/usermanager.lua | 80 +++++++++++++-------- plugins/mod_authz_internal.lua | 159 +++++++++++++++++++++++++---------------- 3 files changed, 146 insertions(+), 95 deletions(-) diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index 426afa7b..924c4968 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -133,7 +133,7 @@ local function make_authenticated(session, username, role_name) local role; if role_name then - role = hosts[session.host].authz.get_role_info(role_name); + role = hosts[session.host].authz.get_role_by_name(role_name); else role = hosts[session.host].authz.get_user_default_role(username); end diff --git a/core/usermanager.lua b/core/usermanager.lua index 23571fe7..af82c940 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -10,8 +10,6 @@ local modulemanager = require "core.modulemanager"; local log = require "util.logger".init("usermanager"); local type = type; local it = require "util.iterators"; -local jid_bare = require "util.jid".bare; -local jid_split = require "util.jid".split; local jid_prep = require "util.jid".prep; local config = require "core.configmanager"; local sasl_new = require "util.sasl".new; @@ -150,48 +148,54 @@ local function get_provider(host) return hosts[host].users; end -local function get_roles(jid, host) +-- Returns a map of { [role_name] = role, ... } that a user is allowed to assume +local function get_user_roles(user, host) if host and not hosts[host] then return false; end - if type(jid) ~= "string" then return false; end + if type(user) ~= "string" then return false; end - jid = jid_bare(jid); host = host or "*"; - local actor_user, actor_host = jid_split(jid); - local roles; - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; + return authz_provider.get_user_roles(user); +end - if actor_user and actor_host == host then -- Local user - roles = authz_provider.get_user_roles(actor_user); - else -- Remote user/JID - roles = authz_provider.get_jid_roles(jid); - end +local function get_user_default_role(user, host) + if host and not hosts[host] then return false; end + if type(user) ~= "string" then return false; end - return roles; + host = host or "*"; + + local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; + return authz_provider.get_user_default_role(user); end -local function set_roles(jid, host, roles) +-- Accepts a set of role names which the user is allowed to assume +local function set_user_roles(user, host, roles) if host and not hosts[host] then return false; end - if type(jid) ~= "string" then return false; end + if type(user) ~= "string" then return false; end - jid = jid_bare(jid); host = host or "*"; - local actor_user, actor_host = jid_split(jid); - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; - if actor_user and actor_host == host then -- Local user - local ok, err = authz_provider.set_user_roles(actor_user, roles); - if ok then - prosody.events.fire_event("user-roles-changed", { - username = actor_user, host = actor_host - }); - end - return ok, err; - else -- Remote entity - return authz_provider.set_jid_roles(jid, roles) + local ok, err = authz_provider.set_user_roles(user, roles); + if ok then + prosody.events.fire_event("user-roles-changed", { + username = user, host = host + }); end + return ok, err; +end + +local function get_jid_role(jid, host) + host = host or "*"; + local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; + return authz_provider.get_jid_role(jid); +end + +local function set_jid_role(jid, host, role_name) + host = host or "*"; + local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; + return authz_provider.set_jid_role(jid, role_name) end local function get_users_with_role(role, host) @@ -211,6 +215,16 @@ local function get_jids_with_role(role, host) return authz_provider.get_jids_with_role(role); end +local function get_role_by_name(role_name, host) + if host and not hosts[host] then return false; end + if type(role_name) ~= "string" then return false; end + + host = host or "*"; + + local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; + return authz_provider.get_role_by_name(role_name); +end + return { new_null_provider = new_null_provider; initialize_host = initialize_host; @@ -224,8 +238,12 @@ return { users = users; get_sasl_handler = get_sasl_handler; get_provider = get_provider; - get_roles = get_roles; - set_roles = set_roles; + get_user_default_role = get_user_default_role; + get_user_roles = get_user_roles; + set_user_roles = set_user_roles; get_users_with_role = get_users_with_role; + get_jid_role = get_jid_role; + set_jid_role = set_jid_role; get_jids_with_role = get_jids_with_role; + get_role_by_name = get_role_by_name; }; 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(-) 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 5f8e4414499e33a05dc8eda7a224e69c70bdb7a2 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 20 Jul 2022 13:05:35 +0200 Subject: moduleapi: Distribute permissions set from global modules to all hosts Roles and permissions will always happen in the context of a host. Prevents error upon indexing since `hosts["*"] == nil` --- core/moduleapi.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index fe248c20..0ec96fb6 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -604,6 +604,14 @@ end function api:default_permission(role_name, permission) permission = permission:gsub("^:", self.name..":"); + if self.host == "*" then + for _, host in pairs(hosts) do + if host.authz then + host.authz.add_default_permission(role_name, permission); + end + end + return + end hosts[self.host].authz.add_default_permission(role_name, permission); end -- cgit v1.2.3 From 55378f128acb817baab9fbeb412768f1a748a969 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 20 Jul 2022 13:07:04 +0200 Subject: moduleapi: Remove redundant expansion of ':' prefix in permission names --- core/moduleapi.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index 0ec96fb6..c1d8851e 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -617,7 +617,6 @@ end function api:default_permissions(role_name, permissions) for _, permission in ipairs(permissions) do - permission = permission:gsub("^:", self.name..":"); self:default_permission(role_name, permission); end end -- cgit v1.2.3 From cdd5608f4a42e7740536d09eeda14451924c9657 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 20 Jul 2022 13:08:07 +0200 Subject: moduleapi: Stricter type check for actor in permission check Non-table but truthy values would trigger "attempt to index a foo value" on the next line otherwise --- core/moduleapi.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index c1d8851e..8790a9d3 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -645,7 +645,7 @@ function api:may(action, context) end local session = context.origin or context.session; - if not session then + if type(session) ~= "table" then error("Unable to identify actor session from context"); end if session.type == "s2sin" or (session.type == "c2s" and session.host ~= self.host) then -- cgit v1.2.3 From a88c982ae232f85bbf30401aa7690dbccc482866 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 20 Jul 2022 13:10:47 +0200 Subject: core.usermanager: Add missing stub authz methods to global authz provider Except, should we have a global authz provider at all? --- core/usermanager.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/usermanager.lua b/core/usermanager.lua index af82c940..ec782906 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -54,6 +54,13 @@ local global_authz_provider = { end; set_user_roles = function (user, roles) end; -- luacheck: ignore 212 set_jid_roles = function (jid, roles) end; -- luacheck: ignore 212 + + get_user_default_role = function (user) end; -- luacheck: ignore 212 + get_users_with_role = function (role_name) end; -- luacheck: ignore 212 + get_jid_role = function (jid) end; -- luacheck: ignore 212 + set_jid_role = function (jid) end; -- luacheck: ignore 212 + add_default_permission = function (role_name, action, policy) end; -- luacheck: ignore 212 + get_role_info = function (role_name) end; -- luacheck: ignore 212 }; local provider_mt = { __index = new_null_provider() }; -- cgit v1.2.3 From 90dd7f694318b638ff1eef4d9a6e2e6f7f69820f Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 24 Jul 2022 21:39:07 +0200 Subject: util.logger: Add Teal interface description --- teal-src/util/logger.d.tl | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 teal-src/util/logger.d.tl diff --git a/teal-src/util/logger.d.tl b/teal-src/util/logger.d.tl new file mode 100644 index 00000000..db29adfd --- /dev/null +++ b/teal-src/util/logger.d.tl @@ -0,0 +1,18 @@ +local record util + enum loglevel + "debug" + "info" + "warn" + "error" + end + type logger = function ( loglevel, string, ...:any ) + type sink = function ( string, loglevel, string, ...:any ) + type simple_sink = function ( string, loglevel, string ) + init : function ( string ) : logger + make_logger : function ( string, loglevel ) : function ( string, ...:any ) + reset : function () + add_level_sink : function ( loglevel, sink ) + add_simple_sink : function ( simple_sink, { loglevel } ) +end + +return util -- cgit v1.2.3 From f307901a09bbb426ae3e0ccf3dec40b69ef68b8f Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 24 Jul 2022 21:39:32 +0200 Subject: util.queue: Add Teal interface description --- teal-src/util/queue.d.tl | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 teal-src/util/queue.d.tl diff --git a/teal-src/util/queue.d.tl b/teal-src/util/queue.d.tl new file mode 100644 index 00000000..cb8458e7 --- /dev/null +++ b/teal-src/util/queue.d.tl @@ -0,0 +1,21 @@ +local record lib + record queue + size : integer + count : function (queue) : integer + enum push_errors + "queue full" + end + + push : function (queue, T) : boolean, push_errors + pop : function (queue) : T + peek : function (queue) : T + replace : function (queue, T) : boolean, push_errors + type iterator = function (T, integer) : integer, T + items : function (queue) : iterator, T, integer + type consume_iter = function (queue) : T + consume : function (queue) : consume_iter + end + + new : function (size:integer, allow_wrapping:boolean) : queue +end +return lib; -- cgit v1.2.3 From 49497f25d3b9db07521f0a45fa2e949f72dbbf7d Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 24 Jul 2022 21:39:46 +0200 Subject: util.termcolours: Add Teal interface description --- teal-src/util/termcolours.d.tl | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 teal-src/util/termcolours.d.tl diff --git a/teal-src/util/termcolours.d.tl b/teal-src/util/termcolours.d.tl new file mode 100644 index 00000000..226259aa --- /dev/null +++ b/teal-src/util/termcolours.d.tl @@ -0,0 +1,7 @@ +local record lib + getstring : function (string, string) : string + getstyle : function (...:string) : string + setstyle : function (string) : string + tohtml : function (string) : string +end +return lib -- cgit v1.2.3 From 93f6968077f48338ff7f925e45f365e7084dd524 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 24 Jul 2022 21:39:51 +0200 Subject: util.timer: Add Teal interface description --- teal-src/util/timer.d.tl | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 teal-src/util/timer.d.tl diff --git a/teal-src/util/timer.d.tl b/teal-src/util/timer.d.tl new file mode 100644 index 00000000..a6394cf3 --- /dev/null +++ b/teal-src/util/timer.d.tl @@ -0,0 +1,8 @@ +local record util_timer + record task end + type timer_callback = function (number) : number + add_task : function ( number, timer_callback, any ) : task + stop : function ( task ) + reschedule : function ( task, number ) : task +end +return util_timer -- cgit v1.2.3 From b1f0061da37e72d12a71eb067ae103fe2ae6ca91 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 1 Aug 2022 20:26:00 +0100 Subject: usermanager: Handle local JIDs being passed to get/set_jid_role() There is no reasonable fallback for set_jid_role() because users may have multiple roles, so that's an error. --- core/usermanager.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index ec782906..09beb6d4 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -10,7 +10,7 @@ local modulemanager = require "core.modulemanager"; local log = require "util.logger".init("usermanager"); local type = type; local it = require "util.iterators"; -local jid_prep = require "util.jid".prep; +local jid_prep, jid_split = require "util.jid".prep, require "util.jid".split; local config = require "core.configmanager"; local sasl_new = require "util.sasl".new; local storagemanager = require "core.storagemanager"; @@ -196,12 +196,20 @@ end local function get_jid_role(jid, host) host = host or "*"; local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; + local jid_node, jid_host = jid_split(jid); + if host == jid_host and jid_node then + return authz_provider.get_user_default_role(jid_node); + end return authz_provider.get_jid_role(jid); end local function set_jid_role(jid, host, role_name) host = host or "*"; local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; + local _, jid_host = jid_split(jid); + if host == jid_host then + return nil, "unexpected-local-jid"; + end return authz_provider.set_jid_role(jid, role_name) end -- cgit v1.2.3 From 9daa82bfc1d3c65ba8c766b277b5846755917439 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 10 Aug 2022 15:10:14 +0200 Subject: util.uuid: Fix syntax of Teal interface declaration file --- teal-src/util/uuid.d.tl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teal-src/util/uuid.d.tl b/teal-src/util/uuid.d.tl index 45fd4312..284a4e4c 100644 --- a/teal-src/util/uuid.d.tl +++ b/teal-src/util/uuid.d.tl @@ -1,5 +1,5 @@ local record lib - get_nibbles : (number) : string + get_nibbles : function (number) : string generate : function () : string seed : function (string) -- cgit v1.2.3 From d7b7e4afb5df5006db1b127e5033050d5e62d079 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 10 Aug 2022 15:10:50 +0200 Subject: util.error: Use avoid name conflict in Teal interface declaration Naming things ... Thing or thing_t? --- teal-src/util/error.d.tl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/teal-src/util/error.d.tl b/teal-src/util/error.d.tl index 05f52405..4c3a7196 100644 --- a/teal-src/util/error.d.tl +++ b/teal-src/util/error.d.tl @@ -38,7 +38,7 @@ local record protoerror code : integer end -local record error +local record Error type : error_type condition : error_condition text : string @@ -55,10 +55,10 @@ local type context = { string : any } local record error_registry_wrapper source : string registry : registry - new : function (string, context) : error - coerce : function (any, string) : any, error - wrap : function (error) : error - wrap : function (string, context) : error + new : function (string, context) : Error + coerce : function (any, string) : any, Error + wrap : function (Error) : Error + wrap : function (string, context) : Error is_error : function (any) : boolean end @@ -66,12 +66,12 @@ local record lib record configure_opt auto_inject_traceback : boolean end - new : function (protoerror, context, { string : protoerror }, string) : error + new : function (protoerror, context, { string : protoerror }, string) : Error init : function (string, string, registry | compact_registry) : error_registry_wrapper init : function (string, registry | compact_registry) : error_registry_wrapper is_error : function (any) : boolean - coerce : function (any, string) : any, error - from_stanza : function (table, context, string) : error + coerce : function (any, string) : any, Error + from_stanza : function (table, context, string) : Error configure : function end -- cgit v1.2.3 From 48ee5e3cbd3acaaeae817af0646a60dbdfa79639 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 10 Aug 2022 15:12:00 +0200 Subject: util.signal: Fix name conflict in Teal interface declaration --- teal-src/util/signal.d.tl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/teal-src/util/signal.d.tl b/teal-src/util/signal.d.tl index 8610aa7f..290cf08f 100644 --- a/teal-src/util/signal.d.tl +++ b/teal-src/util/signal.d.tl @@ -1,5 +1,5 @@ local record lib - enum signal + enum Signal "SIGABRT" "SIGALRM" "SIGBUS" @@ -33,9 +33,9 @@ local record lib "SIGXCPU" "SIGXFSZ" end - signal : function (integer | signal, function, boolean) : boolean - raise : function (integer | signal) - kill : function (integer, integer | signal) + signal : function (integer | Signal, function, boolean) : boolean + raise : function (integer | Signal) + kill : function (integer, integer | Signal) -- enum : integer end return lib -- cgit v1.2.3 From 341a5fd99fdee635257dd03d1f098a5a09338455 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 11 Aug 2022 16:47:09 +0100 Subject: features: Add "permissions" feature for role-auth --- core/features.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/features.lua b/core/features.lua index 7248f881..96023b09 100644 --- a/core/features.lua +++ b/core/features.lua @@ -4,5 +4,7 @@ return { available = set.new{ -- mod_bookmarks bundled "mod_bookmarks"; + -- Roles, module.may and per-session authz + "permissions"; }; }; -- cgit v1.2.3 From afbc47f8bafd599ab7cf7dc5dcac4931757b417a Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 11 Aug 2022 16:56:59 +0100 Subject: usermanager: Remove obsolete function from global authz provider --- core/usermanager.lua | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index 09beb6d4..9adf1f25 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -43,11 +43,6 @@ local global_admins = set.new(global_admins_config) / jid_prep; local admin_role = { ["prosody:admin"] = true }; local global_authz_provider = { get_user_roles = function (user) end; --luacheck: ignore 212/user - get_jid_roles = function (jid) - if global_admins:contains(jid) then - return admin_role; - end - end; get_jids_with_role = function (role) if role ~= "prosody:admin" then return {}; end return it.to_array(global_admins); -- cgit v1.2.3 From bd2b2af7b736d9ef13be1228432443592fbaf6cd Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 12 Aug 2022 11:58:25 +0100 Subject: usermanager: Fix method name of global authz provider (thanks Zash) --- core/usermanager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index 9adf1f25..a299f9d9 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -55,7 +55,7 @@ local global_authz_provider = { get_jid_role = function (jid) end; -- luacheck: ignore 212 set_jid_role = function (jid) end; -- luacheck: ignore 212 add_default_permission = function (role_name, action, policy) end; -- luacheck: ignore 212 - get_role_info = function (role_name) end; -- luacheck: ignore 212 + get_role_by_name = function (role_name) end; -- luacheck: ignore 212 }; local provider_mt = { __index = new_null_provider() }; -- cgit v1.2.3 From 7ccf41ebb5e7a1a21fdf5945c5dd157e40b7024c Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 12 Aug 2022 16:21:57 +0100 Subject: usermanager: Remove concept of global authz provider Rationale: - Removes a bunch of code! - We don't have many cases where an actor is not bound to one of our hosts - A notable exception is the admin shell, but if we ever attempt to lock those sessions down, there is a load of other work that also has to be done. And it's not clear if we would need a global authz provider for that anyway. - Removes an extra edge case from the necessary mental model for operators - Sessions that aren't bound to a host generally are anonymous or have an alternative auth model (such as by IP addres). - With the encapsulation now provided by util.roles, ad-hoc "detached roles" can still be created anyway by code that needs them. --- core/usermanager.lua | 58 +++++++++++----------------------------------------- 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index a299f9d9..2311ce9e 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -9,12 +9,10 @@ local modulemanager = require "core.modulemanager"; local log = require "util.logger".init("usermanager"); local type = type; -local it = require "util.iterators"; -local jid_prep, jid_split = require "util.jid".prep, require "util.jid".split; +local jid_split = require "util.jid".split; local config = require "core.configmanager"; local sasl_new = require "util.sasl".new; local storagemanager = require "core.storagemanager"; -local set = require "util.set"; local prosody = _G.prosody; local hosts = prosody.hosts; @@ -34,19 +32,9 @@ local function new_null_provider() }); end -local global_admins_config = config.get("*", "admins"); -if type(global_admins_config) ~= "table" then - global_admins_config = nil; -- TODO: factor out moduleapi magic config handling and use it here -end -local global_admins = set.new(global_admins_config) / jid_prep; - -local admin_role = { ["prosody:admin"] = true }; -local global_authz_provider = { +local fallback_authz_provider = { get_user_roles = function (user) end; --luacheck: ignore 212/user - get_jids_with_role = function (role) - if role ~= "prosody:admin" then return {}; end - return it.to_array(global_admins); - end; + get_jids_with_role = function (role) end; --luacheck: ignore 212 set_user_roles = function (user, roles) end; -- luacheck: ignore 212 set_jid_roles = function (jid, roles) end; -- luacheck: ignore 212 @@ -66,7 +54,7 @@ local function initialize_host(host) local authz_provider_name = config.get(host, "authorization") or "internal"; local authz_mod = modulemanager.load(host, "authz_"..authz_provider_name); - host_session.authz = authz_mod or global_authz_provider; + host_session.authz = authz_mod or fallback_authz_provider; if host_session.type ~= "local" then return; end @@ -155,20 +143,14 @@ local function get_user_roles(user, host) if host and not hosts[host] then return false; end if type(user) ~= "string" then return false; end - host = host or "*"; - - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; - return authz_provider.get_user_roles(user); + return hosts[host].authz.get_user_roles(user); end local function get_user_default_role(user, host) if host and not hosts[host] then return false; end if type(user) ~= "string" then return false; end - host = host or "*"; - - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; - return authz_provider.get_user_default_role(user); + return hosts[host].authz.get_user_default_role(user); end -- Accepts a set of role names which the user is allowed to assume @@ -176,10 +158,7 @@ local function set_user_roles(user, host, roles) if host and not hosts[host] then return false; end if type(user) ~= "string" then return false; end - host = host or "*"; - - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; - local ok, err = authz_provider.set_user_roles(user, roles); + local ok, err = hosts[host].authz.set_user_roles(user, roles); if ok then prosody.events.fire_event("user-roles-changed", { username = user, host = host @@ -189,50 +168,37 @@ local function set_user_roles(user, host, roles) end local function get_jid_role(jid, host) - host = host or "*"; - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; local jid_node, jid_host = jid_split(jid); if host == jid_host and jid_node then - return authz_provider.get_user_default_role(jid_node); + return hosts[host].authz.get_user_default_role(jid_node); end - return authz_provider.get_jid_role(jid); + return hosts[host].authz.get_jid_role(jid); end local function set_jid_role(jid, host, role_name) - host = host or "*"; - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; local _, jid_host = jid_split(jid); if host == jid_host then return nil, "unexpected-local-jid"; end - return authz_provider.set_jid_role(jid, role_name) + return hosts[host].authz.set_jid_role(jid, role_name) end local function get_users_with_role(role, host) if not hosts[host] then return false; end if type(role) ~= "string" then return false; end - return hosts[host].authz.get_users_with_role(role); end local function get_jids_with_role(role, host) if host and not hosts[host] then return false; end if type(role) ~= "string" then return false; end - - host = host or "*"; - - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; - return authz_provider.get_jids_with_role(role); + return hosts[host].authz.get_jids_with_role(role); end local function get_role_by_name(role_name, host) if host and not hosts[host] then return false; end if type(role_name) ~= "string" then return false; end - - host = host or "*"; - - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; - return authz_provider.get_role_by_name(role_name); + return hosts[host].authz.get_role_by_name(role_name); end return { -- cgit v1.2.3 From ae3a89375d7dcb1df54c072ddebe2a60248c58b0 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 15 Aug 2022 15:25:07 +0100 Subject: usermanager: Add back temporary is_admin to warn about deprecated API usage Goal: Introduce role-auth with minimal disruption is_admin() is unsafe in a system with per-session permissions, so it has been deprecated. Roll-out approach: 1) First, log a warning when is_admin() is used. It should continue to function normally, backed by the new role API. Nothing is really using per-session authz yet, so there is minimal security concern. The 'strict_deprecate_is_admin' global setting can be set to 'true' to force a hard failure of is_admin() attempts (it will log an error and always return false). 2) In some time (at least 1 week), but possibly longer depending on the number of affected deployments: switch 'strict_deprecate_is_admin' to 'true' by default. It can still be disabled for systems that need it. 3) Further in the future, before the next release, the option will be removed and is_admin() will be permanently disabled. --- core/usermanager.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/usermanager.lua b/core/usermanager.lua index 2311ce9e..0a2f5c4d 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -21,6 +21,8 @@ local setmetatable = setmetatable; local default_provider = "internal_hashed"; +local debug = debug; + local _ENV = nil; -- luacheck: std none @@ -183,6 +185,20 @@ local function set_jid_role(jid, host, role_name) return hosts[host].authz.set_jid_role(jid, role_name) end +local strict_deprecate_is_admin; +local legacy_admin_roles = { ["prosody:admin"] = true, ["prosody:operator"] = true }; +local function is_admin(jid, host) + if strict_deprecate_is_admin == nil then + strict_deprecate_is_admin = (config.get("*", "strict_deprecate_is_admin") == true); + end + if strict_deprecate_is_admin then + log("error", "Attempt to use deprecated is_admin() API: %s", debug.traceback()); + return false; + end + log("warn", "Usage of legacy is_admin() API, which will be disabled in a future build: %s", debug.traceback()); + return legacy_admin_roles[get_jid_role(jid, host)] or false; +end + local function get_users_with_role(role, host) if not hosts[host] then return false; end if type(role) ~= "string" then return false; end @@ -222,4 +238,7 @@ return { set_jid_role = set_jid_role; get_jids_with_role = get_jids_with_role; get_role_by_name = get_role_by_name; + + -- Deprecated + is_admin = is_admin; }; -- 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(-) 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 2b0676396dc84dad48735a9e3782bb4f13b36471 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 12 Aug 2022 22:09:09 +0200 Subject: util.roles: Add Teal interface declaration --- teal-src/util/roles.d.tl | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 teal-src/util/roles.d.tl diff --git a/teal-src/util/roles.d.tl b/teal-src/util/roles.d.tl new file mode 100644 index 00000000..fef4f88a --- /dev/null +++ b/teal-src/util/roles.d.tl @@ -0,0 +1,32 @@ +local record util_roles + + type context = any + + record Role + id : string + name : string + description : string + default : boolean + priority : number -- or integer? + permissions : { string : boolean } + + may : function (Role, string, context) + clone : function (Role, role_config) + set_permission : function (Role, string, boolean, boolean) + end + + is_role : function (any) : boolean + + record role_config + name : string + description : string + default : boolean + priority : number -- or integer? + inherits : { Role } + permissions : { string : boolean } + end + + new : function (role_config, Role) : Role +end + +return util_roles -- cgit v1.2.3 From d79609ddc922ef12032c738fdb8e602818f244e0 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 14 Aug 2022 16:51:10 +0200 Subject: util.datetime: Fix argument order in tests The expected value goes first. --- spec/util_datetime_spec.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/util_datetime_spec.lua b/spec/util_datetime_spec.lua index 497ab7d3..aff55c7b 100644 --- a/spec/util_datetime_spec.lua +++ b/spec/util_datetime_spec.lua @@ -16,7 +16,7 @@ describe("util.datetime", function () assert.truthy(string.find(date(), "^%d%d%d%d%-%d%d%-%d%d$")); end); it("should work", function () - assert.equals(date(1136239445), "2006-01-02"); + assert.equals("2006-01-02", date(1136239445)); end); end); describe("#time", function () @@ -32,7 +32,7 @@ describe("util.datetime", function () assert.truthy(string.find(time(), "^%d%d:%d%d:%d%d")); end); it("should work", function () - assert.equals(time(1136239445), "22:04:05"); + assert.equals("22:04:05", time(1136239445)); end); end); describe("#datetime", function () @@ -48,7 +48,7 @@ describe("util.datetime", function () assert.truthy(string.find(datetime(), "^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%d")); end); it("should work", function () - assert.equals(datetime(1136239445), "2006-01-02T22:04:05Z"); + assert.equals("2006-01-02T22:04:05Z", datetime(1136239445)); end); end); describe("#legacy", function () @@ -64,9 +64,9 @@ describe("util.datetime", function () end); it("should work", function () -- Timestamp used by Go - assert.equals(parse("2017-11-19T17:58:13Z"), 1511114293); - assert.equals(parse("2017-11-19T18:58:50+0100"), 1511114330); - assert.equals(parse("2006-01-02T15:04:05-0700"), 1136239445); + assert.equals(1511114293, parse("2017-11-19T17:58:13Z")); + assert.equals(1511114330, parse("2017-11-19T18:58:50+0100")); + assert.equals(1136239445, parse("2006-01-02T15:04:05-0700")); end); it("should handle timezones", function () -- https://xmpp.org/extensions/xep-0082.html#example-2 and 3 -- cgit v1.2.3 From c85c18b03ab627ee81273d4c0f0009e1a5cc2d03 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sun, 14 Aug 2022 16:57:31 +0200 Subject: util.datetime: Add support for sub-second precision timestamps Lua since 5.3 raises a fuss when time functions are handed a number with a fractional part and the underlying C functions are all based on integer seconds without support for more precision. --- CHANGES | 4 ++++ spec/util_datetime_spec.lua | 13 +++++++++++++ util/datetime.lua | 26 +++++++++++++++++++------- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/CHANGES b/CHANGES index 48fe37d5..7df64d70 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,10 @@ TRUNK - Advertise supported SASL Channel-Binding types (XEP-0440) - Implement RFC 9266 'tls-exporter' channel binding with TLS 1.3 +## Changes + +- Support sub-second precision timestamps + ## Removed - Lua 5.1 support diff --git a/spec/util_datetime_spec.lua b/spec/util_datetime_spec.lua index aff55c7b..a35a1037 100644 --- a/spec/util_datetime_spec.lua +++ b/spec/util_datetime_spec.lua @@ -18,6 +18,9 @@ describe("util.datetime", function () it("should work", function () assert.equals("2006-01-02", date(1136239445)); end); + it("should ignore fractional parts", function () + assert.equals("2006-01-02", date(1136239445.5)); + end); end); describe("#time", function () local time = util_datetime.time; @@ -34,6 +37,9 @@ describe("util.datetime", function () it("should work", function () assert.equals("22:04:05", time(1136239445)); end); + it("should handle precision", function () + assert.equal("14:46:32.158200", time(1660488392.1582)) + end) end); describe("#datetime", function () local datetime = util_datetime.datetime; @@ -50,6 +56,9 @@ describe("util.datetime", function () it("should work", function () assert.equals("2006-01-02T22:04:05Z", datetime(1136239445)); end); + it("should handle precision", function () + assert.equal("2022-08-14T14:46:32.158200Z", datetime(1660488392.1582)) + end) end); describe("#legacy", function () local legacy = util_datetime.legacy; @@ -72,5 +81,9 @@ describe("util.datetime", function () -- https://xmpp.org/extensions/xep-0082.html#example-2 and 3 assert.equals(parse("1969-07-21T02:56:15Z"), parse("1969-07-20T21:56:15-05:00")); end); + it("should handle precision", function () + -- floating point comparison is not an exact science + assert.truthy(math.abs(1660488392.1582 - parse("2022-08-14T14:46:32.158200Z")) < 0.001) + end) end); end); diff --git a/util/datetime.lua b/util/datetime.lua index 2d27ece4..8fa1a778 100644 --- a/util/datetime.lua +++ b/util/datetime.lua @@ -12,31 +12,42 @@ local os_date = os.date; local os_time = os.time; local os_difftime = os.difftime; +local floor = math.floor; local tonumber = tonumber; local _ENV = nil; -- luacheck: std none local function date(t) - return os_date("!%Y-%m-%d", t); + return os_date("!%Y-%m-%d", t and floor(t) or nil); end local function datetime(t) - return os_date("!%Y-%m-%dT%H:%M:%SZ", t); + if t == nil or t % 1 == 0 then + return os_date("!%Y-%m-%dT%H:%M:%SZ", t); + end + local m = t % 1; + local s = floor(t); + return os_date("!%Y-%m-%dT%H:%M:%S.%%06dZ", s):format(floor(m * 1000000)); end local function time(t) - return os_date("!%H:%M:%S", t); + if t == nil or t % 1 == 0 then + return os_date("!%H:%M:%S", t); + end + local m = t % 1; + local s = floor(t); + return os_date("!%H:%M:%S.%%06d", s):format(floor(m * 1000000)); end local function legacy(t) - return os_date("!%Y%m%dT%H:%M:%S", t); + return os_date("!%Y%m%dT%H:%M:%S", t and floor(t) or nil); end local function parse(s) if s then local year, month, day, hour, min, sec, tzd; - year, month, day, hour, min, sec, tzd = s:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d)%.?%d*([Z+%-]?.*)$"); + year, month, day, hour, min, sec, tzd = s:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d%.?%d*)([Z+%-]?.*)$"); if year then local now = os_time(); local time_offset = os_difftime(os_time(os_date("*t", now)), os_time(os_date("!*t", now))); -- to deal with local timezone @@ -49,8 +60,9 @@ local function parse(s) tzd_offset = h * 60 * 60 + m * 60; if sign == "-" then tzd_offset = -tzd_offset; end end - sec = (sec + time_offset) - tzd_offset; - return os_time({year=year, month=month, day=day, hour=hour, min=min, sec=sec, isdst=false}); + local prec = sec%1; + sec = floor(sec + time_offset) - tzd_offset; + return os_time({year=year, month=month, day=day, hour=hour, min=min, sec=sec, isdst=false})+prec; end end end -- 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 ++-- spec/scansion/mam_extended.scs | 12 ++++++------ spec/scansion/prosody.cfg.lua | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) 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" }); diff --git a/spec/scansion/mam_extended.scs b/spec/scansion/mam_extended.scs index 2c6840df..70897737 100644 --- a/spec/scansion/mam_extended.scs +++ b/spec/scansion/mam_extended.scs @@ -45,8 +45,8 @@ Romeo sends: Romeo receives: - - + + @@ -59,7 +59,7 @@ Romeo receives: - + Hello @@ -71,7 +71,7 @@ Romeo receives: - + U there? @@ -98,7 +98,7 @@ Romeo receives: - + U there? @@ -110,7 +110,7 @@ Romeo receives: - + Hello diff --git a/spec/scansion/prosody.cfg.lua b/spec/scansion/prosody.cfg.lua index 6901cc11..0779f883 100644 --- a/spec/scansion/prosody.cfg.lua +++ b/spec/scansion/prosody.cfg.lua @@ -6,8 +6,8 @@ function _G.os.time() end package.preload["util.time"] = function () return { - now = function () return 1219439344.1; end; - monotonic = function () return 0.1; end; + now = function () return 1219439344.5; end; + monotonic = function () return 0.5; end; } end -- 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(-) 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(-) 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 68867c09d6c6298fcbea882b884005dbb8aa2792 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 17 Aug 2022 18:07:31 +0200 Subject: util.datetime: Remove a line No idea why the locals were declared on a line by itself. Perhaps line length considerations? But saving 6 characters in width by adding a whole line with 47 characters seems excessive. This is still within the 150 character limit set by .luacheckrc --- util/datetime.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/util/datetime.lua b/util/datetime.lua index 8fa1a778..6df146f4 100644 --- a/util/datetime.lua +++ b/util/datetime.lua @@ -46,8 +46,7 @@ end local function parse(s) if s then - local year, month, day, hour, min, sec, tzd; - year, month, day, hour, min, sec, tzd = s:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d%.?%d*)([Z+%-]?.*)$"); + local year, month, day, hour, min, sec, tzd = s:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d%.?%d*)([Z+%-]?.*)$"); if year then local now = os_time(); local time_offset = os_difftime(os_time(os_date("*t", now)), os_time(os_date("!*t", now))); -- to deal with local timezone -- cgit v1.2.3 From cbff90a7ac5236a86accefdfd501147a1b5007fc Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 17 Aug 2022 18:41:35 +0200 Subject: util.datetime: Update Teal interface description Integers were required before, now any number should work. --- teal-src/util/datetime.d.tl | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/teal-src/util/datetime.d.tl b/teal-src/util/datetime.d.tl index 971e8f9c..9f770a73 100644 --- a/teal-src/util/datetime.d.tl +++ b/teal-src/util/datetime.d.tl @@ -1,11 +1,9 @@ --- TODO s/number/integer/ once Teal gets support for that - local record lib - date : function (t : integer) : string - datetime : function (t : integer) : string - time : function (t : integer) : string - legacy : function (t : integer) : string - parse : function (t : string) : integer + date : function (t : number) : string + datetime : function (t : number) : string + time : function (t : number) : string + legacy : function (t : number) : string + parse : function (t : string) : number end return lib -- 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. --- CHANGES | 1 + doc/doap.xml | 3 ++- plugins/mod_time.lua | 14 -------------- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/CHANGES b/CHANGES index 7df64d70..3eafb718 100644 --- a/CHANGES +++ b/CHANGES @@ -26,6 +26,7 @@ TRUNK ## Removed - Lua 5.1 support +- XEP-0090 support removed from mod_time 0.12.0 ====== diff --git a/doc/doap.xml b/doc/doap.xml index 93376268..2bddedd5 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -241,7 +241,8 @@ 1.2 0.1.0 - complete + trunk + removed mod_time 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 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 --- core/moduleapi.lua | 3 +- core/sessionmanager.lua | 2 +- core/usermanager.lua | 68 ++++++++++++----- plugins/mod_authz_internal.lua | 166 ++++++++++++++++++++++++++++++++--------- plugins/mod_c2s.lua | 2 +- plugins/mod_tokenauth.lua | 2 +- 6 files changed, 184 insertions(+), 59 deletions(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index 8790a9d3..73ce4911 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -538,6 +538,7 @@ function api:load_resource(path, mode) end function api:open_store(name, store_type) + if self.host == "*" then return nil, "global-storage-not-supported"; end return require"core.storagemanager".open(self.host, name or self.name, store_type); end @@ -629,7 +630,7 @@ function api:may(action, context) local role; local node, host = jid_split(context); if host == self.host then - role = hosts[host].authz.get_user_default_role(node); + role = hosts[host].authz.get_user_role(node); else role = hosts[self.host].authz.get_jid_role(context); end diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index 924c4968..dec21674 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -135,7 +135,7 @@ local function make_authenticated(session, username, role_name) if role_name then role = hosts[session.host].authz.get_role_by_name(role_name); else - role = hosts[session.host].authz.get_user_default_role(username); + role = hosts[session.host].authz.get_user_role(username); end if role then sessionlib.set_role(session, role); diff --git a/core/usermanager.lua b/core/usermanager.lua index 0a2f5c4d..cf54fc31 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -37,13 +37,17 @@ end local fallback_authz_provider = { get_user_roles = function (user) end; --luacheck: ignore 212/user get_jids_with_role = function (role) end; --luacheck: ignore 212 - set_user_roles = function (user, roles) end; -- luacheck: ignore 212 - set_jid_roles = function (jid, roles) end; -- luacheck: ignore 212 - get_user_default_role = function (user) end; -- luacheck: ignore 212 - get_users_with_role = function (role_name) end; -- luacheck: ignore 212 + get_user_role = function (user) end; -- luacheck: ignore 212 + set_user_role = function (user, roles) end; -- luacheck: ignore 212 + + add_user_secondary_role = function (user, host, role_name) end; --luacheck: ignore 212 + remove_user_secondary_role = function (user, host, role_name) end; --luacheck: ignore 212 + get_jid_role = function (jid) end; -- luacheck: ignore 212 - set_jid_role = function (jid) end; -- luacheck: ignore 212 + set_jid_role = function (jid, role) end; -- luacheck: ignore 212 + + get_users_with_role = function (role_name) end; -- luacheck: ignore 212 add_default_permission = function (role_name, action, policy) end; -- luacheck: ignore 212 get_role_by_name = function (role_name) end; -- luacheck: ignore 212 }; @@ -140,39 +144,63 @@ local function get_provider(host) return hosts[host].users; end --- Returns a map of { [role_name] = role, ... } that a user is allowed to assume -local function get_user_roles(user, host) +local function get_user_role(user, host) if host and not hosts[host] then return false; end if type(user) ~= "string" then return false; end - return hosts[host].authz.get_user_roles(user); + return hosts[host].authz.get_user_role(user); end -local function get_user_default_role(user, host) +local function set_user_role(user, host, role_name) if host and not hosts[host] then return false; end if type(user) ~= "string" then return false; end - return hosts[host].authz.get_user_default_role(user); + local role, err = hosts[host].authz.set_user_role(user, role_name); + if role then + prosody.events.fire_event("user-role-changed", { + username = user, host = host, role = role; + }); + end + return role, err; end --- Accepts a set of role names which the user is allowed to assume -local function set_user_roles(user, host, roles) +local function add_user_secondary_role(user, host, role_name) if host and not hosts[host] then return false; end if type(user) ~= "string" then return false; end - local ok, err = hosts[host].authz.set_user_roles(user, roles); + local role, err = hosts[host].authz.add_user_secondary_role(user, role_name); + if role then + prosody.events.fire_event("user-role-added", { + username = user, host = host, role = role; + }); + end + return role, err; +end + +local function remove_user_secondary_role(user, host, role_name) + if host and not hosts[host] then return false; end + if type(user) ~= "string" then return false; end + + local ok, err = hosts[host].authz.remove_user_secondary_role(user, role_name); if ok then - prosody.events.fire_event("user-roles-changed", { - username = user, host = host + prosody.events.fire_event("user-role-removed", { + username = user, host = host, role_name = role_name; }); end return ok, err; end +local function get_user_secondary_roles(user, host) + if host and not hosts[host] then return false; end + if type(user) ~= "string" then return false; end + + return hosts[host].authz.get_user_secondary_roles(user); +end + local function get_jid_role(jid, host) local jid_node, jid_host = jid_split(jid); if host == jid_host and jid_node then - return hosts[host].authz.get_user_default_role(jid_node); + return hosts[host].authz.get_user_role(jid_node); end return hosts[host].authz.get_jid_role(jid); end @@ -230,9 +258,11 @@ return { users = users; get_sasl_handler = get_sasl_handler; get_provider = get_provider; - get_user_default_role = get_user_default_role; - get_user_roles = get_user_roles; - set_user_roles = set_user_roles; + get_user_role = get_user_role; + set_user_role = set_user_role; + add_user_secondary_role = add_user_secondary_role; + remove_user_secondary_role = remove_user_secondary_role; + get_user_secondary_roles = get_user_secondary_roles; get_users_with_role = get_users_with_role; get_jid_role = get_jid_role; set_jid_role = set_jid_role; 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 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 +-- spec/util_dataforms_spec.lua | 2 +- util/stanza.lua | 8 ++++++++ 3 files changed, 10 insertions(+), 3 deletions(-) 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; diff --git a/spec/util_dataforms_spec.lua b/spec/util_dataforms_spec.lua index 5293238a..ab402fdb 100644 --- a/spec/util_dataforms_spec.lua +++ b/spec/util_dataforms_spec.lua @@ -130,7 +130,7 @@ describe("util.dataforms", function () assert.truthy(st.is_stanza(xform)); assert.equal("x", xform.name); assert.equal("jabber:x:data", xform.attr.xmlns); - assert.equal("FORM_TYPE", xform:find("field@var")); + assert.equal("FORM_TYPE", xform:get_child_attr("field", nil, "var")); assert.equal("xmpp:prosody.im/spec/util.dataforms#1", xform:find("field/value#")); local allowed_direct_children = { title = true, diff --git a/util/stanza.lua b/util/stanza.lua index a14be5f0..b75a1f32 100644 --- a/util/stanza.lua +++ b/util/stanza.lua @@ -176,6 +176,14 @@ function stanza_mt:get_child_text(name, xmlns) return nil; end +function stanza_mt:get_child_attr(name, xmlns, attr) + local tag = self:get_child(name, xmlns); + if tag then + return tag.attr[attr]; + end + return nil; +end + function stanza_mt:child_with_name(name) for _, child in ipairs(self.tags) do if child.name == name then return child; end -- 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 --- core/usermanager.lua | 8 ++++++++ plugins/mod_authz_internal.lua | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/core/usermanager.lua b/core/usermanager.lua index cf54fc31..4f15c302 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -164,6 +164,13 @@ local function set_user_role(user, host, role_name) return role, err; end +local function user_can_assume_role(user, host, role_name) + if host and not hosts[host] then return false; end + if type(user) ~= "string" then return false; end + + return hosts[host].authz.user_can_assume_role(user, role_name); +end + local function add_user_secondary_role(user, host, role_name) if host and not hosts[host] then return false; end if type(user) ~= "string" then return false; end @@ -260,6 +267,7 @@ return { get_provider = get_provider; get_user_role = get_user_role; set_user_role = set_user_role; + user_can_assume_role = user_can_assume_role; add_user_secondary_role = add_user_secondary_role; remove_user_secondary_role = remove_user_secondary_role; get_user_secondary_roles = get_user_secondary_roles; 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 8bed557afd8266b7c5578608c35e973f42669772 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 14:07:54 +0200 Subject: core.usermanager: Add scoped luacheck ignore rule to reduce clutter --- core/usermanager.lua | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index 4f15c302..03b27327 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -35,21 +35,22 @@ local function new_null_provider() end local fallback_authz_provider = { - get_user_roles = function (user) end; --luacheck: ignore 212/user - get_jids_with_role = function (role) end; --luacheck: ignore 212 + -- luacheck: ignore 212 + get_user_roles = function (user) end; + get_jids_with_role = function (role) end; - get_user_role = function (user) end; -- luacheck: ignore 212 - set_user_role = function (user, roles) end; -- luacheck: ignore 212 + get_user_role = function (user) end; + set_user_role = function (user, roles) end; - add_user_secondary_role = function (user, host, role_name) end; --luacheck: ignore 212 - remove_user_secondary_role = function (user, host, role_name) end; --luacheck: ignore 212 + add_user_secondary_role = function (user, host, role_name) end; + remove_user_secondary_role = function (user, host, role_name) end; - get_jid_role = function (jid) end; -- luacheck: ignore 212 - set_jid_role = function (jid, role) end; -- luacheck: ignore 212 + get_jid_role = function (jid) end; + set_jid_role = function (jid, role) end; - get_users_with_role = function (role_name) end; -- luacheck: ignore 212 - add_default_permission = function (role_name, action, policy) end; -- luacheck: ignore 212 - get_role_by_name = function (role_name) end; -- luacheck: ignore 212 + get_users_with_role = function (role_name) end; + add_default_permission = function (role_name, action, policy) end; + get_role_by_name = function (role_name) end; }; local provider_mt = { __index = new_null_provider() }; -- cgit v1.2.3 From 51b17f1c20421231995d9aecc73e95d7d3e55948 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 14:10:21 +0200 Subject: core.usermanager: Add missing methods to fallback authz provider --- core/usermanager.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/usermanager.lua b/core/usermanager.lua index 03b27327..4bb63477 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -42,9 +42,12 @@ local fallback_authz_provider = { get_user_role = function (user) end; set_user_role = function (user, roles) end; + get_user_secondary_roles = function (user) end; add_user_secondary_role = function (user, host, role_name) end; remove_user_secondary_role = function (user, host, role_name) end; + user_can_assume_role = function(user, role_name) end; + get_jid_role = function (jid) end; set_jid_role = function (jid, role) end; -- cgit v1.2.3 From 1f4b94adabd686877059752453ecc4cb247b903d Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 15:38:18 +0200 Subject: core.usermanager: Remove obsolete authz fallback method --- core/usermanager.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index 4bb63477..f8514bc4 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -36,7 +36,6 @@ end local fallback_authz_provider = { -- luacheck: ignore 212 - get_user_roles = function (user) end; get_jids_with_role = function (role) end; get_user_role = function (user) end; -- cgit v1.2.3 From b608af36ed2d57505ce4ba603dece395a68f3b17 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 15:42:07 +0200 Subject: core.usermanager: Update argument name in authz fallback method It's not plural --- core/usermanager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index f8514bc4..2c469269 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -39,7 +39,7 @@ local fallback_authz_provider = { get_jids_with_role = function (role) end; get_user_role = function (user) end; - set_user_role = function (user, roles) end; + set_user_role = function (user, role_name) end; get_user_secondary_roles = function (user) end; add_user_secondary_role = function (user, host, role_name) end; -- 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(-) 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(-) 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(-) 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(-) 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(-) 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 b79cb49bfba1d64dda54cf7243154624c53b5fb9 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 19 Aug 2022 14:24:31 +0200 Subject: net.connect: Clear TODO for Happy Eyeballs / RFC 8305, close #1246 Gotta have the DOAP references! --- doc/doap.xml | 1 + net/connect.lua | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/doap.xml b/doc/doap.xml index 2bddedd5..cf9e86c6 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -60,6 +60,7 @@ + diff --git a/net/connect.lua b/net/connect.lua index d85afcff..3cb407a1 100644 --- a/net/connect.lua +++ b/net/connect.lua @@ -3,7 +3,6 @@ local log = require "util.logger".init("net.connect"); local new_id = require "util.id".short; local timer = require "util.timer"; --- TODO #1246 Happy Eyeballs -- FIXME RFC 6724 -- FIXME Error propagation from resolvers doesn't work -- FIXME #1428 Reuse DNS resolver object between service and basic resolver -- cgit v1.2.3 From 227f6c033697210a54f671f5b9128cde8699fdcd Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 22 Aug 2022 13:03:10 +0100 Subject: CHANGES: Add role auth --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 3eafb718..d96c5d63 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,7 @@ TRUNK - Advertise supported SASL Channel-Binding types (XEP-0440) - Implement RFC 9266 'tls-exporter' channel binding with TLS 1.3 +- New role and permissions framework and API ## Changes -- 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(+) 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(-) 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 --- core/sessionmanager.lua | 46 ++++++++++++++++++++++++++++++++++- plugins/mod_c2s.lua | 8 +++++++ plugins/mod_smacks.lua | 64 ++++++++++--------------------------------------- 3 files changed, 66 insertions(+), 52 deletions(-) diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index dec21674..cdfd040f 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -10,7 +10,7 @@ local tostring, setmetatable = tostring, setmetatable; local pairs, next= pairs, next; -local hosts = prosody.hosts; +local prosody, hosts = prosody, prosody.hosts; local full_sessions = prosody.full_sessions; local bare_sessions = prosody.bare_sessions; @@ -92,6 +92,49 @@ local function retire_session(session) return setmetatable(session, resting_session); end +-- Update a session with a new one (transplanting connection, filters, etc.) +-- new_session should be discarded after this call returns +local function update_session(to_session, from_session) + to_session.log("debug", "Updating with parameters from session %s", from_session.id); + from_session.log("debug", "Session absorbed into %s", to_session.id); + + local replaced_conn = to_session.conn; + if replaced_conn then + to_session.log("debug", "closing a replaced connection for this session"); + replaced_conn:close(); + end + + to_session.ip = from_session.ip; + to_session.conn = from_session.conn; + to_session.rawsend = from_session.rawsend; + to_session.rawsend.session = to_session; + to_session.rawsend.conn = to_session.conn; + to_session.send = from_session.send; + to_session.send.session = to_session; + to_session.close = from_session.close; + to_session.filter = from_session.filter; + to_session.filter.session = to_session; + to_session.filters = from_session.filters; + to_session.send.filter = to_session.filter; + to_session.stream = from_session.stream; + to_session.secure = from_session.secure; + to_session.hibernating = nil; + to_session.resumption_counter = (to_session.resumption_counter or 0) + 1; + from_session.log = to_session.log; + from_session.type = to_session.type; + -- Inform xmppstream of the new session (passed to its callbacks) + to_session.stream:set_session(to_session); + + -- Retire the session we've pulled from, to avoid two sessions on the same connection + retire_session(from_session); + + prosody.events.fire_event("c2s-session-updated", { + session = to_session; + from_session = from_session; + replaced_conn = replaced_conn; + }); +end + local function destroy_session(session, err) (session.log or log)("debug", "Destroying session for %s (%s@%s)%s", session.full_jid or "(unknown)", session.username or "(unknown)", @@ -267,6 +310,7 @@ end return { new_session = new_session; retire_session = retire_session; + update_session = update_session; destroy_session = destroy_session; make_authenticated = make_authenticated; bind_resource = bind_resource; 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(-) 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 --- core/stanza_router.lua | 4 ++-- plugins/mod_s2s.lua | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/stanza_router.lua b/core/stanza_router.lua index b54ea1ab..89a02c02 100644 --- a/core/stanza_router.lua +++ b/core/stanza_router.lua @@ -127,7 +127,7 @@ function core_process_stanza(origin, stanza) end core_post_stanza(origin, stanza, origin.full_jid); else - local h = hosts[stanza.attr.to or origin.host or origin.to_host]; + local h = hosts[stanza.attr.to or origin.host]; if h then local event; if xmlns == nil then @@ -143,7 +143,7 @@ function core_process_stanza(origin, stanza) if h.events.fire_event(event, {origin = origin, stanza = stanza}) then return; end end if host and not hosts[host] then host = nil; end -- COMPAT: workaround for a Pidgin bug which sets 'to' to the SRV result - handle_unhandled_stanza(host or origin.host or origin.to_host, origin, stanza); + handle_unhandled_stanza(host or origin.host, origin, stanza); end end 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(-) 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 6a37ab635638f39f26359fcac9b50844a69dfdcb Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 27 Aug 2022 17:01:36 +0200 Subject: core.usermanager: Link to docs for new role API to make warning more actionable --- core/usermanager.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/core/usermanager.lua b/core/usermanager.lua index 2c469269..fcb1fa9b 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -234,6 +234,7 @@ local function is_admin(jid, host) return false; end log("warn", "Usage of legacy is_admin() API, which will be disabled in a future build: %s", debug.traceback()); + log("warn", "See https://prosody.im/doc/developers/permissions about the new permissions API"); return legacy_admin_roles[get_jid_role(jid, host)] or false; end -- cgit v1.2.3 From 597e92e569cf17881b663c59a207845e363980dc Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 27 Aug 2022 17:16:18 +0200 Subject: doap: Update for XEP-0215 advancement to Stable No other changes to account for. --- doc/doap.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/doap.xml b/doc/doap.xml index cf9e86c6..c7dc26b3 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -565,7 +565,7 @@ - 0.7.2 + 1.0.0 complete 0.12.0 mod_external_services -- 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(-) 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(-) 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 b02ab508d51481e19991053645ed48dcb819c4d9 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 29 Aug 2022 14:59:46 +0100 Subject: util.stanza: Add add_error() to simplify adding error tags to existing stanzas Some fiddling is required now in error_reply() to ensure the cursor is in the same place as before this change (a lot of code apparently uses that feature). --- spec/util_stanza_spec.lua | 14 ++++++++++++ util/stanza.lua | 56 ++++++++++++++++++++++++++--------------------- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/spec/util_stanza_spec.lua b/spec/util_stanza_spec.lua index 8732a111..e11c70dd 100644 --- a/spec/util_stanza_spec.lua +++ b/spec/util_stanza_spec.lua @@ -314,6 +314,20 @@ describe("util.stanza", function() end) end) + describe("#add_error()", function () + describe("basics", function () + local s = st.stanza("custom", { xmlns = "urn:example:foo" }); + local e = s:add_error("cancel", "not-acceptable", "UNACCEPTABLE!!!! ONE MILLION YEARS DUNGEON!") + :tag("dungeon", { xmlns = "urn:uuid:c9026187-5b05-4e70-b265-c3b6338a7d0f", period="1000000years"}); + assert.equal(s, e); + local typ, cond, text, extra = e:get_error(); + assert.equal("cancel", typ); + assert.equal("not-acceptable", cond); + assert.equal("UNACCEPTABLE!!!! ONE MILLION YEARS DUNGEON!", text); + assert.is_nil(extra); + end) + end) + describe("should reject #invalid", function () local invalid_names = { ["empty string"] = "", ["characters"] = "<>"; diff --git a/util/stanza.lua b/util/stanza.lua index b75a1f32..24f3cb33 100644 --- a/util/stanza.lua +++ b/util/stanza.lua @@ -29,6 +29,7 @@ local valid_utf8 = require "util.encodings".utf8.valid; local do_pretty_printing, termcolours = pcall(require, "util.termcolours"); local xmlns_stanzas = "urn:ietf:params:xml:ns:xmpp-stanzas"; +local xmpp_stanzas_attr = { xmlns = xmlns_stanzas }; local _ENV = nil; -- luacheck: std none @@ -396,6 +397,33 @@ function stanza_mt.get_error(stanza) return error_type, condition or "undefined-condition", text, extra_tag; end +function stanza_mt.add_error(stanza, error_type, condition, error_message, error_by) + local extra; + if type(error_type) == "table" then -- an util.error or similar object + if type(error_type.extra) == "table" then + extra = error_type.extra; + end + if type(error_type.context) == "table" and type(error_type.context.by) == "string" then error_by = error_type.context.by; end + error_type, condition, error_message = error_type.type, error_type.condition, error_type.text; + end + if stanza.attr.from == error_by then + error_by = nil; + end + stanza:tag("error", {type = error_type, by = error_by}) --COMPAT: Some day xmlns:stanzas goes here + :tag(condition, xmpp_stanzas_attr); + if extra and condition == "gone" and type(extra.uri) == "string" then + stanza:text(extra.uri); + end + stanza:up(); + if error_message then stanza:text_tag("text", error_message, xmpp_stanzas_attr); end + if extra and is_stanza(extra.tag) then + stanza:add_child(extra.tag); + elseif extra and extra.namespace and extra.condition then + stanza:tag(extra.condition, { xmlns = extra.namespace }):up(); + end + return stanza:up(); +end + local function preserialize(stanza) local s = { name = stanza.name, attr = stanza.attr }; for _, child in ipairs(stanza) do @@ -470,7 +498,6 @@ local function reply(orig) }); end -local xmpp_stanzas_attr = { xmlns = xmlns_stanzas }; local function error_reply(orig, error_type, condition, error_message, error_by) if not is_stanza(orig) then error("bad argument to error_reply: expected stanza, got "..type(orig)); @@ -479,30 +506,9 @@ local function error_reply(orig, error_type, condition, error_message, error_by) end local t = reply(orig); t.attr.type = "error"; - local extra; - if type(error_type) == "table" then -- an util.error or similar object - if type(error_type.extra) == "table" then - extra = error_type.extra; - end - if type(error_type.context) == "table" and type(error_type.context.by) == "string" then error_by = error_type.context.by; end - error_type, condition, error_message = error_type.type, error_type.condition, error_type.text; - end - if t.attr.from == error_by then - error_by = nil; - end - t:tag("error", {type = error_type, by = error_by}) --COMPAT: Some day xmlns:stanzas goes here - :tag(condition, xmpp_stanzas_attr); - if extra and condition == "gone" and type(extra.uri) == "string" then - t:text(extra.uri); - end - t:up(); - if error_message then t:text_tag("text", error_message, xmpp_stanzas_attr); end - if extra and is_stanza(extra.tag) then - t:add_child(extra.tag); - elseif extra and extra.namespace and extra.condition then - t:tag(extra.condition, { xmlns = extra.namespace }):up(); - end - return t; -- stanza ready for adding app-specific errors + t:add_error(error_type, condition, error_message, error_by); + t.last_add = { t[1] }; -- ready to add application-specific errors + return t; end local function presence(attr) -- 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(-) 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(-) 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 03b3b1b9ad4b6a0b749d0345b34591694bb0cd90 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 29 Aug 2022 11:47:31 +0200 Subject: core.moduleapi: Check for local role-aware sessions before e.g. s2s The condition checked for s2sin but not s2sout, so would have ignored bidi-enabled s2sout sessions. Components as well. --- core/moduleapi.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index 73ce4911..fd54500d 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -649,7 +649,15 @@ function api:may(action, context) if type(session) ~= "table" then error("Unable to identify actor session from context"); end - if session.type == "s2sin" or (session.type == "c2s" and session.host ~= self.host) then + if session.role and session.type == "c2s" and session.host == self.host then + local permit = session.role:may(action, context); + if not permit then + self:log("debug", "Access denied: session %s (%s) may not %s (not permitted by role %s)", + session.id, session.full_jid, action, session.role.name + ); + end + return permit; + else local actor_jid = context.stanza.attr.from; local role = hosts[self.host].authz.get_jid_role(actor_jid); if not role then @@ -661,14 +669,6 @@ function api:may(action, context) self:log("debug", "Access denied: JID <%s> may not %s (not permitted by role %s)", actor_jid, action, role.name); end return permit; - elseif session.role then - local permit = session.role:may(action, context); - if not permit then - self:log("debug", "Access denied: session %s (%s) may not %s (not permitted by role %s)", - session.id, session.full_jid, action, session.role.name - ); - end - return permit; end 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(-) 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 6a64363e782305d17f17bbd22bf940af00108ad2 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 1 Jul 2022 15:11:08 +0100 Subject: util-src: Add new utility header managed_pointer.h The macros in this header allow creation of GC-managed objects from manually- managed C alloc/free APIs. --- util-src/managed_pointer.h | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 util-src/managed_pointer.h diff --git a/util-src/managed_pointer.h b/util-src/managed_pointer.h new file mode 100644 index 00000000..213b5fd7 --- /dev/null +++ b/util-src/managed_pointer.h @@ -0,0 +1,61 @@ +/* managed_pointer.h + +These macros allow wrapping an allocator/deallocator into an object that is +owned and managed by the Lua garbage collector. + +Why? It is too easy to leak objects that need to be manually released, especially +when dealing with the Lua API which can throw errors from many operations. + +USAGE +----- + +For example, given an object that can be created or released with the following +functions: + + fancy_buffer* new_buffer(); + void free_buffer(fancy_buffer* p_buffer) + +You could declare a managed version like so: + + MANAGED_POINTER_ALLOCATOR(new_managed_buffer, fancy_buffer*, new_buffer, free_buffer) + +And then, when you need to create a new fancy_buffer in your code: + + fancy_buffer *my_buffer = new_managed_buffer(L); + +NOTES +----- + +Managed objects MUST NOT be freed manually. They will automatically be +freed during the next GC sweep after your function exits (even if via an error). + +The managed object is pushed onto the stack, but should generally be ignored, +but you'll need to bear this in mind when creating managed pointers in the +middle of a sequence of stack operations. +*/ + +#define MANAGED_POINTER_MT(wrapped_type) #wrapped_type "_managedptr_mt" + +#define MANAGED_POINTER_ALLOCATOR(name, wrapped_type, wrapped_alloc, wrapped_free) \ + static int _release_ ## name(lua_State *L) { \ + wrapped_type *p = (wrapped_type*)lua_topointer(L, 1); \ + if(*p != NULL) { \ + wrapped_free(*p); \ + } \ + return 0; \ + } \ + static wrapped_type name(lua_State *L) { \ + wrapped_type *p = (wrapped_type*)lua_newuserdata(L, sizeof(wrapped_type)); \ + if(luaL_newmetatable(L, MANAGED_POINTER_MT(wrapped_type)) != 0) { \ + lua_pushcfunction(L, _release_ ## name); \ + lua_setfield(L, -2, "__gc"); \ + } \ + lua_setmetatable(L, -2); \ + *p = wrapped_alloc(); \ + if(*p == NULL) { \ + lua_pushliteral(L, "not enough memory"); \ + lua_error(L); \ + } \ + return *p; \ + } + -- cgit v1.2.3 From b357cf1be1572ae4298f5ef3b43134dd00bc5895 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 24 Jun 2022 16:56:16 +0100 Subject: util.crypto: New wrapper for some operations in OpenSSL's libcrypto Specifically, ED25519 key generation/import/export, sign/verify operations, and AES encrypt/decrypt. --- spec/util_crypto_spec.lua | 196 ++++++++++++++++ util-src/GNUmakefile | 4 +- util-src/crypto.c | 555 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 753 insertions(+), 2 deletions(-) create mode 100644 spec/util_crypto_spec.lua create mode 100644 util-src/crypto.c diff --git a/spec/util_crypto_spec.lua b/spec/util_crypto_spec.lua new file mode 100644 index 00000000..19af1395 --- /dev/null +++ b/spec/util_crypto_spec.lua @@ -0,0 +1,196 @@ +local test_keys = { + ecdsa_private_pem = [[ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg7taVK6bPtPz4ah32 +aD9CfvOah5omBxRVtzypwQXvZeahRANCAAQpKFeNIy27+lVo6bJslO6r2ty5rlb5 +xEiCx8GrrbJ8S7b5IPZCS7OrBaO2iqgOf7NMsgO12eLCfMZRnA+gCC34 +-----END PRIVATE KEY----- +]]; + + ecdsa_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKShXjSMtu/pVaOmybJTuq9rcua5W ++cRIgsfBq62yfEu2+SD2QkuzqwWjtoqoDn+zTLIDtdniwnzGUZwPoAgt+A== +-----END PUBLIC KEY----- +]]; + + eddsa_private_pem = [[ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIOmrajEfnqdzdJzkJ4irQMCGbYRqrl0RlwPHIw+a5b7M +-----END PRIVATE KEY----- +]]; + + eddsa_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAFipbSXeGvPVK7eA4+hIOdutZTUUyXswVSbMGi0j1QKE= +-----END PUBLIC KEY----- +]]; + +}; + +describe("util.crypto", function () + local crypto = require "util.crypto"; + local random = require "util.random"; + + describe("generate_ed25519_keypair", function () + local keypair = crypto.generate_ed25519_keypair(); + assert.is_not_nil(keypair); + assert.equal("ED25519", keypair:get_type()); + end) + + describe("import_private_pem", function () + it("can import ECDSA keys", function () + local ecdsa_key = crypto.import_private_pem(test_keys.ecdsa_private_pem); + assert.equal("id-ecPublicKey", ecdsa_key:get_type()); + end); + + it("can import EdDSA (Ed25519) keys", function () + local ed25519_key = crypto.import_private_pem(crypto.generate_ed25519_keypair():private_pem()); + assert.equal("ED25519", ed25519_key:get_type()); + end); + + it("can import RSA keys", function () + -- TODO + end); + + it("rejects invalid keys", function () + assert.is_nil(crypto.import_private_pem(test_keys.eddsa_public_pem)); + assert.is_nil(crypto.import_private_pem(test_keys.ecdsa_public_pem)); + assert.is_nil(crypto.import_private_pem("foo")); + assert.is_nil(crypto.import_private_pem("")); + end); + end); + + describe("import_public_pem", function () + it("can import ECDSA public keys", function () + local ecdsa_key = crypto.import_public_pem(test_keys.ecdsa_public_pem); + assert.equal("id-ecPublicKey", ecdsa_key:get_type()); + end); + + it("can import EdDSA (Ed25519) public keys", function () + local ed25519_key = crypto.import_public_pem(test_keys.eddsa_public_pem); + assert.equal("ED25519", ed25519_key:get_type()); + end); + + it("can import RSA public keys", function () + -- TODO + end); + end); + + describe("PEM export", function () + it("works", function () + local ecdsa_key = crypto.import_public_pem(test_keys.ecdsa_public_pem); + assert.equal("id-ecPublicKey", ecdsa_key:get_type()); + assert.equal(test_keys.ecdsa_public_pem, ecdsa_key:public_pem()); + + assert.has_error(function () + -- Fails because private key is not available + ecdsa_key:private_pem(); + end); + + local ecdsa_private_key = crypto.import_private_pem(test_keys.ecdsa_private_pem); + assert.equal(test_keys.ecdsa_private_pem, ecdsa_private_key:private_pem()); + end); + end); + + describe("sign/verify with", function () + local test_cases = { + ed25519 = { + crypto.ed25519_sign, crypto.ed25519_verify; + key = crypto.import_private_pem(test_keys.eddsa_private_pem); + sig_length = 64; + }; + ecdsa = { + crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify; + key = crypto.import_private_pem(test_keys.ecdsa_private_pem); + }; + }; + for test_name, test in pairs(test_cases) do + local key = test.key; + describe(test_name, function () + it("works", function () + local sign, verify = test[1], test[2]; + local sig = assert(sign(key, "Hello world")); + assert.is_string(sig); + if test.sig_length then + assert.equal(test.sig_length, #sig); + end + + do + local ok = verify(key, "Hello world", sig); + assert.is_truthy(ok); + end + do -- Incorrect signature + local ok = verify(key, "Hello world", sig:sub(1, -2)..string.char((sig:byte(-1)+1)%255)); + assert.is_falsy(ok); + end + do -- Incorrect message + local ok = verify(key, "Hello earth", sig); + assert.is_falsy(ok); + end + do -- Incorrect message (embedded NUL) + local ok = verify(key, "Hello world\0foo", sig); + assert.is_falsy(ok); + end + end); + end); + end + end); + + describe("ECDSA signatures", function () + local hex = require "util.hex"; + local sig = hex.decode((([[ + 304402203e936e7b0bc62887e0e9d675afd08531a930384cfcf301 + f25d13053a2ebf141d02205a5a7c7b7ac5878d004cb79b17b39346 + 6b0cd1043718ffc31c153b971d213a8e + ]]):gsub("%s+", ""))); + it("can be parsed", function () + local r, s = crypto.parse_ecdsa_signature(sig); + assert.is_string(r); + assert.is_string(s); + assert.equal(32, #r); + assert.equal(32, #s); + end); + it("fails to parse invalid signatures", function () + local invalid_sigs = { + ""; + "\000"; + string.rep("\000", 64); + string.rep("\000", 72); + string.rep("\000", 256); + string.rep("\255", 72); + string.rep("\255", 3); + }; + for _, sig in ipairs(invalid_sigs) do + local r, s = crypto.parse_ecdsa_signature(""); + assert.is_nil(r); + assert.is_nil(s); + end + + end); + it("can be built", function () + local r, s = crypto.parse_ecdsa_signature(sig); + local rebuilt_sig = crypto.build_ecdsa_signature(r, s); + assert.equal(sig, rebuilt_sig); + end); + end); + + describe("AES-GCM encryption", function () + it("works", function () + local message = "foo\0bar"; + local key_128_bit = random.bytes(16); + local key_256_bit = random.bytes(32); + local test_cases = { + { crypto.aes_128_gcm_encrypt, crypto.aes_128_gcm_decrypt, key = key_128_bit }; + { crypto.aes_256_gcm_encrypt, crypto.aes_256_gcm_decrypt, key = key_256_bit }; + }; + for _, params in pairs(test_cases) do + local iv = params.iv or random.bytes(12); + local encrypted = params[1](params.key, iv, message); + assert.not_equal(message, encrypted); + local decrypted = params[2](params.key, iv, encrypted); + assert.equal(message, decrypted); + end + end); + end); +end); diff --git a/util-src/GNUmakefile b/util-src/GNUmakefile index 810f39f7..3f539387 100644 --- a/util-src/GNUmakefile +++ b/util-src/GNUmakefile @@ -8,7 +8,7 @@ TARGET?=../util/ ALL=encodings.so hashes.so net.so pposix.so signal.so table.so \ ringbuffer.so time.so poll.so compat.so strbitop.so \ - struct.so + struct.so crypto.so ifdef RANDOM ALL+=crand.so @@ -28,7 +28,7 @@ clean: encodings.o: CFLAGS+=$(IDNA_FLAGS) encodings.so: LDLIBS+=$(IDNA_LIBS) -hashes.so: LDLIBS+=$(OPENSSL_LIBS) +crypto.so hashes.so: LDLIBS+=$(OPENSSL_LIBS) crand.o: CFLAGS+=-DWITH_$(RANDOM) crand.so: LDLIBS+=$(RANDOM_LIBS) diff --git a/util-src/crypto.c b/util-src/crypto.c new file mode 100644 index 00000000..33c175a9 --- /dev/null +++ b/util-src/crypto.c @@ -0,0 +1,555 @@ +/* Prosody IM +-- Copyright (C) 2022 Matthew Wild +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +*/ + +/* +* crypto.c +* Lua library for cryptographic operations using OpenSSL +*/ + +#include +#include + +#ifdef _MSC_VER +typedef unsigned __int32 uint32_t; +#else +#include +#endif + +#include "lua.h" +#include "lauxlib.h" +#include +#include +#include +#include +#include +#include + +#if (LUA_VERSION_NUM == 501) +#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) +#endif + +#include "managed_pointer.h" + +#define PKEY_MT_TAG "util.crypto key" + +static BIO* new_memory_BIO() { + return BIO_new(BIO_s_mem()); +} + +MANAGED_POINTER_ALLOCATOR(new_managed_EVP_MD_CTX, EVP_MD_CTX*, EVP_MD_CTX_new, EVP_MD_CTX_free) +MANAGED_POINTER_ALLOCATOR(new_managed_BIO_s_mem, BIO*, new_memory_BIO, BIO_free) +MANAGED_POINTER_ALLOCATOR(new_managed_EVP_CIPHER_CTX, EVP_CIPHER_CTX*, EVP_CIPHER_CTX_new, EVP_CIPHER_CTX_free) + +static EVP_PKEY* pkey_from_arg(lua_State *L, int idx, const int type, const int require_private) { + EVP_PKEY *pkey = *(EVP_PKEY**)luaL_checkudata(L, idx, PKEY_MT_TAG); + if(type || require_private) { + lua_getuservalue(L, idx); + if(type != 0) { + lua_getfield(L, -1, "type"); + if(lua_tointeger(L, -1) != type) { + luaL_argerror(L, idx, "unexpected key type"); + } + lua_pop(L, 1); + } + if(require_private != 0) { + lua_getfield(L, -1, "private"); + if(lua_toboolean(L, -1) != 1) { + luaL_argerror(L, idx, "private key expected, got public key only"); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + return pkey; +} + +static int Lpkey_finalizer(lua_State *L) { + EVP_PKEY *pkey = pkey_from_arg(L, 1, 0, 0); + EVP_PKEY_free(pkey); + return 0; +} + +static int Lpkey_meth_get_type(lua_State *L) { + EVP_PKEY *pkey = pkey_from_arg(L, 1, 0, 0); + + int key_type = EVP_PKEY_id(pkey); + lua_pushstring(L, OBJ_nid2sn(key_type)); + return 1; +} + +static int base_evp_sign(lua_State *L, const int key_type, const EVP_MD *digest_type) { + EVP_PKEY *pkey = pkey_from_arg(L, 1, key_type, 1); + luaL_Buffer sigbuf; + + size_t msg_len; + const unsigned char* msg = (unsigned char*)lua_tolstring(L, 2, &msg_len); + + size_t sig_len; + unsigned char *sig = NULL; + EVP_MD_CTX *md_ctx = new_managed_EVP_MD_CTX(L); + + if(EVP_DigestSignInit(md_ctx, NULL, digest_type, NULL, pkey) != 1) { + lua_pushnil(L); + return 1; + } + if(EVP_DigestSign(md_ctx, NULL, &sig_len, msg, msg_len) != 1) { + lua_pushnil(L); + return 1; + } + + // COMPAT w/ Lua 5.1 + luaL_buffinit(L, &sigbuf); + sig = memset(luaL_prepbuffer(&sigbuf), 0, sig_len); + + if(EVP_DigestSign(md_ctx, sig, &sig_len, msg, msg_len) != 1) { + lua_pushnil(L); + } + else { + luaL_addsize(&sigbuf, sig_len); + luaL_pushresult(&sigbuf); + return 1; + } + + return 1; +} + +static int base_evp_verify(lua_State *L, const int key_type, const EVP_MD *digest_type) { + EVP_PKEY *pkey = pkey_from_arg(L, 1, key_type, 0); + + size_t msg_len; + const unsigned char *msg = (unsigned char*)luaL_checklstring(L, 2, &msg_len); + + size_t sig_len; + const unsigned char *sig = (unsigned char*)luaL_checklstring(L, 3, &sig_len); + + EVP_MD_CTX *md_ctx = EVP_MD_CTX_new(); + + if(EVP_DigestVerifyInit(md_ctx, NULL, digest_type, NULL, pkey) != 1) { + lua_pushnil(L); + goto cleanup; + } + int result = EVP_DigestVerify(md_ctx, sig, sig_len, msg, msg_len); + if(result == 0) { + lua_pushboolean(L, 0); + } else if(result != 1) { + lua_pushnil(L); + } + else { + lua_pushboolean(L, 1); + } +cleanup: + EVP_MD_CTX_free(md_ctx); + return 1; +} + +static int Lpkey_meth_public_pem(lua_State *L) { + char *data; + size_t bytes; + EVP_PKEY *pkey = pkey_from_arg(L, 1, 0, 0); + BIO *bio = new_managed_BIO_s_mem(L); + if(PEM_write_bio_PUBKEY(bio, pkey)) { + bytes = BIO_get_mem_data(bio, &data); + if (bytes > 0) { + lua_pushlstring(L, data, bytes); + } + else { + lua_pushnil(L); + } + } + else { + lua_pushnil(L); + } + return 1; +} + +static int Lpkey_meth_private_pem(lua_State *L) { + char *data; + size_t bytes; + EVP_PKEY *pkey = pkey_from_arg(L, 1, 0, 1); + BIO *bio = new_managed_BIO_s_mem(L); + + if(PEM_write_bio_PrivateKey(bio, pkey, NULL, NULL, 0, NULL, NULL)) { + bytes = BIO_get_mem_data(bio, &data); + if (bytes > 0) { + lua_pushlstring(L, data, bytes); + } + else { + lua_pushnil(L); + } + } + else { + lua_pushnil(L); + } + return 1; +} + +/* ecdsa_sha256_sign(key, data) */ +static int Lecdsa_sha256_sign(lua_State *L) { + return base_evp_sign(L, NID_X9_62_id_ecPublicKey, EVP_sha256()); +} + +/* ecdsa_sha256_verify(key, data, sig) */ +static int Lecdsa_sha256_verify(lua_State *L) { + return base_evp_verify(L, NID_X9_62_id_ecPublicKey, EVP_sha256()); +} + +static int push_pkey(lua_State *L, EVP_PKEY *pkey, const int type, const int privkey) { + EVP_PKEY **ud = lua_newuserdata(L, sizeof(EVP_PKEY*)); + *ud = pkey; + luaL_newmetatable(L, PKEY_MT_TAG); + lua_setmetatable(L, -2); + + /* Set some info about the key and attach it as a user value */ + lua_newtable(L); + if(type != 0) { + lua_pushinteger(L, type); + lua_setfield(L, -2, "type"); + } + if(privkey != 0) { + lua_pushboolean(L, 1); + lua_setfield(L, -2, "private"); + } + lua_setuservalue(L, -2); + return 1; +} + +static int Lgenerate_ed25519_keypair(lua_State *L) { + EVP_PKEY *pkey = NULL; + EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); + + /* Generate key */ + EVP_PKEY_keygen_init(pctx); + EVP_PKEY_keygen(pctx, &pkey); + EVP_PKEY_CTX_free(pctx); + + push_pkey(L, pkey, NID_ED25519, 1); + return 1; +} + +static int Limport_private_pem(lua_State *L) { + EVP_PKEY *pkey = NULL; + + size_t privkey_bytes; + const char* privkey_data; + BIO *bio = new_managed_BIO_s_mem(L); + + privkey_data = luaL_checklstring(L, 1, &privkey_bytes); + BIO_write(bio, privkey_data, privkey_bytes); + pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); + if (pkey) { + push_pkey(L, pkey, EVP_PKEY_id(pkey), 1); + } + else { + lua_pushnil(L); + } + + return 1; +} + +static int Limport_public_pem(lua_State *L) { + EVP_PKEY *pkey = NULL; + + size_t pubkey_bytes; + const char* pubkey_data; + BIO *bio = new_managed_BIO_s_mem(L); + + pubkey_data = luaL_checklstring(L, 1, &pubkey_bytes); + BIO_write(bio, pubkey_data, pubkey_bytes); + pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); + if (pkey) { + push_pkey(L, pkey, EVP_PKEY_id(pkey), 0); + } + else { + lua_pushnil(L); + } + + return 1; +} + +static int Led25519_sign(lua_State *L) { + return base_evp_sign(L, NID_ED25519, NULL); +} + +static int Led25519_verify(lua_State *L) { + return base_evp_verify(L, NID_ED25519, NULL); +} + +/* gcm_encrypt(key, iv, plaintext) */ +static int Laes_gcm_encrypt(lua_State *L, const EVP_CIPHER *cipher, const unsigned char expected_key_len) { + EVP_CIPHER_CTX *ctx; + luaL_Buffer ciphertext_buffer; + + size_t key_len, iv_len, plaintext_len; + int ciphertext_len, final_len; + + const unsigned char *key = (unsigned char*)luaL_checklstring(L, 1, &key_len); + const unsigned char *iv = (unsigned char*)luaL_checklstring(L, 2, &iv_len); + const unsigned char *plaintext = (unsigned char*)luaL_checklstring(L, 3, &plaintext_len); + + if(key_len != expected_key_len) { + return luaL_error(L, "key must be %d bytes", expected_key_len); + } + luaL_argcheck(L, iv_len == 12, 2, "iv must be 12 bytes"); + if(lua_gettop(L) > 3) { + return luaL_error(L, "Expected 3 arguments, got %d", lua_gettop(L)); + } + + // Create and initialise the context + ctx = new_managed_EVP_CIPHER_CTX(L); + + // Initialise the encryption operation + if(1 != EVP_EncryptInit_ex(ctx, cipher, NULL, NULL, NULL)) { + return luaL_error(L, "Error while initializing encryption engine"); + } + + // Initialise key and IV + if(1 != EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv)) { + return luaL_error(L, "Error while initializing key/iv"); + } + + luaL_buffinit(L, &ciphertext_buffer); + unsigned char *ciphertext = (unsigned char*)luaL_prepbuffsize(&ciphertext_buffer, plaintext_len+16); + + if(1 != EVP_EncryptUpdate(ctx, ciphertext, &ciphertext_len, plaintext, plaintext_len)) { + return luaL_error(L, "Error while encrypting data"); + } + + /* + * Finalise the encryption. Normally ciphertext bytes may be written at + * this stage, but this does not occur in GCM mode + */ + if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + ciphertext_len, &final_len)) { + return luaL_error(L, "Error while encrypting final data"); + } + if(final_len != 0) { + return luaL_error(L, "Non-zero final data"); + } + + /* Get the tag */ + if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, ciphertext + ciphertext_len)) { + return luaL_error(L, "Unable to read AEAD tag of encrypted data"); + } + + luaL_addsize(&ciphertext_buffer, ciphertext_len + 16); + luaL_pushresult(&ciphertext_buffer); + + return 1; +} + +static int Laes_128_gcm_encrypt(lua_State *L) { + return Laes_gcm_encrypt(L, EVP_aes_128_gcm(), 16); +} + +static int Laes_256_gcm_encrypt(lua_State *L) { + return Laes_gcm_encrypt(L, EVP_aes_256_gcm(), 32); +} + +/* gcm_decrypt(key, iv, ciphertext) */ +static int Laes_gcm_decrypt(lua_State *L, const EVP_CIPHER *cipher, const unsigned char expected_key_len) { + EVP_CIPHER_CTX *ctx; + luaL_Buffer plaintext_buffer; + + size_t key_len, iv_len, ciphertext_len; + int plaintext_len, final_len; + + const unsigned char *key = (unsigned char*)luaL_checklstring(L, 1, &key_len); + const unsigned char *iv = (unsigned char*)luaL_checklstring(L, 2, &iv_len); + const unsigned char *ciphertext = (unsigned char*)luaL_checklstring(L, 3, &ciphertext_len); + + if(key_len != expected_key_len) { + return luaL_error(L, "key must be %d bytes", expected_key_len); + } + luaL_argcheck(L, iv_len == 12, 2, "iv must be 12 bytes"); + luaL_argcheck(L, ciphertext_len > 16, 3, "ciphertext must be at least 16 bytes (including tag)"); + if(lua_gettop(L) > 3) { + return luaL_error(L, "Expected 3 arguments, got %d", lua_gettop(L)); + } + + /* Create and initialise the context */ + ctx = new_managed_EVP_CIPHER_CTX(L); + + /* Initialise the decryption operation. */ + if(!EVP_DecryptInit_ex(ctx, cipher, NULL, NULL, NULL)) { + return luaL_error(L, "Error while initializing decryption engine"); + } + + /* Initialise key and IV */ + if(!EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv)) { + return luaL_error(L, "Error while initializing key/iv"); + } + + luaL_buffinit(L, &plaintext_buffer); + unsigned char *plaintext = (unsigned char*)luaL_prepbuffsize(&plaintext_buffer, ciphertext_len); + + /* + * Provide the message to be decrypted, and obtain the plaintext output. + * EVP_DecryptUpdate can be called multiple times if necessary + */ + if(!EVP_DecryptUpdate(ctx, plaintext, &plaintext_len, ciphertext, ciphertext_len-16)) { + return luaL_error(L, "Error while decrypting data"); + } + + /* Set expected tag value. Works in OpenSSL 1.0.1d and later */ + if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, (unsigned char*)ciphertext + (ciphertext_len-16))) { + return luaL_error(L, "Error while processing authentication tag"); + } + + /* + * Finalise the decryption. A positive return value indicates success, + * anything else is a failure - the plaintext is not trustworthy. + */ + int ret = EVP_DecryptFinal_ex(ctx, plaintext + plaintext_len, &final_len); + + if(ret <= 0) { + /* Verify failed */ + lua_pushnil(L); + lua_pushliteral(L, "verify-failed"); + return 2; + } + + luaL_addsize(&plaintext_buffer, plaintext_len + final_len); + luaL_pushresult(&plaintext_buffer); + return 1; +} + +static int Laes_128_gcm_decrypt(lua_State *L) { + return Laes_gcm_decrypt(L, EVP_aes_128_gcm(), 16); +} + +static int Laes_256_gcm_decrypt(lua_State *L) { + return Laes_gcm_decrypt(L, EVP_aes_256_gcm(), 32); +} + +/* r, s = parse_ecdsa_sig(sig_der) */ +static int Lparse_ecdsa_signature(lua_State *L) { + ECDSA_SIG *sig; + size_t sig_der_len; + const unsigned char *sig_der = (unsigned char*)luaL_checklstring(L, 1, &sig_der_len); + const BIGNUM *r, *s; + luaL_Buffer rb, sb; + int rlen, slen; + + sig = d2i_ECDSA_SIG(NULL, &sig_der, sig_der_len); + + if(sig == NULL) { + lua_pushnil(L); + return 1; + } + + ECDSA_SIG_get0(sig, &r, &s); + + rlen = BN_num_bytes(r); + slen = BN_num_bytes(s); + + // COMPAT w/ Lua 5.1 + #if LUAL_BUFFERSIZE < 32 + #error Configured LUAL_BUFFERSIZE is too small for this operation + #endif + + luaL_buffinit(L, &rb); + BN_bn2bin(r, (unsigned char*)luaL_prepbuffer(&rb)); + luaL_addsize(&rb, rlen); + luaL_pushresult(&rb); + + luaL_buffinit(L, &sb); + BN_bn2bin(s, (unsigned char*)luaL_prepbuffer(&sb)); + luaL_addsize(&sb, slen); + luaL_pushresult(&sb); + + ECDSA_SIG_free(sig); + + return 2; +} + +/* sig_der = build_ecdsa_signature(r, s) */ +static int Lbuild_ecdsa_signature(lua_State *L) { + ECDSA_SIG *sig = ECDSA_SIG_new(); + BIGNUM *r, *s; + luaL_Buffer sigbuf; + + size_t rlen, slen; + const unsigned char *rbin, *sbin; + + rbin = (unsigned char*)luaL_checklstring(L, 1, &rlen); + sbin = (unsigned char*)luaL_checklstring(L, 2, &slen); + + r = BN_bin2bn(rbin, (int)rlen, NULL); + s = BN_bin2bn(sbin, (int)slen, NULL); + + ECDSA_SIG_set0(sig, r, s); + + luaL_buffinit(L, &sigbuf); + + // COMPAT w/ Lua 5.1 + #if LUAL_BUFFERSIZE < 128 + #error Configured LUAL_BUFFERSIZE is too small for this operation + #endif + + unsigned char *buffer = (unsigned char*)luaL_prepbuffer(&sigbuf); + int len = i2d_ECDSA_SIG(sig, &buffer); + luaL_addsize(&sigbuf, len); + luaL_pushresult(&sigbuf); + + ECDSA_SIG_free(sig); + + return 1; +} + +static const luaL_Reg Reg[] = { + { "ed25519_sign", Led25519_sign }, + { "ed25519_verify", Led25519_verify }, + { "aes_128_gcm_encrypt", Laes_128_gcm_encrypt }, + { "aes_128_gcm_decrypt", Laes_128_gcm_decrypt }, + { "aes_256_gcm_encrypt", Laes_256_gcm_encrypt }, + { "aes_256_gcm_decrypt", Laes_256_gcm_decrypt }, + { "ecdsa_sha256_sign", Lecdsa_sha256_sign }, + { "ecdsa_sha256_verify", Lecdsa_sha256_verify }, + { "generate_ed25519_keypair", Lgenerate_ed25519_keypair }, + { "import_private_pem", Limport_private_pem }, + { "import_public_pem", Limport_public_pem }, + { "parse_ecdsa_signature", Lparse_ecdsa_signature }, + { "build_ecdsa_signature", Lbuild_ecdsa_signature }, + { NULL, NULL } +}; + +static const luaL_Reg KeyMethods[] = { + { "private_pem", Lpkey_meth_private_pem }, + { "public_pem", Lpkey_meth_public_pem }, + { "get_type", Lpkey_meth_get_type }, + { NULL, NULL } +}; + +static const luaL_Reg KeyMetatable[] = { + { "__gc", Lpkey_finalizer }, + { NULL, NULL } +}; + +LUALIB_API int luaopen_util_crypto(lua_State *L) { +#if (LUA_VERSION_NUM > 501) + luaL_checkversion(L); +#endif + + /* Initialize pkey metatable */ + luaL_newmetatable(L, PKEY_MT_TAG); + luaL_setfuncs(L, KeyMetatable, 0); + lua_newtable(L); + luaL_setfuncs(L, KeyMethods, 0); + lua_setfield(L, -2, "__index"); + lua_pop(L, 1); + + /* Initialize lib table */ + lua_newtable(L); + luaL_setfuncs(L, Reg, 0); + lua_pushliteral(L, "-3.14"); + lua_setfield(L, -2, "version"); +#ifdef OPENSSL_VERSION + lua_pushstring(L, OpenSSL_version(OPENSSL_VERSION)); + lua_setfield(L, -2, "_LIBCRYPTO_VERSION"); +#endif + return 1; +} -- cgit v1.2.3 From 4122acf8c8e80d7654e9ba7e1297a38e834f49de Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 24 Jun 2022 17:03:28 +0100 Subject: util.paseto: Implementation of PASETO v4.public tokens PASETO provides an alternative to JWT with the promise of fewer implementation pitfalls. The v4.public algorithm allows asymmetric cryptographically-verified token issuance and validation. In summary, such tokens can be issued by one party and securely verified by any other party independently using the public key of the issuer. This has a number of potential applications in a decentralized network and ecosystem such as XMPP. For example, such tokens could be combined with XEP-0317 to allow hats to be verified even in the context of a third-party MUC service. --- util/paseto.lua | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 util/paseto.lua diff --git a/util/paseto.lua b/util/paseto.lua new file mode 100644 index 00000000..5f162ad0 --- /dev/null +++ b/util/paseto.lua @@ -0,0 +1,123 @@ +local crypto = require "util.crypto"; +local json = require "util.json"; +local base64_encode = require "util.encodings".base64.encode; +local base64_decode = require "util.encodings".base64.decode; +local secure_equals = require "util.hashes".equals; +local bit = require "util.bitcompat"; +local s_pack = require "util.struct".pack; + +local s_gsub = string.gsub; + +local pubkey_methods = {}; +local privkey_methods = {}; + +local v4_public_pubkey_mt = { __index = pubkey_methods }; +local v4_public_privkey_mt = { __index = privkey_methods }; +local v4_public = {}; + +local b64url_rep = { ["+"] = "-", ["/"] = "_", ["="] = "", ["-"] = "+", ["_"] = "/" }; +local function b64url(data) + return (s_gsub(base64_encode(data), "[+/=]", b64url_rep)); +end +local function unb64url(data) + return base64_decode(s_gsub(data, "[-_]", b64url_rep).."=="); +end + +local function le64(n) + return s_pack(" Date: Thu, 30 Jun 2022 15:38:22 +0200 Subject: util.crypto: Add Teal type specification --- teal-src/util/crypto.d.tl | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 teal-src/util/crypto.d.tl diff --git a/teal-src/util/crypto.d.tl b/teal-src/util/crypto.d.tl new file mode 100644 index 00000000..cf0b0d1b --- /dev/null +++ b/teal-src/util/crypto.d.tl @@ -0,0 +1,29 @@ +local record lib + record key + private_pem : function (key) : string + public_pem : function (key) : string + get_type : function (key) : string + end + + generate_ed25519_keypair : function () : key + ed25519_sign : function (key, string) : string + ed25519_verify : function (key, string, string) : boolean + + ecdsa_sha256_sign : function (key, string) : string + ecdsa_sha256_verify : function (key, string, string) : boolean + parse_ecdsa_signature : function (string) : string, string + build_ecdsa_signature : function (string, string) : string + + import_private_pem : function (string) : key + import_public_pem : function (string) : key + + aes_128_gcm_encrypt : function (key, string, string) : string + aes_128_gcm_decrypt : function (key, string, string) : string + aes_256_gcm_encrypt : function (key, string, string) : string + aes_256_gcm_decrypt : function (key, string, string) : string + + + version : string + _LIBCRYPTO_VERSION : string +end +return lib -- cgit v1.2.3 From ae16ddcac75cb94ea1d699b19b0bd8ed37fd5030 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 1 Jul 2022 18:51:15 +0100 Subject: util.jwt: Add support/tests for ES256 via improved API and using util.crypto In many cases code will be either signing or verifying. With asymmetric algorithms it's clearer and more efficient to just state that once, instead of passing keys (and possibly other parameters) with every sign/verify call. This also allows earlier validation of the key used. The previous (HS256-only) sign/verify methods continue to be exposed for backwards-compatibility. --- spec/util_jwt_spec.lua | 50 ++++++++++++++++++ util/jwt.lua | 140 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 171 insertions(+), 19 deletions(-) diff --git a/spec/util_jwt_spec.lua b/spec/util_jwt_spec.lua index b391a870..854688bd 100644 --- a/spec/util_jwt_spec.lua +++ b/spec/util_jwt_spec.lua @@ -16,5 +16,55 @@ describe("util.jwt", function () local ok = jwt.verify(key, token); assert.falsy(ok) end); + + it("validates ES256", function () + local private_key = [[ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2 +OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r +1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G +-----END PRIVATE KEY----- +]]; + + local sign = jwt.new_signer("ES256", private_key); + + local token = sign({ + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }); + + local public_key = [[ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9 +q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== +-----END PUBLIC KEY----- +]]; + local verify = jwt.new_verifier("ES256", public_key); + + local result = {verify(token)}; + assert.same({ + true; -- success + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }, result); + + local result = {verify[[eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA]]}; + assert.same({ + true; -- success + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }, result); + end); + end); diff --git a/util/jwt.lua b/util/jwt.lua index bf106dfa..58888b5d 100644 --- a/util/jwt.lua +++ b/util/jwt.lua @@ -1,4 +1,5 @@ local s_gsub = string.gsub; +local crypto = require "util.crypto"; local json = require "util.json"; local hashes = require "util.hashes"; local base64_encode = require "util.encodings".base64.encode; @@ -13,17 +14,8 @@ local function unb64url(data) return base64_decode(s_gsub(data, "[-_]", b64url_rep).."=="); end -local static_header = b64url('{"alg":"HS256","typ":"JWT"}') .. '.'; - -local function sign(key, payload) - local encoded_payload = json.encode(payload); - local signed = static_header .. b64url(encoded_payload); - local signature = hashes.hmac_sha256(key, signed); - return signed .. "." .. b64url(signature); -end - local jwt_pattern = "^(([A-Za-z0-9-_]+)%.([A-Za-z0-9-_]+))%.([A-Za-z0-9-_]+)$" -local function verify(key, blob) +local function decode_jwt(blob, expected_alg) local signed, bheader, bpayload, signature = string.match(blob, jwt_pattern); if not signed then return nil, "invalid-encoding"; @@ -31,21 +23,131 @@ local function verify(key, blob) local header = json.decode(unb64url(bheader)); if not header or type(header) ~= "table" then return nil, "invalid-header"; - elseif header.alg ~= "HS256" then + elseif header.alg ~= expected_alg then return nil, "unsupported-algorithm"; end - if not secure_equals(b64url(hashes.hmac_sha256(key, signed)), signature) then - return false, "signature-mismatch"; + return signed, signature, bpayload; +end + +local function new_static_header(algorithm_name) + return b64url('{"alg":"'..algorithm_name..'","typ":"JWT"}') .. '.'; +end + +-- HS*** family +local function new_hmac_algorithm(name, hmac) + local static_header = new_static_header(name); + + local function sign(key, payload) + local encoded_payload = json.encode(payload); + local signed = static_header .. b64url(encoded_payload); + local signature = hmac(key, signed); + return signed .. "." .. b64url(signature); + end + + local function verify(key, blob) + local signed, signature, raw_payload = decode_jwt(blob, name); + if not signed then return nil, signature; end -- nil, err + + if not secure_equals(b64url(hmac(key, signed)), signature) then + return false, "signature-mismatch"; + end + local payload, err = json.decode(unb64url(raw_payload)); + if err ~= nil then + return nil, "json-decode-error"; + end + return true, payload; end - local payload, err = json.decode(unb64url(bpayload)); - if err ~= nil then - return nil, "json-decode-error"; + + local function load_key(key) + assert(type(key) == "string", "key must be string (long, random, secure)"); + return key; + end + + return { sign = sign, verify = verify, load_key = load_key }; +end + +-- ES*** family +local function new_ecdsa_algorithm(name, c_sign, c_verify) + local static_header = new_static_header(name); + + return { + sign = function (private_key, payload) + local encoded_payload = json.encode(payload); + local signed = static_header .. b64url(encoded_payload); + + local der_sig = c_sign(private_key, signed); + + local r, s = crypto.parse_ecdsa_signature(der_sig); + + return signed.."."..b64url(r..s); + end; + + verify = function (public_key, blob) + local signed, signature, raw_payload = decode_jwt(blob, name); + if not signed then return nil, signature; end -- nil, err + + local raw_signature = unb64url(signature); + + local der_sig = crypto.build_ecdsa_signature(raw_signature:sub(1, 32), raw_signature:sub(33, 64)); + if not der_sig then + return false, "signature-mismatch"; + end + + local verify_ok = c_verify(public_key, signed, der_sig); + if not verify_ok then + return false, "signature-mismatch"; + end + + local payload, err = json.decode(unb64url(raw_payload)); + if err ~= nil then + return nil, "json-decode-error"; + end + + return true, payload; + end; + + load_public_key = function (public_key_pem) + local key = assert(crypto.import_public_pem(public_key_pem)); + assert(key:get_type() == "id-ecPublicKey", "incorrect key type"); + return key; + end; + + load_private_key = function (private_key_pem) + local key = assert(crypto.import_private_pem(private_key_pem)); + assert(key:get_type() == "id-ecPublicKey", "incorrect key type"); + return key; + end; + }; +end + +local algorithms = { + HS256 = new_hmac_algorithm("HS256", hashes.hmac_sha256); + ES256 = new_ecdsa_algorithm("ES256", crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify); +}; + +local function new_signer(algorithm, key_input) + local impl = assert(algorithms[algorithm], "Unknown JWT algorithm: "..algorithm); + local key = (impl.load_private_key or impl.load_key)(key_input); + local sign = impl.sign; + return function (payload) + return sign(key, payload); + end +end + +local function new_verifier(algorithm, key_input) + local impl = assert(algorithms[algorithm], "Unknown JWT algorithm: "..algorithm); + local key = (impl.load_public_key or impl.load_key)(key_input); + local verify = impl.verify; + return function (token) + return verify(key, token); end - return true, payload; end return { - sign = sign; - verify = verify; + new_signer = new_signer; + new_verifier = new_verifier; + -- Deprecated + sign = algorithms.HS256.sign; + verify = algorithms.HS256.verify; }; -- cgit v1.2.3 From ba282f10702985ba67b97930ea946524fcce701e Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 2 Jul 2022 11:50:56 +0100 Subject: util.crypto: Add support for RSA signatures (PKCS1-v1.5 + PSS) These are used by the RS*** and PS*** family of JOSE algorithms (e.g. in JWTs) --- util-src/crypto.c | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/util-src/crypto.c b/util-src/crypto.c index 33c175a9..49461a7e 100644 --- a/util-src/crypto.c +++ b/util-src/crypto.c @@ -47,11 +47,13 @@ MANAGED_POINTER_ALLOCATOR(new_managed_EVP_CIPHER_CTX, EVP_CIPHER_CTX*, EVP_CIPHE static EVP_PKEY* pkey_from_arg(lua_State *L, int idx, const int type, const int require_private) { EVP_PKEY *pkey = *(EVP_PKEY**)luaL_checkudata(L, idx, PKEY_MT_TAG); + int got_type; if(type || require_private) { lua_getuservalue(L, idx); if(type != 0) { lua_getfield(L, -1, "type"); - if(lua_tointeger(L, -1) != type) { + got_type = lua_tointeger(L, -1); + if(got_type != type) { luaL_argerror(L, idx, "unexpected key type"); } lua_pop(L, 1); @@ -83,7 +85,7 @@ static int Lpkey_meth_get_type(lua_State *L) { } static int base_evp_sign(lua_State *L, const int key_type, const EVP_MD *digest_type) { - EVP_PKEY *pkey = pkey_from_arg(L, 1, key_type, 1); + EVP_PKEY *pkey = pkey_from_arg(L, 1, (key_type!=NID_rsassaPss)?key_type:NID_rsaEncryption, 1); luaL_Buffer sigbuf; size_t msg_len; @@ -97,6 +99,9 @@ static int base_evp_sign(lua_State *L, const int key_type, const EVP_MD *digest_ lua_pushnil(L); return 1; } + if(key_type == NID_rsassaPss) { + EVP_PKEY_CTX_set_rsa_padding(EVP_MD_CTX_pkey_ctx(md_ctx), RSA_PKCS1_PSS_PADDING); + } if(EVP_DigestSign(md_ctx, NULL, &sig_len, msg, msg_len) != 1) { lua_pushnil(L); return 1; @@ -119,7 +124,7 @@ static int base_evp_sign(lua_State *L, const int key_type, const EVP_MD *digest_ } static int base_evp_verify(lua_State *L, const int key_type, const EVP_MD *digest_type) { - EVP_PKEY *pkey = pkey_from_arg(L, 1, key_type, 0); + EVP_PKEY *pkey = pkey_from_arg(L, 1, (key_type!=NID_rsassaPss)?key_type:NID_rsaEncryption, 0); size_t msg_len; const unsigned char *msg = (unsigned char*)luaL_checklstring(L, 2, &msg_len); @@ -133,6 +138,9 @@ static int base_evp_verify(lua_State *L, const int key_type, const EVP_MD *diges lua_pushnil(L); goto cleanup; } + if(key_type == NID_rsassaPss) { + EVP_PKEY_CTX_set_rsa_padding(EVP_MD_CTX_pkey_ctx(md_ctx), RSA_PKCS1_PSS_PADDING); + } int result = EVP_DigestVerify(md_ctx, sig, sig_len, msg, msg_len); if(result == 0) { lua_pushboolean(L, 0); @@ -279,6 +287,22 @@ static int Led25519_verify(lua_State *L) { return base_evp_verify(L, NID_ED25519, NULL); } +static int Lrsassa_pkcs1_256_sign(lua_State *L) { + return base_evp_sign(L, NID_rsaEncryption, EVP_sha256()); +} + +static int Lrsassa_pkcs1_256_verify(lua_State *L) { + return base_evp_verify(L, NID_rsaEncryption, EVP_sha256()); +} + +static int Lrsassa_pss_256_sign(lua_State *L) { + return base_evp_sign(L, NID_rsassaPss, EVP_sha256()); +} + +static int Lrsassa_pss_256_verify(lua_State *L) { + return base_evp_verify(L, NID_rsassaPss, EVP_sha256()); +} + /* gcm_encrypt(key, iv, plaintext) */ static int Laes_gcm_encrypt(lua_State *L, const EVP_CIPHER *cipher, const unsigned char expected_key_len) { EVP_CIPHER_CTX *ctx; @@ -503,6 +527,10 @@ static int Lbuild_ecdsa_signature(lua_State *L) { static const luaL_Reg Reg[] = { { "ed25519_sign", Led25519_sign }, { "ed25519_verify", Led25519_verify }, + { "rsassa_pkcs1_256_sign", Lrsassa_pkcs1_256_sign }, + { "rsassa_pkcs1_256_verify", Lrsassa_pkcs1_256_verify }, + { "rsassa_pss_256_sign", Lrsassa_pss_256_sign }, + { "rsassa_pss_256_verify", Lrsassa_pss_256_verify }, { "aes_128_gcm_encrypt", Laes_128_gcm_encrypt }, { "aes_128_gcm_decrypt", Laes_128_gcm_decrypt }, { "aes_256_gcm_encrypt", Laes_256_gcm_encrypt }, -- cgit v1.2.3 From d7b32f1b71f4806081a99c37296f316942fc03ac Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 2 Jul 2022 11:51:24 +0100 Subject: util.crypto: Friendlier error message on incorrect key types --- util-src/crypto.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/util-src/crypto.c b/util-src/crypto.c index 49461a7e..f2192ce2 100644 --- a/util-src/crypto.c +++ b/util-src/crypto.c @@ -45,6 +45,8 @@ MANAGED_POINTER_ALLOCATOR(new_managed_EVP_MD_CTX, EVP_MD_CTX*, EVP_MD_CTX_new, E MANAGED_POINTER_ALLOCATOR(new_managed_BIO_s_mem, BIO*, new_memory_BIO, BIO_free) MANAGED_POINTER_ALLOCATOR(new_managed_EVP_CIPHER_CTX, EVP_CIPHER_CTX*, EVP_CIPHER_CTX_new, EVP_CIPHER_CTX_free) +#define CRYPTO_KEY_TYPE_ERR "unexpected key type: got '%s', expected '%s'" + static EVP_PKEY* pkey_from_arg(lua_State *L, int idx, const int type, const int require_private) { EVP_PKEY *pkey = *(EVP_PKEY**)luaL_checkudata(L, idx, PKEY_MT_TAG); int got_type; @@ -54,7 +56,10 @@ static EVP_PKEY* pkey_from_arg(lua_State *L, int idx, const int type, const int lua_getfield(L, -1, "type"); got_type = lua_tointeger(L, -1); if(got_type != type) { - luaL_argerror(L, idx, "unexpected key type"); + const char *got_key_type_name = OBJ_nid2sn(got_type); + const char *want_key_type_name = OBJ_nid2sn(type); + lua_pushfstring(L, CRYPTO_KEY_TYPE_ERR, got_key_type_name, want_key_type_name); + luaL_argerror(L, idx, lua_tostring(L, -1)); } lua_pop(L, 1); } -- cgit v1.2.3 From 0b0555c3390287d2416485b1549186a282c6d1b1 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 2 Jul 2022 12:26:43 +0100 Subject: util.jwt: Add support for RSA-based algorithms (RS256, PS256) --- spec/util_jwt_spec.lua | 170 +++++++++++++++++++++++++++++++++++++++++++++++++ util/jwt.lua | 49 ++++++++++---- 2 files changed, 205 insertions(+), 14 deletions(-) diff --git a/spec/util_jwt_spec.lua b/spec/util_jwt_spec.lua index 854688bd..77b6b30d 100644 --- a/spec/util_jwt_spec.lua +++ b/spec/util_jwt_spec.lua @@ -8,6 +8,9 @@ describe("util.jwt", function () local ok, parsed = jwt.verify(key, token); assert.truthy(ok) assert.same({ payload = "this" }, parsed); + + + end); it("rejects invalid", function () local key = "secret"; @@ -17,6 +20,21 @@ describe("util.jwt", function () assert.falsy(ok) end); + it("validates HS256", function () + local verify = jwt.new_verifier("HS256", "your-256-bit-secret"); + + local result = {verify([[eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c]])}; + assert.same({ + true; -- success + { -- payload + sub = "1234567890"; + name = "John Doe"; + iat = 1516239022; + }; + }, result); + + end); + it("validates ES256", function () local private_key = [[ -----BEGIN PRIVATE KEY----- @@ -66,5 +84,157 @@ q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== }, result); end); + it("validates RS256", function () + local private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj +MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu +NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ +qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg +p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR +ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi +VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV +laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8 +sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H +mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY +dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw +ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ +DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T +N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t +0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv +t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU +AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk +48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL +DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK +xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA +mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh +2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz +et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr +VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD +TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc +dn/RsYEONbwQSjIfMPkvxF+8HQ== +-----END PRIVATE KEY----- +]]; + local sign = jwt.new_signer("RS256", private_key); + + local token = sign({ + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }); + + local public_key = [[ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo +4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u ++qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh +kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ +0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg +cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc +mwIDAQAB +-----END PUBLIC KEY----- +]]; + local verify = jwt.new_verifier("RS256", public_key); + + local result = {verify(token)}; + assert.same({ + true; -- success + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }, result); + + local result = {verify[[eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ]]}; + assert.same({ + true; -- success + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }, result); + end); + + it("validates PS256", function () + local private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj +MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu +NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ +qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg +p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR +ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi +VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV +laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8 +sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H +mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY +dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw +ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ +DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T +N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t +0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv +t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU +AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk +48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL +DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK +xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA +mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh +2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz +et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr +VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD +TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc +dn/RsYEONbwQSjIfMPkvxF+8HQ== +-----END PRIVATE KEY----- +]]; + local sign = jwt.new_signer("PS256", private_key); + + local token = sign({ + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }); + + local public_key = [[ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo +4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u ++qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh +kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ +0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg +cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc +mwIDAQAB +-----END PUBLIC KEY----- +]]; + local verify = jwt.new_verifier("PS256", public_key); + + local result = {verify(token)}; + assert.same({ + true; -- success + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }, result); + + local result = {verify[[eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.iOeNU4dAFFeBwNj6qdhdvm-IvDQrTa6R22lQVJVuWJxorJfeQww5Nwsra0PjaOYhAMj9jNMO5YLmud8U7iQ5gJK2zYyepeSuXhfSi8yjFZfRiSkelqSkU19I-Ja8aQBDbqXf2SAWA8mHF8VS3F08rgEaLCyv98fLLH4vSvsJGf6ueZSLKDVXz24rZRXGWtYYk_OYYTVgR1cg0BLCsuCvqZvHleImJKiWmtS0-CymMO4MMjCy_FIl6I56NqLE9C87tUVpo1mT-kbg5cHDD8I7MjCW5Iii5dethB4Vid3mZ6emKjVYgXrtkOQ-JyGMh6fnQxEFN1ft33GX2eRHluK9eg]]}; + assert.same({ + true; -- success + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }, result); + end); + end); diff --git a/util/jwt.lua b/util/jwt.lua index 58888b5d..433a69f6 100644 --- a/util/jwt.lua +++ b/util/jwt.lua @@ -66,8 +66,7 @@ local function new_hmac_algorithm(name, hmac) return { sign = sign, verify = verify, load_key = load_key }; end --- ES*** family -local function new_ecdsa_algorithm(name, c_sign, c_verify) +local function new_crypto_algorithm(name, key_type, c_sign, c_verify, sig_encode, sig_decode) local static_header = new_static_header(name); return { @@ -75,25 +74,27 @@ local function new_ecdsa_algorithm(name, c_sign, c_verify) local encoded_payload = json.encode(payload); local signed = static_header .. b64url(encoded_payload); - local der_sig = c_sign(private_key, signed); - - local r, s = crypto.parse_ecdsa_signature(der_sig); + local signature = c_sign(private_key, signed); + if sig_encode then + signature = sig_encode(signature); + end - return signed.."."..b64url(r..s); + return signed.."."..b64url(signature); end; - verify = function (public_key, blob) + verify = function (public_key, blob) local signed, signature, raw_payload = decode_jwt(blob, name); if not signed then return nil, signature; end -- nil, err - local raw_signature = unb64url(signature); - - local der_sig = crypto.build_ecdsa_signature(raw_signature:sub(1, 32), raw_signature:sub(33, 64)); - if not der_sig then + signature = unb64url(signature); + if sig_decode and signature then + signature = sig_decode(signature); + end + if not signature then return false, "signature-mismatch"; end - local verify_ok = c_verify(public_key, signed, der_sig); + local verify_ok = c_verify(public_key, signed, signature); if not verify_ok then return false, "signature-mismatch"; end @@ -108,21 +109,41 @@ local function new_ecdsa_algorithm(name, c_sign, c_verify) load_public_key = function (public_key_pem) local key = assert(crypto.import_public_pem(public_key_pem)); - assert(key:get_type() == "id-ecPublicKey", "incorrect key type"); + assert(key:get_type() == key_type, "incorrect key type"); return key; end; load_private_key = function (private_key_pem) local key = assert(crypto.import_private_pem(private_key_pem)); - assert(key:get_type() == "id-ecPublicKey", "incorrect key type"); + assert(key:get_type() == key_type, "incorrect key type"); return key; end; }; end +-- RS***, PS*** +local function new_rsa_algorithm(name, c_sign, c_verify) + return new_crypto_algorithm(name, "rsaEncryption", c_sign, c_verify); +end + +-- ES*** +local function new_ecdsa_algorithm(name, c_sign, c_verify) + local function encode_ecdsa_sig(der_sig) + local r, s = crypto.parse_ecdsa_signature(der_sig); + return r..s; + end + + local function decode_ecdsa_sig(jwk_sig) + return crypto.build_ecdsa_signature(jwk_sig:sub(1, 32), jwk_sig:sub(33, 64)); + end + return new_crypto_algorithm(name, "id-ecPublicKey", c_sign, c_verify, encode_ecdsa_sig, decode_ecdsa_sig); +end + local algorithms = { HS256 = new_hmac_algorithm("HS256", hashes.hmac_sha256); ES256 = new_ecdsa_algorithm("ES256", crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify); + RS256 = new_rsa_algorithm("RS256", crypto.rsassa_pkcs1_256_sign, crypto.rsassa_pkcs1_256_verify); + PS256 = new_rsa_algorithm("PS256", crypto.rsassa_pss_256_sign, crypto.rsassa_pss_256_verify); }; local function new_signer(algorithm, key_input) -- cgit v1.2.3 From 4d94a62f9b6f1a5e7f89a2204ffb7697d9018a9f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 2 Jul 2022 12:36:52 +0100 Subject: spec: Move test crypto keys to a shared file for clarity and easy maintenance --- spec/inputs/test_keys.lua | 74 +++++++++++++++++++++++++++++++++ spec/util_crypto_spec.lua | 30 +------------- spec/util_jwt_spec.lua | 103 ++++------------------------------------------ 3 files changed, 82 insertions(+), 125 deletions(-) create mode 100644 spec/inputs/test_keys.lua diff --git a/spec/inputs/test_keys.lua b/spec/inputs/test_keys.lua new file mode 100644 index 00000000..970ca110 --- /dev/null +++ b/spec/inputs/test_keys.lua @@ -0,0 +1,74 @@ +local test_keys = { + ecdsa_private_pem = [[ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2 +OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r +1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G +-----END PRIVATE KEY----- +]]; + + ecdsa_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9 +q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== +-----END PUBLIC KEY----- +]]; + + eddsa_private_pem = [[ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIOmrajEfnqdzdJzkJ4irQMCGbYRqrl0RlwPHIw+a5b7M +-----END PRIVATE KEY----- +]]; + + eddsa_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAFipbSXeGvPVK7eA4+hIOdutZTUUyXswVSbMGi0j1QKE= +-----END PUBLIC KEY----- +]]; + + rsa_private_pem = [[ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj +MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu +NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ +qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg +p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR +ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi +VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV +laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8 +sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H +mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY +dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw +ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ +DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T +N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t +0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv +t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU +AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk +48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL +DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK +xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA +mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh +2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz +et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr +VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD +TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc +dn/RsYEONbwQSjIfMPkvxF+8HQ== +-----END PRIVATE KEY----- +]]; + + rsa_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo +4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u ++qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh +kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ +0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg +cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc +mwIDAQAB +-----END PUBLIC KEY----- +]]; + +}; + +return test_keys; diff --git a/spec/util_crypto_spec.lua b/spec/util_crypto_spec.lua index 19af1395..14c35188 100644 --- a/spec/util_crypto_spec.lua +++ b/spec/util_crypto_spec.lua @@ -1,32 +1,4 @@ -local test_keys = { - ecdsa_private_pem = [[ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg7taVK6bPtPz4ah32 -aD9CfvOah5omBxRVtzypwQXvZeahRANCAAQpKFeNIy27+lVo6bJslO6r2ty5rlb5 -xEiCx8GrrbJ8S7b5IPZCS7OrBaO2iqgOf7NMsgO12eLCfMZRnA+gCC34 ------END PRIVATE KEY----- -]]; - - ecdsa_public_pem = [[ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKShXjSMtu/pVaOmybJTuq9rcua5W -+cRIgsfBq62yfEu2+SD2QkuzqwWjtoqoDn+zTLIDtdniwnzGUZwPoAgt+A== ------END PUBLIC KEY----- -]]; - - eddsa_private_pem = [[ ------BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEIOmrajEfnqdzdJzkJ4irQMCGbYRqrl0RlwPHIw+a5b7M ------END PRIVATE KEY----- -]]; - - eddsa_public_pem = [[ ------BEGIN PUBLIC KEY----- -MCowBQYDK2VwAyEAFipbSXeGvPVK7eA4+hIOdutZTUUyXswVSbMGi0j1QKE= ------END PUBLIC KEY----- -]]; - -}; +local test_keys = require "spec.inputs.test_keys"; describe("util.crypto", function () local crypto = require "util.crypto"; diff --git a/spec/util_jwt_spec.lua b/spec/util_jwt_spec.lua index 77b6b30d..8204eebf 100644 --- a/spec/util_jwt_spec.lua +++ b/spec/util_jwt_spec.lua @@ -1,4 +1,5 @@ local jwt = require "util.jwt"; +local test_keys = require "spec.inputs.test_keys"; describe("util.jwt", function () it("validates", function () @@ -36,14 +37,7 @@ describe("util.jwt", function () end); it("validates ES256", function () - local private_key = [[ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2 -OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r -1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G ------END PRIVATE KEY----- -]]; - + local private_key = test_keys.ecdsa_private_pem; local sign = jwt.new_signer("ES256", private_key); local token = sign({ @@ -53,12 +47,7 @@ OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r iat = 1516239022; }); - local public_key = [[ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9 -q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== ------END PUBLIC KEY----- -]]; + local public_key = test_keys.ecdsa_public_pem; local verify = jwt.new_verifier("ES256", public_key); local result = {verify(token)}; @@ -85,36 +74,7 @@ q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== end); it("validates RS256", function () - local private_key = [[ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj -MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu -NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ -qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg -p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR -ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi -VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV -laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8 -sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H -mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY -dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw -ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ -DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T -N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t -0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv -t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU -AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk -48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL -DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK -xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA -mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh -2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz -et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr -VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD -TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc -dn/RsYEONbwQSjIfMPkvxF+8HQ== ------END PRIVATE KEY----- -]]; + local private_key = test_keys.rsa_private_pem; local sign = jwt.new_signer("RS256", private_key); local token = sign({ @@ -124,17 +84,7 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ== iat = 1516239022; }); - local public_key = [[ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo -4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u -+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh -kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ -0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg -cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc -mwIDAQAB ------END PUBLIC KEY----- -]]; + local public_key = test_keys.rsa_public_pem; local verify = jwt.new_verifier("RS256", public_key); local result = {verify(token)}; @@ -161,36 +111,7 @@ mwIDAQAB end); it("validates PS256", function () - local private_key = [[ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj -MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu -NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ -qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg -p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR -ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi -VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV -laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8 -sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H -mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY -dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw -ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ -DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T -N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t -0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv -t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU -AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk -48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL -DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK -xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA -mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh -2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz -et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr -VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD -TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc -dn/RsYEONbwQSjIfMPkvxF+8HQ== ------END PRIVATE KEY----- -]]; + local private_key = test_keys.rsa_private_pem; local sign = jwt.new_signer("PS256", private_key); local token = sign({ @@ -200,17 +121,7 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ== iat = 1516239022; }); - local public_key = [[ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo -4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u -+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh -kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ -0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg -cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc -mwIDAQAB ------END PUBLIC KEY----- -]]; + local public_key = test_keys.rsa_public_pem; local verify = jwt.new_verifier("PS256", public_key); local result = {verify(token)}; -- cgit v1.2.3 From 74351799e9428ac7088900aab1afd6827e96eef6 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 2 Jul 2022 14:22:20 +0100 Subject: util.jwt: Overhaul of tests to use declarative approach Now we can consistently apply the same tests to every algorithm, instead of duplicating code. --- spec/inputs/test_keys.lua | 62 ++++++++++++ spec/util_jwt_spec.lua | 252 +++++++++++++++++++++++++--------------------- 2 files changed, 199 insertions(+), 115 deletions(-) diff --git a/spec/inputs/test_keys.lua b/spec/inputs/test_keys.lua index 970ca110..088332c4 100644 --- a/spec/inputs/test_keys.lua +++ b/spec/inputs/test_keys.lua @@ -1,4 +1,5 @@ local test_keys = { + -- ECDSA keypair from jwt.io ecdsa_private_pem = [[ -----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2 @@ -14,6 +15,23 @@ q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== -----END PUBLIC KEY----- ]]; + -- Self-generated ECDSA keypair + alt_ecdsa_private_pem = [[ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgQnn4AHz2Zy+JMAgp +AZfKAm9F3s6791PstPf5XjHtETKhRANCAAScv9jI3+BOXXlCOXwmQYosIbl9mf4V +uOwfIoCYSLylAghyxO0n2of8Kji+D+4C1zxNKmZIQa4s8neaIIzXnMY1 +-----END PRIVATE KEY----- +]]; + + alt_ecdsa_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnL/YyN/gTl15Qjl8JkGKLCG5fZn+ +FbjsHyKAmEi8pQIIcsTtJ9qH/Co4vg/uAtc8TSpmSEGuLPJ3miCM15zGNQ== +-----END PUBLIC KEY----- +]]; + + -- Self-generated EdDSA (Ed25519) keypair eddsa_private_pem = [[ -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIOmrajEfnqdzdJzkJ4irQMCGbYRqrl0RlwPHIw+a5b7M @@ -26,6 +44,7 @@ MCowBQYDK2VwAyEAFipbSXeGvPVK7eA4+hIOdutZTUUyXswVSbMGi0j1QKE= -----END PUBLIC KEY----- ]]; + -- RSA keypair from jwt.io rsa_private_pem = [[ -----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj @@ -69,6 +88,49 @@ mwIDAQAB -----END PUBLIC KEY----- ]]; + + -- Self-generated RSA keypair + alt_rsa_private_pem = [[ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4bt6kor2TomqRXfjCFe6T42ibatloyHntZCUdlDDAkUh4oJ/ +4jDCXAUMYqmEsZKCPXxUGQgrmSmNnJPEDMTq3XLDsjhyN4stxEi0UVAiqqBkcEnk +qbQIJSc9v5gpQF8IuJFWRvSNic0uClFL5W9R2s5AHcOhdFYKeDuitqHT5r+dC7cy +WZs5YleKaESxmK6i6wMVhL9adAilTuETyMH0yLSh+aXsPYhjns4AbjGmiKOjqd5w +sPwllEg6rGcIUi/o79z9HN8yLMXq3XNFCCA8RI4Zh3cADI1I5fe6wk1ETN+30cDw +dGQ+uQbaQrzqmKVRNjZcorMwBjsOX5AMQBFx7wIDAQABAoIBAGxj5pZpTZ4msnEL +ASQnY9oBS4ZXr8UmaamgU/mADDOR2JR4T0ngWeNvtSPG/GV70TgO9B7U8oJoFoyh +05jCEXjmO5vfSNDs7rv6oUMONKczvybABKGMRgD5F8hhGyXCvGBLwV7u3OvXbw0b +PlNcIbTsJpNkNam0CvDyyc3iZOq+HjIqituREV7lDw0rFeAR2YfEWn4VjZsQRZUZ +XkpQJ5silrXgGemIEGqVA4YyM7i2HmTiLozfVYaVckMc02VFgOaoK9Z/wGlBxtS5 +evc/IGErSA4dc7uXBEeVjhtZoBkof2JV9BNt4hl4KN9wX3tkEX5Aq1K2lirSmg2r +k+UEtwkCgYEA/5uYg25OR+jCFY/7uNS8e32Re1lgDeO+TeT1m+hcF1gCb2GBLifL +yprnuytaz1/mPqawfwbilaxntLBoa5cmNKB3zDsgv4sM451yGZ0oxU0dXpDVHblu +3nhxcaOXtb8jiSsr2MqgMbFlu7m8OupIliS+s8Pq72s6HUQQRKbJ+9MCgYEA4hQl +1W/7nDI2SR4Q3UapQnaUjmDVxX5OD+E4RpKuRF6xF7Ao2CLZusMVo8WN8YiSQP2c +RnzQNKgAVy/1zlhaaQDTs2TmSy9iStbuNZ8P+Gh6kmQXuHxwPyURSmwdpgZdL3+D +8tt6pQNQ0vsLjA9VwHmzIT+rsxPmTxKNvBdNK/UCgYByP6zqyioJMDtYAfRkiAn7 +NIQLW0Z4ztvn2zgAyNoowPjNqgpgg/8t/xEm8tjzKg0y4bSwAnbSqa3s8JCrznKQ +QU1qpt8bXl6TenNeiYWIstA2zYvEbnbkz3b9cT7FSLrse7RsgR0bOQyc3QcKWl+5 +ZJEsrpxbCVV/cUXIObi8awKBgQDOI8rfk+0bXhlrkBOWf/CjnpYUQK2LF4C8MALt +Lp/hzWmyjLihYx2eknUv0Fl966ZXxidxiisaaDlvRlbeIGfHqK5fu9fUpE7+qH2p +vPCF81YYF1YdrLF4kiby8iQSl2juf1nj3kY1IhHXXnsH6Y+qIg24emLntXRhkyxT +XffK5QKBgGbzEvVgDkerw1SiefAaZnLumJJXBlKjJ00Sq8YLeViyFC/sr4EfG/cV +7VYRhBw3e7RcYSBAA7uv8i3iIeCFjFooIZUARqXk4+yW753tY5nSJTWfkR7Bp5Pa +9jKloxckbZKMjH23a+ABOxomY3l93KOBvjLvMYqccuREOwaT12cn +-----END RSA PRIVATE KEY----- +]]; + + alt_rsa_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4bt6kor2TomqRXfjCFe6 +T42ibatloyHntZCUdlDDAkUh4oJ/4jDCXAUMYqmEsZKCPXxUGQgrmSmNnJPEDMTq +3XLDsjhyN4stxEi0UVAiqqBkcEnkqbQIJSc9v5gpQF8IuJFWRvSNic0uClFL5W9R +2s5AHcOhdFYKeDuitqHT5r+dC7cyWZs5YleKaESxmK6i6wMVhL9adAilTuETyMH0 +yLSh+aXsPYhjns4AbjGmiKOjqd5wsPwllEg6rGcIUi/o79z9HN8yLMXq3XNFCCA8 +RI4Zh3cADI1I5fe6wk1ETN+30cDwdGQ+uQbaQrzqmKVRNjZcorMwBjsOX5AMQBFx +7wIDAQAB +-----END PUBLIC KEY----- +]]; }; return test_keys; diff --git a/spec/util_jwt_spec.lua b/spec/util_jwt_spec.lua index 8204eebf..9ac3887b 100644 --- a/spec/util_jwt_spec.lua +++ b/spec/util_jwt_spec.lua @@ -21,131 +21,153 @@ describe("util.jwt", function () assert.falsy(ok) end); - it("validates HS256", function () - local verify = jwt.new_verifier("HS256", "your-256-bit-secret"); - - local result = {verify([[eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c]])}; - assert.same({ - true; -- success - { -- payload - sub = "1234567890"; - name = "John Doe"; - iat = 1516239022; + local test_cases = { + { + algorithm = "HS256"; + keys = { + { "your-256-bit-secret", "your-256-bit-secret" }; + { "another-secret", "another-secret" }; }; - }, result); - - end); - - it("validates ES256", function () - local private_key = test_keys.ecdsa_private_pem; - local sign = jwt.new_signer("ES256", private_key); - - local token = sign({ - sub = "1234567890"; - name = "John Doe"; - admin = true; - iat = 1516239022; - }); - - local public_key = test_keys.ecdsa_public_pem; - local verify = jwt.new_verifier("ES256", public_key); - - local result = {verify(token)}; - assert.same({ - true; -- success - { -- payload - sub = "1234567890"; - name = "John Doe"; - admin = true; - iat = 1516239022; + { + name = "jwt.io reference"; + [[eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c]]; + { -- payload + sub = "1234567890"; + name = "John Doe"; + iat = 1516239022; + }; }; - }, result); - - local result = {verify[[eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA]]}; - assert.same({ - true; -- success - { -- payload - sub = "1234567890"; - name = "John Doe"; - admin = true; - iat = 1516239022; + }; + { + algorithm = "ES256"; + keys = { + { test_keys.ecdsa_private_pem, test_keys.ecdsa_public_pem }; + { test_keys.alt_ecdsa_private_pem, test_keys.alt_ecdsa_public_pem }; }; - }, result); - end); - - it("validates RS256", function () - local private_key = test_keys.rsa_private_pem; - local sign = jwt.new_signer("RS256", private_key); - - local token = sign({ - sub = "1234567890"; - name = "John Doe"; - admin = true; - iat = 1516239022; - }); - - local public_key = test_keys.rsa_public_pem; - local verify = jwt.new_verifier("RS256", public_key); - - local result = {verify(token)}; - assert.same({ - true; -- success - { -- payload - sub = "1234567890"; - name = "John Doe"; - admin = true; - iat = 1516239022; + { + name = "jwt.io reference"; + [[eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA]]; + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; }; - }, result); - - local result = {verify[[eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ]]}; - assert.same({ - true; -- success - { -- payload - sub = "1234567890"; - name = "John Doe"; - admin = true; - iat = 1516239022; + }; + { + algorithm = "RS256"; + keys = { + { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; + { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; }; - }, result); - end); + { + name = "jwt.io reference"; + [[eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ]]; + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }; + }; + { + algorithm = "PS256"; + keys = { + { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; + { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; + }; + { + name = "jwt.io reference"; + [[eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.iOeNU4dAFFeBwNj6qdhdvm-IvDQrTa6R22lQVJVuWJxorJfeQww5Nwsra0PjaOYhAMj9jNMO5YLmud8U7iQ5gJK2zYyepeSuXhfSi8yjFZfRiSkelqSkU19I-Ja8aQBDbqXf2SAWA8mHF8VS3F08rgEaLCyv98fLLH4vSvsJGf6ueZSLKDVXz24rZRXGWtYYk_OYYTVgR1cg0BLCsuCvqZvHleImJKiWmtS0-CymMO4MMjCy_FIl6I56NqLE9C87tUVpo1mT-kbg5cHDD8I7MjCW5Iii5dethB4Vid3mZ6emKjVYgXrtkOQ-JyGMh6fnQxEFN1ft33GX2eRHluK9eg]]; + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }; + }; + }; - it("validates PS256", function () - local private_key = test_keys.rsa_private_pem; - local sign = jwt.new_signer("PS256", private_key); + local function do_verify_test(algorithm, verifying_key, token, expect_payload) + local verify = jwt.new_verifier(algorithm, verifying_key); - local token = sign({ + assert.is_string(token); + local result = {verify(token)}; + if expect_payload then + assert.same({ + true; -- success + expect_payload; -- payload + }, result); + else + assert.same({ + false; + "signature-mismatch"; + }, result); + end + end + + local function do_sign_verify_test(algorithm, signing_key, verifying_key, expect_success, expect_token) + local sign = jwt.new_signer(algorithm, signing_key); + + local test_payload = { sub = "1234567890"; name = "John Doe"; admin = true; iat = 1516239022; - }); - - local public_key = test_keys.rsa_public_pem; - local verify = jwt.new_verifier("PS256", public_key); - - local result = {verify(token)}; - assert.same({ - true; -- success - { -- payload - sub = "1234567890"; - name = "John Doe"; - admin = true; - iat = 1516239022; - }; - }, result); - - local result = {verify[[eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.iOeNU4dAFFeBwNj6qdhdvm-IvDQrTa6R22lQVJVuWJxorJfeQww5Nwsra0PjaOYhAMj9jNMO5YLmud8U7iQ5gJK2zYyepeSuXhfSi8yjFZfRiSkelqSkU19I-Ja8aQBDbqXf2SAWA8mHF8VS3F08rgEaLCyv98fLLH4vSvsJGf6ueZSLKDVXz24rZRXGWtYYk_OYYTVgR1cg0BLCsuCvqZvHleImJKiWmtS0-CymMO4MMjCy_FIl6I56NqLE9C87tUVpo1mT-kbg5cHDD8I7MjCW5Iii5dethB4Vid3mZ6emKjVYgXrtkOQ-JyGMh6fnQxEFN1ft33GX2eRHluK9eg]]}; - assert.same({ - true; -- success - { -- payload - sub = "1234567890"; - name = "John Doe"; - admin = true; - iat = 1516239022; - }; - }, result); - end); - + }; + + local token = sign(test_payload); + + if expect_token then + assert.equal(expect_token, token); + end + + do_verify_test(algorithm, verifying_key, token, expect_success and test_payload or false); + end + + + + for _, algorithm_tests in ipairs(test_cases) do + local algorithm = algorithm_tests.algorithm; + local keypairs = algorithm_tests.keys; + describe(algorithm, function () + it("can do basic sign and verify", function () + for _, keypair in ipairs(keypairs) do + local signing_key, verifying_key = keypair[1], keypair[2]; + do_sign_verify_test(algorithm, keypair[1], keypair[2], true); + end + end); + + if #keypairs >= 2 then + it("rejects invalid tokens", function () + do_sign_verify_test(algorithm, keypairs[1][1], keypairs[2][2], false); + end); + else + pending("rejects invalid tokens", function () + error("Needs at least 2 key pairs"); + end); + end + + if #algorithm_tests > 0 then + for test_n, test_case in ipairs(algorithm_tests) do + it("can verify "..(test_case.name or (("test case %d"):format(test_n))), function () + do_verify_test( + algorithm, + test_case.verifying_key or keypairs[1][2], + test_case[1], + test_case[2] + ); + end); + end + else + pending("can verify reference tokens", function () + error("No test tokens provided"); + end); + end + end); + end end); -- cgit v1.2.3 From 5316b0005e042f1abeb1ac5fed8715ab65e6752c Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 2 Jul 2022 14:59:52 +0100 Subject: util.crypto: More digests for sign/verify, use macros for clarity/consistency --- util-src/crypto.c | 72 ++++++++++++++++++++++++++++++------------------------- util/jwt.lua | 4 ++-- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/util-src/crypto.c b/util-src/crypto.c index f2192ce2..6822b9bf 100644 --- a/util-src/crypto.c +++ b/util-src/crypto.c @@ -201,16 +201,6 @@ static int Lpkey_meth_private_pem(lua_State *L) { return 1; } -/* ecdsa_sha256_sign(key, data) */ -static int Lecdsa_sha256_sign(lua_State *L) { - return base_evp_sign(L, NID_X9_62_id_ecPublicKey, EVP_sha256()); -} - -/* ecdsa_sha256_verify(key, data, sig) */ -static int Lecdsa_sha256_verify(lua_State *L) { - return base_evp_verify(L, NID_X9_62_id_ecPublicKey, EVP_sha256()); -} - static int push_pkey(lua_State *L, EVP_PKEY *pkey, const int type, const int privkey) { EVP_PKEY **ud = lua_newuserdata(L, sizeof(EVP_PKEY*)); *ud = pkey; @@ -292,22 +282,6 @@ static int Led25519_verify(lua_State *L) { return base_evp_verify(L, NID_ED25519, NULL); } -static int Lrsassa_pkcs1_256_sign(lua_State *L) { - return base_evp_sign(L, NID_rsaEncryption, EVP_sha256()); -} - -static int Lrsassa_pkcs1_256_verify(lua_State *L) { - return base_evp_verify(L, NID_rsaEncryption, EVP_sha256()); -} - -static int Lrsassa_pss_256_sign(lua_State *L) { - return base_evp_sign(L, NID_rsassaPss, EVP_sha256()); -} - -static int Lrsassa_pss_256_verify(lua_State *L) { - return base_evp_verify(L, NID_rsassaPss, EVP_sha256()); -} - /* gcm_encrypt(key, iv, plaintext) */ static int Laes_gcm_encrypt(lua_State *L, const EVP_CIPHER *cipher, const unsigned char expected_key_len) { EVP_CIPHER_CTX *ctx; @@ -529,22 +503,56 @@ static int Lbuild_ecdsa_signature(lua_State *L) { return 1; } +#define REG_SIGN_VERIFY(algorithm, digest) \ + { #algorithm "_" #digest "_sign", L ## algorithm ## _ ## digest ## _sign },\ + { #algorithm "_" #digest "_verify", L ## algorithm ## _ ## digest ## _verify }, + +#define IMPL_SIGN_VERIFY(algorithm, key_type, digest) \ + static int L ## algorithm ## _ ## digest ## _sign(lua_State *L) { \ + return base_evp_sign(L, key_type, EVP_ ## digest()); \ + } \ + static int L ## algorithm ## _ ## digest ## _verify(lua_State *L) { \ + return base_evp_verify(L, key_type, EVP_ ## digest()); \ + } + +IMPL_SIGN_VERIFY(ecdsa, NID_X9_62_id_ecPublicKey, sha256) +IMPL_SIGN_VERIFY(ecdsa, NID_X9_62_id_ecPublicKey, sha384) +IMPL_SIGN_VERIFY(ecdsa, NID_X9_62_id_ecPublicKey, sha512) + +IMPL_SIGN_VERIFY(rsassa_pkcs1, NID_rsaEncryption, sha256) +IMPL_SIGN_VERIFY(rsassa_pkcs1, NID_rsaEncryption, sha384) +IMPL_SIGN_VERIFY(rsassa_pkcs1, NID_rsaEncryption, sha512) + +IMPL_SIGN_VERIFY(rsassa_pss, NID_rsassaPss, sha256) +IMPL_SIGN_VERIFY(rsassa_pss, NID_rsassaPss, sha384) +IMPL_SIGN_VERIFY(rsassa_pss, NID_rsassaPss, sha512) + static const luaL_Reg Reg[] = { { "ed25519_sign", Led25519_sign }, { "ed25519_verify", Led25519_verify }, - { "rsassa_pkcs1_256_sign", Lrsassa_pkcs1_256_sign }, - { "rsassa_pkcs1_256_verify", Lrsassa_pkcs1_256_verify }, - { "rsassa_pss_256_sign", Lrsassa_pss_256_sign }, - { "rsassa_pss_256_verify", Lrsassa_pss_256_verify }, + + REG_SIGN_VERIFY(ecdsa, sha256) + REG_SIGN_VERIFY(ecdsa, sha384) + REG_SIGN_VERIFY(ecdsa, sha512) + + REG_SIGN_VERIFY(rsassa_pkcs1, sha256) + REG_SIGN_VERIFY(rsassa_pkcs1, sha384) + REG_SIGN_VERIFY(rsassa_pkcs1, sha512) + + REG_SIGN_VERIFY(rsassa_pss, sha256) + REG_SIGN_VERIFY(rsassa_pss, sha384) + REG_SIGN_VERIFY(rsassa_pss, sha512) + { "aes_128_gcm_encrypt", Laes_128_gcm_encrypt }, { "aes_128_gcm_decrypt", Laes_128_gcm_decrypt }, { "aes_256_gcm_encrypt", Laes_256_gcm_encrypt }, { "aes_256_gcm_decrypt", Laes_256_gcm_decrypt }, - { "ecdsa_sha256_sign", Lecdsa_sha256_sign }, - { "ecdsa_sha256_verify", Lecdsa_sha256_verify }, + { "generate_ed25519_keypair", Lgenerate_ed25519_keypair }, + { "import_private_pem", Limport_private_pem }, { "import_public_pem", Limport_public_pem }, + { "parse_ecdsa_signature", Lparse_ecdsa_signature }, { "build_ecdsa_signature", Lbuild_ecdsa_signature }, { NULL, NULL } diff --git a/util/jwt.lua b/util/jwt.lua index 433a69f6..7a05e45d 100644 --- a/util/jwt.lua +++ b/util/jwt.lua @@ -142,8 +142,8 @@ end local algorithms = { HS256 = new_hmac_algorithm("HS256", hashes.hmac_sha256); ES256 = new_ecdsa_algorithm("ES256", crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify); - RS256 = new_rsa_algorithm("RS256", crypto.rsassa_pkcs1_256_sign, crypto.rsassa_pkcs1_256_verify); - PS256 = new_rsa_algorithm("PS256", crypto.rsassa_pss_256_sign, crypto.rsassa_pss_256_verify); + RS256 = new_rsa_algorithm("RS256", crypto.rsassa_pkcs1_sha256_sign, crypto.rsassa_pkcs1_sha256_verify); + PS256 = new_rsa_algorithm("PS256", crypto.rsassa_pss_sha256_sign, crypto.rsassa_pss_sha256_verify); }; local function new_signer(algorithm, key_input) -- cgit v1.2.3 From 12a9ab92de04f89aa7847eeb235180dbaa59cf02 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 2 Jul 2022 15:27:44 +0100 Subject: util.crypto: tests: fix some tests that didn't do much (thanks luacheck!) --- spec/util_crypto_spec.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/util_crypto_spec.lua b/spec/util_crypto_spec.lua index 14c35188..0d7a7ffb 100644 --- a/spec/util_crypto_spec.lua +++ b/spec/util_crypto_spec.lua @@ -133,12 +133,11 @@ describe("util.crypto", function () string.rep("\255", 72); string.rep("\255", 3); }; - for _, sig in ipairs(invalid_sigs) do - local r, s = crypto.parse_ecdsa_signature(""); + for _, invalid_sig in ipairs(invalid_sigs) do + local r, s = crypto.parse_ecdsa_signature(invalid_sig); assert.is_nil(r); assert.is_nil(s); end - end); it("can be built", function () local r, s = crypto.parse_ecdsa_signature(sig); -- cgit v1.2.3 From fad58c5ab2fa449aea07eca2a99a1b5e30a22711 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 2 Jul 2022 15:29:04 +0100 Subject: util.jwt: All the algorithms (+ all the tests!) Except 'none'. Not implementing that one. --- spec/util_jwt_spec.lua | 105 ++++++++++++++++++++++++++++++++++++++++--------- util/jwt.lua | 17 +++++--- 2 files changed, 98 insertions(+), 24 deletions(-) diff --git a/spec/util_jwt_spec.lua b/spec/util_jwt_spec.lua index 9ac3887b..2a2ee76b 100644 --- a/spec/util_jwt_spec.lua +++ b/spec/util_jwt_spec.lua @@ -1,6 +1,13 @@ local jwt = require "util.jwt"; local test_keys = require "spec.inputs.test_keys"; +local array = require "util.array"; +local iter = require "util.iterators"; +local set = require "util.set"; + +-- Ignore long lines. We have some long tokens embedded here. +--luacheck: ignore 631 + describe("util.jwt", function () it("validates", function () local key = "secret"; @@ -21,6 +28,21 @@ describe("util.jwt", function () assert.falsy(ok) end); + local function jwt_reference_token(token) + return { + name = "jwt.io reference"; + token; + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }; + end + + local untested_algorithms = set.new(array.collect(iter.keys(jwt._algorithms))); + local test_cases = { { algorithm = "HS256"; @@ -28,15 +50,26 @@ describe("util.jwt", function () { "your-256-bit-secret", "your-256-bit-secret" }; { "another-secret", "another-secret" }; }; - { - name = "jwt.io reference"; - [[eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c]]; - { -- payload - sub = "1234567890"; - name = "John Doe"; - iat = 1516239022; - }; + + jwt_reference_token [[eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJhZG1pbiI6dHJ1ZX0.F-cvL2RcfQhUtCavIM7q7zYE8drmj2LJk0JRkrS6He4]]; + }; + { + algorithm = "HS384"; + keys = { + { "your-384-bit-secret", "your-384-bit-secret" }; + { "another-secret", "another-secret" }; + }; + + jwt_reference_token [[eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.bQTnz6AuMJvmXXQsVPrxeQNvzDkimo7VNXxHeSBfClLufmCVZRUuyTwJF311JHuh]]; + }; + { + algorithm = "HS512"; + keys = { + { "your-512-bit-secret", "your-512-bit-secret" }; + { "another-secret", "another-secret" }; }; + + jwt_reference_token [[eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.VFb0qJ1LRg_4ujbZoRMXnVkUgiuKq5KxWqNdbKq_G9Vvz-S1zZa9LPxtHWKa64zDl2ofkT8F6jBt_K4riU-fPg]]; }; { algorithm = "ES256"; @@ -72,22 +105,50 @@ describe("util.jwt", function () }; }; }; + { + algorithm = "RS384"; + keys = { + { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; + { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; + }; + + jwt_reference_token [[eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.o1hC1xYbJolSyh0-bOY230w22zEQSk5TiBfc-OCvtpI2JtYlW-23-8B48NpATozzMHn0j3rE0xVUldxShzy0xeJ7vYAccVXu2Gs9rnTVqouc-UZu_wJHkZiKBL67j8_61L6SXswzPAQu4kVDwAefGf5hyYBUM-80vYZwWPEpLI8K4yCBsF6I9N1yQaZAJmkMp_Iw371Menae4Mp4JusvBJS-s6LrmG2QbiZaFaxVJiW8KlUkWyUCns8-qFl5OMeYlgGFsyvvSHvXCzQrsEXqyCdS4tQJd73ayYA4SPtCb9clz76N1zE5WsV4Z0BYrxeb77oA7jJhh994RAPzCG0hmQ]]; + }; + { + algorithm = "RS512"; + keys = { + { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; + { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; + }; + + jwt_reference_token [[eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.jYW04zLDHfR1v7xdrW3lCGZrMIsVe0vWCfVkN2DRns2c3MN-mcp_-RE6TN9umSBYoNV-mnb31wFf8iun3fB6aDS6m_OXAiURVEKrPFNGlR38JSHUtsFzqTOj-wFrJZN4RwvZnNGSMvK3wzzUriZqmiNLsG8lktlEn6KA4kYVaM61_NpmPHWAjGExWv7cjHYupcjMSmR8uMTwN5UuAwgW6FRstCJEfoxwb0WKiyoaSlDuIiHZJ0cyGhhEmmAPiCwtPAwGeaL1yZMcp0p82cpTQ5Qb-7CtRov3N4DcOHgWYk6LomPR5j5cCkePAz87duqyzSMpCB0mCOuE3CU2VMtGeQ]]; + }; { algorithm = "PS256"; keys = { { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; }; - { - name = "jwt.io reference"; - [[eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.iOeNU4dAFFeBwNj6qdhdvm-IvDQrTa6R22lQVJVuWJxorJfeQww5Nwsra0PjaOYhAMj9jNMO5YLmud8U7iQ5gJK2zYyepeSuXhfSi8yjFZfRiSkelqSkU19I-Ja8aQBDbqXf2SAWA8mHF8VS3F08rgEaLCyv98fLLH4vSvsJGf6ueZSLKDVXz24rZRXGWtYYk_OYYTVgR1cg0BLCsuCvqZvHleImJKiWmtS0-CymMO4MMjCy_FIl6I56NqLE9C87tUVpo1mT-kbg5cHDD8I7MjCW5Iii5dethB4Vid3mZ6emKjVYgXrtkOQ-JyGMh6fnQxEFN1ft33GX2eRHluK9eg]]; - { -- payload - sub = "1234567890"; - name = "John Doe"; - admin = true; - iat = 1516239022; - }; + + jwt_reference_token [[eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.iOeNU4dAFFeBwNj6qdhdvm-IvDQrTa6R22lQVJVuWJxorJfeQww5Nwsra0PjaOYhAMj9jNMO5YLmud8U7iQ5gJK2zYyepeSuXhfSi8yjFZfRiSkelqSkU19I-Ja8aQBDbqXf2SAWA8mHF8VS3F08rgEaLCyv98fLLH4vSvsJGf6ueZSLKDVXz24rZRXGWtYYk_OYYTVgR1cg0BLCsuCvqZvHleImJKiWmtS0-CymMO4MMjCy_FIl6I56NqLE9C87tUVpo1mT-kbg5cHDD8I7MjCW5Iii5dethB4Vid3mZ6emKjVYgXrtkOQ-JyGMh6fnQxEFN1ft33GX2eRHluK9eg]]; + }; + { + algorithm = "PS384"; + keys = { + { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; + { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; + }; + + jwt_reference_token [[eyJhbGciOiJQUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.Lfe_aCQme_gQpUk9-6l9qesu0QYZtfdzfy08w8uqqPH_gnw-IVyQwyGLBHPFBJHMbifdSMxPjJjkCD0laIclhnBhowILu6k66_5Y2z78GHg8YjKocAvB-wSUiBhuV6hXVxE5emSjhfVz2OwiCk2bfk2hziRpkdMvfcITkCx9dmxHU6qcEIsTTHuH020UcGayB1-IoimnjTdCsV1y4CMr_ECDjBrqMdnontkqKRIM1dtmgYFsJM6xm7ewi_ksG_qZHhaoBkxQ9wq9OVQRGiSZYowCp73d2BF3jYMhdmv2JiaUz5jRvv6lVU7Quq6ylVAlSPxeov9voYHO1mgZFCY1kQ]]; + }; + { + algorithm = "PS512"; + keys = { + { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; + { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; }; + + jwt_reference_token [[eyJhbGciOiJQUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.J5W09-rNx0pt5_HBiydR-vOluS6oD-RpYNa8PVWwMcBDQSXiw6-EPW8iSsalXPspGj3ouQjAnOP_4-zrlUUlvUIt2T79XyNeiKuooyIFvka3Y5NnGiOUBHWvWcWp4RcQFMBrZkHtJM23sB5D7Wxjx0-HFeNk-Y3UJgeJVhg5NaWXypLkC4y0ADrUBfGAxhvGdRdULZivfvzuVtv6AzW6NRuEE6DM9xpoWX_4here-yvLS2YPiBTZ8xbB3axdM99LhES-n52lVkiX5AWg2JJkEROZzLMpaacA_xlbUz_zbIaOaoqk8gB5oO7kI6sZej3QAdGigQy-hXiRnW_L98d4GQ]]; }; }; @@ -127,17 +188,19 @@ describe("util.jwt", function () do_verify_test(algorithm, verifying_key, token, expect_success and test_payload or false); end - for _, algorithm_tests in ipairs(test_cases) do local algorithm = algorithm_tests.algorithm; local keypairs = algorithm_tests.keys; + + untested_algorithms:remove(algorithm); + describe(algorithm, function () it("can do basic sign and verify", function () for _, keypair in ipairs(keypairs) do local signing_key, verifying_key = keypair[1], keypair[2]; - do_sign_verify_test(algorithm, keypair[1], keypair[2], true); + do_sign_verify_test(algorithm, signing_key, verifying_key, true); end end); @@ -169,5 +232,9 @@ describe("util.jwt", function () end end); end + + for algorithm in untested_algorithms do + pending(algorithm.." tests", function () end); + end end); diff --git a/util/jwt.lua b/util/jwt.lua index 7a05e45d..7bd98eb1 100644 --- a/util/jwt.lua +++ b/util/jwt.lua @@ -34,9 +34,11 @@ local function new_static_header(algorithm_name) end -- HS*** family -local function new_hmac_algorithm(name, hmac) +local function new_hmac_algorithm(name) local static_header = new_static_header(name); + local hmac = hashes["hmac_sha"..name:sub(-3)]; + local function sign(key, payload) local encoded_payload = json.encode(payload); local signed = static_header .. b64url(encoded_payload); @@ -122,7 +124,11 @@ local function new_crypto_algorithm(name, key_type, c_sign, c_verify, sig_encode end -- RS***, PS*** -local function new_rsa_algorithm(name, c_sign, c_verify) +local rsa_sign_algos = { RS = "rsassa_pkcs1", PS = "rsassa_pss" }; +local function new_rsa_algorithm(name) + local family, digest_bits = name:match("^(..)(...)$"); + local c_sign = crypto[rsa_sign_algos[family].."_sha"..digest_bits.."_sign"]; + local c_verify = crypto[rsa_sign_algos[family].."_sha"..digest_bits.."_verify"]; return new_crypto_algorithm(name, "rsaEncryption", c_sign, c_verify); end @@ -140,10 +146,10 @@ local function new_ecdsa_algorithm(name, c_sign, c_verify) end local algorithms = { - HS256 = new_hmac_algorithm("HS256", hashes.hmac_sha256); + HS256 = new_hmac_algorithm("HS256"), HS384 = new_hmac_algorithm("HS384"), HS512 = new_hmac_algorithm("HS512"); ES256 = new_ecdsa_algorithm("ES256", crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify); - RS256 = new_rsa_algorithm("RS256", crypto.rsassa_pkcs1_sha256_sign, crypto.rsassa_pkcs1_sha256_verify); - PS256 = new_rsa_algorithm("PS256", crypto.rsassa_pss_sha256_sign, crypto.rsassa_pss_sha256_verify); + RS256 = new_rsa_algorithm("RS256"), RS384 = new_rsa_algorithm("RS384"), RS512 = new_rsa_algorithm("RS512"); + PS256 = new_rsa_algorithm("PS256"), PS384 = new_rsa_algorithm("PS384"), PS512 = new_rsa_algorithm("PS512"); }; local function new_signer(algorithm, key_input) @@ -167,6 +173,7 @@ end return { new_signer = new_signer; new_verifier = new_verifier; + _algorithms = algorithms; -- Deprecated sign = algorithms.HS256.sign; verify = algorithms.HS256.verify; -- cgit v1.2.3 From 23458111dcf59d6c64e77f3286dbaa752539b77a Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 11 Jul 2022 13:28:29 +0100 Subject: util.jwt: Provide built-in token expiry support (defaults to 3600s lifetime) To avoid every user of the library needing to add and verify expiry info, this is now handled by util.jwt itself (if not overridden or disabled). Issuing tokens that are valid forever is bad practice and rarely desired, and the default token lifetime is now 3600s (1 hour). --- util/jwt.lua | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/util/jwt.lua b/util/jwt.lua index 7bd98eb1..3501d9f2 100644 --- a/util/jwt.lua +++ b/util/jwt.lua @@ -152,21 +152,46 @@ local algorithms = { PS256 = new_rsa_algorithm("PS256"), PS384 = new_rsa_algorithm("PS384"), PS512 = new_rsa_algorithm("PS512"); }; -local function new_signer(algorithm, key_input) +local function new_signer(algorithm, key_input, options) local impl = assert(algorithms[algorithm], "Unknown JWT algorithm: "..algorithm); local key = (impl.load_private_key or impl.load_key)(key_input); local sign = impl.sign; + local default_ttl = (options and options.default_ttl) or 3600; return function (payload) + local issued_at; + if not payload.iat then + issued_at = os.time(); + payload.iat = issued_at; + end + if not payload.exp then + payload.exp = (issued_at or os.time()) + default_ttl; + end return sign(key, payload); end end -local function new_verifier(algorithm, key_input) +local function new_verifier(algorithm, key_input, options) local impl = assert(algorithms[algorithm], "Unknown JWT algorithm: "..algorithm); local key = (impl.load_public_key or impl.load_key)(key_input); local verify = impl.verify; + local check_expiry = not (options and options.accept_expired); + local claim_verifier = options and options.claim_verifier; return function (token) - return verify(key, token); + local ok, payload = verify(key, token); + if ok then + local expires_at = check_expiry and payload.exp; + if expires_at then + if type(expires_at) ~= "number" then + return nil, "invalid-expiry"; + elseif expires_at < os.time() then + return nil, "token-expired"; + end + end + if claim_verifier and not claim_verifier(payload) then + return nil, "incorrect-claims"; + end + end + return ok, payload; end end -- cgit v1.2.3 From d2ede10c5d4ed029731f2b431aea3b35da2dd999 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 11 Jul 2022 13:42:08 +0100 Subject: util.jwt: Consolidate payload parsing, ensure it's always a valid object --- util/jwt.lua | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/util/jwt.lua b/util/jwt.lua index 3501d9f2..53825236 100644 --- a/util/jwt.lua +++ b/util/jwt.lua @@ -33,6 +33,16 @@ local function new_static_header(algorithm_name) return b64url('{"alg":"'..algorithm_name..'","typ":"JWT"}') .. '.'; end +local function decode_raw_payload(raw_payload) + local payload, err = json.decode(unb64url(raw_payload)); + if err ~= nil then + return nil, "json-decode-error"; + elseif type(payload) ~= "table" then + return nil, "invalid-payload-type"; + end + return true, payload; +end + -- HS*** family local function new_hmac_algorithm(name) local static_header = new_static_header(name); @@ -53,11 +63,8 @@ local function new_hmac_algorithm(name) if not secure_equals(b64url(hmac(key, signed)), signature) then return false, "signature-mismatch"; end - local payload, err = json.decode(unb64url(raw_payload)); - if err ~= nil then - return nil, "json-decode-error"; - end - return true, payload; + + return decode_raw_payload(raw_payload); end local function load_key(key) @@ -101,12 +108,7 @@ local function new_crypto_algorithm(name, key_type, c_sign, c_verify, sig_encode return false, "signature-mismatch"; end - local payload, err = json.decode(unb64url(raw_payload)); - if err ~= nil then - return nil, "json-decode-error"; - end - - return true, payload; + return decode_raw_payload(raw_payload); end; load_public_key = function (public_key_pem) -- cgit v1.2.3 From ff54cc5bcfd5fc9ec23f8d974f0c46363f9bb35a Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 11 Jul 2022 13:43:01 +0100 Subject: util.jwt: Add new init() convenience method to obtain both signer and verifier --- util/jwt.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/util/jwt.lua b/util/jwt.lua index 53825236..5c74850a 100644 --- a/util/jwt.lua +++ b/util/jwt.lua @@ -197,9 +197,15 @@ local function new_verifier(algorithm, key_input, options) end end +local function init(algorithm, private_key, public_key, options) + return new_signer(algorithm, private_key, options), new_verifier(algorithm, public_key or private_key, options); +end + return { + init = init; new_signer = new_signer; new_verifier = new_verifier; + -- Exported mainly for tests _algorithms = algorithms; -- Deprecated sign = algorithms.HS256.sign; -- 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(-) 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 e2f61d6e7e2b5862811e22fd7eb065e5997e24e0 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 11 Jul 2022 13:51:39 +0100 Subject: util.paseto: Fix to decode footer before comparison --- util/paseto.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/util/paseto.lua b/util/paseto.lua index 5f162ad0..352c9df0 100644 --- a/util/paseto.lua +++ b/util/paseto.lua @@ -69,6 +69,7 @@ function v4_public.verify(tok, pk, expected_f, i) if not h then return nil, "invalid-token-format"; end + f = f and unb64url(f) or nil; if expected_f then if not f or not secure_equals(expected_f, f) then return nil, "invalid-footer"; -- cgit v1.2.3 From 2db2476824b14ee17f28d0427f8e3b56a5b77a5c Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 11 Jul 2022 14:08:55 +0100 Subject: util.paseto: Error early on invalid keys --- util/paseto.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/util/paseto.lua b/util/paseto.lua index 352c9df0..c658902c 100644 --- a/util/paseto.lua +++ b/util/paseto.lua @@ -106,6 +106,7 @@ end function v4_public.import_public_key(pem) local key = crypto.import_public_pem(pem); + assert(key:get_type() == "ED25519", "Invalid public key type for v4.public"); return setmetatable({ key = key; }, v4_public_pubkey_mt); @@ -113,6 +114,7 @@ end function v4_public.import_private_key(pem) local key = crypto.import_private_pem(pem); + assert(key:get_type() == "ED25519", "Invalid private key type for v4.public"); return setmetatable({ key = key; }, v4_public_privkey_mt); -- cgit v1.2.3 From 4e4c2dd6dc9a7e63070d5ee57332c359d432a4c3 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 11 Jul 2022 14:09:16 +0100 Subject: util.paseto: Export similar API to new util.jwt for ease and consistency --- util/paseto.lua | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/util/paseto.lua b/util/paseto.lua index c658902c..02a0aa70 100644 --- a/util/paseto.lua +++ b/util/paseto.lua @@ -120,6 +120,27 @@ function v4_public.import_private_key(pem) }, v4_public_privkey_mt); end +function v4_public.init(private_key_pem, public_key_pem, options) + local sign, verify = v4_public.sign, v4_public.verify; + local public_key = public_key_pem and v4_public.import_public_key(public_key_pem); + local private_key = private_key_pem and v4_public.import_private_key(private_key_pem); + local default_footer = options and options.default_footer; + local default_assertion = options and options.default_implicit_assertion; + return private_key and function (token, token_footer, token_assertion) + return sign(token, private_key, token_footer or default_footer, token_assertion or default_assertion); + end, public_key and function (token, expected_footer, token_assertion) + return verify(token, public_key, expected_footer or default_footer, token_assertion or default_assertion); + end; +end + +function v4_public.new_signer(private_key_pem, options) + return (v4_public.init(private_key_pem, nil, options)); +end + +function v4_public.new_verifier(public_key_pem, options) + return (select(2, v4_public.init(public_key_pem, options))); +end + return { pae = pae; v4_public = v4_public; -- cgit v1.2.3 From ae070865028fdee8285e924a63689ad88871ab6b Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 11 Jul 2022 14:10:07 +0100 Subject: util.paseto: Add tests based on official PASETO test vectors Unfortunately there are only a few relevant ones, but they did help catch some bugs. --- spec/util_paseto_spec.lua | 118 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 spec/util_paseto_spec.lua diff --git a/spec/util_paseto_spec.lua b/spec/util_paseto_spec.lua new file mode 100644 index 00000000..a57cea18 --- /dev/null +++ b/spec/util_paseto_spec.lua @@ -0,0 +1,118 @@ +-- Ignore long lines in this file +--luacheck: ignore 631 + +describe("util.paseto", function () + local paseto = require "util.paseto"; + local json = require "util.json"; + + local function parse_test_cases(json_test_cases) + local input_cases = json.decode(json_test_cases); + local output_cases = {}; + for _, case in ipairs(input_cases) do + assert.is_string(case.name, "Bad test case: expected name"); + assert.is_nil(output_cases[case.name], "Bad test case: duplicate name"); + output_cases[case.name] = function () + local verify_key = paseto.v4_public.import_public_key(case["public-key-pem"]); + local payload, err = paseto.v4_public.verify(case.token, verify_key, case.footer, case["implicit-assertion"]); + if case["expect-fail"] then + assert.is_nil(payload); + else + assert.is_nil(err); + assert.same(json.decode(case.payload), payload); + end + end; + end + return output_cases; + end + + describe("v4.public", function () + local test_cases = parse_test_cases [=[[ + { + "name": "4-S-1", + "expect-fail": false, + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9bg_XBBzds8lTZShVlwwKSgeKpLT3yukTw6JUz3W4h_ExsQV-P0V54zemZDcAxFaSeef1QlXEFtkqxT1ciiQEDA", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "4-S-2", + "expect-fail": false, + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "" + }, + { + "name": "4-S-3", + "expect-fail": false, + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9NPWciuD3d0o5eXJXG5pJy-DiVEoyPYWs1YSTwWHNJq6DZD3je5gf-0M4JR9ipdUSJbIovzmBECeaWmaqcaP0DQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "{\"test-vector\":\"4-S-3\"}" + }]]=]; + for name, test in pairs(test_cases) do + it("test case "..name, test); + end + + describe("basic sign/verify", function () + local function new_keypair() + local kp = paseto.v4_public.new_keypair(); + return kp.private_key:export(), kp.public_key:export(); + end + + local privkey1, pubkey1 = new_keypair(); + local privkey2, pubkey2 = new_keypair(); + local sign1, verify1 = paseto.v4_public.init(privkey1, pubkey1); + local sign2, verify2 = paseto.v4_public.init(privkey2, pubkey2); + + it("works", function () + local payload = { foo = "hello world", b = { 1, 2, 3 } }; + + local tok1 = sign1(payload); + assert.same(payload, verify1(tok1)); + assert.is_nil(verify2(tok1)); + + local tok2 = sign2(payload); + assert.same(payload, verify2(tok2)); + assert.is_nil(verify1(tok2)); + end); + + it("rejects tokens if implicit assertion fails", function () + local payload = { foo = "hello world", b = { 1, 2, 3 } }; + local tok = sign1(payload, nil, "my-custom-assertion"); + assert.is_nil(verify1(tok, nil, "my-incorrect-assertion")); + assert.is_nil(verify1(tok, nil, nil)); + assert.same(payload, verify1(tok, nil, "my-custom-assertion")); + end); + end); + end); + + describe("pae", function () + it("encodes correctly", function () + -- These test cases are taken from the PASETO docs + -- https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md + assert.equal("\x00\x00\x00\x00\x00\x00\x00\x00", paseto.pae{}); + assert.equal("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", paseto.pae{''}); + assert.equal("\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00test", paseto.pae{'test'}); + assert.has_errors(function () + paseto.pae("test"); + end); + end); + end); +end); -- cgit v1.2.3 From e12c9a83df999049970403cbbd6a6427f79f889e Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 11 Jul 2022 14:30:39 +0100 Subject: util.paseto: Drop custom wrappers around key objects The PASETO spec recommends - no, *requires* - that implementations enforce type safety for keys, and e.g. do not pass them around as arbitrary byte strings. Typed wrapper objects are recommended. I originally followed this advice when starting the lib. However, key wrapping and type safety is now also a feature of util.crypto. All we're doing is duplicating it unnecessarily with this additional wrapper code. --- spec/util_paseto_spec.lua | 2 +- util/paseto.lua | 51 +++++------------------------------------------ 2 files changed, 6 insertions(+), 47 deletions(-) diff --git a/spec/util_paseto_spec.lua b/spec/util_paseto_spec.lua index a57cea18..ec30c6a7 100644 --- a/spec/util_paseto_spec.lua +++ b/spec/util_paseto_spec.lua @@ -73,7 +73,7 @@ describe("util.paseto", function () describe("basic sign/verify", function () local function new_keypair() local kp = paseto.v4_public.new_keypair(); - return kp.private_key:export(), kp.public_key:export(); + return kp:private_pem(), kp:public_pem(); end local privkey1, pubkey1 = new_keypair(); diff --git a/util/paseto.lua b/util/paseto.lua index 02a0aa70..a39f9667 100644 --- a/util/paseto.lua +++ b/util/paseto.lua @@ -8,11 +8,6 @@ local s_pack = require "util.struct".pack; local s_gsub = string.gsub; -local pubkey_methods = {}; -local privkey_methods = {}; - -local v4_public_pubkey_mt = { __index = pubkey_methods }; -local v4_public_privkey_mt = { __index = privkey_methods }; local v4_public = {}; local b64url_rep = { ["+"] = "-", ["/"] = "_", ["="] = "", ["-"] = "+", ["_"] = "/" }; @@ -35,25 +30,14 @@ local function pae(parts) return table.concat(o); end -function privkey_methods:export() - return self.key:private_pem(); -end - -function pubkey_methods:export() - return self.key:public_pem(); -end - function v4_public.sign(m, sk, f, i) - if getmetatable(sk) ~= v4_public_privkey_mt then - error("cannot sign v4.public tokens with this key"); - end if type(m) ~= "table" then return nil, "PASETO payloads must be a table"; end m = json.encode(m); local h = "v4.public."; local m2 = pae({ h, m, f or "", i or "" }); - local sig = crypto.ed25519_sign(sk.key, m2); + local sig = crypto.ed25519_sign(sk, m2); if not f or f == "" then return h..b64url(m..sig); else @@ -62,9 +46,6 @@ function v4_public.sign(m, sk, f, i) end function v4_public.verify(tok, pk, expected_f, i) - if getmetatable(pk) ~= v4_public_pubkey_mt then - error("cannot verify v4.public tokens with this key"); - end local h, sm, f = tok:match("^(v4%.public%.)([^%.]+)%.?(.*)$"); if not h then return nil, "invalid-token-format"; @@ -81,7 +62,7 @@ function v4_public.verify(tok, pk, expected_f, i) end local s, m = raw_sm:sub(-64), raw_sm:sub(1, -65); local m2 = pae({ h, m, f or "", i or "" }); - local ok = crypto.ed25519_verify(pk.key, m2, s); + local ok = crypto.ed25519_verify(pk, m2, s); if not ok then return nil, "invalid-token"; end @@ -92,32 +73,10 @@ function v4_public.verify(tok, pk, expected_f, i) return payload; end +v4_public.import_private_key = crypto.import_private_pem; +v4_public.import_public_key = crypto.import_public_pem; function v4_public.new_keypair() - local key = crypto.generate_ed25519_keypair(); - return { - private_key = setmetatable({ - key = key; - }, v4_public_privkey_mt); - public_key = setmetatable({ - key = key; - }, v4_public_pubkey_mt); - }; -end - -function v4_public.import_public_key(pem) - local key = crypto.import_public_pem(pem); - assert(key:get_type() == "ED25519", "Invalid public key type for v4.public"); - return setmetatable({ - key = key; - }, v4_public_pubkey_mt); -end - -function v4_public.import_private_key(pem) - local key = crypto.import_private_pem(pem); - assert(key:get_type() == "ED25519", "Invalid private key type for v4.public"); - return setmetatable({ - key = key; - }, v4_public_privkey_mt); + return crypto.generate_ed25519_keypair(); end function v4_public.init(private_key_pem, public_key_pem, options) -- cgit v1.2.3 From e893bbf6817ff4193d9639afc40143fd792c9a4b Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 11 Jul 2022 17:01:55 +0200 Subject: util.crypto: Use stack space buffers Removes assumption that LUAL_BUFFERSIZE is known at pre-processing time, which it is not in Lua 5.3 and 5.4, where it is a computed macro based on sizeof. Allocation of stack space is safer and faster, no need to worry about luaL_prepbuffer failing to allocate memory and skipping free() --- util-src/crypto.c | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/util-src/crypto.c b/util-src/crypto.c index 6822b9bf..0f4edb51 100644 --- a/util-src/crypto.c +++ b/util-src/crypto.c @@ -434,7 +434,8 @@ static int Lparse_ecdsa_signature(lua_State *L) { size_t sig_der_len; const unsigned char *sig_der = (unsigned char*)luaL_checklstring(L, 1, &sig_der_len); const BIGNUM *r, *s; - luaL_Buffer rb, sb; + unsigned char rb[32]; + unsigned char sb[32]; int rlen, slen; sig = d2i_ECDSA_SIG(NULL, &sig_der, sig_der_len); @@ -449,23 +450,19 @@ static int Lparse_ecdsa_signature(lua_State *L) { rlen = BN_num_bytes(r); slen = BN_num_bytes(s); - // COMPAT w/ Lua 5.1 - #if LUAL_BUFFERSIZE < 32 - #error Configured LUAL_BUFFERSIZE is too small for this operation - #endif - - luaL_buffinit(L, &rb); - BN_bn2bin(r, (unsigned char*)luaL_prepbuffer(&rb)); - luaL_addsize(&rb, rlen); - luaL_pushresult(&rb); + if (rlen > 32 || slen > 32) { + ECDSA_SIG_free(sig); + luaL_error(L, "unexpectedly large signature integers"); + } - luaL_buffinit(L, &sb); - BN_bn2bin(s, (unsigned char*)luaL_prepbuffer(&sb)); - luaL_addsize(&sb, slen); - luaL_pushresult(&sb); + BN_bn2bin(r, rb); + BN_bn2bin(s, sb); ECDSA_SIG_free(sig); + lua_pushlstring(L, (const char*)rb, rlen); + lua_pushlstring(L, (const char*)sb, slen); + return 2; } -- cgit v1.2.3 From 62438f482ed8cf1c361dc4904baefcd893998403 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 11 Jul 2022 17:11:38 +0200 Subject: util.crypto: Use Lua 5.2 API for predictable buffer size In Lua 5.3 LUAL_BUFFERSIZE is a macro computed from sizeof and is thus not known at pre-processing time, so this does not work. Since Lua 5.1 is no longer supported, we can use luaL_prepbuffsize() which is available from Lua 5.2 --- util-src/crypto.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/util-src/crypto.c b/util-src/crypto.c index 0f4edb51..c5ecf9ab 100644 --- a/util-src/crypto.c +++ b/util-src/crypto.c @@ -485,12 +485,7 @@ static int Lbuild_ecdsa_signature(lua_State *L) { luaL_buffinit(L, &sigbuf); - // COMPAT w/ Lua 5.1 - #if LUAL_BUFFERSIZE < 128 - #error Configured LUAL_BUFFERSIZE is too small for this operation - #endif - - unsigned char *buffer = (unsigned char*)luaL_prepbuffer(&sigbuf); + unsigned char *buffer = (unsigned char*)luaL_prepbuffsize(&sigbuf, rlen+slen+32); int len = i2d_ECDSA_SIG(sig, &buffer); luaL_addsize(&sigbuf, len); luaL_pushresult(&sigbuf); -- cgit v1.2.3 From a3e182ffa1718472d8bae11b27004f6c6311b4a3 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 11 Jul 2022 18:48:57 +0200 Subject: util.paseto: Do strict type check in pae() function Fixes a test failure on Lua 5.4 where ipairs("") does not produce an error. --- util/paseto.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/util/paseto.lua b/util/paseto.lua index a39f9667..8b564c96 100644 --- a/util/paseto.lua +++ b/util/paseto.lua @@ -23,6 +23,9 @@ local function le64(n) end local function pae(parts) + if type(parts) ~= "table" then + error("bad argument #1 to 'pae' (table expected, got "..type(parts)..")"); + end local o = { le64(#parts) }; for _, part in ipairs(parts) do table.insert(o, le64(#part)..part); -- cgit v1.2.3 From 928fe5d0598c7d2348497a508f11b97a757543f5 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 3 Sep 2022 21:17:38 +0100 Subject: semgrep: Catch stanza:text() (assuming it's meant to be :get_text()) --- .semgrep.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.semgrep.yml b/.semgrep.yml index de1ef89e..22bfcfea 100644 --- a/.semgrep.yml +++ b/.semgrep.yml @@ -22,3 +22,9 @@ rules: message: Non-string default from :get_option_string severity: ERROR languages: [lua] +- id: stanza-empty-text-constructor + patterns: + - pattern: $A:text() + message: Use :get_text() to read text, or pass a value here to add text + severity: WARNING + languages: [lua] -- 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(-) 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(-) 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(+) 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(-) 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(-) 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(-) 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(-) 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(-) 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 67db8995113b2772367fcd2cd6d471afd08b410c Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 26 Sep 2022 15:47:15 +0100 Subject: doap: Latest XEP-0440 supported since 9f100ab9ffdf --- doc/doap.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/doap.xml b/doc/doap.xml index c7dc26b3..b8973799 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -852,7 +852,7 @@ - 0.2.0 + 0.4.0 trunk complete -- 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(-) 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(-) 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(-) 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(-) 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(+) 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 dfd1e396bb80af04b25f82efafc6d1dd67d7056a Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 29 Sep 2022 12:57:05 +0100 Subject: CHANGES: Update with MUC permission changes --- CHANGES | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGES b/CHANGES index d96c5d63..ea4ef9e0 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,16 @@ TRUNK - Support for TCP Fast Open in server_epoll (pending LuaSocket support) - Support for deferred accept in server_epoll (pending LuaSocket support) +### MUC + +- Permissions updates: + - Room creation restricted to local users (of the parent host) by default + - restrict_room_creation = true restricts to admins, false disables all restrictions + - Persistent rooms can only be created by local users (parent host) by default + - muc_room_allow_persistent = false restricts to admins + - Public rooms can only be created by local users (parent host) by default + - muc_room_allow_public = false restricts to admins + ### Security and authentication - Advertise supported SASL Channel-Binding types (XEP-0440) -- cgit v1.2.3 From 8695a72a668fa38f1df64653508c360534e5e3db Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 29 Sep 2022 23:15:39 +0100 Subject: util.crypto, util.jwt: Generate consistent signature sizes (via padding) This fixes the signature parsing and building to work correctly. Sometimes a signature was one or two bytes too short, and needed to be padded. OpenSSL can do this for us. --- util-src/crypto.c | 27 +++++++++++++++++---------- util/jwt.lua | 8 ++++---- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/util-src/crypto.c b/util-src/crypto.c index c5ecf9ab..dce1a740 100644 --- a/util-src/crypto.c +++ b/util-src/crypto.c @@ -33,6 +33,9 @@ typedef unsigned __int32 uint32_t; #define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) #endif +/* The max size of an encoded 'R' or 'S' value. P-521 = 521 bits = 66 bytes */ +#define MAX_ECDSA_SIG_INT_BYTES 66 + #include "managed_pointer.h" #define PKEY_MT_TAG "util.crypto key" @@ -433,10 +436,15 @@ static int Lparse_ecdsa_signature(lua_State *L) { ECDSA_SIG *sig; size_t sig_der_len; const unsigned char *sig_der = (unsigned char*)luaL_checklstring(L, 1, &sig_der_len); + const size_t sig_int_bytes = luaL_checkinteger(L, 2); const BIGNUM *r, *s; - unsigned char rb[32]; - unsigned char sb[32]; int rlen, slen; + unsigned char rb[MAX_ECDSA_SIG_INT_BYTES]; + unsigned char sb[MAX_ECDSA_SIG_INT_BYTES]; + + if(sig_int_bytes > MAX_ECDSA_SIG_INT_BYTES) { + luaL_error(L, "requested signature size exceeds supported limit"); + } sig = d2i_ECDSA_SIG(NULL, &sig_der, sig_der_len); @@ -447,17 +455,14 @@ static int Lparse_ecdsa_signature(lua_State *L) { ECDSA_SIG_get0(sig, &r, &s); - rlen = BN_num_bytes(r); - slen = BN_num_bytes(s); + rlen = BN_bn2binpad(r, rb, sig_int_bytes); + slen = BN_bn2binpad(s, sb, sig_int_bytes); - if (rlen > 32 || slen > 32) { + if (rlen == -1 || slen == -1) { ECDSA_SIG_free(sig); - luaL_error(L, "unexpectedly large signature integers"); + luaL_error(L, "encoded integers exceed requested size"); } - BN_bn2bin(r, rb); - BN_bn2bin(s, sb); - ECDSA_SIG_free(sig); lua_pushlstring(L, (const char*)rb, rlen); @@ -485,7 +490,9 @@ static int Lbuild_ecdsa_signature(lua_State *L) { luaL_buffinit(L, &sigbuf); - unsigned char *buffer = (unsigned char*)luaL_prepbuffsize(&sigbuf, rlen+slen+32); + /* DER structure of an ECDSA signature has 7 bytes plus the integers themselves, + which may gain an extra byte once encoded */ + unsigned char *buffer = (unsigned char*)luaL_prepbuffsize(&sigbuf, (rlen+1)+(slen+1)+7); int len = i2d_ECDSA_SIG(sig, &buffer); luaL_addsize(&sigbuf, len); luaL_pushresult(&sigbuf); diff --git a/util/jwt.lua b/util/jwt.lua index 5c74850a..1c7daf12 100644 --- a/util/jwt.lua +++ b/util/jwt.lua @@ -135,21 +135,21 @@ local function new_rsa_algorithm(name) end -- ES*** -local function new_ecdsa_algorithm(name, c_sign, c_verify) +local function new_ecdsa_algorithm(name, c_sign, c_verify, sig_bytes) local function encode_ecdsa_sig(der_sig) - local r, s = crypto.parse_ecdsa_signature(der_sig); + local r, s = crypto.parse_ecdsa_signature(der_sig, sig_bytes); return r..s; end local function decode_ecdsa_sig(jwk_sig) - return crypto.build_ecdsa_signature(jwk_sig:sub(1, 32), jwk_sig:sub(33, 64)); + return crypto.build_ecdsa_signature(jwk_sig:sub(1, sig_bytes), jwk_sig:sub(sig_bytes+1, sig_bytes*2)); end return new_crypto_algorithm(name, "id-ecPublicKey", c_sign, c_verify, encode_ecdsa_sig, decode_ecdsa_sig); end local algorithms = { HS256 = new_hmac_algorithm("HS256"), HS384 = new_hmac_algorithm("HS384"), HS512 = new_hmac_algorithm("HS512"); - ES256 = new_ecdsa_algorithm("ES256", crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify); + ES256 = new_ecdsa_algorithm("ES256", crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify, 32); RS256 = new_rsa_algorithm("RS256"), RS384 = new_rsa_algorithm("RS384"), RS512 = new_rsa_algorithm("RS512"); PS256 = new_rsa_algorithm("PS256"), PS384 = new_rsa_algorithm("PS384"), PS512 = new_rsa_algorithm("PS512"); }; -- cgit v1.2.3 From 5ca81b0e3bcef02ebf6e518e10a855c6ed3efd7f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 29 Sep 2022 23:17:42 +0100 Subject: util.jwt: Add support for ES512 (+ tests) --- spec/inputs/test_keys.lua | 43 +++++++++++++++++++++++++++++++++++++++++++ spec/util_jwt_spec.lua | 25 ++++++++++++++++++++++--- util/jwt.lua | 1 + 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/spec/inputs/test_keys.lua b/spec/inputs/test_keys.lua index 088332c4..e0e9ff8c 100644 --- a/spec/inputs/test_keys.lua +++ b/spec/inputs/test_keys.lua @@ -29,6 +29,49 @@ uOwfIoCYSLylAghyxO0n2of8Kji+D+4C1zxNKmZIQa4s8neaIIzXnMY1 MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnL/YyN/gTl15Qjl8JkGKLCG5fZn+ FbjsHyKAmEi8pQIIcsTtJ9qH/Co4vg/uAtc8TSpmSEGuLPJ3miCM15zGNQ== -----END PUBLIC KEY----- +]]; + + -- JWT reference keys for ES512 + + ecdsa_521_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ +PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47 +6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM +Al8G7CqwoJOsW7Kddns= +-----END PUBLIC KEY----- +]]; + + ecdsa_521_private_pem = [[ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiyAa7aRHFDCh2qga +9sTUGINE5jHAFnmM8xWeT/uni5I4tNqhV5Xx0pDrmCV9mbroFtfEa0XVfKuMAxxf +Z6LM/yKhgYkDgYYABAGBzgdnP798FsLuWYTDDQA7c0r3BVk8NnRUSexpQUsRilPN +v3SchO0lRw9Ru86x1khnVDx+duq4BiDFcvlSAcyjLACJvjvoyTLJiA+TQFdmrear +jMiZNE25pT2yWP1NUndJxPcvVtfBW48kPOmvkY4WlqP5bAwCXwbsKrCgk6xbsp12 +ew== +-----END PRIVATE KEY----- +]]; + + -- Self-generated keys for ES512 + + alt_ecdsa_521_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBIxV0ecG/+qFc/kVPKs8Z6tjJEuRe +dzrEaqABY6THu7BhCjEoxPr6iRYdiFPzNruFORsCAKf/NFLSoCqyrw9S0YMA1xc+ +uW01145oxT7Sp8BOH1MyOh7xNh+LFLi6X4lV6j5GQrM1sKSa3O5m0+VJmLy5b7cy +oxNCzXrnEByz+EO2nYI= +-----END PUBLIC KEY----- +]]; + + alt_ecdsa_521_private_pem = [[ +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIAV2XJQ4/5Pa5m43/AJdL4XzrRV/l7eQ1JObqmI95YDs3zxM5Mfygz +DivhvuPdZCZUR+TdZQEdYN4LpllCzrDwmTCgBwYFK4EEACOhgYkDgYYABAEjFXR5 +wb/6oVz+RU8qzxnq2MkS5F53OsRqoAFjpMe7sGEKMSjE+vqJFh2IU/M2u4U5GwIA +p/80UtKgKrKvD1LRgwDXFz65bTXXjmjFPtKnwE4fUzI6HvE2H4sUuLpfiVXqPkZC +szWwpJrc7mbT5UmYvLlvtzKjE0LNeucQHLP4Q7adgg== +-----END EC PRIVATE KEY----- ]]; -- Self-generated EdDSA (Ed25519) keypair diff --git a/spec/util_jwt_spec.lua b/spec/util_jwt_spec.lua index 2a2ee76b..6946bdd3 100644 --- a/spec/util_jwt_spec.lua +++ b/spec/util_jwt_spec.lua @@ -88,6 +88,23 @@ describe("util.jwt", function () }; }; }; + { + algorithm = "ES512"; + keys = { + { test_keys.ecdsa_521_private_pem, test_keys.ecdsa_521_public_pem }; + { test_keys.alt_ecdsa_521_private_pem, test_keys.alt_ecdsa_521_public_pem }; + }; + { + name = "jwt.io reference"; + [[eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.AbVUinMiT3J_03je8WTOIl-VdggzvoFgnOsdouAs-DLOtQzau9valrq-S6pETyi9Q18HH-EuwX49Q7m3KC0GuNBJAc9Tksulgsdq8GqwIqZqDKmG7hNmDzaQG1Dpdezn2qzv-otf3ZZe-qNOXUMRImGekfQFIuH_MjD2e8RZyww6lbZk]]; + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }; + }; { algorithm = "RS256"; keys = { @@ -197,10 +214,12 @@ describe("util.jwt", function () untested_algorithms:remove(algorithm); describe(algorithm, function () - it("can do basic sign and verify", function () - for _, keypair in ipairs(keypairs) do + describe("can do basic sign and verify", function () + for keypair_n, keypair in ipairs(keypairs) do local signing_key, verifying_key = keypair[1], keypair[2]; - do_sign_verify_test(algorithm, signing_key, verifying_key, true); + it(("(test key pair %d)"):format(keypair_n), function () + do_sign_verify_test(algorithm, signing_key, verifying_key, true); + end); end end); diff --git a/util/jwt.lua b/util/jwt.lua index 1c7daf12..0c878efb 100644 --- a/util/jwt.lua +++ b/util/jwt.lua @@ -150,6 +150,7 @@ end local algorithms = { HS256 = new_hmac_algorithm("HS256"), HS384 = new_hmac_algorithm("HS384"), HS512 = new_hmac_algorithm("HS512"); ES256 = new_ecdsa_algorithm("ES256", crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify, 32); + ES512 = new_ecdsa_algorithm("ES512", crypto.ecdsa_sha512_sign, crypto.ecdsa_sha512_verify, 66); RS256 = new_rsa_algorithm("RS256"), RS384 = new_rsa_algorithm("RS384"), RS512 = new_rsa_algorithm("RS512"); PS256 = new_rsa_algorithm("PS256"), PS384 = new_rsa_algorithm("PS384"), PS512 = new_rsa_algorithm("PS512"); }; -- cgit v1.2.3 From 70e54affa0d780922bd68bc12d411d59c4dbcf81 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 30 Sep 2022 00:27:10 +0200 Subject: util.crypto: Fix tests Found this number in a hat. Sleepy time. Good night. --- spec/util_crypto_spec.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/util_crypto_spec.lua b/spec/util_crypto_spec.lua index 0d7a7ffb..152efed5 100644 --- a/spec/util_crypto_spec.lua +++ b/spec/util_crypto_spec.lua @@ -117,7 +117,7 @@ describe("util.crypto", function () 6b0cd1043718ffc31c153b971d213a8e ]]):gsub("%s+", ""))); it("can be parsed", function () - local r, s = crypto.parse_ecdsa_signature(sig); + local r, s = crypto.parse_ecdsa_signature(sig, 32); assert.is_string(r); assert.is_string(s); assert.equal(32, #r); @@ -134,13 +134,13 @@ describe("util.crypto", function () string.rep("\255", 3); }; for _, invalid_sig in ipairs(invalid_sigs) do - local r, s = crypto.parse_ecdsa_signature(invalid_sig); + local r, s = crypto.parse_ecdsa_signature(invalid_sig, 32); assert.is_nil(r); assert.is_nil(s); end end); it("can be built", function () - local r, s = crypto.parse_ecdsa_signature(sig); + local r, s = crypto.parse_ecdsa_signature(sig, 32); local rebuilt_sig = crypto.build_ecdsa_signature(r, s); assert.equal(sig, rebuilt_sig); end); -- cgit v1.2.3 From e7bfb40a324c806c350f4c87cd41e3b83033377e Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 30 Sep 2022 20:38:31 +0100 Subject: util.jwt: More robust ECDSA signature parsing, fail early on unexpected length --- util/jwt.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/util/jwt.lua b/util/jwt.lua index 0c878efb..42a9f7f2 100644 --- a/util/jwt.lua +++ b/util/jwt.lua @@ -141,8 +141,12 @@ local function new_ecdsa_algorithm(name, c_sign, c_verify, sig_bytes) return r..s; end + local expected_sig_length = sig_bytes*2; local function decode_ecdsa_sig(jwk_sig) - return crypto.build_ecdsa_signature(jwk_sig:sub(1, sig_bytes), jwk_sig:sub(sig_bytes+1, sig_bytes*2)); + if #jwk_sig ~= expected_sig_length then + return nil; + end + return crypto.build_ecdsa_signature(jwk_sig:sub(1, sig_bytes), jwk_sig:sub(sig_bytes+1)); end return new_crypto_algorithm(name, "id-ecPublicKey", c_sign, c_verify, encode_ecdsa_sig, decode_ecdsa_sig); end -- 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(-) 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(-) 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 bfa6d5634afbed156537de9eb620964b24111ff2 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 6 Oct 2022 11:12:57 +0100 Subject: prosodyctl: check turn: More clearly indicate the error is from TURN server --- util/prosodyctl/check.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/prosodyctl/check.lua b/util/prosodyctl/check.lua index 42d73f29..3f89f930 100644 --- a/util/prosodyctl/check.lua +++ b/util/prosodyctl/check.lua @@ -155,7 +155,7 @@ local function check_turn_service(turn_service, ping_service) result.error = "TURN server did not response to allocation request: "..err; return result; elseif alloc_response:is_err_resp() then - result.error = ("TURN allocation failed: %d (%s)"):format(alloc_response:get_error()); + result.error = ("TURN server failed to create allocation: %d (%s)"):format(alloc_response:get_error()); return result; elseif not alloc_response:is_success_resp() then result.error = ("Unexpected TURN response: %d (%s)"):format(alloc_response:get_type()); -- 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(+) 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(+) 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 08b49654d9bfd6a280e336d834f4bcfee8bda8e1 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 6 Oct 2022 18:34:40 +0100 Subject: util.iterators: join: Work even with only a single iterator in the chain --- spec/util_iterators_spec.lua | 8 ++++++++ util/iterators.lua | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spec/util_iterators_spec.lua b/spec/util_iterators_spec.lua index 4cf6f19d..a4b85884 100644 --- a/spec/util_iterators_spec.lua +++ b/spec/util_iterators_spec.lua @@ -10,6 +10,14 @@ describe("util.iterators", function () end assert.same(output, expect); end); + it("should work with only a single iterator", function () + local expect = { "a", "b", "c" }; + local output = {}; + for x in iter.join(iter.values({"a", "b", "c"})) do + table.insert(output, x); + end + assert.same(output, expect); + end); end); describe("sorted_pairs", function () diff --git a/util/iterators.lua b/util/iterators.lua index 4529697a..eb4c54af 100644 --- a/util/iterators.lua +++ b/util/iterators.lua @@ -240,7 +240,8 @@ function join_methods:prepend(f, s, var) end function it.join(f, s, var) - return setmetatable({ {f, s, var} }, join_mt); + local t = setmetatable({ {f, s, var} }, join_mt); + return t, { t, 1 }; end return it; -- 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(-) 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 d6863b51b94b1a9e5e4cf8ad18a5804807dffe8f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 7 Oct 2022 16:58:08 +0100 Subject: util.roles: Return nil if the role has no explicit policy (fixes inheritance) Previously, if the first inherited role had no opinion, it returned false and prevented further consultation of other inherited roles. This bug was found thanks to the implementation of missing test cases identified through mutation testing. --- util/roles.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/roles.lua b/util/roles.lua index 831ce6ae..a38faa58 100644 --- a/util/roles.lua +++ b/util/roles.lua @@ -30,7 +30,7 @@ local function _new_may(permissions, inherited_mays) end end end - return false; + return nil; end end -- cgit v1.2.3 From bda9ea678c9254254c90e3f4626d3aa7f1de364c Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 7 Oct 2022 16:58:52 +0100 Subject: util.roles: Add tests --- spec/util_roles_spec.lua | 116 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 spec/util_roles_spec.lua diff --git a/spec/util_roles_spec.lua b/spec/util_roles_spec.lua new file mode 100644 index 00000000..c74515e9 --- /dev/null +++ b/spec/util_roles_spec.lua @@ -0,0 +1,116 @@ +describe("util.roles", function () + randomize(false); + local roles; + it("can be loaded", function () + roles = require "util.roles"; + end); + local test_role; + it("can create a new role", function () + test_role = roles.new(); + assert.is_not_nil(test_role); + assert.is_truthy(roles.is_role(test_role)); + end); + describe("role object", function () + it("is restrictive by default", function () + assert.falsy(test_role:may("my-permission")); + end); + it("allows you to set permissions", function () + test_role:set_permission("my-permission", true); + assert.truthy(test_role:may("my-permission")); + end); + it("allows you to set negative permissions", function () + test_role:set_permission("my-other-permission", false); + assert.falsy(test_role:may("my-other-permission")); + end); + it("does not allows you to override previously set permissions by default", function () + local ok, err = test_role:set_permission("my-permission", false); + assert.falsy(ok); + assert.is_equal("policy-already-exists", err); + -- Confirm old permission still in place + assert.truthy(test_role:may("my-permission")); + end); + it("allows you to explicitly override previously set permissions", function () + assert.truthy(test_role:set_permission("my-permission", false, true)); + assert.falsy(test_role:may("my-permission")); + end); + describe("inheritance", function () + local child_role; + it("works", function () + test_role:set_permission("inherited-permission", true); + child_role = roles.new({ + inherits = { test_role }; + }); + assert.truthy(child_role:may("inherited-permission")); + assert.falsy(child_role:may("my-permission")); + end); + it("allows listing policies", function () + local expected = { + ["my-permission"] = false; + ["my-other-permission"] = false; + ["inherited-permission"] = true; + }; + local received = {}; + for permission_name, permission_policy in child_role:policies() do + received[permission_name] = permission_policy; + end + assert.same(expected, received); + end); + it("supports multiple depths of inheritance", function () + local grandchild_role = roles.new({ + inherits = { child_role }; + }); + assert.truthy(grandchild_role:may("inherited-permission")); + end); + describe("supports ordered inheritance from multiple roles", function () + local parent_role = roles.new(); + local final_role = roles.new({ + -- Yes, the names are getting confusing. + -- btw, test_role is inherited through child_role. + inherits = { parent_role, child_role }; + }); + + local test_cases = { + -- { , , } + { true, nil, false, result = true }; + { nil, false, true, result = false }; + { nil, true, false, result = true }; + { nil, nil, false, result = false }; + { nil, nil, true, result = true }; + }; + + for n, test_case in ipairs(test_cases) do + it("(case "..n..")", function () + local perm_name = ("multi-inheritance-perm-%d"):format(n); + assert.truthy(final_role:set_permission(perm_name, test_case[1])); + assert.truthy(parent_role:set_permission(perm_name, test_case[2])); + assert.truthy(test_role:set_permission(perm_name, test_case[3])); + assert.equal(test_case.result, final_role:may(perm_name)); + end); + end + end); + it("updates child roles when parent roles change", function () + assert.truthy(child_role:may("inherited-permission")); + assert.truthy(test_role:set_permission("inherited-permission", false, true)); + assert.falsy(child_role:may("inherited-permission")); + end); + end); + describe("cloning", function () + local cloned_role; + it("works", function () + assert.truthy(test_role:set_permission("perm-1", true)); + cloned_role = test_role:clone(); + assert.truthy(cloned_role:may("perm-1")); + end); + it("isolates changes", function () + -- After cloning, changes in either the original or the clone + -- should not appear in the other. + assert.truthy(test_role:set_permission("perm-1", false, true)); + assert.truthy(test_role:set_permission("perm-2", true)); + assert.truthy(cloned_role:set_permission("perm-3", true)); + assert.truthy(cloned_role:may("perm-1")); + assert.falsy(cloned_role:may("perm-2")); + assert.falsy(test_role:may("perm-3")); + end); + end); + end); +end); -- cgit v1.2.3 From d08ddc1f4a1c6d93342bd8efd69c837782656f1f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 7 Oct 2022 17:01:35 +0100 Subject: util.roles: Add a :policies() method to iterate through available policies We don't expose the policies directly, to force people to go through :may(). However, there are times when we really just need to know what policies a role has inside it (e.g. for reporting or debugging purposes). --- util/roles.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/util/roles.lua b/util/roles.lua index a38faa58..2c3a5026 100644 --- a/util/roles.lua +++ b/util/roles.lua @@ -86,6 +86,16 @@ function role_methods:set_permission(permission_name, policy, overwrite) return true; end +function role_methods:policies() + local policy_iterator, s, v = it.join(pairs(self[permissions_key])); + if self.inherits then + for _, inherited_role in ipairs(self.inherits) do + policy_iterator:append(inherited_role:policies()); + end + end + return policy_iterator, s, v; +end + function role_mt.__tostring(self) return ("role<[%s] %s>"):format(self.id or "nil", self.name or "[no name]"); end -- cgit v1.2.3 From aed0c1c5ababe8b44ac8f88ef423b641c0c6ec4f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 7 Oct 2022 17:43:26 +0100 Subject: util.promise: Remove some redundant checks, add tests confirming redundancy This lines don't appear to do anything useful, and all tests pass when they are removed. Discovered via mutation testing. I added extra tests to exercise this code, because I wasn't certain that there were no side-effects caused by removal. Everything appears to be fine, thanks to the "pending" check at the start of promise_settle(). --- spec/util_promise_spec.lua | 21 +++++++++++++++++++++ util/promise.lua | 4 ---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/spec/util_promise_spec.lua b/spec/util_promise_spec.lua index 597b56f8..75e9294e 100644 --- a/spec/util_promise_spec.lua +++ b/spec/util_promise_spec.lua @@ -30,6 +30,27 @@ describe("util.promise", function () r("foo"); assert.spy(cb).was_called(1); end); + it("ignores resolve/reject of settled promises", function () + local res, rej; + local p = promise.new(function (resolve, reject) + res, rej = resolve, reject; + end); + local cb = spy.new(function (v) + assert.equal("foo", v); + end); + p:next(cb, cb); + assert.spy(cb).was_called(0); + res("foo"); + assert.spy(cb).was_called(1); + rej("bar"); + assert.spy(cb).was_called(1); + rej(promise.resolve("bar")); + assert.spy(cb).was_called(1); + res(promise.reject("bar")); + assert.spy(cb).was_called(1); + res(promise.resolve("bar")); + assert.spy(cb).was_called(1); + end); it("allows chaining :next() calls", function () local r; local result; diff --git a/util/promise.lua b/util/promise.lua index 1762d501..d8f0b3d6 100644 --- a/util/promise.lua +++ b/util/promise.lua @@ -57,9 +57,7 @@ local function promise_settle(promise, new_state, new_next, cbs, value) end local function new_resolve_functions(p) - local resolved = false; local function _resolve(v) - if resolved then return; end resolved = true; if is_promise(v) then v:next(new_resolve_functions(p)); @@ -69,8 +67,6 @@ local function new_resolve_functions(p) end local function _reject(e) - if resolved then return; end - resolved = true; if promise_settle(p, "rejected", next_rejected, p._pending_on_rejected, e) then p.reason = e; end -- cgit v1.2.3 From a79238fb37eb9ad993e906fb834e2f7696d70b11 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 7 Oct 2022 17:45:23 +0100 Subject: util.promise: Remove line that was supposed to be removed in eb9814372c54 --- util/promise.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/util/promise.lua b/util/promise.lua index d8f0b3d6..f56502d2 100644 --- a/util/promise.lua +++ b/util/promise.lua @@ -58,7 +58,6 @@ end local function new_resolve_functions(p) local function _resolve(v) - resolved = true; if is_promise(v) then v:next(new_resolve_functions(p)); elseif promise_settle(p, "fulfilled", next_fulfilled, p._pending_on_fulfilled, v) then -- cgit v1.2.3 From e8080773d63fab1c3390927648c1d3cac84402d5 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 7 Oct 2022 17:56:38 +0100 Subject: util.promise: Add some missing test cases (found through mutation testing) --- spec/util_promise_spec.lua | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/util_promise_spec.lua b/spec/util_promise_spec.lua index 75e9294e..7f1ece52 100644 --- a/spec/util_promise_spec.lua +++ b/spec/util_promise_spec.lua @@ -7,6 +7,11 @@ describe("util.promise", function () assert(promise.new()); end); end); + it("supplies a sensible tostring()", function () + local s = tostring(promise.new()); + assert.truthy(s:find("promise", 1, true)); + assert.truthy(s:find("pending", 1, true)); + end); it("notifies immediately for fulfilled promises", function () local p = promise.new(function (resolve) resolve("foo"); @@ -459,6 +464,26 @@ describe("util.promise", function () { status = "rejected", reason = "this fails" }; }, result); end); + it("works when all promises reject", function () + local r1, r2; + local p1, p2 = promise.new(function (_, reject) r1 = reject end), promise.new(function (_, reject) r2 = reject end); + local p = promise.all_settled({ p1, p2 }); + + local result; + local cb = spy.new(function (v) + result = v; + end); + p:next(cb); + assert.spy(cb).was_called(0); + r2("this fails"); + assert.spy(cb).was_called(0); + r1("this fails too"); + assert.spy(cb).was_called(1); + assert.same({ + { status = "rejected", value = "this fails too" }; + { status = "rejected", reason = "this fails" }; + }, result); + end); it("works with non-numeric keys", function () local r1, r2; local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end); -- cgit v1.2.3 From c1627c5d66877e8a0c581172b915ebee52f0ab05 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 7 Oct 2022 18:04:30 +0100 Subject: util.promise: Fix field name in failing test (introduced in 2639e0e1c378) --- spec/util_promise_spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/util_promise_spec.lua b/spec/util_promise_spec.lua index 7f1ece52..91b3d2d1 100644 --- a/spec/util_promise_spec.lua +++ b/spec/util_promise_spec.lua @@ -480,7 +480,7 @@ describe("util.promise", function () r1("this fails too"); assert.spy(cb).was_called(1); assert.same({ - { status = "rejected", value = "this fails too" }; + { status = "rejected", reason = "this fails too" }; { status = "rejected", reason = "this fails" }; }, result); end); -- cgit v1.2.3 From e3d1958cc4f94d51030c2dac92ea6df5bb346995 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 8 Oct 2022 20:33:01 +0100 Subject: util.roles: Add some more missing test cases Found via mutation testing. --- spec/util_roles_spec.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/util_roles_spec.lua b/spec/util_roles_spec.lua index c74515e9..81723657 100644 --- a/spec/util_roles_spec.lua +++ b/spec/util_roles_spec.lua @@ -11,6 +11,24 @@ describe("util.roles", function () assert.is_truthy(roles.is_role(test_role)); end); describe("role object", function () + it("can be initialized with permissions", function () + local test_role_2 = roles.new({ + permissions = { + perm1 = true; + perm2 = false; + }; + }); + assert.truthy(test_role_2:may("perm1")); + assert.falsy(test_role_2:may("perm2")); + end); + it("has a sensible tostring", function () + local test_role_2 = roles.new({ + id = "test-role-2"; + name = "Test Role 2"; + }); + assert.truthy(tostring(test_role_2):find("test-role-2", 1, true)); + assert.truthy(tostring(test_role_2):find("Test Role 2", 1, true)); + end); it("is restrictive by default", function () assert.falsy(test_role:may("my-permission")); end); -- cgit v1.2.3 From c6c5fec97e139ba432252d1b9370b75ad8e34c49 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 8 Oct 2022 20:39:14 +0100 Subject: util.roles: Fix tests to use autogenerated role id --- spec/util_roles_spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/util_roles_spec.lua b/spec/util_roles_spec.lua index 81723657..44d2a977 100644 --- a/spec/util_roles_spec.lua +++ b/spec/util_roles_spec.lua @@ -26,7 +26,7 @@ describe("util.roles", function () id = "test-role-2"; name = "Test Role 2"; }); - assert.truthy(tostring(test_role_2):find("test-role-2", 1, true)); + assert.truthy(tostring(test_role_2):find(test_role_2.id, 1, true)); assert.truthy(tostring(test_role_2):find("Test Role 2", 1, true)); end); it("is restrictive by default", function () -- cgit v1.2.3 From f8f90ea923315383477b936e1ec109ec30eb2dd3 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Sat, 8 Oct 2022 23:55:22 +0100 Subject: util.datetime: Add some missing test cases You guessed it, mutation testing. --- spec/util_datetime_spec.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/util_datetime_spec.lua b/spec/util_datetime_spec.lua index a35a1037..960e8aef 100644 --- a/spec/util_datetime_spec.lua +++ b/spec/util_datetime_spec.lua @@ -38,7 +38,10 @@ describe("util.datetime", function () assert.equals("22:04:05", time(1136239445)); end); it("should handle precision", function () + assert.equal("14:46:31.158200", time(1660488391.1582)) assert.equal("14:46:32.158200", time(1660488392.1582)) + assert.equal("14:46:33.158200", time(1660488393.1582)) + assert.equal("14:46:33.999900", time(1660488393.9999)) end) end); describe("#datetime", function () @@ -57,7 +60,10 @@ describe("util.datetime", function () assert.equals("2006-01-02T22:04:05Z", datetime(1136239445)); end); it("should handle precision", function () + assert.equal("2022-08-14T14:46:31.158200Z", datetime(1660488391.1582)) assert.equal("2022-08-14T14:46:32.158200Z", datetime(1660488392.1582)) + assert.equal("2022-08-14T14:46:33.158200Z", datetime(1660488393.1582)) + assert.equal("2022-08-14T14:46:33.999900Z", datetime(1660488393.9999)) end) end); describe("#legacy", function () @@ -65,6 +71,9 @@ describe("util.datetime", function () it("should exist", function () assert.is_function(legacy); end); + it("should not add precision", function () + assert.equal("20220814T14:46:31", legacy(1660488391.1582)); + end); end); describe("#parse", function () local parse = util_datetime.parse; @@ -76,6 +85,7 @@ describe("util.datetime", function () assert.equals(1511114293, parse("2017-11-19T17:58:13Z")); assert.equals(1511114330, parse("2017-11-19T18:58:50+0100")); assert.equals(1136239445, parse("2006-01-02T15:04:05-0700")); + assert.equals(1136239445, parse("2006-01-02T15:04:05-07")); end); it("should handle timezones", function () -- https://xmpp.org/extensions/xep-0082.html#example-2 and 3 @@ -85,5 +95,10 @@ describe("util.datetime", function () -- floating point comparison is not an exact science assert.truthy(math.abs(1660488392.1582 - parse("2022-08-14T14:46:32.158200Z")) < 0.001) end) + it("should return nil when given invalid inputs", function () + assert.is_nil(parse(nil)); + assert.is_nil(parse("hello world")); + assert.is_nil(parse("2017-11-19T18:58:50$0100")); + end); end); end); -- cgit v1.2.3 From 586a0d8493acfd511a2d2319ad68628d585f94df Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 11 Oct 2022 11:34:47 +0100 Subject: util.dbuffer: Remove redundant code (read_chunk() cannot fail at this point) --- util/dbuffer.lua | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/util/dbuffer.lua b/util/dbuffer.lua index 3ad5fdfe..6c671fa3 100644 --- a/util/dbuffer.lua +++ b/util/dbuffer.lua @@ -96,13 +96,9 @@ function dbuffer_methods:discard(requested_bytes) end local chunk, read_bytes = self:read_chunk(requested_bytes); - if chunk then - requested_bytes = requested_bytes - read_bytes; - if requested_bytes == 0 then -- Already read everything we need - return true; - end - else - return nil; + requested_bytes = requested_bytes - read_bytes; + if requested_bytes == 0 then -- Already read everything we need + return true; end while chunk do -- cgit v1.2.3 From ffa72d829be2b988074f269fc5a2177b74c77ab4 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 11 Oct 2022 11:37:55 +0100 Subject: util.dbuffer: Add efficient shortcuts for discard() in certain cases If the buffer is already empty, nothing to do. If we're throwing away the whole buffer, we can just empty it and avoid read_chunk() (which in turn may collapse()). These shortcuts are much more efficient. --- util/dbuffer.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/util/dbuffer.lua b/util/dbuffer.lua index 6c671fa3..0a36288d 100644 --- a/util/dbuffer.lua +++ b/util/dbuffer.lua @@ -91,8 +91,12 @@ function dbuffer_methods:read_until(char) end function dbuffer_methods:discard(requested_bytes) - if requested_bytes > self._length then - return nil; + if self._length == 0 then return true; end + if not requested_bytes or requested_bytes >= self._length then + self.front_consumed = 0; + self._length = 0; + for _ in self.items:consume() do end + return true; end local chunk, read_bytes = self:read_chunk(requested_bytes); -- cgit v1.2.3 From 44d68caf9a01b40ce42c34160a22b04b1ab4f2cd Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 11 Oct 2022 11:38:32 +0100 Subject: util.dbuffer: Add a bunch of missing test cases Found via mutation testing. --- spec/util_dbuffer_spec.lua | 72 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/spec/util_dbuffer_spec.lua b/spec/util_dbuffer_spec.lua index 83ca1823..0072a59d 100644 --- a/spec/util_dbuffer_spec.lua +++ b/spec/util_dbuffer_spec.lua @@ -6,6 +6,8 @@ describe("util.dbuffer", function () end); it("can be created", function () assert.truthy(dbuffer.new()); + assert.truthy(dbuffer.new(1)); + assert.truthy(dbuffer.new(1024)); end); it("won't create an empty buffer", function () assert.falsy(dbuffer.new(0)); @@ -15,10 +17,21 @@ describe("util.dbuffer", function () end); end); describe(":write", function () - local b = dbuffer.new(); + local b = dbuffer.new(10, 3); it("works", function () assert.truthy(b:write("hi")); end); + it("fails when the buffer is full", function () + local ret = b:write(" there world, this is a long piece of data"); + assert.is_falsy(ret); + end); + it("works when max_chunks is reached", function () + -- Chunks are an optimization, dbuffer should collapse chunks when needed + for _ = 1, 8 do + assert.truthy(b:write("!")); + end + assert.falsy(b:write("!")); -- Length reached + end); end); describe(":read", function () @@ -34,6 +47,14 @@ describe("util.dbuffer", function () assert.equal(" ", b:read()); assert.equal("world", b:read()); end); + it("fails when there is not enough data in the buffer", function () + local b = dbuffer.new(12); + b:write("hello"); + b:write(" "); + b:write("world"); + assert.is_falsy(b:read(12)); + assert.is_falsy(b:read(13)); + end); end); describe(":read_until", function () @@ -68,9 +89,46 @@ describe("util.dbuffer", function () assert.equal(5, b:len()); assert.equal("world", b:read(5)); end); + it("works across chunks", function () + assert.truthy(b:write("hello")); + assert.truthy(b:write(" ")); + assert.truthy(b:write("world")); + assert.truthy(b:discard(3)); + assert.equal(8, b:length()); + assert.truthy(b:discard(3)); + assert.equal(5, b:length()); + assert.equal("world", b:read(5)); + end); + it("can discard the entire buffer", function () + assert.equal(b:len(), 0); + assert.truthy(b:write("hello world")); + assert.truthy(b:discard(11)); + assert.equal(0, b:len()); + assert.truthy(b:write("hello world")); + assert.truthy(b:discard(12)); + assert.equal(0, b:len()); + assert.truthy(b:write("hello world")); + assert.truthy(b:discard(128)); + assert.equal(0, b:len()); + end); + it("works on an empty buffer", function () + assert.truthy(dbuffer.new():discard()); + assert.truthy(dbuffer.new():discard(0)); + assert.truthy(dbuffer.new():discard(1)); + end); end); describe(":collapse()", function () + it("works", function () + local b = dbuffer.new(); + b:write("hello"); + b:write(" "); + b:write("world"); + b:collapse(6); + local ret, bytes = b:read_chunk(); + assert.equal("hello ", ret); + assert.equal(6, bytes); + end); it("works on an empty buffer", function () local b = dbuffer.new(); b:collapse(); @@ -115,6 +173,11 @@ describe("util.dbuffer", function () end end end); + + it("works on an empty buffer", function () + local b = dbuffer.new(); + assert.equal("", b:sub(1, 12)); + end); end); describe(":byte", function () @@ -122,7 +185,11 @@ describe("util.dbuffer", function () local s = "hello world" local function test_byte(b, x, y) local string_result, buffer_result = {s:byte(x, y)}, {b:byte(x, y)}; - assert.same(string_result, buffer_result, ("buffer:byte(%d, %s) does not match string:byte()"):format(x, y and ("%d"):format(y) or "nil")); + assert.same( + string_result, + buffer_result, + ("buffer:byte(%s, %s) does not match string:byte()"):format(x and ("%d"):format(x) or "nil", y and ("%d"):format(y) or "nil") + ); end it("is equivalent to string:byte", function () @@ -132,6 +199,7 @@ describe("util.dbuffer", function () test_byte(b, 3); test_byte(b, -1); test_byte(b, -3); + test_byte(b, nil, 5); for i = -13, 13 do for j = -13, 13 do test_byte(b, i, j); -- cgit v1.2.3 From 8434b4be4b7e2f7ca614410bf008dae0433c7ec7 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 11 Oct 2022 11:53:48 +0100 Subject: tools: Add initial mutation testing script --- tools/test_mutants.sh.lua | 217 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100755 tools/test_mutants.sh.lua diff --git a/tools/test_mutants.sh.lua b/tools/test_mutants.sh.lua new file mode 100755 index 00000000..a0a55a8e --- /dev/null +++ b/tools/test_mutants.sh.lua @@ -0,0 +1,217 @@ +#!/bin/bash + +POLYGLOT=1--[===[ + +set -o pipefail + +if [[ "$#" == "0" ]]; then + echo "Lua mutation testing tool" + echo + echo "Usage:" + echo " $BASH_SOURCE MODULE_NAME SPEC_FILE" + echo + echo "Requires 'lua', 'ltokenp' and 'busted' in PATH" + exit 1; +fi + +MOD_NAME="$1" +MOD_FILE="$(lua "$BASH_SOURCE" resolve "$MOD_NAME")" + +if [[ "$MOD_FILE" == "" || ! -f "$MOD_FILE" ]]; then + echo "EE: Failed to locate module '$MOD_NAME' ($MOD_FILE)"; + exit 1; +fi + +SPEC_FILE="$2" + +if [[ "$SPEC_FILE" == "" ]]; then + SPEC_FILE="spec/${MOD_NAME/./_}_spec.lua" +fi + +if [[ "$SPEC_FILE" == "" || ! -f "$SPEC_FILE" ]]; then + echo "EE: Failed to find test spec file ($SPEC_FILE)" + exit 1; +fi + +if ! busted "$SPEC_FILE"; then + echo "EE: Tests fail on original source. Fix it"\!; + exit 1; +fi + +export MUTANT_N=0 +LIVING_MUTANTS=0 + +FILE_PREFIX="${MOD_FILE%.*}.mutant-" +FILE_SUFFIX=".${MOD_FILE##*.}" + +gen_mutant () { + echo "Generating mutant $2 to $3..." + ltokenp -s "$BASH_SOURCE" "$1" > "$3" + return "$?" +} + +# $1 = MOD_NAME, $2 = MUTANT_N, $3 = SPEC_FILE +test_mutant () { + ( + ulimit -m 131072 # 128MB + ulimit -t 16 # 16s + ulimit -f 32768 # 128MB (?) + exec busted --helper="$BASH_SOURCE" -Xhelper mutate="$1":"$2" "$3" + ) >/dev/null + return "$?"; +} + +MUTANT_FILE="${FILE_PREFIX}${MUTANT_N}${FILE_SUFFIX}" + +gen_mutant "$MOD_FILE" "$MUTANT_N" "$MUTANT_FILE" +while [[ "$?" == "0" ]]; do + if ! test_mutant "$MOD_NAME" "$MUTANT_N" "$SPEC_FILE"; then + echo "Tests successfully killed mutant $MUTANT_N"; + rm "$MUTANT_FILE"; + else + echo "Mutant $MUTANT_N lives on"\! + LIVING_MUTANTS=$((LIVING_MUTANTS+1)) + fi + MUTANT_N=$((MUTANT_N+1)) + MUTANT_FILE="${FILE_PREFIX}${MUTANT_N}${FILE_SUFFIX}" + gen_mutant "$MOD_FILE" "$MUTANT_N" "$MUTANT_FILE" +done + +if [[ "$?" != "2" ]]; then + echo "Failed: $?" + exit "$?"; +fi + +MUTANT_SCORE="$(lua -e "print(('%0.2f'):format((1-($LIVING_MUTANTS/$MUTANT_N))*100))")" +if test -f mutant-scores.txt; then + echo "$MOD_NAME $MUTANT_SCORE" >> mutant-scores.txt +fi +echo "$MOD_NAME: All $MUTANT_N mutants generated, $LIVING_MUTANTS survived (score: $MUTANT_SCORE%)" +rm "$MUTANT_FILE"; # Last file is always unmodified +exit 0; +]===] + +-- busted helper that runs mutations +if arg then + if arg[1] == "resolve" then + local filename = package.searchpath(assert(arg[2], "no module name given"), package.path); + if filename then + print(filename); + end + os.exit(filename and 0 or 1); + end + local mutants = {}; + + for i = 1, #arg do + local opt = arg[i]; + print("LOAD", i, opt) + local module_name, mutant_n = opt:match("^mutate=([^:]+):(%d+)"); + if module_name then + mutants[module_name] = tonumber(mutant_n); + end + end + + local orig_lua_searcher = package.searchers[2]; + + local function mutant_searcher(module_name) + local mutant_n = mutants[module_name]; + if not mutant_n then + return orig_lua_searcher(module_name); + end + local base_file, err = package.searchpath(module_name, package.path); + if not base_file then + return base_file, err; + end + local mutant_file = base_file:gsub("%.lua$", (".mutant-%d.lua"):format(mutant_n)); + return loadfile(mutant_file), mutant_file; + end + + if next(mutants) then + table.insert(package.searchers, 1, mutant_searcher); + end +end + +-- filter for ltokenp to mutate scripts +do + local last_output = {}; + local function emit(...) + last_output = {...}; + io.write(...) + io.write(" ") + return true; + end + + local did_mutate = false; + local count = -1; + local threshold = tonumber(os.getenv("MUTANT_N")) or 0; + local function should_mutate() + count = count + 1; + return count == threshold; + end + + local function mutate(name, value) + if name == "if" then + -- Bypass conditionals + if should_mutate() then + return emit("if true or"); + elseif should_mutate() then + return emit("if false and"); + end + elseif name == "" then + -- Introduce off-by-one errors + if should_mutate() then + return emit(("%d"):format(tonumber(value)+1)); + elseif should_mutate() then + return emit(("%d"):format(tonumber(value)-1)); + end + elseif name == "and" then + if should_mutate() then + return emit("or"); + end + elseif name == "or" then + if should_mutate() then + return emit("and"); + end + end + end + + local current_line_n, current_line_input, current_line_output = 0, {}, {}; + function FILTER(line_n,token,name,value) + if current_line_n ~= line_n then -- Finished a line, moving to the next? + if did_mutate and did_mutate.line == current_line_n then + -- The line we finished was mutated. Store the original and modified outputs. + did_mutate.line_original_src = table.concat(current_line_input, " "); + did_mutate.line_modified_src = table.concat(current_line_output, " "); + end + current_line_input = {}; + current_line_output = {}; + end + current_line_n = line_n; + if name == "" then return; end + if name == "" then + if not did_mutate then + return os.exit(2); + else + emit(("\n-- Mutated line %d (changed '%s' to '%s'):\n"):format(did_mutate.line, did_mutate.original, did_mutate.modified)) + emit( ("-- Original: %s\n"):format(did_mutate.line_original_src)) + emit( ("-- Modified: %s\n"):format(did_mutate.line_modified_src)); + return; + end + end + if name == "" then + value = string.format("%q",value); + end + if mutate(name, value) then + did_mutate = { + original = value; + modified = table.concat(last_output); + line = line_n; + }; + else + emit(value); + end + table.insert(current_line_input, value); + table.insert(current_line_output, table.concat(last_output)); + end +end + -- cgit v1.2.3 From 02352c6dcfa5548b1c43d2a6844cac1d053568cc Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 11 Oct 2022 13:12:26 +0100 Subject: .luacheckrc: Add test_mutants.sh.lua to the ignore list --- .luacheckrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.luacheckrc b/.luacheckrc index e1bb7134..cad6bb13 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -172,6 +172,7 @@ if os.getenv("PROSODY_STRICT_LINT") ~= "1" then "tools/migration/migrator/prosody_sql.lua"; "tools/migration/prosody-migrator.lua"; "tools/openfire2prosody.lua"; + "tools/test_mutants.sh.lua"; "tools/xep227toprosody.lua"; } for _, file in ipairs(exclude_files) do -- cgit v1.2.3 From e8d1a6720e4997baa9585c96078712bed2512dbb Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 11 Oct 2022 13:31:47 +0100 Subject: util.jid: Add missing test cases (98% mutant score, single remaining mutant is a string.sub equivalent) --- spec/util_jid_spec.lua | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/spec/util_jid_spec.lua b/spec/util_jid_spec.lua index b92ca06c..6bfefcec 100644 --- a/spec/util_jid_spec.lua +++ b/spec/util_jid_spec.lua @@ -48,6 +48,46 @@ describe("util.jid", function() end) end); + describe("#prepped_split()", function() + local function test(input_jid, expected_node, expected_server, expected_resource) + local rnode, rserver, rresource = jid.prepped_split(input_jid); + assert.are.equal(expected_node, rnode, "split("..tostring(input_jid)..") failed"); + assert.are.equal(expected_server, rserver, "split("..tostring(input_jid)..") failed"); + assert.are.equal(expected_resource, rresource, "split("..tostring(input_jid)..") failed"); + end + + it("should work", function() + -- Valid JIDs + test("node@server", "node", "server", nil ); + test("node@server/resource", "node", "server", "resource" ); + test("server", nil, "server", nil ); + test("server/resource", nil, "server", "resource" ); + test("server/resource@foo", nil, "server", "resource@foo" ); + test("server/resource@foo/bar", nil, "server", "resource@foo/bar"); + + -- Always invalid JIDs + test(nil, nil, nil, nil); + test("node@/server", nil, nil, nil); + test("@server", nil, nil, nil); + test("@server/resource", nil, nil, nil); + test("@/resource", nil, nil, nil); + test("@server/", nil, nil, nil); + test("server/", nil, nil, nil); + test("/resource", nil, nil, nil); + end); + it("should reject invalid arguments", function () + assert.has_error(function () jid.prepped_split(false) end) + end) + it("should strip empty root label", function () + test("node@server.", "node", "server", nil); + end); + it("should fail for JIDs that fail stringprep", function () + test("node@invalid-\128-server", nil, nil, nil); + test("@server", nil, nil, nil); + test("node@server/invalid-\000-resource", nil, nil, nil); + end); + end); + describe("#bare()", function() it("should work", function() -- cgit v1.2.3 From 6ae850c963db802a606b2fd11d670e599a43626f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 11 Oct 2022 13:33:19 +0100 Subject: util.jid: Remove redundant check from split() (micro-optimization?) --- util/jid.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/util/jid.lua b/util/jid.lua index 759af746..3e1336fc 100644 --- a/util/jid.lua +++ b/util/jid.lua @@ -35,8 +35,7 @@ local function split(jid) if jid == nil then return; end local node, nodepos = match(jid, "^([^@/]+)@()"); local host, hostpos = match(jid, "^([^@/]+)()", nodepos); - if node ~= nil and host == nil then return nil, nil, nil; end - local resource = match(jid, "^/(.+)$", hostpos); + local resource = host and match(jid, "^/(.+)$", hostpos); if (host == nil) or ((resource == nil) and #jid >= hostpos) then return nil, nil, nil; end return node, host, resource; end -- cgit v1.2.3 From 120e01f16248ddc9148b96eef5e061be7ce850be Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 11 Oct 2022 13:35:09 +0100 Subject: util.jid: Simplify boolean logic in conditionals --- util/jid.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util/jid.lua b/util/jid.lua index 3e1336fc..55567ea2 100644 --- a/util/jid.lua +++ b/util/jid.lua @@ -90,9 +90,9 @@ local function compare(jid, acl) -- TODO compare to table of rules? local jid_node, jid_host, jid_resource = split(jid); local acl_node, acl_host, acl_resource = split(acl); - if ((acl_node ~= nil and acl_node == jid_node) or acl_node == nil) and - ((acl_host ~= nil and acl_host == jid_host) or acl_host == nil) and - ((acl_resource ~= nil and acl_resource == jid_resource) or acl_resource == nil) then + if (acl_node == nil or acl_node == jid_node) and + (acl_host == nil or acl_host == jid_host) and + (acl_resource == nil or acl_resource == jid_resource) then return true end return false -- cgit v1.2.3 From 3483944568e0e01460468ca33c886df49962d2ce Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 11 Oct 2022 15:41:05 +0100 Subject: util.jid: Add test for invalid domain but valid UTF-8 (thanks jonas) --- spec/util_jid_spec.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/util_jid_spec.lua b/spec/util_jid_spec.lua index 6bfefcec..4f5fe356 100644 --- a/spec/util_jid_spec.lua +++ b/spec/util_jid_spec.lua @@ -83,6 +83,7 @@ describe("util.jid", function() end); it("should fail for JIDs that fail stringprep", function () test("node@invalid-\128-server", nil, nil, nil); + test("node@invalid-\194\128-server", nil, nil, nil); test("@server", nil, nil, nil); test("node@server/invalid-\000-resource", nil, nil, nil); end); -- cgit v1.2.3 From f081c1a756269972c1d6c8b0caf5995005fbdb21 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 13 Oct 2022 09:55:36 +0100 Subject: util.cache: Add some missing test cases Found via mutation testing. --- spec/util_cache_spec.lua | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/spec/util_cache_spec.lua b/spec/util_cache_spec.lua index 7a5522b8..2cb7b7dd 100644 --- a/spec/util_cache_spec.lua +++ b/spec/util_cache_spec.lua @@ -4,6 +4,20 @@ local cache = require "util.cache"; describe("util.cache", function() describe("#new()", function() it("should work", function() + do + local c = cache.new(1); + assert.is_not_nil(c); + + assert.has_error(function () + cache.new(0); + end); + assert.has_error(function () + cache.new(-1); + end); + assert.has_error(function () + cache.new("foo"); + end); + end local c = cache.new(5); @@ -336,5 +350,43 @@ describe("util.cache", function() assert.spy(i).was_called_with("c", "3"); assert.spy(i).was_called_with("d", "4"); end); + + local function vs(t) + local vs_ = {}; + for v in t:values() do + vs_[#vs_+1] = v; + end + return vs_; + end + + it(":values works", function () + local t = cache.new(3); + t:set("k1", "v1"); + t:set("k2", "v2"); + assert.same({"v2", "v1"}, vs(t)); + t:set("k3", "v3"); + assert.same({"v3", "v2", "v1"}, vs(t)); + t:set("k4", "v4"); + assert.same({"v4", "v3", "v2"}, vs(t)); + end); + + it(":resize works", function () + local c = cache.new(5); + for i = 1, 5 do + c:set(("k%d"):format(i), ("v%d"):format(i)); + end + assert.same({"v5", "v4", "v3", "v2", "v1"}, vs(c)); + assert.has_error(function () + c:resize(-1); + end); + assert.has_error(function () + c:resize(0); + end); + assert.has_error(function () + c:resize("foo"); + end); + c:resize(3); + assert.same({"v5", "v4", "v3"}, vs(c)); + end); end); end); -- 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(-) 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(-) 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 aa8f7d1533ae86e6832a5a1ec506d58dee213d36 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 17 Oct 2022 15:13:04 +0200 Subject: util.stanza: Add new methods to Teal interface specification at_top: 21217f7e82b9 get_child_with_attr: 39164ea2ab9e get_child_attr: e8934ce6ea0f add_error: 5f182bccf33f --- teal-src/util/stanza.d.tl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/teal-src/util/stanza.d.tl b/teal-src/util/stanza.d.tl index 1f565b88..e1ab2105 100644 --- a/teal-src/util/stanza.d.tl +++ b/teal-src/util/stanza.d.tl @@ -49,6 +49,7 @@ local record lib tag : function ( stanza_t, string, { string : string } ) : stanza_t text : function ( stanza_t, string ) : stanza_t up : function ( stanza_t ) : stanza_t + at_top : function ( stanza_t ) : boolean reset : function ( stanza_t ) : stanza_t add_direct_child : function ( stanza_t, stanza_t ) add_child : function ( stanza_t, stanza_t ) @@ -57,6 +58,8 @@ local record lib get_child : function ( stanza_t, string, string ) : stanza_t get_text : function ( stanza_t ) : string get_child_text : function ( stanza_t, string, string ) : string + get_child_attr : function ( stanza_t, string, string ) : string + get_child_with_attr : function ( stanza_t, string, string, string, function (string) : boolean ) : string child_with_name : function ( stanza_t, string, string ) : stanza_t child_with_ns : function ( stanza_t, string, string ) : stanza_t children : function ( stanza_t ) : children_iter, stanza_t, integer @@ -68,7 +71,9 @@ local record lib pretty_print : function ( stanza_t ) : string pretty_top_tag : function ( stanza_t ) : string + -- FIXME Represent util.error support get_error : function ( stanza_t ) : stanza_error_type, stanza_error_condition, string, stanza_t + add_error : function ( stanza_t, stanza_error_type, stanza_error_condition, string, string ) indent : function ( stanza_t, integer, string ) : stanza_t end -- 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. --- CHANGES | 1 + plugins/mod_blocklist.lua | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGES b/CHANGES index ea4ef9e0..12f67082 100644 --- a/CHANGES +++ b/CHANGES @@ -33,6 +33,7 @@ TRUNK ## Changes - Support sub-second precision timestamps +- mod_blocklist: New option 'migrate_legacy_blocking' to disable migration from mod_privacy ## Removed 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 7be6630dfe1f828603b455279b69947edda40c4c Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 19 Oct 2022 14:07:48 +0200 Subject: util.datamapper: Remove leftover debug assert (Only Teal, not included in build) --- teal-src/util/datamapper.tl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/teal-src/util/datamapper.tl b/teal-src/util/datamapper.tl index 73b1dfc0..23f5ecaa 100644 --- a/teal-src/util/datamapper.tl +++ b/teal-src/util/datamapper.tl @@ -133,10 +133,6 @@ local function unpack_propschema( propschema : schema_t, propname : string, curr end end - if current_ns == "urn:xmpp:reactions:0" and name == "reactions" then - assert(proptype=="array") - end - return proptype, value_where, name, namespace, prefix, single_attribute, enums end -- cgit v1.2.3 From 5bf6e7f501cb572b913e31c29780056c2bb9dccd Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 19 Oct 2022 14:21:13 +0200 Subject: util.jsonpointer: Improve tests Result of mutation testing Remaining mutants are mostly relating to the math.type() fallback. Another case being that array[#array+1] == array[#array+2] and thus doesn't matter. --- spec/util_jsonpointer_spec.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/util_jsonpointer_spec.lua b/spec/util_jsonpointer_spec.lua index ce07c7a1..75122296 100644 --- a/spec/util_jsonpointer_spec.lua +++ b/spec/util_jsonpointer_spec.lua @@ -21,9 +21,11 @@ describe("util.jsonpointer", function() }]]) end) it("works", function() + assert.is_nil(jp.resolve("string", "/string")) assert.same(example, jp.resolve(example, "")); assert.same({ "bar", "baz" }, jp.resolve(example, "/foo")); assert.same("bar", jp.resolve(example, "/foo/0")); + assert.same(nil, jp.resolve(example, "/foo/-")); assert.same(0, jp.resolve(example, "/")); assert.same(1, jp.resolve(example, "/a~1b")); assert.same(2, jp.resolve(example, "/c%d")); -- cgit v1.2.3 From 98da2af369892b6f2889d2a00949014f5db3d3a0 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 19 Oct 2022 14:34:41 +0200 Subject: util.smqueue: Improve tests Result of mutation testing. One mutant remaining. --- spec/util_smqueue_spec.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/util_smqueue_spec.lua b/spec/util_smqueue_spec.lua index 0a02a60b..858b99c0 100644 --- a/spec/util_smqueue_spec.lua +++ b/spec/util_smqueue_spec.lua @@ -5,6 +5,9 @@ describe("util.smqueue", function() describe("#new()", function() it("should work", function() + assert.has_error(function () smqueue.new(-1) end); + assert.has_error(function () smqueue.new(0) end); + assert.not_has_error(function () smqueue.new(1) end); local q = smqueue.new(10); assert.truthy(q); end) -- cgit v1.2.3 From e64c5e30c2ab1f59c5051f2bd66f053d34a6eb25 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 19 Oct 2022 16:25:05 +0200 Subject: util.startup: Provide a common Lua 5.3+ math.type() for Lua 5.2 Code deduplication --- util/format.lua | 4 +--- util/jsonpointer.lua | 4 +--- util/serialization.lua | 4 +--- util/startup.lua | 14 ++++++++++++++ 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/util/format.lua b/util/format.lua index 203bdeab..0631f423 100644 --- a/util/format.lua +++ b/util/format.lua @@ -11,9 +11,7 @@ local pack = table.pack; local valid_utf8 = require "util.encodings".utf8.valid; local type = type; local dump = require "util.serialization".new("debug"); -local num_type = math.type or function (n) - return n % 1 == 0 and n <= 9007199254740992 and n >= -9007199254740992 and "integer" or "float"; -end +local num_type = math.type; -- In Lua 5.3+ these formats throw an error if given a float local expects_integer = { c = true, d = true, i = true, o = true, u = true, X = true, x = true, }; diff --git a/util/jsonpointer.lua b/util/jsonpointer.lua index 9b871ae7..f1c354a4 100644 --- a/util/jsonpointer.lua +++ b/util/jsonpointer.lua @@ -1,6 +1,4 @@ -local m_type = math.type or function (n) - return n % 1 == 0 and n <= 9007199254740992 and n >= -9007199254740992 and "integer" or "float"; -end; +local m_type = math.type; local function unescape_token(escaped_token) local unescaped = escaped_token:gsub("~1", "/"):gsub("~0", "~") diff --git a/util/serialization.lua b/util/serialization.lua index d310a3e8..6552d53b 100644 --- a/util/serialization.lua +++ b/util/serialization.lua @@ -22,9 +22,7 @@ local pcall = pcall; local envload = require"util.envload".envload; local pos_inf, neg_inf = math.huge, -math.huge; -local m_type = math.type or function (n) - return n % 1 == 0 and n <= 9007199254740992 and n >= -9007199254740992 and "integer" or "float"; -end; +local m_type = math.type; local function rawpairs(t) return next, t, nil; diff --git a/util/startup.lua b/util/startup.lua index 10ff1875..9e512cb3 100644 --- a/util/startup.lua +++ b/util/startup.lua @@ -277,6 +277,20 @@ function startup.init_global_state() startup.detect_platform(); startup.detect_installed(); _G.prosody = prosody; + + -- COMPAT Lua < 5.3 + if not math.type then + -- luacheck: ignore 122/math + function math.type(n) + if type(n) == "number" then + if n % 1 == 0 and (n + 1 ~= n and n - 1 ~= n) then + return "integer" + else + return "float" + end + end + end + end end function startup.setup_datadir() -- cgit v1.2.3 From e2cff346410c57686478d68fabd96cf4247df927 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 20 Oct 2022 16:50:12 +0200 Subject: util.mathcompat: Module to ease reuse of math.type() Mostly to ensure it is available during tests, as util.startup is not invoked there --- teal-src/util/datamapper.tl | 2 ++ teal-src/util/jsonschema.tl | 2 ++ teal-src/util/mathcompat.tl | 15 +++++++++++++++ util/datamapper.lua | 4 ++++ util/mathcompat.lua | 13 +++++++++++++ util/serialization.lua | 4 ++++ util/startup.lua | 11 +---------- 7 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 teal-src/util/mathcompat.tl create mode 100644 util/mathcompat.lua diff --git a/teal-src/util/datamapper.tl b/teal-src/util/datamapper.tl index 23f5ecaa..4ff3a02c 100644 --- a/teal-src/util/datamapper.tl +++ b/teal-src/util/datamapper.tl @@ -19,6 +19,8 @@ -- TODO s/number/integer/ once we have appropriate math.type() compat -- +if not math.type then require "util.mathcompat" end + local st = require "util.stanza"; local json = require"util.json" local pointer = require"util.jsonpointer"; diff --git a/teal-src/util/jsonschema.tl b/teal-src/util/jsonschema.tl index 160c164c..14b04370 100644 --- a/teal-src/util/jsonschema.tl +++ b/teal-src/util/jsonschema.tl @@ -8,6 +8,8 @@ -- https://json-schema.org/draft/2020-12/json-schema-validation.html -- +if not math.type then require "util.mathcompat" end + local json = require"util.json" local null = json.null; diff --git a/teal-src/util/mathcompat.tl b/teal-src/util/mathcompat.tl new file mode 100644 index 00000000..1e3f9bab --- /dev/null +++ b/teal-src/util/mathcompat.tl @@ -0,0 +1,15 @@ +if not math.type then + local enum number_subtype + "float" "integer" + end + local function math_type(t:any) : number_subtype + if t is number then + if t % 1 == 0 and t ~= t+1 and t ~= t-1 then + return "integer" + else + return "float" + end + end + end + _G.math.type = math_type +end diff --git a/util/datamapper.lua b/util/datamapper.lua index 2378314c..e1484525 100644 --- a/util/datamapper.lua +++ b/util/datamapper.lua @@ -1,5 +1,9 @@ -- This file is generated from teal-src/util/datamapper.lua +if not math.type then + require("util.mathcompat") +end + local st = require("util.stanza"); local pointer = require("util.jsonpointer"); diff --git a/util/mathcompat.lua b/util/mathcompat.lua new file mode 100644 index 00000000..e8acb261 --- /dev/null +++ b/util/mathcompat.lua @@ -0,0 +1,13 @@ +if not math.type then + + local function math_type(t) + if type(t) == "number" then + if t % 1 == 0 and t ~= t + 1 and t ~= t - 1 then + return "integer" + else + return "float" + end + end + end + _G.math.type = math_type +end diff --git a/util/serialization.lua b/util/serialization.lua index 6552d53b..e2e104f1 100644 --- a/util/serialization.lua +++ b/util/serialization.lua @@ -21,6 +21,10 @@ local to_hex = require "util.hex".to; local pcall = pcall; local envload = require"util.envload".envload; +if not math.type then + require "util.mathcompat" +end + local pos_inf, neg_inf = math.huge, -math.huge; local m_type = math.type; diff --git a/util/startup.lua b/util/startup.lua index 9e512cb3..68b74984 100644 --- a/util/startup.lua +++ b/util/startup.lua @@ -280,16 +280,7 @@ function startup.init_global_state() -- COMPAT Lua < 5.3 if not math.type then - -- luacheck: ignore 122/math - function math.type(n) - if type(n) == "number" then - if n % 1 == 0 and (n + 1 ~= n and n - 1 ~= n) then - return "integer" - else - return "float" - end - end - end + require "util.mathcompat" end end -- cgit v1.2.3 From 1acd5e0474c00f29fa436eba61c7aceb65ed08dd Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 20 Oct 2022 17:35:01 +0200 Subject: util.human.io: Fix handling of os.execute() return values in Lua 5.2+ Wrong part of Lua 5.1 compat removed in 0f4feaf9ca64 --- util/human/io.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util/human/io.lua b/util/human/io.lua index 4fce0e94..b272af71 100644 --- a/util/human/io.lua +++ b/util/human/io.lua @@ -8,7 +8,7 @@ end; local function getchar(n) local stty_ret = os.execute("stty raw -echo 2>/dev/null"); local ok, char; - if stty_ret == true or stty_ret == 0 then + if stty_ret then ok, char = pcall(io.read, n or 1); os.execute("stty sane"); else @@ -31,11 +31,11 @@ end local function getpass() local stty_ret = os.execute("stty -echo 2>/dev/null"); - if stty_ret ~= 0 then + if not stty_ret then io.write("\027[08m"); -- ANSI 'hidden' text attribute end local ok, pass = pcall(io.read, "*l"); - if stty_ret == 0 then + if stty_ret then os.execute("stty sane"); else io.write("\027[00m"); -- cgit v1.2.3 From 640b8d49a302b8c5b2ecbc9a8464b0d409eade5d Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 20 Oct 2022 17:36:05 +0200 Subject: util.openssl: Remove Lua 5.1 os.execute() return value compat --- util/openssl.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/util/openssl.lua b/util/openssl.lua index 32b5aea7..3acb4f04 100644 --- a/util/openssl.lua +++ b/util/openssl.lua @@ -166,8 +166,7 @@ do -- Lua to shell calls. setmetatable(_M, { __index = function(_, command) return function(opts) - local ret = os_execute(serialize(command, type(opts) == "table" and opts or {})); - return ret == true or ret == 0; + return os_execute(serialize(command, type(opts) == "table" and opts or {})); end; end; }); -- cgit v1.2.3 From e673312b7f9142c0f8d50daf8de11eb398d5e1e9 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 20 Oct 2022 17:37:07 +0200 Subject: util.prosodyctl: Remove Lua 5.1 os.execute() return value compat --- util/prosodyctl.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/util/prosodyctl.lua b/util/prosodyctl.lua index 4d49cd16..b3163799 100644 --- a/util/prosodyctl.lua +++ b/util/prosodyctl.lua @@ -224,8 +224,7 @@ local function call_luarocks(operation, mod, server) local ok, _, code = os.execute(render_cli("luarocks --lua-version={luav} {op} --tree={dir} {server&--server={server}} {mod?}", { dir = dir; op = operation; mod = mod; server = server; luav = _VERSION:match("5%.%d"); })); - if type(ok) == "number" then code = ok; end - return code; + return ok and code; end return { -- cgit v1.2.3 From 03814250c300b8cbaaccfde6bdb856039c3a4691 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 20 Oct 2022 17:37:33 +0200 Subject: util.prosodyctl.cert: Remove Lua 5.1 os.execute() return value compat --- util/prosodyctl/cert.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/prosodyctl/cert.lua b/util/prosodyctl/cert.lua index 02c81585..ebc14a4e 100644 --- a/util/prosodyctl/cert.lua +++ b/util/prosodyctl/cert.lua @@ -179,7 +179,7 @@ local function copy(from, to, umask, owner, group) os.execute(("chown -c --reference=%s %s"):format(sh_esc(cert_basedir), sh_esc(to))); elseif owner and group then local ok = os.execute(("chown %s:%s %s"):format(sh_esc(owner), sh_esc(group), sh_esc(to))); - assert(ok == true or ok == 0, "Failed to change ownership of "..to); + assert(ok, "Failed to change ownership of "..to); end if old_umask then pposix.umask(old_umask); end return true; -- cgit v1.2.3 From d33904f7e96bbc4d1ea4e5dec544bad82d2be15f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 2 Dec 2022 20:27:32 +0000 Subject: util.hashring: tests: don't randomize order - they are written in a sequential style --- spec/util_hashring_spec.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/util_hashring_spec.lua b/spec/util_hashring_spec.lua index d8801774..05f053e4 100644 --- a/spec/util_hashring_spec.lua +++ b/spec/util_hashring_spec.lua @@ -1,6 +1,7 @@ local hashring = require "util.hashring"; describe("util.hashring", function () + randomize(false); local sha256 = require "util.hashes".sha256; -- cgit v1.2.3 From ae84717255cb196013c1b0fe0a625b2b443e7dc4 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 2 Dec 2022 20:32:36 +0000 Subject: util.hashring: Support associating arbitrary data with nodes In this API, a 'node' is always a simple text string. Sometimes the caller may have a more complex structure representing a node, but the hash ring is really only concerned with the node's name. This API change allows :add_nodes() to take a table of `node_name = value` pairs, as well as the simple array of node names previously accepted. The 'value' of the selected node is returned as a new second result from :get_node(). If no value is passed when a node is added, it defaults to `true` (as before, but this was never previously exposed). --- spec/util_hashring_spec.lua | 7 +++++++ util/hashring.lua | 32 +++++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/spec/util_hashring_spec.lua b/spec/util_hashring_spec.lua index 05f053e4..4f6ec3a3 100644 --- a/spec/util_hashring_spec.lua +++ b/spec/util_hashring_spec.lua @@ -83,4 +83,11 @@ describe("util.hashring", function () end end); + it("should support values associated with nodes", function () + local r = hashring.new(128, sha256); + r:add_node("node1", { a = 1 }); + local node, value = r:get_node("foo"); + assert.is_equal("node1", node); + assert.same({ a = 1 }, value); + end); end); diff --git a/util/hashring.lua b/util/hashring.lua index d4555669..5e71654b 100644 --- a/util/hashring.lua +++ b/util/hashring.lua @@ -1,3 +1,5 @@ +local it = require "util.iterators"; + local function generate_ring(nodes, num_replicas, hash) local new_ring = {}; for _, node_name in ipairs(nodes) do @@ -28,18 +30,22 @@ local function new(num_replicas, hash_function) return setmetatable({ nodes = {}, num_replicas = num_replicas, hash = hash_function }, hashring_mt); end; -function hashring_methods:add_node(name) +function hashring_methods:add_node(name, value) self.ring = nil; - self.nodes[name] = true; + self.nodes[name] = value == nil and true or value; table.insert(self.nodes, name); return true; end function hashring_methods:add_nodes(nodes) self.ring = nil; - for _, node_name in ipairs(nodes) do - if not self.nodes[node_name] then - self.nodes[node_name] = true; + local iter = pairs; + if nodes[1] then -- simple array? + iter = it.values; + end + for node_name, node_value in iter(nodes) do + if self.nodes[node_name] == nil then + self.nodes[node_name] = node_value == nil and true or node_value; table.insert(self.nodes, node_name); end end @@ -48,7 +54,7 @@ end function hashring_methods:remove_node(node_name) self.ring = nil; - if self.nodes[node_name] then + if self.nodes[node_name] ~= nil then for i, stored_node_name in ipairs(self.nodes) do if node_name == stored_node_name then self.nodes[node_name] = nil; @@ -69,18 +75,26 @@ end function hashring_methods:clone() local clone_hashring = new(self.num_replicas, self.hash); - clone_hashring:add_nodes(self.nodes); + for node_name, node_value in pairs(self.nodes) do + clone_hashring.nodes[node_name] = node_value; + end + clone_hashring.ring = nil; return clone_hashring; end function hashring_methods:get_node(key) + local node; local key_hash = self.hash(key); for _, replica_hash in ipairs(self.ring) do if key_hash < replica_hash then - return self.ring[replica_hash]; + node = self.ring[replica_hash]; + break; end end - return self.ring[self.ring[1]]; + if not node then + node = self.ring[self.ring[1]]; + end + return node, self.nodes[node]; end return { -- 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(-) 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