prosody/prosodyctl
2025-02-06 15:43:23 +00:00

747 lines
21 KiB
Lua
Executable file

#!/usr/bin/env lua
-- 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.
--
-- prosodyctl - command-line controller for Prosody XMPP server
-- Will be modified by configure script if run --
CFG_SOURCEDIR=CFG_SOURCEDIR or os.getenv("PROSODY_SRCDIR");
CFG_CONFIGDIR=CFG_CONFIGDIR or os.getenv("PROSODY_CFGDIR");
CFG_PLUGINDIR=CFG_PLUGINDIR or os.getenv("PROSODY_PLUGINDIR");
CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR");
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
local function is_relative(path)
local path_sep = package.config:sub(1,1);
return ((path_sep == "/" and path:sub(1,1) ~= "/")
or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
end
-- Tell Lua where to find our libraries
if CFG_SOURCEDIR then
local function filter_relative_paths(path)
if is_relative(path) then return ""; end
end
local function sanitise_paths(paths)
return (paths:gsub("[^;]+;?", filter_relative_paths):gsub(";;+", ";"));
end
package.path = sanitise_paths(CFG_SOURCEDIR.."/?.lua;"..package.path);
package.cpath = sanitise_paths(CFG_SOURCEDIR.."/?.so;"..package.cpath);
end
-- Substitute ~ with path to home directory in data path
if CFG_DATADIR then
if os.getenv("HOME") then
CFG_DATADIR = CFG_DATADIR:gsub("^~", os.getenv("HOME"));
end
end
-----------
-- Check before first require, to preempt the probable failure
if _VERSION < "Lua 5.2" then
io.stderr:write("Prosody is no longer compatible with Lua 5.1\n")
io.stderr:write("See https://prosody.im/doc/depends#lua for more information\n")
return os.exit(1);
end
if not pcall(require, "prosody.loader") then
pcall(require, "loader");
end
local startup = require "prosody.util.startup";
startup.prosodyctl();
-----------
local configmanager = require "prosody.core.configmanager";
local modulemanager = require "prosody.core.modulemanager"
local prosodyctl = require "prosody.util.prosodyctl"
local socket = require "socket"
local dependencies = require "prosody.util.dependencies";
local lfs = dependencies.softreq "lfs";
-----------------------
local parse_args = require "prosody.util.argparse".parse;
local human_io = require "prosody.util.human.io";
local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning;
local show_usage = prosodyctl.show_usage;
local read_password = human_io.read_password;
local call_luarocks = prosodyctl.call_luarocks;
local error_messages = prosodyctl.error_messages;
local prosodyctl_timeout = (configmanager.get("*", "prosodyctl_timeout") or 5) * 2;
-----------------------
local commands = {};
local command = table.remove(arg, 1);
local only_help = { short_params = { h = "help"; ["?"] = "help" } }
function commands.install(arg)
local opts = parse_args(arg, only_help);
if opts.help or not arg[1] then
show_usage([[install]], [[Installs a prosody/luarocks plugin]]);
return opts.help and 0 or 1;
end
-- TODO finalize config option name
local server = opts.server or configmanager.get("*", "plugin_server");
if not (arg[1]:match("^https://") or lfs.attributes(arg[1]) or server) then
show_warning("There is no 'plugin_server' option in the configuration file");
-- see https://prosody.im/doc/TODO documentation
-- #1602
return 1;
end
show_message("Installing %s in %s", arg[1], prosody.paths.installer);
local ret = call_luarocks("install", arg[1], server);
if ret == 0 and arg[1]:match("^mod_") then
prosodyctl.show_module_configuration_help(arg[1]);
end
return ret;
end
function commands.remove(arg)
local opts = parse_args(arg, only_help);
if opts.help or not arg[1] then
show_usage([[remove]], [[Removes a module installed in the working directory's plugins folder]]);
return opts.help and 0 or 1;
end
show_message("Removing %s from %s", arg[1], prosody.paths.installer);
local ret = call_luarocks("remove", arg[1]);
return ret;
end
function commands.list(arg)
local opts = parse_args(arg, only_help);
if opts.help then
show_usage([[list]], [[Shows installed rocks]]);
return 0;
end
local server = opts.server or configmanager.get("*", "plugin_server");
if opts.outdated then
-- put this back for luarocks
arg[1] = "--outdated";
if not server then
show_warning("There is no 'plugin_server' option in the configuration file, but this is needed for 'list --outdated' to work.");
return 1;
end
end
local ret = call_luarocks("list", arg[1], server);
return ret;
end
function commands.adduser(arg)
local opts = parse_args(arg, only_help);
if opts.help or not arg[1] then
show_usage([[adduser JID]], [[Create the specified user account in Prosody]]);
return opts.help and 0 or 1;
end
local shell = require "prosody.util.prosodyctl.shell";
return shell.shell({ ("user:create(%q, nil, %q)"):format(arg[1], "prosody:member") });
end
function commands.passwd(arg)
local opts = parse_args(arg, only_help);
if opts.help or not arg[1] then
show_usage([[passwd JID]], [[Set the password for the specified user account in Prosody]]);
return opts.help and 0 or 1;
end
local shell = require "prosody.util.prosodyctl.shell";
return shell.shell({ ("user:password(%q, nil)"):format(arg[1]) });
end
function commands.deluser(arg)
local opts = parse_args(arg, only_help);
if opts.help or not arg[1] then
show_usage([[deluser JID]], [[Permanently remove the specified user account from Prosody]]);
return opts.help and 0 or 1;
end
local shell = require "prosody.util.prosodyctl.shell";
return shell.shell({ ("user:delete(%q)"):format(arg[1]) });
end
local function has_init_system() --> which
lfs = lfs or require"lfs";
if lfs.attributes("/etc/systemd") then
return "systemd";
elseif lfs.attributes("/etc/init.d/prosody") then
return "rc.d";
end
end
local function service_command_warning(service_command)
if prosody.installed and configmanager.get("*", "prosodyctl_service_warnings") ~= false then
show_warning("ERROR: Use of 'prosodyctl %s' is disabled in this installation because", service_command);
local init = has_init_system()
if init then
show_warning(" we detected that this system uses %s for managing services.", init);
show_warning("");
show_warning(" To avoid problems, use that directly instead. For example:");
show_warning("");
if init == "systemd" then
show_warning(" systemctl %s prosody", service_command);
elseif init == "rc.d" then
show_warning(" /etc/init.d/prosody %s", service_command);
end
else
show_warning(" it may conflict with your system's service manager.");
show_warning("");
end
show_warning(" Proceeding to use prosodyctl may cause process management issues.");
show_warning(" You can pass --force to override this warning, or set");
show_warning(" prosodyctl_service_warnings = false in your global config.");
os.exit(1);
end
end
function commands.start(arg)
local opts = parse_args(arg, only_help);
if opts.help then
show_usage([[start]], [[Start Prosody]]);
return 0;
end
if not opts.force then
service_command_warning("start");
end
local ok, ret = prosodyctl.isrunning();
if not ok then
show_message(error_messages[ret]);
return 1;
end
if ret then
--luacheck: ignore 421/ret
local ok, ret = prosodyctl.getpid();
if not ok then
show_message("Couldn't get running Prosody's PID");
show_message(error_messages[ret]);
return 1;
end
show_message("Prosody is already running with PID %s", ret or "(unknown)");
return 1;
end
--luacheck: ignore 411/ret
local lua;
do
local i = 0;
repeat
i = i - 1;
until arg[i-1] == nil
lua = arg[i];
end
local ok, ret = prosodyctl.start(prosody.paths.source, lua);
if ok then
local daemonize = configmanager.get("*", "daemonize");
if daemonize == nil then
daemonize = prosody.installed;
end
if daemonize then
local i=1;
while true do
local ok, running = prosodyctl.isrunning();
if ok and running then
break;
elseif i == 5 then
show_message("Still waiting...");
elseif i >= prosodyctl_timeout then
show_message("Prosody is still not running. Please give it some time or check your log files for errors.");
return 2;
end
socket.sleep(0.5);
i = i + 1;
end
show_message("Started");
end
return 0;
end
show_message("Failed to start Prosody");
show_message(error_messages[ret])
return 1;
end
function commands.status(arg)
local opts = parse_args(arg, only_help);
if opts.help then
show_usage([[status]], [[Reports the running status of Prosody]]);
return 0;
end
local ok, ret = prosodyctl.isrunning();
if not ok then
show_message(error_messages[ret]);
return 1;
end
if ret then
--luacheck: ignore 421/ret
local ok, ret = prosodyctl.getpid();
if not ok then
show_message("Couldn't get running Prosody's PID");
show_message(error_messages[ret]);
return 1;
end
show_message("Prosody is running with PID %s", ret or "(unknown)");
return 0;
else
show_message("Prosody is not running");
if not prosody.switched_user and prosody.current_uid ~= 0 then
print("\nNote:")
print(" You will also see this if prosodyctl is not running under");
print(" the same user account as Prosody. Try running as root (e.g. ");
print(" with 'sudo' in front) to gain access to Prosody's real status.");
end
return 2
end
end
function commands.stop(arg)
local opts = parse_args(arg, only_help);
if opts.help then
show_usage([[stop]], [[Stop a running Prosody server]]);
return 0;
end
if not opts.force then
service_command_warning("stop");
end
local ok, running = prosodyctl.isrunning();
if not ok then
show_message(error_messages[running]);
return 1;
elseif not running then
show_message("Prosody is not running");
return 1;
end
local ok, ret = prosodyctl.stop();
if ok then
local i=1;
while true do
local ok, running = prosodyctl.isrunning(); --luacheck: ignore 421
if ok and not running then
break;
elseif i == 5 then
show_message("Still waiting...");
elseif i >= prosodyctl_timeout then
show_message("Prosody is still running. Please give it some time or check your log files for errors.");
return 2;
end
socket.sleep(0.5);
i = i + 1;
end
show_message("Stopped");
return 0;
end
show_message(error_messages[ret]);
return 1;
end
function commands.restart(arg)
local opts = parse_args(arg, only_help);
if opts.help then
show_usage([[restart]], [[Restart a running Prosody server]]);
return 1;
end
if not opts.force then
service_command_warning("restart");
end
commands.stop(arg);
return commands.start(arg);
end
function commands.about(arg)
local opts = parse_args(arg, only_help);
if opts.help then
show_usage([[about]], [[Show information about this Prosody installation]]);
return 0;
end
local pwd = ".";
local sorted_pairs = require "prosody.util.iterators".sorted_pairs;
local hg = require"prosody.util.mercurial";
local relpath = configmanager.resolve_relative_path;
print("Prosody "..(prosody.version or "(unknown version)"));
print("");
print("# Prosody directories");
print("Data directory: "..relpath(pwd, prosody.paths.data));
print("Config directory: "..relpath(pwd, prosody.paths.config or "."));
print("Source directory: "..relpath(pwd, prosody.paths.source or "."));
print("Plugin directories:")
print(" "..(prosody.paths.plugins:gsub("([^;]+);?", function(path)
path = configmanager.resolve_relative_path(pwd, path);
local hgid, hgrepo = hg.check_id(path);
if not hgid and hgrepo then
return path.." - "..hgrepo .."!\n ";
end
-- 010452cfaf53 is the first commit in the prosody-modules repository
hgrepo = hgrepo == "010452cfaf53" and "prosody-modules";
return path..(hgid and " - "..(hgrepo or "HG").." rev: "..hgid or "")
.."\n ";
end)));
print("");
local have_pposix, pposix = pcall(require, "prosody.util.pposix");
if have_pposix and pposix.uname then
print("# Operating system");
local uname, err = pposix.uname();
print(uname and uname.sysname .. " " .. uname.release or "Unknown POSIX", err or "");
print("");
end
print("# Lua environment");
print("Lua version: ", _G._VERSION);
print("");
print("Lua module search paths:");
for path in package.path:gmatch("[^;]+") do
print(" "..path);
end
print("");
print("Lua C module search paths:");
for path in package.cpath:gmatch("[^;]+") do
print(" "..path);
end
print("");
local luarocks_status = "Not installed"
if pcall(require, "luarocks.loader") then
luarocks_status = "Installed (2.x+)";
if package.loaded["luarocks.cfg"] then
luarocks_status = "Installed ("..(package.loaded["luarocks.cfg"].program_version or "2.x+")..")";
end
elseif pcall(require, "luarocks.require") then
luarocks_status = "Installed (1.x)";
end
print("LuaRocks: ", luarocks_status);
print("");
print("# Network");
print("");
print("Backend: "..require "prosody.net.server".get_backend());
print("");
print("# Lua module versions");
local module_versions, longest_name = {}, 8;
local library_versions = {};
dependencies.softreq"ssl";
dependencies.softreq"DBI";
dependencies.softreq"readline";
local friendly_names = {
DBI = "LuaDBI";
lfs = "LuaFileSystem";
lunbound = "luaunbound";
lxp = "LuaExpat";
socket = "LuaSocket";
ssl = "LuaSec";
};
local alternate_version_fields = {
-- These diverge from the module._VERSION convention
readline = "Version";
}
local lunbound = dependencies.softreq"lunbound";
local lxp = dependencies.softreq"lxp";
local hashes = dependencies.softreq"prosody.util.hashes";
for name, module in pairs(package.loaded) do
local version_field = alternate_version_fields[name] or "_VERSION";
if type(module) == "table" and rawget(module, version_field)
and name ~= "_G" and not name:match("%.") then
name = friendly_names[name] or name;
if #name > longest_name then
longest_name = #name;
end
local mod_version = module[version_field];
if tostring(mod_version):sub(1, #name+1) == name .. " " then
mod_version = mod_version:sub(#name+2);
end
module_versions[name] = mod_version;
end
end
if lunbound then
if not module_versions["luaunbound"] then
module_versions["luaunbound"] = "0.5 (?)";
end
library_versions["libunbound"] = lunbound._LIBVER;
end
if lxp then
library_versions["libexpat"] = lxp._EXPAT_VERSION;
end
if hashes then
library_versions["libcrypto"] = hashes._LIBCRYPTO_VERSION;
end
for name, version in sorted_pairs(module_versions) do
print(name..":"..string.rep(" ", longest_name-#name), version);
end
print("");
print("# library versions");
if require "prosody.net.server".event_base then
library_versions["libevent"] = require"luaevent".core.libevent_version();
end
for name, version in sorted_pairs(library_versions) do
print(name..":"..string.rep(" ", longest_name-#name), version);
end
print("");
end
function commands.version(arg)
local flags = { short_params = { h = "help"; ["?"] = "help", v = "verbose" } };
local opts = parse_args(arg, flags);
if opts.help then
show_usage("version [-v]", [[Show current Prosody version, or more]]);
return 0;
elseif opts.verbose then
return commands.about(arg);
end
print("Prosody "..(prosody.version or "(unknown version)"));
end
function commands.lua_paths()
local function shell_escape(s)
return "'" .. tostring(s):gsub("'",[['\'']]) .. "'";
end
print("LUA_PATH="..shell_escape(package.path));
print("LUA_CPATH="..shell_escape(package.cpath));
end
function commands.reload(arg)
local opts = parse_args(arg, only_help);
if opts.help then
show_usage([[reload]], [[Reload Prosody's configuration and re-open log files]]);
return 0;
end
local shell = require "prosody.util.prosodyctl.shell";
if shell.available() then
if arg[1] and arg[1]:match"^mod_" then
-- TODO reword the usage text, document
arg[1] = arg[1]:match("^mod_(.*)"); -- strip mod_ prefix
table.insert(arg, 1, "module");
table.insert(arg, 2, "reload");
return shell.shell(arg);
end
return shell.shell({ "config", "reload" });
elseif arg[1] then
show_message("Admin socket not found - is it enabled and is Prosody running?");
return 1;
end
local ok, running = prosodyctl.isrunning();
if not ok then
show_message(error_messages[running]);
return 1;
elseif not running then
show_message("Prosody is not running");
return 1;
end
if not opts.force then
service_command_warning("reload");
end
local ok, ret = prosodyctl.reload();
if ok then
show_message("Prosody log files re-opened and config file reloaded. You may need to reload modules for some changes to take effect.");
return 0;
end
show_message(error_messages[ret]);
return 1;
end
-- ejabberdctl compatibility
local unpack = table.unpack;
function commands.register(arg)
local user, host, password = unpack(arg);
if (not (user and host)) or arg[1] == "--help" then
if user ~= "--help" then
if not user then
show_message [[No username specified]]
elseif not host then
show_message [[Please specify which host you want to register the user on]];
end
end
show_usage("register USER HOST [PASSWORD]", "Register a user on the server, with the given password");
return 1;
end
if not password then
password = read_password();
if not password then
show_message [[Unable to register user with no password]];
return 1;
end
end
local ok, msg = prosodyctl.adduser { user = user, host = host, password = password };
if ok then return 0; end
show_message(error_messages[msg])
return 1;
end
function commands.unregister(arg)
local user, host = unpack(arg);
if (not (user and host)) or arg[1] == "--help" then
if user ~= "--help" then
if not user then
show_message [[No username specified]]
elseif not host then
show_message [[Please specify which host you want to unregister the user from]];
end
end
show_usage("unregister USER HOST [PASSWORD]", "Permanently remove a user account from the server");
return 1;
end
local ok, msg = prosodyctl.deluser { user = user, host = host };
if ok then return 0; end
show_message(error_messages[msg])
return 1;
end
---------------------
local async = require "prosody.util.async";
local server = require "prosody.net.server";
local watchers = {
error = function (_, err)
error(err);
end;
waiting = function ()
server.loop();
end;
};
local command_runner = async.runner(function ()
if command and command:match("^mod_") then -- Is a command in a module
local module_name = command:match("^mod_(.+)");
do
local ret, err = modulemanager.load("*", module_name);
if not ret then
show_message("Failed to load module '"..module_name.."': "..err);
os.exit(1);
end
end
local module = modulemanager.get_module("*", module_name);
if not module then
show_message("Failed to load module '"..module_name.."': Unknown error");
os.exit(1);
end
if not modulemanager.module_has_method(module, "command") then
show_message("Fail: mod_"..module_name.." does not support any commands");
os.exit(1);
end
local ok, ret = modulemanager.call_module_method(module, "command", arg);
if ok then
if type(ret) == "number" then
os.exit(ret, true);
elseif type(ret) == "string" then
show_message(ret);
end
os.exit(0, true); -- :)
else
show_message("Failed to execute command: %s", error_messages[ret]);
os.exit(1); -- :(
end
end
if command and not commands[command] then
local ok, command_module = pcall(require, "prosody.util.prosodyctl."..command);
if ok and command_module[command] then
commands[command] = command_module[command];
end
end
if not commands[command] then -- Show help for all commands
function show_usage(usage, desc)
print(string.format(" %-14s %s", usage, desc));
end
print("prosodyctl - Manage a Prosody server");
print("");
print("Usage: "..arg[0].." COMMAND [OPTIONS]");
print("");
print("Where COMMAND may be one of:");
local hidden_commands = require "prosody.util.set".new{ "register", "unregister", "lua_paths" };
local commands_order = {
"Process management:",
"start"; "stop"; "restart"; "reload"; "status";
"shell",
"User management:",
"adduser"; "passwd"; "deluser";
"Plugin management:",
"install"; "remove"; "list";
"Informative:",
"check",
"version",
"Other:",
"cert",
};
-- These live in util.prosodyctl.$command so we have their short help here.
local external_commands = {
cert = "Certificate management commands",
check = "Perform basic checks on your Prosody installation",
shell = "Interact with a running Prosody",
}
local done = {};
if prosody.installed and has_init_system() then
-- Hide start, stop, restart
done[table.remove(commands_order, 2)] = true;
done[table.remove(commands_order, 2)] = true;
done[table.remove(commands_order, 2)] = true;
end
for _, command_name in ipairs(commands_order) do
local command_func = commands[command_name];
if command_func then
command_func{ "--help" };
done[command_name] = true;
elseif external_commands[command_name] then
show_usage(command_name, external_commands[command_name]);
done[command_name] = true;
else
print""
print(command_name);
end
end
for command_name, command_func in pairs(commands) do
if not done[command_name] and not hidden_commands:contains(command_name) then
command_func{ "--help" };
done[command_name] = true;
end
end
os.exit(0, true);
end
os.exit(commands[command](arg), true);
end, watchers);
command_runner:run(true);