-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: ignore 212/self module:set_global(); module:depends("admin_socket"); 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 parse_args = require "prosody.util.argparse".parse; local _G = _G; local prosody = _G.prosody; local unpack = table.unpack; local cache = require "prosody.util.cache"; local new_short_id = require "prosody.util.id".short; local iterators = require "prosody.util.iterators"; local keys, values = iterators.keys, iterators.values; 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 "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 "prosody.util.human.units".format; local format_table = require "prosody.util.human.io".table; local function capitalize(s) if not s then return end return (s:gsub("^%a", string.upper):gsub("_", " ")); end local function pre(prefix, str, alt) if type(str) ~= "string" or str == "" then return alt or ""; end return prefix .. str; end local function suf(str, suffix, alt) if type(str) ~= "string" or str == "" then return alt or ""; end return str .. suffix; end 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. 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 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) local f, err = envloadfile(name, env); if not f then return f, err; end return f(); end; return env; end console = {}; local runner_callbacks = {}; function runner_callbacks:error(err) module:log("error", "Traceback[shell]: %s", err); self.data.print("Fatal error while running command, it did not complete"); self.data.print("Error: "..tostring(err)); end local function send_repl_output(session, line, attr) return session.send(st.stanza("repl-output", attr):text(tostring(line))); end local function request_repl_input(session, input_type) if input_type ~= "password" then return promise.reject("internal error - unsupported input type "..tostring(input_type)); end local pending_inputs = session.pending_inputs; if not pending_inputs then pending_inputs = cache.new(5, function (input_id, input_promise) --luacheck: ignore 212/input_id input_promise.reject(); end); session.pending_inputs = pending_inputs; end local input_id = new_short_id(); local p = promise.new(function (resolve, reject) pending_inputs:set(input_id, { resolve = resolve, reject = reject }); end):finally(function () pending_inputs:set(input_id, nil); end); session.send(st.stanza("repl-request-input", { type = input_type, id = input_id })); return p; end module:hook("admin-disconnected", function (event) local pending_inputs = event.session.pending_inputs; if not pending_inputs then return; end for input_promise in pending_inputs:values() do input_promise.reject(); end end); module:hook("admin/repl-requested-input", function (event) local input_id = event.stanza.attr.id; local input_promise = event.origin.pending_inputs:get(input_id); if not input_promise then event.origin.send(st.stanza("repl-result", { type = "error" }):text("Internal error - unexpected input")); return true; end input_promise.resolve(event.stanza:get_text()); return true; end); function console:new_session(admin_session) local session = { send = function (t) return send_repl_output(admin_session, t); end; print = function (...) local t = {}; for i=1,select("#", ...) do t[i] = tostring(select(i, ...)); 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; request_input = function (input_type) return request_repl_input(admin_session, input_type); 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); session.thread = async.runner(function (line) console:process_line(session, line); end, runner_callbacks, session); -- Load up environment with helper objects for name, t in pairs(def_env) do if type(t) == "table" then session.env[name] = setmetatable({ session = session }, { __index = t }); end end session.env.output:configure(); 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 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; session.repl = event.stanza.attr.repl ~= "0"; local result = st.stanza("repl-result"); if line:match("^>") then line = line:gsub("^>", ""); useglobalenv = true; else 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); return; end end session.env._ = line; if not useglobalenv and commands[line:lower()] then commands[line:lower()](session, line); event.origin.send(result); return; end if useglobalenv and not session.globalenv then session.globalenv = redirect_output(_G, session); end local function send_result(taskok, message) if not message then if type(taskok) ~= "string" and useglobalenv then taskok = session.serialize(taskok); end result:text("Result: "..tostring(taskok)); elseif (not taskok) and message then result.attr.type = "error"; result:text("Error: "..tostring(message)); else result:text("OK: "..tostring(message)); end event.origin.send(result); end 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("''", "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) send_result(true, resolved_message); end, function (rejected_message) send_result(nil, rejected_message); end); else send_result(taskok, message); end end module:hook("admin/repl-input", function (event) local ok, err = pcall(handle_line, event); if not ok then event.origin.send(st.stanza("repl-result", { type = "error" }):text(err)); end return true; end); 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); 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); 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 -- Help about individual topics is handled by def_env.help function commands.help(session, data) local print = session.print; 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 -- Session environment -- -- 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", { preset = "pretty"; maxdepth = 2; table_iterator = "pairs"; }) def_env.output = new_section("Configure admin console output"); function def_env.output:configure(opts) if type(opts) ~= "table" then opts = { preset = opts }; end if not opts.fallback then -- XXX Error message passed to fallback is lost, does it matter? opts.fallback = tostring; end for k,v in pairs(serialize_defaults) do if opts[k] == nil then opts[k] = v; end end if opts.table_iterator == "pairs" then opts.table_iterator = pairs; elseif type(opts.table_iterator) ~= "function" then opts.table_iterator = nil; -- rawpairs is the default end self.session.serialize = serialization.new(opts); end 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 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]; 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(); dofile "prosody" prosody = _G.prosody; 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; t = (t - seconds)/60; local minutes = t%60; t = (t - minutes)/60; local hours = t%24; t = (t - hours)/24; local days = t; return true, string.format("This server has been running for %d day%s, %d hour%s and %d minute%s (since %s)", days, (days ~= 1 and "s") or "", hours, (hours ~= 1 and "s") or "", 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"; end 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")); end local mem, lua_mem = pposix.meminfo(), collectgarbage("count"); local print = self.session.print; print("Process: "..human((mem.allocated+mem.allocated_mmap)/1024)); print(" Used: "..human(mem.used/1024).." ("..human(lua_mem).." by Lua)"); print(" Free: "..human(mem.unused/1024).." ("..human(mem.returnable/1024).." returnable)"); return true, "OK"; end def_env.module = new_section("Commands to load/reload/unload modules/plugins"); local function get_hosts_set(hosts) if type(hosts) == "table" then if hosts[1] then return set.new(hosts); elseif hosts._items then return hosts; end elseif type(hosts) == "string" then return set.new { hosts }; elseif hosts == nil then return set.new(array.collect(keys(prosody.hosts))); end end -- Hosts with a module or all virtualhosts if no module given -- matching modules_enabled in the global section local function get_hosts_with_module(hosts, module) local hosts_set = get_hosts_set(hosts) / function (host) if module then -- Module given, filter in hosts with this module loaded if modulemanager.is_loaded(host, module) then return host; else return nil; end end if not hosts then -- No hosts given, filter in VirtualHosts if prosody.hosts[host].type == "local" then return host; else return nil end end; -- No module given, but hosts are, don't filter at all return host; end; if module and modulemanager.get_module("*", module) then hosts_set:add("*"); end 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"; end local print = self.session.print; hosts = get_hosts_with_module(hosts, name); if hosts:empty() then return false, "mod_" .. name .. " does not appear to be loaded on the specified hosts"; end 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", ["http-provider"] = "HTTP services", ["net-provider"] = "Network service", ["storage-provider"] = "Storage driver", ["measure"] = "Legacy metrics", ["metric"] = "Metrics", ["task"] = "Periodic task", }; local item_formatters = { ["feature"] = tostring, ["identity"] = function(ident) return ident.type .. "/" .. ident.category; end, ["adhoc-provider"] = item_name, ["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"] = 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, %s)", item.name or item.id, item.when, task_timefmt(item.last)); end }; for host in hosts do local mod = modulemanager.get_module(host, name); if mod.module.host == "*" then print("in global context"); else print("on " .. tostring(prosody.hosts[mod.module.host])); end print(" path: " .. (mod.module.path or "n/a")); if mod.module.status_message then print(" status: [" .. mod.module.status_type .. "] " .. mod.module.status_message); end if mod.module.items and next(mod.module.items) ~= nil then print(" provides:"); for kind, items in pairs(mod.module.items) do local label = friendly_descriptions[kind] or kind:gsub("%-", " "):gsub("^%a", string.upper); print(string.format(" - %s (%d item%s)", label, #items, #items > 1 and "s" or "")); local formatter = item_formatters[kind]; if formatter then for _, item in ipairs(items) do print(" - " .. formatter(item, mod.module)); end end end end if mod.module.dependencies and next(mod.module.dependencies) ~= nil then print(" dependencies:"); for dep in pairs(mod.module.dependencies) do -- 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 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); local already_loaded = set.new(); -- 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); if not mod then ok = false; if err == "global-module-already-loaded" then if count > 0 then ok, err, count = true, nil, 1; end break; end self.session.print(err or "Unknown error loading module"); 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 else already_loaded:add(host); end end if not ok then return ok, "Last error: "..tostring(err); end if already_loaded == hosts then return ok, "Module already loaded"; end return ok, "Module loaded onto "..count.." host"..(count ~= 1 and "s" or ""); end describe_command [[module:unload(module, host) - The same, but just unloads the module from memory]] 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 ok = false; self.session.print(err or "Unknown error unloading module"); 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 return ok, (ok and "Module unloaded from "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); end local function _sort_hosts(a, b) if a == "*" then return true elseif b == "*" then return false 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) -- Reload the module for each host local ok, err, count = true, nil, 0; for _, host in ipairs(hosts) do if modulemanager.is_loaded(host, name) then ok, err = modulemanager.reload(host, name); if not ok then ok = false; self.session.print(err or "Unknown error reloading module"); else count = count + 1; if ok == nil then ok = true; end self.session.print("Reloaded on "..host); end end end 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); local print = self.session.print; for _, host in ipairs(hosts) do print((host == "*" and "Global" or host)..":"); local modules = array.collect(keys(modulemanager.get_modules(host) or {})):sort(); if #modules == 0 then if prosody.hosts[host] then print(" No modules loaded"); else print(" Host not found"); end else for _, name in ipairs(modules) do local status, status_text = modulemanager.get_module(host, name).module:get_status(); local status_summary = ""; if status == "warn" or status == "error" then status_summary = (" (%s: %s)"):format(status, status_text); end print((" %s%s"):format(name, status_summary)); end end end end def_env.config = new_section("Reloading the configuration, etc."); function def_env.config:load(filename, format) 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"; end 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 "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 = new_section("Commands to manage local client-to-server sessions"); local function get_jid(session) if session.username then return session.full_jid or jid_join(session.username, session.host, session.resource); end local conn = session.conn; local ip = session.ip or "?"; local clientport = conn and conn:clientport() or "?"; local serverip = conn and conn.server and conn:server():ip() or "?"; local serverport = conn and conn:serverport() or "?" return jid_join("["..ip.."]:"..clientport, session.host or "["..serverip.."]:"..serverport); end local function get_c2s() local c2s = array.collect(values(prosody.full_sessions)); c2s:append(array.collect(values(module:shared"/*/c2s/sessions"))); c2s:append(array.collect(values(module:shared"/*/bosh/sessions"))); c2s:unique(); return c2s; end local function _sort_by_jid(a, b) if a.host == b.host then if a.username == b.username then return (a.resource or "") > (b.resource or ""); end return (a.username or "") > (b.username or ""); end return _sort_hosts(a.host or "", b.host or ""); end local function show_c2s(callback) get_c2s():sort(_sort_by_jid):map(function (session) callback(get_jid(session), session) 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"; end local function get_s2s_hosts(session) --> local,remote if session.direction == "outgoing" then return session.host or session.from_host, session.to_host; elseif session.direction == "incoming" then return session.host or session.to_host, session.from_host; end end available_columns = { jid = { title = "JID"; description = "Full JID of user session"; width = "3p"; key = "full_jid"; mapper = function(full_jid, session) return full_jid or get_jid(session) end; }; host = { title = "Host"; description = "Local hostname"; key = "host"; width = "1p"; mapper = function(host, session) return host or get_s2s_hosts(session) or "?"; end; }; remote = { title = "Remote"; description = "Remote hostname"; width = "1p"; mapper = function(_, session) return select(2, get_s2s_hosts(session)); end; }; port = { title = "Port"; description = "Server port used"; width = #string.format("%d", 0xffff); -- max 16 bit unsigned integer align = "right"; key = "conn"; mapper = function(conn) if conn then return conn:serverport(); 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 = #"<->"; key = "direction"; mapper = function(dir, session) if session.incoming and session.outgoing then return "<->"; end if dir == "outgoing" then return "-->"; end if dir == "incoming" then return "<--"; end end; }; 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 = math.max(#"BOSH", #"WebSocket", #"TCP"); mapper = function(_, session) if session.bosh_version then return "BOSH"; elseif session.websocket_request then return "WebSocket"; else return "TCP"; end end; }; ipv = { title = "IPv"; description = "Internet Protocol version (4 or 6)"; 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 = 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 = math.max(#"online", #"chat"); key = "presence"; mapper = function(p) if not p then return ""; end return p:get_child_text("show") or "online"; end; }; secure = { title = "Security"; description = "TLS version or security status"; key = "conn"; 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 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)"; -- 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 info = conn and conn.ssl_info and conn:ssl_info(); if info then return info.cipher end end; }; cert = { title = "Certificate"; description = "Validation status of certificate"; key = "cert_identity_status"; width = math.max(#"Expired", #"Self-signed", #"Untrusted", #"Mismatched", #"Unknown"); mapper = function(cert_status, session) if cert_status == "invalid" then -- non-nil cert_identity_status implies valid chain, which covers just -- about every error condition except mismatched certificate names return "Mismatched"; elseif cert_status then -- basically only "valid" return capitalize(cert_status); end -- no certificate status, if type(session.cert_chain_errors) == "table" then local cert_errors = set.new(session.cert_chain_errors[1]); if cert_errors:contains("certificate has expired") then return "Expired"; elseif cert_errors:contains("self signed certificate") then return "Self-signed"; end -- Some other cert issue, or something up the chain -- TODO borrow more logic from mod_s2s/friendly_cert_error() return "Untrusted"; end -- TODO cert_chain_errors can be a string, handle that return "Unknown"; end; }; sni = { title = "SNI"; description = "Hostname requested in TLS"; width = "1p"; -- same as host, remote etc mapper = function(_, session) if not session.conn then return end local sock = session.conn:socket(); return sock and sock.getsniname and sock:getsniname() or ""; end; }; alpn = { title = "ALPN"; description = "Protocol requested in TLS"; 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(); return sock and sock.getalpn and sock:getalpn() or ""; end; }; smacks = { title = "SM"; description = "Stream Management (XEP-0198) status"; key = "smacks"; -- 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 return "yes"; end; }; smacks_queue = { title = "SM Queue"; description = "Length of Stream Management stanza queue"; key = "outgoing_stanza_queue"; width = 8; align = "right"; mapper = function (queue) return queue and tostring(queue:count_unacked()); end }; csi = { title = "CSI State"; description = "Client State Indication (XEP-0352)"; key = "state"; -- TODO include counter }; s2s_sasl = { title = "SASL"; description = "Server authentication status"; key = "external_auth"; width = 10; mapper = capitalize }; dialback = { title = "Dialback"; description = "Legacy server verification"; key = "dialback_key"; 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 return "Not used"; end return "Not initiated"; elseif session.type == "s2sin_unauthed" or session.type == "s2sout_unauthed" then return "Initiated"; else return "Completed"; 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) if type(colspec) == "string" then colspec = array(colspec:gmatch("%S+")); end 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 = "1p"; key = col }; elseif type(col) ~= "table" then return false, ("argument %d: expected string|table but got %s"):format(i, type(col)); else columns[i] = col; end end 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"; "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 match_jid == "*" or jid_compare(jid, match_jid); end local group_by_host = true; for _, col in ipairs(columns) do if col.key == "full_jid" or col.key == "host" then group_by_host = false; break end end if not group_by_host then print(row()); end local currenthost = nil; local c2s_sessions = get_c2s(); local total_count = #c2s_sessions; c2s_sessions:filter(match):sort(_sort_by_jid); local shown_count = #c2s_sessions; for _, session in ipairs(c2s_sessions) do if group_by_host and session.host ~= currenthost then currenthost = session.host; print("#",prosody.hosts[currenthost] or "Unknown host"); print(row()); end print(row(session)); end if total_count ~= shown_count then return true, ("%d out of %d c2s sessions shown"):format(shown_count, total_count); end 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 local function build_reason(text, condition) if text or condition then return { text = text, condition = condition or "undefined-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) if jid == match_jid or jid_bare(jid) == match_jid then count = count + 1; session:close(build_reason(text, condition)); end end); 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 show_c2s(function (jid, session) count = count + 1; session:close(build_reason(text, condition)); end); return true, "Total: "..count.." sessions closed"; end 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); if (a_local or "") == (b_local or "") then return _sort_hosts(a_remote or "", b_remote or ""); end 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, self.session.width); local function match(session) return match_s2s_jid(session, match_jid); end local group_by_host = true; local currenthost = nil; for _, col in ipairs(columns) do if col.key == "host" then group_by_host = false; break end end if not group_by_host then print(row()); end local s2s_sessions = array(iterators.values(module:shared"/*/s2s/sessions")); local total_count = #s2s_sessions; s2s_sessions:filter(match):sort(_sort_s2s); local shown_count = #s2s_sessions; for _, session in ipairs(s2s_sessions) do if group_by_host and currenthost ~= get_s2s_hosts(session) then currenthost = get_s2s_hosts(session); print("#",prosody.hosts[currenthost] or "Unknown host"); print(row()); end print(row(session)); end if total_count ~= shown_count then return true, ("%d out of %d s2s connections shown"):format(shown_count, total_count); end 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 local function print_subject(print, subject) for _, entry in ipairs(subject) do print( (" %s: %q"):format( entry.name or entry.oid, entry.value:gsub("[\r\n%z%c]", " ") ) ); end end -- As much as it pains me to use the 0-based depths that OpenSSL does, -- I think there's going to be more confusion among operators if we -- break from that. local function print_errors(print, errors) for depth, t in pairs(errors) do print( (" %d: %s"):format( depth-1, table.concat(t, "\n| ") ) ); end end 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 match_s2s_jid(session, domain) and session or nil; end; local cert_set = {}; for session in domain_sessions do local conn = session.conn; conn = conn and conn:socket(); if not conn.getpeerchain then if conn.dohandshake then error("This version of LuaSec does not support certificate viewing"); end else local cert = conn:getpeercertificate(); if cert then local certs = conn:getpeerchain(); local digest = cert:digest("sha1"); if not cert_set[digest] then local chain_valid, chain_errors = conn:getpeerverification(); cert_set[digest] = { { from = session.from_host, to = session.to_host, direction = session.direction }; chain_valid = chain_valid; chain_errors = chain_errors; certs = certs; }; else table.insert(cert_set[digest], { from = session.from_host, to = session.to_host, direction = session.direction }); end end end end local domain_certs = array.collect(values(cert_set)); -- Phew. We now have a array of unique certificates presented by domain. local n_certs = #domain_certs; if n_certs == 0 then return "No certificates found for "..domain; end local function _capitalize_and_colon(byte) return string.upper(byte)..":"; end local function pretty_fingerprint(hash) return hash:gsub("..", _capitalize_and_colon):sub(1, -2); end for cert_info in values(domain_certs) do local certs = cert_info.certs; local cert = certs[1]; print("---") print("Fingerprint (SHA1): "..pretty_fingerprint(cert:digest("sha1"))); print(""); local n_streams = #cert_info; print("Currently used on "..n_streams.." stream"..(n_streams==1 and "" or "s")..":"); for _, stream in ipairs(cert_info) do if stream.direction == "incoming" then print(" "..stream.to.." <- "..stream.from); else print(" "..stream.from.." -> "..stream.to); end end print(""); local chain_valid, errors = cert_info.chain_valid, cert_info.chain_errors; local valid_identity = cert_verify_identity(domain, "xmpp-server", cert); if chain_valid then print("Trusted certificate: Yes"); else print("Trusted certificate: No"); print_errors(print, errors); end print(""); print("Issuer: "); print_subject(print, cert:issuer()); print(""); print("Valid for "..domain..": "..(valid_identity and "Yes" or "No")); print("Subject:"); print_subject(print, cert:subject()); end print("---"); return ("Showing "..n_certs.." certificate" ..(n_certs==1 and "" or "s") .." 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"; local match_id; if from and not to then match_id, from = from, nil; elseif not to then return false, "Syntax: s2s:close('from', 'to') - Closes all s2s sessions from 'from' to 'to'"; elseif from == to then return false, "Both from and to are the same... you can't do that :)"; 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 ((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; 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 host == "*" or match_s2s_jid(session, host) then session:close(build_reason(text, condition)); count = count + 1; end end if count == 0 then return false, "No sessions to close."; else return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end end 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 host_type; for host, host_session in iterators.sorted_pairs(prosody.hosts, _sort_hosts) do i = i + 1; host_type = host_session.type; if host_type == "local" then print(host); else 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, host_type)); end end return true, i.." hosts"; end 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; local n_services, n_ports = 0, 0; for service, interfaces in iterators.sorted_pairs(services) do n_services = n_services + 1; local ports_list = {}; for interface, ports in pairs(interfaces) do for port in pairs(ports) do table.insert(ports_list, "["..interface.."]:"..port); end end n_ports = n_ports + #ports_list; print(service..": "..table.concat(ports_list, ", ")); end 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; local services = portmanager.get_active_services().data; for service, interfaces in pairs(services) do -- luacheck: ignore 213 for interface, ports in pairs(interfaces) do if not close_interface or close_interface == interface then if ports[close_port] then self.session.print("Closing ["..interface.."]:"..close_port.."..."); local ok, err = portmanager.close(interface, close_port) if not ok then self.session.print("Failed to close "..interface.." "..close_port..": "..err); else n_closed = n_closed + 1; end end end end end return true, "Closed "..n_closed.." ports"; end 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; __tostring = function (self) return "MUC room <"..self.room.jid..">"; end; }; local function check_muc(jid) local room_name, host = jid_split(jid); if not prosody.hosts[host] then return nil, "No such host: "..host; elseif not prosody.hosts[host].modules.muc then return nil, "Host '"..host.."' is not a MUC service"; end 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 return room_name, host; end if not room_name then return nil, host end if config ~= nil and type(config) ~= "table" then return nil, "Config must be a table"; end if prosody.hosts[host].modules.muc.get_room_from_jid(room_jid) then return nil, "Room exists already" end 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_obj, err = get_muc(room_jid); if not room_obj then 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 return nil, "Please supply the address of a local MUC component"; end local print = self.session.print; local c = 0; for room in host_session.modules.muc.each_room() do print(room.jid); c = c + 1; end return true, c.." rooms"; end 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 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 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 if not role then role = module:get_option_string("default_provisioned_role", "prosody:member"); end return promise.resolve(password or self.session.request_input("password")):next(function (password_) local ok, err = um.create_user_with_role(username, password_, host, role); if not ok then return promise.reject("Could not create user: "..err); end return ("Created %s with role '%s'"):format(jid, role); end); 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 return true, "User disabled"; else 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 return nil, "No such host: "..host; elseif not um.user_exists(username, host) then return nil, "No such user"; end local ok, err = um.delete_user(username, host); if ok then return true, "User deleted"; else return nil, "Could not delete user: "..err; 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 return nil, "No such host: "..host; elseif not um.user_exists(username, host) then return nil, "No such user"; end return promise.resolve(password or self.session.request_input("password")):next(function (password_) local ok, err = um.set_password(username, password_, host, nil); if ok then return "User password changed"; else return promise.reject("Could not change password for user: "..err); end end); end describe_command [[user:role(jid, host) - Show primary role for a user]] function def_env.user:role(jid, host) local username, userhost = jid_split(jid); if host == nil then host = userhost; 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 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 ""; print(primary_role and primary_role.name or ""); local n_secondary = 0; for role_name in pairs(secondary_roles or {}) do n_secondary = n_secondary + 1; print(role_name.." (secondary)"); end 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: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 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 userhost == host then return um.set_user_role(username, userhost, new_role); else return um.set_jid_role(jid, host, new_role); end end 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 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"; elseif userhost ~= host then return nil, "Can't add roles outside users own host" end 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 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 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"; elseif userhost ~= host then return nil, "Can't remove roles outside users own host" end 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]] -- TODO switch to table view, include roles function def_env.user:list(host, pat) if not host then return nil, "No host given"; elseif not prosody.hosts[host] then return nil, "No such host"; end local print = self.session.print; local total, matches = 0, 0; for user in um.users(host) do if not pat or user:match(pat) then print(user.."@"..host); matches = matches + 1; end total = total + 1; end return true, "Showing "..(pat and (matches.." of ") or "all " )..total.." users"; end def_env.xmpp = new_section("Commands for sending XMPP stanzas"); 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)); if not localhost then return nil, "Invalid sender hostname"; elseif not prosody.hosts[localhost] then return nil, "No such local host"; end if not remotehost then return nil, "Invalid destination hostname"; elseif prosody.hosts[remotehost] then return nil, "Both hosts are local"; end local iq = st.iq{ from=localhost, to=remotehost, type="get", id=new_id()} :tag("ping", {xmlns="urn:xmpp:ping"}); local time_start = time.now(); local print = self.session.print; local function onchange(what) return function(event) local s2s_session = event.session; if (s2s_session.from_host == localhost and s2s_session.to_host == remotehost) or (s2s_session.to_host == localhost and s2s_session.from_host == remotehost) then local dir = available_columns.dir.mapper(s2s_session.direction, s2s_session); print(("Session %s (%s%s%s) %s (%gs)"):format(s2s_session.id, localhost, dir, remotehost, what, time.now() - time_start)); elseif s2s_session.type == "s2sin_unauthed" and s2s_session.to_host == nil and s2s_session.from_host == nil then print(("Session %s %s (%gs)"):format(s2s_session.id, what, time.now() - time_start)); end end end local onconnected = onchange("connected"); local onauthenticated = onchange("authenticated"); local onestablished = onchange("established"); local ondestroyed = onchange("destroyed"); module:hook("s2s-connected", onconnected, 1); module:context(localhost):hook("s2s-authenticated", onauthenticated, 1); module:hook("s2sout-established", onestablished, 1); module:hook("s2sin-established", onestablished, 1); module:hook("s2s-destroyed", ondestroyed, 1); return module:context(localhost):send_iq(iq, nil, timeout):finally(function() module:unhook("s2s-connected", onconnected, 1); module:context(localhost):unhook("s2s-authenticated", onauthenticated); module:unhook("s2sout-established", onestablished); module:unhook("s2sin-established", onestablished); module:unhook("s2s-destroyed", ondestroyed); end):next(function(pong) 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 = 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; if not resolver then resolver = adns.resolver(); session.dns_resolver = resolver; end 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 = 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_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); if #http_apps > 0 then local http_host = module:context(host):get_option_string("http_host"); if host == "*" then print("Global HTTP endpoints available on all hosts:"); else print("HTTP endpoints on "..host..(http_host and (" (using "..http_host.."):") or ":")); end print(output_split()); for _, provider in ipairs(http_apps) do local mod = provider._provided_by; 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((internal=="" and output_simple or output_split){mod, external, internal}); end print(""); end end local default_host = module:get_option_string("http_default_host"); if not default_host then print("HTTP requests to unknown hosts will return 404 Not Found"); else print("HTTP requests to unknown hosts will be handled by "..default_host); end return true; end 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"):format(session.id)); elseif event_type == "received" then self.session.print(("\n"):format(session.id)); else self.session.print(("\n"):format(event_type, session.id)); end self.session.print(stanza); elseif session then self.session.print("\n"); elseif event_type then self.session.print("\n"); 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) 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 "prosody.net.http.server"._events; elseif not prosody.hosts[host] then return false, "Unknown host: "..host; else events_obj = prosody.hosts[host].events; end else events_obj = prosody.events; end 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"prosody.util.timer".add_task; local h, params = add_task.h, add_task.params; local function normalize_time(t) return t; end local function format_time(t) return os.date("%F %T", math.floor(normalize_time(t))); end if h then print("-- util.timer"); elseif server.timer then print("-- net.server.timer"); h = server.timer.add_task.timers; normalize_time = server.timer.to_absolute_time or normalize_time; end if h then local timers = {}; for i, id in ipairs(h.ids) do local t, cb = h.priorities[i], h.items[id]; if not params then local param = cb.param; if param then cb = param.callback; else cb = cb.timer_callback or cb; end elseif params[id] then cb = params[id].callback or cb; end table.insert(timers, { format_time(t), cb }); end table.sort(timers, function (a, b) return a[1] < b[1] end); for _, t in ipairs(timers) do print(t[1], t[2]) end end if server.event_base then local count = 0; for _, v in pairs(debug.getregistry()) do if type(v) == "function" and v.callback and v.callback == add_task._on_timer then count = count + 1; end end print(count .. " libevent callbacks"); end if h then local next_time = h:peek(); if next_time then return true, ("Next event at %s (in %.6fs)"):format(format_time(next_time), normalize_time(next_time) - time.now()); end end return true; end 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 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", bytes = "B", }; local stats_methods = {}; function stats_methods:render_single_fancy_histogram_ex(print, prefix, metric_family, metric, cumulative) local creation_timestamp, sum, count local buckets = {} local prev_bucket_count = 0 for suffix, extra_labels, value in metric:iter_samples() do if suffix == "_created" then creation_timestamp = value elseif suffix == "_sum" then sum = value elseif suffix == "_count" then count = value elseif extra_labels then local bucket_threshold = extra_labels["le"] local bucket_count if cumulative then bucket_count = value else bucket_count = value - prev_bucket_count prev_bucket_count = value end if bucket_threshold == "+Inf" then t_insert(buckets, {threshold = 1/0, count = bucket_count}) elseif bucket_threshold ~= nil then t_insert(buckets, {threshold = tonumber(bucket_threshold), count = bucket_count}) end end end if #buckets == 0 or not creation_timestamp or not sum or not count then print("[no data or not a histogram]") return false end local graph_width, graph_height, wscale = #buckets, 10, 1; if graph_width < 8 then wscale = 8 elseif graph_width < 16 then wscale = 4 elseif graph_width < 32 then wscale = 2 end local eighth_chars = " ▁▂▃▄▅▆▇█"; local max_bin_samples = 0 for _, bucket in ipairs(buckets) do if bucket.count > max_bin_samples then max_bin_samples = bucket.count end end print(""); print(prefix) print(("_"):rep(graph_width*wscale).." "..max_bin_samples); for row = graph_height, 1, -1 do local row_chars = {}; local min_eighths, max_eighths = 8, 0; for i = 1, #buckets do local char_eighths = math.ceil(math.max(math.min((graph_height/(max_bin_samples/buckets[i].count))-(row-1), 1), 0)*8); if char_eighths < min_eighths then min_eighths = char_eighths; end if char_eighths > max_eighths then max_eighths = char_eighths; end if char_eighths == 0 then row_chars[i] = ("-"):rep(wscale); else local char = eighth_chars:sub(char_eighths*3+1, char_eighths*3+3); row_chars[i] = char:rep(wscale); end end print(table.concat(row_chars).."|- "..string.format("%.8g", math.ceil((max_bin_samples/graph_height)*(row-0.5)))); end local legend_pat = string.format("%%%d.%dg", wscale-1, wscale-1) local row = {} for i = 1, #buckets do local threshold = buckets[i].threshold t_insert(row, legend_pat:format(threshold)) end t_insert(row, " " .. metric_family.unit) print(t_concat(row, "/")) return true end function stats_methods:render_single_fancy_histogram(print, prefix, metric_family, metric) return self:render_single_fancy_histogram_ex(print, prefix, metric_family, metric, false) end function stats_methods:render_single_fancy_histogram_cf(print, prefix, metric_family, metric) -- cf = cumulative frequency return self:render_single_fancy_histogram_ex(print, prefix, metric_family, metric, true) end function stats_methods:cfgraph() for _, stat_info in ipairs(self) do local family_name, metric_family = unpack(stat_info, 1, 2) local function print(s) table.insert(stat_info.output, s); end if not self:render_family(print, family_name, metric_family, self.render_single_fancy_histogram_cf) then return self end end return self; end function stats_methods:histogram() for _, stat_info in ipairs(self) do local family_name, metric_family = unpack(stat_info, 1, 2) local function print(s) table.insert(stat_info.output, s); end if not self:render_family(print, family_name, metric_family, self.render_single_fancy_histogram) then return self end end return self; end function stats_methods:render_single_counter(print, prefix, metric_family, metric) local created_timestamp, current_value for suffix, _, value in metric:iter_samples() do if suffix == "_created" then created_timestamp = value elseif suffix == "_total" then current_value = value end end if current_value and created_timestamp then local base_unit = short_units[metric_family.unit] or metric_family.unit local unit = base_unit .. "/s" local factor = 1 if base_unit == "s" then -- be smart! unit = "%" factor = 100 elseif base_unit == "" then unit = "events/s" end print(("%-50s %s"):format(prefix, format_number(factor * current_value / (self.now - created_timestamp), unit.." [avg]"))); end end function stats_methods:render_single_gauge(print, prefix, metric_family, metric) local current_value for _, _, value in metric:iter_samples() do current_value = value end if current_value then local unit = short_units[metric_family.unit] or metric_family.unit print(("%-50s %s"):format(prefix, format_number(current_value, unit))); end end function stats_methods:render_single_summary(print, prefix, metric_family, metric) local sum, count for suffix, _, value in metric:iter_samples() do if suffix == "_sum" then sum = value elseif suffix == "_count" then count = value end end if sum and count then local unit = short_units[metric_family.unit] or metric_family.unit if count == 0 then print(("%-50s %s"):format(prefix, "no obs.")); else print(("%-50s %s"):format(prefix, format_number(sum / count, unit.."/event [avg]"))); end end end function stats_methods:render_family(print, family_name, metric_family, render_func) local labelkeys = metric_family.label_keys if #labelkeys > 0 then print(family_name) for labelset, metric in metric_family:iter_metrics() do local labels = {} for i, k in ipairs(labelkeys) do local v = labelset[i] t_insert(labels, ("%s=%s"):format(k, v)) end local prefix = " "..t_concat(labels, " ") render_func(self, print, prefix, metric_family, metric) end else for _, metric in metric_family:iter_metrics() do render_func(self, print, family_name, metric_family, metric) end end end local function stats_tostring(stats) local print = stats.session.print; for _, stat_info in ipairs(stats) do if #stat_info.output > 0 then print("\n#"..stat_info[1]); print(""); for _, v in ipairs(stat_info.output) do print(v); end print(""); else local metric_family = stat_info[2] if metric_family.type_ == "counter" then stats:render_family(print, stat_info[1], metric_family, stats.render_single_counter) elseif metric_family.type_ == "gauge" or metric_family.type_ == "unknown" then stats:render_family(print, stat_info[1], metric_family, stats.render_single_gauge) elseif metric_family.type_ == "summary" or metric_family.type_ == "histogram" then stats:render_family(print, stat_info[1], metric_family, stats.render_single_summary) end end end return #stats.." statistics displayed"; end local stats_mt = {__index = stats_methods, __tostring = stats_tostring } local function new_stats_context(self) -- TODO: instead of now(), it might be better to take the time of the last -- interval, if the statistics backend is set to use periodic collection -- Otherwise we get strange stuff like average cpu usage decreasing until -- the next sample and so on. 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 "prosody.core.statsmanager" local metric_registry = statsman.get_metric_registry(); if not metric_registry then return nil, [[Statistics disabled. Try `statistics = "internal"` in the global section of the config file and restart.]]; end local collect = statsman.collect if collect then -- force collection if in manual mode collect() end local displayed_stats = new_stats_context(self); for family_name, metric_family in iterators.sorted_pairs(metric_registry:get_metric_families()) do if not name_filter or family_name:match(name_filter) then table.insert(displayed_stats, { family_name, metric_family, output = {} }) end end 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 = event.source and ("mod_"..event.source.name) or ""; 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 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; }; module:log("debug", "Shell command added by %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 ------------- function printbanner(session) local option = module:get_option_string("console_banner", "full"); if option == "full" or option == "graphic" then session.print [[ ____ \ / _ | _ \ _ __ ___ ___ _-_ __| |_ _ | |_) | '__/ _ \/ __|/ _ \ / _` | | | | | __/| | | (_) \__ \ |_| | (_| | |_| | |_| |_| \___/|___/\___/ \__,_|\__, | A study in simplicity |___/ ]] end if option == "short" or option == "full" then session.print("Welcome to the Prosody administration console. For a list of commands, type: help"); session.print("You may find more help on using this console in our online documentation at "); session.print("https://prosody.im/doc/console\n"); end if option ~= "short" and option ~= "full" and option ~= "graphic" then session.print(option); end end