aboutsummaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/mod_account_activity.lua152
-rw-r--r--plugins/mod_admin_shell.lua247
-rw-r--r--plugins/mod_authz_internal.lua15
-rw-r--r--plugins/mod_bosh.lua4
-rw-r--r--plugins/mod_c2s.lua9
-rw-r--r--plugins/mod_cloud_notify.lua14
-rw-r--r--plugins/mod_component.lua4
-rw-r--r--plugins/mod_cron.lua2
-rw-r--r--plugins/mod_external_services.lua1
-rw-r--r--plugins/mod_http_altconnect.lua52
-rw-r--r--plugins/mod_http_file_share.lua48
-rw-r--r--plugins/mod_invites.lua324
-rw-r--r--plugins/mod_mam/mod_mam.lua2
-rw-r--r--plugins/mod_presence.lua12
-rw-r--r--plugins/mod_s2s.lua8
-rw-r--r--plugins/mod_storage_internal.lua4
-rw-r--r--plugins/mod_storage_sql.lua52
-rw-r--r--plugins/mod_vcard.lua32
-rw-r--r--plugins/mod_websocket.lua32
-rw-r--r--plugins/muc/hats.lib.lua2
-rw-r--r--plugins/muc/history.lib.lua2
-rw-r--r--plugins/muc/mod_muc.lua4
-rw-r--r--plugins/muc/vcard.lib.lua82
23 files changed, 877 insertions, 227 deletions
diff --git a/plugins/mod_account_activity.lua b/plugins/mod_account_activity.lua
new file mode 100644
index 00000000..1b1208e7
--- /dev/null
+++ b/plugins/mod_account_activity.lua
@@ -0,0 +1,152 @@
+local jid = require "prosody.util.jid";
+local time = os.time;
+
+local store = module:open_store(nil, "keyval+");
+
+module:hook("authentication-success", function(event)
+ local session = event.session;
+ if session.username then
+ store:set_key(session.username, "timestamp", time());
+ end
+end);
+
+module:hook("resource-unbind", function(event)
+ local session = event.session;
+ if session.username then
+ store:set_key(session.username, "timestamp", time());
+ end
+end);
+
+local user_sessions = prosody.hosts[module.host].sessions;
+function get_last_active(username) --luacheck: ignore 131/get_last_active
+ if user_sessions[username] then
+ return os.time(), true; -- Currently connected
+ else
+ local last_activity = store:get(username);
+ if not last_activity then return nil; end
+ return last_activity.timestamp;
+ end
+end
+
+module:add_item("shell-command", {
+ section = "user";
+ section_desc = "View user activity data";
+ name = "activity";
+ desc = "View the last recorded user activity for an account";
+ args = { { name = "jid"; type = "string" } };
+ host_selector = "jid";
+ handler = function(self, userjid) --luacheck: ignore 212/self
+ local username = jid.prepped_split(userjid);
+ local last_timestamp, is_online = get_last_active(username);
+ if not last_timestamp then
+ return true, "No activity";
+ end
+
+ return true, ("%s (%s)"):format(os.date("%Y-%m-%d %H:%M:%S", last_timestamp), (is_online and "online" or "offline"));
+ end;
+});
+
+module:add_item("shell-command", {
+ section = "user";
+ section_desc = "View user activity data";
+ name = "list_inactive";
+ desc = "List inactive user accounts";
+ args = {
+ { name = "host"; type = "string" };
+ { name = "duration"; type = "string" };
+ };
+ host_selector = "host";
+ handler = function(self, host, duration) --luacheck: ignore 212/self
+ local um = require "prosody.core.usermanager";
+ local duration_sec = require "prosody.util.human.io".parse_duration(duration or "");
+ if not duration_sec then
+ return false, ("Invalid duration %q - try something like \"30d\""):format(duration);
+ end
+
+ local now = os.time();
+ local n_inactive, n_unknown = 0, 0;
+
+ for username in um.users(host) do
+ local last_active = store:get_key(username, "timestamp");
+ if not last_active then
+ local created_at = um.get_account_info(username, host).created;
+ if created_at and (now - created_at) > duration_sec then
+ self.session.print(username, "");
+ n_inactive = n_inactive + 1;
+ elseif not created_at then
+ n_unknown = n_unknown + 1;
+ end
+ elseif (now - last_active) > duration_sec then
+ self.session.print(username, os.date("%Y-%m-%dT%T", last_active));
+ n_inactive = n_inactive + 1;
+ end
+ end
+
+ if n_unknown > 0 then
+ return true, ("%d accounts inactive since %s (%d unknown)"):format(n_inactive, os.date("%Y-%m-%dT%T", now - duration_sec), n_unknown);
+ end
+ return true, ("%d accounts inactive since %s"):format(n_inactive, os.date("%Y-%m-%dT%T", now - duration_sec));
+ end;
+});
+
+module:add_item("shell-command", {
+ section = "migrate";
+ section_desc = "Perform data migrations";
+ name = "account_activity_lastlog2";
+ desc = "Migrate account activity information from mod_lastlog2";
+ args = { { name = "host"; type = "string" } };
+ host_selector = "host";
+ handler = function(self, host) --luacheck: ignore 212/host
+ local lastlog2 = module:open_store("lastlog2", "keyval+");
+ local n_updated, n_errors, n_skipped = 0, 0, 0;
+
+ local async = require "prosody.util.async";
+
+ local p = require "prosody.util.promise".new(function (resolve)
+ local async_runner = async.runner(function ()
+ local n = 0;
+ for username in lastlog2:items() do
+ n = n + 1;
+ if n % 100 == 0 then
+ self.session.print(("Processed %d..."):format(n));
+ async.sleep(0);
+ end
+ local lastlog2_data = lastlog2:get(username);
+ if lastlog2_data then
+ local current_data, err = store:get(username);
+ if not current_data then
+ if not err then
+ current_data = {};
+ else
+ n_errors = n_errors + 1;
+ end
+ end
+ if current_data then
+ local imported_timestamp = current_data.timestamp;
+ local latest;
+ for k, v in pairs(lastlog2_data) do
+ if k ~= "registered" and (not latest or v.timestamp > latest) then
+ latest = v.timestamp;
+ end
+ end
+ if latest and (not imported_timestamp or imported_timestamp < latest) then
+ local ok, err = store:set_key(username, "timestamp", latest);
+ if ok then
+ n_updated = n_updated + 1;
+ else
+ self.session.print(("WW: Failed to import %q: %s"):format(username, err));
+ n_errors = n_errors + 1;
+ end
+ else
+ n_skipped = n_skipped + 1;
+ end
+ end
+ end
+ end
+ return resolve(("%d accounts imported, %d errors, %d skipped"):format(n_updated, n_errors, n_skipped));
+ end);
+ async_runner:run(true);
+ end);
+ return p;
+ end;
+});
diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua
index 974ed8d9..28d758d0 100644
--- a/plugins/mod_admin_shell.lua
+++ b/plugins/mod_admin_shell.lua
@@ -19,6 +19,7 @@ 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 parse_args = require "prosody.util.argparse".parse;
local _G = _G;
@@ -91,13 +92,8 @@ 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'
+If you're ever wondering how to get out, simply type 'quit' (ctrl+d should also
+work).
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
@@ -141,7 +137,7 @@ Built-in roles are:
prosody:registered - Registered user
prosody:member - Provisioned user
prosody:admin - Host administrator
- prosody:operator - Server administrator
+ prosody:operator - Server administrator
Roles can be assigned using the user management commands (see 'help user').
]];
@@ -255,6 +251,83 @@ function console:new_session(admin_session)
return session;
end
+local function process_cmd_line(session, arg_line)
+ local chunk = load("return "..arg_line, "=shell", "t", {});
+ local ok, args = pcall(chunk);
+ if not ok then return nil, args; end
+
+ local section_name, command = args[1], args[2];
+
+ local section_mt = getmetatable(def_env[section_name]);
+ local section_help = section_mt and section_mt.help;
+ local command_help = section_help and section_help.commands[command];
+
+ if not command_help then
+ if commands[section_name] then
+ commands[section_name](session, table.concat(args, " "));
+ return;
+ end
+ if section_help then
+ return nil, "Command not found or necessary module not loaded. Try 'help "..section_name.." for a list of available commands.";
+ end
+ return nil, "Command not found. Is the necessary module loaded?";
+ end
+
+ local fmt = { "%s"; ":%s("; ")" };
+
+ if command_help.flags then
+ local flags, flags_err, flags_err_extra = parse_args(args, command_help.flags);
+ if not flags then
+ if flags_err == "missing-value" then
+ return nil, "Expected value after "..flags_err_extra;
+ elseif flags_err == "param-not-found" then
+ return nil, "Unknown parameter: "..flags_err_extra;
+ end
+ return nil, flags_err;
+ end
+
+ table.remove(flags, 2);
+ table.remove(flags, 1);
+
+ local n_fixed_args = #command_help.args;
+
+ local arg_str = {};
+ for i = 1, n_fixed_args do
+ if flags[i] ~= nil then
+ table.insert(arg_str, ("%q"):format(flags[i]));
+ else
+ table.insert(arg_str, "nil");
+ end
+ end
+
+ table.insert(arg_str, "flags");
+
+ for i = n_fixed_args + 1, #flags do
+ if flags[i] ~= nil then
+ table.insert(arg_str, ("%q"):format(flags[i]));
+ else
+ table.insert(arg_str, "nil");
+ end
+ end
+
+ table.insert(fmt, 3, "%s");
+
+ return "local flags = ...; return "..string.format(table.concat(fmt), section_name, command, table.concat(arg_str, ", ")), flags;
+ end
+
+ for i = 3, #args do
+ if args[i]:sub(1, 1) == ":" then
+ table.insert(fmt, i, ")%s(");
+ elseif i > 3 and fmt[i - 1]:match("%%q$") then
+ table.insert(fmt, i, ", %q");
+ else
+ table.insert(fmt, i, "%q");
+ end
+ end
+
+ return "return "..string.format(table.concat(fmt), table.unpack(args));
+end
+
local function handle_line(event)
local session = event.origin.shell_session;
if not session then
@@ -269,6 +342,8 @@ local function handle_line(event)
local line = event.stanza:get_text();
local useglobalenv;
+ session.repl = event.stanza.attr.repl ~= "0";
+
local result = st.stanza("repl-result");
if line:match("^>") then
@@ -295,23 +370,6 @@ local function handle_line(event)
session.globalenv = redirect_output(_G, session);
end
- local chunkname = "=console";
- local env = (useglobalenv and session.globalenv) or session.env or nil
- -- luacheck: ignore 311/err
- local chunk, err = envload("return "..line, chunkname, env);
- if not chunk then
- chunk, err = envload(line, chunkname, env);
- if not chunk then
- err = err:gsub("^%[string .-%]:%d+: ", "");
- err = err:gsub("^:%d+: ", "");
- err = err:gsub("'<eof>'", "the end of the line");
- result.attr.type = "error";
- result:text("Sorry, I couldn't understand that... "..err);
- event.origin.send(result);
- return;
- end
- end
-
local function send_result(taskok, message)
if not message then
if type(taskok) ~= "string" and useglobalenv then
@@ -328,7 +386,45 @@ local function handle_line(event)
event.origin.send(result);
end
- local taskok, message = chunk();
+ local taskok, message;
+ local env = (useglobalenv and session.globalenv) or session.env or nil;
+ local flags;
+
+ local source;
+ if line:match("^{") then
+ -- Input is a serialized array of strings, typically from
+ -- a command-line invocation of 'prosodyctl shell something'
+ source, flags = process_cmd_line(session, line);
+ if not source then
+ if flags then -- err
+ send_result(false, flags);
+ else -- no err, but nothing more to do
+ -- This happens if it was a "simple" command
+ event.origin.send(result);
+ end
+ return;
+ end
+ end
+
+ local chunkname = "=console";
+ -- luacheck: ignore 311/err
+ local chunk, err = envload(source or ("return "..line), chunkname, env);
+ if not chunk then
+ if not source then
+ chunk, err = envload(line, chunkname, env);
+ end
+ if not chunk then
+ err = err:gsub("^%[string .-%]:%d+: ", "");
+ err = err:gsub("^:%d+: ", "");
+ err = err:gsub("'<eof>'", "the end of the line");
+ result.attr.type = "error";
+ result:text("Sorry, I couldn't understand that... "..err);
+ event.origin.send(result);
+ return;
+ end
+ end
+
+ taskok, message = chunk(flags);
if promise.is_promise(taskok) then
taskok:next(function (resolved_message)
@@ -349,7 +445,7 @@ module:hook("admin/repl-input", function (event)
return true;
end);
-local function describe_command(s)
+local function describe_command(s, hidden)
local section, name, args, desc = s:match("^([%w_]+):([%w_]+)%(([^)]*)%) %- (.+)$");
if not section then
error("Failed to parse command description: "..s);
@@ -360,9 +456,14 @@ local function describe_command(s)
args = array.collect(args:gmatch("[%w_]+")):map(function (arg_name)
return { name = arg_name };
end);
+ hidden = hidden;
};
end
+local function hidden_command(s)
+ return describe_command(s, true);
+end
+
-- Console commands --
-- These are simple commands, not valid standalone in Lua
@@ -455,10 +556,46 @@ def_env.help = setmetatable({}, {
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));
+ if not command_help.hidden then
+ c = c + 1;
+ local desc = command_help.desc or command_help.module and ("Provided by mod_"..command_help.module) or "";
+ if self.session.repl then
+ local args = array.pluck(command_help.args, "name"):concat(", ");
+ print(("%s:%s(%s) - %s"):format(section_name, command, args, desc));
+ else
+ local args = array.pluck(command_help.args, "name"):concat("> <");
+ if args ~= "" then
+ args = "<"..args..">";
+ end
+ print(("%s %s %s"):format(section_name, command, args));
+ print((" %s"):format(desc));
+ if command_help.flags then
+ local flags = command_help.flags;
+ print("");
+ print((" Flags:"));
+
+ if flags.kv_params then
+ for name in it.sorted_pairs(flags.kv_params) do
+ print(" --"..name:gsub("_", "-"));
+ end
+ end
+
+ if flags.value_params then
+ for name in it.sorted_pairs(flags.value_params) do
+ print(" --"..name:gsub("_", "-").." <"..name..">");
+ end
+ end
+
+ if flags.array_params then
+ for name in it.sorted_pairs(flags.array_params) do
+ print(" --"..name:gsub("_", "-").." <"..name..">, ...");
+ end
+ end
+
+ end
+ print("");
+ end
+ end
end
elseif help_topics[section_name] then
local topic = help_topics[section_name];
@@ -1800,9 +1937,8 @@ function def_env.user:password(jid, password)
end);
end
-describe_command [[user:roles(jid, host) - Show current roles for an user]]
+describe_command [[user:role(jid, host) - Show primary role for a 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 not prosody.hosts[host] then
@@ -1814,22 +1950,27 @@ function def_env.user:role(jid, host)
local primary_role = um.get_user_role(username, host);
local secondary_roles = um.get_user_secondary_roles(username, host);
+ local primary_role_desc = primary_role and primary_role.name or "<none>";
+
print(primary_role and primary_role.name or "<none>");
- local count = primary_role and 1 or 0;
+ local n_secondary = 0;
for role_name in pairs(secondary_roles or {}) do
- count = count + 1;
+ n_secondary = n_secondary + 1;
print(role_name.." (secondary)");
end
- return true, count == 1 and "1 role" or count.." roles";
+ if n_secondary > 0 then
+ return true, primary_role_desc.." (primary)";
+ end
+ return true, primary_role_desc;
end
def_env.user.roles = def_env.user.role;
-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)
+describe_command [[user:set_role(jid, host, role) - Set primary role of a user (see 'help roles')]]
+-- user:set_role("someone@example.com", "example.com", "prosody:admin")
+-- user:set_role("someone@example.com", "prosody:admin")
+function def_env.user:set_role(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
@@ -1844,7 +1985,7 @@ function def_env.user:setrole(jid, host, new_role)
end
end
-describe_command [[user:addrole(jid, host, role) - Add a secondary role to a user]]
+hidden_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
@@ -1855,10 +1996,14 @@ function def_env.user:addrole(jid, host, new_role)
elseif userhost ~= host then
return nil, "Can't add roles outside users own host"
end
- return um.add_user_secondary_role(username, host, new_role);
+ local role, err = um.add_user_secondary_role(username, host, new_role);
+ if not role then
+ return nil, err;
+ end
+ return true, "Role added";
end
-describe_command [[user:delrole(jid, host, role) - Remove a secondary role from a user]]
+hidden_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
@@ -1869,7 +2014,11 @@ function def_env.user:delrole(jid, host, role_name)
elseif userhost ~= host then
return nil, "Can't remove roles outside users own host"
end
- return um.remove_user_secondary_role(username, host, role_name);
+ local ok, err = um.remove_user_secondary_role(username, host, role_name);
+ if not ok then
+ return nil, err;
+ end
+ return true, "Role removed";
end
describe_command [[user:list(hostname, pattern) - List users on the specified host, optionally filtering with a pattern]]
@@ -2622,10 +2771,20 @@ local function new_item_handlers(command_host)
section_mt.help = section_help;
end
+ if command.flags then
+ if command.flags.stop_on_positional == nil then
+ command.flags.stop_on_positional = false;
+ end
+ if command.flags.strict == nil then
+ command.flags.strict = true;
+ end
+ end
+
section_help.commands[command.name] = {
desc = command.desc;
full = command.help;
args = array(command.args);
+ flags = command.flags;
module = command._provided_by;
};
diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua
index 7a06c904..f683d90c 100644
--- a/plugins/mod_authz_internal.lua
+++ b/plugins/mod_authz_internal.lua
@@ -161,7 +161,7 @@ end
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));
+ return error("Cannot assign user an unknown role: "..tostring(role_name));
end
local keys_update = {
_default = role_name;
@@ -180,14 +180,19 @@ function set_user_role(user, role_name)
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));
+ local role = role_registry[role_name];
+ if not role then
+ return error("Cannot assign user an unknown role: "..tostring(role_name));
end
- role_map_store:set(user, role_name, true);
+ local ok, err = role_map_store:set(user, role_name, true);
+ if not ok then
+ return nil, err;
+ end
+ return role;
end
function remove_user_secondary_role(user, role_name)
- role_map_store:set(user, role_name, nil);
+ return role_map_store:set(user, role_name, nil);
end
function get_user_secondary_roles(user)
diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua
index 091a7d81..2d1b1922 100644
--- a/plugins/mod_bosh.lua
+++ b/plugins/mod_bosh.lua
@@ -557,6 +557,10 @@ function module.add_host(module)
["POST /"] = handle_POST;
};
});
+
+ if module.host ~= "*" then
+ module:depends("http_altconnect", true);
+ end
end
if require"prosody.core.modulemanager".get_modules_for_host("*"):contains(module.name) then
diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua
index e29ea6a0..c1fbdb9e 100644
--- a/plugins/mod_c2s.lua
+++ b/plugins/mod_c2s.lua
@@ -252,12 +252,16 @@ local function session_close(session, reason)
if not session.destroyed then
session.log("warn", "Failed to receive a stream close response, closing connection anyway...");
sm_destroy_session(session, reason_text);
- if conn then conn:close(); end
+ if conn then
+ conn:close();
+ end
end
end);
else
sm_destroy_session(session, reason_text);
- if conn then conn:close(); end
+ if conn then
+ conn:close();
+ end
end
else
local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason;
@@ -273,6 +277,7 @@ local function disconnect_user_sessions(reason, leave_resource)
if not (hosts[host] and hosts[host].type == "local") then
return -- not a local VirtualHost so no sessions
end
+ module:log("debug", "Disconnecting %s sessions of %s@%s (%s)", not leave_resource and "all" or "other", username, host, reason.text);
local user = hosts[host].sessions[username];
if user and user.sessions then
for r, session in pairs(user.sessions) do
diff --git a/plugins/mod_cloud_notify.lua b/plugins/mod_cloud_notify.lua
index 987be84f..1c660e93 100644
--- a/plugins/mod_cloud_notify.lua
+++ b/plugins/mod_cloud_notify.lua
@@ -5,13 +5,13 @@
-- This file is MIT/X11 licensed.
local os_time = os.time;
-local st = require"util.stanza";
-local jid = require"util.jid";
-local dataform = require"util.dataforms".new;
-local hashes = require"util.hashes";
-local random = require"util.random";
-local cache = require"util.cache";
-local watchdog = require "util.watchdog";
+local st = require"prosody.util.stanza";
+local jid = require"prosody.util.jid";
+local dataform = require"prosody.util.dataforms".new;
+local hashes = require"prosody.util.hashes";
+local random = require"prosody.util.random";
+local cache = require"prosody.util.cache";
+local watchdog = require "prosody.util.watchdog";
local xmlns_push = "urn:xmpp:push:0";
diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua
index 86ceb980..51c73235 100644
--- a/plugins/mod_component.lua
+++ b/plugins/mod_component.lua
@@ -239,7 +239,9 @@ function stream_callbacks.handlestanza(session, stanza)
end
if not stanza.attr.to then
session.log("warn", "Rejecting stanza with no 'to' address");
- session.send(st.error_reply(stanza, "modify", "bad-request", "Components MUST specify a 'to' address on stanzas"));
+ if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
+ session.send(st.error_reply(stanza, "modify", "bad-request", "Components MUST specify a 'to' address on stanzas"));
+ end
return;
end
end
diff --git a/plugins/mod_cron.lua b/plugins/mod_cron.lua
index 67b68514..77bdd7e5 100644
--- a/plugins/mod_cron.lua
+++ b/plugins/mod_cron.lua
@@ -78,7 +78,7 @@ module:add_item("shell-command", {
args = {};
handler = function(self, filter_host)
local format_table = require("prosody.util.human.io").table;
- local it = require("util.iterators");
+ local it = require("prosody.util.iterators");
local row = format_table({
{ title = "Host"; width = "2p" };
{ title = "Task"; width = "3p" };
diff --git a/plugins/mod_external_services.lua b/plugins/mod_external_services.lua
index ade1e327..a2cd232c 100644
--- a/plugins/mod_external_services.lua
+++ b/plugins/mod_external_services.lua
@@ -35,6 +35,7 @@ end
local algorithms = {
turn = behave_turn_rest_credentials;
+ turns = behave_turn_rest_credentials;
}
-- filter config into well-defined service records
diff --git a/plugins/mod_http_altconnect.lua b/plugins/mod_http_altconnect.lua
new file mode 100644
index 00000000..9252433e
--- /dev/null
+++ b/plugins/mod_http_altconnect.lua
@@ -0,0 +1,52 @@
+-- mod_http_altconnect
+-- XEP-0156: Discovering Alternative XMPP Connection Methods
+
+module:depends"http";
+
+local mm = require "prosody.core.modulemanager";
+local json = require"prosody.util.json";
+local st = require"prosody.util.stanza";
+local array = require"prosody.util.array";
+
+local advertise_bosh = module:get_option_boolean("advertise_bosh", true);
+local advertise_websocket = module:get_option_boolean("advertise_websocket", true);
+
+local function get_supported()
+ local uris = array();
+ if advertise_bosh and (mm.is_loaded(module.host, "bosh") or mm.is_loaded("*", "bosh")) then
+ uris:push({ rel = "urn:xmpp:alt-connections:xbosh", href = module:http_url("bosh", "/http-bind") });
+ end
+ if advertise_websocket and (mm.is_loaded(module.host, "websocket") or mm.is_loaded("*", "websocket")) then
+ uris:push({ rel = "urn:xmpp:alt-connections:websocket", href = module:http_url("websocket", "xmpp-websocket"):gsub("^http", "ws") });
+ end
+ return uris;
+end
+
+
+local function GET_xml(event)
+ local response = event.response;
+ local xrd = st.stanza("XRD", { xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0' });
+ local uris = get_supported();
+ for _, method in ipairs(uris) do
+ xrd:tag("Link", method):up();
+ end
+ response.headers.content_type = "application/xrd+xml"
+ response.headers.access_control_allow_origin = "*";
+ return '<?xml version="1.0" encoding="UTF-8"?>' .. tostring(xrd);
+end
+
+local function GET_json(event)
+ local response = event.response;
+ local jrd = { links = get_supported() };
+ response.headers.content_type = "application/json"
+ response.headers.access_control_allow_origin = "*";
+ return json.encode(jrd);
+end;
+
+module:provides("http", {
+ default_path = "/.well-known";
+ route = {
+ ["GET /host-meta"] = GET_xml;
+ ["GET /host-meta.json"] = GET_json;
+ };
+});
diff --git a/plugins/mod_http_file_share.lua b/plugins/mod_http_file_share.lua
index 48972067..7b9aff75 100644
--- a/plugins/mod_http_file_share.lua
+++ b/plugins/mod_http_file_share.lua
@@ -62,12 +62,33 @@ local upload_errors = errors.init(module.name, namespace, {
access = { type = "auth"; condition = "forbidden" };
filename = { type = "modify"; condition = "bad-request"; text = "Invalid filename" };
filetype = { type = "modify"; condition = "not-acceptable"; text = "File type not allowed" };
- filesize = { type = "modify"; condition = "not-acceptable"; text = "File too large";
- extra = {tag = st.stanza("file-too-large", {xmlns = namespace}):tag("max-file-size"):text(tostring(file_size_limit)) };
+ filesize = {
+ code = 413;
+ type = "modify";
+ condition = "not-acceptable";
+ text = "File too large";
+ extra = {
+ tag = st.stanza("file-too-large", { xmlns = namespace }):tag("max-file-size"):text(tostring(file_size_limit));
+ };
};
filesizefmt = { type = "modify"; condition = "bad-request"; text = "File size must be positive integer"; };
quota = { type = "wait"; condition = "resource-constraint"; text = "Daily quota reached"; };
outofdisk = { type = "wait"; condition = "resource-constraint"; text = "Server global storage quota reached" };
+ authzmalformed = {
+ code = 401;
+ type = "auth";
+ condition = "not-authorized";
+ text = "Missing or malformed Authorization header";
+ };
+ unauthz = { code = 403; type = "auth"; condition = "forbidden"; text = "Unauthorized or invalid token" };
+ invalidslot = {
+ code = 400;
+ type = "modify";
+ condition = "bad-request";
+ text = "Invalid upload slot, must not contain '/'";
+ };
+ alreadycompleted = { code = 409; type = "cancel"; condition = "conflict"; text = "Upload already completed" };
+ writefail = { code = 500; type = "wait"; condition = "internal-server-error" }
});
local upload_cache = cache.new(1024);
@@ -224,6 +245,7 @@ function handle_slot_request(event)
end
total_storage_usage = total_storage_usage + filesize;
+ persist_stats:set(nil, "total", total_storage_usage);
module:log("debug", "Total storage usage: %s / %s", B(total_storage_usage), B(total_storage_limit));
local cached_quota = quota_cache:get(uploader);
@@ -259,19 +281,19 @@ function handle_upload(event, path) -- PUT /upload/:slot
if not authz then
module:log("debug", "Missing or malformed Authorization header");
event.response.headers.www_authenticate = "Bearer";
- return 401;
+ return upload_errors.new("authzmalformed", { request = request });
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;
+ return upload_errors.new("unauthz", { request = request; wrapped_error = authed_upload_info });
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;
+ return upload_errors.new("unauthz", { request = request });
end
if request.headers.content_length and tonumber(request.headers.content_length) ~= authed_upload_info.filesize then
- return 413;
+ return upload_errors.new("filesize", { request = request });
-- 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
@@ -287,7 +309,7 @@ function handle_upload(event, path) -- PUT /upload/:slot
local f = io.open(filename, "r");
if f then
f:close();
- return 409;
+ return upload_errors.new("alreadycompleted", { request = request });
end
end
@@ -296,7 +318,7 @@ function handle_upload(event, path) -- PUT /upload/:slot
local fh, err = io.open(filename.."~", "w");
if not fh then
module:log("error", "Could not open file for writing: %s", err);
- return 500;
+ return upload_errors.new("writefail", { request = request; wrapped_error = err });
end
function event.response:on_destroy() -- luacheck: ignore 212/self
-- Clean up incomplete upload
@@ -329,7 +351,7 @@ function handle_upload(event, path) -- PUT /upload/:slot
local uploaded, err = errors.coerce(request.body_sink:close());
if final_size ~= upload_info.filesize then
-- Could be too short as well, but we say the same thing
- uploaded, err = false, 413;
+ uploaded, err = false, upload_errors.new("filesize", { request = request });
end
if uploaded then
module:log("debug", "Upload of %q completed, %s", filename, B(final_size));
@@ -407,18 +429,22 @@ function handle_download(event, path) -- GET /uploads/:slot+filename
local request_range = request.headers.range;
local response_range;
if request_range then
+ local last_byte = string.format("%d", tonumber(filesize) - 1);
local range_start, range_end = request_range:match("^bytes=(%d+)%-(%d*)$")
-- Only support resumption, ie ranges from somewhere in the middle until the end of the file.
- if (range_start and range_start ~= "0") and (range_end == "" or range_end == filesize) then
+ if (range_start and range_start ~= "0") and (range_end == "" or range_end == last_byte) then
local pos, size = tonumber(range_start), tonumber(filesize);
local new_pos = pos < size and handle:seek("set", pos);
if new_pos and new_pos < size then
- response_range = "bytes "..range_start.."-"..filesize.."/"..filesize;
+ response_range = "bytes "..range_start.."-"..last_byte.."/"..filesize;
filesize = string.format("%d", size-pos);
else
handle:close();
return 416;
end
+ else
+ handle:close();
+ return 416;
end
end
diff --git a/plugins/mod_invites.lua b/plugins/mod_invites.lua
index 1dfc8804..c6a66a8f 100644
--- a/plugins/mod_invites.lua
+++ b/plugins/mod_invites.lua
@@ -6,8 +6,8 @@ local jid_split = require "prosody.util.jid".split;
local argparse = require "prosody.util.argparse";
local human_io = require "prosody.util.human.io";
-local url_escape = require "util.http".urlencode;
-local render_url = require "util.interpolation".new("%b{}", url_escape, {
+local url_escape = require "prosody.util.http".urlencode;
+local render_url = require "prosody.util.interpolation".new("%b{}", url_escape, {
urlescape = url_escape;
noscheme = function (urlstring)
return (urlstring:gsub("^[^:]+:", ""));
@@ -239,19 +239,76 @@ end
module:hook("invite-created", add_landing_url, -1);
--- shell command
+-- COMPAT: Dynamic groups are work in progress as of 13.0, so we'll use the
+-- presence of mod_invites_groups (a community module) to determine whether to
+-- expose our support for invites to groups.
+local have_group_invites = module:get_option_inherited_set("modules_enabled"):contains("invites_groups");
+
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";
+ flags = {
+ array_params = { role = true, group = have_group_invites };
+ value_params = { expires_after = true };
+ };
+
+ handler = function (self, user_jid, opts) --luacheck: ignore 212/self
+ local username = jid_split(user_jid);
+ local roles = opts and opts.role or {};
+ local groups = opts and opts.group or {};
+
+ if opts and opts.admin then
+ -- Insert it first since we don't get order out of argparse
+ table.insert(roles, 1, "prosody:admin");
+ end
+
+ local ttl;
+ if opts and opts.expires_after then
+ ttl = human_io.parse_duration(opts.expires_after);
+ if not ttl then
+ return false, "Unable to parse duration: "..opts.expires_after;
+ end
+ end
+
+ local invite = assert(create_account(username, {
+ roles = roles;
+ groups = groups;
+ }, ttl));
+
+ return true, invite.landing_page or invite.uri;
+ end;
+});
+
+module:add_item("shell-command", {
+ section = "invite";
+ section_desc = "Create and manage invitations";
+ name = "create_reset";
+ desc = "Create a password reset link for the specified user";
args = { { name = "user_jid", type = "string" } };
host_selector = "user_jid";
+ flags = {
+ value_params = { expires_after = true };
+ };
- handler = function (self, user_jid) --luacheck: ignore 212/self
+ handler = function (self, user_jid, opts) --luacheck: ignore 212/self
local username = jid_split(user_jid);
- local invite, err = create_account(username);
+ if not username then
+ return nil, "Supply the JID of the account you want to generate a password reset for";
+ end
+ local duration_sec = require "prosody.util.human.io".parse_duration(opts and opts.expires_after or "1d");
+ if not duration_sec then
+ return nil, "Unable to parse duration: "..opts.expires_after;
+ end
+ local invite, err = create_account_reset(username, duration_sec);
if not invite then return nil, err; end
- return true, invite.landing_page or invite.uri;
+ self.session.print(invite.landing_page or invite.uri);
+ return true, ("Password reset link for %s valid until %s"):format(user_jid, os.date("%Y-%m-%d %T", invite.expires));
end;
});
@@ -260,126 +317,185 @@ module:add_item("shell-command", {
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" } };
+ args = { { name = "user_jid", type = "string" } };
host_selector = "user_jid";
+ flags = {
+ value_params = { expires_after = true };
+ kv_params = { allow_registration = true };
+ };
- handler = function (self, user_jid, allow_registration) --luacheck: ignore 212/self
+ handler = function (self, user_jid, opts) --luacheck: ignore 212/self
local username = jid_split(user_jid);
- local invite, err = create_contact(username, allow_registration);
+ if not username then
+ return nil, "Supply the JID of the account you want the recipient to become a contact of";
+ end
+ local ttl;
+ if opts and opts.expires_after then
+ ttl = require "prosody.util.human.io".parse_duration(opts.expires_after);
+ if not ttl then
+ return nil, "Unable to parse duration: "..opts.expires_after;
+ end
+ end
+ local invite, err = create_contact(username, opts and opts.allow_registration, nil, ttl);
if not invite then return nil, err; end
return true, invite.landing_page or invite.uri;
end;
});
-local subcommands = {};
-
---- prosodyctl command
-function module.command(arg)
- 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
- return subcommands[cmd](arg);
-end
-
-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
+module:add_item("shell-command", {
+ section = "invite";
+ section_desc = "Create and manage invitations";
+ name = "show";
+ desc = "Show details of an account invitation token";
+ args = { { name = "host", type = "string" }, { name = "token", type = "string" } };
+ host_selector = "host";
- local earlyopts = argparse.parse(arg, { short_params = { h = "help"; ["?"] = "help" } });
- if earlyopts.help or not earlyopts[1] then
- return help();
- end
+ handler = function (self, host, token) --luacheck: ignore 212/self 212/host
+ local invite, err = get_account_invite_info(token);
+ if not invite then return nil, err; end
- local sm = require "prosody.core.storagemanager";
- local mm = require "prosody.core.modulemanager";
+ local print = self.session.print;
+
+ if invite.type == "roster" then
+ print("Invitation to register and become a contact of "..invite.jid);
+ elseif invite.type == "register" then
+ local jid_user, jid_host = jid_split(invite.jid);
+ if invite.additional_data and invite.additional_data.allow_reset then
+ print("Password reset for "..invite.additional_data.allow_reset.."@"..jid_host);
+ elseif jid_user then
+ print("Invitation to register on "..jid_host.." with username '"..jid_user.."'");
+ else
+ print("Invitation to register on "..jid_host);
+ end
+ else
+ print("Unknown invitation type");
+ end
- local host = table.remove(arg, 1); -- pop host
- if not host then return help(true) end
- sm.initialize_host(host);
- module.host = host; --luacheck: ignore 122/module
- token_storage = module:open_store("invite_token", "map");
+ if invite.inviter then
+ print("Creator:", invite.inviter);
+ end
- 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 };
- });
+ print("Created:", os.date("%Y-%m-%d %T", invite.created_at));
+ print("Expires:", os.date("%Y-%m-%d %T", invite.expires));
- if opts.help then
- return help();
- end
+ print("");
- -- Load mod_invites
- local invites = module:depends("invites");
- -- Optional community module that if used, needs to be loaded here
- local invites_page_module = module:get_option_string("invites_page_module", "invites_page");
- if mm.get_modules_for_host(host):contains(invites_page_module) then
- module:depends(invites_page_module);
- end
+ if invite.uri then
+ print("XMPP URI:", invite.uri);
+ end
- local allow_reset;
+ if invite.landing_page then
+ print("Web link:", invite.landing_page);
+ end
- 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;
+ if invite.additional_data then
+ print("");
+ if invite.additional_data.roles then
+ if invite.additional_data.roles[1] then
+ print("Role:", invite.additional_data.roles[1]);
+ end
+ if invite.additional_data.roles[2] then
+ print("Secondary roles:", table.concat(invite.additional_data.roles, ", ", 2, #invite.additional_data.roles));
+ end
+ end
+ if invite.additional_data.groups then
+ print("Groups:", table.concat(invite.additional_data.groups, ", "));
+ end
+ if invite.additional_data.note then
+ print("Comment:", invite.additional_data.note);
+ end
end
- allow_reset = username;
- end
- local roles = opts.role or {};
- local groups = opts.groups or {};
+ return true, "Invitation valid";
+ end;
+});
- if opts.admin then
- -- Insert it first since we don't get order out of argparse
- table.insert(roles, 1, "prosody:admin");
- end
+module:add_item("shell-command", {
+ section = "invite";
+ section_desc = "Create and manage invitations";
+ name = "delete";
+ desc = "Delete/revoke an invitation token";
+ args = { { name = "host", type = "string" }, { name = "token", type = "string" } };
+ host_selector = "host";
- local invite;
- if allow_reset then
- if roles[1] then
- print("--role/--admin and --reset are mutually exclusive")
- return 2;
- end
- if #groups > 0 then
- print("--group and --reset are mutually exclusive")
+ handler = function (self, host, token) --luacheck: ignore 212/self 212/host
+ local invite, err = delete_account_invite(token);
+ if not invite then return nil, err; end
+ return true, "Invitation deleted";
+ end;
+});
+
+module:add_item("shell-command", {
+ section = "invite";
+ section_desc = "Create and manage invitations";
+ name = "list";
+ desc = "List pending invitations which allow account registration";
+ args = { { name = "host", type = "string" } };
+ host_selector = "host";
+
+ handler = function (self, host) -- luacheck: ignore 212/host
+ local print_row = human_io.table({
+ {
+ title = "Token";
+ key = "invite";
+ width = 24;
+ mapper = function (invite)
+ return invite.token;
+ end;
+ };
+ {
+ title = "Expires";
+ key = "invite";
+ width = 20;
+ mapper = function (invite)
+ return os.date("%Y-%m-%dT%T", invite.expires);
+ end;
+ };
+ {
+ title = "Description";
+ key = "invite";
+ width = "100%";
+ mapper = function (invite)
+ if invite.type == "roster" then
+ return "Contact with "..invite.jid;
+ elseif invite.type == "register" then
+ local jid_user, jid_host = jid_split(invite.jid);
+ if invite.additional_data and invite.additional_data.allow_reset then
+ return "Password reset for "..invite.additional_data.allow_reset.."@"..jid_host;
+ end
+ if jid_user then
+ return "Register on "..jid_host.." with username "..jid_user;
+ end
+ return "Register on "..jid_host;
+ end
+ end;
+ };
+ }, self.session.width);
+
+ self.session.print(print_row());
+ local count = 0;
+ for _, invite in pending_account_invites() do
+ count = count + 1;
+ self.session.print(print_row({ invite = invite }));
end
- invite = assert(invites.create_account_reset(allow_reset));
- else
- invite = assert(invites.create_account(nil, {
- roles = roles,
- groups = groups
- }, opts.expires_after and human_io.parse_duration(opts.expires_after)));
+ return true, ("%d pending invites"):format(count);
+ end;
+});
+
+local subcommands = {};
+
+--- prosodyctl command
+function module.command(arg)
+ 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
+ return subcommands[cmd](arg);
+end
- print(invite.landing_page or invite.uri);
+function subcommands.generate()
+ print("This command is deprecated. Please see 'prosodyctl shell help invite' for available commands.");
+ return 1;
end
diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua
index b57fc030..4eb49819 100644
--- a/plugins/mod_mam/mod_mam.lua
+++ b/plugins/mod_mam/mod_mam.lua
@@ -341,7 +341,6 @@ local function should_store(stanza) --> boolean, reason: string
end
if stanza:get_child("no-store", "urn:xmpp:hints")
or stanza:get_child("no-permanent-store", "urn:xmpp:hints") then
- -- XXX Experimental XEP
return false, "hint";
end
if stanza:get_child("store", "urn:xmpp:hints") then
@@ -365,7 +364,6 @@ local function should_store(stanza) --> boolean, reason: string
return true, "receipt";
end
if stanza:get_child(nil, "urn:xmpp:chat-markers:0") then
- -- XXX Experimental XEP
return true, "marker";
end
if stanza:get_child("x", "jabber:x:conference")
diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua
index f939fa00..c3d6bc04 100644
--- a/plugins/mod_presence.lua
+++ b/plugins/mod_presence.lua
@@ -54,11 +54,12 @@ function handle_normal_presence(origin, stanza)
if priority < -128 then priority = -128 end
if priority > 127 then priority = 127 end
else priority = 0; end
+
+ local node, host = origin.username, origin.host;
+ local roster = origin.roster;
if full_sessions[origin.full_jid] then -- if user is still connected
origin.send(stanza); -- reflect their presence back to them
end
- local roster = origin.roster;
- local node, host = origin.username, origin.host;
local user = bare_sessions[node.."@"..host];
for _, res in pairs(user and user.sessions or NULL) do -- broadcast to all resources
if res ~= origin and res.presence then -- to resource
@@ -72,6 +73,13 @@ function handle_normal_presence(origin, stanza)
core_post_stanza(origin, stanza, true);
end
end
+
+ -- It's possible that after the network activity above, the origin
+ -- has been disconnected (particularly if something happened while
+ -- sending the reflection). So we abort further presence processing
+ -- in that case.
+ if not origin.type then return; end
+
stanza.attr.to = nil;
if stanza.attr.type == nil and not origin.presence then -- initial presence
module:fire_event("presence/initial", { origin = origin, stanza = stanza } );
diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua
index 8eb1565e..84ae34b5 100644
--- a/plugins/mod_s2s.lua
+++ b/plugins/mod_s2s.lua
@@ -1097,6 +1097,10 @@ module:provides("net", {
-- FIXME This only applies to Direct TLS, which we don't use yet.
-- This gets applied for real in mod_tls
verify = { "peer", "client_once", };
+ verifyext = {
+ "lsec_continue", -- Continue past certificate verification errors
+ "lsec_ignore_purpose", -- Validate client certificates as if they were server certificates
+ };
};
multiplex = {
protocol = "xmpp-server";
@@ -1111,6 +1115,10 @@ module:provides("net", {
encryption = "ssl";
ssl_config = {
verify = { "peer", "client_once", };
+ verifyext = {
+ "lsec_continue", -- Continue past certificate verification errors
+ "lsec_ignore_purpose", -- Validate client certificates as if they were server certificates
+ };
};
multiplex = {
protocol = "xmpp-server";
diff --git a/plugins/mod_storage_internal.lua b/plugins/mod_storage_internal.lua
index a43dd272..1332ae75 100644
--- a/plugins/mod_storage_internal.lua
+++ b/plugins/mod_storage_internal.lua
@@ -4,7 +4,7 @@ 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 uuid_v7 = require "prosody.util.uuid".v7;
local jid_join = require "prosody.util.jid".join;
local set = require "prosody.util.set";
local it = require "prosody.util.iterators";
@@ -111,7 +111,7 @@ function archive:append(username, key, value, when, with)
module:log("debug", "%s reached or over quota, not adding to store", username);
return nil, "quota-limit";
end
- key = id();
+ key = uuid_v7();
end
module:log("debug", "%s has %d items out of %d limit in store %s", username, item_count, archive_item_limit, self.store);
diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua
index fa0588b2..f053f729 100644
--- a/plugins/mod_storage_sql.lua
+++ b/plugins/mod_storage_sql.lua
@@ -42,7 +42,7 @@ end
local function has_upsert(engine)
if engine.params.driver == "SQLite3" then
-- SQLite3 >= 3.24.0
- return (engine.sqlite_version[2] or 0) >= 24;
+ return engine.sqlite_version and (engine.sqlite_version[2] or 0) >= 24;
elseif engine.params.driver == "PostgreSQL" then
-- PostgreSQL >= 9.5
-- Versions without support have long since reached end of life.
@@ -866,38 +866,34 @@ 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
+ 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
+ local success = engine:transaction(function ()
+ return assert(engine:execute([[DROP INDEX "prosody_index";]]));
end);
if not success then
- module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error");
+ module:log("error", "Failed to delete obsolete index \"prosody_index\"");
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
diff --git a/plugins/mod_vcard.lua b/plugins/mod_vcard.lua
index ea6839bb..dfd43363 100644
--- a/plugins/mod_vcard.lua
+++ b/plugins/mod_vcard.lua
@@ -6,13 +6,24 @@
-- COPYING file in the source package for more information.
--
+local base64 = require "prosody.util.encodings".base64;
+local jid = require "prosody.util.jid";
+local sha1 = require "prosody.util.hashes".sha1;
local st = require "prosody.util.stanza"
local jid_split = require "prosody.util.jid".split;
-local vcards = module:open_store();
+local store_name = module:get_option_string("vcard_store_name");
+
+local is_component = module:get_host_type() == "component";
+if is_component and not store_name and module:get_option_string("component_module") == "muc" then
+ store_name = "vcard_muc";
+end
+
+local vcards = module:open_store(store_name);
module:add_feature("vcard-temp");
+
local function handle_vcard(event)
local session, stanza = event.origin, event.stanza;
local to = stanza.attr.to;
@@ -21,7 +32,7 @@ local function handle_vcard(event)
if to then
local node = jid_split(to);
vCard = st.deserialize(vcards:get(node)); -- load vCard for user or server
- else
+ elseif not is_component then
vCard = st.deserialize(vcards:get(session.username));-- load user's own vCard
end
if vCard then
@@ -30,9 +41,11 @@ local function handle_vcard(event)
session.send(st.error_reply(stanza, "cancel", "item-not-found"));
end
else -- stanza.attr.type == "set"
- if not to then
- if vcards:set(session.username, st.preserialize(stanza.tags[1])) then
+ if not to or (is_component and event.allow_vcard_modification) then
+ local node = is_component and jid.node(stanza.attr.to) or session.username;
+ if vcards:set(node, st.preserialize(stanza.tags[1])) then
session.send(st.reply(stanza));
+ module:fire_event("vcard-updated", event);
else
-- TODO unable to write file, file may be locked, etc, what's the correct error?
session.send(st.error_reply(stanza, "wait", "internal-server-error"));
@@ -46,3 +59,14 @@ end
module:hook("iq/bare/vcard-temp:vCard", handle_vcard);
module:hook("iq/host/vcard-temp:vCard", handle_vcard);
+
+function get_avatar_hash(username)
+ local vcard = st.deserialize(vcards:get(username));
+ if not vcard then return end
+ local photo = vcard:get_child("PHOTO");
+ if not photo then return end
+
+ local photo_b64 = photo:get_child_text("BINVAL");
+ local photo_raw = photo_b64 and base64.decode(photo_b64);
+ return (sha1(photo_raw, true));
+end
diff --git a/plugins/mod_websocket.lua b/plugins/mod_websocket.lua
index 7120f3cc..17a91076 100644
--- a/plugins/mod_websocket.lua
+++ b/plugins/mod_websocket.lua
@@ -87,6 +87,7 @@ local function session_close(session, reason)
end
end
end
+ stream_error = tostring(stream_error);
log("debug", "Disconnecting client, <stream:error> is: %s", stream_error);
session.send(stream_error);
end
@@ -94,28 +95,33 @@ local function session_close(session, reason)
session.send(st.stanza("close", { xmlns = xmlns_framing }));
function session.send() return false; end
- -- luacheck: ignore 422/reason
- -- FIXME reason should be handled in common place
- local reason = (reason and (reason.name or reason.text or reason.condition)) or reason;
- session.log("debug", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed");
+ local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason;
+ session.log("debug", "c2s stream for %s closed: %s", session.full_jid or session.ip or "<unknown>", reason_text or "session closed");
-- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
local conn = session.conn;
- if reason == nil and not session.notopen and session.type == "c2s" then
+ if reason_text == nil and not session.notopen and session.type == "c2s" then
-- Grace time to process data from authenticated cleanly-closed stream
add_task(stream_close_timeout, function ()
if not session.destroyed then
session.log("warn", "Failed to receive a stream close response, closing connection anyway...");
- sm_destroy_session(session, reason);
- conn:write(build_close(1000, "Stream closed"));
- conn:close();
+ sm_destroy_session(session, reason_text);
+ if conn then
+ conn:write(build_close(1000, "Stream closed"));
+ conn:close();
+ end
end
end);
else
- sm_destroy_session(session, reason);
- conn:write(build_close(1000, "Stream closed"));
- conn:close();
+ sm_destroy_session(session, reason_text);
+ if conn then
+ conn:write(build_close(1000, "Stream closed"));
+ conn:close();
+ end
end
+ else
+ local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason;
+ sm_destroy_session(session, reason_text);
end
end
@@ -367,6 +373,10 @@ function module.add_host(module)
};
});
+ if module.host ~= "*" then
+ module:depends("http_altconnect", true);
+ end
+
module:hook("c2s-read-timeout", keepalive, -0.9);
end
diff --git a/plugins/muc/hats.lib.lua b/plugins/muc/hats.lib.lua
index 7eb71eb4..7ccf194e 100644
--- a/plugins/muc/hats.lib.lua
+++ b/plugins/muc/hats.lib.lua
@@ -1,7 +1,7 @@
local st = require "prosody.util.stanza";
local muc_util = module:require "muc/util";
-local hats_compat = module:get_option_boolean("muc_hats_compat", true); -- COMPAT for pre-XEP namespace, TODO reconsider default for next release
+local hats_compat = module:get_option_boolean("muc_hats_compat", false); -- COMPAT for pre-XEP namespace
local xmlns_hats_legacy = "xmpp:prosody.im/protocol/hats:1";
local xmlns_hats = "urn:xmpp:hats:0";
diff --git a/plugins/muc/history.lib.lua b/plugins/muc/history.lib.lua
index 005bd1d8..f85decfb 100644
--- a/plugins/muc/history.lib.lua
+++ b/plugins/muc/history.lib.lua
@@ -204,7 +204,6 @@ module:hook("muc-message-is-historic", function (event)
local stanza = event.stanza;
if stanza:get_child("no-store", "urn:xmpp:hints")
or stanza:get_child("no-permanent-store", "urn:xmpp:hints") then
- -- XXX Experimental XEP
return false, "hint";
end
if stanza:get_child("store", "urn:xmpp:hints") then
@@ -219,7 +218,6 @@ module:hook("muc-message-is-historic", function (event)
return true, "encrypted";
end
if stanza:get_child(nil, "urn:xmpp:chat-markers:0") then
- -- XXX Experimental XEP
return true, "marker";
end
end, -1);
diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua
index 1dc99f07..2ce6e19a 100644
--- a/plugins/muc/mod_muc.lua
+++ b/plugins/muc/mod_muc.lua
@@ -116,6 +116,10 @@ module:depends "muc_unique"
module:require "muc/hats";
module:require "muc/lock";
+if module:get_option_boolean("muc_vcard", true) ~= false then
+ module:require "muc/vcard";
+end
+
module:default_permissions("prosody:admin", {
":automatic-ownership";
":create-room";
diff --git a/plugins/muc/vcard.lib.lua b/plugins/muc/vcard.lib.lua
new file mode 100644
index 00000000..f9f97721
--- /dev/null
+++ b/plugins/muc/vcard.lib.lua
@@ -0,0 +1,82 @@
+local mod_vcard = module:depends("vcard");
+
+local jid = require "prosody.util.jid";
+local st = require "prosody.util.stanza";
+
+-- This must be the same event that mod_vcard hooks
+local vcard_event = "iq/bare/vcard-temp:vCard";
+local advertise_hashes = module:get_option("muc_avatar_advertise_hashes");
+
+--luacheck: ignore 113/get_room_from_jid
+
+local function get_avatar_hash(room)
+ if room.avatar_hash then return room.avatar_hash; end
+
+ local room_node = jid.split(room.jid);
+ local hash = mod_vcard.get_avatar_hash(room_node);
+ room.avatar_hash = hash;
+
+ return hash;
+end
+
+local function send_avatar_hash(room, to)
+ local hash = get_avatar_hash(room);
+ if not hash and to then return; end -- Don't announce when no avatar
+
+ local presence_vcard = st.presence({to = to, from = room.jid})
+ :tag("x", { xmlns = "vcard-temp:x:update" })
+ :tag("photo"):text(hash):up();
+
+ if to == nil then
+ if not advertise_hashes or advertise_hashes == "presence" then
+ room:broadcast_message(presence_vcard);
+ end
+ if not advertise_hashes or advertise_hashes == "message" then
+ room:broadcast_message(st.message({ from = room.jid, type = "groupchat" })
+ :tag("x", { xmlns = "http://jabber.org/protocol/muc#user" })
+ :tag("status", { code = "104" }));
+ end
+
+ else
+ module:send(presence_vcard);
+ end
+end
+
+module:hook(vcard_event, function (event)
+ local stanza = event.stanza;
+ local to = stanza.attr.to;
+
+ if stanza.attr.type ~= "set" then
+ return;
+ end
+
+ local room = get_room_from_jid(to);
+ if not room then
+ return;
+ end
+
+ local sender_affiliation = room:get_affiliation(stanza.attr.from);
+ if sender_affiliation == "owner" then
+ event.allow_vcard_modification = true;
+ end
+end, 10);
+
+if advertise_hashes ~= "none" then
+ module:hook("muc-occupant-joined", function (event)
+ send_avatar_hash(event.room, event.stanza.attr.from);
+ end);
+ module:hook("vcard-updated", function (event)
+ local room = get_room_from_jid(event.stanza.attr.to);
+ send_avatar_hash(room, nil);
+ end);
+end
+
+module:hook("muc-disco#info", function (event)
+ event.reply:tag("feature", { var = "vcard-temp" }):up();
+
+ table.insert(event.form, {
+ name = "muc#roominfo_avatarhash",
+ type = "text-multi",
+ });
+ event.formdata["muc#roominfo_avatarhash"] = get_avatar_hash(event.room);
+end);