aboutsummaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/adhoc/adhoc.lib.lua14
-rw-r--r--plugins/adhoc/mod_adhoc.lua45
-rw-r--r--plugins/mod_admin_adhoc.lua98
-rw-r--r--plugins/mod_admin_shell.lua1245
-rw-r--r--plugins/mod_admin_socket.lua8
-rw-r--r--plugins/mod_admin_telnet.lua4
-rw-r--r--plugins/mod_announce.lua96
-rw-r--r--plugins/mod_auth_anonymous.lua4
-rw-r--r--plugins/mod_auth_insecure.lua10
-rw-r--r--plugins/mod_auth_internal_hashed.lua70
-rw-r--r--plugins/mod_auth_internal_plain.lua30
-rw-r--r--plugins/mod_auth_ldap.lua32
-rw-r--r--plugins/mod_authz_internal.lua347
-rw-r--r--plugins/mod_blocklist.lua61
-rw-r--r--plugins/mod_bookmarks.lua6
-rw-r--r--plugins/mod_bosh.lua32
-rw-r--r--plugins/mod_c2s.lua71
-rw-r--r--plugins/mod_carbons.lua6
-rw-r--r--plugins/mod_component.lua46
-rw-r--r--plugins/mod_cron.lua64
-rw-r--r--plugins/mod_csi.lua37
-rw-r--r--plugins/mod_csi_simple.lua21
-rw-r--r--plugins/mod_debug_reset.lua36
-rw-r--r--plugins/mod_debug_stanzas/watcher.lib.lua220
-rw-r--r--plugins/mod_dialback.lua12
-rw-r--r--plugins/mod_disco.lua21
-rw-r--r--plugins/mod_external_services.lua20
-rw-r--r--plugins/mod_groups.lua4
-rw-r--r--plugins/mod_http.lua94
-rw-r--r--plugins/mod_http_errors.lua12
-rw-r--r--plugins/mod_http_file_share.lua108
-rw-r--r--plugins/mod_http_files.lua10
-rw-r--r--plugins/mod_http_openmetrics.lua4
-rw-r--r--plugins/mod_invites.lua203
-rw-r--r--plugins/mod_invites_adhoc.lua74
-rw-r--r--plugins/mod_invites_register.lua23
-rw-r--r--plugins/mod_iq.lua2
-rw-r--r--plugins/mod_lastactivity.lua8
-rw-r--r--plugins/mod_legacyauth.lua10
-rw-r--r--plugins/mod_limits.lua8
-rw-r--r--plugins/mod_mam/mamprefs.lib.lua11
-rw-r--r--plugins/mod_mam/mamprefsxml.lib.lua4
-rw-r--r--plugins/mod_mam/mod_mam.lua60
-rw-r--r--plugins/mod_message.lua8
-rw-r--r--plugins/mod_mimicking.lua8
-rw-r--r--plugins/mod_motd.lua2
-rw-r--r--plugins/mod_muc_mam.lua44
-rw-r--r--plugins/mod_muc_unique.lua4
-rw-r--r--plugins/mod_net_multiplex.lua8
-rw-r--r--plugins/mod_offline.lua4
-rw-r--r--plugins/mod_pep.lua58
-rw-r--r--plugins/mod_pep_simple.lua12
-rw-r--r--plugins/mod_ping.lua2
-rw-r--r--plugins/mod_posix.lua163
-rw-r--r--plugins/mod_presence.lua14
-rw-r--r--plugins/mod_private.lua2
-rw-r--r--plugins/mod_proxy65.lua10
-rw-r--r--plugins/mod_pubsub/mod_pubsub.lua101
-rw-r--r--plugins/mod_pubsub/pubsub.lib.lua46
-rw-r--r--plugins/mod_register_ibr.lua31
-rw-r--r--plugins/mod_register_limits.lua16
-rw-r--r--plugins/mod_roster.lua16
-rw-r--r--plugins/mod_s2s.lua165
-rw-r--r--plugins/mod_s2s_auth_certs.lua30
-rw-r--r--plugins/mod_s2s_auth_dane_in.lua130
-rw-r--r--plugins/mod_s2s_bidi.lua13
-rw-r--r--plugins/mod_saslauth.lua154
-rw-r--r--plugins/mod_scansion_record.lua10
-rw-r--r--plugins/mod_server_contact_info.lua41
-rw-r--r--plugins/mod_server_info.lua55
-rw-r--r--plugins/mod_smacks.lua382
-rw-r--r--plugins/mod_stanza_debug.lua2
-rw-r--r--plugins/mod_storage_internal.lua238
-rw-r--r--plugins/mod_storage_memory.lua14
-rw-r--r--plugins/mod_storage_sql.lua253
-rw-r--r--plugins/mod_storage_xep0227.lua30
-rw-r--r--plugins/mod_time.lua21
-rw-r--r--plugins/mod_tls.lua31
-rw-r--r--plugins/mod_tokenauth.lua350
-rw-r--r--plugins/mod_tombstones.lua12
-rw-r--r--plugins/mod_turn_external.lua8
-rw-r--r--plugins/mod_uptime.lua2
-rw-r--r--plugins/mod_user_account_management.lua181
-rw-r--r--plugins/mod_vcard.lua4
-rw-r--r--plugins/mod_vcard4.lua4
-rw-r--r--plugins/mod_vcard_legacy.lua8
-rw-r--r--plugins/mod_version.lua11
-rw-r--r--plugins/mod_watchregistrations.lua6
-rw-r--r--plugins/mod_websocket.lua32
-rw-r--r--plugins/mod_welcome.lua2
-rw-r--r--plugins/muc/hats.lib.lua19
-rw-r--r--plugins/muc/hidden.lib.lua8
-rw-r--r--plugins/muc/history.lib.lua6
-rw-r--r--plugins/muc/lock.lib.lua4
-rw-r--r--plugins/muc/members_only.lib.lua2
-rw-r--r--plugins/muc/mod_muc.lua98
-rw-r--r--plugins/muc/muc.lib.lua39
-rw-r--r--plugins/muc/occupant.lib.lua2
-rw-r--r--plugins/muc/occupant_id.lib.lua6
-rw-r--r--plugins/muc/password.lib.lua2
-rw-r--r--plugins/muc/persistent.lib.lua11
-rw-r--r--plugins/muc/presence_broadcast.lib.lua2
-rw-r--r--plugins/muc/register.lib.lua12
-rw-r--r--plugins/muc/request.lib.lua6
-rw-r--r--plugins/muc/restrict_pm.lib.lua119
-rw-r--r--plugins/muc/subject.lib.lua4
106 files changed, 4559 insertions, 1810 deletions
diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua
index 4cf6911d..0ce45e19 100644
--- a/plugins/adhoc/adhoc.lib.lua
+++ b/plugins/adhoc/adhoc.lib.lua
@@ -4,7 +4,7 @@
-- COPYING file in the source package for more information.
--
-local st, uuid = require "util.stanza", require "util.uuid";
+local st, uuid = require "prosody.util.stanza", require "prosody.util.uuid";
local xmlns_cmd = "http://jabber.org/protocol/commands";
@@ -23,10 +23,16 @@ end
function _M.new(name, node, handler, permission)
if not permission then
error "adhoc.new() expects a permission argument, none given"
- end
- if permission == "user" then
+ elseif permission == "user" then
error "the permission mode 'user' has been renamed 'any', please update your code"
end
+ if permission == "admin" then
+ module:default_permission("prosody:admin", "adhoc:"..node);
+ permission = "check";
+ elseif permission == "global_admin" then
+ module:default_permission("prosody:operator", "adhoc:"..node);
+ permission = "check";
+ end
return { name = name, node = node, handler = handler, cmdtag = _cmdtag, permission = permission };
end
@@ -34,6 +40,8 @@ function _M.handle_cmd(command, origin, stanza)
local cmdtag = stanza.tags[1]
local sessionid = cmdtag.attr.sessionid or uuid.generate();
local dataIn = {
+ origin = origin;
+ stanza = stanza;
to = stanza.attr.to;
from = stanza.attr.from;
action = cmdtag.attr.action or "execute";
diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua
index 09a72075..8abfff99 100644
--- a/plugins/adhoc/mod_adhoc.lua
+++ b/plugins/adhoc/mod_adhoc.lua
@@ -5,28 +5,26 @@
-- COPYING file in the source package for more information.
--
-local it = require "util.iterators";
-local st = require "util.stanza";
-local is_admin = require "core.usermanager".is_admin;
-local jid_host = require "util.jid".host;
+local it = require "prosody.util.iterators";
+local st = require "prosody.util.stanza";
+local jid_host = require "prosody.util.jid".host;
local adhoc_handle_cmd = module:require "adhoc".handle_cmd;
local xmlns_cmd = "http://jabber.org/protocol/commands";
local commands = {};
module:add_feature(xmlns_cmd);
+local function check_permissions(event, node, command, execute)
+ return (command.permission == "check" and module:may("adhoc:"..node, event, not execute))
+ or (command.permission == "local_user" and jid_host(event.stanza.attr.from) == module.host)
+ or (command.permission == "any");
+end
+
module:hook("host-disco-info-node", function (event)
local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
if commands[node] then
- local from = stanza.attr.from;
- local privileged = is_admin(from, stanza.attr.to);
- local global_admin = is_admin(from);
- local hostname = jid_host(from);
local command = commands[node];
- if (command.permission == "admin" and privileged)
- or (command.permission == "global_admin" and global_admin)
- or (command.permission == "local_user" and hostname == module.host)
- or (command.permission == "any") then
+ if check_permissions(event, node, command) then
reply:tag("identity", { name = command.name,
category = "automation", type = "command-node" }):up();
reply:tag("feature", { var = xmlns_cmd }):up();
@@ -44,20 +42,13 @@ module:hook("host-disco-info-node", function (event)
end);
module:hook("host-disco-items-node", function (event)
- local stanza, reply, disco_node = event.stanza, event.reply, event.node;
+ local reply, disco_node = event.reply, event.node;
if disco_node ~= xmlns_cmd then
return;
end
- local from = stanza.attr.from;
- local admin = is_admin(from, stanza.attr.to);
- local global_admin = is_admin(from);
- local hostname = jid_host(from);
for node, command in it.sorted_pairs(commands) do
- if (command.permission == "admin" and admin)
- or (command.permission == "global_admin" and global_admin)
- or (command.permission == "local_user" and hostname == module.host)
- or (command.permission == "any") then
+ if check_permissions(event, node, command) then
reply:tag("item", { name = command.name,
node = node, jid = module:get_host() });
reply:up();
@@ -71,20 +62,14 @@ module:hook("iq-set/host/"..xmlns_cmd..":command", function (event)
local node = stanza.tags[1].attr.node
local command = commands[node];
if command then
- local from = stanza.attr.from;
- local admin = is_admin(from, stanza.attr.to);
- local global_admin = is_admin(from);
- local hostname = jid_host(from);
- if (command.permission == "admin" and not admin)
- or (command.permission == "global_admin" and not global_admin)
- or (command.permission == "local_user" and hostname ~= module.host) then
+ if not check_permissions(event, node, command, true) then
origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up()
- :add_child(commands[node]:cmdtag("canceled")
+ :add_child(command:cmdtag("canceled")
:tag("note", {type="error"}):text("You don't have permission to execute this command")));
return true
end
-- User has permission now execute the command
- adhoc_handle_cmd(commands[node], origin, stanza);
+ adhoc_handle_cmd(command, origin, stanza);
return true;
end
end, 500);
diff --git a/plugins/mod_admin_adhoc.lua b/plugins/mod_admin_adhoc.lua
index 5a565fb3..ca84f975 100644
--- a/plugins/mod_admin_adhoc.lua
+++ b/plugins/mod_admin_adhoc.lua
@@ -14,23 +14,25 @@ local t_sort = table.sort;
local module_host = module:get_host();
-local keys = require "util.iterators".keys;
-local usermanager_user_exists = require "core.usermanager".user_exists;
-local usermanager_create_user = require "core.usermanager".create_user;
-local usermanager_delete_user = require "core.usermanager".delete_user;
-local usermanager_set_password = require "core.usermanager".set_password;
-local hostmanager_activate = require "core.hostmanager".activate;
-local hostmanager_deactivate = require "core.hostmanager".deactivate;
-local rm_load_roster = require "core.rostermanager".load_roster;
-local st, jid = require "util.stanza", require "util.jid";
-local timer_add_task = require "util.timer".add_task;
-local dataforms_new = require "util.dataforms".new;
-local array = require "util.array";
-local modulemanager = require "core.modulemanager";
+local keys = require "prosody.util.iterators".keys;
+local usermanager_user_exists = require "prosody.core.usermanager".user_exists;
+local usermanager_create_user = require "prosody.core.usermanager".create_user;
+local usermanager_delete_user = require "prosody.core.usermanager".delete_user;
+local usermanager_disable_user = require "prosody.core.usermanager".disable_user;
+local usermanager_enable_user = require "prosody.core.usermanager".enable_user;
+local usermanager_set_password = require "prosody.core.usermanager".set_password;
+local hostmanager_activate = require "prosody.core.hostmanager".activate;
+local hostmanager_deactivate = require "prosody.core.hostmanager".deactivate;
+local rm_load_roster = require "prosody.core.rostermanager".load_roster;
+local st, jid = require "prosody.util.stanza", require "prosody.util.jid";
+local timer_add_task = require "prosody.util.timer".add_task;
+local dataforms_new = require "prosody.util.dataforms".new;
+local array = require "prosody.util.array";
+local modulemanager = require "prosody.core.modulemanager";
local core_post_stanza = prosody.core_post_stanza;
-local adhoc_simple = require "util.adhoc".new_simple_form;
-local adhoc_initial = require "util.adhoc".new_initial_data_form;
-local set = require"util.set";
+local adhoc_simple = require "prosody.util.adhoc".new_simple_form;
+local adhoc_initial = require "prosody.util.adhoc".new_initial_data_form;
+local set = require"prosody.util.set";
module:depends("adhoc");
local adhoc_new = module:require "adhoc".new;
@@ -152,6 +154,66 @@ local delete_user_command_handler = adhoc_simple(delete_user_layout, function(fi
"The following accounts could not be deleted:\n"..t_concat(failed, "\n") or "") };
end);
+local disable_user_layout = dataforms_new{
+ title = "Disabling a User";
+ instructions = "Fill out this form to disable a user.";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
+ { name = "accountjids", type = "jid-multi", required = true, label = "The Jabber ID(s) to disable" };
+};
+
+local disable_user_command_handler = adhoc_simple(disable_user_layout, function(fields, err, data)
+ if err then
+ return generate_error_message(err);
+ end
+ local failed = {};
+ local succeeded = {};
+ for _, aJID in ipairs(fields.accountjids) do
+ local username, host = jid.split(aJID);
+ if (host == module_host) and usermanager_user_exists(username, host) and usermanager_disable_user(username, host) then
+ module:log("info", "User %s has been disabled by %s", aJID, jid.bare(data.from));
+ succeeded[#succeeded+1] = aJID;
+ else
+ module:log("debug", "Tried to disable non-existent user %s", aJID);
+ failed[#failed+1] = aJID;
+ end
+ end
+ return {status = "completed", info = (#succeeded ~= 0 and
+ "The following accounts were successfully disabled:\n"..t_concat(succeeded, "\n").."\n" or "")..
+ (#failed ~= 0 and
+ "The following accounts could not be disabled:\n"..t_concat(failed, "\n") or "") };
+end);
+
+local enable_user_layout = dataforms_new{
+ title = "Re-Enable a User";
+ instructions = "Fill out this form to enable a user.";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
+ { name = "accountjids", type = "jid-multi", required = true, label = "The Jabber ID(s) to re-enable" };
+};
+
+local enable_user_command_handler = adhoc_simple(enable_user_layout, function(fields, err, data)
+ if err then
+ return generate_error_message(err);
+ end
+ local failed = {};
+ local succeeded = {};
+ for _, aJID in ipairs(fields.accountjids) do
+ local username, host = jid.split(aJID);
+ if (host == module_host) and usermanager_user_exists(username, host) and usermanager_enable_user(username, host) then
+ module:log("info", "User %s has been enabled by %s", aJID, jid.bare(data.from));
+ succeeded[#succeeded+1] = aJID;
+ else
+ module:log("debug", "Tried to enable non-existent user %s", aJID);
+ failed[#failed+1] = aJID;
+ end
+ end
+ return {status = "completed", info = (#succeeded ~= 0 and
+ "The following accounts were successfully enabled:\n"..t_concat(succeeded, "\n").."\n" or "")..
+ (#failed ~= 0 and
+ "The following accounts could not be enabled:\n"..t_concat(failed, "\n") or "") };
+end);
+
-- Ending a user's session
local function disconnect_user(match_jid)
local node, hostname, givenResource = jid.split(match_jid);
@@ -804,6 +866,8 @@ local add_user_desc = adhoc_new("Add User", "http://jabber.org/protocol/admin#ad
local change_user_password_desc = adhoc_new("Change User Password", "http://jabber.org/protocol/admin#change-user-password", change_user_password_command_handler, "admin");
local config_reload_desc = adhoc_new("Reload configuration", "http://prosody.im/protocol/config#reload", config_reload_handler, "global_admin");
local delete_user_desc = adhoc_new("Delete User", "http://jabber.org/protocol/admin#delete-user", delete_user_command_handler, "admin");
+local disable_user_desc = adhoc_new("Disable User", "http://jabber.org/protocol/admin#disable-user", disable_user_command_handler, "admin");
+local enable_user_desc = adhoc_new("Re-Enable User", "http://jabber.org/protocol/admin#reenable-user", enable_user_command_handler, "admin");
local end_user_session_desc = adhoc_new("End User Session", "http://jabber.org/protocol/admin#end-user-session", end_user_session_handler, "admin");
local get_user_roster_desc = adhoc_new("Get User Roster","http://jabber.org/protocol/admin#get-user-roster", get_user_roster_handler, "admin");
local get_user_stats_desc = adhoc_new("Get User Statistics","http://jabber.org/protocol/admin#user-stats", get_user_stats_handler, "admin");
@@ -824,6 +888,8 @@ module:provides("adhoc", add_user_desc);
module:provides("adhoc", change_user_password_desc);
module:provides("adhoc", config_reload_desc);
module:provides("adhoc", delete_user_desc);
+module:provides("adhoc", disable_user_desc);
+module:provides("adhoc", enable_user_desc);
module:provides("adhoc", end_user_session_desc);
module:provides("adhoc", get_user_roster_desc);
module:provides("adhoc", get_user_stats_desc);
diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua
index f2da286b..e6b44f00 100644
--- a/plugins/mod_admin_shell.lua
+++ b/plugins/mod_admin_shell.lua
@@ -10,38 +10,41 @@
module:set_global();
module:depends("admin_socket");
-local hostmanager = require "core.hostmanager";
-local modulemanager = require "core.modulemanager";
-local s2smanager = require "core.s2smanager";
-local portmanager = require "core.portmanager";
-local helpers = require "util.helpers";
-local server = require "net.server";
-local st = require "util.stanza";
+local hostmanager = require "prosody.core.hostmanager";
+local modulemanager = require "prosody.core.modulemanager";
+local s2smanager = require "prosody.core.s2smanager";
+local portmanager = require "prosody.core.portmanager";
+local helpers = require "prosody.util.helpers";
+local it = require "prosody.util.iterators";
+local server = require "prosody.net.server";
+local schema = require "prosody.util.jsonschema";
+local st = require "prosody.util.stanza";
local _G = _G;
local prosody = _G.prosody;
-local unpack = table.unpack or unpack; -- luacheck: ignore 113
-local iterators = require "util.iterators";
+local unpack = table.unpack;
+local iterators = require "prosody.util.iterators";
local keys, values = iterators.keys, iterators.values;
-local jid_bare, jid_split, jid_join, jid_compare = import("util.jid", "bare", "prepped_split", "join", "compare");
-local set, array = require "util.set", require "util.array";
-local cert_verify_identity = require "util.x509".verify_identity;
-local envload = require "util.envload".envload;
-local envloadfile = require "util.envload".envloadfile;
-local has_pposix, pposix = pcall(require, "util.pposix");
-local async = require "util.async";
-local serialization = require "util.serialization";
+local jid_bare, jid_split, jid_join, jid_resource, jid_compare = import("prosody.util.jid", "bare", "prepped_split", "join", "resource", "compare");
+local set, array = require "prosody.util.set", require "prosody.util.array";
+local cert_verify_identity = require "prosody.util.x509".verify_identity;
+local envload = require "prosody.util.envload".envload;
+local envloadfile = require "prosody.util.envload".envloadfile;
+local has_pposix, pposix = pcall(require, "prosody.util.pposix");
+local async = require "prosody.util.async";
+local serialization = require "prosody.util.serialization";
local serialize_config = serialization.new ({ fatal = false, unquoted = true});
-local time = require "util.time";
-local promise = require "util.promise";
+local time = require "prosody.util.time";
+local promise = require "prosody.util.promise";
+local logger = require "prosody.util.logger";
local t_insert = table.insert;
local t_concat = table.concat;
-local format_number = require "util.human.units".format;
-local format_table = require "util.human.io".table;
+local format_number = require "prosody.util.human.units".format;
+local format_table = require "prosody.util.human.io".table;
local function capitalize(s)
if not s then return end
@@ -62,6 +65,86 @@ local commands = module:shared("commands")
local def_env = module:shared("env");
local default_env_mt = { __index = def_env };
+local function new_section(section_desc)
+ return setmetatable({}, {
+ help = {
+ desc = section_desc;
+ commands = {};
+ };
+ });
+end
+
+local help_topics = {};
+local function help_topic(name)
+ return function (desc)
+ return function (content)
+ help_topics[name] = {
+ desc = desc;
+ content = content;
+ };
+ end;
+ end
+end
+
+-- Seed with default sections and their description text
+help_topic "console" "Help regarding the console itself" [[
+Hey! Welcome to Prosody's admin console.
+First thing, if you're ever wondering how to get out, simply type 'quit'.
+Secondly, note that we don't support the full telnet protocol yet (it's coming)
+so you may have trouble using the arrow keys, etc. depending on your system.
+
+For now we offer a couple of handy shortcuts:
+!! - Repeat the last command
+!old!new! - repeat the last command, but with 'old' replaced by 'new'
+
+For those well-versed in Prosody's internals, or taking instruction from those who are,
+you can prefix a command with > to escape the console sandbox, and access everything in
+the running server. Great fun, but be careful not to break anything :)
+]];
+
+local available_columns; --forward declaration so it is reachable from the help
+
+help_topic "columns" "Information about customizing session listings" (function (self, print)
+ print [[The columns shown by c2s:show() and s2s:show() can be customizied via the]]
+ print [['columns' argument as described here.]]
+ print [[]]
+ print [[Columns can be specified either as "id jid ipv" or as {"id", "jid", "ipv"}.]]
+ print [[Available columns are:]]
+ local meta_columns = {
+ { title = "ID"; width = 5 };
+ { title = "Column Title"; width = 12 };
+ { title = "Description"; width = 12 };
+ };
+ -- auto-adjust widths
+ for column, spec in pairs(available_columns) do
+ meta_columns[1].width = math.max(meta_columns[1].width or 0, #column);
+ meta_columns[2].width = math.max(meta_columns[2].width or 0, #(spec.title or ""));
+ meta_columns[3].width = math.max(meta_columns[3].width or 0, #(spec.description or ""));
+ end
+ local row = format_table(meta_columns, self.session.width)
+ print(row());
+ for column, spec in iterators.sorted_pairs(available_columns) do
+ print(row({ column, spec.title, spec.description }));
+ end
+ print [[]]
+ print [[Most fields on the internal session structures can also be used as columns]]
+ -- Also, you can pass a table column specification directly, with mapper callback and all
+end);
+
+help_topic "roles" "Show information about user roles" [[
+Roles may grant access or restrict users from certain operations.
+
+Built-in roles are:
+ prosody:guest - Guest/anonymous user
+ prosody:registered - Registered user
+ prosody:member - Provisioned user
+ prosody:admin - Host administrator
+ prosody:operator - Server administrator
+
+Roles can be assigned using the user management commands (see 'help user').
+]];
+
+
local function redirect_output(target, session)
local env = setmetatable({ print = session.print }, { __index = function (_, k) return rawget(target, k); end });
env.dofile = function(name)
@@ -83,8 +166,8 @@ function runner_callbacks:error(err)
self.data.print("Error: "..tostring(err));
end
-local function send_repl_output(session, line)
- return session.send(st.stanza("repl-output"):text(tostring(line)));
+local function send_repl_output(session, line, attr)
+ return session.send(st.stanza("repl-output", attr):text(tostring(line)));
end
function console:new_session(admin_session)
@@ -99,8 +182,14 @@ function console:new_session(admin_session)
end
return send_repl_output(admin_session, table.concat(t, "\t"));
end;
+ write = function (t)
+ return send_repl_output(admin_session, t, { eol = "0" });
+ end;
serialize = tostring;
disconnect = function () admin_session:close(); end;
+ is_connected = function ()
+ return not not admin_session.conn;
+ end
};
session.env = setmetatable({}, default_env_mt);
@@ -126,6 +215,11 @@ local function handle_line(event)
session = console:new_session(event.origin);
event.origin.shell_session = session;
end
+
+ local default_width = 132; -- The common default of 80 is a bit too narrow for e.g. s2s:show(), 132 was another common width for hardware terminals
+ local margin = 2; -- To account for '| ' when lines are printed
+ session.width = (tonumber(event.stanza.attr.width) or default_width)-margin;
+
local line = event.stanza:get_text();
local useglobalenv;
@@ -135,7 +229,7 @@ local function handle_line(event)
line = line:gsub("^>", "");
useglobalenv = true;
else
- local command = line:match("^%w+") or line:match("%p");
+ local command = line:match("^(%w+) ") or line:match("^%w+$") or line:match("%p");
if commands[command] then
commands[command](session, line);
event.origin.send(result);
@@ -201,148 +295,50 @@ module:hook("admin/repl-input", function (event)
return true;
end);
+local function describe_command(s)
+ local section, name, args, desc = s:match("^([%w_]+):([%w_]+)%(([^)]*)%) %- (.+)$");
+ if not section then
+ error("Failed to parse command description: "..s);
+ end
+ local command_help = getmetatable(def_env[section]).help.commands;
+ command_help[name] = {
+ desc = desc;
+ args = array.collect(args:gmatch("[%w_]+")):map(function (arg_name)
+ return { name = arg_name };
+ end);
+ };
+end
+
-- Console commands --
-- These are simple commands, not valid standalone in Lua
-local available_columns; --forward declaration so it is reachable from the help
-
+-- Help about individual topics is handled by def_env.help
function commands.help(session, data)
local print = session.print;
- local section = data:match("^help (%w+)");
- if not section then
- print [[Commands are divided into multiple sections. For help on a particular section, ]]
- print [[type: help SECTION (for example, 'help c2s'). Sections are: ]]
- print [[]]
- local row = format_table({ { title = "Section"; width = 7 }; { title = "Description"; width = "100%" } })
- print(row())
- print(row { "c2s"; "Commands to manage local client-to-server sessions" })
- print(row { "s2s"; "Commands to manage sessions between this server and others" })
- print(row { "http"; "Commands to inspect HTTP services" }) -- XXX plural but there is only one so far
- print(row { "module"; "Commands to load/reload/unload modules/plugins" })
- print(row { "host"; "Commands to activate, deactivate and list virtual hosts" })
- print(row { "user"; "Commands to create and delete users, and change their passwords" })
- print(row { "roles"; "Show information about user roles" })
- print(row { "muc"; "Commands to create, list and manage chat rooms" })
- print(row { "stats"; "Commands to show internal statistics" })
- print(row { "server"; "Uptime, version, shutting down, etc." })
- print(row { "port"; "Commands to manage ports the server is listening on" })
- print(row { "dns"; "Commands to manage and inspect the internal DNS resolver" })
- print(row { "xmpp"; "Commands for sending XMPP stanzas" })
- print(row { "debug"; "Commands for debugging the server" })
- print(row { "config"; "Reloading the configuration, etc." })
- print(row { "columns"; "Information about customizing session listings" })
- print(row { "console"; "Help regarding the console itself" })
- elseif section == "c2s" then
- print [[c2s:show(jid, columns) - Show all client sessions with the specified JID (or all if no JID given)]]
- print [[c2s:show_tls(jid) - Show TLS cipher info for encrypted sessions]]
- print [[c2s:count() - Count sessions without listing them]]
- print [[c2s:close(jid) - Close all sessions for the specified JID]]
- print [[c2s:closeall() - Close all active c2s connections ]]
- elseif section == "s2s" then
- print [[s2s:show(domain, columns) - Show all s2s connections for the given domain (or all if no domain given)]]
- print [[s2s:show_tls(domain) - Show TLS cipher info for encrypted sessions]]
- print [[s2s:close(from, to) - Close a connection from one domain to another]]
- print [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]]
- elseif section == "http" then
- print [[http:list(hosts) - Show HTTP endpoints]]
- elseif section == "module" then
- print [[module:info(module, host) - Show information about a loaded module]]
- print [[module:load(module, host) - Load the specified module on the specified host (or all hosts if none given)]]
- print [[module:reload(module, host) - The same, but unloads and loads the module (saving state if the module supports it)]]
- print [[module:unload(module, host) - The same, but just unloads the module from memory]]
- print [[module:list(host) - List the modules loaded on the specified host]]
- elseif section == "host" then
- print [[host:activate(hostname) - Activates the specified host]]
- print [[host:deactivate(hostname) - Disconnects all clients on this host and deactivates]]
- print [[host:list() - List the currently-activated hosts]]
- elseif section == "user" then
- print [[user:create(jid, password, roles) - Create the specified user account]]
- print [[user:password(jid, password) - Set the password for the specified user account]]
- print [[user:roles(jid, host) - Show current roles for an user]]
- print [[user:setroles(jid, host, roles) - Set roles for an user (see 'help roles')]]
- print [[user:delete(jid) - Permanently remove the specified user account]]
- print [[user:list(hostname, pattern) - List users on the specified host, optionally filtering with a pattern]]
- elseif section == "roles" then
- print [[Roles may grant access or restrict users from certain operations]]
- print [[Built-in roles are:]]
- print [[ prosody:admin - Administrator]]
- print [[ (empty set) - Normal user]]
- print [[]]
- print [[The canonical role format looks like: { ["example:role"] = true }]]
- print [[For convenience, the following formats are also accepted:]]
- print [["admin" - short for "prosody:admin", the normal admin status (like the admins config option)]]
- print [["example:role" - short for {["example:role"]=true}]]
- print [[{"example:role"} - short for {["example:role"]=true}]]
- elseif section == "muc" then
- -- TODO `muc:room():foo()` commands
- print [[muc:create(roomjid, { config }) - Create the specified MUC room with the given config]]
- print [[muc:list(host) - List rooms on the specified MUC component]]
- print [[muc:room(roomjid) - Reference the specified MUC room to access MUC API methods]]
- elseif section == "server" then
- print [[server:version() - Show the server's version number]]
- print [[server:uptime() - Show how long the server has been running]]
- print [[server:memory() - Show details about the server's memory usage]]
- print [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]]
- elseif section == "port" then
- print [[port:list() - Lists all network ports prosody currently listens on]]
- print [[port:close(port, interface) - Close a port]]
- elseif section == "dns" then
- print [[dns:lookup(name, type, class) - Do a DNS lookup]]
- print [[dns:addnameserver(nameserver) - Add a nameserver to the list]]
- print [[dns:setnameserver(nameserver) - Replace the list of name servers with the supplied one]]
- print [[dns:purge() - Clear the DNS cache]]
- print [[dns:cache() - Show cached records]]
- elseif section == "xmpp" then
- print [[xmpp:ping(localhost, remotehost) -- Sends a ping to a remote XMPP server and reports the response]]
- elseif section == "config" then
- print [[config:reload() - Reload the server configuration. Modules may need to be reloaded for changes to take effect.]]
- print [[config:get([host,] option) - Show the value of a config option.]]
- elseif section == "stats" then -- luacheck: ignore 542
- print [[stats:show(pattern) - Show internal statistics, optionally filtering by name with a pattern]]
- print [[stats:show():cfgraph() - Show a cumulative frequency graph]]
- print [[stats:show():histogram() - Show a histogram of selected metric]]
- elseif section == "debug" then
- print [[debug:logevents(host) - Enable logging of fired events on host]]
- print [[debug:events(host, event) - Show registered event handlers]]
- print [[debug:timers() - Show information about scheduled timers]]
- elseif section == "console" then
- print [[Hey! Welcome to Prosody's admin console.]]
- print [[First thing, if you're ever wondering how to get out, simply type 'quit'.]]
- print [[Secondly, note that we don't support the full telnet protocol yet (it's coming)]]
- print [[so you may have trouble using the arrow keys, etc. depending on your system.]]
- print [[]]
- print [[For now we offer a couple of handy shortcuts:]]
- print [[!! - Repeat the last command]]
- print [[!old!new! - repeat the last command, but with 'old' replaced by 'new']]
- print [[]]
- print [[For those well-versed in Prosody's internals, or taking instruction from those who are,]]
- print [[you can prefix a command with > to escape the console sandbox, and access everything in]]
- print [[the running server. Great fun, but be careful not to break anything :)]]
- elseif section == "columns" then
- print [[The columns shown by c2s:show() and s2s:show() can be customizied via the]]
- print [['columns' argument as described here.]]
- print [[]]
- print [[Columns can be specified either as "id jid ipv" or as {"id", "jid", "ipv"}.]]
- print [[Available columns are:]]
- local meta_columns = {
- { title = "ID"; width = 5 };
- { title = "Column Title"; width = 12 };
- { title = "Description"; width = 12 };
- };
- -- auto-adjust widths
- for column, spec in pairs(available_columns) do
- meta_columns[1].width = math.max(meta_columns[1].width or 0, #column);
- meta_columns[2].width = math.max(meta_columns[2].width or 0, #(spec.title or ""));
- meta_columns[3].width = math.max(meta_columns[3].width or 0, #(spec.description or ""));
- end
- local row = format_table(meta_columns, 120)
- print(row());
- for column, spec in iterators.sorted_pairs(available_columns) do
- print(row({ column, spec.title, spec.description }));
- end
- print [[]]
- print [[Most fields on the internal session structures can also be used as columns]]
- -- Also, you can pass a table column specification directly, with mapper callback and all
+
+ local topic = data:match("^help (%w+)");
+ if topic then
+ return def_env.help[topic]({ session = session });
+ end
+
+ print [[Commands are divided into multiple sections. For help on a particular section, ]]
+ print [[type: help SECTION (for example, 'help c2s'). Sections are: ]]
+ print [[]]
+ local row = format_table({ { title = "Section", width = 7 }, { title = "Description", width = "100%" } }, session.width)
+ print(row())
+ for section_name, section in it.sorted_pairs(def_env) do
+ local section_mt = getmetatable(section);
+ local section_help = section_mt and section_mt.help;
+ print(row { section_name; section_help and section_help.desc or "" });
+ end
+
+ print("");
+
+ print [[In addition to info about commands, the following general topics are available:]]
+
+ print("");
+ for topic_name, topic_info in it.sorted_pairs(help_topics) do
+ print(topic_name .. " - "..topic_info.desc);
end
end
@@ -350,10 +346,13 @@ end
-- Anything in def_env will be accessible within the session as a global variable
--luacheck: ignore 212/self
-local serialize_defaults = module:get_option("console_prettyprint_settings",
- { fatal = false; unquoted = true; maxdepth = 2; table_iterator = "pairs" })
+local serialize_defaults = module:get_option("console_prettyprint_settings", {
+ preset = "pretty";
+ maxdepth = 2;
+ table_iterator = "pairs";
+})
-def_env.output = {};
+def_env.output = new_section("Configure admin console output");
function def_env.output:configure(opts)
if type(opts) ~= "table" then
opts = { preset = opts };
@@ -375,7 +374,57 @@ function def_env.output:configure(opts)
self.session.serialize = serialization.new(opts);
end
-def_env.server = {};
+def_env.help = setmetatable({}, {
+ help = {
+ desc = "Show this help about available commands";
+ commands = {};
+ };
+ __index = function (_, section_name)
+ return function (self)
+ local print = self.session.print;
+ local section_mt = getmetatable(def_env[section_name]);
+ local section_help = section_mt and section_mt.help;
+
+ local c = 0;
+
+ if section_help then
+ print("Help: "..section_name);
+ if section_help.desc then
+ print(section_help.desc);
+ end
+ print(("-"):rep(#(section_help.desc or section_name)));
+ print("");
+
+ if section_help.content then
+ print(section_help.content);
+ print("");
+ end
+
+ for command, command_help in it.sorted_pairs(section_help.commands or {}) do
+ c = c + 1;
+ local args = command_help.args:pluck("name"):concat(", ");
+ local desc = command_help.desc or command_help.module and ("Provided by mod_"..command_help.module) or "";
+ print(("%s:%s(%s) - %s"):format(section_name, command, args, desc));
+ end
+ elseif help_topics[section_name] then
+ local topic = help_topics[section_name];
+ if type(topic.content) == "function" then
+ topic.content(self, print);
+ else
+ print(topic.content);
+ end
+ print("");
+ return true, "Showing help topic '"..section_name.."'";
+ else
+ print("Unknown topic: "..section_name);
+ end
+ print("");
+ return true, ("%d command(s) listed"):format(c);
+ end;
+ end;
+});
+
+def_env.server = new_section("Uptime, version, shutting down, etc.");
function def_env.server:insane_reload()
prosody.unlock_globals();
@@ -384,10 +433,12 @@ function def_env.server:insane_reload()
return true, "Server reloaded";
end
+describe_command [[server:version() - Show the server's version number]]
function def_env.server:version()
return true, tostring(prosody.version or "unknown");
end
+describe_command [[server:uptime() - Show how long the server has been running]]
function def_env.server:uptime()
local t = os.time()-prosody.start_time;
local seconds = t%60;
@@ -402,6 +453,7 @@ function def_env.server:uptime()
minutes, (minutes ~= 1 and "s") or "", os.date("%c", prosody.start_time));
end
+describe_command [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]]
function def_env.server:shutdown(reason, code)
prosody.shutdown(reason, code);
return true, "Shutdown initiated";
@@ -411,6 +463,7 @@ local function human(kb)
return format_number(kb*1024, "B", "b");
end
+describe_command [[server:memory() - Show details about the server's memory usage]]
function def_env.server:memory()
if not has_pposix or not pposix.meminfo then
return true, "Lua is using "..human(collectgarbage("count"));
@@ -423,7 +476,7 @@ function def_env.server:memory()
return true, "OK";
end
-def_env.module = {};
+def_env.module = new_section("Commands to load/reload/unload modules/plugins");
local function get_hosts_set(hosts)
if type(hosts) == "table" then
@@ -469,6 +522,7 @@ local function get_hosts_with_module(hosts, module)
return hosts_set;
end
+describe_command [[module:info(module, host) - Show information about a loaded module]]
function def_env.module:info(name, hosts)
if not name then
return nil, "module name expected";
@@ -481,6 +535,16 @@ function def_env.module:info(name, hosts)
local function item_name(item) return item.name; end
+ local function task_timefmt(t)
+ if not t then
+ return "no last run time"
+ elseif os.difftime(os.time(), t) < 86400 then
+ return os.date("last run today at %H:%M", t);
+ else
+ return os.date("last run %A at %H:%M", t);
+ end
+ end
+
local friendly_descriptions = {
["adhoc-provider"] = "Ad-hoc commands",
["auth-provider"] = "Authentication provider",
@@ -498,12 +562,22 @@ function def_env.module:info(name, hosts)
["auth-provider"] = item_name,
["storage-provider"] = item_name,
["http-provider"] = function(item, mod) return mod:http_url(item.name, item.default_path); end,
- ["net-provider"] = item_name,
+ ["net-provider"] = function(item)
+ local service_name = item.name;
+ local ports_list = {};
+ for _, interface, port in portmanager.get_active_services():iter(service_name, nil, nil) do
+ table.insert(ports_list, "["..interface.."]:"..port);
+ end
+ if not ports_list[1] then
+ return service_name..": not listening on any ports";
+ end
+ return service_name..": "..table.concat(ports_list, ", ");
+ end,
["measure"] = function(item) return item.name .. " (" .. suf(item.conf and item.conf.unit, " ") .. item.type .. ")"; end,
["metric"] = function(item)
return ("%s (%s%s)%s"):format(item.name, suf(item.mf.unit, " "), item.mf.type_, pre(": ", item.mf.description));
end,
- ["task"] = function (item) return string.format("%s (%s)", item.name or item.id, item.when); end
+ ["task"] = function (item) return string.format("%s (%s, %s)", item.name or item.id, item.when, task_timefmt(item.last)); end
};
for host in hosts do
@@ -533,21 +607,37 @@ function def_env.module:info(name, hosts)
if mod.module.dependencies and next(mod.module.dependencies) ~= nil then
print(" dependencies:");
for dep in pairs(mod.module.dependencies) do
- print(" - mod_" .. dep);
+ -- Dependencies are per module instance, not per host, so dependencies
+ -- of/on global modules may list modules not actually loaded on the
+ -- current host.
+ if modulemanager.is_loaded(host, dep) then
+ print(" - mod_" .. dep);
+ end
+ end
+ end
+ if mod.module.reverse_dependencies and next(mod.module.reverse_dependencies) ~= nil then
+ print(" reverse dependencies:");
+ for dep in pairs(mod.module.reverse_dependencies) do
+ if modulemanager.is_loaded(host, dep) then
+ print(" - mod_" .. dep);
+ end
end
end
end
return true;
end
-function def_env.module:load(name, hosts, config)
+describe_command [[module:load(module, host) - Load the specified module on the specified host (or all hosts if none given)]]
+function def_env.module:load(name, hosts)
hosts = get_hosts_with_module(hosts);
-- Load the module for each host
local ok, err, count, mod = true, nil, 0;
for host in hosts do
+ local configured_modules, component = modulemanager.get_modules_for_host(host);
+
if (not modulemanager.is_loaded(host, name)) then
- mod, err = modulemanager.load(host, name, config);
+ mod, err = modulemanager.load(host, name);
if not mod then
ok = false;
if err == "global-module-already-loaded" then
@@ -560,6 +650,10 @@ function def_env.module:load(name, hosts, config)
else
count = count + 1;
self.session.print("Loaded for "..mod.module.host);
+
+ if not (configured_modules:contains(name) or name == component) then
+ self.session.print("Note: Module will not be loaded after restart unless enabled in configuration");
+ end
end
end
end
@@ -567,12 +661,15 @@ function def_env.module:load(name, hosts, config)
return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
end
+describe_command [[module:unload(module, host) - The same, but just unloads the module from memory]]
function def_env.module:unload(name, hosts)
hosts = get_hosts_with_module(hosts, name);
-- Unload the module for each host
local ok, err, count = true, nil, 0;
for host in hosts do
+ local configured_modules, component = modulemanager.get_modules_for_host(host);
+
if modulemanager.is_loaded(host, name) then
ok, err = modulemanager.unload(host, name);
if not ok then
@@ -581,6 +678,10 @@ function def_env.module:unload(name, hosts)
else
count = count + 1;
self.session.print("Unloaded from "..host);
+
+ if configured_modules:contains(name) or name == component then
+ self.session.print("Note: Module will be loaded after restart unless disabled in configuration");
+ end
end
end
end
@@ -593,6 +694,7 @@ local function _sort_hosts(a, b)
else return a:gsub("[^.]+", string.reverse):reverse() < b:gsub("[^.]+", string.reverse):reverse(); end
end
+describe_command [[module:reload(module, host) - The same, but unloads and loads the module (saving state if the module supports it)]]
function def_env.module:reload(name, hosts)
hosts = array.collect(get_hosts_with_module(hosts, name)):sort(_sort_hosts)
@@ -616,6 +718,7 @@ function def_env.module:reload(name, hosts)
return ok, (ok and "Module reloaded on "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
end
+describe_command [[module:list(host) - List the modules loaded on the specified host]]
function def_env.module:list(hosts)
hosts = array.collect(set.new({ not hosts and "*" or nil }) + get_hosts_set(hosts)):sort(_sort_hosts);
@@ -642,9 +745,10 @@ function def_env.module:list(hosts)
end
end
-def_env.config = {};
+def_env.config = new_section("Reloading the configuration, etc.");
+
function def_env.config:load(filename, format)
- local config_load = require "core.configmanager".load;
+ local config_load = require "prosody.core.configmanager".load;
local ok, err = config_load(filename, format);
if not ok then
return false, err or "Unknown error loading config";
@@ -652,20 +756,30 @@ function def_env.config:load(filename, format)
return true, "Config loaded";
end
+describe_command [[config:get([host,] option) - Show the value of a config option.]]
function def_env.config:get(host, key)
if key == nil then
host, key = "*", host;
end
- local config_get = require "core.configmanager".get
+ local config_get = require "prosody.core.configmanager".get
return true, serialize_config(config_get(host, key));
end
+describe_command [[config:set([host,] option, value) - Update the value of a config option without writing to the config file.]]
+function def_env.config:set(host, key, value)
+ if host ~= "*" and not prosody.hosts[host] then
+ host, key, value = "*", host, key;
+ end
+ return require "prosody.core.configmanager".set(host, key, value);
+end
+
+describe_command [[config:reload() - Reload the server configuration. Modules may need to be reloaded for changes to take effect.]]
function def_env.config:reload()
local ok, err = prosody.reload_config();
return ok, (ok and "Config reloaded (you may need to reload modules to take effect)") or tostring(err);
end
-def_env.c2s = {};
+def_env.c2s = new_section("Commands to manage local client-to-server sessions");
local function get_jid(session)
if session.username then
@@ -702,6 +816,7 @@ local function show_c2s(callback)
end);
end
+describe_command [[c2s:count() - Count sessions without listing them]]
function def_env.c2s:count()
local c2s = get_c2s();
return true, "Total: ".. #c2s .." clients";
@@ -719,7 +834,7 @@ available_columns = {
jid = {
title = "JID";
description = "Full JID of user session";
- width = 32;
+ width = "3p";
key = "full_jid";
mapper = function(full_jid, session) return full_jid or get_jid(session) end;
};
@@ -727,7 +842,7 @@ available_columns = {
title = "Host";
description = "Local hostname";
key = "host";
- width = 22;
+ width = "1p";
mapper = function(host, session)
return host or get_s2s_hosts(session) or "?";
end;
@@ -735,7 +850,7 @@ available_columns = {
remote = {
title = "Remote";
description = "Remote hostname";
- width = 22;
+ width = "1p";
mapper = function(_, session)
return select(2, get_s2s_hosts(session));
end;
@@ -743,7 +858,7 @@ available_columns = {
port = {
title = "Port";
description = "Server port used";
- width = 5;
+ width = #string.format("%d", 0xffff); -- max 16 bit unsigned integer
align = "right";
key = "conn";
mapper = function(conn)
@@ -752,10 +867,22 @@ 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";
- width = 3;
+ width = #"<->";
key = "direction";
mapper = function(dir, session)
if session.incoming and session.outgoing then return "<->"; end
@@ -763,12 +890,23 @@ available_columns = {
if dir == "incoming" then return "<--"; end
end;
};
- id = { title = "Session ID"; description = "Internal session ID used in logging"; width = 20; key = "id" };
- type = { title = "Type"; description = "Session type"; width = #"c2s_unauthed"; key = "type" };
+ id = {
+ title = "Session ID";
+ description = "Internal session ID used in logging";
+ -- Depends on log16(?) of pointers which may vary over runtime, so + some margin
+ width = math.max(#"c2s", #"s2sin", #"s2sout") + #(tostring({}):match("%x+$")) + 2;
+ key = "id";
+ };
+ type = {
+ title = "Type";
+ description = "Session type";
+ width = math.max(#"c2s_unauthed", #"s2sout_unauthed");
+ key = "type";
+ };
method = {
title = "Method";
description = "Connection method";
- width = 10;
+ width = math.max(#"BOSH", #"WebSocket", #"TCP");
mapper = function(_, session)
if session.bosh_version then
return "BOSH";
@@ -782,15 +920,20 @@ available_columns = {
ipv = {
title = "IPv";
description = "Internet Protocol version (4 or 6)";
- width = 4;
+ width = #"IPvX";
key = "ip";
mapper = function(ip) if ip then return ip:find(":") and "IPv6" or "IPv4"; end end;
};
- ip = { title = "IP address"; description = "IP address the session connected from"; width = 40; key = "ip" };
+ ip = {
+ title = "IP address";
+ description = "IP address the session connected from";
+ width = module:get_option_boolean("use_ipv6", true) and #"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" or #"198.051.100.255";
+ key = "ip";
+ };
status = {
title = "Status";
description = "Presence status";
- width = 6;
+ width = math.max(#"online", #"chat");
key = "presence";
mapper = function(p)
if not p then return ""; end
@@ -801,24 +944,22 @@ available_columns = {
title = "Security";
description = "TLS version or security status";
key = "conn";
- width = 8;
+ width = math.max(#"secure", #"TLSvX.Y");
mapper = function(conn, session)
if not session.secure then return "insecure"; end
if not conn or not conn:ssl() then return "secure" end
- local sock = conn and conn:socket();
- if not sock then return "secure"; end
- local tls_info = sock.info and sock:info();
+ local tls_info = conn.ssl_info and conn:ssl_info();
return tls_info and tls_info.protocol or "secure";
end;
};
encryption = {
title = "Encryption";
description = "Encryption algorithm used (TLS cipher suite)";
- width = 30;
+ -- openssl ciphers 'ALL:COMPLEMENTOFALL' | tr : \\n | awk 'BEGIN {n=1} length() > n {n=length()} END {print(n)}'
+ width = #"ECDHE-ECDSA-CHACHA20-POLY1305";
key = "conn";
mapper = function(conn)
- local sock = conn and conn:socket();
- local info = sock and sock.info and sock:info();
+ local info = conn and conn.ssl_info and conn:ssl_info();
if info then return info.cipher end
end;
};
@@ -826,27 +967,36 @@ available_columns = {
title = "Certificate";
description = "Validation status of certificate";
key = "cert_identity_status";
- width = 11;
+ width = math.max(#"Expired", #"Self-signed", #"Untrusted", #"Mismatched", #"Unknown");
mapper = function(cert_status, session)
- if cert_status then return capitalize(cert_status); end
- if session.cert_chain_status == "invalid" then
+ if cert_status == "invalid" then
+ -- non-nil cert_identity_status implies valid chain, which covers just
+ -- about every error condition except mismatched certificate names
+ return "Mismatched";
+ elseif cert_status then
+ -- basically only "valid"
+ return capitalize(cert_status);
+ end
+ -- no certificate status,
+ if type(session.cert_chain_errors) == "table" then
local cert_errors = set.new(session.cert_chain_errors[1]);
if cert_errors:contains("certificate has expired") then
return "Expired";
elseif cert_errors:contains("self signed certificate") then
return "Self-signed";
end
+ -- Some other cert issue, or something up the chain
+ -- TODO borrow more logic from mod_s2s/friendly_cert_error()
return "Untrusted";
- elseif session.cert_identity_status == "invalid" then
- return "Mismatched";
end
+ -- TODO cert_chain_errors can be a string, handle that
return "Unknown";
end;
};
sni = {
title = "SNI";
description = "Hostname requested in TLS";
- width = 22;
+ width = "1p"; -- same as host, remote etc
mapper = function(_, session)
if not session.conn then return end
local sock = session.conn:socket();
@@ -856,7 +1006,7 @@ available_columns = {
alpn = {
title = "ALPN";
description = "Protocol requested in TLS";
- width = 11;
+ width = math.max(#"http/1.1", #"xmpp-client", #"xmpp-server");
mapper = function(_, session)
if not session.conn then return end
local sock = session.conn:socket();
@@ -867,7 +1017,8 @@ available_columns = {
title = "SM";
description = "Stream Management (XEP-0198) status";
key = "smacks";
- width = 11;
+ -- FIXME shorter synonym for hibernating
+ width = math.max(#"yes", #"no", #"hibernating");
mapper = function(smacks_xmlns, session)
if not smacks_xmlns then return "no"; end
if session.hibernating then return "hibernating"; end
@@ -901,7 +1052,7 @@ available_columns = {
title = "Dialback";
description = "Legacy server verification";
key = "dialback_key";
- width = 13;
+ width = math.max(#"Not used", #"Not initiated", #"Initiated", #"Completed");
mapper = function (dialback_key, session)
if not dialback_key then
if session.type == "s2sin" or session.type == "s2sout" then
@@ -915,6 +1066,16 @@ available_columns = {
end
end
};
+ role = {
+ title = "Role";
+ description = "Session role with 'prosody:' prefix removed";
+ width = "1p";
+ key = "role";
+ mapper = function(role)
+ local name = role and role.name;
+ return name and name:match"^prosody:(%w+)" or name;
+ end;
+ }
};
local function get_colspec(colspec, default)
@@ -922,7 +1083,7 @@ local function get_colspec(colspec, default)
local columns = {};
for i, col in pairs(colspec or default) do
if type(col) == "string" then
- columns[i] = available_columns[col] or { title = capitalize(col); width = 20; key = col };
+ columns[i] = available_columns[col] or { title = capitalize(col); width = "1p"; key = col };
elseif type(col) ~= "table" then
return false, ("argument %d: expected string|table but got %s"):format(i, type(col));
else
@@ -933,14 +1094,15 @@ local function get_colspec(colspec, default)
return columns;
end
+describe_command [[c2s:show(jid, columns) - Show all client sessions with the specified JID (or all if no JID given)]]
function def_env.c2s:show(match_jid, colspec)
local print = self.session.print;
- local columns = get_colspec(colspec, { "id"; "jid"; "ipv"; "status"; "secure"; "smacks"; "csi" });
- local row = format_table(columns, 120);
+ local columns = get_colspec(colspec, { "id"; "jid"; "role"; "ipv"; "status"; "secure"; "smacks"; "csi" });
+ local row = format_table(columns, self.session.width);
local function match(session)
local jid = get_jid(session)
- return (not match_jid) or jid_compare(jid, match_jid);
+ return (not match_jid) or match_jid == "*" or jid_compare(jid, match_jid);
end
local group_by_host = true;
@@ -973,6 +1135,7 @@ function def_env.c2s:show(match_jid, colspec)
return true, ("%d c2s sessions shown"):format(total_count);
end
+describe_command [[c2s:show_tls(jid) - Show TLS cipher info for encrypted sessions]]
function def_env.c2s:show_tls(match_jid)
return self:show(match_jid, { "jid"; "id"; "secure"; "encryption" });
end
@@ -986,6 +1149,7 @@ local function build_reason(text, condition)
end
end
+describe_command [[c2s:close(jid) - Close all sessions for the specified JID]]
function def_env.c2s:close(match_jid, text, condition)
local count = 0;
show_c2s(function (jid, session)
@@ -997,6 +1161,7 @@ function def_env.c2s:close(match_jid, text, condition)
return true, "Total: "..count.." sessions closed";
end
+describe_command [[c2s:closeall() - Close all active c2s connections ]]
function def_env.c2s:closeall(text, condition)
local count = 0;
--luacheck: ignore 212/jid
@@ -1008,7 +1173,8 @@ function def_env.c2s:closeall(text, condition)
end
-def_env.s2s = {};
+def_env.s2s = new_section("Commands to manage sessions between this server and others");
+
local function _sort_s2s(a, b)
local a_local, a_remote = get_s2s_hosts(a);
local b_local, b_remote = get_s2s_hosts(b);
@@ -1016,14 +1182,31 @@ local function _sort_s2s(a, b)
return _sort_hosts(a_local or "", b_local or "");
end
+local function match_wildcard(match_jid, jid)
+ -- host == host or (host) == *.(host) or sub(.host) == *(.host)
+ return jid == match_jid or jid == match_jid:sub(3) or jid:sub(-#match_jid + 1) == match_jid:sub(2);
+end
+
+local function match_s2s_jid(session, match_jid)
+ local host, remote = get_s2s_hosts(session);
+ if not match_jid or match_jid == "*" then
+ return true;
+ elseif host == match_jid or remote == match_jid then
+ return true;
+ elseif match_jid:sub(1, 2) == "*." then
+ return match_wildcard(match_jid, host) or match_wildcard(match_jid, remote);
+ end
+ return false;
+end
+
+describe_command [[s2s:show(domain, columns) - Show all s2s connections for the given domain (or all if no domain given)]]
function def_env.s2s:show(match_jid, colspec)
local print = self.session.print;
local columns = get_colspec(colspec, { "id"; "host"; "dir"; "remote"; "ipv"; "secure"; "s2s_sasl"; "dialback" });
- local row = format_table(columns, 132);
+ local row = format_table(columns, self.session.width);
local function match(session)
- local host, remote = get_s2s_hosts(session);
- return not match_jid or host == match_jid or remote == match_jid;
+ return match_s2s_jid(session, match_jid);
end
local group_by_host = true;
@@ -1057,6 +1240,7 @@ function def_env.s2s:show(match_jid, colspec)
return true, ("%d s2s connections shown"):format(total_count);
end
+describe_command [[s2s:show_tls(domain) - Show TLS cipher info for encrypted sessions]]
function def_env.s2s:show_tls(match_jid)
return self:show(match_jid, { "id"; "host"; "dir"; "remote"; "secure"; "encryption"; "cert" });
end
@@ -1090,7 +1274,7 @@ function def_env.s2s:showcert(domain)
local print = self.session.print;
local s2s_sessions = module:shared"/*/s2s/sessions";
local domain_sessions = set.new(array.collect(values(s2s_sessions)))
- /function(session) return (session.to_host == domain or session.from_host == domain) and session or nil; end;
+ /function(session) return match_s2s_jid(session, domain) and session or nil; end;
local cert_set = {};
for session in domain_sessions do
local conn = session.conn;
@@ -1179,6 +1363,7 @@ function def_env.s2s:showcert(domain)
.." presented by "..domain..".");
end
+describe_command [[s2s:close(from, to) - Close a connection from one domain to another]]
function def_env.s2s:close(from, to, text, condition)
local print, count = self.session.print, 0;
local s2s_sessions = module:shared"/*/s2s/sessions";
@@ -1193,22 +1378,22 @@ function def_env.s2s:close(from, to, text, condition)
end
for _, session in pairs(s2s_sessions) do
- local id = session.id or (session.type..tostring(session):match("[a-f0-9]+$"));
- if (match_id and match_id == id)
- or (session.from_host == from and session.to_host == to) then
+ local id = session.id or (session.type .. tostring(session):match("[a-f0-9]+$"));
+ if (match_id and match_id == id) or ((from and match_wildcard(from, session.to_host)) or (to and match_wildcard(to, session.to_host))) then
print(("Closing connection from %s to %s [%s]"):format(session.from_host, session.to_host, id));
(session.close or s2smanager.destroy_session)(session, build_reason(text, condition));
- count = count + 1 ;
+ count = count + 1;
end
end
return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s");
end
+describe_command [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]]
function def_env.s2s:closeall(host, text, condition)
local count = 0;
local s2s_sessions = module:shared"/*/s2s/sessions";
for _,session in pairs(s2s_sessions) do
- if not host or session.from_host == host or session.to_host == host then
+ if not host or host == "*" or match_s2s_jid(session, host) then
session:close(build_reason(text, condition));
count = count + 1;
end
@@ -1217,37 +1402,42 @@ function def_env.s2s:closeall(host, text, condition)
else return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end
end
-def_env.host = {}; def_env.hosts = def_env.host;
+def_env.host = new_section("Commands to activate, deactivate and list virtual hosts");
+describe_command [[host:activate(hostname) - Activates the specified host]]
function def_env.host:activate(hostname, config)
return hostmanager.activate(hostname, config);
end
+
+describe_command [[host:deactivate(hostname) - Disconnects all clients on this host and deactivates]]
function def_env.host:deactivate(hostname, reason)
return hostmanager.deactivate(hostname, reason);
end
+describe_command [[host:list() - List the currently-activated hosts]]
function def_env.host:list()
local print = self.session.print;
local i = 0;
- local type;
+ local host_type;
for host, host_session in iterators.sorted_pairs(prosody.hosts, _sort_hosts) do
i = i + 1;
- type = host_session.type;
- if type == "local" then
+ host_type = host_session.type;
+ if host_type == "local" then
print(host);
else
- type = module:context(host):get_option_string("component_module", type);
- if type ~= "component" then
- type = type .. " component";
+ host_type = module:context(host):get_option_string("component_module", host_type);
+ if host_type ~= "component" then
+ host_type = host_type .. " component";
end
- print(("%s (%s)"):format(host, type));
+ print(("%s (%s)"):format(host, host_type));
end
end
return true, i.." hosts";
end
-def_env.port = {};
+def_env.port = new_section("Commands to manage ports the server is listening on");
+describe_command [[port:list() - Lists all network ports prosody currently listens on]]
function def_env.port:list()
local print = self.session.print;
local services = portmanager.get_active_services().data;
@@ -1266,6 +1456,7 @@ function def_env.port:list()
return true, n_services.." services listening on "..n_ports.." ports";
end
+describe_command [[port:close(port, interface) - Close a port]]
function def_env.port:close(close_port, close_interface)
close_port = assert(tonumber(close_port), "Invalid port number");
local n_closed = 0;
@@ -1288,7 +1479,7 @@ function def_env.port:close(close_port, close_interface)
return true, "Closed "..n_closed.." ports";
end
-def_env.muc = {};
+def_env.muc = new_section("Commands to create, list and manage chat rooms");
local console_room_mt = {
__index = function (self, k) return self.room[k]; end;
@@ -1307,6 +1498,21 @@ local function check_muc(jid)
return room_name, host;
end
+local function get_muc(room_jid)
+ local room_name, host = check_muc(room_jid);
+ if not room_name then
+ return room_name, host;
+ end
+ local room_obj = prosody.hosts[host].modules.muc.get_room_from_jid(room_jid);
+ if not room_obj then
+ return nil, "No such room: "..room_jid;
+ end
+ return room_obj;
+end
+
+local muc_util = module:require"muc/util";
+
+describe_command [[muc:create(roomjid, { config }) - Create the specified MUC room with the given config]]
function def_env.muc:create(room_jid, config)
local room_name, host = check_muc(room_jid);
if not room_name then
@@ -1318,18 +1524,16 @@ function def_env.muc:create(room_jid, config)
return prosody.hosts[host].modules.muc.create_room(room_jid, config);
end
+describe_command [[muc:room(roomjid) - Reference the specified MUC room to access MUC API methods]]
function def_env.muc:room(room_jid)
- local room_name, host = check_muc(room_jid);
- if not room_name then
- return room_name, host;
- end
- local room_obj = prosody.hosts[host].modules.muc.get_room_from_jid(room_jid);
+ local room_obj, err = get_muc(room_jid);
if not room_obj then
- return nil, "No such room: "..room_jid;
+ return room_obj, err;
end
return setmetatable({ room = room_obj }, console_room_mt);
end
+describe_command [[muc:list(host) - List rooms on the specified MUC component]]
function def_env.muc:list(host)
local host_session = prosody.hosts[host];
if not host_session or not host_session.modules.muc then
@@ -1344,36 +1548,160 @@ function def_env.muc:list(host)
return true, c.." rooms";
end
-local um = require"core.usermanager";
+describe_command [[muc:occupants(roomjid, filter) - List room occupants, optionally filtered on substring or role]]
+function def_env.muc:occupants(room_jid, filter)
+ local room_obj, err = get_muc(room_jid);
+ if not room_obj then
+ return room_obj, err;
+ end
+
+ local print = self.session.print;
+ local row = format_table({
+ { title = "Role"; width = 12; key = "role" }; -- longest role name
+ { title = "JID"; width = "75%"; key = "bare_jid" };
+ { title = "Nickname"; width = "25%"; key = "nick"; mapper = jid_resource };
+ }, self.session.width);
+ local occupants = array.collect(iterators.select(2, room_obj:each_occupant()));
+ local total = #occupants;
+ if filter then
+ occupants:filter(function(occupant)
+ return occupant.role == filter or jid_resource(occupant.nick):find(filter, 1, true);
+ end);
+ end
+ local displayed = #occupants;
+ occupants:sort(function(a, b)
+ if a.role ~= b.role then
+ return muc_util.valid_roles[a.role] > muc_util.valid_roles[b.role];
+ else
+ return a.bare_jid < b.bare_jid;
+ end
+ end);
+
+ if displayed == 0 then
+ return true, ("%d out of %d occupant%s listed"):format(displayed, total, total ~= 1 and "s" or "")
+ end
+
+ print(row());
+ for _, occupant in ipairs(occupants) do
+ print(row(occupant));
+ end
+
+ if total == displayed then
+ return true, ("%d occupant%s listed"):format(total, total ~= 1 and "s" or "")
+ else
+ return true, ("%d out of %d occupant%s listed"):format(displayed, total, total ~= 1 and "s" or "")
+ end
+end
+
+describe_command [[muc:affiliations(roomjid, filter) - List affiliated members of the room, optionally filtered on substring or affiliation]]
+function def_env.muc:affiliations(room_jid, filter)
+ local room_obj, err = get_muc(room_jid);
+ if not room_obj then
+ return room_obj, err;
+ end
+
+ local print = self.session.print;
+ local row = format_table({
+ { title = "Affiliation"; width = 12 }; -- longest affiliation name
+ { title = "JID"; width = "75%" };
+ { title = "Nickname"; width = "25%"; key = "reserved_nickname" };
+ }, self.session.width);
+ local affiliated = array();
+ for affiliated_jid, affiliation, affiliation_data in room_obj:each_affiliation() do
+ affiliated:push(setmetatable({ affiliation; affiliated_jid }, { __index = affiliation_data }));
+ end
+
+ local total = #affiliated;
+ if filter then
+ affiliated:filter(function(affiliation)
+ return filter == affiliation[1] or affiliation[2]:find(filter, 1, true);
+ end);
+ end
+ local displayed = #affiliated;
+ local aff_ranking = muc_util.valid_affiliations;
+ affiliated:sort(function(a, b)
+ if a[1] ~= b[1] then
+ return aff_ranking[a[1]] > aff_ranking[b[1]];
+ else
+ return a[2] < b[2];
+ end
+ end);
+
+ if displayed == 0 then
+ return true, ("%d out of %d affiliations%s listed"):format(displayed, total, total ~= 1 and "s" or "")
+ end
+
+ print(row());
+ for _, affiliation in ipairs(affiliated) do
+ print(row(affiliation));
+ end
+
-local function coerce_roles(roles)
- if roles == "admin" then roles = "prosody:admin"; end
- if type(roles) == "string" then roles = { [roles] = true }; end
- if roles[1] then for i, role in ipairs(roles) do roles[role], roles[i] = true, nil; end end
- return roles;
+ if total == displayed then
+ return true, ("%d affiliation%s listed"):format(total, total ~= 1 and "s" or "")
+ else
+ return true, ("%d out of %d affiliation%s listed"):format(displayed, total, total ~= 1 and "s" or "")
+ end
end
-def_env.user = {};
-function def_env.user:create(jid, password, roles)
+local um = require"prosody.core.usermanager";
+
+def_env.user = new_section("Commands to create and delete users, and change their passwords");
+
+describe_command [[user:create(jid, password, role) - Create the specified user account]]
+function def_env.user:create(jid, password, role)
local username, host = jid_split(jid);
if not prosody.hosts[host] then
return nil, "No such host: "..host;
elseif um.user_exists(username, host) then
return nil, "User exists";
end
- local ok, err = um.create_user(username, password, host);
+
+ if not role then
+ role = module:get_option_string("default_provisioned_role", "prosody:member");
+ end
+
+ local ok, err = um.create_user_with_role(username, password, host, role);
+ if not ok then
+ return nil, "Could not create user: "..err;
+ end
+
+ return true, ("Created %s with role '%s'"):format(jid, role);
+end
+
+describe_command [[user:disable(jid) - Disable the specified user account, preventing login]]
+function def_env.user:disable(jid)
+ local username, host = jid_split(jid);
+ if not prosody.hosts[host] then
+ return nil, "No such host: "..host;
+ elseif not um.user_exists(username, host) then
+ return nil, "No such user";
+ end
+ local ok, err = um.disable_user(username, host);
if ok then
- if ok and roles then
- roles = coerce_roles(roles);
- local roles_ok, rerr = um.set_roles(jid, host, roles);
- if not roles_ok then return nil, "User created, but could not set roles: " .. tostring(rerr); end
- end
- return true, "User created";
+ return true, "User disabled";
else
- return nil, "Could not create user: "..err;
+ return nil, "Could not disable user: "..err;
end
end
+describe_command [[user:enable(jid) - Enable the specified user account, restoring login access]]
+function def_env.user:enable(jid)
+ local username, host = jid_split(jid);
+ if not prosody.hosts[host] then
+ return nil, "No such host: "..host;
+ elseif not um.user_exists(username, host) then
+ return nil, "No such user";
+ end
+ local ok, err = um.enable_user(username, host);
+ if ok then
+ return true, "User enabled";
+ else
+ return nil, "Could not enable user: "..err;
+ end
+end
+
+describe_command [[user:delete(jid) - Permanently remove the specified user account]]
function def_env.user:delete(jid)
local username, host = jid_split(jid);
if not prosody.hosts[host] then
@@ -1389,6 +1717,7 @@ function def_env.user:delete(jid)
end
end
+describe_command [[user:password(jid, password) - Set the password for the specified user account]]
function def_env.user:password(jid, password)
local username, host = jid_split(jid);
if not prosody.hosts[host] then
@@ -1404,43 +1733,71 @@ function def_env.user:password(jid, password)
end
end
-function def_env.user:roles(jid, host, new_roles)
- if new_roles or type(host) == "table" then
- return nil, "Use user:setroles(jid, host, roles) to change user roles";
- end
+describe_command [[user:roles(jid, host) - Show current roles for an user]]
+function def_env.user:role(jid, host)
+ local print = self.session.print;
local username, userhost = jid_split(jid);
if host == nil then host = userhost; end
- if host ~= "*" and not prosody.hosts[host] then
+ if not prosody.hosts[host] then
return nil, "No such host: "..host;
elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then
return nil, "No such user";
end
- local roles = um.get_roles(jid, host);
- if not roles then return true, "No roles"; end
- local count = 0;
- local print = self.session.print;
- for role in pairs(roles) do
+
+ local primary_role = um.get_user_role(username, host);
+ local secondary_roles = um.get_user_secondary_roles(username, host);
+
+ print(primary_role and primary_role.name or "<none>");
+
+ local count = primary_role and 1 or 0;
+ for role_name in pairs(secondary_roles or {}) do
count = count + 1;
- print(role);
+ print(role_name.." (secondary)");
end
+
return true, count == 1 and "1 role" or count.." roles";
end
-def_env.user.showroles = def_env.user.roles; -- COMPAT
+def_env.user.roles = def_env.user.role;
--- user:roles("someone@example.com", "example.com", {"prosody:admin"})
--- user:roles("someone@example.com", {"prosody:admin"})
-function def_env.user:setroles(jid, host, new_roles)
+describe_command [[user:setrole(jid, host, role) - Set primary role of a user (see 'help roles')]]
+-- user:setrole("someone@example.com", "example.com", "prosody:admin")
+-- user:setrole("someone@example.com", "prosody:admin")
+function def_env.user:setrole(jid, host, new_role)
local username, userhost = jid_split(jid);
- if new_roles == nil then host, new_roles = userhost, host; end
- if host ~= "*" and not prosody.hosts[host] then
+ if new_role == nil then host, new_role = userhost, host; end
+ if not prosody.hosts[host] then
return nil, "No such host: "..host;
elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then
return nil, "No such user";
end
- if host == "*" then host = nil; end
- return um.set_roles(jid, host, coerce_roles(new_roles));
+ return um.set_user_role(username, host, new_role);
end
+describe_command [[user:addrole(jid, host, role) - Add a secondary role to a user]]
+function def_env.user:addrole(jid, host, new_role)
+ local username, userhost = jid_split(jid);
+ if new_role == nil then host, new_role = userhost, host; end
+ if not prosody.hosts[host] then
+ return nil, "No such host: "..host;
+ elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then
+ return nil, "No such user";
+ end
+ return um.add_user_secondary_role(username, host, new_role);
+end
+
+describe_command [[user:delrole(jid, host, role) - Remove a secondary role from a user]]
+function def_env.user:delrole(jid, host, role_name)
+ local username, userhost = jid_split(jid);
+ if role_name == nil then host, role_name = userhost, host; end
+ if not prosody.hosts[host] then
+ return nil, "No such host: "..host;
+ elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then
+ return nil, "No such user";
+ end
+ return um.remove_user_secondary_role(username, host, role_name);
+end
+
+describe_command [[user:list(hostname, pattern) - List users on the specified host, optionally filtering with a pattern]]
-- TODO switch to table view, include roles
function def_env.user:list(host, pat)
if not host then
@@ -1460,9 +1817,10 @@ function def_env.user:list(host, pat)
return true, "Showing "..(pat and (matches.." of ") or "all " )..total.." users";
end
-def_env.xmpp = {};
+def_env.xmpp = new_section("Commands for sending XMPP stanzas");
-local new_id = require "util.id".medium;
+describe_command [[xmpp:ping(localhost, remotehost) - Sends a ping to a remote XMPP server and reports the response]]
+local new_id = require "prosody.util.id".medium;
function def_env.xmpp:ping(localhost, remotehost, timeout)
localhost = select(2, jid_split(localhost));
remotehost = select(2, jid_split(remotehost));
@@ -1509,12 +1867,12 @@ function def_env.xmpp:ping(localhost, remotehost, timeout)
module:unhook("s2sin-established", onestablished);
module:unhook("s2s-destroyed", ondestroyed);
end):next(function(pong)
- return ("pong from %s in %gs"):format(pong.stanza.attr.from, time.now() - time_start);
+ return ("pong from %s on %s in %gs"):format(pong.stanza.attr.from, pong.origin.id, time.now() - time_start);
end);
end
-def_env.dns = {};
-local adns = require"net.adns";
+def_env.dns = new_section("Commands to manage and inspect the internal DNS resolver");
+local adns = require"prosody.net.adns";
local function get_resolver(session)
local resolver = session.dns_resolver;
@@ -1525,43 +1883,54 @@ local function get_resolver(session)
return resolver;
end
+describe_command [[dns:lookup(name, type, class) - Do a DNS lookup]]
function def_env.dns:lookup(name, typ, class)
local resolver = get_resolver(self.session);
return resolver:lookup_promise(name, typ, class)
end
+describe_command [[dns:addnameserver(nameserver) - Add a nameserver to the list]]
function def_env.dns:addnameserver(...)
local resolver = get_resolver(self.session);
resolver._resolver:addnameserver(...)
return true
end
+describe_command [[dns:setnameserver(nameserver) - Replace the list of name servers with the supplied one]]
function def_env.dns:setnameserver(...)
local resolver = get_resolver(self.session);
resolver._resolver:setnameserver(...)
return true
end
+describe_command [[dns:purge() - Clear the DNS cache]]
function def_env.dns:purge()
local resolver = get_resolver(self.session);
resolver._resolver:purge()
return true
end
+describe_command [[dns:cache() - Show cached records]]
function def_env.dns:cache()
local resolver = get_resolver(self.session);
return true, "Cache:\n"..tostring(resolver._resolver.cache)
end
-def_env.http = {};
+def_env.http = new_section("Commands to inspect HTTP services");
+describe_command [[http:list(hosts) - Show HTTP endpoints]]
function def_env.http:list(hosts)
local print = self.session.print;
hosts = array.collect(set.new({ not hosts and "*" or nil }) + get_hosts_set(hosts)):sort(_sort_hosts);
- local output = format_table({
- { title = "Module", width = "20%" },
- { title = "URL", width = "80%" },
- }, 132);
+ local output_simple = format_table({
+ { title = "Module"; width = "1p" };
+ { title = "External URL"; width = "6p" };
+ }, self.session.width);
+ local output_split = format_table({
+ { title = "Module"; width = "1p" };
+ { title = "External URL"; width = "3p" };
+ { title = "Internal URL"; width = "3p" };
+ }, self.session.width);
for _, host in ipairs(hosts) do
local http_apps = modulemanager.get_items("http-provider", host);
@@ -1572,12 +1941,14 @@ function def_env.http:list(hosts)
else
print("HTTP endpoints on "..host..(http_host and (" (using "..http_host.."):") or ":"));
end
- print(output());
+ print(output_split());
for _, provider in ipairs(http_apps) do
local mod = provider._provided_by;
- local url = module:context(host):http_url(provider.name, provider.default_path);
+ local external = module:context(host):http_url(provider.name, provider.default_path);
+ local internal = module:context(host):http_url(provider.name, provider.default_path, "internal");
+ if external==internal then internal="" end
mod = mod and "mod_"..mod or ""
- print(output{mod, url});
+ print((internal=="" and output_simple or output_split){mod, external, internal});
end
print("");
end
@@ -1592,18 +1963,83 @@ function def_env.http:list(hosts)
return true;
end
-def_env.debug = {};
+def_env.watch = new_section("Commands for watching live logs from the server");
+
+describe_command [[watch:log() - Follow debug logs]]
+function def_env.watch:log()
+ local writing = false;
+ local sink = logger.add_simple_sink(function (source, level, message)
+ if writing then return; end
+ writing = true;
+ self.session.print(source, level, message);
+ writing = false;
+ end);
+
+ while self.session.is_connected() do
+ async.sleep(3);
+ end
+ if not logger.remove_sink(sink) then
+ module:log("warn", "Unable to remove watch:log() sink");
+ end
+end
+
+describe_command [[watch:stanzas(target, filter) - Watch live stanzas matching the specified target and filter]]
+local stanza_watchers = module:require("mod_debug_stanzas/watcher");
+function def_env.watch:stanzas(target_spec, filter_spec)
+ local function handler(event_type, stanza, session)
+ if stanza then
+ if event_type == "sent" then
+ self.session.print(("\n<!-- sent to %s -->"):format(session.id));
+ elseif event_type == "received" then
+ self.session.print(("\n<!-- received from %s -->"):format(session.id));
+ else
+ self.session.print(("\n<!-- %s (%s) -->"):format(event_type, session.id));
+ end
+ self.session.print(stanza);
+ elseif session then
+ self.session.print("\n<!-- session "..session.id.." "..event_type.." -->");
+ elseif event_type then
+ self.session.print("\n<!-- "..event_type.." -->");
+ end
+ end
+ stanza_watchers.add({
+ target_spec = {
+ jid = target_spec;
+ };
+ filter_spec = filter_spec and {
+ with_jid = filter_spec;
+ };
+ }, handler);
+
+ while self.session.is_connected() do
+ async.sleep(3);
+ end
+
+ stanza_watchers.remove(handler);
+end
+
+def_env.debug = new_section("Commands for debugging the server");
+
+describe_command [[debug:logevents(host) - Enable logging of fired events on host]]
function def_env.debug:logevents(host)
- helpers.log_host_events(host);
+ if host == "*" then
+ helpers.log_events(prosody.events);
+ elseif host == "http" then
+ helpers.log_events(require "prosody.net.http.server"._events);
+ return true
+ else
+ helpers.log_host_events(host);
+ end
return true;
end
+describe_command [[debug:events(host, event) - Show registered event handlers]]
function def_env.debug:events(host, event)
local events_obj;
if host and host ~= "*" then
if host == "http" then
- events_obj = require "net.http.server"._events;
+ events_obj = require "prosody.net.http.server"._events;
elseif not prosody.hosts[host] then
return false, "Unknown host: "..host;
else
@@ -1615,9 +2051,10 @@ function def_env.debug:events(host, event)
return true, helpers.show_events(events_obj, event);
end
+describe_command [[debug:timers() - Show information about scheduled timers]]
function def_env.debug:timers()
local print = self.session.print;
- local add_task = require"util.timer".add_task;
+ local add_task = require"prosody.util.timer".add_task;
local h, params = add_task.h, add_task.params;
local function normalize_time(t)
return t;
@@ -1671,10 +2108,70 @@ function def_env.debug:timers()
return true;
end
--- COMPAT: debug:timers() was timer:info() for some time in trunk
-def_env.timer = { info = def_env.debug.timers };
+describe_command [[debug:async() - Show information about pending asynchronous tasks]]
+function def_env.debug:async(runner_id)
+ local print = self.session.print;
+ local time_now = time.now();
+
+ if runner_id then
+ for runner, since in pairs(async.waiting_runners) do
+ if runner.id == runner_id then
+ print("ID ", runner.id);
+ local f = runner.func;
+ if f == async.default_runner_func then
+ print("Function ", tostring(runner.current_item).." (from work queue)");
+ else
+ print("Function ", tostring(f));
+ if st.is_stanza(runner.current_item) then
+ print("Stanza:")
+ print("\t"..runner.current_item:indent(2):pretty_print());
+ else
+ print("Work item", self.session.serialize(runner.current_item, "debug"));
+ end
+ end
+
+ print("Coroutine ", tostring(runner.thread).." ("..coroutine.status(runner.thread)..")");
+ print("Since ", since);
+ print("Status ", ("%s since %s (%0.2f seconds ago)"):format(runner.state, os.date("%Y-%m-%d %R:%S", math.floor(since)), time_now-since));
+ print("");
+ print(debug.traceback(runner.thread));
+ return true, "Runner is "..runner.state;
+ end
+ end
+ return nil, "Runner not found or is currently idle";
+ end
-def_env.stats = {};
+ local row = format_table({
+ { title = "ID"; width = 12 };
+ { title = "Function"; width = "10p" };
+ { title = "Status"; width = "16" };
+ { title = "Location"; width = "10p" };
+ }, self.session.width);
+ print(row())
+
+ local c = 0;
+ for runner, since in pairs(async.waiting_runners) do
+ c = c + 1;
+ local f = runner.func;
+ if f == async.default_runner_func then
+ f = runner.current_item;
+ end
+ -- We want to fetch the location in the code that the runner yielded from,
+ -- excluding util.async's wrapper code. A level of `2` assumes that we
+ -- yielded directly from a function in util.async. This is *currently* true
+ -- of all util.async yields, but it's fragile.
+ local location = debug.getinfo(runner.thread, 2);
+ print(row {
+ runner.id;
+ tostring(f);
+ ("%s (%0.2fs)"):format(runner.state, time_now - since);
+ location.short_src..(location.currentline and ":"..location.currentline or "");
+ });
+ end
+ return true, ("%d runners pending"):format(c);
+end
+
+def_env.stats = new_section("Commands to show internal statistics");
local short_units = {
seconds = "s",
@@ -1913,8 +2410,10 @@ local function new_stats_context(self)
return setmetatable({ session = self.session, stats = true, now = time.now() }, stats_mt);
end
+describe_command [[stats:show(pattern) - Show internal statistics, optionally filtering by name with a pattern.]]
+-- Undocumented currently, you can append :histogram() or :cfgraph() to stats:show() for rendered graphs.
function def_env.stats:show(name_filter)
- local statsman = require "core.statsmanager"
+ local statsman = require "prosody.core.statsmanager"
local collect = statsman.collect
if collect then
-- force collection if in manual mode
@@ -1934,6 +2433,176 @@ function def_env.stats:show(name_filter)
return displayed_stats;
end
+local command_metadata_schema = {
+ type = "object";
+ properties = {
+ section = { type = "string" };
+ section_desc = { type = "string" };
+
+ name = { type = "string" };
+ desc = { type = "string" };
+ help = { type = "string" };
+ args = {
+ type = "array";
+ items = {
+ type = "object";
+ properties = {
+ name = { type = "string", required = true };
+ type = { type = "string", required = false };
+ };
+ };
+ };
+ };
+
+ required = { "name", "section", "desc", "args" };
+};
+
+-- host_commands[section..":"..name][host] = handler
+-- host_commands[section..":"..name][false] = metadata
+local host_commands = {};
+
+local function new_item_handlers(command_host)
+ local function on_command_added(event)
+ local command = event.item;
+ local mod_name = command._provided_by and ("mod_"..command._provided_by) or "<unknown module>";
+ if not schema.validate(command_metadata_schema, command) or type(command.handler) ~= "function" then
+ module:log("warn", "Ignoring command added by %s: missing or invalid data", mod_name);
+ return;
+ end
+
+ local handler = command.handler;
+
+ if command_host then
+ if type(command.host_selector) ~= "string" then
+ module:log("warn", "Ignoring command %s:%s() added by %s - missing/invalid host_selector", command.section, command.name, mod_name);
+ return;
+ end
+ local qualified_name = command.section..":"..command.name;
+ local host_command_info = host_commands[qualified_name];
+ if not host_command_info then
+ local selector_index;
+ for i, arg in ipairs(command.args) do
+ if arg.name == command.host_selector then
+ selector_index = i + 1; -- +1 to account for 'self'
+ break;
+ end
+ end
+ if not selector_index then
+ module:log("warn", "Command %s() host selector argument '%s' not found - not registering", qualified_name, command.host_selector);
+ return;
+ end
+ host_command_info = {
+ [false] = {
+ host_selector = command.host_selector;
+ handler = function (...)
+ local selected_host = select(2, jid_split((select(selector_index, ...))));
+ if type(selected_host) ~= "string" then
+ return nil, "Invalid or missing argument '"..command.host_selector.."'";
+ end
+ if not prosody.hosts[selected_host] then
+ return nil, "Unknown host: "..selected_host;
+ end
+ local host_handler = host_commands[qualified_name][selected_host];
+ if not host_handler then
+ return nil, "This command is not available on "..selected_host;
+ end
+ return host_handler(...);
+ end;
+ };
+ };
+ host_commands[qualified_name] = host_command_info;
+ end
+ if host_command_info[command_host] then
+ module:log("warn", "Command %s() is already registered - overwriting with %s", qualified_name, mod_name);
+ end
+ host_command_info[command_host] = handler;
+ end
+
+ local section_t = def_env[command.section];
+ if not section_t then
+ section_t = {};
+ def_env[command.section] = section_t;
+ end
+
+ if command_host then
+ section_t[command.name] = host_commands[command.section..":"..command.name][false].handler;
+ else
+ section_t[command.name] = command.handler;
+ end
+
+ local section_mt = getmetatable(section_t);
+ if not section_mt then
+ section_mt = {};
+ setmetatable(section_t, section_mt);
+ end
+ local section_help = section_mt.help;
+ if not section_help then
+ section_help = {
+ desc = command.section_desc;
+ commands = {};
+ };
+ section_mt.help = section_help;
+ end
+
+ section_help.commands[command.name] = {
+ desc = command.desc;
+ full = command.help;
+ args = array(command.args);
+ module = command._provided_by;
+ };
+
+ module:log("debug", "Shell command added by mod_%s: %s:%s()", mod_name, command.section, command.name);
+ end
+
+ local function on_command_removed(event)
+ local command = event.item;
+
+ local handler = event.item.handler;
+ if type(handler) ~= "function" or not schema.validate(command_metadata_schema, command) then
+ return;
+ end
+
+ local section_t = def_env[command.section];
+ if not section_t or section_t[command.name] ~= handler then
+ return;
+ end
+
+ section_t[command.name] = nil;
+ if next(section_t) == nil then -- Delete section if empty
+ def_env[command.section] = nil;
+ end
+
+ if command_host then
+ local host_command_info = host_commands[command.section..":"..command.name];
+ if host_command_info then
+ -- Remove our host handler
+ host_command_info[command_host] = nil;
+ -- Clean up entire command entry if there are no per-host handlers left
+ local any_hosts = false;
+ for k in pairs(host_command_info) do
+ if k then -- metadata is false, ignore it
+ any_hosts = true;
+ break;
+ end
+ end
+ if not any_hosts then
+ host_commands[command.section..":"..command.name] = nil;
+ end
+ end
+ end
+ end
+ return on_command_added, on_command_removed;
+end
+
+module:handle_items("shell-command", new_item_handlers());
+
+function module.add_host(host_module)
+ host_module:handle_items("shell-command", new_item_handlers(host_module.host));
+end
+
+function module.unload()
+ stanza_watchers.cleanup();
+end
-------------
diff --git a/plugins/mod_admin_socket.lua b/plugins/mod_admin_socket.lua
index 157e746c..ad6aa5d7 100644
--- a/plugins/mod_admin_socket.lua
+++ b/plugins/mod_admin_socket.lua
@@ -8,7 +8,7 @@ if have_unix and type(unix) == "function" then
-- constructor was exported instead of a module table. Due to the lack of a
-- proper release of LuaSocket, distros have settled on shipping either the
-- last RC tag or some commit since then.
- -- Here we accomodate both variants.
+ -- Here we accommodate both variants.
unix = { stream = unix };
end
if not have_unix or type(unix) ~= "table" then
@@ -16,10 +16,10 @@ if not have_unix or type(unix) ~= "table" then
return;
end
-local server = require "net.server";
+local server = require "prosody.net.server";
-local adminstream = require "util.adminstream";
-local st = require "util.stanza";
+local adminstream = require "prosody.util.adminstream";
+local st = require "prosody.util.stanza";
local socket_path = module:get_option_path("admin_socket", "prosody.sock", "data");
diff --git a/plugins/mod_admin_telnet.lua b/plugins/mod_admin_telnet.lua
index 15220ec9..e93f61a9 100644
--- a/plugins/mod_admin_telnet.lua
+++ b/plugins/mod_admin_telnet.lua
@@ -12,8 +12,8 @@ module:depends("admin_shell");
local console_listener = { default_port = 5582; default_mode = "*a"; interface = "127.0.0.1" };
-local async = require "util.async";
-local st = require "util.stanza";
+local async = require "prosody.util.async";
+local st = require "prosody.util.stanza";
local def_env = module:shared("admin_shell/env");
local default_env_mt = { __index = def_env };
diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua
index c742ebb8..f54d2db9 100644
--- a/plugins/mod_announce.lua
+++ b/plugins/mod_announce.lua
@@ -6,12 +6,15 @@
-- COPYING file in the source package for more information.
--
-local st, jid = require "util.stanza", require "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;
-local is_admin = require "core.usermanager".is_admin;
function send_to_online(message, host)
+ host = host or module.host;
local sessions;
if host then
sessions = { [host] = hosts[host] };
@@ -34,6 +37,29 @@ 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
function handle_announcement(event)
@@ -45,8 +71,8 @@ function handle_announcement(event)
return; -- Not an announcement
end
- if not is_admin(stanza.attr.from, host) then
- -- Not an admin? Not allowed!
+ if not module:may(":send-announcement", event) then
+ -- Not allowed!
module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from);
return;
end
@@ -63,7 +89,7 @@ end
module:hook("message/host", handle_announcement);
-- Ad-hoc command (XEP-0133)
-local dataforms_new = require "util.dataforms".new;
+local dataforms_new = require "prosody.util.dataforms".new;
local announce_layout = dataforms_new{
title = "Making an Announcement";
instructions = "Fill out this form to make an announcement to all\nactive users of this service.";
@@ -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_auth_anonymous.lua b/plugins/mod_auth_anonymous.lua
index 90646e71..21373698 100644
--- a/plugins/mod_auth_anonymous.lua
+++ b/plugins/mod_auth_anonymous.lua
@@ -7,8 +7,8 @@
--
-- luacheck: ignore 212
-local new_sasl = require "util.sasl".new;
-local datamanager = require "util.datamanager";
+local new_sasl = require "prosody.util.sasl".new;
+local datamanager = require "prosody.util.datamanager";
local hosts = prosody.hosts;
local allow_storage = module:get_option_boolean("allow_anonymous_storage", false);
diff --git a/plugins/mod_auth_insecure.lua b/plugins/mod_auth_insecure.lua
index dc5ee616..133c3292 100644
--- a/plugins/mod_auth_insecure.lua
+++ b/plugins/mod_auth_insecure.lua
@@ -7,9 +7,9 @@
--
-- luacheck: ignore 212
-local datamanager = require "util.datamanager";
-local new_sasl = require "util.sasl".new;
-local saslprep = require "util.encodings".stringprep.saslprep;
+local datamanager = require "prosody.util.datamanager";
+local new_sasl = require "prosody.util.sasl".new;
+local saslprep = require "prosody.util.encodings".stringprep.saslprep;
local host = module.host;
local provider = { name = "insecure" };
@@ -27,6 +27,7 @@ function provider.set_password(username, password)
return nil, "Password fails SASLprep.";
end
if account then
+ account.updated = os.time();
account.password = password;
return datamanager.store(username, host, "accounts", account);
end
@@ -38,7 +39,8 @@ function provider.user_exists(username)
end
function provider.create_user(username, password)
- return datamanager.store(username, host, "accounts", {password = password});
+ local now = os.time();
+ return datamanager.store(username, host, "accounts", { created = now; updated = now; password = password });
end
function provider.delete_user(username)
diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua
index cf851eef..806eb9bd 100644
--- a/plugins/mod_auth_internal_hashed.lua
+++ b/plugins/mod_auth_internal_hashed.lua
@@ -9,26 +9,27 @@
local max = math.max;
-local scram_hashers = require "util.sasl.scram".hashers;
-local usermanager = require "core.usermanager";
-local generate_uuid = require "util.uuid".generate;
-local new_sasl = require "util.sasl".new;
-local hex = require"util.hex";
+local scram_hashers = require "prosody.util.sasl.scram".hashers;
+local generate_uuid = require "prosody.util.uuid".generate;
+local new_sasl = require "prosody.util.sasl".new;
+local hex = require"prosody.util.hex";
local to_hex, from_hex = hex.encode, hex.decode;
-local saslprep = require "util.encodings".stringprep.saslprep;
-local secure_equals = require "util.hashes".equals;
+local saslprep = require "prosody.util.encodings".stringprep.saslprep;
+local secure_equals = require "prosody.util.hashes".equals;
local log = module._log;
local host = module.host;
local accounts = module:open_store("accounts");
-local hash_name = module:get_option_string("password_hash", "SHA-1");
+local hash_name = module:get_option_enum("password_hash", "SHA-1", "SHA-256");
local get_auth_db = assert(scram_hashers[hash_name], "SCRAM-"..hash_name.." not supported by SASL library");
local scram_name = "scram_"..hash_name:gsub("%-","_"):lower();
-- Default; can be set per-user
-local default_iteration_count = module:get_option_number("default_iteration_count", 10000);
+local default_iteration_count = module:get_option_integer("default_iteration_count", 10000, 4096);
+
+local tokenauth = module:depends("tokenauth");
-- define auth provider
local provider = {};
@@ -36,6 +37,9 @@ local provider = {};
function provider.test_password(username, password)
log("debug", "test password for user '%s'", username);
local credentials = accounts:get(username) or {};
+ if credentials.disabled then
+ return nil, "Account disabled.";
+ end
password = saslprep(password);
if not password then
return nil, "Password fails SASLprep.";
@@ -86,11 +90,22 @@ function provider.set_password(username, password)
account.server_key = server_key_hex
account.password = nil;
+ account.updated = os.time();
return accounts:set(username, account);
end
return nil, "Account not available.";
end
+function provider.get_account_info(username)
+ local account = accounts:get(username);
+ if not account then return nil, "Account not available"; end
+ return {
+ created = account.created;
+ password_updated = account.updated;
+ enabled = not account.disabled;
+ };
+end
+
function provider.user_exists(username)
local account = accounts:get(username);
if not account then
@@ -100,13 +115,36 @@ function provider.user_exists(username)
return true;
end
+function provider.is_enabled(username) -- luacheck: ignore 212
+ local info, err = provider.get_account_info(username);
+ if not info then return nil, err; end
+ return info.enabled;
+end
+
+function provider.enable(username)
+ -- TODO map store?
+ local account = accounts:get(username);
+ account.disabled = nil;
+ account.updated = os.time();
+ return accounts:set(username, account);
+end
+
+function provider.disable(username, meta)
+ local account = accounts:get(username);
+ account.disabled = true;
+ account.disabled_meta = meta;
+ account.updated = os.time();
+ return accounts:set(username, account);
+end
+
function provider.users()
return accounts:users();
end
function provider.create_user(username, password)
+ local now = os.time();
if password == nil then
- return accounts:set(username, {});
+ return accounts:set(username, { created = now; updated = now; disabled = true });
end
local salt = generate_uuid();
local valid, stored_key, server_key = get_auth_db(password, salt, default_iteration_count);
@@ -117,7 +155,8 @@ function provider.create_user(username, password)
local server_key_hex = to_hex(server_key);
return accounts:set(username, {
stored_key = stored_key_hex, server_key = server_key_hex,
- salt = salt, iteration_count = default_iteration_count
+ salt = salt, iteration_count = default_iteration_count,
+ created = now, updated = now;
});
end
@@ -127,8 +166,8 @@ end
function provider.get_sasl_handler()
local testpass_authentication_profile = {
- plain_test = function(_, username, password, realm)
- return usermanager.test_password(username, realm, password), true;
+ plain_test = function(_, username, password)
+ return provider.test_password(username, password), provider.is_enabled(username);
end,
[scram_name] = function(_, username)
local credentials = accounts:get(username);
@@ -145,8 +184,9 @@ function provider.get_sasl_handler()
local iteration_count, salt = credentials.iteration_count, credentials.salt;
stored_key = stored_key and from_hex(stored_key);
server_key = server_key and from_hex(server_key);
- return stored_key, server_key, iteration_count, salt, true;
- end
+ return stored_key, server_key, iteration_count, salt, not credentials.disabled;
+ end;
+ oauthbearer = tokenauth.sasl_handler(provider, "oauth2", module:shared("tokenauth/oauthbearer_config"));
};
return new_sasl(host, testpass_authentication_profile);
end
diff --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua
index 8a50e820..6cced803 100644
--- a/plugins/mod_auth_internal_plain.lua
+++ b/plugins/mod_auth_internal_plain.lua
@@ -6,10 +6,10 @@
-- COPYING file in the source package for more information.
--
-local usermanager = require "core.usermanager";
-local new_sasl = require "util.sasl".new;
-local saslprep = require "util.encodings".stringprep.saslprep;
-local secure_equals = require "util.hashes".equals;
+local usermanager = require "prosody.core.usermanager";
+local new_sasl = require "prosody.util.sasl".new;
+local saslprep = require "prosody.util.encodings".stringprep.saslprep;
+local secure_equals = require "prosody.util.hashes".equals;
local log = module._log;
local host = module.host;
@@ -22,6 +22,9 @@ local provider = {};
function provider.test_password(username, password)
log("debug", "test password for user '%s'", username);
local credentials = accounts:get(username) or {};
+ if credentials.disabled then
+ return nil, "Account disabled.";
+ end
password = saslprep(password);
if not password then
return nil, "Password fails SASLprep.";
@@ -48,11 +51,21 @@ function provider.set_password(username, password)
local account = accounts:get(username);
if account then
account.password = password;
+ account.updated = os.time();
return accounts:set(username, account);
end
return nil, "Account not available.";
end
+function provider.get_account_info(username)
+ local account = accounts:get(username);
+ if not account then return nil, "Account not available"; end
+ return {
+ created = account.created;
+ password_updated = account.updated;
+ };
+end
+
function provider.user_exists(username)
local account = accounts:get(username);
if not account then
@@ -67,11 +80,18 @@ function provider.users()
end
function provider.create_user(username, password)
+ local now = os.time();
+ if password == nil then
+ return accounts:set(username, { created = now, updated = now, disabled = true });
+ end
password = saslprep(password);
if not password then
return nil, "Password fails SASLprep.";
end
- return accounts:set(username, {password = password});
+ return accounts:set(username, {
+ password = password;
+ created = now, updated = now;
+ });
end
function provider.delete_user(username)
diff --git a/plugins/mod_auth_ldap.lua b/plugins/mod_auth_ldap.lua
index 4d484aaa..569cef6b 100644
--- a/plugins/mod_auth_ldap.lua
+++ b/plugins/mod_auth_ldap.lua
@@ -1,7 +1,6 @@
-- mod_auth_ldap
-local jid_split = require "util.jid".split;
-local new_sasl = require "util.sasl".new;
+local new_sasl = require "prosody.util.sasl".new;
local lualdap = require "lualdap";
local function ldap_filter_escape(s)
@@ -13,14 +12,21 @@ local ldap_server = module:get_option_string("ldap_server", "localhost");
local ldap_rootdn = module:get_option_string("ldap_rootdn", "");
local ldap_password = module:get_option_string("ldap_password", "");
local ldap_tls = module:get_option_boolean("ldap_tls");
-local ldap_scope = module:get_option_string("ldap_scope", "subtree");
+local ldap_scope = module:get_option_enum("ldap_scope", "subtree", "base", "onelevel");
local ldap_filter = module:get_option_string("ldap_filter", "(uid=$user)"):gsub("%%s", "$user", 1);
local ldap_base = assert(module:get_option_string("ldap_base"), "ldap_base is a required option for ldap");
-local ldap_mode = module:get_option_string("ldap_mode", "bind");
+local ldap_mode = module:get_option_enum("ldap_mode", "bind", "getpasswd");
local ldap_admins = module:get_option_string("ldap_admin_filter",
module:get_option_string("ldap_admins")); -- COMPAT with mistake in documentation
local host = ldap_filter_escape(module:get_option_string("realm", module.host));
+if ldap_admins then
+ module:log("error", "The 'ldap_admin_filter' option has been deprecated, "..
+ "and will be ignored. Equivalent functionality may be added in "..
+ "the future if there is demand."
+ );
+end
+
-- Initiate connection
local ld = nil;
module.unload = function() if ld then pcall(ld, ld.close); end end
@@ -133,22 +139,4 @@ else
module:log("error", "Unsupported ldap_mode %s", tostring(ldap_mode));
end
-if ldap_admins then
- function provider.is_admin(jid)
- local username, user_host = jid_split(jid);
- if user_host ~= module.host then
- return false;
- end
- return ldap_do("search", 2, {
- base = ldap_base;
- scope = ldap_scope;
- sizelimit = 1;
- filter = ldap_admins:gsub("%$(%a+)", {
- user = ldap_filter_escape(username);
- host = host;
- });
- });
- end
-end
-
module:provides("auth", provider);
diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua
index 17687959..96324734 100644
--- a/plugins/mod_authz_internal.lua
+++ b/plugins/mod_authz_internal.lua
@@ -1,59 +1,350 @@
-local array = require "util.array";
-local it = require "util.iterators";
-local set = require "util.set";
-local jid_split = require "util.jid".split;
-local normalize = require "util.jid".prep;
+local array = require "prosody.util.array";
+local it = require "prosody.util.iterators";
+local set = require "prosody.util.set";
+local jid_split, jid_bare, jid_host = import("prosody.util.jid", "split", "bare", "host");
+local normalize = require "prosody.util.jid".prep;
+local roles = require "prosody.util.roles";
+
+local config_global_admin_jids = module:context("*"):get_option_set("admins", {}) / normalize;
local config_admin_jids = module:get_option_inherited_set("admins", {}) / normalize;
local host = module.host;
-local role_store = module:open_store("roles");
-local role_map_store = module:open_store("roles", "map");
+local host_suffix = host:gsub("^[^%.]+%.", "");
+
+local hosts = prosody.hosts;
+local is_anon_host = module:get_option_string("authentication") == "anonymous";
+local default_user_role = module:get_option_string("default_user_role", is_anon_host and "prosody:guest" or "prosody:registered");
+
+local is_component = hosts[host].type == "component";
+local host_user_role, server_user_role, public_user_role;
+if is_component then
+ host_user_role = module:get_option_string("host_user_role", "prosody:registered");
+ server_user_role = module:get_option_string("server_user_role");
+ public_user_role = module:get_option_string("public_user_role");
+end
+
+local role_store = module:open_store("account_roles");
+local role_map_store = module:open_store("account_roles", "map");
-local admin_role = { ["prosody:admin"] = true };
+local role_registry = {};
-function get_user_roles(user)
- if config_admin_jids:contains(user.."@"..host) then
- return admin_role;
+function register_role(role)
+ if role_registry[role.name] ~= nil then
+ return error("A role '"..role.name.."' is already registered");
end
- return role_store:get(user);
+ if not roles.is_role(role) then
+ -- Convert table syntax to real role object
+ for i, inherited_role in ipairs(role.inherits or {}) do
+ if type(inherited_role) == "string" then
+ role.inherits[i] = assert(role_registry[inherited_role], "The named role '"..inherited_role.."' is not registered");
+ end
+ end
+ if not role.permissions then role.permissions = {}; end
+ for _, allow_permission in ipairs(role.allow or {}) do
+ role.permissions[allow_permission] = true;
+ end
+ for _, deny_permission in ipairs(role.deny or {}) do
+ role.permissions[deny_permission] = false;
+ end
+ role = roles.new(role);
+ end
+ role_registry[role.name] = role;
end
-function set_user_roles(user, roles)
- role_store:set(user, roles)
- return true;
+-- Default roles
+
+-- For untrusted guest/anonymous users
+register_role {
+ name = "prosody:guest";
+ priority = 15;
+};
+
+-- For e.g. self-registered accounts
+register_role {
+ name = "prosody:registered";
+ priority = 25;
+ inherits = { "prosody:guest" };
+};
+
+
+-- For trusted/provisioned accounts
+register_role {
+ name = "prosody:member";
+ priority = 35;
+ inherits = { "prosody:registered" };
+};
+
+-- For administrators, e.g. of a host
+register_role {
+ name = "prosody:admin";
+ priority = 50;
+ inherits = { "prosody:member" };
+};
+
+-- For server operators (full access)
+register_role {
+ name = "prosody:operator";
+ priority = 75;
+ inherits = { "prosody:admin" };
+};
+
+
+-- Process custom roles from config
+
+local custom_roles = module:get_option_array("custom_roles", {});
+for n, role_config in ipairs(custom_roles) do
+ local ok, err = pcall(register_role, role_config);
+ if not ok then
+ module:log("error", "Error registering custom role %s: %s", role_config.name or tostring(n), err);
+ end
end
-function get_users_with_role(role)
- local storage_role_users = it.to_array(it.keys(role_map_store:get_all(role) or {}));
- if role == "prosody:admin" then
- local config_admin_users = config_admin_jids / function (admin_jid)
+-- Process custom permissions from config
+
+local config_add_perms = module:get_option("add_permissions", {});
+local config_remove_perms = module:get_option("remove_permissions", {});
+
+for role_name, added_permissions in pairs(config_add_perms) do
+ if not role_registry[role_name] then
+ module:log("error", "Cannot add permissions to unknown role '%s'", role_name);
+ else
+ for _, permission in ipairs(added_permissions) do
+ role_registry[role_name]:set_permission(permission, true, true);
+ end
+ end
+end
+
+for role_name, removed_permissions in pairs(config_remove_perms) do
+ if not role_registry[role_name] then
+ module:log("error", "Cannot remove permissions from unknown role '%s'", role_name);
+ else
+ for _, permission in ipairs(removed_permissions) do
+ role_registry[role_name]:set_permission(permission, false, true);
+ end
+ end
+end
+
+-- Public API
+
+-- Get the primary role of a user
+function get_user_role(user)
+ local bare_jid = user.."@"..host;
+
+ -- Check config first
+ if config_global_admin_jids:contains(bare_jid) then
+ return role_registry["prosody:operator"];
+ elseif config_admin_jids:contains(bare_jid) then
+ return role_registry["prosody:admin"];
+ end
+
+ -- Check storage
+ local stored_roles, err = role_store:get(user);
+ if not stored_roles then
+ if err then
+ -- Unable to fetch role, fail
+ return nil, err;
+ end
+ -- No role set, use default role
+ return role_registry[default_user_role];
+ end
+ if stored_roles._default == nil then
+ -- No primary role explicitly set, return default
+ return role_registry[default_user_role];
+ end
+ local primary_stored_role = role_registry[stored_roles._default];
+ if not primary_stored_role then
+ return nil, "unknown-role";
+ end
+ return primary_stored_role;
+end
+
+-- Set the primary role of a user
+function set_user_role(user, role_name)
+ local role = role_registry[role_name];
+ if not role then
+ return error("Cannot assign default user an unknown role: "..tostring(role_name));
+ end
+ local keys_update = {
+ _default = role_name;
+ -- Primary role cannot be secondary role
+ [role_name] = role_map_store.remove;
+ };
+ if role_name == default_user_role then
+ -- Don't store default
+ keys_update._default = role_map_store.remove;
+ end
+ local ok, err = role_map_store:set_keys(user, keys_update);
+ if not ok then
+ return nil, err;
+ end
+ return role;
+end
+
+function add_user_secondary_role(user, role_name)
+ if not role_registry[role_name] then
+ return error("Cannot assign default user an unknown role: "..tostring(role_name));
+ end
+ role_map_store:set(user, role_name, true);
+end
+
+function remove_user_secondary_role(user, role_name)
+ role_map_store:set(user, role_name, nil);
+end
+
+function get_user_secondary_roles(user)
+ local stored_roles, err = role_store:get(user);
+ if not stored_roles then
+ if err then
+ -- Unable to fetch role, fail
+ return nil, err;
+ end
+ -- No role set
+ return {};
+ end
+ stored_roles._default = nil;
+ for role_name in pairs(stored_roles) do
+ stored_roles[role_name] = role_registry[role_name];
+ end
+ return stored_roles;
+end
+
+function user_can_assume_role(user, role_name)
+ local primary_role = get_user_role(user);
+ if primary_role and primary_role.name == role_name then
+ return true;
+ end
+ local secondary_roles = get_user_secondary_roles(user);
+ if secondary_roles and secondary_roles[role_name] then
+ return true;
+ end
+ return false;
+end
+
+-- This function is *expensive*
+function get_users_with_role(role_name)
+ local function role_filter(username, default_role) --luacheck: ignore 212/username
+ return default_role == role_name;
+ end
+ local primary_role_users = set.new(it.to_array(it.filter(role_filter, pairs(role_map_store:get_all("_default") or {}))));
+ local secondary_role_users = set.new(it.to_array(it.keys(role_map_store:get_all(role_name) or {})));
+
+ local config_set;
+ if role_name == "prosody:admin" then
+ config_set = config_admin_jids;
+ elseif role_name == "prosody:operator" then
+ config_set = config_global_admin_jids;
+ end
+ if config_set then
+ local config_admin_users = config_set / function (admin_jid)
local j_node, j_host = jid_split(admin_jid);
if j_host == host then
return j_node;
end
end;
- return it.to_array(config_admin_users + set.new(storage_role_users));
+ return it.to_array(config_admin_users + primary_role_users + secondary_role_users);
end
- return storage_role_users;
+ return it.to_array(primary_role_users + secondary_role_users);
end
-function get_jid_roles(jid)
- if config_admin_jids:contains(jid) then
- return admin_role;
+function get_jid_role(jid)
+ local bare_jid = jid_bare(jid);
+ if config_global_admin_jids:contains(bare_jid) then
+ return role_registry["prosody:operator"];
+ elseif config_admin_jids:contains(bare_jid) then
+ return role_registry["prosody:admin"];
+ elseif is_component then
+ local user_host = jid_host(bare_jid);
+ if host_user_role and user_host == host_suffix then
+ return role_registry[host_user_role];
+ elseif server_user_role and hosts[user_host] then
+ return role_registry[server_user_role];
+ elseif public_user_role then
+ return role_registry[public_user_role];
+ end
end
return nil;
end
-function set_jid_roles(jid) -- luacheck: ignore 212
+function set_jid_role(jid, role_name) -- luacheck: ignore 212
return false;
end
-function get_jids_with_role(role)
+function get_jids_with_role(role_name)
-- Fetch role users from storage
- local storage_role_jids = array.map(get_users_with_role(role), function (username)
+ local storage_role_jids = array.map(get_users_with_role(role_name), function (username)
return username.."@"..host;
end);
- if role == "prosody:admin" then
+ if role_name == "prosody:admin" then
return it.to_array(config_admin_jids + set.new(storage_role_jids));
+ elseif role_name == "prosody:operator" then
+ return it.to_array(config_global_admin_jids + set.new(storage_role_jids));
end
return storage_role_jids;
end
+
+function add_default_permission(role_name, action, policy)
+ local role = role_registry[role_name];
+ if not role then
+ module:log("warn", "Attempt to add default permission for unknown role: %s", role_name);
+ return nil, "no-such-role";
+ end
+ if policy == nil then policy = true; end
+ module:log("debug", "Adding policy %s for permission %s on role %s", policy, action, role_name);
+ return role:set_permission(action, policy);
+end
+
+function get_role_by_name(role_name)
+ return assert(role_registry[role_name], role_name);
+end
+
+function get_all_roles()
+ return role_registry;
+end
+
+-- COMPAT: Migrate from 0.12 role storage
+local function do_migration(migrate_host)
+ local old_role_store = assert(module:context(migrate_host):open_store("roles"));
+ local new_role_store = assert(module:context(migrate_host):open_store("account_roles"));
+
+ local migrated, failed, skipped = 0, 0, 0;
+ -- Iterate all users
+ for username in assert(old_role_store:users()) do
+ local old_roles = it.to_array(it.filter(function (k) return k:sub(1,1) ~= "_"; end, it.keys(old_role_store:get(username))));
+ if #old_roles == 1 then
+ local ok, err = new_role_store:set(username, {
+ _default = old_roles[1];
+ });
+ if ok then
+ migrated = migrated + 1;
+ else
+ failed = failed + 1;
+ print("EE: Failed to store new role info for '"..username.."': "..err);
+ end
+ else
+ print("WW: User '"..username.."' has multiple roles and cannot be automatically migrated");
+ skipped = skipped + 1;
+ end
+ end
+ return migrated, failed, skipped;
+end
+
+function module.command(arg)
+ if arg[1] == "migrate" then
+ table.remove(arg, 1);
+ local migrate_host = arg[1];
+ if not migrate_host or not prosody.hosts[migrate_host] then
+ print("EE: Please supply a valid host to migrate to the new role storage");
+ return 1;
+ end
+
+ -- Initialize storage layer
+ require "prosody.core.storagemanager".initialize_host(migrate_host);
+
+ print("II: Migrating roles...");
+ local migrated, failed, skipped = do_migration(migrate_host);
+ print(("II: %d migrated, %d failed, %d skipped"):format(migrated, failed, skipped));
+ return (failed + skipped == 0) and 0 or 1;
+ else
+ print("EE: Unknown command: "..(arg[1] or "<none given>"));
+ print(" Hint: try 'migrate'?");
+ end
+end
diff --git a/plugins/mod_blocklist.lua b/plugins/mod_blocklist.lua
index 6b8ce16c..6587c8b1 100644
--- a/plugins/mod_blocklist.lua
+++ b/plugins/mod_blocklist.lua
@@ -9,34 +9,29 @@
-- This module implements XEP-0191: Blocking Command
--
-local user_exists = require"core.usermanager".user_exists;
-local rostermanager = require"core.rostermanager";
+local user_exists = require"prosody.core.usermanager".user_exists;
+local rostermanager = require"prosody.core.rostermanager";
local is_contact_subscribed = rostermanager.is_contact_subscribed;
local is_contact_pending_in = rostermanager.is_contact_pending_in;
local load_roster = rostermanager.load_roster;
local save_roster = rostermanager.save_roster;
-local st = require"util.stanza";
+local st = require"prosody.util.stanza";
local st_error_reply = st.error_reply;
-local jid_prep = require"util.jid".prep;
-local jid_split = require"util.jid".split;
+local jid_prep = require"prosody.util.jid".prep;
+local jid_split = require"prosody.util.jid".split;
local storage = module:open_store();
local sessions = prosody.hosts[module.host].sessions;
local full_sessions = prosody.full_sessions;
--- First level cache of blocklists by username.
--- Weak table so may randomly expire at any time.
-local cache = setmetatable({}, { __mode = "v" });
-
--- Second level of caching, keeps a fixed number of items, also anchors
--- items in the above cache.
+-- Cache of blocklists, keeps a fixed number of items.
--
-- The size of this affects how often we will need to load a blocklist from
-- disk, which we want to avoid during routing. On the other hand, we don't
-- want to use too much memory either, so this can be tuned by advanced
-- users. TODO use science to figure out a better default, 64 is just a guess.
-local cache_size = module:get_option_number("blocklist_cache_size", 64);
-local cache2 = require"util.cache".new(cache_size);
+local cache_size = module:get_option_integer("blocklist_cache_size", 256, 1);
+local blocklist_cache = require"prosody.util.cache".new(cache_size);
local null_blocklist = {};
@@ -48,12 +43,12 @@ local function set_blocklist(username, blocklist)
return ok, err;
end
-- Successful save, update the cache
- cache2:set(username, blocklist);
- cache[username] = blocklist;
+ blocklist_cache:set(username, blocklist);
return true;
end
-- Migrates from the old mod_privacy storage
+-- TODO mod_privacy was removed in 0.10.0, this should be phased out
local function migrate_privacy_list(username)
local legacy_data = module:open_store("privacy"):get(username);
if not legacy_data or not legacy_data.lists or not legacy_data.default then return; end
@@ -77,8 +72,15 @@ local function migrate_privacy_list(username)
return migrated_data;
end
+if not module:get_option_boolean("migrate_legacy_blocking", true) then
+ migrate_privacy_list = function (username)
+ module:log("debug", "Migrating from mod_privacy disabled, user '%s' will start with a fresh blocklist", username);
+ return nil;
+ end
+end
+
local function get_blocklist(username)
- local blocklist = cache2:get(username);
+ local blocklist = blocklist_cache:get(username);
if not blocklist then
if not user_exists(username, module.host) then
return null_blocklist;
@@ -90,9 +92,8 @@ local function get_blocklist(username)
if not blocklist then
blocklist = { [false] = { created = os.time(); }; };
end
- cache2:set(username, blocklist);
+ blocklist_cache:set(username, blocklist);
end
- cache[username] = blocklist;
return blocklist;
end
@@ -100,7 +101,7 @@ module:hook("iq-get/self/urn:xmpp:blocking:blocklist", function (event)
local origin, stanza = event.origin, event.stanza;
local username = origin.username;
local reply = st.reply(stanza):tag("blocklist", { xmlns = "urn:xmpp:blocking" });
- local blocklist = cache[username] or get_blocklist(username);
+ local blocklist = get_blocklist(username);
for jid in pairs(blocklist) do
if jid then
reply:tag("item", { jid = jid }):up();
@@ -159,7 +160,7 @@ local function edit_blocklist(event)
return true;
end
- local blocklist = cache[username] or get_blocklist(username);
+ local blocklist = get_blocklist(username);
local new_blocklist = {
-- We set the [false] key to something as a signal not to migrate privacy lists
@@ -233,8 +234,7 @@ module:hook("iq-set/self/urn:xmpp:blocking:unblock", edit_blocklist, -1);
-- Cache invalidation, solved!
module:hook_global("user-deleted", function (event)
if event.host == module.host then
- cache2:set(event.username, nil);
- cache[event.username] = nil;
+ blocklist_cache:set(event.username, nil);
end
end);
@@ -249,7 +249,7 @@ module:hook("iq-error/self/blocklist-push", function (event)
end);
local function is_blocked(user, jid)
- local blocklist = cache[user] or get_blocklist(user);
+ local blocklist = get_blocklist(user);
if blocklist[jid] then return true; end
local node, host = jid_split(jid);
return blocklist[host] or node and blocklist[node..'@'..host];
@@ -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
diff --git a/plugins/mod_bookmarks.lua b/plugins/mod_bookmarks.lua
index d67915f8..be665d0f 100644
--- a/plugins/mod_bookmarks.lua
+++ b/plugins/mod_bookmarks.lua
@@ -1,10 +1,10 @@
-local mm = require "core.modulemanager";
+local mm = require "prosody.core.modulemanager";
if mm.get_modules_for_host(module.host):contains("bookmarks2") then
error("mod_bookmarks and mod_bookmarks2 are conflicting, please disable one of them.", 0);
end
-local st = require "util.stanza";
-local jid_split = require "util.jid".split;
+local st = require "prosody.util.stanza";
+local jid_split = require "prosody.util.jid".split;
local mod_pep = module:depends "pep";
local private_storage = module:open_store("private", "map");
diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua
index 11bfb51d..091a7d81 100644
--- a/plugins/mod_bosh.lua
+++ b/plugins/mod_bosh.lua
@@ -8,21 +8,21 @@
module:set_global();
-local new_xmpp_stream = require "util.xmppstream".new;
-local sm = require "core.sessionmanager";
+local new_xmpp_stream = require "prosody.util.xmppstream".new;
+local sm = require "prosody.core.sessionmanager";
local sm_destroy_session = sm.destroy_session;
-local new_uuid = require "util.uuid".generate;
+local new_uuid = require "prosody.util.uuid".generate;
local core_process_stanza = prosody.core_process_stanza;
-local st = require "util.stanza";
-local logger = require "util.logger";
+local st = require "prosody.util.stanza";
+local logger = require "prosody.util.logger";
local log = module._log;
-local initialize_filters = require "util.filters".initialize;
+local initialize_filters = require "prosody.util.filters".initialize;
local math_min = math.min;
local tostring, type = tostring, type;
local traceback = debug.traceback;
-local runner = require"util.async".runner;
-local nameprep = require "util.encodings".stringprep.nameprep;
-local cache = require "util.cache";
+local runner = require"prosody.util.async".runner;
+local nameprep = require "prosody.util.encodings".stringprep.nameprep;
+local cache = require "prosody.util.cache";
local xmlns_streams = "http://etherx.jabber.org/streams";
local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
@@ -36,16 +36,16 @@ local BOSH_HOLD = 1;
local BOSH_MAX_REQUESTS = 2;
-- The number of seconds a BOSH session should remain open with no requests
-local bosh_max_inactivity = module:get_option_number("bosh_max_inactivity", 60);
+local bosh_max_inactivity = module:get_option_period("bosh_max_inactivity", 60);
-- The minimum amount of time between requests with no payload
-local bosh_max_polling = module:get_option_number("bosh_max_polling", 5);
+local bosh_max_polling = module:get_option_period("bosh_max_polling", 5);
-- The maximum amount of time that the server will hold onto a request before replying
-- (the client can set this to a lower value when it connects, if it chooses)
-local bosh_max_wait = module:get_option_number("bosh_max_wait", 120);
+local bosh_max_wait = module:get_option_period("bosh_max_wait", 120);
local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure");
local cross_domain = module:get_option("cross_domain_bosh");
-local stanza_size_limit = module:get_option_number("c2s_stanza_size_limit", 1024*256);
+local stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024*256, 10000);
if cross_domain ~= nil then
module:log("info", "The 'cross_domain_bosh' option has been deprecated");
@@ -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();
@@ -456,7 +456,7 @@ function stream_callbacks.streamopened(context, attr)
if session.notopen then
local features = st.stanza("stream:features");
- module:context(session.host):fire_event("stream-features", { origin = session, features = features });
+ module:context(session.host):fire_event("stream-features", { origin = session, features = features, stream = attr });
session.send(features);
session.notopen = nil;
end
@@ -559,6 +559,6 @@ function module.add_host(module)
});
end
-if require"core.modulemanager".get_modules_for_host("*"):contains(module.name) then
+if require"prosody.core.modulemanager".get_modules_for_host("*"):contains(module.name) then
module:add_host();
end
diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua
index c8f54fa7..09d4be08 100644
--- a/plugins/mod_c2s.lua
+++ b/plugins/mod_c2s.lua
@@ -8,15 +8,15 @@
module:set_global();
-local add_task = require "util.timer".add_task;
-local new_xmpp_stream = require "util.xmppstream".new;
-local nameprep = require "util.encodings".stringprep.nameprep;
-local sessionmanager = require "core.sessionmanager";
-local statsmanager = require "core.statsmanager";
-local st = require "util.stanza";
+local add_task = require "prosody.util.timer".add_task;
+local new_xmpp_stream = require "prosody.util.xmppstream".new;
+local nameprep = require "prosody.util.encodings".stringprep.nameprep;
+local sessionmanager = require "prosody.core.sessionmanager";
+local statsmanager = require "prosody.core.statsmanager";
+local st = require "prosody.util.stanza";
local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session;
-local uuid_generate = require "util.uuid".generate;
-local async = require "util.async";
+local uuid_generate = require "prosody.util.uuid".generate;
+local async = require "prosody.util.async";
local runner = async.runner;
local tostring, type = tostring, type;
@@ -25,10 +25,16 @@ local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
local log = module._log;
-local c2s_timeout = module:get_option_number("c2s_timeout", 300);
-local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5);
+local c2s_timeout = module:get_option_period("c2s_timeout", "5 minutes");
+local stream_close_timeout = module:get_option_period("c2s_close_timeout", 5);
local opt_keepalives = module:get_option_boolean("c2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true));
-local stanza_size_limit = module:get_option_number("c2s_stanza_size_limit", 1024*256);
+local stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024*256,10000);
+
+local advertised_idle_timeout = 14*60; -- default in all net.server implementations
+local network_settings = module:get_option("network_settings");
+if type(network_settings) == "table" and type(network_settings.read_timeout) == "number" then
+ advertised_idle_timeout = network_settings.read_timeout;
+end
local measure_connections = module:metric("gauge", "connections", "", "Established c2s connections", {"host", "type", "ip_family"});
@@ -117,8 +123,7 @@ function stream_callbacks._streamopened(session, attr)
session.secure = true;
session.encrypted = true;
- local sock = session.conn:socket();
- local info = sock.info and sock:info();
+ local info = session.conn:ssl_info();
if type(info) == "table" then
(session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
session.compressed = info.compression;
@@ -129,8 +134,19 @@ function stream_callbacks._streamopened(session, attr)
end
local features = st.stanza("stream:features");
- hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
+ hosts[session.host].events.fire_event("stream-features", { origin = session, features = features, stream = attr });
if features.tags[1] or session.full_jid then
+ if stanza_size_limit or advertised_idle_timeout then
+ features:reset();
+ local limits = features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" });
+ if stanza_size_limit then
+ limits:text_tag("max-bytes", string.format("%d", stanza_size_limit));
+ end
+ if advertised_idle_timeout then
+ limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout));
+ end
+ limits:reset();
+ end
send(features);
else
if session.secure then
@@ -248,6 +264,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
@@ -260,8 +279,18 @@ local function disconnect_user_sessions(reason, leave_resource)
end
module:hook_global("user-password-changed", disconnect_user_sessions({ condition = "reset", text = "Password changed" }, true), 200);
-module:hook_global("user-roles-changed", disconnect_user_sessions({ condition = "reset", text = "Roles changed" }), 200);
+module:hook_global("user-role-changed", disconnect_user_sessions({ condition = "reset", text = "Role changed" }), 200);
module:hook_global("user-deleted", disconnect_user_sessions({ condition = "not-authorized", text = "Account deleted" }), 200);
+module:hook_global("user-disabled", disconnect_user_sessions({ condition = "not-authorized", text = "Account disabled" }), 200);
+
+module:hook_global("c2s-session-updated", function (event)
+ sessions[event.session.conn] = event.session;
+ local replaced_conn = event.replaced_conn;
+ if replaced_conn then
+ sessions[replaced_conn] = nil;
+ replaced_conn:close();
+ end
+end);
function runner_callbacks:ready()
if self.data.conn then
@@ -293,10 +322,10 @@ function listener.onconnect(conn)
if conn:ssl() then
session.secure = true;
session.encrypted = true;
+ session.ssl_ctx = conn:sslctx();
-- Check if TLS compression is used
- local sock = conn:socket();
- local info = sock.info and sock:info();
+ local info = conn:ssl_info();
if type(info) == "table" then
(session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
session.compressed = info.compression;
@@ -354,11 +383,13 @@ function listener.onconnect(conn)
end
end
- if c2s_timeout then
- add_task(c2s_timeout, function ()
+ if c2s_timeout < math.huge then
+ session.c2s_timeout = add_task(c2s_timeout, function ()
if session.type == "c2s_unauthed" then
(session.log or log)("debug", "Connection still not authenticated after c2s_timeout=%gs, closing it", c2s_timeout);
session:close("connection-timeout");
+ else
+ session.c2s_timeout = nil;
end
end);
end
@@ -426,7 +457,7 @@ module:hook("c2s-read-timeout", keepalive, -1);
module:hook("server-stopping", function(event) -- luacheck: ignore 212/event
-- Close ports
- local pm = require "core.portmanager";
+ local pm = require "prosody.core.portmanager";
for _, netservice in pairs(module.items["net-provider"]) do
pm.unregister_service(netservice.name, netservice);
end
diff --git a/plugins/mod_carbons.lua b/plugins/mod_carbons.lua
index 7a5b757c..3fa34be7 100644
--- a/plugins/mod_carbons.lua
+++ b/plugins/mod_carbons.lua
@@ -3,9 +3,9 @@
--
-- This file is MIT/X11 licensed.
-local st = require "util.stanza";
-local jid_bare = require "util.jid".bare;
-local jid_resource = require "util.jid".resource;
+local st = require "prosody.util.stanza";
+local jid_bare = require "prosody.util.jid".bare;
+local jid_resource = require "prosody.util.jid".resource;
local xmlns_carbons = "urn:xmpp:carbons:2";
local xmlns_forward = "urn:xmpp:forward:0";
local full_sessions, bare_sessions = prosody.full_sessions, prosody.bare_sessions;
diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua
index f57c4381..86ceb980 100644
--- a/plugins/mod_component.lua
+++ b/plugins/mod_component.lua
@@ -10,16 +10,16 @@ module:set_global();
local t_concat = table.concat;
local tostring, type = tostring, type;
-local xpcall = require "util.xpcall".xpcall;
+local xpcall = require "prosody.util.xpcall".xpcall;
local traceback = debug.traceback;
-local logger = require "util.logger";
-local sha1 = require "util.hashes".sha1;
-local st = require "util.stanza";
+local logger = require "prosody.util.logger";
+local sha1 = require "prosody.util.hashes".sha1;
+local st = require "prosody.util.stanza";
-local jid_split = require "util.jid".split;
-local new_xmpp_stream = require "util.xmppstream".new;
-local uuid_gen = require "util.uuid".generate;
+local jid_host = require "prosody.util.jid".host;
+local new_xmpp_stream = require "prosody.util.xmppstream".new;
+local uuid_gen = require "prosody.util.uuid".generate;
local core_process_stanza = prosody.core_process_stanza;
local hosts = prosody.hosts;
@@ -27,7 +27,8 @@ local hosts = prosody.hosts;
local log = module._log;
local opt_keepalives = module:get_option_boolean("component_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true));
-local stanza_size_limit = module:get_option_number("component_stanza_size_limit", module:get_option_number("s2s_stanza_size_limit", 1024*512));
+local stanza_size_limit = module:get_option_integer("component_stanza_size_limit",
+ module:get_option_integer("s2s_stanza_size_limit", 1024 * 512, 10000), 10000);
local sessions = module:shared("sessions");
@@ -85,7 +86,7 @@ function module.add_host(module)
end
if env.connected then
- local policy = module:get_option_string("component_conflict_resolve", "kick_new");
+ local policy = module:get_option_enum("component_conflict_resolve", "kick_new", "kick_old");
if policy == "kick_old" then
env.session:close{ condition = "conflict", text = "Replaced by a new connection" };
else -- kick_new
@@ -222,22 +223,19 @@ function stream_callbacks.handlestanza(session, stanza)
end
if not stanza.attr.xmlns or stanza.attr.xmlns == "jabber:client" then
local from = stanza.attr.from;
- if from then
- if session.component_validate_from then
- local _, domain = jid_split(stanza.attr.from);
- if domain ~= session.host then
- -- Return error
- session.log("warn", "Component sent stanza with missing or invalid 'from' address");
- session:close{
- condition = "invalid-from";
- text = "Component tried to send from address <"..tostring(from)
- .."> which is not in domain <"..tostring(session.host)..">";
- };
- return;
- end
+ if session.component_validate_from then
+ if not from or (jid_host(from) ~= session.host) then
+ -- Return error
+ session.log("warn", "Component sent stanza with missing or invalid 'from' address");
+ session:close{
+ condition = "invalid-from";
+ text = "Component tried to send from address <"..(from or "< [missing 'from' attribute] >")
+ .."> which is not in domain <"..tostring(session.host)..">";
+ };
+ return;
end
- else
- stanza.attr.from = session.host; -- COMPAT: Strictly we shouldn't allow this
+ elseif not from then
+ stanza.attr.from = session.host;
end
if not stanza.attr.to then
session.log("warn", "Rejecting stanza with no 'to' address");
diff --git a/plugins/mod_cron.lua b/plugins/mod_cron.lua
index 33d97df6..29c1aa93 100644
--- a/plugins/mod_cron.lua
+++ b/plugins/mod_cron.lua
@@ -1,9 +1,10 @@
module:set_global();
-local async = require("util.async");
-local datetime = require("util.datetime");
+local async = require("prosody.util.async");
-local periods = { hourly = 3600; daily = 86400; weekly = 7 * 86400 }
+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 = {}
@@ -14,18 +15,16 @@ function module.add_host(host_module)
local function save_task(task, started_at) last_run_times:set(nil, task.id, started_at); end
+ local function restore_task(task) if task.last == nil then task.last = last_run_times:get(nil, task.id); end end
+
local function task_added(event)
local task = event.item;
if task.name == nil then task.name = task.when; end
if task.id == nil then task.id = event.source.name .. "/" .. task.name:gsub("%W", "_"):lower(); end
- if task.last == nil then task.last = last_run_times:get(nil, task.id); end
+ task.period = host_module:get_option_period(task.id:gsub("/", "_") .. "_period", "1" .. task.when, 60, 86400 * 7 * 53);
+ task.restore = restore_task;
task.save = save_task;
- module:log("debug", "%s task %s added, last run %s", task.when, task.id,
- task.last and datetime.datetime(task.last) or "never");
- if task.last == nil then
- local now = os.time();
- task.last = now - now % periods[task.when];
- end
+ module:log("debug", "%s task %s added", task.when, task.id);
return true
end
@@ -40,26 +39,55 @@ function module.add_host(host_module)
function host_module.unload() active_hosts[host_module.host] = nil; end
end
-local function should_run(when, last) return not last or last + periods[when] * 0.995 <= os.time() end
+local function should_run(task, last) return not last or last + task.period * 0.995 <= os.time() end
local function run_task(task)
+ task:restore();
+ if not should_run(task, task.last) then return end
local started_at = os.time();
task:run(started_at);
task.last = started_at;
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
- module:log("debug", "Considering %s task %s (%s)", task.when, task.id, task.run);
- if should_run(task.when, task.last) then task_runner:run(task); end
- end
+ 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);
+
+module:add_item("shell-command", {
+ section = "cron";
+ section_desc = "View and manage recurring tasks";
+ name = "tasks";
+ desc = "View registered tasks";
+ args = {};
+ handler = function(self, filter_host)
+ local format_table = require("prosody.util.human.io").table;
+ local it = require("util.iterators");
+ local row = format_table({
+ { title = "Host"; width = "2p" };
+ { title = "Task"; width = "3p" };
+ { title = "Desc"; width = "3p" };
+ { title = "When"; width = "1p" };
+ { title = "Last run"; width = "20" };
+ }, self.session.width);
+ local print = self.session.print;
+ print(row());
+ for host in it.sorted_pairs(filter_host and { [filter_host] = true } or active_hosts) do
+ for _, task in ipairs(module:context(host):get_host_items("task")) do
+ print(row({ host; task.id; task.name; task.when; task.last and os.date("%Y-%m-%d %R:%S", task.last) or "never" }));
+ end
+ end
+ end;
+});
diff --git a/plugins/mod_csi.lua b/plugins/mod_csi.lua
index 458ff491..76a5afd4 100644
--- a/plugins/mod_csi.lua
+++ b/plugins/mod_csi.lua
@@ -1,10 +1,12 @@
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
local xmlns_csi = "urn:xmpp:csi:0";
local csi_feature = st.stanza("csi", { xmlns = xmlns_csi });
-local csi_handler_available = nil;
+local change = module:metric("counter", "changes", "events", "CSI state changes", {"csi_state"});
+local count = module:metric("gauge", "state", "sessions", "", { "csi_state" });
+
module:hook("stream-features", function (event)
- if event.origin.username and csi_handler_available then
+ if event.origin.username then
event.features:add_child(csi_feature);
end
end);
@@ -13,6 +15,7 @@ function refire_event(name)
return function (event)
if event.origin.username then
event.origin.state = event.stanza.name;
+ change:with_labels(event.stanza.name):add(1);
module:fire_event(name, event);
return true;
end
@@ -22,14 +25,22 @@ end
module:hook("stanza/"..xmlns_csi..":active", refire_event("csi-client-active"));
module:hook("stanza/"..xmlns_csi..":inactive", refire_event("csi-client-inactive"));
-function module.load()
- if prosody.hosts[module.host].events._handlers["csi-client-active"] then
- csi_handler_available = true;
- module:set_status("core", "CSI handler module loaded");
- else
- csi_handler_available = false;
- module:set_status("warn", "No CSI handler module loaded");
+module:hook_global("stats-update", function()
+ local sessions = prosody.hosts[module.host].sessions;
+ if not sessions then return end
+ local active, inactive, flushing = 0, 0, 0;
+ for _, user_session in pairs(sessions) do
+ for _, session in pairs(user_session.sessions) do
+ if session.state == "inactive" then
+ inactive = inactive + 1;
+ elseif session.state == "active" then
+ active = active + 1;
+ elseif session.state == "flushing" then
+ flushing = flushing + 1;
+ end
+ end
end
-end
-module:hook("module-loaded", module.load);
-module:hook("module-unloaded", module.load);
+ count:with_labels("active"):set(active);
+ count:with_labels("inactive"):set(inactive);
+ count:with_labels("flushing"):set(flushing);
+end);
diff --git a/plugins/mod_csi_simple.lua b/plugins/mod_csi_simple.lua
index fdd1fd6c..379371ef 100644
--- a/plugins/mod_csi_simple.lua
+++ b/plugins/mod_csi_simple.lua
@@ -6,14 +6,14 @@
module:depends"csi"
-local jid = require "util.jid";
-local st = require "util.stanza";
-local dt = require "util.datetime";
-local filters = require "util.filters";
-local timer = require "util.timer";
+local jid = require "prosody.util.jid";
+local st = require "prosody.util.stanza";
+local dt = require "prosody.util.datetime";
+local filters = require "prosody.util.filters";
+local timer = require "prosody.util.timer";
-local queue_size = module:get_option_number("csi_queue_size", 256);
-local resume_delay = module:get_option_number("csi_resume_inactive_delay", 5);
+local queue_size = module:get_option_integer("csi_queue_size", 256, 1);
+local resume_delay = module:get_option_period("csi_resume_inactive_delay", 5);
local important_payloads = module:get_option_set("csi_important_payloads", { });
@@ -116,6 +116,9 @@ local flush_reasons = module:metric(
{ "reason" }
);
+local flush_sizes = module:metric("histogram", "flush_stanza_count", "", "Number of stanzas flushed at once", {},
+ { buckets = { 0, 1, 2, 4, 8, 16, 32, 64, 128, 256 } }):with_labels();
+
local function manage_buffer(stanza, session)
local ctr = session.csi_counter or 0;
if session.state ~= "inactive" then
@@ -129,6 +132,7 @@ local function manage_buffer(stanza, session)
session.csi_measure_buffer_hold = nil;
end
flush_reasons:with_labels(why or "important"):add(1);
+ flush_sizes:sample(ctr);
session.log("debug", "Flushing buffer (%s; queue size is %d)", why or "important", session.csi_counter);
session.state = "flushing";
module:fire_event("csi-flushing", { session = session });
@@ -147,6 +151,7 @@ local function flush_buffer(data, session)
session.log("debug", "Flushing buffer (%s; queue size is %d)", "client activity", session.csi_counter);
session.state = "flushing";
module:fire_event("csi-flushing", { session = session });
+ flush_sizes:sample(ctr);
flush_reasons:with_labels("client activity"):add(1);
if session.csi_measure_buffer_hold then
session.csi_measure_buffer_hold();
@@ -258,7 +263,7 @@ function module.command(arg)
return 1;
end
-- luacheck: ignore 212/self
- local xmppstream = require "util.xmppstream";
+ local xmppstream = require "prosody.util.xmppstream";
local input_session = { notopen = true }
local stream_callbacks = { stream_ns = "jabber:client", default_ns = "jabber:client" };
function stream_callbacks:handlestanza(stanza)
diff --git a/plugins/mod_debug_reset.lua b/plugins/mod_debug_reset.lua
new file mode 100644
index 00000000..5964aff0
--- /dev/null
+++ b/plugins/mod_debug_reset.lua
@@ -0,0 +1,36 @@
+-- This module will "reset" the server when the client connection count drops
+-- to zero. This is somewhere between a reload and a full process restart.
+-- It is useful to ensure isolation between test runs, for example. It may
+-- also be of use for some kinds of manual testing.
+
+module:set_global();
+
+local hostmanager = require "prosody.core.hostmanager";
+
+local function do_reset()
+ module:log("info", "Performing reset...");
+ local hosts = {};
+ for host in pairs(prosody.hosts) do
+ table.insert(hosts, host);
+ end
+ module:fire_event("server-resetting");
+ for _, host in ipairs(hosts) do
+ hostmanager.deactivate(host);
+ hostmanager.activate(host);
+ module:log("info", "Reset complete");
+ module:fire_event("server-reset");
+ end
+end
+
+function module.add_host(host_module)
+ host_module:hook("resource-unbind", function ()
+ if next(prosody.full_sessions) == nil then
+ do_reset();
+ end
+ end);
+end
+
+local console_env = module:shared("/*/admin_shell/env");
+console_env.debug_reset = {
+ reset = do_reset;
+};
diff --git a/plugins/mod_debug_stanzas/watcher.lib.lua b/plugins/mod_debug_stanzas/watcher.lib.lua
new file mode 100644
index 00000000..1e673648
--- /dev/null
+++ b/plugins/mod_debug_stanzas/watcher.lib.lua
@@ -0,0 +1,220 @@
+local filters = require "prosody.util.filters";
+local jid = require "prosody.util.jid";
+local set = require "prosody.util.set";
+
+local client_watchers = {};
+
+-- active_filters[session] = {
+-- filter_func = filter_func;
+-- downstream = { cb1, cb2, ... };
+-- }
+local active_filters = {};
+
+local function subscribe_session_stanzas(session, handler, reason)
+ if active_filters[session] then
+ table.insert(active_filters[session].downstream, handler);
+ if reason then
+ handler(reason, nil, session);
+ end
+ return;
+ end
+ local downstream = { handler };
+ active_filters[session] = {
+ filter_in = function (stanza)
+ module:log("debug", "NOTIFY WATCHER %d", #downstream);
+ for i = 1, #downstream do
+ downstream[i]("received", stanza, session);
+ end
+ return stanza;
+ end;
+ filter_out = function (stanza)
+ module:log("debug", "NOTIFY WATCHER %d", #downstream);
+ for i = 1, #downstream do
+ downstream[i]("sent", stanza, session);
+ end
+ return stanza;
+ end;
+ downstream = downstream;
+ };
+ filters.add_filter(session, "stanzas/in", active_filters[session].filter_in);
+ filters.add_filter(session, "stanzas/out", active_filters[session].filter_out);
+ if reason then
+ handler(reason, nil, session);
+ end
+end
+
+local function unsubscribe_session_stanzas(session, handler, reason)
+ local active_filter = active_filters[session];
+ if not active_filter then
+ return;
+ end
+ for i = #active_filter.downstream, 1, -1 do
+ if active_filter.downstream[i] == handler then
+ table.remove(active_filter.downstream, i);
+ if reason then
+ handler(reason, nil, session);
+ end
+ end
+ end
+ if #active_filter.downstream == 0 then
+ filters.remove_filter(session, "stanzas/in", active_filter.filter_in);
+ filters.remove_filter(session, "stanzas/out", active_filter.filter_out);
+ end
+ active_filters[session] = nil;
+end
+
+local function unsubscribe_all_from_session(session, reason)
+ local active_filter = active_filters[session];
+ if not active_filter then
+ return;
+ end
+ for i = #active_filter.downstream, 1, -1 do
+ local handler = table.remove(active_filter.downstream, i);
+ if reason then
+ handler(reason, nil, session);
+ end
+ end
+ filters.remove_filter(session, "stanzas/in", active_filter.filter_in);
+ filters.remove_filter(session, "stanzas/out", active_filter.filter_out);
+ active_filters[session] = nil;
+end
+
+local function unsubscribe_handler_from_all(handler, reason)
+ for session in pairs(active_filters) do
+ unsubscribe_session_stanzas(session, handler, reason);
+ end
+end
+
+local s2s_watchers = {};
+
+module:hook("s2sin-established", function (event)
+ for _, watcher in ipairs(s2s_watchers) do
+ if watcher.target_spec == event.session.from_host then
+ subscribe_session_stanzas(event.session, watcher.handler, "opened");
+ end
+ end
+end);
+
+module:hook("s2sout-established", function (event)
+ for _, watcher in ipairs(s2s_watchers) do
+ if watcher.target_spec == event.session.to_host then
+ subscribe_session_stanzas(event.session, watcher.handler, "opened");
+ end
+ end
+end);
+
+module:hook("s2s-closed", function (event)
+ unsubscribe_all_from_session(event.session, "closed");
+end);
+
+local watched_hosts = set.new();
+
+local handler_map = setmetatable({}, { __mode = "kv" });
+
+local function add_stanza_watcher(spec, orig_handler)
+ local function filtering_handler(event_type, stanza, session)
+ if stanza and spec.filter_spec then
+ if spec.filter_spec.with_jid then
+ if event_type == "sent" and (not stanza.attr.from or not jid.compare(stanza.attr.from, spec.filter_spec.with_jid)) then
+ return;
+ elseif event_type == "received" and (not stanza.attr.to or not jid.compare(stanza.attr.to, spec.filter_spec.with_jid)) then
+ return;
+ end
+ end
+ end
+ return orig_handler(event_type, stanza, session);
+ end
+ handler_map[orig_handler] = filtering_handler;
+ if spec.target_spec.jid then
+ local target_is_remote_host = not jid.node(spec.target_spec.jid) and not prosody.hosts[spec.target_spec.jid];
+
+ if target_is_remote_host then
+ -- Watch s2s sessions
+ table.insert(s2s_watchers, {
+ target_spec = spec.target_spec.jid;
+ handler = filtering_handler;
+ orig_handler = orig_handler;
+ });
+
+ -- Scan existing s2sin for matches
+ for session in pairs(prosody.incoming_s2s) do
+ if spec.target_spec.jid == session.from_host then
+ subscribe_session_stanzas(session, filtering_handler, "attached");
+ end
+ end
+ -- Scan existing s2sout for matches
+ for local_host, local_session in pairs(prosody.hosts) do --luacheck: ignore 213/local_host
+ for remote_host, remote_session in pairs(local_session.s2sout) do
+ if spec.target_spec.jid == remote_host then
+ subscribe_session_stanzas(remote_session, filtering_handler, "attached");
+ end
+ end
+ end
+ else
+ table.insert(client_watchers, {
+ target_spec = spec.target_spec.jid;
+ handler = filtering_handler;
+ orig_handler = orig_handler;
+ });
+ local host = jid.host(spec.target_spec.jid);
+ if not watched_hosts:contains(host) and prosody.hosts[host] then
+ module:context(host):hook("resource-bind", function (event)
+ for _, watcher in ipairs(client_watchers) do
+ module:log("debug", "NEW CLIENT: %s vs %s", event.session.full_jid, watcher.target_spec);
+ if jid.compare(event.session.full_jid, watcher.target_spec) then
+ module:log("debug", "MATCH");
+ subscribe_session_stanzas(event.session, watcher.handler, "opened");
+ else
+ module:log("debug", "NO MATCH");
+ end
+ end
+ end);
+
+ module:context(host):hook("resource-unbind", function (event)
+ unsubscribe_all_from_session(event.session, "closed");
+ end);
+
+ watched_hosts:add(host);
+ end
+ for full_jid, session in pairs(prosody.full_sessions) do
+ if jid.compare(full_jid, spec.target_spec.jid) then
+ subscribe_session_stanzas(session, filtering_handler, "attached");
+ end
+ end
+ end
+ else
+ error("No recognized target selector");
+ end
+end
+
+local function remove_stanza_watcher(orig_handler)
+ local handler = handler_map[orig_handler];
+ unsubscribe_handler_from_all(handler, "detached");
+ handler_map[orig_handler] = nil;
+
+ for i = #client_watchers, 1, -1 do
+ if client_watchers[i].orig_handler == orig_handler then
+ table.remove(client_watchers, i);
+ end
+ end
+
+ for i = #s2s_watchers, 1, -1 do
+ if s2s_watchers[i].orig_handler == orig_handler then
+ table.remove(s2s_watchers, i);
+ end
+ end
+end
+
+local function cleanup(reason)
+ client_watchers = {};
+ s2s_watchers = {};
+ for session in pairs(active_filters) do
+ unsubscribe_all_from_session(session, reason or "cancelled");
+ end
+end
+
+return {
+ add = add_stanza_watcher;
+ remove = remove_stanza_watcher;
+ cleanup = cleanup;
+};
diff --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua
index 66082333..a0a8bcb9 100644
--- a/plugins/mod_dialback.lua
+++ b/plugins/mod_dialback.lua
@@ -10,12 +10,12 @@ local hosts = _G.hosts;
local log = module._log;
-local st = require "util.stanza";
-local sha256_hash = require "util.hashes".sha256;
-local sha256_hmac = require "util.hashes".hmac_sha256;
-local secure_equals = require "util.hashes".equals;
-local nameprep = require "util.encodings".stringprep.nameprep;
-local uuid_gen = require"util.uuid".generate;
+local st = require "prosody.util.stanza";
+local sha256_hash = require "prosody.util.hashes".sha256;
+local sha256_hmac = require "prosody.util.hashes".hmac_sha256;
+local secure_equals = require "prosody.util.hashes".equals;
+local nameprep = require "prosody.util.encodings".stringprep.nameprep;
+local uuid_gen = require"prosody.util.uuid".generate;
local xmlns_stream = "http://etherx.jabber.org/streams";
diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua
index 9d2991bf..3517344d 100644
--- a/plugins/mod_disco.lua
+++ b/plugins/mod_disco.lua
@@ -6,13 +6,12 @@
-- COPYING file in the source package for more information.
--
-local get_children = require "core.hostmanager".get_children;
-local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
-local um_is_admin = require "core.usermanager".is_admin;
-local jid_split = require "util.jid".split;
-local jid_bare = require "util.jid".bare;
-local st = require "util.stanza"
-local calculate_hash = require "util.caps".calculate_hash;
+local get_children = require "prosody.core.hostmanager".get_children;
+local is_contact_subscribed = require "prosody.core.rostermanager".is_contact_subscribed;
+local jid_split = require "prosody.util.jid".split;
+local jid_bare = require "prosody.util.jid".bare;
+local st = require "prosody.util.stanza"
+local calculate_hash = require "prosody.util.caps".calculate_hash;
local expose_admins = module:get_option_boolean("disco_expose_admins", false);
@@ -162,14 +161,16 @@ module:hook("s2s-stream-features", function (event)
end
end);
+module:default_permission("prosody:admin", ":be-discovered-admin");
+
-- Handle disco requests to user accounts
if module:get_host_type() ~= "local" then return end -- skip for components
module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function(event)
local origin, stanza = event.origin, event.stanza;
local node = stanza.tags[1].attr.node;
local username = jid_split(stanza.attr.to) or origin.username;
- local is_admin = um_is_admin(stanza.attr.to or origin.full_jid, module.host)
- if not stanza.attr.to or (expose_admins and is_admin) or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+ local target_is_admin = module:may(":be-discovered-admin", stanza.attr.to or origin.full_jid);
+ if not stanza.attr.to or (expose_admins and target_is_admin) or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
if node and node ~= "" then
local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node});
reply:tag("feature", { var = "http://jabber.org/protocol/disco#info" }):up();
@@ -187,7 +188,7 @@ module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function(
end
local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'});
if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
- if is_admin then
+ if target_is_admin then
reply:tag('identity', {category='account', type='admin'}):up();
elseif prosody.hosts[module.host].users.name == "anonymous" then
reply:tag('identity', {category='account', type='anonymous'}):up();
diff --git a/plugins/mod_external_services.lua b/plugins/mod_external_services.lua
index ae418fd8..ade1e327 100644
--- a/plugins/mod_external_services.lua
+++ b/plugins/mod_external_services.lua
@@ -1,22 +1,22 @@
-local dt = require "util.datetime";
-local base64 = require "util.encodings".base64;
-local hashes = require "util.hashes";
-local st = require "util.stanza";
-local jid = require "util.jid";
-local array = require "util.array";
-local set = require "util.set";
+local dt = require "prosody.util.datetime";
+local base64 = require "prosody.util.encodings".base64;
+local hashes = require "prosody.util.hashes";
+local st = require "prosody.util.stanza";
+local jid = require "prosody.util.jid";
+local array = require "prosody.util.array";
+local set = require "prosody.util.set";
local default_host = module:get_option_string("external_service_host", module.host);
-local default_port = module:get_option_number("external_service_port");
+local default_port = module:get_option_integer("external_service_port", nil, 1, 65535);
local default_secret = module:get_option_string("external_service_secret");
-local default_ttl = module:get_option_number("external_service_ttl", 86400);
+local default_ttl = module:get_option_period("external_service_ttl", "1 day");
local configured_services = module:get_option_array("external_services", {});
local access = module:get_option_set("external_service_access", {});
--- https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
+-- https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00
local function behave_turn_rest_credentials(srv, item, secret)
local ttl = default_ttl;
if type(item.ttl) == "number" then
diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua
index 0c44f481..1a31c51f 100644
--- a/plugins/mod_groups.lua
+++ b/plugins/mod_groups.lua
@@ -10,8 +10,8 @@
local groups;
local members;
-local datamanager = require "util.datamanager";
-local jid_prep = require "util.jid".prep;
+local datamanager = require "prosody.util.datamanager";
+local jid_prep = require "prosody.util.jid".prep;
local module_host = module:get_host();
diff --git a/plugins/mod_http.lua b/plugins/mod_http.lua
index 0cee26c4..c13a2363 100644
--- a/plugins/mod_http.lua
+++ b/plugins/mod_http.lua
@@ -11,24 +11,26 @@ pcall(function ()
module:depends("http_errors");
end);
-local portmanager = require "core.portmanager";
-local moduleapi = require "core.moduleapi";
+local portmanager = require "prosody.core.portmanager";
+local moduleapi = require "prosody.core.moduleapi";
local url_parse = require "socket.url".parse;
local url_build = require "socket.url".build;
-local normalize_path = require "util.http".normalize_path;
-local set = require "util.set";
+local http_util = require "prosody.util.http";
+local normalize_path = http_util.normalize_path;
+local set = require "prosody.util.set";
+local array = require "prosody.util.array";
-local ip_util = require "util.ip";
+local ip_util = require "prosody.util.ip";
local new_ip = ip_util.new_ip;
local match_ip = ip_util.match;
local parse_cidr = ip_util.parse_cidr;
-local server = require "net.http.server";
+local server = require "prosody.net.http.server";
server.set_default_host(module:get_option_string("http_default_host"));
-server.set_option("body_size_limit", module:get_option_number("http_max_content_size"));
-server.set_option("buffer_size_limit", module:get_option_number("http_max_buffer_size"));
+server.set_option("body_size_limit", module:get_option_number("http_max_content_size", nil, 0));
+server.set_option("buffer_size_limit", module:get_option_number("http_max_buffer_size", nil, 0));
-- CORS settings
local cors_overrides = module:get_option("http_cors_override", {});
@@ -36,7 +38,7 @@ local opt_methods = module:get_option_set("access_control_allow_methods", { "GET
local opt_headers = module:get_option_set("access_control_allow_headers", { "Content-Type" });
local opt_origins = module:get_option_set("access_control_allow_origins");
local opt_credentials = module:get_option_boolean("access_control_allow_credentials", false);
-local opt_max_age = module:get_option_number("access_control_max_age", 2 * 60 * 60);
+local opt_max_age = module:get_option_period("access_control_max_age", "2 hours");
local opt_default_cors = module:get_option_boolean("http_default_cors_enabled", true);
local function get_http_event(host, app_path, key)
@@ -75,11 +77,12 @@ end
local ports_by_scheme = { http = 80, https = 443, };
-- Helper to deduce a module's external URL
-function moduleapi.http_url(module, app_name, default_path)
+function moduleapi.http_url(module, app_name, default_path, mode)
app_name = app_name or (module.name:gsub("^http_", ""));
local external_url = url_parse(module:get_option_string("http_external_url"));
- if external_url then
+ if external_url and mode ~= "internal" then
+ -- Current URL does not depend on knowing which ports are used, only configuration.
local url = {
scheme = external_url.scheme;
host = external_url.host;
@@ -91,6 +94,36 @@ function moduleapi.http_url(module, app_name, default_path)
return url_build(url);
end
+ if prosody.process_type ~= "prosody" then
+ -- We generally don't open ports outside of Prosody, so we can't rely on
+ -- portmanager to tell us which ports and services are used and derive the
+ -- URL from that, so instead we derive it entirely from configuration.
+ local https_ports = module:get_option_array("https_ports", { 5281 });
+ local scheme = "https";
+ local port = tonumber(https_ports[1]);
+ if not port then
+ -- https is disabled and no http_external_url set
+ scheme = "http";
+ local http_ports = module:get_option_array("http_ports", { 5280 });
+ port = tonumber(http_ports[1]);
+ if not port then
+ return "http://disabled.invalid/";
+ end
+ end
+
+ local url = {
+ scheme = scheme;
+ host = module:get_option_string("http_host", module.global and module:get_option_string("http_default_host") or module.host);
+ port = port;
+ path = get_base_path(module, app_name, default_path or "/" .. app_name);
+ }
+ if ports_by_scheme[url.scheme] == url.port then
+ url.port = nil
+ end
+ return url_build(url);
+ end
+
+ -- Use portmanager to find the actual port of https or http services
local services = portmanager.get_active_services();
local http_services = services:get("https") or services:get("http") or {};
for interface, ports in pairs(http_services) do -- luacheck: ignore 213/interface
@@ -112,12 +145,16 @@ function moduleapi.http_url(module, app_name, default_path)
return "http://disabled.invalid/";
end
+local function header_set_tostring(header_value)
+ return array(header_value:items()):concat(", ");
+end
+
local function apply_cors_headers(response, methods, headers, max_age, allow_credentials, allowed_origins, origin)
if allowed_origins and not allowed_origins[origin] then
return;
end
- response.headers.access_control_allow_methods = tostring(methods);
- response.headers.access_control_allow_headers = tostring(headers);
+ response.headers.access_control_allow_methods = header_set_tostring(methods);
+ response.headers.access_control_allow_headers = header_set_tostring(headers);
response.headers.access_control_max_age = tostring(max_age)
response.headers.access_control_allow_origin = origin or "*";
if allow_credentials then
@@ -292,7 +329,13 @@ module.add_host(module); -- set up handling on global context too
local trusted_proxies = module:get_option_set("trusted_proxies", { "127.0.0.1", "::1" })._items;
+--- deal with [ipv6]:port / ip:port format
+local function normal_ip(ip)
+ return ip:match("^%[([%x:]*)%]") or ip:match("^([%d.]+)") or ip;
+end
+
local function is_trusted_proxy(ip)
+ ip = normal_ip(ip);
if trusted_proxies[ip] then
return true;
end
@@ -308,6 +351,30 @@ end
local function get_forwarded_connection_info(request) --> ip:string, secure:boolean
local ip = request.ip;
local secure = request.secure; -- set by net.http.server
+
+ local forwarded = http_util.parse_forwarded(request.headers.forwarded);
+ if forwarded then
+ request.forwarded = forwarded;
+ for i = #forwarded, 1, -1 do
+ local proxy = forwarded[i]
+ if is_trusted_proxy(ip) then
+ ip = normal_ip(proxy["for"]);
+ secure = secure and proxy.proto == "https";
+ else
+ break
+ end
+ end
+ end
+
+ return ip, secure;
+end
+
+-- TODO switch to RFC 7239 by default once support is more common
+if module:get_option_boolean("http_legacy_x_forwarded", true) then
+function get_forwarded_connection_info(request) --> ip:string, secure:boolean
+ local ip = request.ip;
+ local secure = request.secure; -- set by net.http.server
+
local forwarded_for = request.headers.x_forwarded_for;
if forwarded_for then
-- luacheck: ignore 631
@@ -330,6 +397,7 @@ local function get_forwarded_connection_info(request) --> ip:string, secure:bool
return ip, secure;
end
+end
module:wrap_object_event(server._events, false, function (handlers, event_name, event_data)
local request = event_data.request;
diff --git a/plugins/mod_http_errors.lua b/plugins/mod_http_errors.lua
index ec54860c..c92e44ce 100644
--- a/plugins/mod_http_errors.lua
+++ b/plugins/mod_http_errors.lua
@@ -1,9 +1,9 @@
module:set_global();
-local server = require "net.http.server";
-local codes = require "net.http.codes";
-local xml_escape = require "util.stanza".xml_escape;
-local render = require "util.interpolation".new("%b{}", xml_escape);
+local server = require "prosody.net.http.server";
+local codes = require "prosody.net.http.codes";
+local xml_escape = require "prosody.util.stanza".xml_escape;
+local render = require "prosody.util.interpolation".new("%b{}", xml_escape);
local show_private = module:get_option_boolean("http_errors_detailed", false);
local always_serve = module:get_option_boolean("http_errors_always_show", true);
@@ -35,13 +35,13 @@ local html = [[
<meta charset="utf-8">
<title>{title}</title>
<style>
-body{margin-top:14%;text-align:center;background-color:#f8f8f8;font-family:sans-serif}
+:root{color-scheme:light dark}
+body{margin-top:14%;text-align:center;font-family:sans-serif}
h1{font-size:xx-large}
p{font-size:x-large}
p.warning>span{font-size:large;background-color:yellow}
p.extra{font-size:large;font-family:courier}
@media(prefers-color-scheme:dark){
-body{background-color:#161616;color:#eee}
p.warning>span{background-color:inherit;color:yellow}
}
</style>
diff --git a/plugins/mod_http_file_share.lua b/plugins/mod_http_file_share.lua
index b6200628..cfc647d4 100644
--- a/plugins/mod_http_file_share.lua
+++ b/plugins/mod_http_file_share.lua
@@ -8,17 +8,16 @@
-- Again, from the top!
local t_insert = table.insert;
-local jid = require "util.jid";
-local st = require "util.stanza";
+local jid = require "prosody.util.jid";
+local st = require "prosody.util.stanza";
local url = require "socket.url";
-local dm = require "core.storagemanager".olddm;
-local jwt = require "util.jwt";
-local errors = require "util.error";
-local dataform = require "util.dataforms".new;
-local urlencode = require "util.http".urlencode;
-local dt = require "util.datetime";
-local hi = require "util.human.units";
-local cache = require "util.cache";
+local dm = require "prosody.core.storagemanager".olddm;
+local errors = require "prosody.util.error";
+local dataform = require "prosody.util.dataforms".new;
+local urlencode = require "prosody.util.http".urlencode;
+local dt = require "prosody.util.datetime";
+local hi = require "prosody.util.human.units";
+local cache = require "prosody.util.cache";
local lfs = require "lfs";
local unknown = math.abs(0/0);
@@ -35,17 +34,21 @@ local uploads = module:open_store("uploads", "archive");
local persist_stats = module:open_store("upload_stats", "map");
-- id, <request>, time, owner
-local secret = module:get_option_string(module.name.."_secret", require"util.id".long());
+local secret = module:get_option_string(module.name.."_secret", require"prosody.util.id".long());
local external_base_url = module:get_option_string(module.name .. "_base_url");
-local file_size_limit = module:get_option_number(module.name .. "_size_limit", 10 * 1024 * 1024); -- 10 MB
+local file_size_limit = module:get_option_integer(module.name .. "_size_limit", 10 * 1024 * 1024, 0); -- 10 MB
local file_types = module:get_option_set(module.name .. "_allowed_file_types", {});
local safe_types = module:get_option_set(module.name .. "_safe_file_types", {"image/*","video/*","audio/*","text/plain"});
-local expiry = module:get_option_number(module.name .. "_expires_after", 7 * 86400);
-local daily_quota = module:get_option_number(module.name .. "_daily_quota", file_size_limit*10); -- 100 MB / day
-local total_storage_limit = module:get_option_number(module.name.."_global_quota", unlimited);
+local expiry = module:get_option_period(module.name .. "_expires_after", "1w");
+local daily_quota = module:get_option_integer(module.name .. "_daily_quota", file_size_limit*10, 0); -- 100 MB / day
+local total_storage_limit = module:get_option_integer(module.name.."_global_quota", unlimited, 0);
+
+local create_jwt, verify_jwt = require"prosody.util.jwt".init("HS256", secret, secret, { default_ttl = 600 });
local access = module:get_option_set(module.name .. "_access", {});
+module:default_permission("prosody:registered", ":upload");
+
if not external_base_url then
module:depends("http");
end
@@ -76,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" });
-do
+module:on_ready(function ()
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());
@@ -135,7 +138,7 @@ end
function may_upload(uploader, filename, filesize, filetype) -- > boolean, error
local uploader_host = jid.host(uploader);
- if not ((access:empty() and prosody.hosts[uploader_host]) or access:contains(uploader) or access:contains(uploader_host)) then
+ if not (module:may(":upload", uploader) or access:contains(uploader) or access:contains(uploader_host)) then
return false, upload_errors.new("access");
end
@@ -169,16 +172,13 @@ function may_upload(uploader, filename, filesize, filetype) -- > boolean, error
end
function get_authz(slot, uploader, filename, filesize, filetype)
-local now = os.time();
- return jwt.sign(secret, {
+ return create_jwt({
-- token properties
sub = uploader;
- iat = now;
- exp = now+300;
-- slot properties
slot = slot;
- expires = expiry >= 0 and (now+expiry) or nil;
+ expires = expiry < math.huge and (os.time()+expiry) or nil;
-- file properties
filename = filename;
filesize = filesize;
@@ -249,32 +249,34 @@ end
function handle_upload(event, path) -- PUT /upload/:slot
local request = event.request;
- local authz = request.headers.authorization;
- if authz then
- authz = authz:match("^Bearer (.*)")
- end
- if not authz then
- module:log("debug", "Missing or malformed Authorization header");
- event.response.headers.www_authenticate = "Bearer";
- return 401;
- end
- local authed, upload_info = jwt.verify(secret, authz);
- if not (authed and type(upload_info) == "table" and type(upload_info.exp) == "number") then
- module:log("debug", "Unauthorized or invalid token: %s, %q", authed, upload_info);
- return 401;
- end
- if not request.body_sink and upload_info.exp < os.time() then
- module:log("debug", "Authorization token expired on %s", dt.datetime(upload_info.exp));
- return 410;
- end
- if not path or upload_info.slot ~= path:match("^[^/]+") then
- module:log("debug", "Invalid upload slot: %q, path: %q", upload_info.slot, path);
- return 400;
- end
- if request.headers.content_length and tonumber(request.headers.content_length) ~= upload_info.filesize then
- return 413;
- -- Note: We don't know the size if the upload is streamed in chunked encoding,
- -- so we also check the final file size on completion.
+ local upload_info = request.http_file_share_upload_info;
+
+ if not upload_info then -- Initial handling of request
+ local authz = request.headers.authorization;
+ if authz then
+ authz = authz:match("^Bearer (.*)")
+ end
+ if not authz then
+ module:log("debug", "Missing or malformed Authorization header");
+ event.response.headers.www_authenticate = "Bearer";
+ return 401;
+ end
+ local authed, authed_upload_info = verify_jwt(authz);
+ if not authed then
+ module:log("debug", "Unauthorized or invalid token: %s, %q", authz, authed_upload_info);
+ return 401;
+ end
+ if not path or authed_upload_info.slot ~= path:match("^[^/]+") then
+ module:log("debug", "Invalid upload slot: %q, path: %q", authed_upload_info.slot, path);
+ return 400;
+ end
+ if request.headers.content_length and tonumber(request.headers.content_length) ~= authed_upload_info.filesize then
+ return 413;
+ -- Note: We don't know the size if the upload is streamed in chunked encoding,
+ -- so we also check the final file size on completion.
+ end
+ upload_info = authed_upload_info;
+ request.http_file_share_upload_info = upload_info;
end
local filename = get_filename(upload_info.slot, true);
@@ -450,11 +452,11 @@ 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 "util.array";
- local async = require "util.async";
- local ENOENT = require "util.pposix".ENOENT;
+ local array = require "prosody.util.array";
+ local async = require "prosody.util.async";
+ local ENOENT = require "prosody.util.pposix".ENOENT;
local function sleep(t)
local wait, done = async.waiter();
diff --git a/plugins/mod_http_files.lua b/plugins/mod_http_files.lua
index b921116a..799fb9c8 100644
--- a/plugins/mod_http_files.lua
+++ b/plugins/mod_http_files.lua
@@ -9,11 +9,11 @@
module:depends("http");
local open = io.open;
-local fileserver = require"net.http.files";
+local fileserver = require"prosody.net.http.files";
local base_path = module:get_option_path("http_files_dir", module:get_option_path("http_path"));
-local cache_size = module:get_option_number("http_files_cache_size", 128);
-local cache_max_file_size = module:get_option_number("http_files_cache_max_file_size", 4096);
+local cache_size = module:get_option_integer("http_files_cache_size", 128, 1);
+local cache_max_file_size = module:get_option_integer("http_files_cache_max_file_size", 4096, 1);
local dir_indices = module:get_option_array("http_index_files", { "index.html", "index.htm" });
local directory_index = module:get_option_boolean("http_dir_listing");
@@ -74,12 +74,12 @@ function serve(opts)
if opts.index_files == nil then
opts.index_files = dir_indices;
end
- module:log("warn", "%s should be updated to use 'net.http.files' instead of mod_http_files", get_calling_module());
+ module:log("warn", "%s should be updated to use 'prosody.net.http.files' instead of mod_http_files", get_calling_module());
return fileserver.serve(opts);
end
function wrap_route(routes)
- module:log("debug", "%s should be updated to use 'net.http.files' instead of mod_http_files", get_calling_module());
+ module:log("debug", "%s should be updated to use 'prosody.net.http.files' instead of mod_http_files", get_calling_module());
for route,handler in pairs(routes) do
if type(handler) ~= "function" then
routes[route] = fileserver.serve(handler);
diff --git a/plugins/mod_http_openmetrics.lua b/plugins/mod_http_openmetrics.lua
index 0c204ff4..5f151521 100644
--- a/plugins/mod_http_openmetrics.lua
+++ b/plugins/mod_http_openmetrics.lua
@@ -8,8 +8,8 @@
module:set_global();
-local statsman = require "core.statsmanager";
-local ip = require "util.ip";
+local statsman = require "prosody.core.statsmanager";
+local ip = require "prosody.util.ip";
local get_metric_registry = statsman.get_metric_registry;
local collect = statsman.collect;
diff --git a/plugins/mod_invites.lua b/plugins/mod_invites.lua
index 88690f7e..5ee9430a 100644
--- a/plugins/mod_invites.lua
+++ b/plugins/mod_invites.lua
@@ -1,10 +1,12 @@
-local id = require "util.id";
-local it = require "util.iterators";
+local id = require "prosody.util.id";
+local it = require "prosody.util.iterators";
local url = require "socket.url";
-local jid_node = require "util.jid".node;
-local jid_split = require "util.jid".split;
+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_number("invite_expiry", 86400 * 7);
+local default_ttl = module:get_option_period("invite_expiry", "1 week");
local token_storage;
if prosody.process_type == "prosody" or prosody.shutdown then
@@ -201,53 +203,103 @@ function use(token) --luacheck: ignore 131/use
end
--- shell command
-do
- -- Since the console is global this overwrites the command for
- -- each host it's loaded on, but this should be fine.
-
- local get_module = require "core.modulemanager".get_module;
-
- local console_env = module:shared("/*/admin_shell/env");
-
- -- luacheck: ignore 212/self
- console_env.invite = {};
- function console_env.invite:create_account(user_jid)
- local username, host = jid_split(user_jid);
- local mod_invites, err = get_module(host, "invites");
- if not mod_invites then return nil, err or "mod_invites not loaded on this host"; end
- local invite, err = mod_invites.create_account(username);
+module:add_item("shell-command", {
+ section = "invite";
+ section_desc = "Create and manage invitations";
+ name = "create_account";
+ desc = "Create an invitation to make an account on this server with the specified JID (supply only a hostname to allow any username)";
+ args = { { name = "user_jid", type = "string" } };
+ host_selector = "user_jid";
+
+ handler = function (self, user_jid) --luacheck: ignore 212/self
+ local username = jid_split(user_jid);
+ local invite, err = create_account(username);
if not invite then return nil, err; end
return true, invite.landing_page or invite.uri;
- end
-
- function console_env.invite:create_contact(user_jid, allow_registration)
- local username, host = jid_split(user_jid);
- local mod_invites, err = get_module(host, "invites");
- if not mod_invites then return nil, err or "mod_invites not loaded on this host"; end
- local invite, err = mod_invites.create_contact(username, allow_registration);
+ end;
+});
+
+module:add_item("shell-command", {
+ section = "invite";
+ section_desc = "Create and manage invitations";
+ name = "create_contact";
+ desc = "Create an invitation to become contacts with the specified user";
+ args = { { name = "user_jid", type = "string" }, { name = "allow_registration" } };
+ host_selector = "user_jid";
+
+ handler = function (self, user_jid, allow_registration) --luacheck: ignore 212/self
+ local username = jid_split(user_jid);
+ local invite, err = create_contact(username, allow_registration);
if not invite then return nil, err; end
return true, invite.landing_page or invite.uri;
- end
-end
+ end;
+});
+
+local subcommands = {};
--- prosodyctl command
function module.command(arg)
- if #arg < 2 or arg[1] ~= "generate" then
+ local opts = argparse.parse(arg, { short_params = { h = "help"; ["?"] = "help" } });
+ local cmd = table.remove(arg, 1); -- pop command
+ if opts.help or not cmd or not subcommands[cmd] then
print("usage: prosodyctl mod_"..module.name.." generate example.com");
return 2;
end
- table.remove(arg, 1); -- pop command
+ return subcommands[cmd](arg);
+end
- local sm = require "core.storagemanager";
- local mm = require "core.modulemanager";
+function subcommands.generate(arg)
+ 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.")
+ print()
+ print("required arguments in password reset mode:")
+ print()
+ print(" --reset USERNAME Generate a password reset link for the given USERNAME.")
+ print()
+ print("optional arguments in new account mode:")
+ print()
+ print(" --admin Make the new user privileged")
+ print(" Equivalent to --role prosody:admin")
+ 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()
+ print("--reset and the other options cannot be mixed.")
+ return 2
+ end
- local host = arg[1];
- assert(prosody.hosts[host], "Host "..tostring(host).." does not exist");
+ 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);
- table.remove(arg, 1); -- pop 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
@@ -257,71 +309,28 @@ function module.command(arg)
end
local allow_reset;
- local roles;
- local groups = {};
-
- while #arg > 0 do
- local value = arg[1];
- table.remove(arg, 1);
- if value == "--help" then
- print("usage: prosodyctl mod_"..module.name.." generate DOMAIN --reset USERNAME")
- print("usage: prosodyctl mod_"..module.name.." generate DOMAIN [--admin] [--role ROLE] [--group GROUPID]...")
- 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.")
- print()
- print("required arguments in password reset mode:")
- print()
- print(" --reset USERNAME Generate a password reset link for the given USERNAME.")
- print()
- print("optional arguments in new account mode:")
- print()
- print(" --admin Make the new user privileged")
- print(" Equivalent to --role prosody:admin")
- 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()
- print("--role and --admin override each other; the last one wins")
- print("--group can be specified multiple times; the user will be added to all groups.")
- print()
- print("--reset and the other options cannot be mixed.")
- return 2
- elseif value == "--reset" then
- local nodeprep = require "util.encodings".stringprep.nodeprep;
- local username = nodeprep(arg[1])
- table.remove(arg, 1);
- if not username then
- print("Please supply a valid username to generate a reset link for");
- return 2;
- end
- allow_reset = username;
- elseif value == "--admin" then
- roles = { ["prosody:admin"] = true };
- elseif value == "--role" then
- local rolename = arg[1];
- if not rolename then
- print("Please supply a role name");
- return 2;
- end
- roles = { [rolename] = true };
- table.remove(arg, 1);
- elseif value == "--group" or value == "-g" then
- local groupid = arg[1];
- if not groupid then
- print("Please supply a group ID")
- return 2;
- end
- table.insert(groups, groupid);
- table.remove(arg, 1);
- else
- print("unexpected argument: "..value)
+
+ if opts.reset then
+ local nodeprep = require "prosody.util.encodings".stringprep.nodeprep;
+ local username = nodeprep(opts.reset)
+ if not username then
+ print("Please supply a valid username to generate a reset link for");
+ return 2;
end
+ allow_reset = username;
+ end
+
+ local roles = opts.role or {};
+ local groups = opts.groups or {};
+
+ if opts.admin then
+ -- Insert it first since we don't get order out of argparse
+ table.insert(roles, 1, "prosody:admin");
end
local invite;
if allow_reset then
- if roles then
+ if roles[1] then
print("--role/--admin and --reset are mutually exclusive")
return 2;
end
@@ -333,7 +342,7 @@ function module.command(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 9c0660e9..3ef4116d 100644
--- a/plugins/mod_invites_adhoc.lua
+++ b/plugins/mod_invites_adhoc.lua
@@ -1,8 +1,8 @@
-- XEP-0401: Easy User Onboarding
-local dataforms = require "util.dataforms";
-local datetime = require "util.datetime";
-local split_jid = require "util.jid".split;
-local usermanager = require "core.usermanager";
+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;
@@ -13,8 +13,7 @@ local allow_user_invites = module:get_option_boolean("allow_user_invites", false
-- on the server, use the option above instead.
local allow_contact_invites = module:get_option_boolean("allow_contact_invites", true);
-local allow_user_invite_roles = module:get_option_set("allow_user_invites_by_roles");
-local deny_user_invite_roles = module:get_option_set("deny_user_invites_by_roles");
+module:default_permission(allow_user_invites and "prosody:registered" or "prosody:admin", ":invite-users");
local invites;
if prosody.shutdown then -- COMPAT hack to detect prosodyctl
@@ -42,36 +41,8 @@ local invite_result_form = dataforms.new({
-- This is for checking if the specified JID may create invites
-- that allow people to register accounts on this host.
-local function may_invite_new_users(jid)
- if usermanager.get_roles then
- local user_roles = usermanager.get_roles(jid, module.host);
- if not user_roles then
- -- User has no roles we can check, just return default
- return allow_user_invites;
- end
-
- if user_roles["prosody:admin"] then
- return true;
- end
- if allow_user_invite_roles then
- for allowed_role in allow_user_invite_roles do
- if user_roles[allowed_role] then
- return true;
- end
- end
- end
- if deny_user_invite_roles then
- for denied_role in deny_user_invite_roles do
- if user_roles[denied_role] then
- return false;
- end
- end
- end
- elseif usermanager.is_admin(jid, module.host) then -- COMPAT w/0.11
- return true; -- Admins may always create invitations
- end
- -- No role matches, so whatever the default is
- return allow_user_invites;
+local function may_invite_new_users(context)
+ return module:may(":invite-users", context);
end
module:depends("adhoc");
@@ -91,7 +62,7 @@ module:provides("adhoc", new_adhoc("Create new contact invite", "urn:xmpp:invite
};
};
end
- local invite = invites.create_contact(username, may_invite_new_users(data.from), {
+ local invite = invites.create_contact(username, may_invite_new_users(data), {
source = data.from
});
--TODO: check errors
@@ -128,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_invites_register.lua b/plugins/mod_invites_register.lua
index d1d801ad..d9274ce4 100644
--- a/plugins/mod_invites_register.lua
+++ b/plugins/mod_invites_register.lua
@@ -1,7 +1,7 @@
-local st = require "util.stanza";
-local jid_split = require "util.jid".split;
-local jid_bare = require "util.jid".bare;
-local rostermanager = require "core.rostermanager";
+local st = require "prosody.util.stanza";
+local jid_split = require "prosody.util.jid".split;
+local jid_bare = require "prosody.util.jid".bare;
+local rostermanager = require "prosody.core.rostermanager";
local require_encryption = module:get_option_boolean("c2s_require_encryption",
module:get_option_boolean("require_encryption", true));
@@ -147,7 +147,20 @@ module:hook("user-registered", function (event)
if validated_invite.additional_data then
module:log("debug", "Importing roles from invite");
local roles = validated_invite.additional_data.roles;
- if roles then
+ if roles and roles[1] ~= nil then
+ local um = require "prosody.core.usermanager";
+ local ok, err = um.set_user_role(event.username, module.host, roles[1]);
+ if not ok then
+ module:log("error", "Could not set role %s for newly registered user %s: %s", roles[1], event.username, err);
+ end
+ for i = 2, #roles do
+ local ok, err = um.add_user_secondary_role(event.username, module.host, roles[i]);
+ if not ok then
+ module:log("warn", "Could not add secondary role %s for newly registered user %s: %s", roles[i], event.username, err);
+ end
+ end
+ elseif roles and type(next(roles)) == "string" then
+ module:log("warn", "Invite carries legacy, migration required for user '%s' for role set %q to take effect", event.username, roles);
module:open_store("roles"):set(contact_username, roles);
end
end
diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua
index 87c3a467..77969147 100644
--- a/plugins/mod_iq.lua
+++ b/plugins/mod_iq.lua
@@ -7,7 +7,7 @@
--
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
local full_sessions = prosody.full_sessions;
diff --git a/plugins/mod_lastactivity.lua b/plugins/mod_lastactivity.lua
index 91d11bd2..e41bc02a 100644
--- a/plugins/mod_lastactivity.lua
+++ b/plugins/mod_lastactivity.lua
@@ -6,10 +6,10 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
-local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
-local jid_bare = require "util.jid".bare;
-local jid_split = require "util.jid".split;
+local st = require "prosody.util.stanza";
+local is_contact_subscribed = require "prosody.core.rostermanager".is_contact_subscribed;
+local jid_bare = require "prosody.util.jid".bare;
+local jid_split = require "prosody.util.jid".split;
module:add_feature("jabber:iq:last");
diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua
index 52f2c143..048cd3e1 100644
--- a/plugins/mod_legacyauth.lua
+++ b/plugins/mod_legacyauth.lua
@@ -8,17 +8,17 @@
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
local t_concat = table.concat;
local secure_auth_only = module:get_option("c2s_require_encryption",
module:get_option("require_encryption", true))
or not(module:get_option("allow_unencrypted_plain_auth"));
-local sessionmanager = require "core.sessionmanager";
-local usermanager = require "core.usermanager";
-local nodeprep = require "util.encodings".stringprep.nodeprep;
-local resourceprep = require "util.encodings".stringprep.resourceprep;
+local sessionmanager = require "prosody.core.sessionmanager";
+local usermanager = require "prosody.core.usermanager";
+local nodeprep = require "prosody.util.encodings".stringprep.nodeprep;
+local resourceprep = require "prosody.util.encodings".stringprep.resourceprep;
module:add_feature("jabber:iq:auth");
module:hook("stream-features", function(event)
diff --git a/plugins/mod_limits.lua b/plugins/mod_limits.lua
index 4f1b618e..407f681f 100644
--- a/plugins/mod_limits.lua
+++ b/plugins/mod_limits.lua
@@ -1,13 +1,13 @@
-- Because we deal with pre-authed sessions and streams we can't be host-specific
module:set_global();
-local filters = require "util.filters";
-local throttle = require "util.throttle";
-local timer = require "util.timer";
+local filters = require "prosody.util.filters";
+local throttle = require "prosody.util.throttle";
+local timer = require "prosody.util.timer";
local ceil = math.ceil;
local limits_cfg = module:get_option("limits", {});
-local limits_resolution = module:get_option_number("limits_resolution", 1);
+local limits_resolution = module:get_option_period("limits_resolution", 1);
local default_bytes_per_second = 3000;
local default_burst = 2;
diff --git a/plugins/mod_mam/mamprefs.lib.lua b/plugins/mod_mam/mamprefs.lib.lua
index dd82b626..cddcbd30 100644
--- a/plugins/mod_mam/mamprefs.lib.lua
+++ b/plugins/mod_mam/mamprefs.lib.lua
@@ -10,12 +10,15 @@
--
-- luacheck: ignore 122/prosody
-local global_default_policy = module:get_option_string("default_archive_policy", true);
-if global_default_policy ~= "roster" then
- global_default_policy = module:get_option_boolean("default_archive_policy", global_default_policy);
-end
+local global_default_policy = module:get_option_enum("default_archive_policy", "always", "roster", "never", true, false);
local smart_enable = module:get_option_boolean("mam_smart_enable", false);
+if global_default_policy == "always" then
+ global_default_policy = true;
+elseif global_default_policy == "never" then
+ global_default_policy = false;
+end
+
do
-- luacheck: ignore 211/prefs_format
local prefs_format = {
diff --git a/plugins/mod_mam/mamprefsxml.lib.lua b/plugins/mod_mam/mamprefsxml.lib.lua
index c408fbea..b325e886 100644
--- a/plugins/mod_mam/mamprefsxml.lib.lua
+++ b/plugins/mod_mam/mamprefsxml.lib.lua
@@ -10,8 +10,8 @@
-- XEP-0313: Message Archive Management for Prosody
--
-local st = require"util.stanza";
-local jid_prep = require"util.jid".prep;
+local st = require"prosody.util.stanza";
+local jid_prep = require"prosody.util.jid".prep;
local xmlns_mam = "urn:xmpp:mam:2";
local default_attrs = {
diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua
index bebee812..b57fc030 100644
--- a/plugins/mod_mam/mod_mam.lua
+++ b/plugins/mod_mam/mod_mam.lua
@@ -15,36 +15,36 @@ local xmlns_delay = "urn:xmpp:delay";
local xmlns_forward = "urn:xmpp:forward:0";
local xmlns_st_id = "urn:xmpp:sid:0";
-local um = require "core.usermanager";
-local st = require "util.stanza";
-local rsm = require "util.rsm";
+local um = require "prosody.core.usermanager";
+local st = require "prosody.util.stanza";
+local rsm = require "prosody.util.rsm";
local get_prefs = module:require"mamprefs".get;
local set_prefs = module:require"mamprefs".set;
local prefs_to_stanza = module:require"mamprefsxml".tostanza;
local prefs_from_stanza = module:require"mamprefsxml".fromstanza;
-local jid_bare = require "util.jid".bare;
-local jid_split = require "util.jid".split;
-local jid_resource = require "util.jid".resource;
-local jid_prepped_split = require "util.jid".prepped_split;
-local dataform = require "util.dataforms".new;
-local get_form_type = require "util.dataforms".get_type;
+local jid_bare = require "prosody.util.jid".bare;
+local jid_split = require "prosody.util.jid".split;
+local jid_resource = require "prosody.util.jid".resource;
+local jid_prepped_split = require "prosody.util.jid".prepped_split;
+local dataform = require "prosody.util.dataforms".new;
+local get_form_type = require "prosody.util.dataforms".get_type;
local host = module.host;
-local rm_load_roster = require "core.rostermanager".load_roster;
+local rm_load_roster = require "prosody.core.rostermanager".load_roster;
local is_stanza = st.is_stanza;
local tostring = tostring;
-local time_now = os.time;
+local time_now = require "prosody.util.time".now;
local m_min = math.min;
local timestamp, datestamp = import( "util.datetime", "datetime", "date");
-local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
+local default_max_items, max_max_items = 20, module:get_option_integer("max_archive_query_results", 50, 0);
local strip_tags = module:get_option_set("dont_archive_namespaces", { "http://jabber.org/protocol/chatstates" });
local archive_store = module:get_option_string("archive_store", "archive");
local archive = module:open_store(archive_store, "archive");
-local cleanup_after = module:get_option_string("archive_expires_after", "1w");
-local archive_item_limit = module:get_option_number("storage_archive_item_limit", archive.caps and archive.caps.quota or 1000);
+local cleanup_after = module:get_option_period("archive_expires_after", "1w");
+local archive_item_limit = module:get_option_integer("storage_archive_item_limit", archive.caps and archive.caps.quota or 1000, 0);
local archive_truncate = math.floor(archive_item_limit * 0.99);
if not archive.find then
@@ -53,8 +53,12 @@ if not archive.find then
end
local use_total = module:get_option_boolean("mam_include_total", true);
-function schedule_cleanup()
- -- replaced later if cleanup is enabled
+function schedule_cleanup(_username, _date) -- luacheck: ignore 212
+ -- Called to make a note of which users have messages on which days, which in
+ -- turn is used to optimize the message expiry routine.
+ --
+ -- This noop is conditionally replaced later depending on retention settings
+ -- and storage backend capabilities.
end
-- Handle prefs.
@@ -245,8 +249,7 @@ module:hook("iq-get/self/"..xmlns_mam..":metadata", function (event)
return true;
end
- local id, _, when = first();
- if id then
+ for id, _, when in first do
reply:tag("start", { id = id, timestamp = timestamp(when) }):up();
end
end
@@ -258,8 +261,7 @@ module:hook("iq-get/self/"..xmlns_mam..":metadata", function (event)
return true;
end
- local id, _, when = last();
- if id then
+ for id, _, when in last do
reply:tag("end", { id = id, timestamp = timestamp(when) }):up();
end
end
@@ -437,7 +439,7 @@ local function message_handler(event, c2s)
local time = time_now();
local ok, err = archive:append(store_user, nil, clone_for_storage, time, with);
if not ok and err == "quota-limit" then
- if type(cleanup_after) == "number" then
+ if cleanup_after ~= math.huge then
module:log("debug", "User '%s' over quota, cleaning archive", store_user);
local cleaned = archive:delete(store_user, {
["end"] = (os.time() - cleanup_after);
@@ -502,20 +504,10 @@ module:hook("message/offline/broadcast", function (event)
end
end);
-if cleanup_after ~= "never" then
+if cleanup_after ~= math.huge then
local cleanup_storage = module:open_store("archive_cleanup");
local cleanup_map = module:open_store("archive_cleanup", "map");
- local day = 86400;
- local multipliers = { d = day, w = day * 7, m = 31 * day, y = 365.2425 * day };
- local n, m = cleanup_after:lower():match("(%d+)%s*([dwmy]?)");
- if not n then
- module:log("error", "Could not parse archive_expires_after string %q", cleanup_after);
- return false;
- end
-
- cleanup_after = tonumber(n) * ( multipliers[m] or 1 );
-
module:log("debug", "archive_expires_after = %d -- in seconds", cleanup_after);
if not archive.delete then
@@ -528,7 +520,7 @@ if cleanup_after ~= "never" then
-- outside the cleanup range.
if not (archive.caps and archive.caps.wildcard_delete) then
- local last_date = require "util.cache".new(module:get_option_number("archive_cleanup_date_cache_size", 1000));
+ local last_date = require "prosody.util.cache".new(module:get_option_integer("archive_cleanup_date_cache_size", 1000, 1));
function schedule_cleanup(username, date)
date = date or datestamp();
if last_date:get(username) == date then return end
@@ -541,7 +533,7 @@ if cleanup_after ~= "never" then
local cleanup_time = module:measure("cleanup", "times");
- local async = require "util.async";
+ local async = require "prosody.util.async";
module:daily("Remove expired messages", function ()
local cleanup_done = cleanup_time();
diff --git a/plugins/mod_message.lua b/plugins/mod_message.lua
index 9c07e796..aa9f5c2d 100644
--- a/plugins/mod_message.lua
+++ b/plugins/mod_message.lua
@@ -10,10 +10,10 @@
local full_sessions = prosody.full_sessions;
local bare_sessions = prosody.bare_sessions;
-local st = require "util.stanza";
-local jid_bare = require "util.jid".bare;
-local jid_split = require "util.jid".split;
-local user_exists = require "core.usermanager".user_exists;
+local st = require "prosody.util.stanza";
+local jid_bare = require "prosody.util.jid".bare;
+local jid_split = require "prosody.util.jid".split;
+local user_exists = require "prosody.core.usermanager".user_exists;
local function process_to_bare(bare, origin, stanza)
local user = bare_sessions[bare];
diff --git a/plugins/mod_mimicking.lua b/plugins/mod_mimicking.lua
index ab7612cb..52a070f5 100644
--- a/plugins/mod_mimicking.lua
+++ b/plugins/mod_mimicking.lua
@@ -6,13 +6,13 @@
-- COPYING file in the source package for more information.
--
-local encodings = require "util.encodings";
+local encodings = require "prosody.util.encodings";
assert(encodings.confusable, "This module requires that Prosody be built with ICU");
local skeleton = encodings.confusable.skeleton;
-local usage = require "util.prosodyctl".show_usage;
-local usermanager = require "core.usermanager";
-local storagemanager = require "core.storagemanager";
+local usage = require "prosody.util.prosodyctl".show_usage;
+local usermanager = require "prosody.core.usermanager";
+local storagemanager = require "prosody.core.storagemanager";
local skeletons
function module.load()
diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua
index 47e64be3..bee0820c 100644
--- a/plugins/mod_motd.lua
+++ b/plugins/mod_motd.lua
@@ -13,7 +13,7 @@ local motd_jid = module:get_option_string("motd_jid", host);
if not motd_text then return; end
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
motd_text = motd_text:gsub("^%s*(.-)%s*$", "%1"):gsub("\n[ \t]+", "\n"); -- Strip indentation from the config
diff --git a/plugins/mod_muc_mam.lua b/plugins/mod_muc_mam.lua
index 0918b95d..23bb7dab 100644
--- a/plugins/mod_muc_mam.lua
+++ b/plugins/mod_muc_mam.lua
@@ -16,28 +16,28 @@ local xmlns_st_id = "urn:xmpp:sid:0";
local xmlns_muc_user = "http://jabber.org/protocol/muc#user";
local muc_form_enable = "muc#roomconfig_enablearchiving"
-local st = require "util.stanza";
-local rsm = require "util.rsm";
-local jid_bare = require "util.jid".bare;
-local jid_split = require "util.jid".split;
-local jid_prep = require "util.jid".prep;
-local dataform = require "util.dataforms".new;
-local get_form_type = require "util.dataforms".get_type;
+local st = require "prosody.util.stanza";
+local rsm = require "prosody.util.rsm";
+local jid_bare = require "prosody.util.jid".bare;
+local jid_split = require "prosody.util.jid".split;
+local jid_prep = require "prosody.util.jid".prep;
+local dataform = require "prosody.util.dataforms".new;
+local get_form_type = require "prosody.util.dataforms".get_type;
local mod_muc = module:depends"muc";
local get_room_from_jid = mod_muc.get_room_from_jid;
local is_stanza = st.is_stanza;
local tostring = tostring;
-local time_now = os.time;
+local time_now = require "prosody.util.time".now;
local m_min = math.min;
-local timestamp, datestamp = import("util.datetime", "datetime", "date");
-local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
+local timestamp, datestamp = import("prosody.util.datetime", "datetime", "date");
+local default_max_items, max_max_items = 20, module:get_option_integer("max_archive_query_results", 50, 0);
-local cleanup_after = module:get_option_string("muc_log_expires_after", "1w");
+local cleanup_after = module:get_option_period("muc_log_expires_after", "1w");
local default_history_length = 20;
-local max_history_length = module:get_option_number("max_history_messages", math.huge);
+local max_history_length = module:get_option_integer("max_history_messages", math.huge, 0);
local function get_historylength(room)
return math.min(room._data.history_length or default_history_length, max_history_length);
@@ -53,7 +53,7 @@ local log_by_default = module:get_option_boolean("muc_log_by_default", true);
local archive_store = "muc_log";
local archive = module:open_store(archive_store, "archive");
-local archive_item_limit = module:get_option_number("storage_archive_item_limit", archive.caps and archive.caps.quota or 1000);
+local archive_item_limit = module:get_option_integer("storage_archive_item_limit", archive.caps and archive.caps.quota or 1000, 0);
local archive_truncate = math.floor(archive_item_limit * 0.99);
if archive.name == "null" or not archive.find then
@@ -397,7 +397,7 @@ local function save_to_history(self, stanza)
local id, err = archive:append(room_node, nil, stored_stanza, time, with);
if not id and err == "quota-limit" then
- if type(cleanup_after) == "number" then
+ if cleanup_after ~= math.huge then
module:log("debug", "Room '%s' over quota, cleaning archive", room_node);
local cleaned = archive:delete(room_node, {
["end"] = (os.time() - cleanup_after);
@@ -467,20 +467,10 @@ end);
-- Cleanup
-if cleanup_after ~= "never" then
+if cleanup_after ~= math.huge then
local cleanup_storage = module:open_store("muc_log_cleanup");
local cleanup_map = module:open_store("muc_log_cleanup", "map");
- local day = 86400;
- local multipliers = { d = day, w = day * 7, m = 31 * day, y = 365.2425 * day };
- local n, m = cleanup_after:lower():match("(%d+)%s*([dwmy]?)");
- if not n then
- module:log("error", "Could not parse muc_log_expires_after string %q", cleanup_after);
- return false;
- end
-
- cleanup_after = tonumber(n) * ( multipliers[m] or 1 );
-
module:log("debug", "muc_log_expires_after = %d -- in seconds", cleanup_after);
if not archive.delete then
@@ -492,7 +482,7 @@ if cleanup_after ~= "never" then
-- messages, we collect the union of sets of rooms from dates that fall
-- outside the cleanup range.
- local last_date = require "util.cache".new(module:get_option_number("muc_log_cleanup_date_cache_size", 1000));
+ local last_date = require "prosody.util.cache".new(module:get_option_integer("muc_log_cleanup_date_cache_size", 1000, 1));
if not ( archive.caps and archive.caps.wildcard_delete ) then
function schedule_cleanup(roomname, date)
date = date or datestamp();
@@ -506,7 +496,7 @@ if cleanup_after ~= "never" then
local cleanup_time = module:measure("cleanup", "times");
- local async = require "util.async";
+ local async = require "prosody.util.async";
module:daily("Remove expired messages", function ()
local cleanup_done = cleanup_time();
diff --git a/plugins/mod_muc_unique.lua b/plugins/mod_muc_unique.lua
index 13284745..62ec74b8 100644
--- a/plugins/mod_muc_unique.lua
+++ b/plugins/mod_muc_unique.lua
@@ -1,6 +1,6 @@
-- XEP-0307: Unique Room Names for Multi-User Chat
-local st = require "util.stanza";
-local unique_name = require "util.id".medium;
+local st = require "prosody.util.stanza";
+local unique_name = require "prosody.util.id".medium;
module:add_feature "http://jabber.org/protocol/muc#unique"
module:hook("iq-get/host/http://jabber.org/protocol/muc#unique:unique", function(event)
local origin, stanza = event.origin, event.stanza;
diff --git a/plugins/mod_net_multiplex.lua b/plugins/mod_net_multiplex.lua
index ddd58463..3f5ee54d 100644
--- a/plugins/mod_net_multiplex.lua
+++ b/plugins/mod_net_multiplex.lua
@@ -1,10 +1,10 @@
module:set_global();
-local array = require "util.array";
-local max_buffer_len = module:get_option_number("multiplex_buffer_size", 1024);
-local default_mode = module:get_option_number("network_default_read_size", 4096);
+local array = require "prosody.util.array";
+local max_buffer_len = module:get_option_integer("multiplex_buffer_size", 1024, 1);
+local default_mode = module:get_option_integer("network_default_read_size", 4096, 0);
-local portmanager = require "core.portmanager";
+local portmanager = require "prosody.core.portmanager";
local available_services = {};
local service_by_protocol = {};
diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua
index dffe8357..b71bbfd9 100644
--- a/plugins/mod_offline.lua
+++ b/plugins/mod_offline.lua
@@ -7,8 +7,8 @@
--
-local datetime = require "util.datetime";
-local jid_split = require "util.jid".split;
+local datetime = require "prosody.util.datetime";
+local jid_split = require "prosody.util.jid".split;
local offline_messages = module:open_store("offline", "archive");
diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua
index 71e45e7c..33eee2ec 100644
--- a/plugins/mod_pep.lua
+++ b/plugins/mod_pep.lua
@@ -1,21 +1,23 @@
-local pubsub = require "util.pubsub";
-local jid_bare = require "util.jid".bare;
-local jid_split = require "util.jid".split;
-local jid_join = require "util.jid".join;
-local set_new = require "util.set".new;
-local st = require "util.stanza";
-local calculate_hash = require "util.caps".calculate_hash;
-local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
-local cache = require "util.cache";
-local set = require "util.set";
-local new_id = require "util.id".medium;
-local storagemanager = require "core.storagemanager";
-local usermanager = require "core.usermanager";
+local pubsub = require "prosody.util.pubsub";
+local jid_bare = require "prosody.util.jid".bare;
+local jid_split = require "prosody.util.jid".split;
+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 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;
+local storagemanager = require "prosody.core.storagemanager";
+local usermanager = require "prosody.core.usermanager";
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();
@@ -24,7 +26,7 @@ local empty_set = set_new();
local pep_service_items = {};
-- size of caches with full pubsub service objects
-local service_cache_size = module:get_option_number("pep_service_cache_size", 1000);
+local service_cache_size = module:get_option_integer("pep_service_cache_size", 1000, 1);
-- username -> util.pubsub service object
local services = cache.new(service_cache_size, function (username, _)
@@ -36,7 +38,7 @@ local services = cache.new(service_cache_size, function (username, _)
end):table();
-- size of caches with smaller objects
-local info_cache_size = module:get_option_number("pep_info_cache_size", 10000);
+local info_cache_size = module:get_option_integer("pep_info_cache_size", 10000, 1);
-- username -> recipient -> set of nodes
local recipients = cache.new(info_cache_size):table();
@@ -49,7 +51,7 @@ local host = module.host;
local node_config = module:open_store("pep", "map");
local known_nodes = module:open_store("pep");
-local max_max_items = module:get_option_number("pep_max_items", 256);
+local max_max_items = module:get_option_number("pep_max_items", 256, 0);
local function tonumber_max_items(n)
if n == "max" then
@@ -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;
@@ -136,10 +139,14 @@ end
local function get_broadcaster(username)
local user_bare = jid_join(username, host);
local function simple_broadcast(kind, node, jids, item, _, node_obj)
+ local expose_publisher;
if node_obj then
if node_obj.config["notify_"..kind] == false then
return;
end
+ if node_obj.config.itemreply == "publisher" then
+ expose_publisher = true;
+ end
end
if kind == "retract" then
kind = "items"; -- XEP-0060 signals retraction in an <items> container
@@ -151,6 +158,9 @@ local function get_broadcaster(username)
if node_obj and node_obj.config.include_payload == false then
item:maptags(function () return nil; end);
end
+ if not expose_publisher then
+ item.attr.publisher = nil;
+ end
end
end
@@ -249,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;
@@ -306,7 +330,7 @@ local function resend_last_item(jid, node, service)
if ok and config.send_last_published_item ~= "on_sub_and_presence" then return end
local ok, id, item = service:get_last_item(node, jid);
if not (ok and id) then return; end
- service.config.broadcaster("items", node, { [jid] = true }, item);
+ service.config.broadcaster("items", node, { [jid] = true }, item, true, service.nodes[node], service);
end
local function update_subscriptions(recipient, service_name, nodes)
diff --git a/plugins/mod_pep_simple.lua b/plugins/mod_pep_simple.lua
index e686b99b..a196a0ff 100644
--- a/plugins/mod_pep_simple.lua
+++ b/plugins/mod_pep_simple.lua
@@ -7,15 +7,15 @@
--
-local jid_bare = require "util.jid".bare;
-local jid_split = require "util.jid".split;
-local st = require "util.stanza";
-local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
+local jid_bare = require "prosody.util.jid".bare;
+local jid_split = require "prosody.util.jid".split;
+local st = require "prosody.util.stanza";
+local is_contact_subscribed = require "prosody.core.rostermanager".is_contact_subscribed;
local pairs = pairs;
local next = next;
local type = type;
-local unpack = table.unpack or unpack; -- luacheck: ignore 113
-local calculate_hash = require "util.caps".calculate_hash;
+local unpack = table.unpack;
+local calculate_hash = require "prosody.util.caps".calculate_hash;
local core_post_stanza = prosody.core_post_stanza;
local bare_sessions = prosody.bare_sessions;
diff --git a/plugins/mod_ping.lua b/plugins/mod_ping.lua
index b6ccc928..018f815a 100644
--- a/plugins/mod_ping.lua
+++ b/plugins/mod_ping.lua
@@ -6,7 +6,7 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
module:add_feature("urn:xmpp:ping");
diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua
index 7f048be3..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 "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, "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 "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_presence.lua b/plugins/mod_presence.lua
index 3f9a0c12..f939fa00 100644
--- a/plugins/mod_presence.lua
+++ b/plugins/mod_presence.lua
@@ -15,19 +15,19 @@ local tonumber = tonumber;
local core_post_stanza = prosody.core_post_stanza;
local core_process_stanza = prosody.core_process_stanza;
-local st = require "util.stanza";
-local jid_split = require "util.jid".split;
-local jid_bare = require "util.jid".bare;
-local datetime = require "util.datetime";
+local st = require "prosody.util.stanza";
+local jid_split = require "prosody.util.jid".split;
+local jid_bare = require "prosody.util.jid".bare;
+local datetime = require "prosody.util.datetime";
local hosts = prosody.hosts;
local bare_sessions = prosody.bare_sessions;
local full_sessions = prosody.full_sessions;
local NULL = {};
-local rostermanager = require "core.rostermanager";
-local sessionmanager = require "core.sessionmanager";
+local rostermanager = require "prosody.core.rostermanager";
+local sessionmanager = require "prosody.core.sessionmanager";
-local recalc_resource_map = require "util.presence".recalc_resource_map;
+local recalc_resource_map = require "prosody.util.presence".recalc_resource_map;
local ignore_presence_priority = module:get_option_boolean("ignore_presence_priority", false);
diff --git a/plugins/mod_private.lua b/plugins/mod_private.lua
index 6046d490..2359494c 100644
--- a/plugins/mod_private.lua
+++ b/plugins/mod_private.lua
@@ -7,7 +7,7 @@
--
-local st = require "util.stanza"
+local st = require "prosody.util.stanza"
local private_storage = module:open_store("private", "map");
diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua
index 069ce0a9..38acc79a 100644
--- a/plugins/mod_proxy65.lua
+++ b/plugins/mod_proxy65.lua
@@ -9,11 +9,11 @@
module:set_global();
-local jid_compare, jid_prep = require "util.jid".compare, require "util.jid".prep;
-local st = require "util.stanza";
-local sha1 = require "util.hashes".sha1;
-local server = require "net.server";
-local portmanager = require "core.portmanager";
+local jid_compare, jid_prep = require "prosody.util.jid".compare, require "prosody.util.jid".prep;
+local st = require "prosody.util.stanza";
+local sha1 = require "prosody.util.hashes".sha1;
+local server = require "prosody.net.server";
+local portmanager = require "prosody.core.portmanager";
local sessions = module:shared("sessions");
local transfers = module:shared("transfers");
diff --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua
index ef31f326..6efc419f 100644
--- a/plugins/mod_pubsub/mod_pubsub.lua
+++ b/plugins/mod_pubsub/mod_pubsub.lua
@@ -1,10 +1,9 @@
-local pubsub = require "util.pubsub";
-local st = require "util.stanza";
-local jid_bare = require "util.jid".bare;
-local usermanager = require "core.usermanager";
-local new_id = require "util.id".medium;
-local storagemanager = require "core.storagemanager";
-local xtemplate = require "util.xtemplate";
+local pubsub = require "prosody.util.pubsub";
+local st = require "prosody.util.stanza";
+local jid_bare = require "prosody.util.jid".bare;
+local new_id = require "prosody.util.id".medium;
+local storagemanager = require "prosody.core.storagemanager";
+local xtemplate = require "prosody.util.xtemplate";
local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
@@ -13,7 +12,7 @@ local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false);
local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false);
local pubsub_disco_name = module:get_option_string("name", "Prosody PubSub Service");
-local expose_publisher = module:get_option_boolean("expose_publisher", false)
+local service_expose_publisher = module:get_option_boolean("expose_publisher")
local service;
@@ -40,7 +39,7 @@ end
-- get(node_name)
-- users(): iterator over (node_name)
-local max_max_items = module:get_option_number("pubsub_max_items", 256);
+local max_max_items = module:get_option_integer("pubsub_max_items", 256, 1);
local function tonumber_max_items(n)
if n == "max" then
@@ -82,7 +81,11 @@ function simple_broadcast(kind, node, jids, item, actor, node_obj, service) --lu
if node_obj and node_obj.config.include_payload == false then
item:maptags(function () return nil; end);
end
- if not expose_publisher then
+ local node_expose_publisher = service_expose_publisher;
+ if node_expose_publisher == nil and node_obj and node_obj.config.itemreply == "publisher" then
+ node_expose_publisher = true;
+ end
+ if not node_expose_publisher then
item.attr.publisher = nil;
elseif not item.attr.publisher and actor ~= true then
item.attr.publisher = service.config.normalize_jid(actor);
@@ -136,12 +139,22 @@ end
-- Compose a textual representation of Atom payloads
local summary_templates = module:get_option("pubsub_summary_templates", {
- ["http://www.w3.org/2005/Atom"] = "{summary|or{{author/name|and{{author/name} posted }}{title}}}";
+ ["http://www.w3.org/2005/Atom"] = "{@pubsub:title|and{*{@pubsub:title}*\n\n}}{summary|or{{author/name|and{{author/name} posted }}{title}}}";
})
for pubsub_type, template in pairs(summary_templates) do
module:hook("pubsub-summary/"..pubsub_type, function (event)
local payload = event.payload;
+
+ local got_config, node_config = service:get_node_config(event.node, true);
+ if got_config then
+ payload = st.clone(payload);
+ payload.attr["xmlns:pubsub"] = xmlns_pubsub;
+ payload.attr["pubsub:node"] = event.node;
+ payload.attr["pubsub:title"] = node_config.title;
+ payload.attr["pubsub:description"] = node_config.description;
+ end
+
return xtemplate.render(template, payload, tostring);
end, -1);
end
@@ -176,10 +189,23 @@ module:hook("host-disco-items", function (event)
end
end);
-local admin_aff = module:get_option_string("default_admin_affiliation", "owner");
-local function get_affiliation(jid)
+local admin_aff = module:get_option_enum("default_admin_affiliation", "owner", "publisher", "member", "outcast", "none");
+
+module:default_permission("prosody:admin", ":service-admin");
+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 usermanager.is_admin(bare_jid, module.host) 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
@@ -192,7 +218,7 @@ function set_service(new_service)
service = new_service;
service.config.autocreate_on_publish = autocreate_on_publish;
service.config.autocreate_on_subscribe = autocreate_on_subscribe;
- service.config.expose_publisher = expose_publisher;
+ service.config.expose_publisher = service_expose_publisher;
service.config.nodestore = node_store;
service.config.itemstore = create_simple_itemstore;
@@ -219,7 +245,7 @@ function module.load()
set_service(pubsub.new({
autocreate_on_publish = autocreate_on_publish;
autocreate_on_subscribe = autocreate_on_subscribe;
- expose_publisher = expose_publisher;
+ expose_publisher = service_expose_publisher;
node_defaults = {
["persist_items"] = true;
@@ -236,3 +262,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 3196569f..8ae0a896 100644
--- a/plugins/mod_pubsub/pubsub.lib.lua
+++ b/plugins/mod_pubsub/pubsub.lib.lua
@@ -1,13 +1,13 @@
-local t_unpack = table.unpack or unpack; -- luacheck: ignore 113
+local t_unpack = table.unpack;
local time_now = os.time;
-local jid_prep = require "util.jid".prep;
-local set = require "util.set";
-local st = require "util.stanza";
-local it = require "util.iterators";
-local uuid_generate = require "util.uuid".generate;
-local dataform = require"util.dataforms".new;
-local errors = require "util.error";
+local jid_prep = require "prosody.util.jid".prep;
+local set = require "prosody.util.set";
+local st = require "prosody.util.stanza";
+local it = require "prosody.util.iterators";
+local uuid_generate = require "prosody.util.uuid".generate;
+local dataform = require"prosody.util.dataforms".new;
+local errors = require "prosody.util.error";
local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
@@ -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";
@@ -164,6 +170,17 @@ local node_config_form = dataform {
var = "pubsub#notify_retract";
value = true;
};
+ {
+ type = "list-single";
+ label = "Specify whose JID to include as the publisher of items";
+ name = "itemreply";
+ var = "pubsub#itemreply";
+ options = {
+ { label = "Include the node owner's JID", value = "owner" };
+ { label = "Include the item publisher's JID", value = "publisher" };
+ { label = "Don't include any JID with items", value = "none", default = true };
+ };
+ };
};
_M.node_config_form = node_config_form;
@@ -347,6 +364,13 @@ function handlers.get_items(origin, stanza, items, service)
origin.send(pubsub_error_reply(stanza, "nodeid-required"));
return true;
end
+
+ local node_obj = service.nodes[node];
+ if not node_obj then
+ origin.send(pubsub_error_reply(stanza, "item-not-found"));
+ return true;
+ end
+
local resultspec; -- TODO rsm.get()
if items.attr.max_items then
resultspec = { max = tonumber(items.attr.max_items) };
@@ -358,6 +382,9 @@ function handlers.get_items(origin, stanza, items, service)
end
local expose_publisher = service.config.expose_publisher;
+ if expose_publisher == nil and node_obj.config.itemreply == "publisher" then
+ expose_publisher = true;
+ end
local data = st.stanza("items", { node = node });
local iter, v, i = ipairs(results);
@@ -678,8 +705,7 @@ end
function handlers.set_retract(origin, stanza, retract, service)
local node, notify = retract.attr.node, retract.attr.notify;
notify = (notify == "1") or (notify == "true");
- local item = retract:get_child("item");
- local id = item and item.attr.id
+ local id = retract:get_child_attr("item", nil, "id");
if not (node and id) then
origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required"));
return true;
diff --git a/plugins/mod_register_ibr.lua b/plugins/mod_register_ibr.lua
index 8042de7e..ee47a1e0 100644
--- a/plugins/mod_register_ibr.lua
+++ b/plugins/mod_register_ibr.lua
@@ -7,19 +7,21 @@
--
-local st = require "util.stanza";
-local dataform_new = require "util.dataforms".new;
-local usermanager_user_exists = require "core.usermanager".user_exists;
-local usermanager_create_user = require "core.usermanager".create_user;
-local usermanager_set_password = require "core.usermanager".create_user;
-local usermanager_delete_user = require "core.usermanager".delete_user;
-local nodeprep = require "util.encodings".stringprep.nodeprep;
-local util_error = require "util.error";
-
-local additional_fields = module:get_option("additional_registration_fields", {});
+local st = require "prosody.util.stanza";
+local dataform_new = require "prosody.util.dataforms".new;
+local usermanager_user_exists = require "prosody.core.usermanager".user_exists;
+local usermanager_create_user_with_role = require "prosody.core.usermanager".create_user_with_role;
+local usermanager_set_password = require "prosody.core.usermanager".create_user;
+local usermanager_delete_user = require "prosody.core.usermanager".delete_user;
+local nodeprep = require "prosody.util.encodings".stringprep.nodeprep;
+local util_error = require "prosody.util.error";
+
+local additional_fields = module:get_option_array("additional_registration_fields", {});
local require_encryption = module:get_option_boolean("c2s_require_encryption",
module:get_option_boolean("require_encryption", true));
+local default_role = module:get_option_string("register_ibr_default_role", "prosody:registered");
+
pcall(function ()
module:depends("register_limits");
end);
@@ -166,7 +168,12 @@ module:hook("stanza/iq/jabber:iq:register:query", function(event)
return true;
end
- local user = { username = username, password = password, host = host, additional = data, ip = session.ip, session = session, allowed = true }
+ local user = {
+ username = username, password = password, host = host;
+ additional = data, ip = session.ip, session = session;
+ role = default_role;
+ allowed = true;
+ };
module:fire_event("user-registering", user);
if not user.allowed then
local error_type, error_condition, reason;
@@ -200,7 +207,7 @@ module:hook("stanza/iq/jabber:iq:register:query", function(event)
end
end
- local created, err = usermanager_create_user(username, password, host);
+ local created, err = usermanager_create_user_with_role(username, password, host, user.role);
if created then
data.registered = os.time();
if not account_details:set(username, data) then
diff --git a/plugins/mod_register_limits.lua b/plugins/mod_register_limits.lua
index cb430f7f..e127bb86 100644
--- a/plugins/mod_register_limits.lua
+++ b/plugins/mod_register_limits.lua
@@ -7,23 +7,23 @@
--
-local create_throttle = require "util.throttle".create;
-local new_cache = require "util.cache".new;
-local ip_util = require "util.ip";
+local create_throttle = require "prosody.util.throttle".create;
+local new_cache = require "prosody.util.cache".new;
+local ip_util = require "prosody.util.ip";
local new_ip = ip_util.new_ip;
local match_ip = ip_util.match;
local parse_cidr = ip_util.parse_cidr;
-local errors = require "util.error";
+local errors = require "prosody.util.error";
-- COMPAT drop old option names
-local min_seconds_between_registrations = module:get_option_number("min_seconds_between_registrations");
+local min_seconds_between_registrations = module:get_option_period("min_seconds_between_registrations");
local allowlist_only = module:get_option_boolean("allowlist_registration_only", module:get_option_boolean("whitelist_registration_only"));
local allowlisted_ips = module:get_option_set("registration_allowlist", module:get_option("registration_whitelist", { "127.0.0.1", "::1" }))._items;
local blocklisted_ips = module:get_option_set("registration_blocklist", module:get_option_set("registration_blacklist", {}))._items;
-local throttle_max = module:get_option_number("registration_throttle_max", min_seconds_between_registrations and 1);
-local throttle_period = module:get_option_number("registration_throttle_period", min_seconds_between_registrations);
-local throttle_cache_size = module:get_option_number("registration_throttle_cache_size", 100);
+local throttle_max = module:get_option_number("registration_throttle_max", min_seconds_between_registrations and 1, 0);
+local throttle_period = module:get_option_period("registration_throttle_period", min_seconds_between_registrations);
+local throttle_cache_size = module:get_option_integer("registration_throttle_cache_size", 100, 1);
local blocklist_overflow = module:get_option_boolean("blocklist_on_registration_throttle_overload",
module:get_option_boolean("blacklist_on_registration_throttle_overload", false));
diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua
index 37fa197a..53b404f7 100644
--- a/plugins/mod_roster.lua
+++ b/plugins/mod_roster.lua
@@ -7,18 +7,18 @@
--
-local st = require "util.stanza"
+local st = require "prosody.util.stanza"
-local jid_split = require "util.jid".split;
-local jid_resource = require "util.jid".resource;
-local jid_prep = require "util.jid".prep;
+local jid_split = require "prosody.util.jid".split;
+local jid_resource = require "prosody.util.jid".resource;
+local jid_prep = require "prosody.util.jid".prep;
local tonumber = tonumber;
local pairs = pairs;
-local rm_load_roster = require "core.rostermanager".load_roster;
-local rm_remove_from_roster = require "core.rostermanager".remove_from_roster;
-local rm_add_to_roster = require "core.rostermanager".add_to_roster;
-local rm_roster_push = require "core.rostermanager".roster_push;
+local rm_load_roster = require "prosody.core.rostermanager".load_roster;
+local rm_remove_from_roster = require "prosody.core.rostermanager".remove_from_roster;
+local rm_add_to_roster = require "prosody.core.rostermanager".add_to_roster;
+local rm_roster_push = require "prosody.core.rostermanager".roster_push;
module:add_feature("jabber:iq:roster");
diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua
index ee65ba70..04fd5bc3 100644
--- a/plugins/mod_s2s.lua
+++ b/plugins/mod_s2s.lua
@@ -16,32 +16,38 @@ local tostring, type = tostring, type;
local t_insert = table.insert;
local traceback = debug.traceback;
-local add_task = require "util.timer".add_task;
-local stop_timer = require "util.timer".stop;
-local st = require "util.stanza";
-local initialize_filters = require "util.filters".initialize;
-local nameprep = require "util.encodings".stringprep.nameprep;
-local new_xmpp_stream = require "util.xmppstream".new;
-local s2s_new_incoming = require "core.s2smanager".new_incoming;
-local s2s_new_outgoing = require "core.s2smanager".new_outgoing;
-local s2s_destroy_session = require "core.s2smanager".destroy_session;
-local uuid_gen = require "util.uuid".generate;
-local async = require "util.async";
+local add_task = require "prosody.util.timer".add_task;
+local stop_timer = require "prosody.util.timer".stop;
+local st = require "prosody.util.stanza";
+local initialize_filters = require "prosody.util.filters".initialize;
+local nameprep = require "prosody.util.encodings".stringprep.nameprep;
+local new_xmpp_stream = require "prosody.util.xmppstream".new;
+local s2s_new_incoming = require "prosody.core.s2smanager".new_incoming;
+local s2s_new_outgoing = require "prosody.core.s2smanager".new_outgoing;
+local s2s_destroy_session = require "prosody.core.s2smanager".destroy_session;
+local uuid_gen = require "prosody.util.uuid".generate;
+local async = require "prosody.util.async";
local runner = async.runner;
-local connect = require "net.connect".connect;
-local service = require "net.resolvers.service";
-local resolver_chain = require "net.resolvers.chain";
-local errors = require "util.error";
-local set = require "util.set";
-
-local connect_timeout = module:get_option_number("s2s_timeout", 90);
-local stream_close_timeout = module:get_option_number("s2s_close_timeout", 5);
+local connect = require "prosody.net.connect".connect;
+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 connect_timeout = module:get_option_period("s2s_timeout", 90);
+local stream_close_timeout = module:get_option_period("s2s_close_timeout", 5);
local opt_keepalives = module:get_option_boolean("s2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true));
local secure_auth = module:get_option_boolean("s2s_secure_auth", false); -- One day...
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_number("s2s_stanza_size_limit", 1024*512);
+local stanza_size_limit = module:get_option_integer("s2s_stanza_size_limit", 1024*512, 10000);
+
+local advertised_idle_timeout = 14*60; -- default in all net.server implementations
+local network_settings = module:get_option("network_settings");
+if type(network_settings) == "table" and type(network_settings.read_timeout) == "number" then
+ advertised_idle_timeout = network_settings.read_timeout;
+end
local measure_connections_inbound = module:metric(
"gauge", "connections_inbound", "",
@@ -95,6 +101,12 @@ local s2s_service_options = {
};
local s2s_service_options_mt = { __index = s2s_service_options }
+if module:get_option_boolean("use_dane", false) then
+ -- DANE is supported in net.connect but only for outgoing connections,
+ -- to authenticate incoming connections with DANE we need
+ module:depends("s2s_auth_dane_in");
+end
+
module:hook("stats-update", function ()
measure_connections_inbound:clear()
measure_connections_outbound:clear()
@@ -146,17 +158,17 @@ local function bounce_sendq(session, reason)
elseif type(reason) == "string" then
reason_text = reason;
end
- for i, data in ipairs(sendq) do
- local reply = data[2];
- if reply and not(reply.attr.xmlns) and bouncy_stanzas[reply.name] then
- reply.attr.type = "error";
- reply:tag("error", {type = error_type, by = session.from_host})
- :tag(condition, {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up();
- if reason_text then
- reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"})
- :text("Server-to-server connection failed: "..reason_text):up();
- end
+ for i, stanza in ipairs(sendq) do
+ if not stanza.attr.xmlns and bouncy_stanzas[stanza.name] and stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
+ local reply = st.error_reply(
+ stanza,
+ error_type,
+ condition,
+ reason_text and ("Server-to-server connection failed: "..reason_text) or nil
+ );
core_process_stanza(dummy, reply);
+ else
+ (session.log or log)("debug", "Not eligible for bouncing, discarding %s", stanza:top_tag());
end
sendq[i] = nil;
end
@@ -182,15 +194,11 @@ function route_to_existing_session(event)
(host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host);
-- Queue stanza until we are able to send it
- local queued_item = {
- tostring(stanza),
- stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza);
- };
if host.sendq then
- t_insert(host.sendq, queued_item);
+ t_insert(host.sendq, st.clone(stanza));
else
-- luacheck: ignore 122
- host.sendq = { queued_item };
+ host.sendq = { st.clone(stanza) };
end
host.log("debug", "stanza [%s] queued ", stanza.name);
return true;
@@ -215,7 +223,7 @@ function route_to_new_session(event)
-- Store in buffer
host_session.bounce_sendq = bounce_sendq;
- host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };
+ host_session.sendq = { st.clone(stanza) };
log("debug", "stanza [%s] queued until connection complete", stanza.name);
-- FIXME Cleaner solution to passing extra data from resolvers to net.server
-- This mt-clone allows resolvers to add extra data, currently used for DANE TLSA records
@@ -255,9 +263,37 @@ function module.add_host(module)
end
module:hook("route/remote", route_to_existing_session, -1);
module:hook("route/remote", route_to_new_session, -10);
+ module:hook("s2sout-stream-features", function (event)
+ if not (stanza_size_limit or advertised_idle_timeout) then return end
+ local limits = event.features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" })
+ if stanza_size_limit then
+ limits:text_tag("max-bytes", string.format("%d", stanza_size_limit));
+ end
+ if advertised_idle_timeout then
+ limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout));
+ end
+ limits:up();
+ end);
+ module:hook_tag("urn:xmpp:bidi", "bidi", function(session, stanza)
+ -- Advertising features on bidi connections where no <stream:features> is sent in the other direction
+ local limits = stanza:get_child("limits", "urn:xmpp:stream-limits:0");
+ if limits then
+ session.outgoing_stanza_size_limit = tonumber(limits:get_child_text("max-bytes"));
+ end
+ end, 100);
module:hook("s2s-authenticated", make_authenticated, -1);
module:hook("s2s-read-timeout", keepalive, -1);
+ module:hook("smacks-ack-delayed", function (event)
+ if event.origin.type == "s2sin" or event.origin.type == "s2sout" then
+ event.origin:close("connection-timeout");
+ return true;
+ end
+ end, -1);
module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza) -- luacheck: ignore 212/stanza
+ local limits = stanza:get_child("limits", "urn:xmpp:stream-limits:0");
+ if limits then
+ session.outgoing_stanza_size_limit = tonumber(limits:get_child_text("max-bytes"));
+ end
if session.type == "s2sout" then
-- Stream is authenticated and we are seem to be done with feature negotiation,
-- so the stream is ready for stanzas. RFC 6120 Section 4.3
@@ -283,7 +319,7 @@ function module.add_host(module)
function module.unload()
if module.reloading then return end
for _, session in pairs(sessions) do
- if session.to_host == module.host or session.from_host == module.host then
+ if session.host == module.host then
session:close("host-gone");
end
end
@@ -328,8 +364,8 @@ function mark_connected(session)
if sendq then
session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host);
local send = session.sends2s;
- for i, data in ipairs(sendq) do
- send(data[1]);
+ for i, stanza in ipairs(sendq) do
+ send(stanza);
sendq[i] = nil;
end
session.sendq = nil;
@@ -393,10 +429,10 @@ end
--- Helper to check that a session peer's certificate is valid
local function check_cert_status(session)
local host = session.direction == "outgoing" and session.to_host or session.from_host
- local conn = session.conn:socket()
+ local conn = session.conn
local cert
- if conn.getpeercertificate then
- cert = conn:getpeercertificate()
+ if conn.ssl_peercertificate then
+ cert = conn:ssl_peercertificate()
end
return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert });
@@ -408,8 +444,7 @@ local function session_secure(session)
session.secure = true;
session.encrypted = true;
- local sock = session.conn:socket();
- local info = sock.info and sock:info();
+ local info = session.conn:ssl_info();
if type(info) == "table" then
(session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
session.compressed = info.compression;
@@ -438,7 +473,8 @@ function stream_callbacks._streamopened(session, attr)
session.had_stream = true; -- Had a stream opened at least once
-- TODO: Rename session.secure to session.encrypted
- if session.secure == false then
+ if session.secure == false then -- Set by mod_tls during STARTTLS handshake
+ session.starttls = "completed";
session_secure(session);
end
@@ -526,6 +562,18 @@ function stream_callbacks._streamopened(session, attr)
end
if ( session.type == "s2sin" or session.type == "s2sout" ) or features.tags[1] then
+ if stanza_size_limit or advertised_idle_timeout then
+ features:reset();
+ local limits = features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" });
+ if stanza_size_limit then
+ limits:text_tag("max-bytes", string.format("%d", stanza_size_limit));
+ end
+ if advertised_idle_timeout then
+ limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout));
+ end
+ features:reset();
+ end
+
log("debug", "Sending stream features: %s", features);
session.sends2s(features);
else
@@ -760,6 +808,7 @@ local function initialize_session(session)
local w = conn.write;
if conn:ssl() then
+ -- Direct TLS was used
session_secure(session);
end
@@ -770,6 +819,11 @@ local function initialize_session(session)
end
if t then
t = filter("bytes/out", tostring(t));
+ if session.outgoing_stanza_size_limit and #t > session.outgoing_stanza_size_limit then
+ log("warn", "Attempt to send a stanza exceeding session limit of %dB (%dB)!", session.outgoing_stanza_size_limit, #t);
+ -- TODO Pass identifiable error condition back to allow appropriate handling
+ return false
+ end
if t then
return w(conn, t);
end
@@ -932,14 +986,27 @@ end
-- Complete the sentence "Your certificate " with what's wrong
local function friendly_cert_error(session) --> string
if session.cert_chain_status == "invalid" then
- if session.cert_chain_errors then
+ if type(session.cert_chain_errors) == "table" then
local cert_errors = set.new(session.cert_chain_errors[1]);
if cert_errors:contains("certificate has expired") then
return "has expired";
elseif cert_errors:contains("self signed certificate") then
return "is self-signed";
+ elseif cert_errors:contains("no matching DANE TLSA records") then
+ return "does not match any DANE TLSA records";
+ end
+
+ local chain_errors = set.new(session.cert_chain_errors[2]);
+ for i, e in pairs(session.cert_chain_errors) do
+ if i > 2 then chain_errors:add_list(e); end
+ end
+ if chain_errors:contains("certificate has expired") then
+ return "has an expired certificate chain";
+ elseif chain_errors:contains("no matching DANE TLSA records") then
+ return "does not match any DANE TLSA records";
end
end
+ -- TODO cert_chain_errors can be a string, handle that
return "is not trusted"; -- for some other reason
elseif session.cert_identity_status == "invalid" then
return "is not valid for this name";
@@ -966,6 +1033,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;
@@ -976,7 +1045,7 @@ module:hook("s2s-check-certificate", check_auth_policy, -1);
module:hook("server-stopping", function(event)
-- Close ports
- local pm = require "core.portmanager";
+ local pm = require "prosody.core.portmanager";
for _, netservice in pairs(module.items["net-provider"]) do
pm.unregister_service(netservice.name, netservice);
end
diff --git a/plugins/mod_s2s_auth_certs.lua b/plugins/mod_s2s_auth_certs.lua
index 992ee934..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 "util.x509".verify_identity;
-local NULL = {};
+local cert_verify_identity = require "prosody.util.x509".verify_identity;
local log = module._log;
local measure_cert_statuses = module:metric("counter", "checked", "", "Certificate validation results",
@@ -9,25 +8,26 @@ local measure_cert_statuses = module:metric("counter", "checked", "", "Certifica
module:hook("s2s-check-certificate", function(event)
local session, host, cert = event.session, event.host, event.cert;
- local conn = session.conn:socket();
+ local conn = session.conn;
local log = session.log or log;
+ local secure_hostname = conn.extra and conn.extra.secure_hostname;
+
if not cert then
log("warn", "No certificate provided by %s", host or "unknown host");
return;
end
- local chain_valid, errors;
- if conn.getpeerverification then
- chain_valid, errors = conn:getpeerverification();
- else
- chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } };
- end
+ local chain_valid, errors = conn:ssl_peerverification();
-- 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;
@@ -45,6 +45,14 @@ module:hook("s2s-check-certificate", function(event)
end
log("debug", "certificate identity validation result: %s", session.cert_identity_status);
end
+
+ -- Check for DNSSEC-signed SRV hostname
+ if secure_hostname and session.cert_identity_status ~= "valid" then
+ if cert_verify_identity(secure_hostname, "xmpp-server", cert) then
+ module:log("info", "Secure SRV name delegation %q -> %q", secure_hostname, host);
+ session.cert_identity_status = "valid"
+ end
+ end
end
measure_cert_statuses:with_labels(session.cert_chain_status or "unknown", session.cert_identity_status or "unknown"):add(1);
end, 509);
diff --git a/plugins/mod_s2s_auth_dane_in.lua b/plugins/mod_s2s_auth_dane_in.lua
new file mode 100644
index 00000000..9167e8a9
--- /dev/null
+++ b/plugins/mod_s2s_auth_dane_in.lua
@@ -0,0 +1,130 @@
+module:set_global();
+
+local dns = require "prosody.net.adns";
+local async = require "prosody.util.async";
+local encodings = require "prosody.util.encodings";
+local hashes = require "prosody.util.hashes";
+local promise = require "prosody.util.promise";
+local x509 = require "prosody.util.x509";
+
+local idna_to_ascii = encodings.idna.to_ascii;
+local sha256 = hashes.sha256;
+local sha512 = hashes.sha512;
+
+local use_dane = module:get_option_boolean("use_dane", nil);
+if use_dane == nil then
+ module:log("warn", "DANE support incomplete, add use_dane = true in the global section to support outgoing s2s connections");
+elseif use_dane == false then
+ module:log("debug", "DANE support disabled with use_dane = false, disabling.")
+ return
+end
+
+local function ensure_secure(r)
+ assert(r.secure, "insecure");
+ 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
+ local h = sha256(t[0]);
+ t[1] = h;
+ return h;
+ elseif i == 2 then
+ local h = sha512(t[0]);
+ t[1] = h;
+ return h;
+ end
+ end;
+}
+local function lazy_hash(t)
+ return setmetatable(t, lazy_tlsa_mt);
+end
+
+module:hook("s2s-check-certificate", function(event)
+ local session, host, cert = event.session, event.host, event.cert;
+ local log = session.log or module._log;
+
+ if not host or not cert or session.direction ~= "incoming" then
+ return
+ end
+
+ local by_select_match = {
+ [0] = lazy_hash {
+ -- cert
+ [0] = x509.pem2der(cert:pem());
+
+ };
+ }
+ if cert.pubkey then
+ by_select_match[1] = lazy_hash {
+ -- spki
+ [0] = x509.pem2der(cert:pubkey());
+ };
+ end
+
+ local resolver = dns.resolver();
+
+ local dns_domain = idna_to_ascii(host);
+
+ local function fetch_tlsa(res)
+ local tlsas = {};
+ for _, rr in ipairs(res) do
+ 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):next(flatten);
+ end
+
+ 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 _, 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
+
+ if found_supported then
+ session.cert_chain_status = "invalid";
+ session.cert_identity_status = nil;
+ return true;
+ end
+
+end, 800);
diff --git a/plugins/mod_s2s_bidi.lua b/plugins/mod_s2s_bidi.lua
index addcd6e2..8588ce59 100644
--- a/plugins/mod_s2s_bidi.lua
+++ b/plugins/mod_s2s_bidi.lua
@@ -5,17 +5,22 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
local xmlns_bidi_feature = "urn:xmpp:features:bidi"
local xmlns_bidi = "urn:xmpp:bidi";
local require_encryption = module:get_option_boolean("s2s_require_encryption", true);
+local offers_sent = module:metric("counter", "offers_sent", "", "Bidirectional connection offers sent", {});
+local offers_recv = module:metric("counter", "offers_recv", "", "Bidirectional connection offers received", {});
+local offers_taken = module:metric("counter", "offers_taken", "", "Bidirectional connection offers taken", {});
+
module:hook("s2s-stream-features", function(event)
local origin, features = event.origin, event.features;
if origin.type == "s2sin_unauthed" and (not require_encryption or origin.secure) then
features:tag("bidi", { xmlns = xmlns_bidi_feature }):up();
+ offers_sent:with_labels():add(1);
end
end);
@@ -25,7 +30,10 @@ module:hook_tag("http://etherx.jabber.org/streams", "features", function (sessio
if bidi then
session.incoming = true;
session.log("debug", "Requesting bidirectional stream");
- session.sends2s(st.stanza("bidi", { xmlns = xmlns_bidi }));
+ local request_bidi = st.stanza("bidi", { xmlns = xmlns_bidi });
+ module:fire_event("s2sout-stream-features", { origin = session, features = request_bidi });
+ session.sends2s(request_bidi);
+ offers_taken:with_labels():add(1);
end
end
end, 200);
@@ -34,6 +42,7 @@ module:hook_tag("urn:xmpp:bidi", "bidi", function(session)
if session.type == "s2sin_unauthed" and (not require_encryption or session.secure) then
session.log("debug", "Requested bidirectional stream");
session.outgoing = true;
+ offers_recv:with_labels():add(1);
return true;
end
end);
diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua
index ab863aa3..b6cd31c8 100644
--- a/plugins/mod_saslauth.lua
+++ b/plugins/mod_saslauth.lua
@@ -8,19 +8,26 @@
-- luacheck: ignore 431/log
-local st = require "util.stanza";
-local sm_bind_resource = require "core.sessionmanager".bind_resource;
-local sm_make_authenticated = require "core.sessionmanager".make_authenticated;
-local base64 = require "util.encodings".base64;
-local set = require "util.set";
-local errors = require "util.error";
-
-local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler;
+local st = require "prosody.util.stanza";
+local sm_bind_resource = require "prosody.core.sessionmanager".bind_resource;
+local sm_make_authenticated = require "prosody.core.sessionmanager".make_authenticated;
+local base64 = require "prosody.util.encodings".base64;
+local set = require "prosody.util.set";
+local errors = require "prosody.util.error";
+local hex = require "prosody.util.hex";
+local pem2der = require"util.x509".pem2der;
+local hashes = require"util.hashes";
+local ssl = require "ssl"; -- FIXME Isolate LuaSec from the rest of the code
+
+local certmanager = require "core.certmanager";
+local pm_get_tls_config_at = require "prosody.core.portmanager".get_tls_config_at;
+local usermanager_get_sasl_handler = require "prosody.core.usermanager".get_sasl_handler;
local secure_auth_only = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", true));
local allow_unencrypted_plain_auth = module:get_option_boolean("allow_unencrypted_plain_auth", false)
local insecure_mechanisms = module:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth and {} or {"PLAIN", "LOGIN"});
local disabled_mechanisms = module:get_option_set("disable_sasl_mechanisms", { "DIGEST-MD5" });
+local tls_server_end_point_hash = module:get_option_string("tls_server_end_point_hash");
local log = module._log;
@@ -49,11 +56,14 @@ local function handle_status(session, status, ret, err_msg)
return "failure", "temporary-auth-failure", "Connection gone";
end
if status == "failure" then
- module:fire_event("authentication-failure", { session = session, condition = ret, text = err_msg });
+ local event = { session = session, condition = ret, text = err_msg };
+ module:fire_event("authentication-failure", event);
session.sasl_handler = session.sasl_handler:clean_clone();
+ ret, err_msg = event.condition, event.text;
elseif status == "success" then
- local ok, err = sm_make_authenticated(session, session.sasl_handler.username, session.sasl_handler.scope);
+ local ok, err = sm_make_authenticated(session, session.sasl_handler.username, session.sasl_handler.role);
if ok then
+ session.sasl_resource = session.sasl_handler.resource;
module:fire_event("authentication-success", { session = session });
session.sasl_handler = nil;
session:reset_stream();
@@ -77,9 +87,12 @@ local function sasl_process_cdata(session, stanza)
return true;
end
end
- local status, ret, err_msg = session.sasl_handler:process(text);
+ local sasl_handler = session.sasl_handler;
+ local status, ret, err_msg = sasl_handler:process(text);
status, ret, err_msg = handle_status(session, status, ret, err_msg);
- local s = build_reply(status, ret, err_msg);
+ local event = { session = session, message = ret, error_text = err_msg };
+ module:fire_event("sasl/"..session.base_type.."/"..status, event);
+ local s = build_reply(status, event.message, event.error_text);
session.send(s);
return true;
end
@@ -205,6 +218,12 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event)
if session.type ~= "c2s_unauthed" or module:get_host_type() ~= "local" then return; end
+ -- event for preemptive checks, rate limiting etc
+ module:fire_event("authentication-attempt", event);
+ if event.allowed == false then
+ session.send(build_reply("failure", event.error_condition or "not-authorized", event.error_text));
+ return true;
+ end
if session.sasl_handler and session.sasl_handler.selected then
session.sasl_handler = nil; -- allow starting a new SASL negotiation before completing an old one
end
@@ -242,7 +261,53 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event)
end);
local function tls_unique(self)
- return self.userdata["tls-unique"]:getpeerfinished();
+ return self.userdata["tls-unique"]:ssl_peerfinished();
+end
+
+local function tls_exporter(conn)
+ if not conn.ssl_exportkeyingmaterial then return end
+ return conn:ssl_exportkeyingmaterial("EXPORTER-Channel-Binding", 32, "");
+end
+
+local function sasl_tls_exporter(self)
+ return tls_exporter(self.userdata["tls-exporter"]);
+end
+
+local function tls_server_end_point(self)
+ local cert_hash = self.userdata["tls-server-end-point"];
+ if cert_hash then return hex.from(cert_hash); end
+
+ local conn = self.userdata["tls-server-end-point-conn"];
+ local cert = conn.getlocalcertificate and conn:getlocalcertificate();
+
+ if not cert then
+ -- We don't know that this is the right cert, it could have been replaced on
+ -- disk since we started.
+ local certfile = self.userdata["tls-server-end-point-cert"];
+ if not certfile then return end
+ local f = io.open(certfile);
+ if not f then return end
+ local certdata = f:read("*a");
+ f:close();
+ cert = ssl.loadcertificate(certdata);
+ end
+
+ -- Hash function selection, see RFC 5929 §4.1
+ local hash, hash_name = hashes.sha256, "sha256";
+ if cert.getsignaturename then
+ local sigalg = cert:getsignaturename():lower():match("sha%d+");
+ if sigalg and sigalg ~= "sha1" and hashes[sigalg] then
+ -- This should have ruled out MD5 and SHA1
+ hash, hash_name = hashes[sigalg], sigalg;
+ end
+ end
+
+ local certdata_der = pem2der(cert:pem());
+ local hashed_der = hash(certdata_der);
+
+ module:log("debug", "tls-server-end-point: hex(%s(der)) = %q, hash = %s", hash_name, hex.encode(hashed_der));
+
+ return hashed_der;
end
local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' };
@@ -258,22 +323,60 @@ module:hook("stream-features", function(event)
end
local sasl_handler = usermanager_get_sasl_handler(module.host, origin)
origin.sasl_handler = sasl_handler;
+ local channel_bindings = set.new()
if origin.encrypted then
-- check whether LuaSec has the nifty binding to the function needed for tls-unique
-- FIXME: would be nice to have this check only once and not for every socket
if sasl_handler.add_cb_handler then
- local socket = origin.conn:socket();
- local info = socket.info and socket:info();
- if info.protocol == "TLSv1.3" then
+ local info = origin.conn:ssl_info();
+ if info and info.protocol == "TLSv1.3" then
log("debug", "Channel binding 'tls-unique' undefined in context of TLS 1.3");
- elseif socket.getpeerfinished and socket:getpeerfinished() then
+ if tls_exporter(origin.conn) then
+ log("debug", "Channel binding 'tls-exporter' supported");
+ sasl_handler:add_cb_handler("tls-exporter", sasl_tls_exporter);
+ channel_bindings:add("tls-exporter");
+ 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");
sasl_handler:add_cb_handler("tls-unique", tls_unique);
+ channel_bindings:add("tls-unique");
else
log("debug", "Channel binding 'tls-unique' not supported (by LuaSec?)");
end
+
+ local certfile;
+ if tls_server_end_point_hash == "auto" then
+ tls_server_end_point_hash = nil;
+ local ssl_cfg = origin.ssl_cfg;
+ if not ssl_cfg then
+ local server = origin.conn:server();
+ local tls_config = pm_get_tls_config_at(server:ip(), server:serverport());
+ local autocert = certmanager.find_host_cert(origin.conn:socket():getsniname());
+ ssl_cfg = autocert or tls_config;
+ end
+
+ certfile = ssl_cfg and ssl_cfg.certificate;
+ if certfile then
+ log("debug", "Channel binding 'tls-server-end-point' can be offered based on the certificate used");
+ sasl_handler:add_cb_handler("tls-server-end-point", tls_server_end_point);
+ channel_bindings:add("tls-server-end-point");
+ else
+ log("debug", "Channel binding 'tls-server-end-point' set to 'auto' but cannot determine cert");
+ end
+ elseif tls_server_end_point_hash then
+ log("debug", "Channel binding 'tls-server-end-point' can be offered with the configured certificate hash");
+ sasl_handler:add_cb_handler("tls-server-end-point", tls_server_end_point);
+ channel_bindings:add("tls-server-end-point");
+ end
+
sasl_handler["userdata"] = {
- ["tls-unique"] = socket;
+ ["tls-unique"] = origin.conn;
+ ["tls-exporter"] = origin.conn;
+ ["tls-server-end-point-cert"] = certfile;
+ ["tls-server-end-point-conn"] = origin.conn;
+ ["tls-server-end-point"] = tls_server_end_point_hash;
};
else
log("debug", "Channel binding not supported by SASL handler");
@@ -306,6 +409,14 @@ module:hook("stream-features", function(event)
mechanisms:tag("mechanism"):text(mechanism):up();
end
features:add_child(mechanisms);
+ if not channel_bindings:empty() then
+ -- XXX XEP-0440 is Experimental
+ features:tag("sasl-channel-binding", {xmlns='urn:xmpp:sasl-cb:0'})
+ for channel_binding in channel_bindings do
+ features:tag("channel-binding", {type=channel_binding}):up()
+ end
+ features:up();
+ end
return;
end
@@ -328,7 +439,7 @@ module:hook("stream-features", function(event)
authmod, available_disabled);
end
- else
+ elseif not origin.full_jid then
features:tag("bind", bind_attr):tag("required"):up():up();
features:tag("session", xmpp_session_attr):tag("optional"):up():up();
end
@@ -350,14 +461,15 @@ end);
module:hook("stanza/iq/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event)
local origin, stanza = event.origin, event.stanza;
- local resource;
- if stanza.attr.type == "set" then
+ local resource = origin.sasl_resource;
+ if stanza.attr.type == "set" and not resource then
local bind = stanza.tags[1];
resource = bind:get_child("resource");
resource = resource and #resource.tags == 0 and resource[1] or nil;
end
local success, err_type, err, err_msg = sm_bind_resource(origin, resource);
if success then
+ origin.sasl_resource = nil;
origin.send(st.reply(stanza)
:tag("bind", { xmlns = xmlns_bind })
:tag("jid"):text(origin.full_jid));
diff --git a/plugins/mod_scansion_record.lua b/plugins/mod_scansion_record.lua
index 5fefd398..1ec55952 100644
--- a/plugins/mod_scansion_record.lua
+++ b/plugins/mod_scansion_record.lua
@@ -2,11 +2,11 @@ local names = { "Romeo", "Juliet", "Mercutio", "Tybalt", "Benvolio" };
local devices = { "", "phone", "laptop", "tablet", "toaster", "fridge", "shoe" };
local users = {};
-local filters = require "util.filters";
-local id = require "util.id";
-local dt = require "util.datetime";
-local dm = require "util.datamanager";
-local st = require "util.stanza";
+local filters = require "prosody.util.filters";
+local id = require "prosody.util.id";
+local dt = require "prosody.util.datetime";
+local dm = require "prosody.util.datamanager";
+local st = require "prosody.util.stanza";
local record_id = id.short():lower();
local record_date = os.date("%Y%b%d"):lower();
diff --git a/plugins/mod_server_contact_info.lua b/plugins/mod_server_contact_info.lua
index 42316078..67fed752 100644
--- a/plugins/mod_server_contact_info.lua
+++ b/plugins/mod_server_contact_info.lua
@@ -6,21 +6,23 @@
-- COPYING file in the source package for more information.
--
-local array = require "util.array";
-local jid = require "util.jid";
+local array = require "prosody.util.array";
+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 = require "util.dataforms".new({
- { var = "FORM_TYPE"; type = "hidden"; value = "http://jabber.org/network/serverinfo"; };
- { 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", type = "list-multi" },
-});
+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", {});
@@ -29,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 e0a7bbfb..d4f0f371 100644
--- a/plugins/mod_smacks.lua
+++ b/plugins/mod_smacks.lua
@@ -2,7 +2,7 @@
--
-- Copyright (C) 2010-2015 Matthew Wild
-- Copyright (C) 2010 Waqas Hussain
--- Copyright (C) 2012-2021 Kim Alvefur
+-- Copyright (C) 2012-2022 Kim Alvefur
-- Copyright (C) 2012 Thijs Alkemade
-- Copyright (C) 2014 Florian Zeitz
-- Copyright (C) 2016-2020 Thilo Molitor
@@ -10,6 +10,7 @@
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
+-- TODO unify sendq and smqueue
local tonumber = tonumber;
local tostring = tostring;
@@ -38,23 +39,23 @@ 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");
-local datetime = require "util.datetime";
-local add_filter = require "util.filters".add_filter;
-local jid = require "util.jid";
-local smqueue = require "util.smqueue";
-local st = require "util.stanza";
-local timer = require "util.timer";
-local new_id = require "util.id".short;
-local watchdog = require "util.watchdog";
-local it = require"util.iterators";
+local datetime = require "prosody.util.datetime";
+local add_filter = require "prosody.util.filters".add_filter;
+local jid = require "prosody.util.jid";
+local smqueue = require "prosody.util.smqueue";
+local st = require "prosody.util.stanza";
+local timer = require "prosody.util.timer";
+local new_id = require "prosody.util.id".short;
+local watchdog = require "prosody.util.watchdog";
+local it = require"prosody.util.iterators";
-local sessionmanager = require "core.sessionmanager";
+local sessionmanager = require "prosody.core.sessionmanager";
local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas";
local xmlns_delay = "urn:xmpp:delay";
@@ -65,14 +66,14 @@ local xmlns_sm3 = "urn:xmpp:sm:3";
local sm2_attr = { xmlns = xmlns_sm2 };
local sm3_attr = { xmlns = xmlns_sm3 };
-local queue_size = module:get_option_number("smacks_max_queue_size", 500);
-local resume_timeout = module:get_option_number("smacks_hibernation_time", 600);
+local queue_size = module:get_option_integer("smacks_max_queue_size", 500, 1);
+local resume_timeout = module:get_option_period("smacks_hibernation_time", "10 minutes");
local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", true);
local s2s_resend = module:get_option_boolean("smacks_s2s_resend", false);
-local max_unacked_stanzas = module:get_option_number("smacks_max_unacked_stanzas", 0);
-local max_inactive_unacked_stanzas = module:get_option_number("smacks_max_inactive_unacked_stanzas", 256);
-local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 30);
-local max_old_sessions = module:get_option_number("smacks_max_old_sessions", 10);
+local max_unacked_stanzas = module:get_option_integer("smacks_max_unacked_stanzas", 0, 0);
+local max_inactive_unacked_stanzas = module:get_option_integer("smacks_max_inactive_unacked_stanzas", 256, 0);
+local delayed_ack_timeout = module:get_option_period("smacks_max_ack_delay", 30);
+local max_old_sessions = module:get_option_integer("smacks_max_old_sessions", 10, 0);
local c2s_sessions = module:shared("/*/c2s/sessions");
local local_sessions = prosody.hosts[module.host].sessions;
@@ -83,13 +84,43 @@ local all_old_sessions = module:open_store("smacks_h");
local old_session_registry = module:open_store("smacks_h", "map");
local session_registry = module:shared "/*/smacks/resumption-tokens"; -- > user@host/resumption-token --> resource
-local ack_errors = require"util.error".init("mod_smacks", xmlns_sm3, {
+local function registry_key(session, id)
+ return jid.join(session.username, session.host, id or session.resumption_token);
+end
+
+local function track_session(session, id)
+ session_registry[registry_key(session, id)] = session;
+ session.resumption_token = id;
+end
+
+local function save_old_session(session)
+ session_registry[registry_key(session)] = nil;
+ return old_session_registry:set(session.username, session.resumption_token,
+ { h = session.handled_stanza_count; t = os.time() })
+end
+
+local function clear_old_session(session, id)
+ session_registry[registry_key(session, id)] = nil;
+ return old_session_registry:set(session.username, id or session.resumption_token, nil)
+end
+
+local ack_errors = require"prosody.util.error".init("mod_smacks", xmlns_sm3, {
head = { condition = "undefined-condition"; text = "Client acknowledged more stanzas than sent by server" };
tail = { condition = "undefined-condition"; text = "Client acknowledged less stanzas than already acknowledged" };
pop = { condition = "internal-server-error"; text = "Something went wrong with Stream Management" };
overflow = { condition = "resource-constraint", text = "Too many unacked stanzas remaining, session can't be resumed" }
});
+local enable_errors = require "prosody.util.error".init("mod_smacks", xmlns_sm3, {
+ already_enabled = { condition = "unexpected-request", text = "Stream management is already enabled" };
+ bind_required = { condition = "unexpected-request", text = "Client must bind a resource before enabling stream management" };
+ unavailable = { condition = "service-unavailable", text = "Stream management is not available for this stream" };
+ -- Resumption
+ expired = { condition = "item-not-found", text = "Session expired, and cannot be resumed" };
+ already_bound = { condition = "unexpected-request", text = "Cannot resume another session after a resource is bound" };
+ unknown_session = { condition = "item-not-found", text = "Unknown session" };
+});
+
-- COMPAT note the use of compatibility wrapper in events (queue:table())
local function ack_delayed(session, stanza)
@@ -104,18 +135,18 @@ local function ack_delayed(session, stanza)
end
local function can_do_smacks(session, advertise_only)
- if session.smacks then return false, "unexpected-request", "Stream management is already enabled"; end
+ if session.smacks then return false, enable_errors.new("already_enabled"); end
local session_type = session.type;
if session.username then
if not(advertise_only) and not(session.resource) then -- Fail unless we're only advertising sm
- return false, "unexpected-request", "Client must bind a resource before enabling stream management";
+ return false, enable_errors.new("bind_required");
end
return true;
elseif s2s_smacks and (session_type == "s2sin" or session_type == "s2sout") then
return true;
end
- return false, "service-unavailable", "Stream management is not available for this stream";
+ return false, enable_errors.new("unavailable");
end
module:hook("stream-features",
@@ -155,13 +186,12 @@ end
local function request_ack(session, reason)
local queue = session.outgoing_stanza_queue;
- session.log("debug", "Sending <r> (inside timer, before send) from %s - #queue=%d", reason, queue:count_unacked());
+ session.log("debug", "Sending <r> from %s - #queue=%d", reason, queue:count_unacked());
session.awaiting_ack = true;
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }))
if session.destroyed then return end -- sending something can trigger destruction
-- expected_h could be lower than this expression e.g. more stanzas added to the queue meanwhile)
session.last_requested_h = queue:count_acked() + queue:count_unacked();
- session.log("debug", "Sending <r> (inside timer, after send) from %s - #queue=%d", reason, queue:count_unacked());
if not session.delayed_ack_timer then
session.delayed_ack_timer = timer.add_task(delayed_ack_timeout, function()
ack_delayed(session, nil); -- we don't know if this is the only new stanza in the queue
@@ -180,7 +210,6 @@ local function outgoing_stanza_filter(stanza, session)
-- supposed to be nil.
-- However, when using mod_smacks with mod_websocket, then mod_websocket's
-- stanzas/out filter can get called before this one and adds the xmlns.
- if session.resending_unacked then return stanza end
if not session.smacks then return stanza end
local is_stanza = st.is_stanza(stanza) and
(not stanza.attr.xmlns or stanza.attr.xmlns == 'jabber:client')
@@ -234,8 +263,7 @@ module:hook("pre-session-close", function(event)
if session.smacks == nil then return end
if session.resumption_token then
session.log("debug", "Revoking resumption token");
- session_registry[jid.join(session.username, session.host, session.resumption_token)] = nil;
- old_session_registry:set(session.username, session.resumption_token, nil);
+ clear_old_session(session);
session.resumption_token = nil;
else
session.log("debug", "Session not resumable");
@@ -274,17 +302,16 @@ local function wrap_session(session, resume)
return session;
end
-function handle_enable(session, stanza, xmlns_sm)
- local ok, err, err_text = can_do_smacks(session);
+function do_enable(session, stanza)
+ local ok, err = can_do_smacks(session);
if not ok then
- session.log("warn", "Failed to enable smacks: %s", err_text); -- TODO: XEP doesn't say we can send error text, should it?
- (session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):tag(err, { xmlns = xmlns_errors}));
- return true;
+ session.log("warn", "Failed to enable smacks: %s", err.text); -- TODO: XEP doesn't say we can send error text, should it?
+ return nil, err;
end
if session.username then
local old_sessions, err = all_old_sessions:get(session.username);
- module:log("debug", "Old sessions: %q", old_sessions)
+ session.log("debug", "Old sessions: %q", old_sessions)
if old_sessions then
local keep, count = {}, 0;
for token, info in it.sorted_pairs(old_sessions, function(a, b)
@@ -296,54 +323,73 @@ function handle_enable(session, stanza, xmlns_sm)
end
all_old_sessions:set(session.username, keep);
elseif err then
- module:log("error", "Unable to retrieve old resumption counters: %s", err);
+ session.log("error", "Unable to retrieve old resumption counters: %s", err);
end
end
- module:log("debug", "Enabling stream management");
- session.smacks = xmlns_sm;
-
- wrap_session(session, false);
-
- local resume_max;
local resume_token;
local resume = stanza.attr.resume;
if (resume == "true" or resume == "1") and session.username then
-- resumption on s2s is not currently supported
resume_token = new_id();
- session_registry[jid.join(session.username, session.host, resume_token)] = session;
- session.resumption_token = resume_token;
- resume_max = tostring(resume_timeout);
end
- (session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm, id = resume_token, resume = resume, max = resume_max }));
+
+ return {
+ type = "enabled";
+ id = resume_token;
+ resume_max = resume_token and tostring(resume_timeout) or nil;
+ session = session;
+ finish = function ()
+ session.log("debug", "Enabling stream management");
+
+ session.smacks = stanza.attr.xmlns;
+ if resume_token then
+ track_session(session, resume_token);
+ end
+ wrap_session(session, false);
+ end;
+ };
+end
+
+function handle_enable(session, stanza, xmlns_sm)
+ local enabled, err = do_enable(session, stanza);
+ if not enabled then
+ (session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):add_error(err));
+ return true;
+ end
+
+ (session.sends2s or session.send)(st.stanza("enabled", {
+ xmlns = xmlns_sm;
+ id = enabled.id;
+ resume = enabled.id and "true" or nil; -- COMPAT w/ Conversations 2.10.10 requires 'true' not '1'
+ max = enabled.resume_max;
+ }));
+
+ session.smacks = xmlns_sm;
+ enabled.finish();
+
return true;
end
module:hook_tag(xmlns_sm2, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm2); end, 100);
module:hook_tag(xmlns_sm3, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm3); end, 100);
-module:hook_tag("http://etherx.jabber.org/streams", "features",
- function (session, stanza)
- -- Needs to be done after flushing sendq since those aren't stored as
- -- stanzas and counting them is weird.
- -- TODO unify sendq and smqueue
- timer.add_task(1e-6, function ()
- if can_do_smacks(session) then
- if stanza:get_child("sm", xmlns_sm3) then
- session.sends2s(st.stanza("enable", sm3_attr));
- session.smacks = xmlns_sm3;
- elseif stanza:get_child("sm", xmlns_sm2) then
- session.sends2s(st.stanza("enable", sm2_attr));
- session.smacks = xmlns_sm2;
- else
- return;
- end
- wrap_session_out(session, false);
- end
- end);
- end);
+module:hook_tag("http://etherx.jabber.org/streams", "features", function(session, stanza)
+ if can_do_smacks(session) then
+ session.smacks_feature = stanza:get_child("sm", xmlns_sm3) or stanza:get_child("sm", xmlns_sm2);
+ end
+end);
+
+module:hook("s2sout-established", function (event)
+ local session = event.session;
+ if not session.smacks_feature then return end
+
+ session.smacks = session.smacks_feature.attr.xmlns;
+ wrap_session_out(session, false);
+ session.sends2s(st.stanza("enable", { xmlns = session.smacks }));
+end);
function handle_enabled(session, stanza, xmlns_sm) -- luacheck: ignore 212/stanza
- module:log("debug", "Enabling stream management");
+ session.log("debug", "Enabling stream management");
session.smacks = xmlns_sm;
wrap_session_in(session, false);
@@ -357,10 +403,10 @@ module:hook_tag(xmlns_sm3, "enabled", function (session, stanza) return handle_e
function handle_r(origin, stanza, xmlns_sm) -- luacheck: ignore 212/stanza
if not origin.smacks then
- module:log("debug", "Received ack request from non-smack-enabled session");
+ origin.log("debug", "Received ack request from non-smack-enabled session");
return;
end
- module:log("debug", "Received ack request, acking for %d", origin.handled_stanza_count);
+ origin.log("debug", "Received ack request, acking for %d", origin.handled_stanza_count);
-- Reply with <a>
(origin.sends2s or origin.send)(st.stanza("a", { xmlns = xmlns_sm, h = format_h(origin.handled_stanza_count) }));
-- piggyback our own ack request if needed (see request_ack_if_needed() for explanation of last_requested_h)
@@ -413,13 +459,14 @@ local function handle_unacked_stanzas(session)
local queue = session.outgoing_stanza_queue;
local unacked = queue:count_unacked()
if unacked > 0 then
+ local error_from = jid.join(session.username, session.host or module.host);
tx_dropped_stanzas:sample(unacked);
session.smacks = false; -- Disable queueing
session.outgoing_stanza_queue = nil;
for stanza in queue._queue:consume() do
if not module:fire_event("delivery/failure", { session = session, stanza = stanza }) then
if stanza.attr.type ~= "error" and stanza.attr.from ~= session.full_jid then
- local reply = st.error_reply(stanza, "cancel", "recipient-unavailable");
+ local reply = st.error_reply(stanza, "cancel", "recipient-unavailable", nil, error_from);
module:send(reply);
end
end
@@ -495,11 +542,8 @@ module:hook("pre-resource-unbind", function (event)
end
session.log("debug", "Destroying session for hibernating too long");
- session_registry[jid.join(session.username, session.host, session.resumption_token)] = nil;
- old_session_registry:set(session.username, session.resumption_token,
- { h = session.handled_stanza_count; t = os.time() });
+ save_old_session(session);
session.resumption_token = nil;
- session.resending_unacked = true; -- stop outgoing_stanza_filter from re-queueing anything anymore
sessionmanager.destroy_session(session, "Hibernating too long");
sessions_expired(1);
end);
@@ -533,131 +577,110 @@ end
module:hook("s2sout-destroyed", handle_s2s_destroyed);
module:hook("s2sin-destroyed", handle_s2s_destroyed);
-local function get_session_id(session)
- return session.id or (tostring(session):match("[a-f0-9]+$"));
-end
-
-function handle_resume(session, stanza, xmlns_sm)
+function do_resume(session, stanza)
if session.full_jid then
session.log("warn", "Tried to resume after resource binding");
- session.send(st.stanza("failed", { xmlns = xmlns_sm })
- :tag("unexpected-request", { xmlns = xmlns_errors })
- );
- return true;
+ return nil, enable_errors.new("already_bound");
end
local id = stanza.attr.previd;
- local original_session = session_registry[jid.join(session.username, session.host, id)];
+ local original_session = session_registry[registry_key(session, id)];
if not original_session then
local old_session = old_session_registry:get(session.username, id);
if old_session then
session.log("debug", "Tried to resume old expired session with id %s", id);
- session.send(st.stanza("failed", { xmlns = xmlns_sm, h = format_h(old_session.h) })
- :tag("item-not-found", { xmlns = xmlns_errors })
- );
- old_session_registry:set(session.username, id, nil);
+ clear_old_session(session, id);
resumption_expired(1);
- else
- session.log("debug", "Tried to resume non-existent session with id %s", id);
- session.send(st.stanza("failed", { xmlns = xmlns_sm })
- :tag("item-not-found", { xmlns = xmlns_errors })
- );
- end;
- else
- if original_session.hibernating_watchdog then
- original_session.log("debug", "Letting the watchdog go");
- original_session.hibernating_watchdog:cancel();
- original_session.hibernating_watchdog = nil;
- elseif session.hibernating then
- original_session.log("error", "Hibernating session has no watchdog!")
- end
- -- zero age = was not hibernating yet
- local age = 0;
- if original_session.hibernating then
- local now = os_time();
- age = now - original_session.hibernating;
- end
- session.log("debug", "mod_smacks resuming existing session %s...", get_session_id(original_session));
- original_session.log("debug", "mod_smacks session resumed from %s...", get_session_id(session));
- -- TODO: All this should move to sessionmanager (e.g. session:replace(new_session))
- if original_session.conn then
- original_session.log("debug", "mod_smacks closing an old connection for this session");
- local conn = original_session.conn;
- c2s_sessions[conn] = nil;
- conn:close();
+ return nil, enable_errors.new("expired", { h = old_session.h });
end
+ session.log("debug", "Tried to resume non-existent session with id %s", id);
+ return nil, enable_errors.new("unknown_session");
+ end
- local migrated_session_log = session.log;
- original_session.ip = session.ip;
- original_session.conn = session.conn;
- original_session.rawsend = session.rawsend;
- original_session.rawsend.session = original_session;
- original_session.rawsend.conn = original_session.conn;
- original_session.send = session.send;
- original_session.send.session = original_session;
- original_session.close = session.close;
- original_session.filter = session.filter;
- original_session.filter.session = original_session;
- original_session.filters = session.filters;
- original_session.send.filter = original_session.filter;
- original_session.stream = session.stream;
- original_session.secure = session.secure;
- original_session.hibernating = nil;
- original_session.resumption_counter = (original_session.resumption_counter or 0) + 1;
- session.log = original_session.log;
- session.type = original_session.type;
- wrap_session(original_session, true);
- -- Inform xmppstream of the new session (passed to its callbacks)
- original_session.stream:set_session(original_session);
- -- Similar for connlisteners
- c2s_sessions[session.conn] = original_session;
-
- local queue = original_session.outgoing_stanza_queue;
- local h = tonumber(stanza.attr.h);
-
- original_session.log("debug", "Pre-resumption #queue = %d", queue:count_unacked())
- local acked, err = ack_errors.coerce(queue:ack(h)); -- luacheck: ignore 211/acked
-
- if not err and not queue:resumable() then
- err = ack_errors.new("overflow");
- end
+ if original_session.hibernating_watchdog then
+ original_session.log("debug", "Letting the watchdog go");
+ original_session.hibernating_watchdog:cancel();
+ original_session.hibernating_watchdog = nil;
+ elseif session.hibernating then
+ original_session.log("error", "Hibernating session has no watchdog!")
+ end
+ -- zero age = was not hibernating yet
+ local age = 0;
+ if original_session.hibernating then
+ local now = os_time();
+ age = now - original_session.hibernating;
+ end
- if err or not queue:resumable() then
- original_session.send(st.stanza("failed",
- { xmlns = xmlns_sm; h = format_h(original_session.handled_stanza_count); previd = id }));
- original_session:close(err);
- return false;
- end
+ session.log("debug", "mod_smacks resuming existing session %s...", original_session.id);
- original_session.send(st.stanza("resumed", { xmlns = xmlns_sm,
- h = format_h(original_session.handled_stanza_count), previd = id }));
+ local queue = original_session.outgoing_stanza_queue;
+ local h = tonumber(stanza.attr.h);
- -- Ok, we need to re-send any stanzas that the client didn't see
- -- ...they are what is now left in the outgoing stanza queue
- -- We have to use the send of "session" because we don't want to add our resent stanzas
- -- to the outgoing queue again
+ original_session.log("debug", "Pre-resumption #queue = %d", queue:count_unacked())
+ local acked, err = ack_errors.coerce(queue:ack(h)); -- luacheck: ignore 211/acked
- session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", queue:count_unacked());
- -- FIXME Which session is it that the queue filter sees?
- session.resending_unacked = true;
- original_session.resending_unacked = true;
- for _, queued_stanza in queue:resume() do
- session.send(queued_stanza);
- end
- session.resending_unacked = nil;
- original_session.resending_unacked = nil;
- session.log("debug", "all stanzas resent, now disabling send() in this migrated session, #queue = %d", queue:count_unacked());
- function session.send(stanza) -- luacheck: ignore 432
- migrated_session_log("error", "Tried to send stanza on old session migrated by smacks resume (maybe there is a bug?): %s", tostring(stanza));
- return false;
- end
- module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue:table()});
- original_session.awaiting_ack = nil; -- Don't wait for acks from before the resumption
- request_ack_now_if_needed(original_session, true, "handle_resume", nil);
- resumption_age:sample(age);
+ if not err and not queue:resumable() then
+ err = ack_errors.new("overflow");
end
+
+ if err then
+ session.log("debug", "Resumption failed: %s", err);
+ return nil, err;
+ end
+
+ -- Update original_session with the parameters (connection, etc.) from the new session
+ sessionmanager.update_session(original_session, session);
+
+ return {
+ type = "resumed";
+ session = original_session;
+ id = id;
+ -- Return function to complete the resumption and resync unacked stanzas
+ -- This is two steps so we can support SASL2/ISR
+ finish = function ()
+ -- Ok, we need to re-send any stanzas that the client didn't see
+ -- ...they are what is now left in the outgoing stanza queue
+ -- We have to use the send of "session" because we don't want to add our resent stanzas
+ -- to the outgoing queue again
+
+ original_session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", queue:count_unacked());
+ for _, queued_stanza in queue:resume() do
+ original_session.send(queued_stanza);
+ end
+ original_session.log("debug", "all stanzas resent, enabling stream management on resumed stream, #queue = %d", queue:count_unacked());
+
+ -- Add our own handlers to the resumed session (filters have been reset in the update)
+ wrap_session(original_session, true);
+
+ -- Let everyone know that we are no longer hibernating
+ module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue:table()});
+ original_session.awaiting_ack = nil; -- Don't wait for acks from before the resumption
+ request_ack_now_if_needed(original_session, true, "handle_resume", nil);
+ resumption_age:sample(age);
+ end;
+ };
+end
+
+function handle_resume(session, stanza, xmlns_sm)
+ local resumed, err = do_resume(session, stanza);
+ if not resumed then
+ session.send(st.stanza("failed", { xmlns = xmlns_sm, h = format_h(err.context.h) })
+ :tag(err.condition, { xmlns = xmlns_errors }));
+ return true;
+ end
+
+ session = resumed.session;
+
+ -- Inform client of successful resumption
+ session.send(st.stanza("resumed", { xmlns = xmlns_sm,
+ h = format_h(session.handled_stanza_count), previd = resumed.id }));
+
+ -- Complete resume (sync stanzas, etc.)
+ resumed.finish();
+
return true;
end
+
module:hook_tag(xmlns_sm2, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm2); end);
module:hook_tag(xmlns_sm3, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm3); end);
@@ -712,8 +735,7 @@ module:hook_global("server-stopping", function(event)
for _, user in pairs(local_sessions) do
for _, session in pairs(user.sessions) do
if session.resumption_token then
- if old_session_registry:set(session.username, session.resumption_token,
- { h = session.handled_stanza_count; t = os.time() }) then
+ if save_old_session(session) then
session.resumption_token = nil;
-- Deal with unacked stanzas
diff --git a/plugins/mod_stanza_debug.lua b/plugins/mod_stanza_debug.lua
index af98670c..4feab7ae 100644
--- a/plugins/mod_stanza_debug.lua
+++ b/plugins/mod_stanza_debug.lua
@@ -1,6 +1,6 @@
module:set_global();
-local filters = require "util.filters";
+local filters = require "prosody.util.filters";
local function log_send(t, session)
if t and t ~= "" and t ~= " " then
diff --git a/plugins/mod_storage_internal.lua b/plugins/mod_storage_internal.lua
index fa87e495..a43dd272 100644
--- a/plugins/mod_storage_internal.lua
+++ b/plugins/mod_storage_internal.lua
@@ -1,17 +1,20 @@
-local cache = require "util.cache";
-local datamanager = require "core.storagemanager".olddm;
-local array = require "util.array";
-local datetime = require "util.datetime";
-local st = require "util.stanza";
-local now = require "util.time".now;
-local id = require "util.id".medium;
-local jid_join = require "util.jid".join;
-local set = require "util.set";
+local cache = require "prosody.util.cache";
+local datamanager = require "prosody.core.storagemanager".olddm;
+local array = require "prosody.util.array";
+local datetime = require "prosody.util.datetime";
+local st = require "prosody.util.stanza";
+local now = require "prosody.util.time".now;
+local id = require "prosody.util.id".medium;
+local jid_join = require "prosody.util.jid".join;
+local set = require "prosody.util.set";
+local it = require "prosody.util.iterators";
local host = module.host;
-local archive_item_limit = module:get_option_number("storage_archive_item_limit", 10000);
-local archive_item_count_cache = cache.new(module:get_option("storage_archive_item_limit_cache_size", 1000));
+local archive_item_limit = module:get_option_integer("storage_archive_item_limit", 10000, 0);
+local archive_item_count_cache = cache.new(module:get_option_integer("storage_archive_item_limit_cache_size", 1000, 1));
+
+local use_shift = module:get_option_boolean("storage_archive_experimental_fast_delete", false);
local driver = {};
@@ -121,100 +124,144 @@ function archive:append(username, key, value, when, with)
return key;
end
+local function binary_search(haystack, test, min, max)
+ if min == nil then
+ min = 1;
+ end
+ if max == nil then
+ max = #haystack;
+ end
+
+ local floor = math.floor;
+ while min < max do
+ local mid = floor((max + min) / 2);
+
+ local result = test(haystack[mid]);
+ if result < 0 then
+ max = mid;
+ elseif result > 0 then
+ min = mid + 1;
+ else
+ return mid, haystack[mid];
+ end
+ end
+
+ return min, nil;
+end
+
function archive:find(username, query)
- local items, err = datamanager.list_load(username, host, self.store);
- if not items then
+ local list, err = datamanager.list_open(username, host, self.store);
+ if not list then
if err then
- return items, err;
+ return list, err;
elseif query then
if query.before or query.after then
return nil, "item-not-found";
end
if query.total then
- return function () end, 0;
+ return function()
+ end, 0;
end
end
- return function () end;
+ return function()
+ end;
+ end
+
+ local i = 0;
+ local iter = function()
+ i = i + 1;
+ return list[i]
end
- local count = nil;
- local i, last_key = 0;
+
if query then
- items = array(items);
+ if query.reverse then
+ i = #list + 1
+ iter = function()
+ i = i - 1
+ return list[i]
+ end
+ query.before, query.after = query.after, query.before;
+ end
if query.key then
- items:filter(function (item)
+ iter = it.filter(function(item)
return item.key == query.key;
- end);
+ end, iter);
end
if query.ids then
local ids = set.new(query.ids);
- items:filter(function (item)
+ iter = it.filter(function(item)
return ids:contains(item.key);
- end);
+ end, iter);
end
if query.with then
- items:filter(function (item)
+ iter = it.filter(function(item)
return item.with == query.with;
- end);
+ end, iter);
end
if query.start then
- items:filter(function (item)
- local when = item.when or datetime.parse(item.attr.stamp);
- return when >= query.start;
- end);
+ if not query.reverse then
+ local wi = binary_search(list, function(item)
+ local when = item.when or datetime.parse(item.attr.stamp);
+ return query.start - when;
+ end);
+ i = wi - 1;
+ else
+ iter = it.filter(function(item)
+ local when = item.when or datetime.parse(item.attr.stamp);
+ return when >= query.start;
+ end, iter);
+ end
end
if query["end"] then
- items:filter(function (item)
- local when = item.when or datetime.parse(item.attr.stamp);
- return when <= query["end"];
- end);
- end
- if query.total then
- count = #items;
- end
- if query.reverse then
- items:reverse();
- if query.before then
- local found = false;
- for j = 1, #items do
- if (items[j].key or tostring(j)) == query.before then
- found = true;
- i = j;
- break;
- end
- end
- if not found then
- return nil, "item-not-found";
+ if query.reverse then
+ local wi = binary_search(list, function(item)
+ local when = item.when or datetime.parse(item.attr.stamp);
+ return query["end"] - when;
+ end);
+ if wi then
+ i = wi + 1;
end
+ else
+ iter = it.filter(function(item)
+ local when = item.when or datetime.parse(item.attr.stamp);
+ return when <= query["end"];
+ end, iter);
end
- last_key = query.after;
- elseif query.after then
+ end
+ if query.after then
local found = false;
- for j = 1, #items do
- if (items[j].key or tostring(j)) == query.after then
- found = true;
- i = j;
- break;
+ iter = it.filter(function(item)
+ local found_after = found;
+ if item.key == query.after then
+ found = true
end
- end
- if not found then
- return nil, "item-not-found";
- end
- last_key = query.before;
- elseif query.before then
- last_key = query.before;
+ return found_after;
+ end, iter);
end
- if query.limit and #items - i > query.limit then
- items[i+query.limit+1] = nil;
+ if query.before then
+ local found = false;
+ iter = it.filter(function(item)
+ if item.key == query.before then
+ found = true
+ end
+ return not found;
+ end, iter);
+ end
+ if query.limit then
+ iter = it.head(query.limit, iter);
end
end
- return function ()
- i = i + 1;
- local item = items[i];
- if not item or (last_key and item.key == last_key) then
- return;
+
+ return function()
+ local item = iter();
+ if item == nil then
+ if list.close then
+ list:close();
+ end
+ return
end
- local key = item.key or tostring(i);
- local when = item.when or datetime.parse(item.attr.stamp);
+ local key = item.key;
+ local when = item.when or item.attr and datetime.parse(item.attr.stamp);
local with = item.with;
item.key, item.when, item.with = nil, nil, nil;
item.attr.stamp = nil;
@@ -222,7 +269,7 @@ function archive:find(username, query)
item.attr.stamp_legacy = nil;
item = st.deserialize(item);
return key, item, when, with;
- end, count;
+ end
end
function archive:get(username, wanted_key)
@@ -297,12 +344,53 @@ function archive:users()
return datamanager.users(host, self.store, "list");
end
+function archive:trim(username, to_when)
+ local cache_key = jid_join(username, host, self.store);
+ local list, err = datamanager.list_open(username, host, self.store);
+ if not list then
+ if err == nil then
+ module:log("debug", "store already empty, can't trim");
+ return 0;
+ end
+ return list, err;
+ end
+
+ -- shortcut: check if the last item should be trimmed, if so, drop the whole archive
+ local last = list[#list].when or datetime.parse(list[#list].attr.stamp);
+ if last <= to_when then
+ if list.close then
+ list:close()
+ end
+ return datamanager.list_store(username, host, self.store, nil);
+ end
+
+ -- luacheck: ignore 211/exact
+ local i, exact = binary_search(list, function(item)
+ local when = item.when or datetime.parse(item.attr.stamp);
+ return to_when - when;
+ end);
+ if list.close then
+ list:close()
+ end
+ -- TODO if exact then ... off by one?
+ if i == 1 then return 0; end
+ local ok, err = datamanager.list_shift(username, host, self.store, i);
+ if not ok then return ok, err; end
+ archive_item_count_cache:set(cache_key, nil); -- TODO calculate how many items are left
+ return i-1;
+end
+
function archive:delete(username, query)
local cache_key = jid_join(username, host, self.store);
if not query or next(query) == nil then
- archive_item_count_cache:set(cache_key, nil);
+ archive_item_count_cache:set(cache_key, nil); -- nil because we don't check if the following succeeds
return datamanager.list_store(username, host, self.store, nil);
end
+
+ if use_shift and next(query) == "end" and next(query, "end") == nil then
+ return self:trim(username, query["end"]);
+ end
+
local items, err = datamanager.list_load(username, host, self.store);
if not items then
if err then
diff --git a/plugins/mod_storage_memory.lua b/plugins/mod_storage_memory.lua
index 9b0024ab..49f94d1d 100644
--- a/plugins/mod_storage_memory.lua
+++ b/plugins/mod_storage_memory.lua
@@ -1,15 +1,15 @@
-local serialize = require "util.serialization".serialize;
-local array = require "util.array";
-local envload = require "util.envload".envload;
-local st = require "util.stanza";
+local serialize = require "prosody.util.serialization".serialize;
+local array = require "prosody.util.array";
+local envload = require "prosody.util.envload".envload;
+local st = require "prosody.util.stanza";
local is_stanza = st.is_stanza or function (s) return getmetatable(s) == st.stanza_mt end
-local new_id = require "util.id".medium;
-local set = require "util.set";
+local new_id = require "prosody.util.id".medium;
+local set = require "prosody.util.set";
local auto_purge_enabled = module:get_option_boolean("storage_memory_temporary", false);
local auto_purge_stores = module:get_option_set("storage_memory_temporary_stores", {});
-local archive_item_limit = module:get_option_number("storage_archive_item_limit", 1000);
+local archive_item_limit = module:get_option_integer("storage_archive_item_limit", 1000, 0);
local memory = setmetatable({}, {
__index = function(t, k)
diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua
index b3ed7638..3f606160 100644
--- a/plugins/mod_storage_sql.lua
+++ b/plugins/mod_storage_sql.lua
@@ -1,19 +1,34 @@
-- luacheck: ignore 212/self
-local cache = require "util.cache";
-local json = require "util.json";
-local sql = require "util.sql";
-local xml_parse = require "util.xml".parse;
-local uuid = require "util.uuid";
-local resolve_relative_path = require "util.paths".resolve_relative_path;
-local jid_join = require "util.jid".join;
-
-local is_stanza = require"util.stanza".is_stanza;
+local cache = require "prosody.util.cache";
+local json = require "prosody.util.json";
+local xml_parse = require "prosody.util.xml".parse;
+local uuid = require "prosody.util.uuid";
+local resolve_relative_path = require "prosody.util.paths".resolve_relative_path;
+local jid_join = require "prosody.util.jid".join;
+
+local is_stanza = require"prosody.util.stanza".is_stanza;
local t_concat = table.concat;
+local have_dbisql, dbisql = pcall(require, "prosody.util.sql");
+local have_sqlite, sqlite = pcall(require, "prosody.util.sqlite3");
+if not have_dbisql then
+ module:log("debug", "Could not load LuaDBI, error was: %s", dbisql)
+ dbisql = nil;
+end
+if not have_sqlite then
+ module:log("debug", "Could not load LuaSQLite3, error was: %s", sqlite)
+ sqlite = nil;
+end
+if not (have_dbisql or have_sqlite) then
+ module:log("error", "LuaDBI or LuaSQLite3 are required for using SQL databases but neither are installed");
+ module:log("error", "Please install at least one of LuaDBI and LuaSQLite3. See https://prosody.im/doc/depends");
+ error("No SQL library available")
+end
+
local noop = function() end
-local unpack = table.unpack or unpack; -- luacheck: ignore 113
+local unpack = table.unpack;
local function iterator(result)
return function(result_)
local row = result_();
@@ -59,9 +74,8 @@ local function deserialize(t, value)
end
local host = module.host;
-local user, store;
-local function keyval_store_get()
+local function keyval_store_get(user, store)
local haveany;
local result = {};
local select_sql = [[
@@ -86,7 +100,7 @@ local function keyval_store_get()
return result;
end
end
-local function keyval_store_set(data)
+local function keyval_store_set(data, user, store)
local delete_sql = [[
DELETE FROM "prosody"
WHERE "host"=? AND "user"=? AND "store"=?
@@ -121,19 +135,15 @@ end
local keyval_store = {};
keyval_store.__index = keyval_store;
function keyval_store:get(username)
- user, store = username, self.store;
- local ok, result = engine:transaction(keyval_store_get);
+ local ok, result = engine:transaction(keyval_store_get, username, self.store);
if not ok then
- module:log("error", "Unable to read from database %s store for %s: %s", store, username or "<host>", result);
+ module:log("error", "Unable to read from database %s store for %s: %s", self.store, username or "<host>", result);
return nil, result;
end
return result;
end
function keyval_store:set(username, data)
- user,store = username,self.store;
- return engine:transaction(function()
- return keyval_store_set(data);
- end);
+ return engine:transaction(keyval_store_set, data, username, self.store);
end
function keyval_store:users()
local ok, result = engine:transaction(function()
@@ -150,8 +160,8 @@ end
--- Archive store API
-local archive_item_limit = module:get_option_number("storage_archive_item_limit");
-local archive_item_count_cache = cache.new(module:get_option("storage_archive_item_limit_cache_size", 1000));
+local archive_item_limit = module:get_option_integer("storage_archive_item_limit", nil, 0);
+local archive_item_count_cache = cache.new(module:get_option_integer("storage_archive_item_limit_cache_size", 1000, 1));
local item_count_cache_hit = module:measure("item_count_cache_hit", "rate");
local item_count_cache_miss = module:measure("item_count_cache_miss", "rate")
@@ -201,6 +211,13 @@ function map_store:set_keys(username, keydatas)
("host","user","store","key","type","value")
VALUES (?,?,?,?,?,?);
]];
+ local upsert_sql = [[
+ INSERT INTO "prosody"
+ ("host","user","store","key","type","value")
+ VALUES (?,?,?,?,?,?)
+ ON CONFLICT ("host", "user","store", "key")
+ DO UPDATE SET "type"=?, "value"=?;
+ ]];
local select_extradata_sql = [[
SELECT "type", "value"
FROM "prosody"
@@ -208,7 +225,10 @@ function map_store:set_keys(username, keydatas)
LIMIT 1;
]];
for key, data in pairs(keydatas) do
- if type(key) == "string" and key ~= "" then
+ if type(key) == "string" and key ~= "" and engine.params.driver ~= "MySQL" and data ~= self.remove then
+ local t, value = assert(serialize(data));
+ engine:insert(upsert_sql, host, username or "", self.store, key, t, value, t, value);
+ elseif type(key) == "string" and key ~= "" then
engine:delete(delete_sql,
host, username or "", self.store, key);
if data ~= self.remove then
@@ -291,37 +311,43 @@ function archive_store:append(username, key, value, when, with)
local user,store = username,self.store;
local cache_key = jid_join(username, host, store);
local item_count = archive_item_count_cache:get(cache_key);
- if not item_count then
- item_count_cache_miss();
- local ok, ret = engine:transaction(function()
- local count_sql = [[
- SELECT COUNT(*) FROM "prosodyarchive"
- WHERE "host"=? AND "user"=? AND "store"=?;
- ]];
- local result = engine:select(count_sql, host, user, store);
- if result then
- for row in result do
- item_count = row[1];
+
+ if archive_item_limit then
+ if not item_count then
+ item_count_cache_miss();
+ local ok, ret = engine:transaction(function()
+ local count_sql = [[
+ SELECT COUNT(*) FROM "prosodyarchive"
+ WHERE "host"=? AND "user"=? AND "store"=?;
+ ]];
+ local result = engine:select(count_sql, host, user, store);
+ if result then
+ for row in result do
+ item_count = row[1];
+ end
end
+ end);
+ if not ok or not item_count then
+ module:log("error", "Failed while checking quota for %s: %s", username, ret);
+ return nil, "Failure while checking quota";
end
- end);
- if not ok or not item_count then
- module:log("error", "Failed while checking quota for %s: %s", username, ret);
- return nil, "Failure while checking quota";
+ archive_item_count_cache:set(cache_key, item_count);
+ else
+ item_count_cache_hit();
end
- archive_item_count_cache:set(cache_key, item_count);
- else
- item_count_cache_hit();
- end
- if archive_item_limit then
module:log("debug", "%s has %d items out of %d limit", username, item_count, archive_item_limit);
if item_count >= archive_item_limit then
return nil, "quota-limit";
end
end
+ -- FIXME update the schema to allow precision timestamps
when = when or os.time();
+ if engine.params.driver ~= "SQLite3" then
+ -- SQLite3 doesn't enforce types :)
+ when = math.floor(when);
+ end
with = with or "";
local ok, ret = engine:transaction(function()
local delete_sql = [[
@@ -334,16 +360,19 @@ function archive_store:append(username, key, value, when, with)
VALUES (?,?,?,?,?,?,?,?);
]];
if key then
+ -- TODO use UPSERT like map store
local result = engine:delete(delete_sql, host, user or "", store, key);
- if result then
+ if result and item_count then
item_count = item_count - result:affected();
end
else
- key = uuid.generate();
+ key = uuid.v7();
end
local t, encoded_value = assert(serialize(value));
engine:insert(insert_sql, host, user or "", store, when, with, key, t, encoded_value);
- archive_item_count_cache:set(cache_key, item_count+1);
+ if item_count then
+ archive_item_count_cache:set(cache_key, item_count+1);
+ end
return key;
end);
if not ok then return ok, ret; end
@@ -354,12 +383,12 @@ end
local function archive_where(query, args, where)
-- Time range, inclusive
if query.start then
- args[#args+1] = query.start
+ args[#args+1] = math.floor(query.start);
where[#where+1] = "\"when\" >= ?"
end
if query["end"] then
- args[#args+1] = query["end"];
+ args[#args+1] = math.floor(query["end"]);
if query.start then
where[#where] = "\"when\" BETWEEN ? AND ?" -- is this inclusive?
else
@@ -382,8 +411,7 @@ local function archive_where(query, args, where)
-- Set of ids
if query.ids then
local nids, nargs = #query.ids, #args;
- -- COMPAT Lua 5.1: No separator argument to string.rep
- where[#where + 1] = "\"key\" IN (" .. string.rep("?,", nids):sub(1,-2) .. ")";
+ where[#where + 1] = "\"key\" IN (" .. string.rep("?", nids, ",") .. ")";
for i, id in ipairs(query.ids) do
args[nargs+i] = id;
end
@@ -611,7 +639,7 @@ function archive_store:delete(username, query)
LIMIT %s OFFSET ?
);]];
if engine.params.driver == "SQLite3" then
- if engine._have_delete_limit then
+ if engine.sqlite_compile_options.enable_update_delete_limit then
sql_query = [[
DELETE FROM "prosodyarchive"
WHERE %s
@@ -630,7 +658,13 @@ function archive_store:delete(username, query)
archive_item_count_cache:clear();
else
local cache_key = jid_join(username, host, self.store);
- archive_item_count_cache:set(cache_key, nil);
+ if query.start == nil and query.with == nil and query["end"] == nil and query.key == nil and query.ids == nil and query.truncate == nil then
+ -- All items deleted, count should be zero.
+ archive_item_count_cache:set(cache_key, 0);
+ else
+ -- Not sure how many items left
+ archive_item_count_cache:set(cache_key, nil);
+ end
end
return ok and stmt:affected(), stmt;
end
@@ -648,10 +682,27 @@ function archive_store:users()
return iterator(result);
end
+local keyvalplus = {
+ __index = {
+ -- keyval
+ get = keyval_store.get;
+ set = keyval_store.set;
+ items = keyval_store.users;
+ -- map
+ get_key = map_store.get;
+ set_key = map_store.set;
+ remove = map_store.remove;
+ set_keys = map_store.set_keys;
+ get_key_from_all = map_store.get_all;
+ delete_key_from_all = map_store.delete_all;
+ };
+}
+
local stores = {
keyval = keyval_store;
map = map_store;
archive = archive_store;
+ ["keyval+"] = keyvalplus;
};
--- Implement storage driver API
@@ -692,6 +743,7 @@ end
local function create_table(engine) -- luacheck: ignore 431/engine
+ local sql = engine.params.driver == "SQLite3" and sqlite or dbisql;
local Table, Column, Index = sql.Table, sql.Column, sql.Index;
local ProsodyTable = Table {
@@ -702,7 +754,7 @@ local function create_table(engine) -- luacheck: ignore 431/engine
Column { name="key", type="TEXT", nullable=false };
Column { name="type", type="TEXT", nullable=false };
Column { name="value", type="MEDIUMTEXT", nullable=false };
- Index { name="prosody_index", "host", "user", "store", "key" };
+ Index { name = "prosody_unique_index"; unique = engine.params.driver ~= "MySQL"; "host"; "user"; "store"; "key" };
};
engine:transaction(function()
ProsodyTable:create(engine);
@@ -732,6 +784,7 @@ end
local function upgrade_table(engine, params, apply_changes) -- luacheck: ignore 431/engine
local changes = false;
if params.driver == "MySQL" then
+ local sql = dbisql;
local success,err = engine:transaction(function()
do
local result = assert(engine:execute("SHOW COLUMNS FROM \"prosody\" WHERE \"Field\"='value' and \"Type\"='text'"));
@@ -799,12 +852,38 @@ local function upgrade_table(engine, params, apply_changes) -- luacheck: ignore
success,err = engine:transaction(function()
return engine:execute(check_encoding_query, params.database,
engine.charset, engine.charset.."_bin");
- end);
- if not success then
- module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error");
- return false;
+ end);
+ if not success then
+ module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error");
+ return false;
+ end
+ else
+ local indices = {};
+ engine:transaction(function ()
+ if params.driver == "SQLite3" then
+ for row in engine:select [[SELECT "name" FROM "sqlite_schema" WHERE "type"='index' AND "tbl_name"='prosody' AND "name"='prosody_index';]] do
+ indices[row[1]] = true;
+ end
+ elseif params.driver == "PostgreSQL" then
+ for row in engine:select [[SELECT "indexname" FROM "pg_indexes" WHERE "tablename"='prosody' AND "indexname"='prosody_index';]] do
+ indices[row[1]] = true;
+ end
+ end
+ end)
+ if indices["prosody_index"] then
+ if apply_changes then
+ local success = engine:transaction(function ()
+ return assert(engine:execute([[DROP INDEX "prosody_index";]]));
+ end);
+ if not success then
+ module:log("error", "Failed to delete obsolete index \"prosody_index\"");
+ return false;
+ end
+ else
+ changes = true;
+ end
+ end
end
- end
return changes;
end
@@ -831,12 +910,13 @@ end
function module.load()
local engines = module:shared("/*/sql/connections");
local params = normalize_params(module:get_option("sql", default_params));
+ local sql = params.driver == "SQLite3" and sqlite or dbisql;
local db_uri = sql.db2uri(params);
engine = engines[db_uri];
if not engine then
module:log("debug", "Creating new engine %s", db_uri);
engine = sql:create_engine(params, function (engine) -- luacheck: ignore 431/engine
- if module:get_option("sql_manage_tables", true) then
+ if module:get_option_boolean("sql_manage_tables", true) then
-- Automatically create table, ignore failure (table probably already exists)
-- FIXME: we should check in information_schema, etc.
create_table(engine);
@@ -847,28 +927,74 @@ function module.load()
end
end
if engine.params.driver == "SQLite3" then
+ local compile_options = {}
for row in engine:select("PRAGMA compile_options") do
- if row[1] == "ENABLE_UPDATE_DELETE_LIMIT" then
- engine._have_delete_limit = true;
+ local option = row[1]:lower();
+ local opt, val = option:match("^([^=]+)=(.*)$");
+ compile_options[opt or option] = tonumber(val) or val or true;
+ end
+ engine.sqlite_compile_options = compile_options;
+
+ local journal_mode = "delete";
+ for row in engine:select[[PRAGMA journal_mode;]] do
+ journal_mode = row[1];
+ end
+
+ -- Note: These things can't be changed with in a transaction. LuaDBI
+ -- opens a transaction automatically for every statement(?), so this
+ -- will not work there.
+ local tune = module:get_option_enum("sqlite_tune", "default", "normal", "fast", "safe");
+ if tune == "normal" then
+ if journal_mode ~= "wal" then
+ engine:execute("PRAGMA journal_mode=WAL;");
+ end
+ engine:execute("PRAGMA auto_vacuum=FULL;");
+ engine:execute("PRAGMA synchronous=NORMAL;")
+ elseif tune == "fast" then
+ if journal_mode ~= "wal" then
+ engine:execute("PRAGMA journal_mode=WAL;");
end
+ if compile_options.secure_delete then
+ engine:execute("PRAGMA secure_delete=FAST;");
+ end
+ engine:execute("PRAGMA synchronous=OFF;")
+ engine:execute("PRAGMA fullfsync=0;")
+ elseif tune == "safe" then
+ if journal_mode ~= "delete" then
+ engine:execute("PRAGMA journal_mode=DELETE;");
+ end
+ engine:execute("PRAGMA synchronous=EXTRA;")
+ engine:execute("PRAGMA fullfsync=1;")
+ end
+
+ for row in engine:select[[PRAGMA journal_mode;]] do
+ journal_mode = row[1];
end
+
+ module:log("debug", "SQLite3 database %q operating with journal_mode=%s", engine.params.database, journal_mode);
end
+ module:set_status("info", "Connected to " .. engine.params.driver);
+ end, function (engine) -- luacheck: ignore 431/engine
+ module:set_status("error", "Disconnected from " .. engine.params.driver);
end);
engines[sql.db2uri(params)] = engine;
+ else
+ module:set_status("info", "Using existing engine");
end
module:provides("storage", driver);
end
function module.command(arg)
- local config = require "core.configmanager";
- local hi = require "util.human.io";
+ local config = require "prosody.core.configmanager";
+ local hi = require "prosody.util.human.io";
local command = table.remove(arg, 1);
if command == "upgrade" then
-- We need to find every unique dburi in the config
local uris = {};
for host in pairs(prosody.hosts) do -- luacheck: ignore 431/host
local params = normalize_params(config.get(host, "sql") or default_params);
+ local sql = engine.params.driver == "SQLite3" and sqlite or dbisql;
uris[sql.db2uri(params)] = params;
end
print("We will check and upgrade the following databases:\n");
@@ -884,6 +1010,7 @@ function module.command(arg)
-- Upgrade each one
for _, params in pairs(uris) do
print("Checking "..params.database.."...");
+ local sql = params.driver == "SQLite3" and sqlite or dbisql;
engine = sql:create_engine(params);
upgrade_table(engine, params, true);
end
diff --git a/plugins/mod_storage_xep0227.lua b/plugins/mod_storage_xep0227.lua
index 5c3cf7f6..5b324885 100644
--- a/plugins/mod_storage_xep0227.lua
+++ b/plugins/mod_storage_xep0227.lua
@@ -2,22 +2,22 @@
local ipairs, pairs = ipairs, pairs;
local setmetatable = setmetatable;
local tostring = tostring;
-local next, unpack = next, table.unpack or unpack; --luacheck: ignore 113/unpack
+local next, unpack = next, table.unpack;
local os_remove = os.remove;
local io_open = io.open;
-local jid_bare = require "util.jid".bare;
-local jid_prep = require "util.jid".prep;
-local jid_join = require "util.jid".join;
-
-local array = require "util.array";
-local base64 = require "util.encodings".base64;
-local dt = require "util.datetime";
-local hex = require "util.hex";
-local it = require "util.iterators";
-local paths = require"util.paths";
-local set = require "util.set";
-local st = require "util.stanza";
-local parse_xml_real = require "util.xml".parse;
+local jid_bare = require "prosody.util.jid".bare;
+local jid_prep = require "prosody.util.jid".prep;
+local jid_join = require "prosody.util.jid".join;
+
+local array = require "prosody.util.array";
+local base64 = require "prosody.util.encodings".base64;
+local dt = require "prosody.util.datetime";
+local hex = require "prosody.util.hex";
+local it = require "prosody.util.iterators";
+local paths = require"prosody.util.paths";
+local set = require "prosody.util.set";
+local st = require "prosody.util.stanza";
+local parse_xml_real = require "prosody.util.xml".parse;
local lfs = require "lfs";
@@ -80,7 +80,7 @@ local handlers = {};
-- In order to support custom account properties
local extended = "http://prosody.im/protocol/extended-xep0227\1";
-local scram_hash_name = module:get_option_string("password_hash", "SHA-1");
+local scram_hash_name = module:get_option_enum("password_hash", "SHA-1", "SHA-256");
local scram_properties = set.new({ "server_key", "stored_key", "iteration_count", "salt" });
handlers.accounts = {
diff --git a/plugins/mod_time.lua b/plugins/mod_time.lua
index 0cd5a4ea..4d9e4f4f 100644
--- a/plugins/mod_time.lua
+++ b/plugins/mod_time.lua
@@ -6,9 +6,9 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
-local datetime = require "util.datetime".datetime;
-local legacy = require "util.datetime".legacy;
+local st = require "prosody.util.stanza";
+local datetime = require "prosody.util.datetime".datetime;
+local now = require "prosody.util.time".now;
-- XEP-0202: Entity Time
@@ -18,23 +18,10 @@ local function time_handler(event)
local origin, stanza = event.origin, event.stanza;
origin.send(st.reply(stanza):tag("time", {xmlns="urn:xmpp:time"})
:tag("tzo"):text("+00:00"):up() -- TODO get the timezone in a platform independent fashion
- :tag("utc"):text(datetime()));
+ :tag("utc"):text(datetime(now())));
return true;
end
module:hook("iq-get/bare/urn:xmpp:time:time", time_handler);
module:hook("iq-get/host/urn:xmpp:time:time", time_handler);
--- XEP-0090: Entity Time (deprecated)
-
-module:add_feature("jabber:iq:time");
-
-local function legacy_time_handler(event)
- local origin, stanza = event.origin, event.stanza;
- origin.send(st.reply(stanza):tag("query", {xmlns="jabber:iq:time"})
- :tag("utc"):text(legacy()));
- return true;
-end
-
-module:hook("iq-get/bare/jabber:iq:time:query", legacy_time_handler);
-module:hook("iq-get/host/jabber:iq:time:query", legacy_time_handler);
diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua
index afc1653a..b240a64c 100644
--- a/plugins/mod_tls.lua
+++ b/plugins/mod_tls.lua
@@ -6,14 +6,14 @@
-- COPYING file in the source package for more information.
--
-local create_context = require "core.certmanager".create_context;
-local rawgetopt = require"core.configmanager".rawget;
-local st = require "util.stanza";
+local create_context = require "prosody.core.certmanager".create_context;
+local rawgetopt = require"prosody.core.configmanager".rawget;
+local st = require "prosody.util.stanza";
-local c2s_require_encryption = module:get_option("c2s_require_encryption", module:get_option("require_encryption", true));
-local s2s_require_encryption = module:get_option("s2s_require_encryption", true);
-local allow_s2s_tls = module:get_option("s2s_allow_encryption") ~= false;
-local s2s_secure_auth = module:get_option("s2s_secure_auth");
+local c2s_require_encryption = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", true));
+local s2s_require_encryption = module:get_option_boolean("s2s_require_encryption", true);
+local allow_s2s_tls = module:get_option_boolean("s2s_allow_encryption", true);
+local s2s_secure_auth = module:get_option_boolean("s2s_secure_auth", false);
if s2s_secure_auth and s2s_require_encryption == false then
module:log("warn", "s2s_secure_auth implies s2s_require_encryption, but s2s_require_encryption is set to false");
@@ -62,7 +62,7 @@ function module.load(reload)
module:log("debug", "Creating context for s2sout");
-- for outgoing server connections
- ssl_ctx_s2sout, err_s2sout, ssl_cfg_s2sout = create_context(host.host, "client", host_s2s, host_ssl, global_s2s, request_client_certs, xmpp_alpn);
+ ssl_ctx_s2sout, err_s2sout, ssl_cfg_s2sout = create_context(host.host, "client", host_s2s, host_ssl, global_s2s, xmpp_alpn);
if not ssl_ctx_s2sout then module:log("error", "Error creating contexts for s2sout: %s", err_s2sout); end
module:log("debug", "Creating context for s2sin");
@@ -80,6 +80,9 @@ end
module:hook_global("config-reloaded", module.load);
local function can_do_tls(session)
+ if session.secure then
+ return false;
+ end
if session.conn and not session.conn.starttls then
if not session.secure then
session.log("debug", "Underlying connection does not support STARTTLS");
@@ -125,7 +128,15 @@ end);
-- Hook <starttls/>
module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event)
local origin = event.origin;
+ origin.starttls = "requested";
if can_do_tls(origin) then
+ if origin.conn.block_reads then
+ -- we need to ensure that no data is read anymore, otherwise we could end up in a situation where
+ -- <proceed/> is sent and the socket receives the TLS handshake (and passes the data to lua) before
+ -- it is asked to initiate TLS
+ -- (not with the classical single-threaded server backends)
+ origin.conn:block_reads()
+ end
(origin.sends2s or origin.send)(starttls_proceed);
if origin.destroyed then return end
origin:reset_stream();
@@ -166,6 +177,7 @@ module:hook_tag("http://etherx.jabber.org/streams", "features", function (sessio
module:log("debug", "%s is not offering TLS", session.to_host);
return;
end
+ session.starttls = "initiated";
session.sends2s(starttls_initiate);
return true;
end
@@ -183,7 +195,8 @@ module:hook_tag(xmlns_starttls, "proceed", function (session, stanza) -- luachec
if session.type == "s2sout_unauthed" and can_do_tls(session) then
module:log("debug", "Proceeding with TLS on s2sout...");
session:reset_stream();
- session.conn:starttls(session.ssl_ctx);
+ session.starttls = "proceeding"
+ session.conn:starttls(session.ssl_ctx, session.to_host);
session.secure = false;
return true;
end
diff --git a/plugins/mod_tokenauth.lua b/plugins/mod_tokenauth.lua
index c04a1aa4..95b0f8d6 100644
--- a/plugins/mod_tokenauth.lua
+++ b/plugins/mod_tokenauth.lua
@@ -1,82 +1,354 @@
-local id = require "util.id";
-local jid = require "util.jid";
-local base64 = require "util.encodings".base64;
+local base64 = require "prosody.util.encodings".base64;
+local hashes = require "prosody.util.hashes";
+local id = require "prosody.util.id";
+local jid = require "prosody.util.jid";
+local random = require "prosody.util.random";
+local usermanager = require "prosody.core.usermanager";
+local generate_identifier = require "prosody.util.id".short;
-local token_store = module:open_store("auth_tokens", "map");
+local token_store = module:open_store("auth_tokens", "keyval+");
-function create_jid_token(actor_jid, token_jid, token_scope, token_ttl)
- token_jid = jid.prep(token_jid);
- if not actor_jid or token_jid ~= actor_jid and not jid.compare(token_jid, actor_jid) then
+local access_time_granularity = module:get_option_period("token_auth_access_time_granularity", 60);
+local empty_grant_lifetime = module:get_option_period("tokenless_grant_ttl", "2w");
+
+local function select_role(username, host, role_name)
+ if not role_name then return end
+ local role = usermanager.get_role_by_name(role_name, host);
+ if not role then return end
+ if not usermanager.user_can_assume_role(username, host, role.name) then return end
+ return role;
+end
+
+function create_grant(actor_jid, grant_jid, grant_ttl, grant_data)
+ grant_jid = jid.prep(grant_jid);
+ if not actor_jid or actor_jid ~= grant_jid and not jid.compare(grant_jid, actor_jid) then
+ module:log("debug", "Actor <%s> is not permitted to create a token granting access to JID <%s>", actor_jid, grant_jid);
return nil, "not-authorized";
end
- local token_username, token_host, token_resource = jid.split(token_jid);
+ local grant_username, grant_host, grant_resource = jid.split(grant_jid);
- if token_host ~= module.host then
+ if grant_host ~= module.host then
return nil, "invalid-host";
end
- local token_info = {
+ local grant_id = id.short();
+ local now = os.time();
+
+ local grant = {
+ id = grant_id;
+
owner = actor_jid;
- created = os.time();
- expires = token_ttl and (os.time() + token_ttl) or nil;
- jid = token_jid;
- session = {
- username = token_username;
- host = token_host;
- resource = token_resource;
-
- auth_scope = token_scope;
- };
+ created = now;
+ expires = grant_ttl and (now + grant_ttl) or nil;
+ accessed = now;
+
+ jid = grant_jid;
+ resource = grant_resource;
+
+ data = grant_data;
+
+ -- tokens[<hash-name>..":"..<secret>] = token_info
+ tokens = {};
+ };
+
+ local ok, err = token_store:set_key(grant_username, grant_id, grant);
+ if not ok then
+ return nil, err;
+ end
+
+ module:fire_event("token-grant-created", {
+ id = grant_id;
+ grant = grant;
+ username = grant_username;
+ host = grant_host;
+ });
+
+ return grant;
+end
+
+function create_token(grant_jid, grant, token_role, token_ttl, token_purpose, token_data)
+ if (token_data and type(token_data) ~= "table") or (token_purpose and type(token_purpose) ~= "string") then
+ return nil, "bad-request";
+ end
+ local grant_username, grant_host = jid.split(grant_jid);
+ if grant_host ~= module.host then
+ return nil, "invalid-host";
+ end
+ if type(grant) == "string" then -- lookup by id
+ grant = token_store:get_key(grant_username, grant);
+ if not grant then return nil; end
+ end
+
+ if not grant.tokens then return nil, "internal-server-error"; end -- old-style token?
+
+ local now = os.time();
+ local expires = grant.expires; -- Default to same expiry as grant
+ if token_ttl then -- explicit lifetime requested
+ if expires then
+ -- Grant has an expiry, so limit to that or shorter
+ expires = math.min(now + token_ttl, expires);
+ else
+ -- Grant never expires, just use whatever expiry is requested for the token
+ expires = now + token_ttl;
+ end
+ end
+
+ local token_info = {
+ role = token_role;
+
+ created = now;
+ expires = expires;
+ purpose = token_purpose;
+
+ data = token_data;
};
- local token_id = id.long();
- local token = base64.encode("1;"..jid.join(token_username, token_host)..";"..token_id);
- token_store:set(token_username, token_id, token_info);
+ local token_secret = random.bytes(18);
+ grant.tokens["sha256:"..hashes.sha256(token_secret, true)] = token_info;
+
+ local ok, err = token_store:set_key(grant_username, grant.id, grant);
+ if not ok then
+ return nil, err;
+ end
- return token, token_info;
+ local token_string = "secret-token:"..base64.encode("2;"..grant.id..";"..token_secret..";"..grant.jid);
+ return token_string, token_info;
end
local function parse_token(encoded_token)
- local token = base64.decode(encoded_token);
+ if not encoded_token then return nil; end
+ local encoded_data = encoded_token:match("^secret%-token:(.+)$");
+ if not encoded_data then return nil; end
+ local token = base64.decode(encoded_data);
if not token then return nil; end
- local token_jid, token_id = token:match("^1;([^;]+);(.+)$");
- if not token_jid then return nil; end
+ local token_id, token_secret, token_jid = token:match("^2;([^;]+);(..................);(.+)$");
+ if not token_id then return nil; end
local token_user, token_host = jid.split(token_jid);
- return token_id, token_user, token_host;
+ return token_id, token_user, token_host, token_secret;
end
-function get_token_info(token)
- local token_id, token_user, token_host = parse_token(token);
- if not token_id then
- return nil, "invalid-token-format";
+local function clear_expired_grant_tokens(grant, now)
+ local updated;
+ now = now or os.time();
+ for secret, token_info in pairs(grant.tokens) do
+ local expires = token_info.expires;
+ if expires and expires < now then
+ grant.tokens[secret] = nil;
+ updated = true;
+ end
+ end
+ return updated;
+end
+
+local function _get_validated_grant_info(username, grant)
+ if type(grant) == "string" then
+ grant = token_store:get_key(username, grant);
+ end
+ if not grant or not grant.created or not grant.id then return nil; end
+
+ -- Invalidate grants from before last password change
+ local account_info = usermanager.get_account_info(username, module.host);
+ local password_updated_at = account_info and account_info.password_updated;
+ local now = os.time();
+ if password_updated_at and grant.created < password_updated_at then
+ module:log("debug", "Token grant %s of %s issued before last password change, invalidating it now", grant.id, username);
+ token_store:set_key(username, grant.id, nil);
+ return nil, "not-authorized";
+ elseif grant.expires and grant.expires < now then
+ module:log("debug", "Token grant %s of %s expired, cleaning up", grant.id, username);
+ token_store:set_key(username, grant.id, nil);
+ return nil, "expired";
+ end
+
+ if not grant.tokens then
+ module:log("debug", "Token grant %s of %s without tokens, cleaning up", grant.id, username);
+ token_store:set_key(username, grant.id, nil);
+ return nil, "invalid";
+ end
+
+ local found_expired = false
+ for secret_hash, token_info in pairs(grant.tokens) do
+ if token_info.expires and token_info.expires < now then
+ module:log("debug", "Token %s of grant %s of %s has expired, cleaning it up", secret_hash:sub(-8), grant.id, username);
+ grant.tokens[secret_hash] = nil;
+ found_expired = true;
+ end
+ end
+
+ if not grant.expires and next(grant.tokens) == nil and grant.accessed + empty_grant_lifetime < now then
+ module:log("debug", "Token %s of %s grant has no tokens, discarding", grant.id, username);
+ token_store:set_key(username, grant.id, nil);
+ return nil, "expired";
+ elseif found_expired then
+ token_store:set_key(username, grant.id, grant);
end
+
+ return grant;
+end
+
+local function _get_validated_token_info(token_id, token_user, token_host, token_secret)
if token_host ~= module.host then
return nil, "invalid-host";
end
- local token_info, err = token_store:get(token_user, token_id);
- if not token_info then
+ local grant, err = token_store:get_key(token_user, token_id);
+ if not grant or not grant.tokens then
if err then
+ module:log("error", "Unable to read from token storage: %s", err);
return nil, "internal-error";
end
+ module:log("warn", "Invalid token in storage (%s / %s)", token_user, token_id);
+ return nil, "not-authorized";
+ end
+
+ -- Check provided secret
+ local secret_hash = "sha256:"..hashes.sha256(token_secret, true);
+ local token_info = grant.tokens[secret_hash];
+ if not token_info then
+ module:log("debug", "No tokens matched the given secret");
+ return nil, "not-authorized";
+ end
+
+ -- Check expiry
+ local now = os.time();
+ if token_info.expires and token_info.expires < now then
+ module:log("debug", "Token has expired, cleaning it up");
+ grant.tokens[secret_hash] = nil;
+ token_store:set_key(token_user, token_id, grant);
return nil, "not-authorized";
end
- if token_info.expires and token_info.expires < os.time() then
+ -- Verify grant validity (expiry, etc.)
+ grant = _get_validated_grant_info(token_user, grant);
+ if not grant then
return nil, "not-authorized";
end
- return token_info
+ -- Update last access time if necessary
+ local last_accessed = grant.accessed;
+ if not last_accessed or (now - last_accessed) > access_time_granularity then
+ grant.accessed = now;
+ clear_expired_grant_tokens(grant); -- Clear expired tokens while we're here
+ token_store:set_key(token_user, token_id, grant);
+ end
+
+ token_info.id = token_id;
+ token_info.grant = grant;
+ token_info.jid = grant.jid;
+
+ return token_info;
end
-function revoke_token(token)
- local token_id, token_user, token_host = parse_token(token);
+function get_grant_info(username, grant_id)
+ local grant = _get_validated_grant_info(username, grant_id);
+ if not grant then return nil; end
+
+ -- Caller is only interested in the grant, no need to expose token stuff to them
+ grant.tokens = nil;
+
+ return grant;
+end
+
+function get_user_grants(username)
+ local grants = token_store:get(username);
+ if not grants then return nil; end
+ for grant_id, grant in pairs(grants) do
+ grants[grant_id] = _get_validated_grant_info(username, grant);
+ end
+ return grants;
+end
+
+function get_token_info(token)
+ local token_id, token_user, token_host, token_secret = parse_token(token);
+ if not token_id then
+ module:log("warn", "Failed to verify access token: %s", token_user);
+ return nil, "invalid-token-format";
+ end
+ return _get_validated_token_info(token_id, token_user, token_host, token_secret);
+end
+
+function get_token_session(token, resource)
+ local token_id, token_user, token_host, token_secret = parse_token(token);
if not token_id then
+ module:log("warn", "Failed to verify access token: %s", token_user);
+ return nil, "invalid-token-format";
+ end
+
+ local token_info, err = _get_validated_token_info(token_id, token_user, token_host, token_secret);
+ if not token_info then return nil, err; end
+
+ local role = select_role(token_user, token_host, token_info.role);
+ if not role then return nil, "not-authorized"; end
+ return {
+ username = token_user;
+ host = token_host;
+ resource = token_info.resource or resource or generate_identifier();
+
+ role = role;
+ };
+end
+
+function revoke_token(token)
+ local grant_id, token_user, token_host, token_secret = parse_token(token);
+ if not grant_id then
+ module:log("warn", "Failed to verify access token: %s", token_user);
return nil, "invalid-token-format";
end
if token_host ~= module.host then
return nil, "invalid-host";
end
- return token_store:set(token_user, token_id, nil);
+ local grant, err = _get_validated_grant_info(token_user, grant_id);
+ if not grant then return grant, err; end
+ local secret_hash = "sha256:"..hashes.sha256(token_secret, true);
+ local token_info = grant.tokens[secret_hash];
+ if not grant or not token_info then
+ return nil, "item-not-found";
+ end
+ grant.tokens[secret_hash] = nil;
+ local ok, err = token_store:set_key(token_user, grant_id, grant);
+ if not ok then
+ return nil, err;
+ end
+ module:fire_event("token-revoked", {
+ grant_id = grant_id;
+ grant = grant;
+ info = token_info;
+ username = token_user;
+ host = token_host;
+ });
+ return true;
+end
+
+function revoke_grant(username, grant_id)
+ local ok, err = token_store:set_key(username, grant_id, nil);
+ if not ok then return nil, err; end
+ module:fire_event("token-grant-revoked", { id = grant_id, username = username, host = module.host });
+ return true;
+end
+
+function sasl_handler(auth_provider, purpose, extra)
+ return function (sasl, token, realm, _authzid)
+ local token_info, err = get_token_info(token);
+ if not token_info then
+ module:log("debug", "SASL handler failed to verify token: %s", err);
+ return nil, nil, extra;
+ end
+ local token_user, token_host, resource = jid.split(token_info.grant.jid);
+ if realm ~= token_host or (purpose and token_info.purpose ~= purpose) then
+ return nil, nil, extra;
+ end
+ if auth_provider.is_enabled and not auth_provider.is_enabled(token_user) then
+ return true, false, token_info;
+ end
+ sasl.resource = resource;
+ sasl.token_info = token_info;
+ return token_user, true, token_info;
+ end;
end
+
+module:daily("clear expired grants", function()
+ for username in token_store:items() do
+ get_user_grants(username); -- clears out expired grants
+ end
+end)
diff --git a/plugins/mod_tombstones.lua b/plugins/mod_tombstones.lua
index b5a04c9f..e0f1a827 100644
--- a/plugins/mod_tombstones.lua
+++ b/plugins/mod_tombstones.lua
@@ -1,16 +1,16 @@
-- TODO warn when trying to create an user before the tombstone expires
-- e.g. via telnet or other admin interface
-local datetime = require "util.datetime";
-local errors = require "util.error";
-local jid_node = require"util.jid".node;
-local st = require "util.stanza";
+local datetime = require "prosody.util.datetime";
+local errors = require "prosody.util.error";
+local jid_node = require"prosody.util.jid".node;
+local st = require "prosody.util.stanza";
-- Using a map store as key-value store so that removal of all user data
-- does not also remove the tombstone, which would defeat the point
local graveyard = module:open_store(nil, "map");
-local graveyard_cache = require "util.cache".new(module:get_option_number("tombstone_cache_size", 1024));
+local graveyard_cache = require "prosody.util.cache".new(module:get_option_integer("tombstone_cache_size", 1024, 1));
-local ttl = module:get_option_number("user_tombstone_expiry", nil);
+local ttl = module:get_option_period("user_tombstone_expiry", nil);
-- Keep tombstones forever by default
--
-- Rationale:
diff --git a/plugins/mod_turn_external.lua b/plugins/mod_turn_external.lua
index ee50740c..6cdd8c99 100644
--- a/plugins/mod_turn_external.lua
+++ b/plugins/mod_turn_external.lua
@@ -1,12 +1,12 @@
-local set = require "util.set";
+local set = require "prosody.util.set";
local secret = module:get_option_string("turn_external_secret");
local host = module:get_option_string("turn_external_host", module.host);
local user = module:get_option_string("turn_external_user");
-local port = module:get_option_number("turn_external_port", 3478);
-local ttl = module:get_option_number("turn_external_ttl", 86400);
+local port = module:get_option_integer("turn_external_port", 3478, 1, 65535);
+local ttl = module:get_option_period("turn_external_ttl", "1 day");
local tcp = module:get_option_boolean("turn_external_tcp", false);
-local tls_port = module:get_option_number("turn_external_tls_port");
+local tls_port = module:get_option_integer("turn_external_tls_port", nil, 1, 65535);
if not secret then
module:log_status("error", "Failed to initialize: the 'turn_external_secret' option is not set in your configuration");
diff --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua
index 8a01fb17..9fbf7612 100644
--- a/plugins/mod_uptime.lua
+++ b/plugins/mod_uptime.lua
@@ -6,7 +6,7 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
local start_time = prosody.start_time;
module:hook_global("server-started", function() start_time = prosody.start_time end);
diff --git a/plugins/mod_user_account_management.lua b/plugins/mod_user_account_management.lua
index 130ed089..c2a0e3a2 100644
--- a/plugins/mod_user_account_management.lua
+++ b/plugins/mod_user_account_management.lua
@@ -7,16 +7,28 @@
--
-local st = require "util.stanza";
-local usermanager_set_password = require "core.usermanager".set_password;
-local usermanager_delete_user = require "core.usermanager".delete_user;
-local nodeprep = require "util.encodings".stringprep.nodeprep;
-local jid_bare = require "util.jid".bare;
+local st = require "prosody.util.stanza";
+local usermanager = require "prosody.core.usermanager";
+local nodeprep = require "prosody.util.encodings".stringprep.nodeprep;
+local jid_bare, jid_node = import("prosody.util.jid", "bare", "node");
local compat = module:get_option_boolean("registration_compat", true);
+local soft_delete_period = module:get_option_period("registration_delete_grace_period");
+local deleted_accounts = module:open_store("accounts_cleanup");
module:add_feature("jabber:iq:register");
+-- Allow us to 'freeze' a session and retrieve properties even after it is
+-- destroyed
+local function capture_session_properties(session)
+ return setmetatable({
+ id = session.id;
+ ip = session.ip;
+ type = session.type;
+ client_id = session.client_id;
+ }, { __index = session });
+end
+
-- Password change and account deletion handler
local function handle_registration_stanza(event)
local session, stanza = event.origin, event.stanza;
@@ -34,6 +46,12 @@ local function handle_registration_stanza(event)
if query.tags[1] and query.tags[1].name == "remove" then
local username, host = session.username, session.host;
+ if host ~= module.host then -- Sanity check for safety
+ module:log("error", "Host mismatch on deletion request (a bug): %s ~= %s", host, module.host);
+ session.send(st.error_reply(stanza, "cancel", "internal-server-error"));
+ return true;
+ end
+
-- This one weird trick sends a reply to this stanza before the user is deleted
local old_session_close = session.close;
session.close = function(self, ...)
@@ -41,24 +59,57 @@ local function handle_registration_stanza(event)
return old_session_close(self, ...);
end
- local ok, err = usermanager_delete_user(username, host);
+ local old_session = capture_session_properties(session);
- if not ok then
- log("debug", "Removing user account %s@%s failed: %s", username, host, err);
- session.close = old_session_close;
- session.send(st.error_reply(stanza, "cancel", "service-unavailable", err));
- return true;
- end
+ if not soft_delete_period then
+ local ok, err = usermanager.delete_user(username, host);
+
+ if not ok then
+ log("debug", "Removing user account %s@%s failed: %s", username, host, err);
+ session.close = old_session_close;
+ session.send(st.error_reply(stanza, "cancel", "service-unavailable", err));
+ return true;
+ end
+
+ log("info", "User removed their account: %s@%s (deleted)", username, host);
+ module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = old_session });
+ else
+ local ok, err = usermanager.disable_user(username, host, {
+ reason = "ibr";
+ comment = "Deletion requested by user";
+ when = os.time();
+ });
- log("info", "User removed their account: %s@%s", username, host);
- module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session });
+ if not ok then
+ log("debug", "Removing (disabling) user account %s@%s failed: %s", username, host, err);
+ session.close = old_session_close;
+ session.send(st.error_reply(stanza, "cancel", "service-unavailable", err));
+ return true;
+ end
+
+ local status = {
+ deleted_at = os.time();
+ pending_until = os.time() + soft_delete_period;
+ client_id = session.client_id;
+ };
+ deleted_accounts:set(username, status);
+
+ log("info", "User removed their account: %s@%s (disabled, pending deletion)", username, host);
+ module:fire_event("user-deregistered-pending", {
+ username = username;
+ host = host;
+ source = "mod_register";
+ session = old_session;
+ status = status;
+ });
+ end
else
local username = query:get_child_text("username");
local password = query:get_child_text("password");
if username and password then
username = nodeprep(username);
if username == session.username then
- if usermanager_set_password(username, password, session.host, session.resource) then
+ if usermanager.set_password(username, password, session.host, session.resource) then
session.send(st.reply(stanza));
else
-- TODO unable to write file, file may be locked, etc, what's the correct error?
@@ -85,3 +136,103 @@ if compat then
end);
end
+-- This improves UX of soft-deleted accounts by informing the user that the
+-- account has been deleted, rather than just disabled. They can e.g. contact
+-- their admin if this was a mistake.
+module:hook("authentication-failure", function (event)
+ if event.condition ~= "account-disabled" then return; end
+ local session = event.session;
+ local sasl_handler = session and session.sasl_handler;
+ if sasl_handler.username then
+ local status = deleted_accounts:get(sasl_handler.username);
+ if status then
+ event.text = "Account deleted";
+ end
+ end
+end, -1000);
+
+function restore_account(username)
+ local pending, pending_err = deleted_accounts:get(username);
+ if not pending then
+ return nil, pending_err or "Account not pending deletion";
+ end
+ local account_info, err = usermanager.get_account_info(username, module.host);
+ if not account_info then
+ return nil, "Couldn't fetch account info: "..err;
+ end
+ local forget_ok, forget_err = deleted_accounts:set(username, nil);
+ if not forget_ok then
+ return nil, "Couldn't remove account from deletion queue: "..forget_err;
+ end
+ local enable_ok, enable_err = usermanager.enable_user(username, module.host);
+ if not enable_ok then
+ return nil, "Removed account from deletion queue, but couldn't enable it: "..enable_err;
+ end
+ return true, "Account restored";
+end
+
+-- Automatically clear pending deletion if an account is re-enabled
+module:context("*"):hook("user-enabled", function (event)
+ if event.host ~= module.host then return; end
+ deleted_accounts:set(event.username, nil);
+end);
+
+local cleanup_time = module:measure("cleanup", "times");
+
+function cleanup_soft_deleted_accounts()
+ local cleanup_done = cleanup_time();
+ local success, fail, restored, pending = 0, 0, 0, 0;
+
+ for username in deleted_accounts:users() do
+ module:log("debug", "Processing account cleanup for '%s'", username);
+ local account_info, account_info_err = usermanager.get_account_info(username, module.host);
+ if not account_info then
+ module:log("warn", "Unable to process delayed deletion of user '%s': %s", username, account_info_err);
+ fail = fail + 1;
+ else
+ if account_info.enabled == false then
+ local meta = deleted_accounts:get(username);
+ if meta.pending_until <= os.time() then
+ local ok, err = usermanager.delete_user(username, module.host);
+ if not ok then
+ module:log("warn", "Unable to process delayed deletion of user '%s': %s", username, err);
+ fail = fail + 1;
+ else
+ success = success + 1;
+ deleted_accounts:set(username, nil);
+ module:log("debug", "Deleted account '%s' successfully", username);
+ module:fire_event("user-deregistered", { username = username, host = module.host, source = "mod_register" });
+ end
+ else
+ pending = pending + 1;
+ end
+ else
+ module:log("warn", "Account '%s' is not disabled, removing from deletion queue", username);
+ restored = restored + 1;
+ end
+ end
+ end
+
+ module:log("debug", "%d accounts scheduled for future deletion", pending);
+
+ if success > 0 or fail > 0 then
+ module:log("info", "Completed account cleanup - %d accounts deleted (%d failed, %d restored, %d pending)", success, fail, restored, pending);
+ end
+ cleanup_done();
+end
+
+module:daily("Remove deleted accounts", cleanup_soft_deleted_accounts);
+
+--- shell command
+module:add_item("shell-command", {
+ section = "user";
+ name = "restore";
+ desc = "Restore a user account scheduled for deletion";
+ args = {
+ { name = "jid", type = "string" };
+ };
+ host_selector = "jid";
+ handler = function (self, jid) --luacheck: ignore 212/self
+ return restore_account(jid_node(jid));
+ end;
+});
diff --git a/plugins/mod_vcard.lua b/plugins/mod_vcard.lua
index c3d6fb8b..ea6839bb 100644
--- a/plugins/mod_vcard.lua
+++ b/plugins/mod_vcard.lua
@@ -6,8 +6,8 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza"
-local jid_split = require "util.jid".split;
+local st = require "prosody.util.stanza"
+local jid_split = require "prosody.util.jid".split;
local vcards = module:open_store();
diff --git a/plugins/mod_vcard4.lua b/plugins/mod_vcard4.lua
index 04dbca9e..1917f609 100644
--- a/plugins/mod_vcard4.lua
+++ b/plugins/mod_vcard4.lua
@@ -1,5 +1,5 @@
-local st = require "util.stanza"
-local jid_split = require "util.jid".split;
+local st = require "prosody.util.stanza"
+local jid_split = require "prosody.util.jid".split;
local mod_pep = module:depends("pep");
diff --git a/plugins/mod_vcard_legacy.lua b/plugins/mod_vcard_legacy.lua
index 107f20da..eb392309 100644
--- a/plugins/mod_vcard_legacy.lua
+++ b/plugins/mod_vcard_legacy.lua
@@ -1,10 +1,10 @@
-local st = require "util.stanza";
-local jid_split = require "util.jid".split;
+local st = require "prosody.util.stanza";
+local jid_split = require "prosody.util.jid".split;
local mod_pep = module:depends("pep");
-local sha1 = require "util.hashes".sha1;
-local base64_decode = require "util.encodings".base64.decode;
+local sha1 = require "prosody.util.hashes".sha1;
+local base64_decode = require "prosody.util.encodings".base64.decode;
local vcards = module:open_store("vcard");
diff --git a/plugins/mod_version.lua b/plugins/mod_version.lua
index 1d24001c..72b13387 100644
--- a/plugins/mod_version.lua
+++ b/plugins/mod_version.lua
@@ -6,7 +6,7 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
module:add_feature("jabber:iq:version");
@@ -20,9 +20,14 @@ if not module:get_option_boolean("hide_os_type") then
platform = "Windows";
else
local os_version_command = module:get_option_string("os_version_command");
- local ok, pposix = pcall(require, "util.pposix");
+ 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/mod_watchregistrations.lua b/plugins/mod_watchregistrations.lua
index 825b8a73..d433d732 100644
--- a/plugins/mod_watchregistrations.lua
+++ b/plugins/mod_watchregistrations.lua
@@ -8,14 +8,14 @@
local host = module:get_host();
-local jid_prep = require "util.jid".prep;
+local jid_prep = require "prosody.util.jid".prep;
local registration_watchers = module:get_option_set("registration_watchers", module:get_option("admins", {})) / jid_prep;
local registration_from = module:get_option_string("registration_from", host);
local registration_notification = module:get_option_string("registration_notification", "User $username just registered on $host from $ip");
-local msg_type = module:get_option_string("registration_notification_type", "chat");
+local msg_type = module:get_option_enum("registration_notification_type", "chat", "normal", "headline");
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
module:hook("user-registered", function (user)
module:log("debug", "Notifying of new registration");
diff --git a/plugins/mod_websocket.lua b/plugins/mod_websocket.lua
index f0caa968..7120f3cc 100644
--- a/plugins/mod_websocket.lua
+++ b/plugins/mod_websocket.lua
@@ -8,19 +8,19 @@
module:set_global();
-local add_task = require "util.timer".add_task;
-local add_filter = require "util.filters".add_filter;
-local sha1 = require "util.hashes".sha1;
-local base64 = require "util.encodings".base64.encode;
-local st = require "util.stanza";
-local parse_xml = require "util.xml".parse;
-local contains_token = require "util.http".contains_token;
-local portmanager = require "core.portmanager";
-local sm_destroy_session = require"core.sessionmanager".destroy_session;
+local add_task = require "prosody.util.timer".add_task;
+local add_filter = require "prosody.util.filters".add_filter;
+local sha1 = require "prosody.util.hashes".sha1;
+local base64 = require "prosody.util.encodings".base64.encode;
+local st = require "prosody.util.stanza";
+local parse_xml = require "prosody.util.xml".parse;
+local contains_token = require "prosody.util.http".contains_token;
+local portmanager = require "prosody.core.portmanager";
+local sm_destroy_session = require"prosody.core.sessionmanager".destroy_session;
local log = module._log;
-local dbuffer = require "util.dbuffer";
+local dbuffer = require "prosody.util.dbuffer";
-local websocket_frames = require"net.websocket.frames";
+local websocket_frames = require"prosody.net.websocket.frames";
local parse_frame = websocket_frames.parse;
local build_frame = websocket_frames.build;
local build_close = websocket_frames.build_close;
@@ -28,10 +28,10 @@ local parse_close = websocket_frames.parse_close;
local t_concat = table.concat;
-local stanza_size_limit = module:get_option_number("c2s_stanza_size_limit", 1024 * 256);
-local frame_buffer_limit = module:get_option_number("websocket_frame_buffer_limit", 2 * stanza_size_limit);
-local frame_fragment_limit = module:get_option_number("websocket_frame_fragment_limit", 8);
-local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5);
+local stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024 * 256, 10000);
+local frame_buffer_limit = module:get_option_integer("websocket_frame_buffer_limit", 2 * stanza_size_limit, 0);
+local frame_fragment_limit = module:get_option_integer("websocket_frame_fragment_limit", 8, 0);
+local stream_close_timeout = module:get_option_period("c2s_close_timeout", 5);
local consider_websocket_secure = module:get_option_boolean("consider_websocket_secure");
local cross_domain = module:get_option("cross_domain_websocket");
if cross_domain ~= nil then
@@ -370,6 +370,6 @@ function module.add_host(module)
module:hook("c2s-read-timeout", keepalive, -0.9);
end
-if require"core.modulemanager".get_modules_for_host("*"):contains(module.name) then
+if require"prosody.core.modulemanager".get_modules_for_host("*"):contains(module.name) then
module:add_host();
end
diff --git a/plugins/mod_welcome.lua b/plugins/mod_welcome.lua
index f6b13df5..0dd0c069 100644
--- a/plugins/mod_welcome.lua
+++ b/plugins/mod_welcome.lua
@@ -9,7 +9,7 @@
local host = module:get_host();
local welcome_text = module:get_option_string("welcome_message", "Hello $username, welcome to the $host IM server!");
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
module:hook("user-registered",
function (user)
diff --git a/plugins/muc/hats.lib.lua b/plugins/muc/hats.lib.lua
index 358e5100..7eb71eb4 100644
--- a/plugins/muc/hats.lib.lua
+++ b/plugins/muc/hats.lib.lua
@@ -1,7 +1,10 @@
-local st = require "util.stanza";
+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 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();
+ 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/hidden.lib.lua b/plugins/muc/hidden.lib.lua
index 153df21a..d24fa47e 100644
--- a/plugins/muc/hidden.lib.lua
+++ b/plugins/muc/hidden.lib.lua
@@ -8,7 +8,7 @@
--
local restrict_public = not module:get_option_boolean("muc_room_allow_public", true);
-local um_is_admin = require "core.usermanager".is_admin;
+module:default_permission(restrict_public and "prosody:admin" or "prosody:registered", ":create-public-room");
local function get_hidden(room)
return room._data.hidden;
@@ -22,8 +22,8 @@ local function set_hidden(room, hidden)
end
module:hook("muc-config-form", function(event)
- if restrict_public and not um_is_admin(event.actor, module.host) then
- -- Don't show option if public rooms are restricted and user is not admin of this host
+ if not module:may(":create-public-room", event.actor) then
+ -- Hide config option if this user is not allowed to create public rooms
return;
end
table.insert(event.form, {
@@ -36,7 +36,7 @@ module:hook("muc-config-form", function(event)
end, 100-9);
module:hook("muc-config-submitted/muc#roomconfig_publicroom", function(event)
- if restrict_public and not um_is_admin(event.actor, module.host) then
+ if not module:may(":create-public-room", event.actor) then
return; -- Not allowed
end
if set_hidden(event.room, not event.value) then
diff --git a/plugins/muc/history.lib.lua b/plugins/muc/history.lib.lua
index 075b1890..005bd1d8 100644
--- a/plugins/muc/history.lib.lua
+++ b/plugins/muc/history.lib.lua
@@ -8,11 +8,11 @@
--
local gettime = os.time;
-local datetime = require "util.datetime";
-local st = require "util.stanza";
+local datetime = require "prosody.util.datetime";
+local st = require "prosody.util.stanza";
local default_history_length = 20;
-local max_history_length = module:get_option_number("max_history_messages", math.huge);
+local max_history_length = module:get_option_integer("max_history_messages", math.huge, 0);
local function set_max_history_length(_max_history_length)
max_history_length = _max_history_length or math.huge;
diff --git a/plugins/muc/lock.lib.lua b/plugins/muc/lock.lib.lua
index 32f2647b..bb5bf82b 100644
--- a/plugins/muc/lock.lib.lua
+++ b/plugins/muc/lock.lib.lua
@@ -7,10 +7,10 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
local lock_rooms = module:get_option_boolean("muc_room_locking", true);
-local lock_room_timeout = module:get_option_number("muc_room_lock_timeout", 300);
+local lock_room_timeout = module:get_option_period("muc_room_lock_timeout", "5 minutes");
local function lock(room)
module:fire_event("muc-room-locked", {room = room;});
diff --git a/plugins/muc/members_only.lib.lua b/plugins/muc/members_only.lib.lua
index b10dc120..4f4e88fa 100644
--- a/plugins/muc/members_only.lib.lua
+++ b/plugins/muc/members_only.lib.lua
@@ -7,7 +7,7 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
local muc_util = module:require "muc/util";
local valid_affiliations = muc_util.valid_affiliations;
diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua
index 5873b1a2..1dc99f07 100644
--- a/plugins/muc/mod_muc.lua
+++ b/plugins/muc/mod_muc.lua
@@ -86,21 +86,26 @@ room_mt.get_registered_nick = register.get_registered_nick;
room_mt.get_registered_jid = register.get_registered_jid;
room_mt.handle_register_iq = register.handle_register_iq;
+local restrict_pm = module:require "muc/restrict_pm";
+room_mt.get_allow_pm = restrict_pm.get_allow_pm;
+room_mt.set_allow_pm = restrict_pm.set_allow_pm;
+room_mt.get_allow_modpm = restrict_pm.get_allow_modpm;
+room_mt.set_allow_modpm = restrict_pm.set_allow_modpm;
+
local presence_broadcast = module:require "muc/presence_broadcast";
room_mt.get_presence_broadcast = presence_broadcast.get;
room_mt.set_presence_broadcast = presence_broadcast.set;
-room_mt.get_valid_broadcast_roles = presence_broadcast.get_valid_broadcast_roles;
+room_mt.get_valid_broadcast_roles = presence_broadcast.get_valid_broadcast_roles; -- FIXME doesn't exist in the library
local occupant_id = module:require "muc/occupant_id";
room_mt.get_salt = occupant_id.get_room_salt;
room_mt.get_occupant_id = occupant_id.get_occupant_id;
-local jid_split = require "util.jid".split;
-local jid_prep = require "util.jid".prep;
-local jid_bare = require "util.jid".bare;
-local st = require "util.stanza";
-local cache = require "util.cache";
-local um_is_admin = require "core.usermanager".is_admin;
+local jid_split = require "prosody.util.jid".split;
+local jid_prep = require "prosody.util.jid".prep;
+local jid_bare = require "prosody.util.jid".bare;
+local st = require "prosody.util.stanza";
+local cache = require "prosody.util.cache";
module:require "muc/config_form_sections";
@@ -111,21 +116,26 @@ module:depends "muc_unique"
module:require "muc/hats";
module:require "muc/lock";
-local function is_admin(jid)
- return um_is_admin(jid, module.host);
-end
+module:default_permissions("prosody:admin", {
+ ":automatic-ownership";
+ ":create-room";
+ ":recreate-destroyed-room";
+});
+module:default_permissions("prosody:guest", {
+ ":list-rooms";
+});
-if module:get_option_boolean("component_admins_as_room_owners", true) then
+if module:get_option_boolean("component_admins_as_room_owners", false) then
-- Monkey patch to make server admins room owners
local _get_affiliation = room_mt.get_affiliation;
function room_mt:get_affiliation(jid)
- if is_admin(jid) then return "owner"; end
+ if module:could(":automatic-ownership", jid) then return "owner"; end
return _get_affiliation(self, jid);
end
local _set_affiliation = room_mt.set_affiliation;
function room_mt:set_affiliation(actor, jid, affiliation, reason, data)
- if affiliation ~= "owner" and is_admin(jid) then return nil, "modify", "not-acceptable"; end
+ if affiliation ~= "owner" and module:could(":automatic-ownership", jid) then return nil, "modify", "not-acceptable"; end
return _set_affiliation(self, actor, jid, affiliation, reason, data);
end
end
@@ -158,8 +168,8 @@ local function room_save(room, forced, savestate)
end
end
-local max_rooms = module:get_option_number("muc_max_rooms");
-local max_live_rooms = module:get_option_number("muc_room_cache_size", 100);
+local max_rooms = module:get_option_integer("muc_max_rooms", nil, 0);
+local max_live_rooms = module:get_option_integer("muc_room_cache_size", 100, 1);
local room_hit = module:measure("room_hit", "rate");
local room_miss = module:measure("room_miss", "rate")
@@ -281,15 +291,16 @@ local function set_room_defaults(room, lang)
room:set_public(module:get_option_boolean("muc_room_default_public", false));
room:set_persistent(module:get_option_boolean("muc_room_default_persistent", room:get_persistent()));
room:set_members_only(module:get_option_boolean("muc_room_default_members_only", room:get_members_only()));
- room:set_allow_member_invites(module:get_option_boolean("muc_room_default_allow_member_invites",
- room:get_allow_member_invites()));
+ room:set_allow_member_invites(module:get_option_boolean("muc_room_default_allow_member_invites", room:get_allow_member_invites()));
room:set_moderated(module:get_option_boolean("muc_room_default_moderated", room:get_moderated()));
- room:set_whois(module:get_option_boolean("muc_room_default_public_jids",
- room:get_whois() == "anyone") and "anyone" or "moderators");
+ room:set_whois(module:get_option_boolean("muc_room_default_public_jids", room:get_whois() == "anyone") and "anyone" or "moderators");
room:set_changesubject(module:get_option_boolean("muc_room_default_change_subject", room:get_changesubject()));
- room:set_historylength(module:get_option_number("muc_room_default_history_length", room:get_historylength()));
+ room:set_historylength(module:get_option_integer("muc_room_default_history_length", room:get_historylength(), 0));
room:set_language(lang or module:get_option_string("muc_room_default_language"));
- room:set_presence_broadcast(module:get_option("muc_room_default_presence_broadcast", room:get_presence_broadcast()));
+ room:set_presence_broadcast(module:get_option_enum("muc_room_default_presence_broadcast", room:get_presence_broadcast(), "visitor", "participant",
+ "moderator"));
+ room:set_allow_pm(module:get_option_enum("muc_room_default_allow_pm", room:get_allow_pm(), "visitor", "participant", "moderator"));
+ room:set_allow_modpm(module:get_option_boolean("muc_room_default_always_allow_moderator_pms", room:get_allow_modpm()));
end
function create_room(room_jid, config)
@@ -350,8 +361,12 @@ function each_room(live_only)
end
module:hook("host-disco-items", function(event)
- local reply = event.reply;
module:log("debug", "host-disco-items called");
+ if not module:could(":list-rooms", event) then
+ module:log("debug", "Returning empty room list to unauthorized request");
+ return;
+ end
+ local reply = event.reply;
if next(room_items_cache) ~= nil then
for jid, room_name in pairs(room_items_cache) do
if room_name == "" then room_name = nil; end
@@ -388,7 +403,7 @@ end);
if module:get_option_boolean("muc_tombstones", true) then
- local ttl = module:get_option_number("muc_tombstone_expiry", 86400 * 31);
+ local ttl = module:get_option_period("muc_tombstone_expiry", "31 days");
module:hook("muc-room-destroyed",function(event)
local room = event.room;
@@ -412,26 +427,15 @@ if module:get_option_boolean("muc_tombstones", true) then
end, -10);
end
-do
- local restrict_room_creation = module:get_option("restrict_room_creation");
- if restrict_room_creation == true then
- restrict_room_creation = "admin";
- end
- if restrict_room_creation then
- local host_suffix = module.host:gsub("^[^%.]+%.", "");
- module:hook("muc-room-pre-create", function(event)
- local origin, stanza = event.origin, event.stanza;
- local user_jid = stanza.attr.from;
- if not is_admin(user_jid) and not (
- restrict_room_creation == "local" and
- select(2, jid_split(user_jid)) == host_suffix
- ) then
- origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Room creation is restricted", module.host));
- return true;
- end
- end);
+local restrict_room_creation = module:get_option_enum("restrict_room_creation", false, true, "local");
+module:default_permission(restrict_room_creation == true and "prosody:admin" or "prosody:registered", ":create-room");
+module:hook("muc-room-pre-create", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if restrict_room_creation ~= false and not module:may(":create-room", event) then
+ origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Room creation is restricted", module.host));
+ return true;
end
-end
+end);
for event_name, method in pairs {
-- Normal room interactions
@@ -465,7 +469,7 @@ for event_name, method in pairs {
if room and room._data.destroyed then
if room._data.locked < os.time()
- or (is_admin(stanza.attr.from) and stanza.name == "presence" and stanza.attr.type == nil) then
+ or (module:may(":recreate-destroyed-room", event) and stanza.name == "presence" and stanza.attr.type == nil) then
-- Allow the room to be recreated by admin or after time has passed
delete_room(room);
room = nil;
@@ -516,10 +520,10 @@ do -- Ad-hoc commands
module:depends "adhoc";
local t_concat = table.concat;
local adhoc_new = module:require "adhoc".new;
- local adhoc_initial = require "util.adhoc".new_initial_data_form;
- local adhoc_simple = require "util.adhoc".new_simple_form;
- local array = require "util.array";
- local dataforms_new = require "util.dataforms".new;
+ local adhoc_initial = require "prosody.util.adhoc".new_initial_data_form;
+ local adhoc_simple = require "prosody.util.adhoc".new_simple_form;
+ local array = require "prosody.util.array";
+ local dataforms_new = require "prosody.util.dataforms".new;
local destroy_rooms_layout = dataforms_new {
title = "Destroy rooms";
diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua
index 01427dbe..b8f276cf 100644
--- a/plugins/muc/muc.lib.lua
+++ b/plugins/muc/muc.lib.lua
@@ -12,18 +12,18 @@ local pairs = pairs;
local next = next;
local setmetatable = setmetatable;
-local dataform = require "util.dataforms";
-local iterators = require "util.iterators";
-local jid_split = require "util.jid".split;
-local jid_bare = require "util.jid".bare;
-local jid_prep = require "util.jid".prep;
-local jid_join = require "util.jid".join;
-local jid_resource = require "util.jid".resource;
-local resourceprep = require "util.encodings".stringprep.resourceprep;
-local st = require "util.stanza";
-local base64 = require "util.encodings".base64;
-local hmac_sha256 = require "util.hashes".hmac_sha256;
-local new_id = require "util.id".medium;
+local dataform = require "prosody.util.dataforms";
+local iterators = require "prosody.util.iterators";
+local jid_split = require "prosody.util.jid".split;
+local jid_bare = require "prosody.util.jid".bare;
+local jid_prep = require "prosody.util.jid".prep;
+local jid_join = require "prosody.util.jid".join;
+local jid_resource = require "prosody.util.jid".resource;
+local resourceprep = require "prosody.util.encodings".stringprep.resourceprep;
+local st = require "prosody.util.stanza";
+local base64 = require "prosody.util.encodings".base64;
+local hmac_sha256 = require "prosody.util.hashes".hmac_sha256;
+local new_id = require "prosody.util.id".medium;
local log = module._log;
@@ -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/plugins/muc/occupant.lib.lua b/plugins/muc/occupant.lib.lua
index 8fe4bbdf..a7d9cef7 100644
--- a/plugins/muc/occupant.lib.lua
+++ b/plugins/muc/occupant.lib.lua
@@ -1,6 +1,6 @@
local pairs = pairs;
local setmetatable = setmetatable;
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
local util = module:require "muc/util";
local function get_filtered_presence(stanza)
diff --git a/plugins/muc/occupant_id.lib.lua b/plugins/muc/occupant_id.lib.lua
index 1d310b3d..b1081c9b 100644
--- a/plugins/muc/occupant_id.lib.lua
+++ b/plugins/muc/occupant_id.lib.lua
@@ -4,9 +4,9 @@
-- (C) 2020 Maxime “pep” Buquet <pep@bouah.net>
-- (C) 2020 Matthew Wild <mwild1@gmail.com>
-local uuid = require "util.uuid";
-local hmac_sha256 = require "util.hashes".hmac_sha256;
-local b64encode = require "util.encodings".base64.encode;
+local uuid = require "prosody.util.uuid";
+local hmac_sha256 = require "prosody.util.hashes".hmac_sha256;
+local b64encode = require "prosody.util.encodings".base64.encode;
local xmlns_occupant_id = "urn:xmpp:occupant-id:0";
diff --git a/plugins/muc/password.lib.lua b/plugins/muc/password.lib.lua
index dd3cb658..9d3c0cca 100644
--- a/plugins/muc/password.lib.lua
+++ b/plugins/muc/password.lib.lua
@@ -7,7 +7,7 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
local function get_password(room)
return room._data.password;
diff --git a/plugins/muc/persistent.lib.lua b/plugins/muc/persistent.lib.lua
index c3b16ea4..29ed7784 100644
--- a/plugins/muc/persistent.lib.lua
+++ b/plugins/muc/persistent.lib.lua
@@ -8,7 +8,10 @@
--
local restrict_persistent = not module:get_option_boolean("muc_room_allow_persistent", true);
-local um_is_admin = require "core.usermanager".is_admin;
+module:default_permission(
+ restrict_persistent and "prosody:admin" or "prosody:registered",
+ ":create-persistent-room"
+);
local function get_persistent(room)
return room._data.persistent;
@@ -22,8 +25,8 @@ local function set_persistent(room, persistent)
end
module:hook("muc-config-form", function(event)
- if restrict_persistent and not um_is_admin(event.actor, module.host) then
- -- Don't show option if hidden rooms are restricted and user is not admin of this host
+ if not module:may(":create-persistent-room", event.actor) then
+ -- Hide config option if this user is not allowed to create persistent rooms
return;
end
table.insert(event.form, {
@@ -36,7 +39,7 @@ module:hook("muc-config-form", function(event)
end, 100-5);
module:hook("muc-config-submitted/muc#roomconfig_persistentroom", function(event)
- if restrict_persistent and not um_is_admin(event.actor, module.host) then
+ if not module:may(":create-persistent-room", event.actor) then
return; -- Not allowed
end
if set_persistent(event.room, event.value) then
diff --git a/plugins/muc/presence_broadcast.lib.lua b/plugins/muc/presence_broadcast.lib.lua
index 82a89fee..721c47aa 100644
--- a/plugins/muc/presence_broadcast.lib.lua
+++ b/plugins/muc/presence_broadcast.lib.lua
@@ -7,7 +7,7 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
local valid_roles = { "none", "visitor", "participant", "moderator" };
local default_broadcast = {
diff --git a/plugins/muc/register.lib.lua b/plugins/muc/register.lib.lua
index 84045f33..82ccb8ea 100644
--- a/plugins/muc/register.lib.lua
+++ b/plugins/muc/register.lib.lua
@@ -1,8 +1,8 @@
-local jid_bare = require "util.jid".bare;
-local jid_resource = require "util.jid".resource;
-local resourceprep = require "util.encodings".stringprep.resourceprep;
-local st = require "util.stanza";
-local dataforms = require "util.dataforms";
+local jid_bare = require "prosody.util.jid".bare;
+local jid_resource = require "prosody.util.jid".resource;
+local resourceprep = require "prosody.util.encodings".stringprep.resourceprep;
+local st = require "prosody.util.stanza";
+local dataforms = require "prosody.util.dataforms";
local allow_unaffiliated = module:get_option_boolean("allow_unaffiliated_register", false);
@@ -94,8 +94,10 @@ local function enforce_nick_policy(event)
local nick = get_registered_nick(room, jid_bare(stanza.attr.from));
if nick then
if event.occupant then
+ -- someone is joining, force their nickname to the registered one
event.occupant.nick = jid_bare(event.occupant.nick) .. "/" .. nick;
elseif event.dest_occupant.nick ~= jid_bare(event.dest_occupant.nick) .. "/" .. nick then
+ -- someone is trying to change nickname to something other than their registered nickname, can't have that
module:log("debug", "Attempt by %s to join as %s, but their reserved nick is %s", stanza.attr.from, requested_nick, nick);
local reply = st.error_reply(stanza, "cancel", "not-acceptable", nil, room.jid):up();
origin.send(reply);
diff --git a/plugins/muc/request.lib.lua b/plugins/muc/request.lib.lua
index 4e95fdc3..f3786595 100644
--- a/plugins/muc/request.lib.lua
+++ b/plugins/muc/request.lib.lua
@@ -7,14 +7,14 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
-local jid_resource = require "util.jid".resource;
+local st = require "prosody.util.stanza";
+local jid_resource = require "prosody.util.jid".resource;
module:hook("muc-disco#info", function(event)
event.reply:tag("feature", {var = "http://jabber.org/protocol/muc#request"}):up();
end);
-local voice_request_form = require "util.dataforms".new({
+local voice_request_form = require "prosody.util.dataforms".new({
title = "Voice Request";
{
name = "FORM_TYPE";
diff --git a/plugins/muc/restrict_pm.lib.lua b/plugins/muc/restrict_pm.lib.lua
new file mode 100644
index 00000000..e0b25cc8
--- /dev/null
+++ b/plugins/muc/restrict_pm.lib.lua
@@ -0,0 +1,119 @@
+-- Based on code from mod_muc_restrict_pm in prosody-modules@d82c0383106a
+-- by Nicholas George <wirlaburla@worlio.com>
+
+local st = require "util.stanza";
+local muc_util = module:require "muc/util";
+local valid_roles = muc_util.valid_roles;
+
+-- COMPAT w/ prosody-modules allow_pm
+local compat_map = {
+ everyone = "visitor";
+ participants = "participant";
+ moderators = "moderator";
+ members = "affiliated";
+};
+
+local function get_allow_pm(room)
+ local val = room._data.allow_pm;
+ return compat_map[val] or val or "visitor";
+end
+
+local function set_allow_pm(room, val)
+ if get_allow_pm(room) == val then return false; end
+ room._data.allow_pm = val;
+ return true;
+end
+
+local function get_allow_modpm(room)
+ return room._data.allow_modpm or false;
+end
+
+local function set_allow_modpm(room, val)
+ if get_allow_modpm(room) == val then return false; end
+ room._data.allow_modpm = val;
+ return true;
+end
+
+module:hook("muc-config-form", function(event)
+ local pmval = get_allow_pm(event.room);
+ table.insert(event.form, {
+ name = 'muc#roomconfig_allowpm';
+ type = 'list-single';
+ label = 'Allow private messages from';
+ options = {
+ { value = 'visitor', label = 'Everyone', default = pmval == 'visitor' };
+ { value = 'participant', label = 'Participants', default = pmval == 'participant' };
+ { value = 'moderator', label = 'Moderators', default = pmval == 'moderator' };
+ { value = 'affiliated', label = "Members", default = pmval == "affiliated" };
+ { value = 'none', label = 'No one', default = pmval == 'none' };
+ }
+ });
+ table.insert(event.form, {
+ name = '{xmpp:prosody.im}muc#allow_modpm';
+ type = 'boolean';
+ label = 'Always allow private messages to moderators';
+ value = get_allow_modpm(event.room)
+ });
+end);
+
+module:hook("muc-config-submitted/muc#roomconfig_allowpm", function(event)
+ if set_allow_pm(event.room, event.value) then
+ event.status_codes["104"] = true;
+ end
+end);
+
+module:hook("muc-config-submitted/{xmpp:prosody.im}muc#allow_modpm", function(event)
+ if set_allow_modpm(event.room, event.value) then
+ event.status_codes["104"] = true;
+ end
+end);
+
+local who_restricted = {
+ none = "in this group";
+ participant = "from guests";
+ moderator = "from non-moderators";
+ affiliated = "from non-members";
+};
+
+module:hook("muc-private-message", function(event)
+ local stanza, room = event.stanza, event.room;
+ local from_occupant = room:get_occupant_by_nick(stanza.attr.from);
+ local to_occupant = room:get_occupant_by_nick(stanza.attr.to);
+
+ -- To self is always okay
+ if to_occupant.bare_jid == from_occupant.bare_jid then return; end
+
+ if get_allow_modpm(room) then
+ if to_occupant and to_occupant.role == 'moderator'
+ or from_occupant and from_occupant.role == "moderator" then
+ return; -- Allow to/from moderators
+ end
+ end
+
+ local pmval = get_allow_pm(room);
+
+ if pmval ~= "none" then
+ if pmval == "affiliated" and room:get_affiliation(from_occupant.bare_jid) then
+ return; -- Allow from affiliated users
+ elseif valid_roles[from_occupant.role] >= valid_roles[pmval] then
+ module:log("debug", "Allowing PM: %s(%d) >= %s(%d)", from_occupant.role, valid_roles[from_occupant.role], pmval, valid_roles[pmval]);
+ return; -- Allow from a permitted role
+ end
+ end
+
+ local msg = ("Private messages are restricted %s"):format(who_restricted[pmval]);
+ module:log("debug", "Blocking PM from %s %s: %s", from_occupant.role, stanza.attr.from, msg);
+
+ room:route_to_occupant(
+ from_occupant,
+ st.error_reply(stanza, "cancel", "policy-violation", msg, room.jid)
+ );
+ return false;
+end, 1);
+
+return {
+ get_allow_pm = get_allow_pm;
+ set_allow_pm = set_allow_pm;
+ get_allow_modpm = get_allow_modpm;
+ set_allow_modpm = set_allow_modpm;
+};
diff --git a/plugins/muc/subject.lib.lua b/plugins/muc/subject.lib.lua
index 3230817c..047ea2df 100644
--- a/plugins/muc/subject.lib.lua
+++ b/plugins/muc/subject.lib.lua
@@ -7,8 +7,8 @@
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
-local dt = require "util.datetime";
+local st = require "prosody.util.stanza";
+local dt = require "prosody.util.datetime";
local muc_util = module:require "muc/util";
local valid_roles = muc_util.valid_roles;