#!/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 jid_split = require "prosody.util.jid".prepped_split; 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 user, host = jid_split(arg[1]); if not user and host then show_message [[Failed to understand JID, please supply the JID you want to create]] show_usage [[adduser user@host]] return 1; end if not host then show_message [[Please specify a JID, including a host. e.g. alice@example.com]]; return 1; end if not prosody.hosts[host] then show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host) show_warning("The user will not be able to log in until this is changed."); prosody.hosts[host] = startup.make_host(host); --luacheck: ignore 122 end if prosodyctl.user_exists{ user = user, host = host } then show_message [[That user already exists]]; return 1; end local password = read_password(); if not password then return 1; 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.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 user, host = jid_split(arg[1]); if not user and host then show_message [[Failed to understand JID, please supply the JID you want to set the password for]] show_usage [[passwd user@host]] return 1; end if not host then show_message [[Please specify a JID, including a host. e.g. alice@example.com]]; return 1; end if not prosody.hosts[host] then show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host) show_warning("The user will not be able to log in until this is changed."); prosody.hosts[host] = startup.make_host(host); --luacheck: ignore 122 end if not prosodyctl.user_exists { user = user, host = host } then show_message [[That user does not exist, use prosodyctl adduser to create a new user]] return 1; end local password = read_password(); if not password then return 1; end local ok, msg = prosodyctl.passwd { user = user, host = host, password = password }; if ok then return 0; end show_message(error_messages[msg]) return 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 user, host = jid_split(arg[1]); if not user and host then show_message [[Failed to understand JID, please supply the JID to the user account you want to delete]] show_usage [[deluser user@host]] return 1; end if not host then show_message [[Please specify a JID, including a host. e.g. alice@example.com]]; return 1; end if not prosody.hosts[host] then show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host) prosody.hosts[host] = startup.make_host(host); --luacheck: ignore 122 end if not prosodyctl.user_exists { user = user, host = host } then show_message [[That user does not exist on this 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 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("WARNING: Use of prosodyctl start/stop/restart/reload is not recommended"); show_warning(" if Prosody is managed by an init system - use that directly instead."); local init = has_init_system() if init == "systemd" then show_warning(" e.g. systemctl %s prosody", service_command); elseif init == "rc.d" then show_warning(" e.g. /etc/init.d/prosody %s", service_command); end show_warning(""); 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 service_command_warning("start"); 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 service_command_warning("stop"); if not prosodyctl.isrunning() 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(); 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 service_command_warning("restart"); 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.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 if arg[1] and arg[1]:match"^mod_" then -- TODO reword the usage text, document local shell = require "prosody.util.prosodyctl.shell"; arg[1] = arg[1]:match("^mod_(.*)"); -- strip mod_ prefix table.insert(arg, 1, "module"); table.insert(arg, 2, "reload"); return shell.shell(arg); end service_command_warning("reload"); if not prosodyctl.isrunning() then show_message("Prosody is not running"); return 1; 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(" %-11s %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" }; local commands_order = { "Process management:", "start"; "stop"; "restart"; "reload"; "status"; "shell", "User management:", "adduser"; "passwd"; "deluser"; "Plugin management:", "install"; "remove"; "list"; "Informative:", "about", "check", "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);