diff options
53 files changed, 1062 insertions, 538 deletions
diff --git a/.luacheckrc b/.luacheckrc index 6eb4c526..90e18ce5 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -161,7 +161,6 @@ if os.getenv("PROSODY_STRICT_LINT") ~= "1" then "spec/util_http_spec.lua"; "spec/util_ip_spec.lua"; "spec/util_multitable_spec.lua"; - "spec/util_rfc6724_spec.lua"; "spec/util_throttle_spec.lua"; "tools/ejabberd2prosody.lua"; @@ -24,6 +24,7 @@ TRUNK - Public rooms can only be created by local users (parent host) by default - muc_room_allow_public = false restricts to admins - Commands to show occupants and affiliations in the Shell +- Save 'reason' text supplied with affiliation change ### Security and authentication @@ -65,11 +66,14 @@ TRUNK - Intervals of mod_cron managed periodic jobs made configurable - When mod_smacks is enabled, s2s connections not responding to ack requests are closed. - Arguments to `prosodyctl shell` that start with ':' are now turned into method calls +- Support for Type=notify and notify-reload systemd service type added +- Support for the roster *group* access_model in mod_pep ## Removed - Lua 5.1 support - XEP-0090 support removed from mod_time +- util.rfc6724 0.12.0 ====== diff --git a/core/features.lua b/core/features.lua index db1bc986..99edde51 100644 --- a/core/features.lua +++ b/core/features.lua @@ -4,6 +4,8 @@ return { available = set.new{ -- mod_bookmarks bundled "mod_bookmarks"; + -- mod_server_info bundled + "mod_server_info"; -- Roles, module.may and per-session authz "permissions"; -- prosody.* namespace @@ -21,5 +23,11 @@ return { "getopt-interval"; "getopt-period"; "getopt-integer"; + + -- new module.ready() + "module-ready"; + + -- SIGUSR1 and 2 events + "signal-events"; }; }; diff --git a/doc/doap.xml b/doc/doap.xml index 79ef9d68..62e4063c 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -67,6 +67,7 @@ <implements rdf:resource="https://datatracker.ietf.org/doc/draft-cridland-xmpp-session/"> <!-- since=0.6.0 note=Added in hg:0bbbc9042361 --> </implements> + <implements rdf:resource="https://datatracker.ietf.org/doc/draft-ietf-dance-client-auth"/> <implements rdf:resource="http://www.unicode.org/reports/tr39/"/> <implements> <xmpp:SupportedXep> @@ -698,8 +699,8 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0317.html"/> - <xmpp:version>0.1</xmpp:version> - <xmpp:status>planned</xmpp:status> + <xmpp:version>0.2.0</xmpp:version> + <xmpp:status>complete</xmpp:status> <xmpp:since>0.12.0</xmpp:since> <xmpp:note>muc/hats</xmpp:note> </xmpp:SupportedXep> diff --git a/net/http/files.lua b/net/http/files.lua index 24d9f204..8ef054e2 100644 --- a/net/http/files.lua +++ b/net/http/files.lua @@ -58,7 +58,7 @@ local function serve(opts) local cache = new_cache(opts.cache_size or 256); local cache_max_file_size = tonumber(opts.cache_max_file_size) or 1024 -- luacheck: ignore 431 - local base_path = opts.path; + local base_path = assert(opts.path, "invalid argument to net.http.files.path(), missing required 'path'"); local dir_indices = opts.index_files or { "index.html", "index.htm" }; local directory_index = opts.directory_index; local function serve_file(event, path) diff --git a/net/server_epoll.lua b/net/server_epoll.lua index fe60dc78..c946a751 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -29,6 +29,7 @@ local new_id = require "prosody.util.id".short; local xpcall = require "prosody.util.xpcall".xpcall; local sslconfig = require "prosody.util.sslconfig"; local tls_impl = require "prosody.net.tls_luasec"; +local have_signal, signal = pcall(require, "prosody.util.signal"); local poller = require "prosody.util.poll" local EEXIST = poller.EEXIST; @@ -630,30 +631,35 @@ end function interface:ssl_info() local sock = self.conn; + if not sock then return nil, "not-connected" end if not sock.info then return nil, "not-implemented"; end return sock:info(); end function interface:ssl_peercertificate() local sock = self.conn; + if not sock then return nil, "not-connected" end if not sock.getpeercertificate then return nil, "not-implemented"; end return sock:getpeercertificate(); end function interface:ssl_peerverification() local sock = self.conn; + if not sock then return nil, "not-connected" end if not sock.getpeerverification then return nil, { { "Chain verification not supported" } }; end return sock:getpeerverification(); end function interface:ssl_peerfinished() local sock = self.conn; + if not sock then return nil, "not-connected" end if not sock.getpeerfinished then return nil, "not-implemented"; end return sock:getpeerfinished(); end function interface:ssl_exportkeyingmaterial(label, len, context) local sock = self.conn; + if not sock then return nil, "not-connected" end if sock.exportkeyingmaterial then return sock:exportkeyingmaterial(label, len, context); end @@ -1138,6 +1144,26 @@ local function loop(once) return quitting; end +local hook_signal; +if have_signal and signal.signalfd then + local function dispatch(self) + return self:on("signal", self.conn:read()); + end + + function hook_signal(signum, cb) + local sigfd = signal.signalfd(signum); + if not sigfd then + log("error", "Could not hook signal %d", signum); + return nil, "failed"; + end + local watch = watchfd(sigfd, dispatch); + watch.listeners = { onsignal = cb }; + watch.close = nil; -- revert to default + watch:noise("Signal handler %d ready", signum); + return watch; + end +end + return { get_backend = function () return "epoll"; end; addserver = addserver; @@ -1163,6 +1189,7 @@ return { set_config = function (newconfig) cfg = setmetatable(newconfig, default_config); end; + hook_signal = hook_signal; tls_builder = function(basedir) return sslconfig._new(tls_impl.new_context, basedir) diff --git a/net/unbound.lua b/net/unbound.lua index c3ccb9ea..176a6156 100644 --- a/net/unbound.lua +++ b/net/unbound.lua @@ -80,8 +80,12 @@ local answer_mt = { h = h .. s_format(", Bogus: %s", self.bogus); end local t = { h }; + local qname = self.canonname or self.qname; + if self.canonname then + table.insert(t, self.qname .. "\t" .. classes[self.qclass] .. "\tCNAME\t" .. self.canonname); + end for i = 1, #self do - t[i+1]=self.qname.."\t"..classes[self.qclass].."\t"..types[self.qtype].."\t"..tostring(self[i]); + table.insert(t, qname .. "\t" .. classes[self.qclass] .. "\t" .. types[self.qtype] .. "\t" .. tostring(self[i])); end local _string = t_concat(t, "\n"); self._string = _string; diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 45a891f4..d085ce43 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -867,6 +867,18 @@ available_columns = { end end; }; + created = { + title = "Connection Created"; + description = "Time when connection was created"; + width = #"YYYY MM DD HH:MM:SS"; + align = "right"; + key = "conn"; + mapper = function(conn) + if conn then + return os.date("%F %T", math.floor(conn.created)); + end + end; + }; dir = { title = "Dir"; description = "Direction of server-to-server connection"; diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua index ba62c11b..f54d2db9 100644 --- a/plugins/mod_announce.lua +++ b/plugins/mod_announce.lua @@ -6,11 +6,15 @@ -- COPYING file in the source package for more information. -- -local st, jid = require "prosody.util.stanza", require "prosody.util.jid"; +local usermanager = require "prosody.core.usermanager"; +local id = require "prosody.util.id"; +local jid = require "prosody.util.jid"; +local st = require "prosody.util.stanza"; local hosts = prosody.hosts; function send_to_online(message, host) + host = host or module.host; local sessions; if host then sessions = { [host] = hosts[host] }; @@ -33,6 +37,28 @@ function send_to_online(message, host) return c; end +function send_to_all(message, host) + host = host or module.host; + local c = 0; + for username in usermanager.users(host) do + message.attr.to = username.."@"..host; + module:send(st.clone(message)); + c = c + 1; + end + return c; +end + +function send_to_role(message, role, host) + host = host or module.host; + local c = 0; + for _, recipient_jid in ipairs(usermanager.get_jids_with_role(role, host)) do + message.attr.to = recipient_jid; + module:send(st.clone(message)); + c = c + 1; + end + return c; +end + module:default_permission("prosody:admin", ":send-announcement"); -- Old <message>-based jabberd-style announcement sending @@ -82,8 +108,10 @@ function announce_handler(_, data, state) local fields = announce_layout:data(data.form); module:log("info", "Sending server announcement to all online users"); - local message = st.message({type = "headline"}, fields.announcement):up() - :tag("subject"):text(fields.subject or "Announcement"); + local message = st.message({type = "headline"}, fields.announcement):up(); + if fields.subject and fields.subject ~= "" then + message:text_tag("subject", fields.subject); + end local count = send_to_online(message, data.to); @@ -99,3 +127,57 @@ local adhoc_new = module:require "adhoc".new; local announce_desc = adhoc_new("Send Announcement to Online Users", "http://jabber.org/protocol/admin#announce", announce_handler, "admin"); module:provides("adhoc", announce_desc); +module:add_item("shell-command", { + section = "announce"; + section_desc = "Broadcast announcements to users"; + name = "all"; + desc = "Send announcement to all users on the host"; + args = { + { name = "host", type = "string" }; + { name = "text", type = "string" }; + }; + host_selector = "host"; + handler = function(self, host, text) --luacheck: ignore 212/self + local msg = st.message({ from = host, id = id.short() }) + :text_tag("body", text); + local count = send_to_all(msg, host); + return true, ("Announcement sent to %d users"):format(count); + end; +}); + +module:add_item("shell-command", { + section = "announce"; + section_desc = "Broadcast announcements to users"; + name = "online"; + desc = "Send announcement to all online users on the host"; + args = { + { name = "host", type = "string" }; + { name = "text", type = "string" }; + }; + host_selector = "host"; + handler = function(self, host, text) --luacheck: ignore 212/self + local msg = st.message({ from = host, id = id.short(), type = "headline" }) + :text_tag("body", text); + local count = send_to_online(msg, host); + return true, ("Announcement sent to %d users"):format(count); + end; +}); + +module:add_item("shell-command", { + section = "announce"; + section_desc = "Broadcast announcements to users"; + name = "role"; + desc = "Send announcement to users with a specific role on the host"; + args = { + { name = "host", type = "string" }; + { name = "role", type = "string" }; + { name = "text", type = "string" }; + }; + host_selector = "host"; + handler = function(self, host, role, text) --luacheck: ignore 212/self + local msg = st.message({ from = host, id = id.short() }) + :text_tag("body", text); + local count = send_to_role(msg, role, host); + return true, ("Announcement sent to %d users"):format(count); + end; +}); diff --git a/plugins/mod_blocklist.lua b/plugins/mod_blocklist.lua index 3a621753..6587c8b1 100644 --- a/plugins/mod_blocklist.lua +++ b/plugins/mod_blocklist.lua @@ -262,7 +262,20 @@ local function drop_stanza(event) local to, from = attr.to, attr.from; to = to and jid_split(to); if to and from then - return is_blocked(to, from); + if is_blocked(to, from) then + return true; + end + + -- Check mediated MUC inviter + if stanza.name == "message" then + local invite = stanza:find("{http://jabber.org/protocol/muc#user}x/invite"); + if invite then + from = jid_prep(invite.attr.from); + if is_blocked(to, from) then + return true; + end + end + end end end @@ -322,8 +335,13 @@ local prio_in, prio_out = 100, 100; module:hook("presence/bare", drop_stanza, prio_in); module:hook("presence/full", drop_stanza, prio_in); -module:hook("message/bare", bounce_message, prio_in); -module:hook("message/full", bounce_message, prio_in); +if module:get_option_boolean("bounce_blocked_messages", false) then + module:hook("message/bare", bounce_message, prio_in); + module:hook("message/full", bounce_message, prio_in); +else + module:hook("message/bare", drop_stanza, prio_in); + module:hook("message/full", drop_stanza, prio_in); +end module:hook("iq/bare", bounce_iq, prio_in); module:hook("iq/full", bounce_iq, prio_in); diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua index f4fec8f0..091a7d81 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -325,7 +325,7 @@ function stream_callbacks.streamopened(context, attr) sid = new_uuid(); -- TODO use util.session local session = { - type = "c2s_unauthed", conn = request.conn, sid = sid, host = attr.to, + base_type = "c2s", type = "c2s_unauthed", conn = request.conn, sid = sid, host = attr.to, rid = rid - 1, -- Hack for initial session setup, "previous" rid was $current_request - 1 bosh_version = attr.ver, bosh_wait = wait, streamid = sid, bosh_max_inactive = bosh_max_inactivity, bosh_responses = cache.new(BOSH_HOLD+1):table(); diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua index 4dabf34b..1a24c27c 100644 --- a/plugins/mod_c2s.lua +++ b/plugins/mod_c2s.lua @@ -252,6 +252,9 @@ end local function disconnect_user_sessions(reason, leave_resource) return function (event) local username, host, resource = event.username, event.host, event.resource; + if not (hosts[host] and hosts[host].type == "local") then + return -- not a local VirtualHost so no sessions + end local user = hosts[host].sessions[username]; if user and user.sessions then for r, session in pairs(user.sessions) do diff --git a/plugins/mod_cron.lua b/plugins/mod_cron.lua index 077dc80e..29c1aa93 100644 --- a/plugins/mod_cron.lua +++ b/plugins/mod_cron.lua @@ -2,6 +2,10 @@ module:set_global(); local async = require("prosody.util.async"); +local cron_initial_delay = module:get_option_number("cron_initial_delay", 1); +local cron_check_delay = module:get_option_number("cron_check_delay", 3600); +local cron_spread_factor = module:get_option_number("cron_spread_factor", 0); + local active_hosts = {} function module.add_host(host_module) @@ -46,15 +50,19 @@ local function run_task(task) task:save(started_at); end +local function spread(t, factor) + return t * (1 - factor + 2*factor*math.random()); +end + local task_runner = async.runner(run_task); -scheduled = module:add_timer(1, function() +scheduled = module:add_timer(cron_initial_delay, function() module:log("info", "Running periodic tasks"); - local delay = 3600; + local delay = spread(cron_check_delay, cron_spread_factor); for host in pairs(active_hosts) do module:log("debug", "Running periodic tasks for host %s", host); for _, task in ipairs(module:context(host):get_host_items("task")) do task_runner:run(task); end end - module:log("debug", "Wait %ds", delay); + module:log("debug", "Wait %gs", delay); return delay end); diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua index 540678e2..3517344d 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -173,6 +173,8 @@ module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function( 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}); + reply:tag("feature", { var = "http://jabber.org/protocol/disco#info" }):up(); + reply:tag("feature", { var = "http://jabber.org/protocol/disco#items" }):up(); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; local ret = module:fire_event("account-disco-info-node", node_event); @@ -193,6 +195,8 @@ module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function( else reply:tag('identity', {category='account', type='registered'}):up(); end + reply:tag("feature", { var = "http://jabber.org/protocol/disco#info" }):up(); + reply:tag("feature", { var = "http://jabber.org/protocol/disco#items" }):up(); module:fire_event("account-disco-info", { origin = origin, reply = reply }); origin.send(reply); return true; diff --git a/plugins/mod_http_file_share.lua b/plugins/mod_http_file_share.lua index 718b4d71..cfc647d4 100644 --- a/plugins/mod_http_file_share.lua +++ b/plugins/mod_http_file_share.lua @@ -452,7 +452,7 @@ function handle_download(event, path) -- GET /uploads/:slot+filename return response:send_file(handle); end -if expiry >= 0 and not external_base_url then +if expiry < math.huge and not external_base_url then -- TODO HTTP DELETE to the external endpoint? local array = require "prosody.util.array"; local async = require "prosody.util.async"; diff --git a/plugins/mod_invites.lua b/plugins/mod_invites.lua index 04265070..559170cc 100644 --- a/plugins/mod_invites.lua +++ b/plugins/mod_invites.lua @@ -4,6 +4,7 @@ local url = require "socket.url"; local jid_node = require "prosody.util.jid".node; local jid_split = require "prosody.util.jid".split; local argparse = require "prosody.util.argparse"; +local human_io = require "prosody.util.human.io"; local default_ttl = module:get_option_period("invite_expiry", "1 week"); @@ -248,26 +249,10 @@ function module.command(arg) end function subcommands.generate(arg) - - local sm = require "prosody.core.storagemanager"; - local mm = require "prosody.core.modulemanager"; - - local host = table.remove(arg, 1); -- pop host - assert(prosody.hosts[host], "Host "..tostring(host).." does not exist"); - sm.initialize_host(host); - module.host = host; --luacheck: ignore 122/module - token_storage = module:open_store("invite_token", "map"); - - local opts = argparse.parse(arg, { - short_params = { h = "help"; ["?"] = "help"; g = "group" }; - value_params = { group = true; reset = true; role = true }; - array_params = { group = true; role = true }; - }); - - - if opts.help then + local function help(short) print("usage: prosodyctl mod_" .. module.name .. " generate DOMAIN --reset USERNAME") print("usage: prosodyctl mod_" .. module.name .. " generate DOMAIN [--admin] [--role ROLE] [--group GROUPID]...") + if short then return 2 end print() print("This command has two modes: password reset and new account.") print("If --reset is given, the command operates in password reset mode and in new account mode otherwise.") @@ -283,6 +268,7 @@ function subcommands.generate(arg) print(" --role ROLE Grant the given ROLE to the new user") print(" --group GROUPID Add the user to the group with the given ID") print(" Can be specified multiple times") + print(" --expires-after T Time until the invite expires (e.g. '1 week')") print() print("--group can be specified multiple times; the user will be added to all groups.") print() @@ -290,6 +276,30 @@ function subcommands.generate(arg) return 2 end + local earlyopts = argparse.parse(arg, { short_params = { h = "help"; ["?"] = "help" } }); + if earlyopts.help or not earlyopts[1] then + return help(); + end + + local sm = require "prosody.core.storagemanager"; + local mm = require "prosody.core.modulemanager"; + + local host = table.remove(arg, 1); -- pop host + if not host then return help(true) end + sm.initialize_host(host); + module.host = host; --luacheck: ignore 122/module + token_storage = module:open_store("invite_token", "map"); + + local opts = argparse.parse(arg, { + short_params = { h = "help"; ["?"] = "help"; g = "group" }; + value_params = { group = true; reset = true; role = true }; + array_params = { group = true; role = true }; + }); + + if opts.help then + return help(); + end + -- Load mod_invites local invites = module:depends("invites"); -- Optional community module that if used, needs to be loaded here @@ -332,7 +342,7 @@ function subcommands.generate(arg) invite = assert(invites.create_account(nil, { roles = roles, groups = groups - })); + }, opts.expires_after and human_io.parse_duration(opts.expires_after))); end print(invite.landing_page or invite.uri); diff --git a/plugins/mod_invites_adhoc.lua b/plugins/mod_invites_adhoc.lua index 02e6a7dd..c9954d8c 100644 --- a/plugins/mod_invites_adhoc.lua +++ b/plugins/mod_invites_adhoc.lua @@ -67,7 +67,7 @@ module:provides("adhoc", new_adhoc("Create new contact invite", "urn:xmpp:invite --TODO: check errors return { status = "completed"; - form = { + result = { layout = invite_result_form; values = { uri = invite.uri; @@ -88,7 +88,7 @@ module:provides("adhoc", new_adhoc("Create new account invite", "urn:xmpp:invite --TODO: check errors return { status = "completed"; - form = { + result = { layout = invite_result_form; values = { uri = invite.uri; diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index fbc06fdb..33eee2ec 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -5,7 +5,7 @@ local jid_join = require "prosody.util.jid".join; local set_new = require "prosody.util.set".new; local st = require "prosody.util.stanza"; local calculate_hash = require "prosody.util.caps".calculate_hash; -local is_contact_subscribed = require "prosody.core.rostermanager".is_contact_subscribed; +local rostermanager = require "prosody.core.rostermanager"; local cache = require "prosody.util.cache"; local set = require "prosody.util.set"; local new_id = require "prosody.util.id".medium; @@ -16,6 +16,8 @@ local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; +local is_contact_subscribed = rostermanager.is_contact_subscribed; + local lib_pubsub = module:require "pubsub"; local empty_set = set_new(); @@ -84,6 +86,7 @@ function check_node_config(node, actor, new_config) -- luacheck: ignore 212/node return false; end if new_config["access_model"] ~= "presence" + and new_config["access_model"] ~= "roster" and new_config["access_model"] ~= "whitelist" and new_config["access_model"] ~= "open" then return false; @@ -256,6 +259,20 @@ function get_pep_service(username) end return "outcast"; end; + roster = function (jid, node) + jid = jid_bare(jid); + local allowed_groups = set_new(node.config.roster_groups_allowed); + local roster = rostermanager.load_roster(username, host); + if not roster[jid] then + return "outcast"; + end + for group in pairs(roster[jid].groups) do + if allowed_groups:contains(group) then + return "member"; + end + end + return "outcast"; + end; }; jid = user_bare; diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua index 3aa6a895..101e6e62 100644 --- a/plugins/mod_posix.lua +++ b/plugins/mod_posix.lua @@ -6,167 +6,6 @@ -- COPYING file in the source package for more information. -- - -local want_pposix_version = "0.4.0"; - -local pposix = assert(require "prosody.util.pposix"); -if pposix._VERSION ~= want_pposix_version then - module:log("warn", "Unknown version (%s) of binary pposix module, expected %s." - .. "Perhaps you need to recompile?", tostring(pposix._VERSION), want_pposix_version); -end - -local have_signal, signal = pcall(require, "prosody.util.signal"); -if not have_signal then - module:log("warn", "Couldn't load signal library, won't respond to SIGTERM"); -end - -local lfs = require "lfs"; -local stat = lfs.attributes; - -local prosody = _G.prosody; - module:set_global(); -- we're a global module -local umask = module:get_option_string("umask", "027"); -pposix.umask(umask); - --- Don't even think about it! -if not prosody.start_time then -- server-starting - if pposix.getuid() == 0 and not module:get_option_boolean("run_as_root") then - module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!"); - module:log("error", "For more information on running Prosody as root, see https://prosody.im/doc/root"); - prosody.shutdown("Refusing to run as root", 1); - end -end - -local pidfile; -local pidfile_handle; - -local function remove_pidfile() - if pidfile_handle then - pidfile_handle:close(); - os.remove(pidfile); - pidfile, pidfile_handle = nil, nil; - end -end - -local function write_pidfile() - if pidfile_handle then - remove_pidfile(); - end - pidfile = module:get_option_path("pidfile", nil, "data"); - if pidfile then - local err; - local mode = stat(pidfile) and "r+" or "w+"; - pidfile_handle, err = io.open(pidfile, mode); - if not pidfile_handle then - module:log("error", "Couldn't write pidfile at %s; %s", pidfile, err); - prosody.shutdown("Couldn't write pidfile", 1); - else - if not lfs.lock(pidfile_handle, "w") then -- Exclusive lock - local other_pid = pidfile_handle:read("*a"); - module:log("error", "Another Prosody instance seems to be running with PID %s, quitting", other_pid); - pidfile_handle = nil; - prosody.shutdown("Prosody already running", 1); - else - pidfile_handle:close(); - pidfile_handle, err = io.open(pidfile, "w+"); - if not pidfile_handle then - module:log("error", "Couldn't write pidfile at %s; %s", pidfile, err); - prosody.shutdown("Couldn't write pidfile", 1); - else - if lfs.lock(pidfile_handle, "w") then - pidfile_handle:write(tostring(pposix.getpid())); - pidfile_handle:flush(); - end - end - end - end - end -end - -local daemonize = prosody.opts.daemonize; - -if daemonize == nil then - -- Fall back to config file if not specified on command-line - daemonize = module:get_option_boolean("daemonize", nil); - if daemonize ~= nil then - module:log("warn", "The 'daemonize' option has been deprecated, specify -D or -F on the command line instead."); - -- TODO: Write some docs and include a link in the warning. - end -end - -local function remove_log_sinks() - local lm = require "prosody.core.loggingmanager"; - lm.register_sink_type("console", nil); - lm.register_sink_type("stdout", nil); - lm.reload_logging(); -end - -if daemonize then - local function daemonize_server() - module:log("info", "Prosody is about to detach from the console, disabling further console output"); - remove_log_sinks(); - local ok, ret = pposix.daemonize(); - if not ok then - module:log("error", "Failed to daemonize: %s", ret); - elseif ret and ret > 0 then - os.exit(0); - else - module:log("info", "Successfully daemonized to PID %d", pposix.getpid()); - write_pidfile(); - end - end - module:hook("server-started", daemonize_server) -else - -- Not going to daemonize, so write the pid of this process - write_pidfile(); -end - -module:hook("server-stopped", remove_pidfile); - --- Set signal handlers -if have_signal then - module:add_timer(0, function () - signal.signal("SIGTERM", function () - module:log("warn", "Received SIGTERM"); - prosody.main_thread:run(function () - prosody.unlock_globals(); - prosody.shutdown("Received SIGTERM"); - prosody.lock_globals(); - end); - end); - - signal.signal("SIGHUP", function () - module:log("info", "Received SIGHUP"); - prosody.main_thread:run(function () - prosody.reload_config(); - end); - -- this also reloads logging - end); - - signal.signal("SIGINT", function () - module:log("info", "Received SIGINT"); - prosody.main_thread:run(function () - prosody.unlock_globals(); - prosody.shutdown("Received SIGINT"); - prosody.lock_globals(); - end); - end); - - signal.signal("SIGUSR1", function () - module:log("info", "Received SIGUSR1"); - module:fire_event("signal/SIGUSR1"); - end); - - signal.signal("SIGUSR2", function () - module:log("info", "Received SIGUSR2"); - module:fire_event("signal/SIGUSR2"); - end); - end); -end - --- For other modules to reference -features = { - signal_events = true; -}; +-- TODO delete this whole concept diff --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua index de09ec7d..4f83088a 100644 --- a/plugins/mod_pubsub/mod_pubsub.lua +++ b/plugins/mod_pubsub/mod_pubsub.lua @@ -250,3 +250,46 @@ function module.load() normalize_jid = jid_bare; })); end + +local function get_service(service_jid) + return assert(assert(prosody.hosts[service_jid], "Unknown pubsub service").modules.pubsub, "Not a pubsub service").service; +end + +module:add_item("shell-command", { + section = "pubsub"; + section_desc = "Manage publish/subscribe nodes"; + name = "create_node"; + desc = "Create a node with the specified name"; + args = { + { name = "service_jid", type = "string" }; + { name = "node_name", type = "string" }; + }; + host_selector = "service_jid"; + + handler = function (self, service_jid, node_name) --luacheck: ignore 212/self + return get_service(service_jid):create(node_name, true); + end; +}); + +module:add_item("shell-command", { + section = "pubsub"; + section_desc = "Manage publish/subscribe nodes"; + name = "list_nodes"; + desc = "List nodes on a pubsub service"; + args = { + { name = "service_jid", type = "string" }; + }; + host_selector = "service_jid"; + + handler = function (self, service_jid) --luacheck: ignore 212/self + -- luacheck: ignore 431/service + local service = get_service(service_jid); + local nodes = select(2, assert(service:get_nodes(true))); + local count = 0; + for node_name in pairs(nodes) do + count = count + 1; + self.session.print(node_name); + end + return true, ("%d nodes"):format(count); + end; +}); diff --git a/plugins/mod_pubsub/pubsub.lib.lua b/plugins/mod_pubsub/pubsub.lib.lua index 28b7be50..8ae0a896 100644 --- a/plugins/mod_pubsub/pubsub.lib.lua +++ b/plugins/mod_pubsub/pubsub.lib.lua @@ -110,6 +110,12 @@ local node_config_form = dataform { }; }; { + type = "list-multi"; -- TODO some way to inject options + name = "roster_groups_allowed"; + var = "pubsub#roster_groups_allowed"; + label = "Roster groups allowed to subscribe"; + }; + { type = "list-single"; name = "publish_model"; var = "pubsub#publish_model"; diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index fcdfbca8..88b73eba 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -1015,6 +1015,8 @@ function check_auth_policy(event) -- In practice most cases are configuration mistakes or forgotten -- certificate renewals. We think it's better to let the other party -- know about the problem so that they can fix it. + -- + -- Note: Bounce message must not include name of server, as it may leak half your JID in semi-anon MUCs. session:close({ condition = "not-authorized", text = "Your server's certificate "..reason }, nil, "Remote server's certificate "..reason); return false; diff --git a/plugins/mod_s2s_auth_certs.lua b/plugins/mod_s2s_auth_certs.lua index 3606a6a0..2517c95f 100644 --- a/plugins/mod_s2s_auth_certs.lua +++ b/plugins/mod_s2s_auth_certs.lua @@ -1,7 +1,6 @@ module:set_global(); local cert_verify_identity = require "prosody.util.x509".verify_identity; -local NULL = {}; local log = module._log; local measure_cert_statuses = module:metric("counter", "checked", "", "Certificate validation results", @@ -23,8 +22,12 @@ module:hook("s2s-check-certificate", function(event) -- Is there any interest in printing out all/the number of errors here? if not chain_valid then log("debug", "certificate chain validation result: invalid"); - for depth, t in pairs(errors or NULL) do - log("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", ")) + if type(errors) == "table" then + for depth, t in pairs(errors) do + log("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", ")); + end + else + log("debug", "certificate error: %s", errors); end session.cert_chain_status = "invalid"; session.cert_chain_errors = errors; diff --git a/plugins/mod_s2s_auth_dane_in.lua b/plugins/mod_s2s_auth_dane_in.lua index 777fa582..9167e8a9 100644 --- a/plugins/mod_s2s_auth_dane_in.lua +++ b/plugins/mod_s2s_auth_dane_in.lua @@ -24,6 +24,25 @@ local function ensure_secure(r) return r; end +local function ensure_nonempty(r) + assert(r[1], "empty"); + return r; +end + +local function flatten(a) + local seen = {}; + local ret = {}; + for _, rrset in ipairs(a) do + for _, rr in ipairs(rrset) do + if not seen[tostring(rr)] then + table.insert(ret, rr); + seen[tostring(rr)] = true; + end + end + end + return ret; +end + local lazy_tlsa_mt = { __index = function(t, i) if i == 1 then @@ -73,36 +92,32 @@ module:hook("s2s-check-certificate", function(event) if rr.srv.target == "." then return {}; end table.insert(tlsas, resolver:lookup_promise(("_%d._tcp.%s"):format(rr.srv.port, rr.srv.target), "TLSA"):next(ensure_secure)); end - return promise.all(tlsas); + return promise.all(tlsas):next(flatten); end - local ret = async.wait_for(promise.all({ - resolver:lookup_promise("_xmpps-server._tcp." .. dns_domain, "SRV"):next(ensure_secure):next(fetch_tlsa); - resolver:lookup_promise("_xmpp-server._tcp." .. dns_domain, "SRV"):next(ensure_secure):next(fetch_tlsa); - })); + local ret = async.wait_for(resolver:lookup_promise("_xmpp-server." .. dns_domain, "TLSA"):next(ensure_secure):next(ensure_nonempty):catch(function() + return promise.all({ + resolver:lookup_promise("_xmpps-server._tcp." .. dns_domain, "SRV"):next(ensure_secure):next(fetch_tlsa); + resolver:lookup_promise("_xmpp-server._tcp." .. dns_domain, "SRV"):next(ensure_secure):next(fetch_tlsa); + }):next(flatten); + end)); if not ret then return end local found_supported = false; - for _, by_proto in ipairs(ret) do - for _, by_srv in ipairs(by_proto) do - for _, by_target in ipairs(by_srv) do - for _, rr in ipairs(by_target) do - if rr.tlsa.use == 3 and by_select_match[rr.tlsa.select] and rr.tlsa.match <= 2 then - found_supported = true; - if rr.tlsa.data == by_select_match[rr.tlsa.select][rr.tlsa.match] then - module:log("debug", "%s matches", rr) - session.cert_chain_status = "valid"; - session.cert_identity_status = "valid"; - return true; - end - else - log("debug", "Unsupported DANE TLSA record: %s", rr); - end - end + for _, rr in ipairs(ret) do + if rr.tlsa.use == 3 and by_select_match[rr.tlsa.select] and rr.tlsa.match <= 2 then + found_supported = true; + if rr.tlsa.data == by_select_match[rr.tlsa.select][rr.tlsa.match] then + module:log("debug", "%s matches", rr) + session.cert_chain_status = "valid"; + session.cert_identity_status = "valid"; + return true; end + else + log("debug", "Unsupported DANE TLSA record: %s", rr); end end diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index b219d711..b6cd31c8 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -335,6 +335,8 @@ module:hook("stream-features", function(event) log("debug", "Channel binding 'tls-exporter' supported"); sasl_handler:add_cb_handler("tls-exporter", sasl_tls_exporter); channel_bindings:add("tls-exporter"); + else + log("debug", "Channel binding 'tls-exporter' not supported"); end elseif origin.conn.ssl_peerfinished and origin.conn:ssl_peerfinished() then log("debug", "Channel binding 'tls-unique' supported"); diff --git a/plugins/mod_server_contact_info.lua b/plugins/mod_server_contact_info.lua index b7f4c7f3..67fed752 100644 --- a/plugins/mod_server_contact_info.lua +++ b/plugins/mod_server_contact_info.lua @@ -7,21 +7,22 @@ -- local array = require "prosody.util.array"; -local dataforms = require "prosody.util.dataforms"; +local it = require "prosody.util.iterators"; local jid = require "prosody.util.jid"; local url = require "socket.url"; +module:depends("server_info"); + -- Source: http://xmpp.org/registrar/formtypes.html#http:--jabber.org-network-serverinfo -local form_layout = dataforms.new({ - { var = "FORM_TYPE"; type = "hidden"; value = "http://jabber.org/network/serverinfo" }; - { type = "list-multi"; name = "abuse"; var = "abuse-addresses" }; - { type = "list-multi"; name = "admin"; var = "admin-addresses" }; - { type = "list-multi"; name = "feedback"; var = "feedback-addresses" }; - { type = "list-multi"; name = "sales"; var = "sales-addresses" }; - { type = "list-multi"; name = "security"; var = "security-addresses" }; - { type = "list-multi"; name = "status"; var = "status-addresses" }; - { type = "list-multi"; name = "support"; var = "support-addresses" }; -}); +local address_types = { + abuse = "abuse-addresses"; + admin = "admin-addresses"; + feedback = "feedback-addresses"; + sales = "sales-addresses"; + security = "security-addresses"; + status = "status-addresses"; + support = "support-addresses"; +}; -- JIDs of configured service admins are used as fallback local admins = module:get_option_inherited_set("admins", {}); @@ -30,4 +31,17 @@ local contact_config = module:get_option("contact_info", { admin = array.collect(admins / jid.prep / function(admin) return url.build({scheme = "xmpp"; path = admin}); end); }); -module:add_extension(form_layout:form(contact_config, "result")); +local fields = {}; + +for key, field_var in it.sorted_pairs(address_types) do + if contact_config[key] then + table.insert(fields, { + type = "list-multi"; + name = key; + var = field_var; + value = contact_config[key]; + }); + end +end + +module:add_item("server-info-fields", fields); diff --git a/plugins/mod_server_info.lua b/plugins/mod_server_info.lua new file mode 100644 index 00000000..5469bf02 --- /dev/null +++ b/plugins/mod_server_info.lua @@ -0,0 +1,55 @@ +local dataforms = require "prosody.util.dataforms"; + +local server_info_config = module:get_option("server_info", {}); +local server_info_custom_fields = module:get_option_array("server_info_extensions"); + +-- Source: http://xmpp.org/registrar/formtypes.html#http:--jabber.org-network-serverinfo +local form_layout = dataforms.new({ + { var = "FORM_TYPE"; type = "hidden"; value = "http://jabber.org/network/serverinfo" }; +}); + +if server_info_custom_fields then + for _, field in ipairs(server_info_custom_fields) do + table.insert(form_layout, field); + end +end + +local generated_form; + +function update_form() + local new_form = form_layout:form(server_info_config, "result"); + if generated_form then + module:remove_item("extension", generated_form); + end + generated_form = new_form; + module:add_item("extension", generated_form); +end + +function add_fields(event) + local fields = event.item; + for _, field in ipairs(fields) do + table.insert(form_layout, field); + end + update_form(); +end + +function remove_fields(event) + local removed_fields = event.item; + for _, removed_field in ipairs(removed_fields) do + local removed_var = removed_field.var or removed_field.name; + for i, field in ipairs(form_layout) do + local var = field.var or field.name + if var == removed_var then + table.remove(form_layout, i); + break; + end + end + end + update_form(); +end + +module:handle_items("server-info-fields", add_fields, remove_fields); + +function module.load() + update_form(); +end diff --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 486f611a..d4f0f371 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -39,7 +39,7 @@ local resumption_age = module:metric( "histogram", "resumption_age", "seconds", "time the session had been hibernating at the time of a resumption", {}, - {buckets = { 0, 1, 2, 5, 10, 30, 60, 120, 300, 600 }} + {buckets = {0, 1, 12, 60, 360, 900, 1440, 3600, 14400, 86400}} ):with_labels(); local sessions_expired = module:measure("sessions_expired", "counter"); local sessions_started = module:measure("sessions_started", "counter"); diff --git a/plugins/mod_storage_internal.lua b/plugins/mod_storage_internal.lua index deab7dfd..a43dd272 100644 --- a/plugins/mod_storage_internal.lua +++ b/plugins/mod_storage_internal.lua @@ -200,15 +200,11 @@ function archive:find(username, query) end if query.start then if not query.reverse then - local wi, exact = binary_search(list, function(item) + local wi = binary_search(list, function(item) local when = item.when or datetime.parse(item.attr.stamp); return query.start - when; end); - if exact then - i = wi - 1; - elseif wi then - i = wi; - end + i = wi - 1; else iter = it.filter(function(item) local when = item.when or datetime.parse(item.attr.stamp); diff --git a/plugins/mod_version.lua b/plugins/mod_version.lua index d9d3844c..72b13387 100644 --- a/plugins/mod_version.lua +++ b/plugins/mod_version.lua @@ -22,7 +22,12 @@ if not module:get_option_boolean("hide_os_type") then local os_version_command = module:get_option_string("os_version_command"); local ok, pposix = pcall(require, "prosody.util.pposix"); if not os_version_command and (ok and pposix and pposix.uname) then - platform = pposix.uname().sysname; + local uname, err = pposix.uname(); + if not uname then + module:log("debug", "Could not retrieve OS name: %s", err); + else + platform = uname.sysname; + end end if not platform then local uname = io.popen(os_version_command or "uname"); diff --git a/plugins/muc/hats.lib.lua b/plugins/muc/hats.lib.lua index e1587974..492dc72c 100644 --- a/plugins/muc/hats.lib.lua +++ b/plugins/muc/hats.lib.lua @@ -1,7 +1,10 @@ local st = require "prosody.util.stanza"; local muc_util = module:require "muc/util"; -local xmlns_hats = "xmpp:prosody.im/protocol/hats:1"; +local hats_compat = module:get_option_boolean("muc_hats_compat", true); -- COMPAT for pre-XEP namespace, TODO reconsider default for next release + +local xmlns_hats_legacy = "xmpp:prosody.im/protocol/hats:1"; +local xmlns_hats = "urn:xmpp:hats:0"; -- Strip any hats claimed by the client (to prevent spoofing) muc_util.add_filtered_namespace(xmlns_hats); @@ -13,14 +16,26 @@ module:hook("muc-build-occupant-presence", function (event) local hats = aff_data and aff_data.hats; if not hats then return; end local hats_el; + local legacy_hats_el; for hat_id, hat_data in pairs(hats) do if hat_data.active then if not hats_el then hats_el = st.stanza("hats", { xmlns = xmlns_hats }); end hats_el:tag("hat", { uri = hat_id, title = hat_data.title }):up(); + + if hats_compat then + if not 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(); + end end end if not hats_el then return; end event.stanza:add_direct_child(hats_el); + + if legacy_hats_el then + event.stanza:add_direct_child(legacy_hats_el); + end end); diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index 76b722ba..b8f276cf 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -1079,7 +1079,10 @@ function room_mt:handle_admin_query_set_command(origin, stanza) local reason = item:get_child_text("reason"); local success, errtype, err if item.attr.affiliation and item.attr.jid and not item.attr.role then - local registration_data; + local registration_data = self:get_affiliation_data(item.attr.jid) or {}; + if reason then + registration_data.reason = reason; + end if item.attr.nick then local room_nick = self.jid.."/"..item.attr.nick; local existing_occupant = self:get_occupant_by_nick(room_nick); @@ -1088,7 +1091,7 @@ function room_mt:handle_admin_query_set_command(origin, stanza) self:set_role(true, room_nick, nil, "This nickname is reserved"); end module:log("debug", "Reserving %s for %s (%s)", item.attr.nick, item.attr.jid, item.attr.affiliation); - registration_data = { reserved_nickname = item.attr.nick }; + registration_data.reserved_nickname = item.attr.nick; end success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, reason, registration_data); elseif item.attr.role and item.attr.nick and not item.attr.affiliation then @@ -1119,9 +1122,13 @@ function room_mt:handle_admin_query_get_command(origin, stanza) if (affiliation_rank >= valid_affiliations.admin and affiliation_rank >= _aff_rank) or (self:get_members_only() and self:get_whois() == "anyone" and affiliation_rank >= valid_affiliations.member) then local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); - for jid in self:each_affiliation(_aff or "none") do + for jid, _, data in self:each_affiliation(_aff or "none") do local nick = self:get_registered_nick(jid); - reply:tag("item", {affiliation = _aff, jid = jid, nick = nick }):up(); + reply:tag("item", {affiliation = _aff, jid = jid, nick = nick }); + if data and data.reason then + reply:text_tag("reason", data.reason); + end + reply:up(); end origin.send(reply:up()); return true; diff --git a/spec/scansion/disco_self.scs b/spec/scansion/disco_self.scs new file mode 100644 index 00000000..6782e884 --- /dev/null +++ b/spec/scansion/disco_self.scs @@ -0,0 +1,26 @@ +# Basic login and initial presence + +[Client] Romeo + jid: discoverer@localhost + password: password + +--------- + +Romeo connects + +Romeo sends: + <iq type="get" id="info1"> + <query xmlns="http://jabber.org/protocol/disco#info"/> + </iq> + +Romeo receives: + <iq type="result" id="info1"> + <query xmlns="http://jabber.org/protocol/disco#info" scansion:strict="false"> + <identity xmlns="http://jabber.org/protocol/disco#info" category="account" type="registered"/> + <feature var="http://jabber.org/protocol/disco#info"/> + <feature var="http://jabber.org/protocol/disco#items"/> + </query> + </iq> + +Romeo disconnects + diff --git a/spec/scansion/muc_outcast_reason.scs b/spec/scansion/muc_outcast_reason.scs new file mode 100644 index 00000000..e2725653 --- /dev/null +++ b/spec/scansion/muc_outcast_reason.scs @@ -0,0 +1,72 @@ +# Save ban reason + +[Client] Romeo + password: password + jid: user@localhost + +----- + +Romeo connects + +Romeo sends: + <presence to="muc-outcast-reason@conference.localhost/Romeo"> + <x xmlns="http://jabber.org/protocol/muc"/> + </presence> + +Romeo receives: + <presence from="muc-outcast-reason@conference.localhost/Romeo"> + <x xmlns="http://jabber.org/protocol/muc#user"> + <status code="201"/> + <item jid="${Romeo's full JID}" role="moderator" affiliation="owner"/> + <status code="110"/> + </x> + </presence> + +Romeo receives: + <message type="groupchat" from="muc-outcast-reason@conference.localhost"> + <subject/> + </message> + +Romeo sends: + <iq id="lx5" to="muc-outcast-reason@conference.localhost" type="set"> + <query xmlns="http://jabber.org/protocol/muc#admin"> + <item affiliation="outcast" jid="tybalt@localhost"> + <reason>Hey calm down</reason> + </item> + </query> + </iq> + +Romeo receives: + <message from="muc-outcast-reason@conference.localhost"> + <x xmlns="http://jabber.org/protocol/muc#user"> + <status code="301"/> + <item jid="tybalt@localhost" affiliation="outcast"> + <reason>Hey calm down</reason> + </item> + </x> + </message> + +Romeo receives: + <iq id="lx5" type="result" from="muc-outcast-reason@conference.localhost"/> + +Romeo sends: + <iq id="lx6" to="muc-outcast-reason@conference.localhost" type="get"> + <query xmlns="http://jabber.org/protocol/muc#admin"> + <item affiliation="outcast"/> + </query> + </iq> + +Romeo receives: + <iq id="lx6" type="result" from="muc-outcast-reason@conference.localhost"> + <query xmlns="http://jabber.org/protocol/muc#admin"> + <item jid="tybalt@localhost" affiliation="outcast"> + <reason>Hey calm down</reason> + </item> + </query> + </iq> + +Romeo disconnects + +Romeo sends: + <presence type='unavailable'/> + diff --git a/spec/scansion/muc_subject_issue_667.scs b/spec/scansion/muc_subject_issue_667.scs index 74980073..a4544ce4 100644 --- a/spec/scansion/muc_subject_issue_667.scs +++ b/spec/scansion/muc_subject_issue_667.scs @@ -42,6 +42,21 @@ Romeo receives: <body>Hello everyone</body> </message> +# this should be treated as a normal message +Romeo sends: + <message to="issue667@conference.localhost" type="groupchat"> + <subject>New thread</subject> + <thread>498acea5-5894-473f-b4c6-c77319d11c75</thread> + <store xmlns="urn:xmpp:hints"/> + </message> + +Romeo receives: + <message type="groupchat" from="issue667@conference.localhost/Romeo"> + <subject>New thread</subject> + <thread>498acea5-5894-473f-b4c6-c77319d11c75</thread> + <store xmlns="urn:xmpp:hints"/> + </message> + # Resync Romeo sends: <presence to="issue667@conference.localhost/Romeo"> @@ -63,6 +78,13 @@ Romeo receives: <body>Hello everyone</body> </message> +Romeo receives: + <message type="groupchat" from="issue667@conference.localhost/Romeo"> + <subject>New thread</subject> + <thread>498acea5-5894-473f-b4c6-c77319d11c75</thread> + <store xmlns="urn:xmpp:hints"/> + </message> + # the still empty subject Romeo receives: <message type="groupchat" from="issue667@conference.localhost"> @@ -116,6 +138,13 @@ Romeo receives: Romeo receives: <message type="groupchat" from="issue667@conference.localhost/Romeo"> + <subject>New thread</subject> + <thread>498acea5-5894-473f-b4c6-c77319d11c75</thread> + <store xmlns="urn:xmpp:hints"/> + </message> + +Romeo receives: + <message type="groupchat" from="issue667@conference.localhost/Romeo"> <body>Lorem ipsum dolor sit amet</body> </message> diff --git a/spec/util_bitcompat_spec.lua b/spec/util_bitcompat_spec.lua index 34a87f5b..99642821 100644 --- a/spec/util_bitcompat_spec.lua +++ b/spec/util_bitcompat_spec.lua @@ -24,4 +24,8 @@ describe("util.bitcompat", function () it("lshift works", function () assert.equal(0xFF00, bit.lshift(0xFF, 8)); end); + + it("bnot works", function () + assert.equal(0x0000FF00, bit.band(0xFFFFFFFF, bit.bnot(0xFFFF00FF))); + end); end); diff --git a/spec/util_ip_spec.lua b/spec/util_ip_spec.lua index 2725ba3a..a0287ee7 100644 --- a/spec/util_ip_spec.lua +++ b/spec/util_ip_spec.lua @@ -36,6 +36,8 @@ describe("util.ip", function() assert.are.equal(match(_"8.8.8.8", _"8.8.0.0", 16), true); assert.are.equal(match(_"8.8.4.4", _"8.8.0.0", 16), true); + + assert.are.equal(match(_"fe80::1", _"fec0::", 10), false); end); end); @@ -98,6 +100,8 @@ describe("util.ip", function() assert_cpl6("abcd::1", "abcd::1", 128); assert_cpl6("abcd::abcd", "abcd::", 112); assert_cpl6("abcd::abcd", "abcd::abcd:abcd", 96); + + assert_cpl6("fe80::1", "fec0::", 9); end); end); diff --git a/spec/util_rfc6724_spec.lua b/spec/util_rfc6724_spec.lua deleted file mode 100644 index 30e935b6..00000000 --- a/spec/util_rfc6724_spec.lua +++ /dev/null @@ -1,97 +0,0 @@ - -local rfc6724 = require "util.rfc6724"; -local new_ip = require"util.ip".new_ip; - -describe("util.rfc6724", function() - describe("#source()", function() - it("should work", function() - assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"), - {new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, - "2001:db8:3::1", - "prefer appropriate scope"); - assert.are.equal(rfc6724.source(new_ip("ff05::1", "IPv6"), - {new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, - "2001:db8:3::1", - "prefer appropriate scope"); - assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"), - {new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:2::1", "IPv6")}).addr, - "2001:db8:1::1", - "prefer same address"); -- "2001:db8:1::1" should be marked "deprecated" here, we don't handle that right now - assert.are.equal(rfc6724.source(new_ip("fe80::1", "IPv6"), - {new_ip("fe80::2", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}).addr, - "fe80::2", - "prefer appropriate scope"); -- "fe80::2" should be marked "deprecated" here, we don't handle that right now - assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"), - {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr, - "2001:db8:1::2", - "longest matching prefix"); - --[[ "2001:db8:1::2" should be a care-of address and "2001:db8:3::2" a home address, we can't handle this and would fail - assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"), - {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr, - "2001:db8:3::2", - "prefer home address"); - ]] - assert.are.equal(rfc6724.source(new_ip("2002:c633:6401::1", "IPv6"), - {new_ip("2002:c633:6401::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001:db8:1::2", "IPv6")}).addr, - "2002:c633:6401::d5e3:7953:13eb:22e8", - "prefer matching label"); -- "2002:c633:6401::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now - assert.are.equal(rfc6724.source(new_ip("2001:db8:1::d5e3:0:0:1", "IPv6"), - {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:1::d5e3:7953:13eb:22e8", "IPv6")}).addr, - "2001:db8:1::d5e3:7953:13eb:22e8", - "prefer temporary address") -- "2001:db8:1::2" should be marked "public" and "2001:db8:1::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now - end); - end); - describe("#destination()", function() - it("should work", function() - local order; - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")}, - {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")}) - assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer matching scope"); - assert.are.equal(order[2].addr, "198.51.100.121", "prefer matching scope"); - - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")}, - {new_ip("fe80::1", "IPv6"), new_ip("198.51.100.117", "IPv4")}) - assert.are.equal(order[1].addr, "198.51.100.121", "prefer matching scope"); - assert.are.equal(order[2].addr, "2001:db8:1::1", "prefer matching scope"); - - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("10.1.2.3", "IPv4")}, - {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")}) - assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence"); - assert.are.equal(order[2].addr, "10.1.2.3", "prefer higher precedence"); - - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")}, - {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert.are.equal(order[1].addr, "fe80::1", "prefer smaller scope"); - assert.are.equal(order[2].addr, "2001:db8:1::1", "prefer smaller scope"); - - --[[ "2001:db8:1::2" and "fe80::2" should be marked "care-of address", while "2001:db8:3::1" should be marked "home address", we can't currently handle this and would fail the test - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")}, - {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer home address"); - assert.are.equal(order[2].addr, "fe80::1", "prefer home address"); - ]] - - --[[ "fe80::2" should be marked "deprecated", we can't currently handle this and would fail the test - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")}, - {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert.are.equal(order[1].addr, "2001:db8:1::1", "avoid deprecated addresses"); - assert.are.equal(order[2].addr, "fe80::1", "avoid deprecated addresses"); - ]] - - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:3ffe::1", "IPv6")}, - {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert.are.equal(order[1].addr, "2001:db8:1::1", "longest matching prefix"); - assert.are.equal(order[2].addr, "2001:db8:3ffe::1", "longest matching prefix"); - - order = rfc6724.destination({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}, - {new_ip("2002:c633:6401::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert.are.equal(order[1].addr, "2002:c633:6401::1", "prefer matching label"); - assert.are.equal(order[2].addr, "2001:db8:1::1", "prefer matching label"); - - order = rfc6724.destination({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}, - {new_ip("2002:c633:6401::2", "IPv6"), new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence"); - assert.are.equal(order[2].addr, "2002:c633:6401::1", "prefer higher precedence"); - end); - end); -end); diff --git a/spec/util_strbitop.lua b/spec/util_strbitop_spec.lua index 58a13772..963c9516 100644 --- a/spec/util_strbitop.lua +++ b/spec/util_strbitop_spec.lua @@ -38,4 +38,48 @@ describe("util.strbitop", function () assert.equal("hello", strbitop.sxor("hello", "")); end); end); + + describe("common_prefix_bits()", function () + local function B(s) + assert(#s%8==0, "Invalid test input: B(s): s should be a multiple of 8 bits in length"); + local byte = 0; + local out_str = {}; + for i = 1, #s do + local bit_ascii = s:byte(i); + if bit_ascii == 49 then -- '1' + byte = byte + 2^((7-(i-1))%8); + elseif bit_ascii ~= 48 then + error("Invalid test input: B(s): s should contain only '0' or '1' characters"); + end + if (i-1)%8 == 7 then + table.insert(out_str, string.char(byte)); + byte = 0; + end + end + return table.concat(out_str); + end + + local _cpb = strbitop.common_prefix_bits; + local function test(a, b) + local Ba, Bb = B(a), B(b); + local ret1 = _cpb(Ba, Bb); + local ret2 = _cpb(Bb, Ba); + assert(ret1 == ret2, ("parameter order should not make a difference to the result (%s, %s) = %d, reversed = %d"):format(a, b, ret1, ret2)); + return ret1; + end + + it("works on single bytes", function () + assert.equal(0, test("00000000", "11111111")); + assert.equal(1, test("10000000", "11111111")); + assert.equal(0, test("01000000", "11111111")); + assert.equal(0, test("01000000", "11111111")); + assert.equal(8, test("11111111", "11111111")); + end); + + it("works on multiple bytes", function () + for i = 0, 16 do + assert.equal(i, test(string.rep("1", i)..string.rep("0", 16-i), "1111111111111111")); + end + end); + end); end); diff --git a/teal-src/prosody/plugins/mod_cron.tl b/teal-src/prosody/plugins/mod_cron.tl index 9c6f1601..4defc808 100644 --- a/teal-src/prosody/plugins/mod_cron.tl +++ b/teal-src/prosody/plugins/mod_cron.tl @@ -2,6 +2,10 @@ module:set_global(); local async = require "prosody.util.async"; +local cron_initial_delay = module:get_option_number("cron_initial_delay", 1); +local cron_check_delay = module:get_option_number("cron_check_delay", 3600); +local cron_spread_factor = module:get_option_number("cron_spread_factor", 0); + local record map_store<K,V> -- TODO move to somewhere sensible get : function (map_store<K,V>, string, K) : V @@ -90,17 +94,21 @@ local function run_task(task : task_spec) task:save(started_at); end +local function spread(t : number, factor : number) : number + return t * (1 - factor + 2*factor*math.random()); +end + local task_runner : async.runner_t<task_spec> = async.runner(run_task); -scheduled = module:add_timer(1, function() : integer +scheduled = module:add_timer(cron_initial_delay, function() : number module:log("info", "Running periodic tasks"); - local delay = 3600; + local delay = spread(cron_check_delay, cron_spread_factor); for host in pairs(active_hosts) do module:log("debug", "Running periodic tasks for host %s", host); for _, task in ipairs(module:context(host):get_host_items("task") as { task_spec } ) do task_runner:run(task); end end - module:log("debug", "Wait %ds", delay); + module:log("debug", "Wait %gs", delay); return delay; end); diff --git a/teal-src/prosody/util/crypto.d.tl b/teal-src/prosody/util/crypto.d.tl index cf0b0d1b..866185d0 100644 --- a/teal-src/prosody/util/crypto.d.tl +++ b/teal-src/prosody/util/crypto.d.tl @@ -5,23 +5,51 @@ local record lib get_type : function (key) : string end - generate_ed25519_keypair : function () : key - ed25519_sign : function (key, string) : string - ed25519_verify : function (key, string, string) : boolean + type base_evp_sign = function (key, message : string) : string + type base_evp_verify = function (key, message : string, signature : string) : boolean + + ed25519_sign : base_evp_sign + ed25519_verify : base_evp_verify + + ecdsa_sha256_sign : base_evp_sign + ecdsa_sha256_verify : base_evp_verify + ecdsa_sha384_sign : base_evp_sign + ecdsa_sha384_verify : base_evp_verify + ecdsa_sha512_sign : base_evp_sign + ecdsa_sha512_verify : base_evp_verify + + rsassa_pkcs1_sha256_sign : base_evp_sign + rsassa_pkcs1_sha256_verify : base_evp_verify + rsassa_pkcs1_sha384_sign : base_evp_sign + rsassa_pkcs1_sha384_verify : base_evp_verify + rsassa_pkcs1_sha512_sign : base_evp_sign + rsassa_pkcs1_sha512_verify : base_evp_verify + + rsassa_pss_sha256_sign : base_evp_sign + rsassa_pss_sha256_verify : base_evp_verify + rsassa_pss_sha384_sign : base_evp_sign + rsassa_pss_sha384_verify : base_evp_verify + rsassa_pss_sha512_sign : base_evp_sign + rsassa_pss_sha512_verify : base_evp_verify - 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 + type Levp_encrypt = function (key : string, iv : string, plaintext : string) : string + type Levp_decrypt = function (key : string, iv : string, ciphertext : string) : string, string + + aes_128_gcm_encrypt : Levp_encrypt + aes_128_gcm_decrypt : Levp_decrypt + aes_256_gcm_encrypt : Levp_encrypt + aes_256_gcm_decrypt : Levp_decrypt + + aes_256_ctr_encrypt : Levp_encrypt + aes_256_ctr_decrypt : Levp_decrypt + + generate_ed25519_keypair : function () : key 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 - + parse_ecdsa_signature : function (string, integer) : string, string + build_ecdsa_signature : function (r : string, s : string) : string version : string _LIBCRYPTO_VERSION : string diff --git a/teal-src/prosody/util/hashes.d.tl b/teal-src/prosody/util/hashes.d.tl index 5c249627..64c5a12b 100644 --- a/teal-src/prosody/util/hashes.d.tl +++ b/teal-src/prosody/util/hashes.d.tl @@ -4,8 +4,8 @@ local type kdf = function (pass : string, salt : string, i : integer) : string local record lib sha1 : hash - sha256 : hash sha224 : hash + sha256 : hash sha384 : hash sha512 : hash md5 : hash @@ -14,16 +14,20 @@ local record lib blake2s256 : hash blake2b512 : hash hmac_sha1 : hmac - hmac_sha256 : hmac hmac_sha224 : hmac + hmac_sha256 : hmac hmac_sha384 :hmac hmac_sha512 : hmac hmac_md5 : hmac hmac_sha3_256 : hmac hmac_sha3_512 : hmac + hmac_blake2s256 : hmac + hmac_blake2b512 : hmac scram_Hi_sha1 : kdf pbkdf2_hmac_sha1 : kdf pbkdf2_hmac_sha256 : kdf + hkdf_hmac_sha256 : kdf + hkdf_hmac_sha384 : kdf equals : function (string, string) : boolean version : string _LIBCRYPTO_VERSION : string diff --git a/teal-src/prosody/util/strbitop.d.tl b/teal-src/prosody/util/strbitop.d.tl index 010efdb8..86577ef2 100644 --- a/teal-src/prosody/util/strbitop.d.tl +++ b/teal-src/prosody/util/strbitop.d.tl @@ -2,5 +2,6 @@ local record mod sand : function (string, string) : string sor : function (string, string) : string sxor : function (string, string) : string + common_prefix_bits : function (string, string) : integer end return mod diff --git a/tools/test_mutants.sh.lua b/tools/test_mutants.sh.lua index a0a55a8e..6e2423db 100755 --- a/tools/test_mutants.sh.lua +++ b/tools/test_mutants.sh.lua @@ -33,7 +33,7 @@ if [[ "$SPEC_FILE" == "" || ! -f "$SPEC_FILE" ]]; then exit 1; fi -if ! busted "$SPEC_FILE"; then +if ! busted --helper=loader "$SPEC_FILE"; then echo "EE: Tests fail on original source. Fix it"\!; exit 1; fi diff --git a/util-src/signal.c b/util-src/signal.c index a55b6f87..76d25d6f 100644 --- a/util-src/signal.c +++ b/util-src/signal.c @@ -32,6 +32,10 @@ #include <signal.h> #include <stdlib.h> +#ifdef __linux__ +#include <unistd.h> +#include <sys/signalfd.h> +#endif #include "lua.h" #include "lauxlib.h" @@ -368,12 +372,81 @@ static int l_kill(lua_State *L) { #endif +#ifdef __linux__ +struct lsignalfd { + int fd; + sigset_t mask; +}; + +static int l_signalfd(lua_State *L) { + struct lsignalfd *sfd = lua_newuserdata(L, sizeof(struct lsignalfd)); + + sigemptyset(&sfd->mask); + sigaddset(&sfd->mask, luaL_checkinteger(L, 1)); + + if (sigprocmask(SIG_BLOCK, &sfd->mask, NULL) != 0) { + lua_pushnil(L); + return 1; + }; + + sfd->fd = signalfd(-1, &sfd->mask, SFD_NONBLOCK); + + if(sfd->fd == -1) { + lua_pushnil(L); + return 1; + } + + luaL_setmetatable(L, "signalfd"); + return 1; +} + +static int l_signalfd_getfd(lua_State *L) { + struct lsignalfd *sfd = luaL_checkudata(L, 1, "signalfd"); + + if (sfd->fd == -1) { + lua_pushnil(L); + return 1; + } + + lua_pushinteger(L, sfd->fd); + return 1; +} + +static int l_signalfd_read(lua_State *L) { + struct lsignalfd *sfd = luaL_checkudata(L, 1, "signalfd"); + struct signalfd_siginfo siginfo; + + if(read(sfd->fd, &siginfo, sizeof(siginfo)) < 0) { + return 0; + } + + lua_pushinteger(L, siginfo.ssi_signo); + return 1; +} + +static int l_signalfd_close(lua_State *L) { + struct lsignalfd *sfd = luaL_checkudata(L, 1, "signalfd"); + + if(close(sfd->fd) != 0) { + lua_pushboolean(L, 0); + return 1; + } + + sfd->fd = -1; + lua_pushboolean(L, 1); + return 1; +} +#endif + static const struct luaL_Reg lsignal_lib[] = { {"signal", l_signal}, {"raise", l_raise}, #if defined(__unix__) || defined(__APPLE__) {"kill", l_kill}, #endif +#ifdef __linux__ + {"signalfd", l_signalfd}, +#endif {NULL, NULL} }; @@ -381,6 +454,23 @@ int luaopen_prosody_util_signal(lua_State *L) { luaL_checkversion(L); int i = 0; +#ifdef __linux__ + luaL_newmetatable(L, "signalfd"); + lua_pushcfunction(L, l_signalfd_close); + lua_setfield(L, -2, "__gc"); + lua_createtable(L, 0, 1); + { + lua_pushcfunction(L, l_signalfd_getfd); + lua_setfield(L, -2, "getfd"); + lua_pushcfunction(L, l_signalfd_read); + lua_setfield(L, -2, "read"); + lua_pushcfunction(L, l_signalfd_close); + lua_setfield(L, -2, "close"); + } + lua_setfield(L, -2, "__index"); + lua_pop(L, 1); +#endif + /* add the library */ lua_newtable(L); luaL_setfuncs(L, lsignal_lib, 0); diff --git a/util-src/strbitop.c b/util-src/strbitop.c index 75cfea81..2f6bf6e6 100644 --- a/util-src/strbitop.c +++ b/util-src/strbitop.c @@ -8,6 +8,8 @@ #include <lua.h> #include <lauxlib.h> +#include <sys/param.h> +#include <limits.h> /* TODO Deduplicate code somehow */ @@ -74,11 +76,46 @@ static int strop_xor(lua_State *L) { return 1; } +unsigned int clz(unsigned char c) { +#if __GNUC__ + return __builtin_clz((unsigned int) c) - ((sizeof(int)-1)*CHAR_BIT); +#else + if(c & 0x80) return 0; + if(c & 0x40) return 1; + if(c & 0x20) return 2; + if(c & 0x10) return 3; + if(c & 0x08) return 4; + if(c & 0x04) return 5; + if(c & 0x02) return 6; + if(c & 0x01) return 7; + return 8; +#endif +} + +LUA_API int strop_common_prefix_bits(lua_State *L) { + size_t a, b, i; + const char *str_a = luaL_checklstring(L, 1, &a); + const char *str_b = luaL_checklstring(L, 2, &b); + + size_t min_len = MIN(a, b); + + for(i=0; i<min_len; i++) { + if(str_a[i] != str_b[i]) { + lua_pushinteger(L, i*8 + (clz(str_a[i] ^ str_b[i]))); + return 1; + } + } + + lua_pushinteger(L, i*8); + return 1; +} + LUA_API int luaopen_prosody_util_strbitop(lua_State *L) { luaL_Reg exports[] = { { "sand", strop_and }, { "sor", strop_or }, { "sxor", strop_xor }, + { "common_prefix_bits", strop_common_prefix_bits }, { NULL, NULL } }; diff --git a/util/bit53.lua b/util/bit53.lua index b5c473a3..42f17ce8 100644 --- a/util/bit53.lua +++ b/util/bit53.lua @@ -27,6 +27,9 @@ return { end return ret; end; + bnot = function (x) + return ~x; + end; rshift = function (a, n) return a >> n end; lshift = function (a, n) return a << n end; }; diff --git a/util/ip.lua b/util/ip.lua index 268b7d10..d820e72d 100644 --- a/util/ip.lua +++ b/util/ip.lua @@ -6,7 +6,7 @@ -- local net = require "prosody.util.net"; -local hex = require "prosody.util.hex"; +local strbit = require "prosody.util.strbitop"; local ip_methods = {}; @@ -28,13 +28,6 @@ ip_mt.__eq = function (ipA, ipB) return ipA.packed == ipB.packed; end -local hex2bits = { - ["0"] = "0000", ["1"] = "0001", ["2"] = "0010", ["3"] = "0011", - ["4"] = "0100", ["5"] = "0101", ["6"] = "0110", ["7"] = "0111", - ["8"] = "1000", ["9"] = "1001", ["A"] = "1010", ["B"] = "1011", - ["C"] = "1100", ["D"] = "1101", ["E"] = "1110", ["F"] = "1111", -}; - local function new_ip(ipStr, proto) local zone; if (not proto or proto == "IPv6") and ipStr:find('%', 1, true) then @@ -66,27 +59,18 @@ function ip_methods:normal() return net.ntop(self.packed); end -function ip_methods.bits(ip) - return hex.encode(ip.packed):upper():gsub(".", hex2bits); -end - -function ip_methods.bits_full(ip) +-- Returns the longest packed representation, i.e. IPv4 will be mapped +function ip_methods.packed_full(ip) if ip.proto == "IPv4" then ip = ip.toV4mapped; end - return ip.bits; + return ip.packed; end local match; local function commonPrefixLength(ipA, ipB) - ipA, ipB = ipA.bits_full, ipB.bits_full; - for i = 1, 128 do - if ipA:sub(i,i) ~= ipB:sub(i,i) then - return i-1; - end - end - return 128; + return strbit.common_prefix_bits(ipA.packed_full, ipB.packed_full); end -- Instantiate once @@ -238,7 +222,7 @@ function match(ipA, ipB, bits) bits = bits + (128 - 32); end end - return ipA.bits:sub(1, bits) == ipB.bits:sub(1, bits); + return strbit.common_prefix_bits(ipA.packed, ipB.packed) >= bits; end local function is_ip(obj) diff --git a/util/prosodyctl/check.lua b/util/prosodyctl/check.lua index 5e7087c5..8d085c85 100644 --- a/util/prosodyctl/check.lua +++ b/util/prosodyctl/check.lua @@ -735,6 +735,57 @@ local function check(arg) end end + -- Check hostname validity + do + local idna = require "prosody.util.encodings".idna; + local invalid_hosts = {}; + local alabel_hosts = {}; + for host in it.filter("*", pairs(configmanager.getconfig())) do + local _, h, _ = jid_split(host); + if not h or not idna.to_ascii(h) then + table.insert(invalid_hosts, host); + else + for label in h:gmatch("[^%.]+") do + if label:match("^xn%-%-") then + table.insert(alabel_hosts, host); + break; + end + end + end + end + + if #invalid_hosts > 0 then + table.sort(invalid_hosts); + print(""); + print(" Your configuration contains invalid host names:"); + print(" "..table.concat(invalid_hosts, "\n ")); + print(""); + print(" Clients may not be able to log in to these hosts, or you may not be able to"); + print(" communicate with remote servers."); + print(" Use a valid domain name to correct this issue."); + end + + if #alabel_hosts > 0 then + table.sort(alabel_hosts); + print(""); + print(" Your configuration contains incorrectly-encoded hostnames:"); + for _, ahost in ipairs(alabel_hosts) do + print((" '%s' (should be '%s')"):format(ahost, idna.to_unicode(ahost))); + end + print(""); + print(" Clients may not be able to log in to these hosts, or you may not be able to"); + print(" communicate with remote servers."); + print(" To correct this issue, use the Unicode version of the domain in Prosody's config file."); + end + + if #invalid_hosts > 0 or #alabel_hosts > 0 then + print(""); + print("WARNING: Changing the name of a VirtualHost in Prosody's config file"); + print(" WILL NOT migrate any existing data (user accounts, etc.) to the new name."); + ok = false; + end + end + print("Done.\n"); end function checks.dns() diff --git a/util/prosodyctl/shell.lua b/util/prosodyctl/shell.lua index f3279e75..05f81f15 100644 --- a/util/prosodyctl/shell.lua +++ b/util/prosodyctl/shell.lua @@ -83,7 +83,7 @@ local function start(arg) --luacheck: ignore 212/arg for i = 3, #arg do if arg[i]:sub(1, 1) == ":" then table.insert(fmt, i, ")%s("); - elseif i > 3 and fmt[i - 1] == "%q" then + elseif i > 3 and fmt[i - 1]:match("%%q$") then table.insert(fmt, i, ", %q"); else table.insert(fmt, i, "%q"); diff --git a/util/pubsub.lua b/util/pubsub.lua index e089b08c..ccde8b53 100644 --- a/util/pubsub.lua +++ b/util/pubsub.lua @@ -263,7 +263,7 @@ function service:get_default_affiliation(node, actor) --> affiliation if self.config.access_models then local check = self.config.access_models[access_model]; if check then - local aff = check(actor); + local aff = check(actor, node_obj); if aff then return aff; end diff --git a/util/rfc6724.lua b/util/rfc6724.lua deleted file mode 100644 index 33477ed7..00000000 --- a/util/rfc6724.lua +++ /dev/null @@ -1,141 +0,0 @@ --- Prosody IM --- Copyright (C) 2011-2013 Florian Zeitz --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - --- This is used to sort destination addresses by preference --- during S2S connections. --- We can't hand this off to getaddrinfo, since it blocks - -local ip_commonPrefixLength = require"prosody.util.ip".commonPrefixLength - -local function commonPrefixLength(ipA, ipB) - local len = ip_commonPrefixLength(ipA, ipB); - return len < 64 and len or 64; -end - -local function t_sort(t, comp) - for i = 1, (#t - 1) do - for j = (i + 1), #t do - local a, b = t[i], t[j]; - if not comp(a,b) then - t[i], t[j] = b, a; - end - end - end -end - -local function source(dest, candidates) - local function comp(ipA, ipB) - -- Rule 1: Prefer same address - if dest == ipA then - return true; - elseif dest == ipB then - return false; - end - - -- Rule 2: Prefer appropriate scope - if ipA.scope < ipB.scope then - if ipA.scope < dest.scope then - return false; - else - return true; - end - elseif ipA.scope > ipB.scope then - if ipB.scope < dest.scope then - return true; - else - return false; - end - end - - -- Rule 3: Avoid deprecated addresses - -- XXX: No way to determine this - -- Rule 4: Prefer home addresses - -- XXX: Mobility Address related, no way to determine this - -- Rule 5: Prefer outgoing interface - -- XXX: Interface to address relation. No way to determine this - -- Rule 6: Prefer matching label - if ipA.label == dest.label and ipB.label ~= dest.label then - return true; - elseif ipB.label == dest.label and ipA.label ~= dest.label then - return false; - end - - -- Rule 7: Prefer temporary addresses (over public ones) - -- XXX: No way to determine this - -- Rule 8: Use longest matching prefix - if commonPrefixLength(ipA, dest) > commonPrefixLength(ipB, dest) then - return true; - else - return false; - end - end - - t_sort(candidates, comp); - return candidates[1]; -end - -local function destination(candidates, sources) - local sourceAddrs = {}; - local function comp(ipA, ipB) - local ipAsource = sourceAddrs[ipA]; - local ipBsource = sourceAddrs[ipB]; - -- Rule 1: Avoid unusable destinations - -- XXX: No such information - -- Rule 2: Prefer matching scope - if ipA.scope == ipAsource.scope and ipB.scope ~= ipBsource.scope then - return true; - elseif ipA.scope ~= ipAsource.scope and ipB.scope == ipBsource.scope then - return false; - end - - -- Rule 3: Avoid deprecated addresses - -- XXX: No way to determine this - -- Rule 4: Prefer home addresses - -- XXX: Mobility Address related, no way to determine this - -- Rule 5: Prefer matching label - if ipAsource.label == ipA.label and ipBsource.label ~= ipB.label then - return true; - elseif ipBsource.label == ipB.label and ipAsource.label ~= ipA.label then - return false; - end - - -- Rule 6: Prefer higher precedence - if ipA.precedence > ipB.precedence then - return true; - elseif ipA.precedence < ipB.precedence then - return false; - end - - -- Rule 7: Prefer native transport - -- XXX: No way to determine this - -- Rule 8: Prefer smaller scope - if ipA.scope < ipB.scope then - return true; - elseif ipA.scope > ipB.scope then - return false; - end - - -- Rule 9: Use longest matching prefix - if commonPrefixLength(ipA, ipAsource) > commonPrefixLength(ipB, ipBsource) then - return true; - elseif commonPrefixLength(ipA, ipAsource) < commonPrefixLength(ipB, ipBsource) then - return false; - end - - -- Rule 10: Otherwise, leave order unchanged - return true; - end - for _, ip in ipairs(candidates) do - sourceAddrs[ip] = source(ip, sources); - end - - t_sort(candidates, comp); - return candidates; -end - -return {source = source, - destination = destination}; diff --git a/util/startup.lua b/util/startup.lua index 0066fb8c..507c0528 100644 --- a/util/startup.lua +++ b/util/startup.lua @@ -392,6 +392,8 @@ function startup.load_secondary_libraries() require "prosody.util.stanza" require "prosody.util.jid" + + prosody.features = require "prosody.core.features".available; end function startup.init_http_client() @@ -529,21 +531,30 @@ function startup.force_console_logging() config.set("*", "log", { { levels = { min = log_level or "info" }, to = "console" } }); end +local function check_posix() + if prosody.platform ~= "posix" then return end + + local want_pposix_version = "0.4.0"; + local have_pposix, pposix = pcall(require, "prosody.util.pposix"); + + if pposix._VERSION ~= want_pposix_version then + print(string.format("Unknown version (%s) of binary pposix module, expected %s", + tostring(pposix._VERSION), want_pposix_version)); + os.exit(1); + end + if have_pposix and pposix then + return pposix; + end +end + function startup.switch_user() -- Switch away from root and into the prosody user -- -- NOTE: This function is only used by prosodyctl. -- The prosody process is built with the assumption that -- it is already started as the appropriate user. - local want_pposix_version = "0.4.0"; - local have_pposix, pposix = pcall(require, "prosody.util.pposix"); - - if have_pposix and pposix then - if pposix._VERSION ~= want_pposix_version then - print(string.format("Unknown version (%s) of binary pposix module, expected %s", - tostring(pposix._VERSION), want_pposix_version)); - os.exit(1); - end + local pposix = check_posix() + if pposix then prosody.current_uid = pposix.getuid(); local arg_root = prosody.opts.root; if prosody.current_uid == 0 and config.get("*", "run_as_root") ~= true and not arg_root then @@ -669,6 +680,168 @@ function startup.make_dummy_hosts() end end +function startup.posix_umask() + if prosody.platform ~= "posix" then return end + local pposix = require "prosody.util.pposix"; + local umask = config.get("*", "umask") or "027"; + pposix.umask(umask); +end + +function startup.check_user() + local pposix = check_posix(); + if not pposix then return end + -- Don't even think about it! + if pposix.getuid() == 0 and not config.get("*", "run_as_root") then + print("Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!"); + print("For more information on running Prosody as root, see https://prosody.im/doc/root"); + os.exit(1); -- Refusing to run as root + end +end + +local function remove_pidfile() + local pidfile = prosody.pidfile; + if prosody.pidfile_handle then + prosody.pidfile_handle:close(); + os.remove(pidfile); + prosody.pidfile, prosody.pidfile_handle = nil, nil; + end +end + +function startup.write_pidfile() + local pposix = check_posix(); + if not pposix then return end + local lfs = require "lfs"; + local stat = lfs.attributes; + local pidfile = config.get("*", "pidfile") or nil; + if not pidfile then return end + pidfile = config.resolve_relative_path(prosody.paths.data, pidfile); + local mode = stat(pidfile) and "r+" or "w+"; + local pidfile_handle, err = io.open(pidfile, mode); + if not pidfile_handle then + log("error", "Couldn't write pidfile at %s; %s", pidfile, err); + os.exit(1); + else + prosody.pidfile = pidfile; + if not lfs.lock(pidfile_handle, "w") then -- Exclusive lock + local other_pid = pidfile_handle:read("*a"); + log("error", "Another Prosody instance seems to be running with PID %s, quitting", other_pid); + prosody.pidfile_handle = nil; + os.exit(1); + else + pidfile_handle:close(); + pidfile_handle, err = io.open(pidfile, "w+"); + if not pidfile_handle then + log("error", "Couldn't write pidfile at %s; %s", pidfile, err); + os.exit(1); + else + if lfs.lock(pidfile_handle, "w") then + pidfile_handle:write(tostring(pposix.getpid())); + pidfile_handle:flush(); + prosody.pidfile_handle = pidfile_handle; + end + end + end + end + prosody.events.add_handler("server-stopped", remove_pidfile); +end + +local function remove_log_sinks() + local lm = require "prosody.core.loggingmanager"; + lm.register_sink_type("console", nil); + lm.register_sink_type("stdout", nil); + lm.reload_logging(); +end + +function startup.posix_daemonize() + if not prosody.opts.daemonize then return end + local pposix = check_posix(); + log("info", "Prosody is about to detach from the console, disabling further console output"); + remove_log_sinks(); + local ok, ret = pposix.daemonize(); + if not ok then + log("error", "Failed to daemonize: %s", ret); + elseif ret and ret > 0 then + os.exit(0); + else + log("info", "Successfully daemonized to PID %d", pposix.getpid()); + end +end + +function startup.hook_posix_signals() + if prosody.platform ~= "posix" then return end + local have_signal, signal = pcall(require, "prosody.util.signal"); + if not have_signal then + log("warn", "Couldn't load signal library, won't respond to SIGTERM"); + return + end + signal.signal("SIGTERM", function() + log("warn", "Received SIGTERM"); + prosody.main_thread:run(function() + prosody.unlock_globals(); + prosody.shutdown("Received SIGTERM"); + prosody.lock_globals(); + end); + end); + + signal.signal("SIGHUP", function() + log("info", "Received SIGHUP"); + prosody.main_thread:run(function() prosody.reload_config(); end); + -- this also reloads logging + end); + + signal.signal("SIGINT", function() + log("info", "Received SIGINT"); + prosody.main_thread:run(function() + prosody.unlock_globals(); + prosody.shutdown("Received SIGINT"); + prosody.lock_globals(); + end); + end); + + signal.signal("SIGUSR1", function() + log("info", "Received SIGUSR1"); + prosody.events.fire_event("signal/SIGUSR1"); + end); + + signal.signal("SIGUSR2", function() + log("info", "Received SIGUSR2"); + prosody.events.fire_event("signal/SIGUSR2"); + end); +end + +function startup.systemd_notify() + local notify_socket_name = os.getenv("NOTIFY_SOCKET"); + if not notify_socket_name then return end + local have_unix, unix = pcall(require, "socket.unix"); + if not have_unix or type(unix) ~= "table" then + log("error", "LuaSocket without UNIX socket support, can't notify systemd.") + return os.exit(1); + end + log("debug", "Will notify on socket %q", notify_socket_name); + notify_socket_name = notify_socket_name:gsub("^@", "\0"); + local notify_socket = unix.dgram(); + local ok, err = notify_socket:setpeername(notify_socket_name); + if not ok then + log("error", "Could not connect to systemd notification socket %q: %q", notify_socket_name, err); + return os.exit(1); + end + local time = require "prosody.util.time"; + + prosody.notify_socket = notify_socket; + prosody.events.add_handler("server-started", function() + notify_socket:send("READY=1"); + end); + prosody.events.add_handler("reloading-config", function() + notify_socket:send(string.format("RELOADING=1\nMONOTONIC_USEC=%d", math.floor(time.monotonic() * 1000000))); + end); + prosody.events.add_handler("config-reloaded", function() + notify_socket:send("READY=1"); + end); + prosody.events.add_handler("server-stopping", function() + notify_socket:send("STOPPING=1"); + end); +end + function startup.cleanup() prosody.log("info", "Shutdown status: Cleaning up"); prosody.events.fire_event("server-cleanup"); @@ -724,6 +897,7 @@ function startup.prosody() startup.parse_args(); startup.init_global_state(); startup.read_config(); + startup.check_user(); startup.init_logging(); startup.init_gc(); startup.init_errors(); @@ -746,6 +920,10 @@ function startup.prosody() startup.init_http_client(); startup.init_data_store(); startup.init_global_protection(); + startup.posix_daemonize(); + startup.write_pidfile(); + startup.hook_posix_signals(); + startup.systemd_notify(); startup.prepare_to_start(); startup.notify_started(); end |