diff options
-rw-r--r-- | CHANGES | 2 | ||||
-rw-r--r-- | doc/doap.xml | 30 | ||||
-rw-r--r-- | net/http/server.lua | 2 | ||||
-rw-r--r-- | net/server_epoll.lua | 108 | ||||
-rw-r--r-- | plugins/mod_admin_adhoc.lua | 20 | ||||
-rw-r--r-- | plugins/mod_admin_shell.lua | 21 | ||||
-rw-r--r-- | plugins/mod_authz_internal.lua | 2 | ||||
-rw-r--r-- | plugins/mod_bookmarks.lua | 15 | ||||
-rw-r--r-- | plugins/mod_csi.lua | 4 | ||||
-rw-r--r-- | plugins/mod_http_file_share.lua | 4 | ||||
-rw-r--r-- | plugins/mod_invites_adhoc.lua | 30 | ||||
-rw-r--r-- | plugins/mod_pubsub/mod_pubsub.lua | 23 | ||||
-rw-r--r-- | plugins/mod_pubsub/pubsub.lib.lua | 56 | ||||
-rw-r--r-- | plugins/mod_s2s.lua | 30 | ||||
-rw-r--r-- | plugins/mod_smacks.lua | 12 | ||||
-rw-r--r-- | plugins/muc/hats.lib.lua | 2 | ||||
-rw-r--r-- | plugins/muc/muc.lib.lua | 10 | ||||
-rw-r--r-- | spec/scansion/bookmarks2.scs | 181 | ||||
-rw-r--r-- | spec/util_crypto_spec.lua | 21 | ||||
-rw-r--r-- | spec/util_error_spec.lua | 14 | ||||
-rw-r--r-- | spec/util_pubsub_spec.lua | 12 | ||||
-rw-r--r-- | spec/util_queue_spec.lua | 19 | ||||
-rw-r--r-- | teal-src/prosody/util/xtemplate.tl | 8 | ||||
-rw-r--r-- | util-src/crypto.c | 116 | ||||
-rw-r--r-- | util/dnsregistry.lua | 7 | ||||
-rw-r--r-- | util/pubsub.lua | 26 | ||||
-rw-r--r-- | util/xtemplate.lua | 6 |
27 files changed, 622 insertions, 159 deletions
@@ -6,6 +6,7 @@ TRUNK ### Administration - Add 'watch log' command to follow live debug logs at runtime (even if disabled) +- mod_announce: Add shell commands to send messages to all users, online users, or limited by roles ### Networking @@ -73,6 +74,7 @@ TRUNK - Support for Type=notify and notify-reload systemd service type added - Support for the roster *group* access_model in mod_pep - Support for systemd socket activation in server_epoll +- mod_invites_adhoc gained a command for creating password resets ## Removed diff --git a/doc/doap.xml b/doc/doap.xml index 62e4063c..cf150c36 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -72,7 +72,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html"/> - <xmpp:version>2.13.0</xmpp:version> + <xmpp:version>2.13.1</xmpp:version> <xmpp:since>0.4.0</xmpp:since> <xmpp:status>partial</xmpp:status> <xmpp:note>no support for multiple items (reported tag)</xmpp:note> @@ -107,7 +107,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html"/> - <xmpp:version>2.5rc3</xmpp:version> + <xmpp:version>2.5.0</xmpp:version> <xmpp:since>0.10.0</xmpp:since> <xmpp:status>complete</xmpp:status> </xmpp:SupportedXep> @@ -274,13 +274,13 @@ <xmpp:version>1.0</xmpp:version> <xmpp:since>0.9.0</xmpp:since> <xmpp:status>complete</xmpp:status> - <xmpp:note>util.jid.(un)escape, missing rejection of \20 at start or end per xep version 1.1</xmpp:note> + <xmpp:note>util.jid.(un)escape, missing rejection of \20 at start or end per xep version 1.1. Missing PRECIS for version 1.1.1.</xmpp:note> </xmpp:SupportedXep> </implements> <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0107.html"/> - <xmpp:version>1.2.1</xmpp:version> + <xmpp:version>1.2.2</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:note>via XEP-0163</xmpp:note> </xmpp:SupportedXep> @@ -353,7 +353,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0133.html"/> - <xmpp:version>1.2</xmpp:version> + <xmpp:version>1.3.1</xmpp:version> <xmpp:since>0.7.0</xmpp:since> <xmpp:status>partial</xmpp:status> <xmpp:note>mod_admin_adhoc, missing some commands</xmpp:note> @@ -372,7 +372,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0153.html"/> - <xmpp:version>1.1</xmpp:version> + <xmpp:version>1.1.1</xmpp:version> <xmpp:since>0.11.0</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>via XEP-0398</xmpp:note> @@ -508,7 +508,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html"/> - <xmpp:version>1.6</xmpp:version> + <xmpp:version>1.6.2</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:since>0.12.0</xmpp:since> <xmpp:note>mod_smacks</xmpp:note> @@ -594,7 +594,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0223.html"/> - <xmpp:version>1.1</xmpp:version> + <xmpp:version>1.1.1</xmpp:version> <xmpp:since>0.11.0</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>mod_pep</xmpp:note> @@ -655,7 +655,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0292.html"/> - <xmpp:version>0.11</xmpp:version> + <xmpp:version>0.12.0</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:since>0.11.0</xmpp:since> <xmpp:note>mod_vcard4, mod_vcard_legacy</xmpp:note> @@ -690,7 +690,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0313.html"/> - <xmpp:version>1.1.0</xmpp:version> + <xmpp:version>1.1.2</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:since>0.10.0</xmpp:since> <xmpp:note>mod_mam, mod_muc_mam</xmpp:note> @@ -787,7 +787,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0398.html"/> - <xmpp:version>0.2.1</xmpp:version> + <xmpp:version>1.0.0</xmpp:version> <xmpp:since>0.11.0</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>mod_vcard_legacy</xmpp:note> @@ -804,7 +804,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0402.html"/> - <xmpp:version>1.1.3</xmpp:version> + <xmpp:version>1.2.0</xmpp:version> <xmpp:since>0.12.0</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>mod_bookmarks</xmpp:note> @@ -831,7 +831,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0421.html"/> - <xmpp:version>0.1.0</xmpp:version> + <xmpp:version>0.2.0</xmpp:version> <xmpp:since>0.12.0</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>mod_muc</xmpp:note> @@ -847,7 +847,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0440.html"/> - <xmpp:version>0.4.0</xmpp:version> + <xmpp:version>0.4.2</xmpp:version> <xmpp:since>trunk</xmpp:since> <xmpp:status>complete</xmpp:status> </xmpp:SupportedXep> @@ -871,7 +871,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0478.html"/> - <xmpp:version>0.1.0</xmpp:version> + <xmpp:version>0.2.0</xmpp:version> <xmpp:since>trunk</xmpp:since> </xmpp:SupportedXep> </implements> diff --git a/net/http/server.lua b/net/http/server.lua index edc6d879..1ef42b2e 100644 --- a/net/http/server.lua +++ b/net/http/server.lua @@ -206,7 +206,7 @@ local function handle_result(request, response, result) end elseif result_type == "string" then body = result; - elseif errors.is_err(result) then + elseif errors.is_error(result) then response.status_code = result.code or 500; body = events.fire_event("http-error", { request = request, response = response, code = result.code or 500, error = result }); elseif promise.is_promise(result) then diff --git a/net/server_epoll.lua b/net/server_epoll.lua index 6c110241..24678842 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -6,8 +6,6 @@ -- -local t_insert = table.insert; -local t_concat = table.concat; local setmetatable = setmetatable; local pcall = pcall; local type = type; @@ -22,6 +20,7 @@ local realtime = require "prosody.util.time".now; local monotonic = require "prosody.util.time".monotonic; local indexedbheap = require "prosody.util.indexedbheap"; local createtable = require "prosody.util.table".create; +local dbuffer = require "prosody.util.dbuffer"; local inet = require "prosody.util.net"; local inet_pton = inet.pton; local _SOCKETINVALID = socket._SOCKETINVALID or -1; @@ -94,6 +93,15 @@ local default_config = { __index = { -- Size of chunks to read from sockets read_size = 8192; + -- Maximum size of send buffer, after which additional data is rejected + max_send_buffer_size = 32*1024*1024; + + -- How many chunks (immutable strings) to keep in the send buffer + send_buffer_chunks = nil; + + -- Maximum amount of data to send at once (to the TCP buffers), default based on /proc/sys/net/ipv4/tcp_wmem + max_send_chunk = 4*1024*1024; + -- Timeout used during between steps in TLS handshakes ssl_handshake_timeout = 60; @@ -533,26 +541,21 @@ function interface:onwritable() self:onconnect(); if not self.conn then return nil, "no-conn"; end -- could have been closed in onconnect self:on("predrain"); - local buffer = self.writebuffer; - local data = buffer or ""; - if type(buffer) == "table" then - if buffer[3] then - data = t_concat(data); - elseif buffer[2] then - data = buffer[1] .. buffer[2]; - else - data = buffer[1] or ""; - end - end + local buffer = self.writebuffer or ""; + -- Naming things ... s/data/slice/ ? + local data = buffer:sub(1, cfg.max_send_chunk); local ok, err, partial = self.conn:send(data); self._writable = ok; - if ok then + if ok and #data < #buffer then + -- Sent the whole 'data' but there's more in the buffer + ok, err, partial = nil, "timeout", ok; + end + self:debug("Sent %d out of %d buffered bytes", ok and #data or partial or 0, #buffer); + if ok then -- all the data we had was sent successfully self:set(nil, false); if cfg.keep_buffers and type(buffer) == "table" then - for i = #buffer, 1, -1 do - buffer[i] = nil; - end - else + buffer:discard(ok); + else -- string or don't keep buffers self.writebuffer = nil; end self._writing = nil; @@ -560,14 +563,10 @@ function interface:onwritable() self:ondrain(); -- Be aware of writes in ondrain return ok; elseif partial then - self:debug("Sent %d out of %d buffered bytes", partial, #data); - if cfg.keep_buffers and type(buffer) == "table" then - buffer[1] = data:sub(partial+1); - for i = #buffer, 2, -1 do - buffer[i] = nil; - end + if type(buffer) == "table" then + buffer:discard(partial); else - self.writebuffer = data:sub(partial+1); + self.writebuffer = data:sub(partial + 1); end self:set(nil, true); self:setwritetimeout(); @@ -595,13 +594,51 @@ end -- Add data to write buffer and set flag for wanting to write function interface:write(data) local buffer = self.writebuffer; - if type(buffer) == "table" then - t_insert(buffer, data); - elseif type(buffer) == "string" then - self:noise("Allocating buffer!") - self.writebuffer = { buffer, data }; - elseif buffer == nil then + -- (nil) -> save string + -- (string) -> convert to buffer (3 tables!) + -- (buffer) -> write to buffer + if not buffer then self.writebuffer = data; + elseif type(buffer) == "string" then + local prev_buffer = buffer; + buffer = dbuffer.new(cfg.max_send_buffer_size, cfg.send_buffer_chunks); + self.writebuffer = buffer; + if prev_buffer then + -- TODO refactor, there's 3 copies of these lines + if not buffer:write(prev_buffer) then + if self._write_lock then + return false; + end + -- Try to flush buffer to make room + self:onwritable(); + if not buffer:write(prev_buffer) then + self:on("disconnect", "no space left in buffer"); + self:destroy(); + return false; + end + end + end + if not buffer:write(data) then + if self._write_lock then + return false; + end + self:onwritable(); + if not buffer:write(data) then + self:on("disconnect", "no space left in buffer"); + self:destroy(); + return false; + end + end + elseif not buffer:write(data) then + if self._write_lock then + return false; + end + self:onwritable(); + if not buffer:write(data) then + self:on("disconnect", "no space left in buffer"); + self:destroy(); + return false; + end end if not self._write_lock and not self._writing then if self._writable and cfg.opportunistic_writes and not self._opportunistic_write then @@ -619,7 +656,7 @@ interface.send = interface.write; -- Close, possibly after writing is done function interface:close() - if self._connected and self.writebuffer and (self.writebuffer[1] or type(self.writebuffer) == "string") then + if self.writebuffer and #self.writebuffer ~= 0 then self._connected = false; self:set(false, true); -- Flush final buffer contents self:setreadtimeout(false); @@ -701,7 +738,7 @@ end function interface:starttls(tls_ctx) if tls_ctx then self.tls_ctx = tls_ctx; end self.starttls = false; - if self.writebuffer and (self.writebuffer[1] or type(self.writebuffer) == "string") then + if self.writebuffer and #self.writebuffer ~= 0 then self:debug("Start TLS after write"); self.ondrain = interface.starttls; self:set(nil, true); -- make sure wantwrite is set @@ -935,7 +972,10 @@ function interface:resume_writes() end self:noise("Resume writes"); self._write_lock = nil; - if self.writebuffer and (self.writebuffer[1] or type(self.writebuffer) == "string") then + if self.writebuffer and #self.writebuffer ~= 0 then + if cfg.opportunistic_writes then + return self:onwritable(); + end self:setwritetimeout(); self:set(nil, true); end diff --git a/plugins/mod_admin_adhoc.lua b/plugins/mod_admin_adhoc.lua index ee26b7e5..ca84f975 100644 --- a/plugins/mod_admin_adhoc.lua +++ b/plugins/mod_admin_adhoc.lua @@ -592,15 +592,15 @@ end, function(fields, err, data) return generate_error_message(err); end local ok_list, err_list = {}, {}; - for _, module in ipairs(fields.modules) do - local ok, err = modulemanager.reload(module_host, module); + for _, module_ in ipairs(fields.modules) do + local ok, err = modulemanager.reload(module_host, module_); if ok then - ok_list[#ok_list + 1] = module; + ok_list[#ok_list + 1] = module_; else - err_list[#err_list + 1] = module .. "(Error: " .. tostring(err) .. ")"; + err_list[#err_list + 1] = module_ .. "(Error: " .. tostring(err) .. ")"; end + module:log("info", "mod_%s reloaded by %s", module_, jid.bare(data.from)); end - module:log("info", "mod_%s reloaded by %s", fields.module, jid.bare(data.from)); local info = (#ok_list > 0 and ("The following modules were successfully reloaded on host "..module_host..":\n"..t_concat(ok_list, "\n")) or "") .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. (#err_list > 0 and ("Failed to reload the following modules on host "..module_host..":\n"..t_concat(err_list, "\n")) or ""); @@ -742,15 +742,15 @@ end, function(fields, err, data) return generate_error_message(err); end local ok_list, err_list = {}, {}; - for _, module in ipairs(fields.modules) do - local ok, err = modulemanager.unload(module_host, module); + for _, module_ in ipairs(fields.modules) do + local ok, err = modulemanager.unload(module_host, module_); if ok then - ok_list[#ok_list + 1] = module; + ok_list[#ok_list + 1] = module_; else - err_list[#err_list + 1] = module .. "(Error: " .. tostring(err) .. ")"; + err_list[#err_list + 1] = module_ .. "(Error: " .. tostring(err) .. ")"; end + module:log("info", "mod_%s unloaded by %s", module_, jid.bare(data.from)); end - module:log("info", "mod_%s unloaded by %s", fields.module, jid.bare(data.from)); local info = (#ok_list > 0 and ("The following modules were successfully unloaded on host "..module_host..":\n"..t_concat(ok_list, "\n")) or "") .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. (#err_list > 0 and ("Failed to unload the following modules on host "..module_host..":\n"..t_concat(err_list, "\n")) or ""); diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index e6b44f00..0b8d3c43 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -631,6 +631,7 @@ describe_command [[module:load(module, host) - Load the specified module on the function def_env.module:load(name, hosts) hosts = get_hosts_with_module(hosts); + local already_loaded = set.new(); -- Load the module for each host local ok, err, count, mod = true, nil, 0; for host in hosts do @@ -655,10 +656,18 @@ function def_env.module:load(name, hosts) self.session.print("Note: Module will not be loaded after restart unless enabled in configuration"); end end + else + already_loaded:add(host); end end - return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); + if not ok then + return ok, "Last error: "..tostring(err); + end + if already_loaded == hosts then + return ok, "Module already loaded"; + end + return ok, "Module loaded onto "..count.." host"..(count ~= 1 and "s" or ""); end describe_command [[module:unload(module, host) - The same, but just unloads the module from memory]] @@ -1770,7 +1779,11 @@ function def_env.user:setrole(jid, host, new_role) 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); + if userhost == host then + return um.set_user_role(username, userhost, new_role); + else + return um.set_jid_role(jid, host, new_role); + end end describe_command [[user:addrole(jid, host, role) - Add a secondary role to a user]] @@ -1781,6 +1794,8 @@ function def_env.user:addrole(jid, host, new_role) return nil, "No such host: "..host; elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then return nil, "No such user"; + elseif userhost ~= host then + return nil, "Can't add roles outside users own host" end return um.add_user_secondary_role(username, host, new_role); end @@ -1793,6 +1808,8 @@ function def_env.user:delrole(jid, host, role_name) return nil, "No such host: "..host; elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then return nil, "No such user"; + elseif userhost ~= host then + return nil, "Can't remove roles outside users own host" end return um.remove_user_secondary_role(username, host, role_name); end diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index 96324734..07091a04 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -265,7 +265,7 @@ function get_jid_role(jid) end function set_jid_role(jid, role_name) -- luacheck: ignore 212 - return false; + return false, "not-implemented"; end function get_jids_with_role(role_name) diff --git a/plugins/mod_bookmarks.lua b/plugins/mod_bookmarks.lua index be665d0f..e6e74f56 100644 --- a/plugins/mod_bookmarks.lua +++ b/plugins/mod_bookmarks.lua @@ -167,10 +167,15 @@ local function publish_to_pep(jid, bookmarks, synchronise) if synchronise then -- If we set zero legacy bookmarks, purge the bookmarks 2 node. module:log("debug", "No bookmark in the set, purging instead."); - return service:purge(namespace, jid, true); - else - return true; + local ok, err = service:purge(namespace, jid, true); + -- It's okay if no node exists when purging, user has + -- no bookmarks anyway. + if not ok and err ~= "item-not-found" then + module:log("error", "Failed to clear items from bookmarks 2 node: %s", err); + return ok, err; + end end + return true; end -- Retrieve the current bookmarks2. @@ -309,7 +314,7 @@ local function on_publish_legacy_pep(event) local ok, err = publish_to_pep(session.full_jid, bookmarks, true); if not ok then - module:log("error", "Failed to publish to PEP bookmarks for %s@%s: %s", session.username, session.host, err); + module:log("error", "Failed to sync legacy bookmarks to PEP for %s@%s: %s", session.username, session.host, err); session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to store bookmarks to PEP")); return true; end @@ -335,7 +340,7 @@ local function on_publish_private_xml(event) local ok, err = publish_to_pep(session.full_jid, bookmarks, true); if not ok then - module:log("error", "Failed to publish to PEP bookmarks for %s@%s: %s", session.username, session.host, err); + module:log("error", "Failed to sync private XML bookmarks to PEP for %s@%s: %s", session.username, session.host, err); session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to store bookmarks to PEP")); return true; end diff --git a/plugins/mod_csi.lua b/plugins/mod_csi.lua index 73c081b1..76a5afd4 100644 --- a/plugins/mod_csi.lua +++ b/plugins/mod_csi.lua @@ -34,9 +34,9 @@ module:hook_global("stats-update", function() if session.state == "inactive" then inactive = inactive + 1; elseif session.state == "active" then - inactive = inactive + 1; + active = active + 1; elseif session.state == "flushing" then - inactive = inactive + 1; + flushing = flushing + 1; end end end diff --git a/plugins/mod_http_file_share.lua b/plugins/mod_http_file_share.lua index cfc647d4..48972067 100644 --- a/plugins/mod_http_file_share.lua +++ b/plugins/mod_http_file_share.lua @@ -79,12 +79,12 @@ local measure_upload_cache_size = module:measure("upload_cache", "amount"); local measure_quota_cache_size = module:measure("quota_cache", "amount"); local measure_total_storage_usage = module:measure("total_storage", "amount", { unit = "bytes" }); -module:on_ready(function () +do local total, err = persist_stats:get(nil, "total"); if not err then total_storage_usage = tonumber(total) or 0; end -end) +end module:hook_global("stats-update", function () measure_upload_cache_size(upload_cache:count()); diff --git a/plugins/mod_invites_adhoc.lua b/plugins/mod_invites_adhoc.lua index c9954d8c..3ef4116d 100644 --- a/plugins/mod_invites_adhoc.lua +++ b/plugins/mod_invites_adhoc.lua @@ -2,6 +2,7 @@ local dataforms = require "prosody.util.dataforms"; local datetime = require "prosody.util.datetime"; local split_jid = require "prosody.util.jid".split; +local adhocutil = require "prosody.util.adhoc"; local new_adhoc = module:require("adhoc").new; @@ -98,3 +99,32 @@ module:provides("adhoc", new_adhoc("Create new account invite", "urn:xmpp:invite }; }; end, "admin")); + +local password_reset_form = dataforms.new({ + title = "Generate Password Reset Invite"; + { + name = "accountjid"; + type = "jid-single"; + required = true; + label = "The XMPP ID for the account to generate a password reset invite for"; + }; +}); + +module:provides("adhoc", new_adhoc("Create password reset invite", "xmpp:prosody.im/mod_invites_adhoc#password-reset", + adhocutil.new_simple_form(password_reset_form, + function (fields, err) + if err then return { status = "completed"; error = { message = "Fill in the form correctly" } }; end + local username = split_jid(fields.accountjid); + local invite = invites.create_account_reset(username); + return { + status = "completed"; + result = { + layout = invite_result_form; + values = { + uri = invite.uri; + url = invite.landing_page; + expire = datetime.datetime(invite.expires); + }; + }; + }; + end), "admin")); diff --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua index 4f83088a..c17d9e63 100644 --- a/plugins/mod_pubsub/mod_pubsub.lua +++ b/plugins/mod_pubsub/mod_pubsub.lua @@ -190,10 +190,22 @@ module:hook("host-disco-items", function (event) end); local admin_aff = module:get_option_enum("default_admin_affiliation", "owner", "publisher", "member", "outcast", "none"); + module:default_permission("prosody:admin", ":service-admin"); -local function get_affiliation(jid) +module:default_permission("prosody:admin", ":create-node"); + +local function get_affiliation(jid, _, action) local bare_jid = jid_bare(jid); - if bare_jid == module.host or module:may(":service-admin", bare_jid) then + if bare_jid == module.host then + -- The host itself (i.e. local modules) is treated as an admin. + -- Check this first as to avoid sendig a host JID to :may() + return admin_aff; + end + if action == "create" and module:may(":create-node", bare_jid) then + -- Only one affiliation is allowed to create nodes by default + return "owner"; + end + if module:may(":service-admin", bare_jid) then return admin_aff; end end @@ -244,6 +256,13 @@ function module.load() broadcaster = simple_broadcast; itemcheck = is_item_stanza; check_node_config = check_node_config; + metadata_subset = { + "title"; + "description"; + "payload_type"; + "access_model"; + "publish_model"; + }; get_affiliation = get_affiliation; jid = module.host; diff --git a/plugins/mod_pubsub/pubsub.lib.lua b/plugins/mod_pubsub/pubsub.lib.lua index 8ae0a896..f4d44f36 100644 --- a/plugins/mod_pubsub/pubsub.lib.lua +++ b/plugins/mod_pubsub/pubsub.lib.lua @@ -1,4 +1,3 @@ -local t_unpack = table.unpack; local time_now = os.time; local jid_prep = require "prosody.util.jid".prep; @@ -18,7 +17,7 @@ local _M = {}; local handlers = {}; _M.handlers = handlers; -local pubsub_errors = { +local pubsub_errors = errors.init("pubsub", xmlns_pubsub_errors, { ["conflict"] = { "cancel", "conflict" }; ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" }; ["jid-required"] = { "modify", "bad-request", nil, "jid-required" }; @@ -33,16 +32,13 @@ local pubsub_errors = { ["precondition-not-met"] = { "cancel", "conflict", nil, "precondition-not-met" }; ["invalid-item"] = { "modify", "bad-request", "invalid item" }; ["persistent-items-unsupported"] = { "cancel", "feature-not-implemented", nil, "persistent-items" }; -}; -local function pubsub_error_reply(stanza, error) - local e = pubsub_errors[error]; - if not e and errors.is_err(error) then - e = { error.type, error.condition, error.text, error.pubsub_condition }; - end - local reply = st.error_reply(stanza, t_unpack(e, 1, 3)); - if e[4] then - reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up(); +}); +local function pubsub_error_reply(stanza, error, context) + local err = pubsub_errors.wrap(error, context); + if error == "precondition-not-met" and type(context) == "table" and type(context.field) == "string" then + err.text = "Field does not match: " .. context.field; end + local reply = st.error_reply(stanza, err); return reply; end _M.pubsub_error_reply = pubsub_error_reply; @@ -206,23 +202,28 @@ local node_metadata_form = dataform { }; { type = "text-single"; - name = "pubsub#title"; + name = "title"; + var = "pubsub#title"; }; { type = "text-single"; - name = "pubsub#description"; + name = "description"; + var = "pubsub#description"; }; { type = "text-single"; - name = "pubsub#type"; + name = "payload_type"; + var = "pubsub#type"; }; { type = "text-single"; - name = "pubsub#access_model"; + name = "access_model"; + var = "pubsub#access_model"; }; { type = "text-single"; - name = "pubsub#publish_model"; + name = "publish_model"; + var = "pubsub#publish_model"; }; }; _M.node_metadata_form = node_metadata_form; @@ -294,27 +295,14 @@ end function _M.handle_disco_info_node(event, service) local stanza, reply, node = event.stanza, event.reply, event.node; - local ok, ret = service:get_nodes(stanza.attr.from); + local ok, meta = service:get_node_metadata(node, stanza.attr.from); if not ok then - event.origin.send(pubsub_error_reply(stanza, ret)); - return true; - end - local node_obj = ret[node]; - if not node_obj then - event.origin.send(pubsub_error_reply(stanza, "item-not-found")); + event.origin.send(pubsub_error_reply(stanza, meta)); return true; end event.exists = true; reply:tag("identity", { category = "pubsub", type = "leaf" }):up(); - if node_obj.config then - reply:add_child(node_metadata_form:form({ - ["pubsub#title"] = node_obj.config.title; - ["pubsub#description"] = node_obj.config.description; - ["pubsub#type"] = node_obj.config.payload_type; - ["pubsub#access_model"] = node_obj.config.access_model; - ["pubsub#publish_model"] = node_obj.config.publish_model; - }, "result")); - end + reply:add_child(node_metadata_form:form(meta, "result")); end function _M.handle_disco_items_node(event, service) @@ -685,7 +673,7 @@ function handlers.set_publish(origin, stanza, publish, service) if item then item.attr.publisher = service.config.normalize_jid(stanza.attr.from); end - local ok, ret = service:publish(node, stanza.attr.from, id, item, required_config); + local ok, ret, context = service:publish(node, stanza.attr.from, id, item, required_config); local reply; if ok then if type(ok) == "string" then @@ -696,7 +684,7 @@ function handlers.set_publish(origin, stanza, publish, service) :tag("publish", { node = node }) :tag("item", { id = id }); else - reply = pubsub_error_reply(stanza, ret); + reply = pubsub_error_reply(stanza, ret, context); end origin.send(reply); return true; diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index 04fd5bc3..638ace3d 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -13,7 +13,6 @@ local hosts = prosody.hosts; local core_process_stanza = prosody.core_process_stanza; local tostring, type = tostring, type; -local t_insert = table.insert; local traceback = debug.traceback; local add_task = require "prosody.util.timer".add_task; @@ -33,6 +32,7 @@ local service = require "prosody.net.resolvers.service"; local resolver_chain = require "prosody.net.resolvers.chain"; local errors = require "prosody.util.error"; local set = require "prosody.util.set"; +local queue = require "prosody.util.queue"; local connect_timeout = module:get_option_period("s2s_timeout", 90); local stream_close_timeout = module:get_option_period("s2s_close_timeout", 5); @@ -42,6 +42,7 @@ local secure_domains, insecure_domains = module:get_option_set("s2s_secure_domains", {})._items, module:get_option_set("s2s_insecure_domains", {})._items; local require_encryption = module:get_option_boolean("s2s_require_encryption", true); local stanza_size_limit = module:get_option_integer("s2s_stanza_size_limit", 1024*512, 10000); +local sendq_size = module:get_option_integer("s2s_send_queue_size", 1024*32, 1); local advertised_idle_timeout = 14*60; -- default in all net.server implementations local network_settings = module:get_option("network_settings"); @@ -134,7 +135,7 @@ local bouncy_stanzas = { message = true, presence = true, iq = true }; local function bounce_sendq(session, reason) local sendq = session.sendq; if not sendq then return; end - session.log("info", "Sending error replies for %d queued stanzas because of failed outgoing connection to %s", #sendq, session.to_host); + session.log("info", "Sending error replies for %d queued stanzas because of failed outgoing connection to %s", sendq.count(), session.to_host); local dummy = { type = "s2sin"; send = function () @@ -153,12 +154,12 @@ local function bounce_sendq(session, reason) if session.had_stream then -- set when a stream is opened by the remote error_type, condition = "wait", "remote-server-timeout"; end - if errors.is_err(reason) then + if errors.is_error(reason) then error_type, condition, reason_text = reason.type, reason.condition, reason.text; elseif type(reason) == "string" then reason_text = reason; end - for i, stanza in ipairs(sendq) do + for stanza in sendq:consume() do 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, @@ -170,7 +171,6 @@ local function bounce_sendq(session, reason) else (session.log or log)("debug", "Not eligible for bouncing, discarding %s", stanza:top_tag()); end - sendq[i] = nil; end session.sendq = nil; end @@ -194,11 +194,14 @@ 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 - if host.sendq then - t_insert(host.sendq, st.clone(stanza)); - else + if not host.sendq then -- luacheck: ignore 122 - host.sendq = { st.clone(stanza) }; + host.sendq = queue.new(sendq_size); + end + if not host.sendq:push(st.clone(stanza)) then + host.log("warn", "stanza [%s] not queued ", stanza.name); + event.origin.send(st.error_reply(stanza, "wait", "resource-constraint", "Outgoing stanza queue full")); + return true; end host.log("debug", "stanza [%s] queued ", stanza.name); return true; @@ -223,7 +226,8 @@ function route_to_new_session(event) -- Store in buffer host_session.bounce_sendq = bounce_sendq; - host_session.sendq = { st.clone(stanza) }; + host_session.sendq = queue.new(sendq_size); + host_session.sendq:push(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 @@ -362,11 +366,11 @@ function mark_connected(session) if session.direction == "outgoing" then if sendq then - session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host); + session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", sendq.count(), session.to_host); local send = session.sends2s; - for i, stanza in ipairs(sendq) do + for stanza in sendq:consume() do + -- TODO check send success send(stanza); - sendq[i] = nil; end session.sendq = nil; end diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index d4f0f371..7a9a67b3 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -541,11 +541,13 @@ module:hook("pre-resource-unbind", function (event) return end - session.log("debug", "Destroying session for hibernating too long"); - save_old_session(session); - session.resumption_token = nil; - sessionmanager.destroy_session(session, "Hibernating too long"); - sessions_expired(1); + prosody.main_thread:run(function () + session.log("debug", "Destroying session for hibernating too long"); + save_old_session(session); + session.resumption_token = nil; + sessionmanager.destroy_session(session, "Hibernating too long"); + sessions_expired(1); + end); end); if session.conn then local conn = session.conn; diff --git a/plugins/muc/hats.lib.lua b/plugins/muc/hats.lib.lua index 492dc72c..7eb71eb4 100644 --- a/plugins/muc/hats.lib.lua +++ b/plugins/muc/hats.lib.lua @@ -25,7 +25,7 @@ module:hook("muc-build-occupant-presence", function (event) hats_el:tag("hat", { uri = hat_id, title = hat_data.title }):up(); if hats_compat then - if not hats_el then + if not legacy_hats_el then legacy_hats_el = st.stanza("hats", { xmlns = xmlns_hats_legacy }); end legacy_hats_el:tag("hat", { uri = hat_id, title = hat_data.title }):up(); diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index b8f276cf..359afc87 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -304,10 +304,10 @@ function room_mt:publicise_occupant_status(occupant, x, nick, actor, reason, pre -- General populace for occupant_nick, n_occupant in self:each_occupant() do if occupant_nick ~= occupant.nick then - local pr = get_p(n_occupant); if broadcast_roles[occupant.role or "none"] or force_unavailable then - self:route_to_occupant(n_occupant, pr); + self:route_to_occupant(n_occupant, get_p(n_occupant)); elseif prev_role and broadcast_roles[prev_role] then + local pr = get_p(n_occupant); pr.attr.type = 'unavailable'; self:route_to_occupant(n_occupant, pr); end @@ -339,16 +339,14 @@ function room_mt:send_occupant_list(to, filter) local broadcast_bare_jids = {}; -- Track which bare JIDs we have sent presence for for occupant_jid, occupant in self:each_occupant() do broadcast_bare_jids[occupant.bare_jid] = true; - if filter == nil or filter(occupant_jid, occupant) then + if (filter == nil or filter(occupant_jid, occupant)) and (to_bare == occupant.bare_jid or broadcast_roles[occupant.role or "none"]) then local x = st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'}); self:build_item_list(occupant, x, is_anonymous and to_bare ~= occupant.bare_jid); -- can always see your own jids local pres = st.clone(occupant:get_presence()); pres.attr.to = to; pres:add_child(x); module:fire_event("muc-build-occupant-presence", { room = self, occupant = occupant, stanza = pres }); - if to_bare == occupant.bare_jid or broadcast_roles[occupant.role or "none"] then - self:route_stanza(pres); - end + self:route_stanza(pres); end end if broadcast_roles.none then diff --git a/spec/scansion/bookmarks2.scs b/spec/scansion/bookmarks2.scs new file mode 100644 index 00000000..0243ca54 --- /dev/null +++ b/spec/scansion/bookmarks2.scs @@ -0,0 +1,181 @@ +# Pubsub: Bookmarks 2.0 + +[Client] Juliet + jid: admin@localhost + password: password + +// admin@localhost is assumed to have node creation privileges + +--------- + +Juliet connects + +-- Generated with https://gitlab.com/xmpp-rs/xmpp-parsers: +-- cargo run --example=generate-caps https://code.matthewwild.co.uk/scansion/ <<< "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' name='scansion' type='bot'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='urn:xmpp:bookmarks:1+notify'/></query>" +Juliet sends: + <presence id='presence0'> + <c xmlns='http://jabber.org/protocol/caps' + hash='sha-1' + node='https://code.matthewwild.co.uk/scansion/' + ver='CPuQARM1gCTq2f6/ZjHUzWL2QHg='/> + <c xmlns='urn:xmpp:caps'> + <hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>OTy9GPCvBZRvqzOHmD/ThA1WbBH3tNoeKbdqKQCRPHc=</hash> + <hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>f/rxDeTf6HyjQ382V3GEG/UfAs5IeclC05jBSBnVQCI=</hash> + <hash xmlns='urn:xmpp:hashes:2' algo='blake2b-256'>ucfqg/NrLj0omE+26hYMrbpcmxHcU4Z3hfAQIF+6tt0=</hash> + </c> + </presence> + +Juliet receives: + <iq from="${Juliet's JID}" id='disco' type='get'> + <query xmlns='http://jabber.org/protocol/disco#info' node='https://code.matthewwild.co.uk/scansion/#CPuQARM1gCTq2f6/ZjHUzWL2QHg='/> + </iq> + +Juliet sends: + <iq to="${Juliet's JID}" id='disco' type='result'> + <query xmlns='http://jabber.org/protocol/disco#info' node='https://code.matthewwild.co.uk/scansion/#CPuQARM1gCTq2f6/ZjHUzWL2QHg='> + <identity category='client' name='scansion' type='bot'/> + <feature var='http://jabber.org/protocol/disco#info'/> + <feature var='urn:xmpp:bookmarks:1+notify'/> + </query> + </iq> + +Juliet sends: + <iq type='set' id='pub0'> + <pubsub xmlns='http://jabber.org/protocol/pubsub'> + <publish node='urn:xmpp:bookmarks:1'> + <item id='theplay@conference.shakespeare.lit'> + <conference xmlns='urn:xmpp:bookmarks:1' + name='The Play's the Thing' + autojoin='true'> + <nick>JC</nick> + </conference> + </item> + </publish> + <publish-options> + <x xmlns='jabber:x:data' type='submit'> + <field var='FORM_TYPE' type='hidden'> + <value>http://jabber.org/protocol/pubsub#publish-options</value> + </field> + <field var='pubsub#persist_items'> + <value>true</value> + </field> + <field var='pubsub#max_items'> + <value>255</value> + </field> + <field var='pubsub#send_last_published_item'> + <value>never</value> + </field> + <field var='pubsub#access_model'> + <value>whitelist</value> + </field> + </x> + </publish-options> + </pubsub> + </iq> + +Juliet receives: + <message type='headline' from="${Juliet's JID}"> + <event xmlns='http://jabber.org/protocol/pubsub#event'> + <items node='urn:xmpp:bookmarks:1'> + <item id='theplay@conference.shakespeare.lit'> + <conference xmlns='urn:xmpp:bookmarks:1' + name='The Play's the Thing' + autojoin='true'> + <nick>JC</nick> + </conference> + </item> + </items> + </event> + </message> + +Juliet receives: + <iq type='result' id='pub0'> + <pubsub xmlns='http://jabber.org/protocol/pubsub'> + <publish node='urn:xmpp:bookmarks:1'> + <item id='theplay@conference.shakespeare.lit'/> + </publish> + </pubsub> + </iq> + +Juliet sends: + <iq type='set' id='pub1'> + <pubsub xmlns='http://jabber.org/protocol/pubsub'> + <publish node='urn:xmpp:bookmarks:1'> + <item id='orchard@conference.shakespeare.lit'> + <conference xmlns='urn:xmpp:bookmarks:1' + name='The Orchard' + autojoin='true'> + <nick>JC</nick> + </conference> + </item> + </publish> + <publish-options> + <x xmlns='jabber:x:data' type='submit'> + <field var='FORM_TYPE' type='hidden'> + <value>http://jabber.org/protocol/pubsub#publish-options</value> + </field> + <field var='pubsub#persist_items'> + <value>true</value> + </field> + <field var='pubsub#max_items'> + <value>255</value> + </field> + <field var='pubsub#send_last_published_item'> + <value>never</value> + </field> + <field var='pubsub#access_model'> + <value>whitelist</value> + </field> + </x> + </publish-options> + </pubsub> + </iq> + +Juliet receives: + <message type='headline' from="${Juliet's JID}"> + <event xmlns='http://jabber.org/protocol/pubsub#event'> + <items node='urn:xmpp:bookmarks:1'> + <item id='orchard@conference.shakespeare.lit'> + <conference xmlns='urn:xmpp:bookmarks:1' + name='The Orchard' + autojoin='true'> + <nick>JC</nick> + </conference> + </item> + </items> + </event> + </message> + +Juliet receives: + <iq type='result' id='pub1'> + <pubsub xmlns='http://jabber.org/protocol/pubsub'> + <publish node='urn:xmpp:bookmarks:1'> + <item id='orchard@conference.shakespeare.lit'/> + </publish> + </pubsub> + </iq> + +Juliet sends: + <iq type='set' id='retract0'> + <pubsub xmlns='http://jabber.org/protocol/pubsub'> + <retract node='urn:xmpp:bookmarks:1' notify='1'> + <item id='theplay@conference.shakespeare.lit'/> + </retract> + </pubsub> + </iq> + +Juliet receives: + <message type='headline' from="${Juliet's JID}"> + <event xmlns='http://jabber.org/protocol/pubsub#event'> + <items node='urn:xmpp:bookmarks:1'> + <retract id='theplay@conference.shakespeare.lit'/> + </items> + </event> + </message> + +Juliet receives: + <iq type='result' id='retract0'/> + +Juliet disconnects + +// vim: syntax=xml: diff --git a/spec/util_crypto_spec.lua b/spec/util_crypto_spec.lua index 77d046ac..4a62e0bc 100644 --- a/spec/util_crypto_spec.lua +++ b/spec/util_crypto_spec.lua @@ -3,6 +3,7 @@ local test_keys = require "spec.inputs.test_keys"; describe("util.crypto", function () local crypto = require "util.crypto"; local random = require "util.random"; + local encodings = require "util.encodings"; describe("generate_ed25519_keypair", function () local keypair = crypto.generate_ed25519_keypair(); @@ -10,6 +11,26 @@ describe("util.crypto", function () assert.equal("ED25519", keypair:get_type()); end) + describe("generate_p256_keypair", function () + local keypair = crypto.generate_p256_keypair(); + assert.is_not_nil(keypair); + assert.equal("id-ecPublicKey", keypair:get_type()); + end) + + describe("export/import raw", function () + local keypair = crypto.generate_p256_keypair(); + assert.is_not_nil(keypair); + local raw = keypair:public_raw() + local imported = crypto.import_public_ec_raw(raw, "P-256") + assert.equal(keypair:public_pem(), imported:public_pem()); + end) + + describe("derive", function () + local key = crypto.import_private_pem(test_keys.ecdsa_private_pem); + local peer_key = crypto.import_public_pem(test_keys.ecdsa_public_pem); + assert.equal("n1v4KeKmOVwjC67fiKtjJnqcEaasbpZa2fLPNHW51co=", encodings.base64.encode(key:derive(peer_key))) + end) + describe("import_private_pem", function () it("can import ECDSA keys", function () local ecdsa_key = crypto.import_private_pem(test_keys.ecdsa_private_pem); diff --git a/spec/util_error_spec.lua b/spec/util_error_spec.lua index ebe8bff7..cd30ee94 100644 --- a/spec/util_error_spec.lua +++ b/spec/util_error_spec.lua @@ -29,10 +29,10 @@ describe("util.error", function () end); - describe("is_err()", function () + describe("is_error()", function () it("works", function () - assert.truthy(errors.is_err(errors.new())); - assert.falsy(errors.is_err("not an error")); + assert.truthy(errors.is_error(errors.new())); + assert.falsy(errors.is_error("not an error")); end); end); @@ -40,7 +40,7 @@ describe("util.error", function () it("works", function () local ok, err = errors.coerce(nil, "it dun goofed"); assert.is_nil(ok); - assert.truthy(errors.is_err(err)) + assert.truthy(errors.is_error(err)) end); end); @@ -50,7 +50,7 @@ describe("util.error", function () local m = st.message({ type = "chat" }); local e = st.error_reply(m, "modify", "bad-request", nil, "error.example"):tag("extra", { xmlns = "xmpp:example.test" }); local err = errors.from_stanza(e); - assert.truthy(errors.is_err(err)); + assert.truthy(errors.is_error(err)); assert.equal("modify", err.type); assert.equal("bad-request", err.condition); assert.equal(e, err.context.stanza); @@ -187,7 +187,7 @@ describe("util.error", function () end local ok, err = reg.coerce(test()); assert.is_nil(ok); - assert.is_truthy(errors.is_err(err)); + assert.is_truthy(errors.is_error(err)); assert.equal("forbidden", err.condition); end); @@ -208,7 +208,7 @@ describe("util.error", function () end local ok, err = reg.coerce(test()); assert.is_nil(ok); - assert.is_truthy(errors.is_err(err)); + assert.is_truthy(errors.is_error(err)); assert.equal("internal-server-error", err.condition); assert.equal("Oh no", err.text); end); diff --git a/spec/util_pubsub_spec.lua b/spec/util_pubsub_spec.lua index 45a612a0..a03ffa64 100644 --- a/spec/util_pubsub_spec.lua +++ b/spec/util_pubsub_spec.lua @@ -108,7 +108,7 @@ describe("util.pubsub", function () it("fails to publish to a node with differing config", function () local ok, err = service:publish("node", true, "1", "item 2", { myoption = false }); assert.falsy(ok); - assert.equals("precondition-not-met", err.pubsub_condition); + assert.equals("precondition-not-met", err); end); it("allows to publish to a node with differing config when only defaults are suggested", function () @@ -605,4 +605,14 @@ describe("util.pubsub", function () end); end) + + describe("metadata", function() + it("works", function() + local service = pubsub.new { metadata_subset = { "title" } }; + assert.truthy(service:create("node", true, { title = "Hello", secret = "hidden" })) + local ok, meta = service:get_node_metadata("node", "nobody"); + assert.truthy(ok, meta); + assert.same({ title = "Hello" }, meta); + end) + end); end); diff --git a/spec/util_queue_spec.lua b/spec/util_queue_spec.lua index d73f523d..d9e92e3d 100644 --- a/spec/util_queue_spec.lua +++ b/spec/util_queue_spec.lua @@ -137,4 +137,23 @@ describe("util.queue", function() assert.equal(c, 6); end); end); + describe("replace()", function () + it("should work", function () + local q = queue.new(10); + for i = 1, 5 do + q:push(i); + end + q:replace(6); + local c = 0; + for i in q:consume() do + c = c + 1; + if c > 1 then + assert.is_equal(c, i); + elseif c == 1 then + assert.is_equal(6, i); + end + end + assert.is_equal(5, c); + end); + end); end); diff --git a/teal-src/prosody/util/xtemplate.tl b/teal-src/prosody/util/xtemplate.tl index 84003051..4d293359 100644 --- a/teal-src/prosody/util/xtemplate.tl +++ b/teal-src/prosody/util/xtemplate.tl @@ -17,7 +17,7 @@ local t_concat = table.concat; local st = require "prosody.util.stanza"; local type escape_t = function (string) : string -local type filter_t = function (string, string | st.stanza_t, string) : string | st.stanza_t, boolean +local type filter_t = function (string | st.stanza_t, string | st.stanza_t, string) : string | st.stanza_t, boolean local type filter_coll = { string : filter_t } local function render(template : string, root : st.stanza_t, escape : escape_t, filters : filter_coll) : string @@ -85,11 +85,7 @@ local function render(template : string, root : st.stanza_t, escape : escape_t, end elseif filters and filters[func] then local f = filters[func]; - if args == nil then - value, is_escaped = f(value, tmpl); - else - value, is_escaped = f(args, value, tmpl); - end + value, is_escaped = f(value, args, tmpl); else error("No such filter function: " .. func); end diff --git a/util-src/crypto.c b/util-src/crypto.c index 1e69599d..68733c7c 100644 --- a/util-src/crypto.c +++ b/util-src/crypto.c @@ -27,6 +27,7 @@ typedef unsigned __int32 uint32_t; #include <openssl/err.h> #include <openssl/evp.h> #include <openssl/obj_mac.h> +#include <openssl/param_build.h> #include <openssl/pem.h> #if (LUA_VERSION_NUM == 501) @@ -92,6 +93,40 @@ static int Lpkey_meth_get_type(lua_State *L) { return 1; } +static int Lpkey_meth_derive(lua_State *L) { + size_t outlen; + EVP_PKEY *key = pkey_from_arg(L, 1, 0, 0); + EVP_PKEY *peer = pkey_from_arg(L, 2, 0, 0); + EVP_PKEY_CTX *ctx; + BUF_MEM *buf; + BIO *bio = new_managed_BIO_s_mem(L); + BIO_get_mem_ptr(bio, &buf); + if (!(ctx = EVP_PKEY_CTX_new(key, NULL))) + goto sslerr; + if (EVP_PKEY_derive_init(ctx) <= 0) + goto sslerr; + if (EVP_PKEY_derive_set_peer(ctx, peer) <= 0) + goto sslerr; + if (EVP_PKEY_derive(ctx, NULL, &outlen) <= 0) + goto sslerr; + if (!BUF_MEM_grow_clean(buf, outlen)) + goto sslerr; + if (EVP_PKEY_derive(ctx, (unsigned char*)buf->data, &outlen) <= 0) + goto sslerr; + EVP_PKEY_CTX_free(ctx); + ctx = NULL; + lua_pushlstring(L, buf->data, outlen); + BIO_reset(bio); + return 1; +sslerr: + if (ctx) { + EVP_PKEY_CTX_free(ctx); + ctx = NULL; + } + BIO_reset(bio); + return luaL_error(L, "pkey:derive failed"); +} + 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!=NID_rsassaPss)?key_type:NID_rsaEncryption, 1); luaL_Buffer sigbuf; @@ -163,6 +198,28 @@ cleanup: return 1; } +static int Lpkey_meth_public_raw(lua_State *L) { + OSSL_PARAM *params; + EVP_PKEY *pkey = pkey_from_arg(L, 1, 0, 0); + + if (EVP_PKEY_todata(pkey, EVP_PKEY_PUBLIC_KEY, ¶ms)) { + OSSL_PARAM *item = params; + while (item->key) { + if (!strcmp("pub", item->key)) { + lua_pushlstring(L, item->data, item->data_size); + break; + } + item++; + } + if (!item->key) lua_pushnil(L); + OSSL_PARAM_free(params); + } else { + lua_pushnil(L); + } + + return 1; +} + static int Lpkey_meth_public_pem(lua_State *L) { char *data; size_t bytes; @@ -237,6 +294,25 @@ static int Lgenerate_ed25519_keypair(lua_State *L) { return 1; } +static int Lgenerate_p256_keypair(lua_State *L) { + EVP_PKEY *pkey = NULL; + EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL); + + /* Generate key */ + if (EVP_PKEY_keygen_init(pctx) <= 0) goto err; + if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, NID_X9_62_prime256v1) <= 0) goto err; + if (EVP_PKEY_keygen(pctx, &pkey) <= 0) goto err; + EVP_PKEY_CTX_free(pctx); + + push_pkey(L, pkey, NID_X9_62_prime256v1, 1); + return 1; + +err: + if (pctx) EVP_PKEY_CTX_free(pctx); + lua_pushnil(L); + return 1; +} + static int Limport_private_pem(lua_State *L) { EVP_PKEY *pkey = NULL; @@ -257,6 +333,42 @@ static int Limport_private_pem(lua_State *L) { return 1; } +static int Limport_public_ec_raw(lua_State *L) { + OSSL_PARAM_BLD *param_bld = NULL; + OSSL_PARAM *params = NULL; + EVP_PKEY_CTX *ctx = NULL; + EVP_PKEY *pkey = NULL; + + size_t pubkey_bytes; + const char* pubkey_data = luaL_checklstring(L, 1, &pubkey_bytes); + const char* curve = luaL_checkstring(L, 2); + + param_bld = OSSL_PARAM_BLD_new(); + if (!param_bld) goto err; + if (!OSSL_PARAM_BLD_push_utf8_string(param_bld, "group", curve, 0)) goto err; + if (!OSSL_PARAM_BLD_push_octet_string(param_bld, "pub", pubkey_data, pubkey_bytes)) goto err; + params = OSSL_PARAM_BLD_to_param(param_bld); + if (!params) goto err; + ctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL); + if (!ctx) goto err; + if (!EVP_PKEY_fromdata_init(ctx)) goto err; + if (EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_PUBLIC_KEY, params) <= 0) goto err; + + push_pkey(L, pkey, EVP_PKEY_id(pkey), 0); + + EVP_PKEY_CTX_free(ctx); + OSSL_PARAM_free(params); + OSSL_PARAM_BLD_free(param_bld); + + return 1; +err: + if (ctx) EVP_PKEY_CTX_free(ctx); + if (params) OSSL_PARAM_free(params); + if (param_bld) OSSL_PARAM_BLD_free(param_bld); + lua_pushnil(L); + return 1; +} + static int Limport_public_pem(lua_State *L) { EVP_PKEY *pkey = NULL; @@ -571,9 +683,11 @@ static const luaL_Reg Reg[] = { { "aes_256_ctr_decrypt", Laes_256_ctr_decrypt }, { "generate_ed25519_keypair", Lgenerate_ed25519_keypair }, + { "generate_p256_keypair", Lgenerate_p256_keypair }, { "import_private_pem", Limport_private_pem }, { "import_public_pem", Limport_public_pem }, + { "import_public_ec_raw", Limport_public_ec_raw }, { "parse_ecdsa_signature", Lparse_ecdsa_signature }, { "build_ecdsa_signature", Lbuild_ecdsa_signature }, @@ -583,7 +697,9 @@ static const luaL_Reg Reg[] = { static const luaL_Reg KeyMethods[] = { { "private_pem", Lpkey_meth_private_pem }, { "public_pem", Lpkey_meth_public_pem }, + { "public_raw", Lpkey_meth_public_raw }, { "get_type", Lpkey_meth_get_type }, + { "derive", Lpkey_meth_derive }, { NULL, NULL } }; diff --git a/util/dnsregistry.lua b/util/dnsregistry.lua index c52abee9..b65debe0 100644 --- a/util/dnsregistry.lua +++ b/util/dnsregistry.lua @@ -1,5 +1,5 @@ -- Source: https://www.iana.org/assignments/dns-parameters/dns-parameters.xml --- Generated on 2023-01-20 +-- Generated on 2024-10-26 return { classes = { ["IN"] = 1; [1] = "IN"; @@ -79,6 +79,7 @@ return { ["LP"] = 107; [107] = "LP"; ["EUI48"] = 108; [108] = "EUI48"; ["EUI64"] = 109; [109] = "EUI64"; + ["NXNAME"] = 128; [128] = "NXNAME"; ["TKEY"] = 249; [249] = "TKEY"; ["TSIG"] = 250; [250] = "TSIG"; ["IXFR"] = 251; [251] = "IXFR"; @@ -91,6 +92,10 @@ return { ["AVC"] = 258; [258] = "AVC"; ["DOA"] = 259; [259] = "DOA"; ["AMTRELAY"] = 260; [260] = "AMTRELAY"; + ["RESINFO"] = 261; [261] = "RESINFO"; + ["WALLET"] = 262; [262] = "WALLET"; + ["CLA"] = 263; [263] = "CLA"; + ["IPN"] = 264; [264] = "IPN"; ["TA"] = 32768; [32768] = "TA"; ["DLV"] = 32769; [32769] = "DLV"; }; diff --git a/util/pubsub.lua b/util/pubsub.lua index ccde8b53..d6779736 100644 --- a/util/pubsub.lua +++ b/util/pubsub.lua @@ -1,6 +1,5 @@ local events = require "prosody.util.events"; local cache = require "prosody.util.cache"; -local errors = require "prosody.util.error"; local service_mt = {}; @@ -12,6 +11,7 @@ local default_config = { itemcheck = function () return true; end; get_affiliation = function () end; normalize_jid = function (jid) return jid; end; + metadata_subset = {}; capabilities = { outcast = { create = false; @@ -46,6 +46,7 @@ local default_config = { get_subscription = true; get_subscriptions = true; get_items = false; + get_metadata = true; subscribe_other = false; unsubscribe_other = false; @@ -68,6 +69,7 @@ local default_config = { get_subscription = true; get_subscriptions = true; get_items = true; + get_metadata = true; subscribe_other = false; unsubscribe_other = false; @@ -91,6 +93,7 @@ local default_config = { get_subscription = true; get_subscriptions = true; get_items = true; + get_metadata = true; subscribe_other = false; unsubscribe_other = false; @@ -116,6 +119,7 @@ local default_config = { get_subscription = true; get_subscriptions = true; get_items = true; + get_metadata = true; subscribe_other = true; @@ -562,11 +566,7 @@ function service:publish(node, actor, id, item, requested_config) --> ok, err -- Check that node has the requested config before we publish local ok, field = check_preconditions(node_obj.config, requested_config); if not ok then - local err = errors.new({ - type = "cancel", condition = "conflict", text = "Field does not match: "..field; - }); - err.pubsub_condition = "precondition-not-met"; - return false, err; + return false, "precondition-not-met", { field = field }; end end if not self.config.itemcheck(item) then @@ -877,6 +877,20 @@ function service:get_node_config(node, actor) --> (true, config) or (false, err) return true, config_table; end +function service:get_node_metadata(node, actor) + if not self:may(node, actor, "get_metadata") then + return false, "forbidden"; + end + + local ok, config = self:get_node_config(node, true); + if not ok then return ok, config; end + local meta = {}; + for _, k in ipairs(self.config.metadata_subset) do + meta[k] = config[k]; + end + return true, meta; +end + return { new = new; }; diff --git a/util/xtemplate.lua b/util/xtemplate.lua index e23b1a01..446d7d1f 100644 --- a/util/xtemplate.lua +++ b/util/xtemplate.lua @@ -70,11 +70,7 @@ local function render(template, root, escape, filters) end elseif filters and filters[func] then local f = filters[func]; - if args == nil then - value, is_escaped = f(value, tmpl); - else - value, is_escaped = f(args, value, tmpl); - end + value, is_escaped = f(value, args, tmpl); else error("No such filter function: " .. func); end |