#!/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 ----------- local startup = require "util.startup"; startup.prosodyctl(); ----------- local error_messages = setmetatable({ ["invalid-username"] = "The given username is invalid in a Jabber ID"; ["invalid-hostname"] = "The given hostname is invalid"; ["no-password"] = "No password was supplied"; ["no-such-user"] = "The given user does not exist on the server"; ["no-such-host"] = "The given hostname does not exist in the config"; ["unable-to-save-data"] = "Unable to store, perhaps you don't have permission?"; ["no-pidfile"] = "There is no 'pidfile' option in the configuration file, see https://prosody.im/doc/prosodyctl#pidfile for help"; ["invalid-pidfile"] = "The 'pidfile' option in the configuration file is not a string, see https://prosody.im/doc/prosodyctl#pidfile for help"; ["no-posix"] = "The mod_posix module is not enabled in the Prosody config file, see https://prosody.im/doc/prosodyctl for more info"; ["no-such-method"] = "This module has no commands"; ["not-running"] = "Prosody is not running"; }, { __index = function (_,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end }); local configmanager = require "core.configmanager"; local modulemanager = require "core.modulemanager" local prosodyctl = require "util.prosodyctl" local socket = require "socket" local dependencies = require "util.dependencies"; ----------------------- local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning; local show_usage = prosodyctl.show_usage; local show_yesno = prosodyctl.show_yesno; local show_prompt = prosodyctl.show_prompt; local read_password = prosodyctl.read_password; local show_module_configuration_help = prosodyctl.show_module_configuration_help; local jid_split = require "util.jid".prepped_split; local prosodyctl_timeout = (configmanager.get("*", "prosodyctl_timeout") or 5) * 2; ----------------------- local commands = {}; local command = table.remove(arg, 1); -- Command to remove a rockspec -- Receives as an argument the name of the plugin to be removed from the plugins folder function commands.remove(arg) if arg[1] == "--help" then show_usage([[make]], [[Removes a module installed in the wroking directory's plugins folder]]); return 1; end local flag = "--tree=" -- I'm considering the flag is the first, but there can be many flags if arg[1] and arg[1]:sub(1, #flag) == flag then local dir = arg[1]:match("=(.+)$") print("Removing module "..arg[2].." at "..dir) -- These extra double brackets allow us to correctly process names with spaces os.execute("luarocks remove --tree='"..dir.."' "..arg[2]) return 0; else print("Removing "..arg[1].." from "..prosody.paths.plugins) os.execute("luarocks --tree='"..prosody.paths.plugins.."' remove "..arg[1]) print("Done!") return 0; end end function commands.install(arg) if arg[1] == "--help" then show_usage([[make]], [[Installs a prosody/luarocks plugin]]); return 1; end -- Need to think about the case with many flags local flag = "--tree=" -- I'm considering the flag is the first, but there can be many flags if arg[1] and arg[1]:sub(1, #flag) == flag then local dir = arg[1]:match("=(.+)$") print("Installing module "..arg[2].." at "..dir) -- These extra double brackets allow us to correctly process names with spaces os.execute("luarocks --tree='"..dir.."' --server='http://localhost/' install "..arg[2]) show_module_configuration_help(arg[2]); return 0; else print("Installing module "..arg[1].." at "..prosody.paths.plugins) -- I've build a local server to upload some new rockspecs, like mod_smacks'. We can replace this server by one from -- prosody's, where we can oficially disbrute rocks/rockspecs for all modules os.execute("luarocks --tree='"..prosody.paths.plugins.."' --server='http://localhost/' install "..arg[1]) show_module_configuration_help(arg[1]); return 0; end end function commands.list(arg) if arg[1] == "--help" then show_usage([[list]], [[Shows installed rocks]]); return 1; end -- Need to think about the case with many flags local flag = "--tree=" -- I'm considering the flag is the first, but there can be many flags if arg[1] and arg[1]:sub(1, #flag) == flag then local dir = arg[1]:match("=(.+)$") -- These extra double brackets allow us to correctly process names with spaces os.execute("luarocks list --tree='"..dir.."'") return 0; else os.execute("luarocks list --tree="..prosody.paths.plugins) return 0; end end function commands.enabled_plugins(arg) if arg[1] == "--help" then show_usage([[enabled_plugins]], [[Shows plugins currently enabled on prosody]]); return 1; end for module in modulemanager.get_modules_for_host() do show_warning("%s", module) end end function commands.local_plugins(arg) if arg[1] == "--help" then show_usage([[local_plugins]], [[Shows plugins currently available for prosody, locally]]); return 1; end local directory = "./plugins" local i, t, popen = 0, {}, io.popen local pfile = popen('ls -a "'..directory..'"') for filename in pfile:lines() do if filename == "." or filename == ".." then i = i + 1 else i = i + 1 t[i] = filename show_warning("%s", t[i]) end end pfile:close() return 0; end function commands.adduser(arg) if not arg[1] or arg[1] == "--help" then show_usage([[adduser JID]], [[Create the specified user account in Prosody]]); return 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(msg) return 1; end function commands.passwd(arg) if not arg[1] or arg[1] == "--help" then show_usage([[passwd JID]], [[Set the password for the specified user account in Prosody]]); return 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) if not arg[1] or arg[1] == "--help" then show_usage([[deluser JID]], [[Permanently remove the specified user account from Prosody]]); return 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 function commands.start(arg) if arg[1] == "--help" then show_usage([[start]], [[Start Prosody]]); return 1; 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 ok, ret = prosodyctl.start(prosody.paths.source, arg[-1]); 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) if arg[1] == "--help" then show_usage([[status]], [[Reports the running status of Prosody]]); return 1; 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) if arg[1] == "--help" then show_usage([[stop]], [[Stop a running Prosody server]]); return 1; end 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) if arg[1] == "--help" then show_usage([[restart]], [[Restart a running Prosody server]]); return 1; end commands.stop(arg); return commands.start(arg); end function commands.about(arg) if arg[1] == "--help" then show_usage([[about]], [[Show information about this Prosody installation]]); return 1; end local pwd = "."; local array = require "util.array"; local keys = require "util.iterators".keys; local hg = require"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, "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 "net.server".get_backend()); print(""); print("# Lua module versions"); local module_versions, longest_name = {}, 8; local luaevent =dependencies.softreq"luaevent"; dependencies.softreq"ssl"; dependencies.softreq"DBI"; for name, module in pairs(package.loaded) do if type(module) == "table" and rawget(module, "_VERSION") and name ~= "_G" and not name:match("%.") then if #name > longest_name then longest_name = #name; end module_versions[name] = module._VERSION; end end if luaevent then module_versions["libevent"] = luaevent.core.libevent_version(); end local sorted_keys = array.collect(keys(module_versions)):sort(); for _, name in ipairs(sorted_keys) do print(name..":"..string.rep(" ", longest_name-#name), module_versions[name]); end print(""); end function commands.reload(arg) if arg[1] == "--help" then show_usage([[reload]], [[Reload Prosody's configuration and re-open log files]]); return 1; end 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 or unpack; -- luacheck: ignore 113 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 openssl; local lfs; local cert_commands = {}; -- If a file already exists, ask if the user wants to use it or replace it -- Backups the old file if replaced local function use_existing(filename) local attrs = lfs.attributes(filename); if attrs then if show_yesno(filename .. " exists, do you want to replace it? [y/n]") then local backup = filename..".bkp~"..os.date("%FT%T", attrs.change); os.rename(filename, backup); show_message(filename.." backed up to "..backup); else -- Use the existing file return true; end end end local have_pposix, pposix = pcall(require, "util.pposix"); local cert_basedir = prosody.paths.data == "." and "./certs" or prosody.paths.data; if have_pposix and pposix.getuid() == 0 then -- FIXME should be enough to check if this directory is writable local cert_dir = configmanager.get("*", "certificates") or "certs"; cert_basedir = configmanager.resolve_relative_path(prosody.paths.config, cert_dir); end function cert_commands.config(arg) if #arg >= 1 and arg[1] ~= "--help" then local conf_filename = cert_basedir .. "/" .. arg[1] .. ".cnf"; if use_existing(conf_filename) then return nil, conf_filename; end local distinguished_name; if arg[#arg]:find("^/") then distinguished_name = table.remove(arg); end local conf = openssl.config.new(); conf:from_prosody(prosody.hosts, configmanager, arg); if distinguished_name then local dn = {}; for k, v in distinguished_name:gmatch("/([^=/]+)=([^/]+)") do table.insert(dn, k); dn[k] = v; end conf.distinguished_name = dn; else show_message("Please provide details to include in the certificate config file."); show_message("Leave the field empty to use the default value or '.' to exclude the field.") for _, k in ipairs(openssl._DN_order) do local v = conf.distinguished_name[k]; if v then local nv = nil; if k == "commonName" then v = arg[1] elseif k == "emailAddress" then v = "xmpp@" .. arg[1]; elseif k == "countryName" then local tld = arg[1]:match"%.([a-z]+)$"; if tld and #tld == 2 and tld ~= "uk" then v = tld:upper(); end end nv = show_prompt(("%s (%s):"):format(k, nv or v)); nv = (not nv or nv == "") and v or nv; if nv:find"[\192-\252][\128-\191]+" then conf.req.string_mask = "utf8only" end conf.distinguished_name[k] = nv ~= "." and nv or nil; end end end local conf_file, err = io.open(conf_filename, "w"); if not conf_file then show_warning("Could not open OpenSSL config file for writing"); show_warning(err); os.exit(1); end conf_file:write(conf:serialize()); conf_file:close(); print(""); show_message("Config written to " .. conf_filename); return nil, conf_filename; else show_usage("cert config HOSTNAME [HOSTNAME+]", "Builds a certificate config file covering the supplied hostname(s)") end end function cert_commands.key(arg) if #arg >= 1 and arg[1] ~= "--help" then local key_filename = cert_basedir .. "/" .. arg[1] .. ".key"; if use_existing(key_filename) then return nil, key_filename; end os.remove(key_filename); -- This file, if it exists is unlikely to have write permissions local key_size = tonumber(arg[2] or show_prompt("Choose key size (2048):") or 2048); local old_umask = pposix.umask("0377"); if openssl.genrsa{out=key_filename, key_size} then os.execute(("chmod 400 '%s'"):format(key_filename)); show_message("Key written to ".. key_filename); pposix.umask(old_umask); return nil, key_filename; end show_message("There was a problem, see OpenSSL output"); else show_usage("cert key HOSTNAME ", "Generates a RSA key named HOSTNAME.key\n " .."Prompts for a key size if none given") end end function cert_commands.request(arg) if #arg >= 1 and arg[1] ~= "--help" then local req_filename = cert_basedir .. "/" .. arg[1] .. ".req"; if use_existing(req_filename) then return nil, req_filename; end local _, key_filename = cert_commands.key({arg[1]}); local _, conf_filename = cert_commands.config(arg); if openssl.req{new=true, key=key_filename, utf8=true, sha256=true, config=conf_filename, out=req_filename} then show_message("Certificate request written to ".. req_filename); else show_message("There was a problem, see OpenSSL output"); end else show_usage("cert request HOSTNAME [HOSTNAME+]", "Generates a certificate request for the supplied hostname(s)") end end function cert_commands.generate(arg) if #arg >= 1 and arg[1] ~= "--help" then local cert_filename = cert_basedir .. "/" .. arg[1] .. ".crt"; if use_existing(cert_filename) then return nil, cert_filename; end local _, key_filename = cert_commands.key({arg[1]}); local _, conf_filename = cert_commands.config(arg); if key_filename and conf_filename and cert_filename and openssl.req{new=true, x509=true, nodes=true, key=key_filename, days=365, sha256=true, utf8=true, config=conf_filename, out=cert_filename} then show_message("Certificate written to ".. cert_filename); print(); else show_message("There was a problem, see OpenSSL output"); end else show_usage("cert generate HOSTNAME [HOSTNAME+]", "Generates a self-signed certificate for the current hostname(s)") end end local function sh_esc(s) return "'" .. s:gsub("'", "'\\''") .. "'"; end local function copy(from, to, umask, owner, group) local old_umask = umask and pposix.umask(umask); local attrs = lfs.attributes(to); if attrs then -- Move old file out of the way local backup = to..".bkp~"..os.date("%FT%T", attrs.change); os.rename(to, backup); end -- FIXME friendlier error handling, maybe move above backup back? local input = assert(io.open(from)); local output = assert(io.open(to, "w")); local data = input:read(2^11); while data and output:write(data) do data = input:read(2^11); end assert(input:close()); assert(output:close()); if not prosody.installed then -- FIXME this is possibly specific to GNU chown os.execute(("chown -c --reference=%s %s"):format(sh_esc(cert_basedir), sh_esc(to))); elseif owner and group then local ok = os.execute(("chown %s:%s %s"):format(sh_esc(owner), sh_esc(group), sh_esc(to))); assert(ok == true or ok == 0, "Failed to change ownership of "..to); end if old_umask then pposix.umask(old_umask); end return true; end function cert_commands.import(arg) local hostnames = {}; -- Move hostname arguments out of arg, the rest should be a list of paths while arg[1] and prosody.hosts[ arg[1] ] do table.insert(hostnames, table.remove(arg, 1)); end if hostnames[1] == nil then local domains = os.getenv"RENEWED_DOMAINS"; -- Set if invoked via certbot if domains then for host in domains:gmatch("%S+") do table.insert(hostnames, host); end else for host in pairs(prosody.hosts) do if host ~= "*" and configmanager.get(host, "enabled") ~= false then table.insert(hostnames, host); end end end end if not arg[1] or arg[1] == "--help" then -- Probably forgot the path show_usage("cert import [HOSTNAME+] /path/to/certs [/other/paths/]+", "Copies certificates to "..cert_basedir); return 1; end local owner, group; if pposix.getuid() == 0 then -- We need root to change ownership owner = configmanager.get("*", "prosody_user") or "prosody"; group = configmanager.get("*", "prosody_group") or owner; end local cm = require "core.certmanager"; local imported = {}; for _, host in ipairs(hostnames) do for _, dir in ipairs(arg) do local paths = cm.find_cert(dir, host); if paths then copy(paths.certificate, cert_basedir .. "/" .. host .. ".crt", nil, owner, group); copy(paths.key, cert_basedir .. "/" .. host .. ".key", "0377", owner, group); table.insert(imported, host); else -- TODO Say where we looked show_warning("No certificate for host "..host.." found :("); end -- TODO Additional checks -- Certificate names matches the hostname -- Private key matches public key in certificate end end if imported[1] then show_message("Imported certificate and key for hosts "..table.concat(imported, ", ")); local ok, err = prosodyctl.reload(); if not ok and err ~= "not-running" then show_message(error_messages[err]); end else show_warning("No certificates imported :("); return 1; end end function commands.cert(arg) if #arg >= 1 and arg[1] ~= "--help" then openssl = require "util.openssl"; lfs = require "lfs"; local cert_dir_attrs = lfs.attributes(cert_basedir); if not cert_dir_attrs then show_warning("The directory "..cert_basedir.." does not exist"); return 1; -- TODO Should we create it? end local uid = pposix.getuid(); if uid ~= 0 and uid ~= cert_dir_attrs.uid then show_warning("The directory "..cert_basedir.." is not owned by the current user, won't be able to write files to it"); return 1; elseif not cert_dir_attrs.permissions then -- COMPAT with LuaFilesystem < 1.6.2 (hey CentOS!) show_message("Unable to check permissions on "..cert_basedir.." (LuaFilesystem 1.6.2+ required)"); show_message("Please confirm that Prosody (and only Prosody) can write to this directory)"); elseif cert_dir_attrs.permissions:match("^%.w..%-..%-.$") then show_warning("The directory "..cert_basedir.." not only writable by its owner"); return 1; end local subcmd = table.remove(arg, 1); if type(cert_commands[subcmd]) == "function" then if subcmd ~= "import" then -- hostnames are optional for import if not arg[1] then show_message"You need to supply at least one hostname" arg = { "--help" }; end if arg[1] ~= "--help" and not prosody.hosts[arg[1]] then show_message(error_messages["no-such-host"]); return 1; end end return cert_commands[subcmd](arg); elseif subcmd == "check" then return commands.check({"certs"}); end end show_usage("cert config|request|generate|key|import", "Helpers for generating X.509 certificates and keys.") for _, cmd in pairs(cert_commands) do print() cmd{ "--help" } end end function commands.check(arg) if arg[1] == "--help" then show_usage([[check]], [[Perform basic checks on your Prosody installation]]); return 1; end local what = table.remove(arg, 1); local set = require "util.set"; local it = require "util.iterators"; local ok = true; local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end local function enabled_hosts() return it.filter(disabled_hosts, pairs(configmanager.getconfig())); end if not (what == nil or what == "disabled" or what == "config" or what == "dns" or what == "certs") then show_warning("Don't know how to check '%s'. Try one of 'config', 'dns', 'certs' or 'disabled'.", what); return 1; end if not what or what == "disabled" then local disabled_hosts_set = set.new(); for host, host_options in it.filter("*", pairs(configmanager.getconfig())) do if host_options.enabled == false then disabled_hosts_set:add(host); end end if not disabled_hosts_set:empty() then local msg = "Checks will be skipped for these disabled hosts: %s"; if what then msg = "These hosts are disabled: %s"; end show_warning(msg, tostring(disabled_hosts_set)); if what then return 0; end print"" end end if not what or what == "config" then print("Checking config..."); local deprecated = set.new({ "bosh_ports", "disallow_s2s", "no_daemonize", "anonymous_login", "require_encryption", "vcard_compatibility", "cross_domain_bosh", "cross_domain_websocket" }); local known_global_options = set.new({ "pidfile", "log", "plugin_paths", "prosody_user", "prosody_group", "daemonize", "umask", "prosodyctl_timeout", "use_ipv6", "use_libevent", "network_settings", "network_backend", "http_default_host", "statistics_interval", "statistics", "statistics_config", }); local config = configmanager.getconfig(); -- Check that we have any global options (caused by putting a host at the top) if it.count(it.filter("log", pairs(config["*"]))) == 0 then ok = false; print(""); print(" No global options defined. Perhaps you have put a host definition at the top") print(" of the config file? They should be at the bottom, see https://prosody.im/doc/configure#overview"); end if it.count(enabled_hosts()) == 0 then ok = false; print(""); if it.count(it.filter("*", pairs(config))) == 0 then print(" No hosts are defined, please add at least one VirtualHost section") elseif config["*"]["enabled"] == false then print(" No hosts are enabled. Remove enabled = false from the global section or put enabled = true under at least one VirtualHost section") else print(" All hosts are disabled. Remove enabled = false from at least one VirtualHost section") end end if not config["*"].modules_enabled then print(" No global modules_enabled is set?"); local suggested_global_modules; for host, options in enabled_hosts() do --luacheck: ignore 213/host if not options.component_module and options.modules_enabled then suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled)); end end if suggested_global_modules and not suggested_global_modules:empty() then print(" Consider moving these modules into modules_enabled in the global section:") print(" "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end)); end print(); end do -- Check for modules enabled both normally and as components local modules = set.new(config["*"]["modules_enabled"]); for host, options in enabled_hosts() do local component_module = options.component_module; if component_module and modules:contains(component_module) then print((" mod_%s is enabled both in modules_enabled and as Component %q %q"):format(component_module, host, component_module)); print(" This means the service is enabled on all VirtualHosts as well as the Component."); print(" Are you sure this what you want? It may cause unexpected behaviour."); end end end -- Check for global options under hosts local global_options = set.new(it.to_array(it.keys(config["*"]))); local deprecated_global_options = set.intersection(global_options, deprecated); if not deprecated_global_options:empty() then print(""); print(" You have some deprecated options in the global section:"); print(" "..tostring(deprecated_global_options)) ok = false; end for host, options in it.filter(function (h) return h ~= "*" end, pairs(configmanager.getconfig())) do local host_options = set.new(it.to_array(it.keys(options))); local misplaced_options = set.intersection(host_options, known_global_options); for name in pairs(options) do if name:match("^interfaces?") or name:match("_ports?$") or name:match("_interfaces?$") or (name:match("_ssl$") and not name:match("^[cs]2s_ssl$")) then misplaced_options:add(name); end end if not misplaced_options:empty() then ok = false; print(""); local n = it.count(misplaced_options); print(" You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be"); print(" in the global section of the config file, above any VirtualHost or Component definitions,") print(" see https://prosody.im/doc/configure#overview for more information.") print(""); print(" You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", ")); end end for host, options in enabled_hosts() do local host_options = set.new(it.to_array(it.keys(options))); local subdomain = host:match("^[^.]+"); if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp" or subdomain == "chat" or subdomain == "im") then print(""); print(" Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to"); print(" "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host.."."); print(" For more information see: https://prosody.im/doc/dns"); end end local all_modules = set.new(config["*"].modules_enabled); local all_options = set.new(it.to_array(it.keys(config["*"]))); for host in enabled_hosts() do all_options:include(set.new(it.to_array(it.keys(config[host])))); all_modules:include(set.new(config[host].modules_enabled)); end for mod in all_modules do if mod:match("^mod_") then print(""); print(" Modules in modules_enabled should not have the 'mod_' prefix included."); print(" Change '"..mod.."' to '"..mod:match("^mod_(.*)").."'."); elseif mod:match("^auth_") then print(""); print(" Authentication modules should not be added to modules_enabled,"); print(" but be specified in the 'authentication' option."); print(" Remove '"..mod.."' from modules_enabled and instead add"); print(" authentication = '"..mod:match("^auth_(.*)").."'"); print(" For more information see https://prosody.im/doc/authentication"); elseif mod:match("^storage_") then print(""); print(" storage modules should not be added to modules_enabled,"); print(" but be specified in the 'storage' option."); print(" Remove '"..mod.."' from modules_enabled and instead add"); print(" storage = '"..mod:match("^storage_(.*)").."'"); print(" For more information see https://prosody.im/doc/storage"); end end for host, host_config in pairs(config) do --luacheck: ignore 213/host if type(rawget(host_config, "storage")) == "string" and rawget(host_config, "default_storage") then print(""); print(" The 'default_storage' option is not needed if 'storage' is set to a string."); break; end end local require_encryption = set.intersection(all_options, set.new({ "require_encryption", "c2s_require_encryption", "s2s_require_encryption" })):empty(); local ssl = dependencies.softreq"ssl"; if not ssl then if not require_encryption then print(""); print(" You require encryption but LuaSec is not available."); print(" Connections will fail."); ok = false; end elseif not ssl.loadcertificate then if all_options:contains("s2s_secure_auth") then print(""); print(" You have set s2s_secure_auth but your version of LuaSec does "); print(" not support certificate validation, so all s2s connections will"); print(" fail."); ok = false; elseif all_options:contains("s2s_secure_domains") then local secure_domains = set.new(); for host in enabled_hosts() do if config[host].s2s_secure_auth == true then secure_domains:add("*"); else secure_domains:include(set.new(config[host].s2s_secure_domains)); end end if not secure_domains:empty() then print(""); print(" You have set s2s_secure_domains but your version of LuaSec does "); print(" not support certificate validation, so s2s connections to/from "); print(" these domains will fail."); ok = false; end end elseif require_encryption and not all_modules:contains("tls") then print(""); print(" You require encryption but mod_tls is not enabled."); print(" Connections will fail."); ok = false; end print("Done.\n"); end if not what or what == "dns" then local dns = require "net.dns"; local idna = require "util.encodings".idna; local ip = require "util.ip"; local c2s_ports = set.new(configmanager.get("*", "c2s_ports") or {5222}); local s2s_ports = set.new(configmanager.get("*", "s2s_ports") or {5269}); local c2s_srv_required, s2s_srv_required; if not c2s_ports:contains(5222) then c2s_srv_required = true; end if not s2s_ports:contains(5269) then s2s_srv_required = true; end local problem_hosts = set.new(); local external_addresses, internal_addresses = set.new(), set.new(); local fqdn = socket.dns.tohostname(socket.dns.gethostname()); if fqdn then do local res = dns.lookup(idna.to_ascii(fqdn), "A"); if res then for _, record in ipairs(res) do external_addresses:add(record.a); end end end do local res = dns.lookup(idna.to_ascii(fqdn), "AAAA"); if res then for _, record in ipairs(res) do external_addresses:add(record.aaaa); end end end end local local_addresses = require"util.net".local_addresses() or {}; for addr in it.values(local_addresses) do if not ip.new_ip(addr).private then external_addresses:add(addr); else internal_addresses:add(addr); end end if external_addresses:empty() then print(""); print(" Failed to determine the external addresses of this server. Checks may be inaccurate."); c2s_srv_required, s2s_srv_required = true, true; end local v6_supported = not not socket.tcp6; for jid, host_options in enabled_hosts() do local all_targets_ok, some_targets_ok = true, false; local node, host = jid_split(jid); local modules, component_module = modulemanager.get_modules_for_host(host); if component_module then modules:add(component_module); end local is_component = not not host_options.component_module; print("Checking DNS for "..(is_component and "component" or "host").." "..jid.."..."); if node then print("Only the domain part ("..host..") is used in DNS.") end local target_hosts = set.new(); if modules:contains("c2s") then local res = dns.lookup("_xmpp-client._tcp."..idna.to_ascii(host)..".", "SRV"); if res then for _, record in ipairs(res) do target_hosts:add(record.srv.target); if not c2s_ports:contains(record.srv.port) then print(" SRV target "..record.srv.target.." contains unknown client port: "..record.srv.port); end end else if c2s_srv_required then print(" No _xmpp-client SRV record found for "..host..", but it looks like you need one."); all_targets_ok = false; else target_hosts:add(host); end end end if modules:contains("s2s") then local res = dns.lookup("_xmpp-server._tcp."..idna.to_ascii(host)..".", "SRV"); if res then for _, record in ipairs(res) do target_hosts:add(record.srv.target); if not s2s_ports:contains(record.srv.port) then print(" SRV target "..record.srv.target.." contains unknown server port: "..record.srv.port); end end else if s2s_srv_required then print(" No _xmpp-server SRV record found for "..host..", but it looks like you need one."); all_targets_ok = false; else target_hosts:add(host); end end end if target_hosts:empty() then target_hosts:add(host); end if target_hosts:contains("localhost") then print(" Target 'localhost' cannot be accessed from other servers"); target_hosts:remove("localhost"); end if modules:contains("proxy65") then local proxy65_target = configmanager.get(host, "proxy65_address") or host; local A, AAAA = dns.lookup(idna.to_ascii(proxy65_target), "A"), dns.lookup(idna.to_ascii(proxy65_target), "AAAA"); local prob = {}; if not A then table.insert(prob, "A"); end if v6_supported and not AAAA then table.insert(prob, "AAAA"); end if #prob > 0 then print(" File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/") .." record. Create one or set 'proxy65_address' to the correct host/IP."); end end for target_host in target_hosts do local host_ok_v4, host_ok_v6; do local res = dns.lookup(idna.to_ascii(target_host), "A"); if res then for _, record in ipairs(res) do if external_addresses:contains(record.a) then some_targets_ok = true; host_ok_v4 = true; elseif internal_addresses:contains(record.a) then host_ok_v4 = true; some_targets_ok = true; print(" "..target_host.." A record points to internal address, external connections might fail"); else print(" "..target_host.." A record points to unknown address "..record.a); all_targets_ok = false; end end end end do local res = dns.lookup(idna.to_ascii(target_host), "AAAA"); if res then for _, record in ipairs(res) do if external_addresses:contains(record.aaaa) then some_targets_ok = true; host_ok_v6 = true; elseif internal_addresses:contains(record.aaaa) then host_ok_v6 = true; some_targets_ok = true; print(" "..target_host.." AAAA record points to internal address, external connections might fail"); else print(" "..target_host.." AAAA record points to unknown address "..record.aaaa); all_targets_ok = false; end end end end local bad_protos = {} if not host_ok_v4 then table.insert(bad_protos, "IPv4"); end if not host_ok_v6 then table.insert(bad_protos, "IPv6"); end if #bad_protos > 0 then print(" Host "..target_host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")"); end if host_ok_v6 and not v6_supported then print(" Host "..target_host.." has AAAA records, but your version of LuaSocket does not support IPv6."); print(" Please see https://prosody.im/doc/ipv6 for more information."); end end if not all_targets_ok then print(" "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server."); if is_component then print(" DNS records are necessary if you want users on other servers to access this component."); end problem_hosts:add(host); end print(""); end if not problem_hosts:empty() then print(""); print("For more information about DNS configuration please see https://prosody.im/doc/dns"); print(""); ok = false; end end if not what or what == "certs" then local cert_ok; print"Checking certificates..." local x509_verify_identity = require"util.x509".verify_identity; local create_context = require "core.certmanager".create_context; local ssl = dependencies.softreq"ssl"; -- local datetime_parse = require"util.datetime".parse_x509; local load_cert = ssl and ssl.loadcertificate; -- or ssl.cert_from_pem if not ssl then print("LuaSec not available, can't perform certificate checks") if what == "certs" then cert_ok = false end elseif not load_cert then print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking"); cert_ok = false else local function skip_bare_jid_hosts(host) if jid_split(host) then -- See issue #779 return false; end return true; end for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do print("Checking certificate for "..host); -- First, let's find out what certificate this host uses. local host_ssl_config = configmanager.rawget(host, "ssl") or configmanager.rawget(host:match("%.(.*)"), "ssl"); local global_ssl_config = configmanager.rawget("*", "ssl"); local ok, err, ssl_config = create_context(host, "server", host_ssl_config, global_ssl_config); if not ok then print(" Error: "..err); cert_ok = false elseif not ssl_config.certificate then print(" No 'certificate' found for "..host) cert_ok = false elseif not ssl_config.key then print(" No 'key' found for "..host) cert_ok = false else local key, err = io.open(ssl_config.key); -- Permissions check only if not key then print(" Could not open "..ssl_config.key..": "..err); cert_ok = false else key:close(); end local cert_fh, err = io.open(ssl_config.certificate); -- Load the file. if not cert_fh then print(" Could not open "..ssl_config.certificate..": "..err); cert_ok = false else print(" Certificate: "..ssl_config.certificate) local cert = load_cert(cert_fh:read"*a"); cert_fh:close(); if not cert:validat(os.time()) then print(" Certificate has expired.") cert_ok = false elseif not cert:validat(os.time() + 86400) then print(" Certificate expires within one day.") cert_ok = false elseif not cert:validat(os.time() + 86400*7) then print(" Certificate expires within one week.") elseif not cert:validat(os.time() + 86400*31) then print(" Certificate expires within one month.") end if configmanager.get(host, "component_module") == nil and not x509_verify_identity(host, "_xmpp-client", cert) then print(" Not valid for client connections to "..host..".") cert_ok = false end if (not (configmanager.get(host, "anonymous_login") or configmanager.get(host, "authentication") == "anonymous")) and not x509_verify_identity(host, "_xmpp-server", cert) then print(" Not valid for server-to-server connections to "..host..".") cert_ok = false end end end end end if cert_ok == false then print("") print("For more information about certificates please see https://prosody.im/doc/certificates"); ok = false end print("") end if not ok then print("Problems found, see above."); else print("All checks passed, congratulations!"); end return ok and 0 or 2; end --------------------- local async = require "util.async"; local server = require "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); elseif type(ret) == "string" then show_message(ret); end os.exit(0); -- :) else show_message("Failed to execute command: "..error_messages[ret]); os.exit(1); -- :( end end if not commands[command] then -- Show help for all commands function show_usage(usage, desc) print(" "..usage); print(" "..desc); end print("prosodyctl - Manage a Prosody server"); print(""); print("Usage: "..arg[0].." COMMAND [OPTIONS]"); print(""); print("Where COMMAND may be one of:\n"); local hidden_commands = require "util.set".new{ "register", "unregister", "addplugin" }; local commands_order = { "install", "remove", "list", "enabled_plugins", "local_plugins","adduser", "passwd", "deluser", "start", "stop", "restart", "reload", "about", "list" }; local done = {}; for _, command_name in ipairs(commands_order) do local command_func = commands[command_name]; if command_func then command_func{ "--help" }; print"" done[command_name] = true; 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" }; print"" done[command_name] = true; end end os.exit(0); end os.exit(commands[command](arg)); end, watchers); command_runner:run(true);