aboutsummaryrefslogtreecommitdiffstats
path: root/plugins/mod_admin_shell.lua
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/mod_admin_shell.lua')
-rw-r--r--plugins/mod_admin_shell.lua1244
1 files changed, 956 insertions, 288 deletions
diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua
index f2da286b..d085ce43 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,19 +967,27 @@ 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 session.cert_chain_errors 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
return "Unknown";
end;
@@ -846,7 +995,7 @@ available_columns = {
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 +1005,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 +1016,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 +1051,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 +1065,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 +1082,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 +1093,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 +1134,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 +1148,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 +1160,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 +1172,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 +1181,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 +1239,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 +1273,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 +1362,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 +1377,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 +1401,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 +1455,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 +1478,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 +1497,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 +1523,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 +1547,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 +1716,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 +1732,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 +1816,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 +1866,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 +1882,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 +1940,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 +1962,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 +2050,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 +2107,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 +2409,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 +2432,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
-------------