3 -- Copyright (C) 2008-2010 Matthew Wild
4 -- Copyright (C) 2008-2010 Waqas Hussain
6 -- This project is MIT/X11 licensed. Please see the
7 -- COPYING file in the source package for more information.
10 -- prosodyctl - command-line controller for Prosody XMPP server
12 -- Will be modified by configure script if run --
13 CFG_SOURCEDIR=CFG_SOURCEDIR or os.getenv("PROSODY_SRCDIR");
14 CFG_CONFIGDIR=CFG_CONFIGDIR or os.getenv("PROSODY_CFGDIR");
15 CFG_PLUGINDIR=CFG_PLUGINDIR or os.getenv("PROSODY_PLUGINDIR");
16 CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR");
18 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20 local function is_relative(path)
21 local path_sep = package.config:sub(1,1);
22 return ((path_sep == "/" and path:sub(1,1) ~= "/")
23 or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
26 -- Tell Lua where to find our libraries
28 local function filter_relative_paths(path)
29 if is_relative(path) then return ""; end
31 local function sanitise_paths(paths)
32 return (paths:gsub("[^;]+;?", filter_relative_paths):gsub(";;+", ";"));
34 package.path = sanitise_paths(CFG_SOURCEDIR.."/?.lua;"..package.path);
35 package.cpath = sanitise_paths(CFG_SOURCEDIR.."/?.so;"..package.cpath);
38 -- Substitute ~ with path to home directory in data path
40 if os.getenv("HOME") then
41 CFG_DATADIR = CFG_DATADIR:gsub("^~", os.getenv("HOME"));
47 local startup = require "util.startup";
52 local error_messages = setmetatable({
53 ["invalid-username"] = "The given username is invalid in a Jabber ID";
54 ["invalid-hostname"] = "The given hostname is invalid";
55 ["no-password"] = "No password was supplied";
56 ["no-such-user"] = "The given user does not exist on the server";
57 ["no-such-host"] = "The given hostname does not exist in the config";
58 ["unable-to-save-data"] = "Unable to store, perhaps you don't have permission?";
59 ["no-pidfile"] = "There is no 'pidfile' option in the configuration file, see https://prosody.im/doc/prosodyctl#pidfile for help";
60 ["invalid-pidfile"] = "The 'pidfile' option in the configuration file is not a string, see https://prosody.im/doc/prosodyctl#pidfile for help";
61 ["no-posix"] = "The mod_posix module is not enabled in the Prosody config file, see https://prosody.im/doc/prosodyctl for more info";
62 ["no-such-method"] = "This module has no commands";
63 ["not-running"] = "Prosody is not running";
64 }, { __index = function (_,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end });
66 local configmanager = require "core.configmanager";
67 local modulemanager = require "core.modulemanager"
68 local prosodyctl = require "util.prosodyctl"
69 local socket = require "socket"
70 local dependencies = require "util.dependencies";
72 -----------------------
74 local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning;
75 local show_usage = prosodyctl.show_usage;
76 local show_yesno = prosodyctl.show_yesno;
77 local show_prompt = prosodyctl.show_prompt;
78 local read_password = prosodyctl.read_password;
79 local call_luarocks = prosodyctl.call_luarocks;
81 local jid_split = require "util.jid".prepped_split;
83 local prosodyctl_timeout = (configmanager.get("*", "prosodyctl_timeout") or 5) * 2;
84 -----------------------
86 local command = table.remove(arg, 1);
88 function commands.install(arg)
89 if arg[1] == "--help" then
90 show_usage([[install]], [[Installs a prosody/luarocks plugin]]);
93 call_luarocks(arg[1], "install")
96 function commands.remove(arg)
97 if arg[1] == "--help" then
98 show_usage([[remove]], [[Removes a module installed in the working directory's plugins folder]]);
101 call_luarocks(arg[1], "remove")
104 function commands.list(arg)
105 if arg[1] == "--help" then
106 show_usage([[list]], [[Shows installed rocks]]);
109 call_luarocks(arg[1], "list")
112 function commands.adduser(arg)
113 if not arg[1] or arg[1] == "--help" then
114 show_usage([[adduser JID]], [[Create the specified user account in Prosody]]);
117 local user, host = jid_split(arg[1]);
118 if not user and host then
119 show_message [[Failed to understand JID, please supply the JID you want to create]]
120 show_usage [[adduser user@host]]
125 show_message [[Please specify a JID, including a host. e.g. alice@example.com]];
129 if not prosody.hosts[host] then
130 show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
131 show_warning("The user will not be able to log in until this is changed.");
132 prosody.hosts[host] = startup.make_host(host); --luacheck: ignore 122
135 if prosodyctl.user_exists{ user = user, host = host } then
136 show_message [[That user already exists]];
140 local password = read_password();
141 if not password then return 1; end
143 local ok, msg = prosodyctl.adduser { user = user, host = host, password = password };
145 if ok then return 0; end
151 function commands.passwd(arg)
152 if not arg[1] or arg[1] == "--help" then
153 show_usage([[passwd JID]], [[Set the password for the specified user account in Prosody]]);
156 local user, host = jid_split(arg[1]);
157 if not user and host then
158 show_message [[Failed to understand JID, please supply the JID you want to set the password for]]
159 show_usage [[passwd user@host]]
164 show_message [[Please specify a JID, including a host. e.g. alice@example.com]];
168 if not prosody.hosts[host] then
169 show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
170 show_warning("The user will not be able to log in until this is changed.");
171 prosody.hosts[host] = startup.make_host(host); --luacheck: ignore 122
174 if not prosodyctl.user_exists { user = user, host = host } then
175 show_message [[That user does not exist, use prosodyctl adduser to create a new user]]
179 local password = read_password();
180 if not password then return 1; end
182 local ok, msg = prosodyctl.passwd { user = user, host = host, password = password };
184 if ok then return 0; end
186 show_message(error_messages[msg])
190 function commands.deluser(arg)
191 if not arg[1] or arg[1] == "--help" then
192 show_usage([[deluser JID]], [[Permanently remove the specified user account from Prosody]]);
195 local user, host = jid_split(arg[1]);
196 if not user and host then
197 show_message [[Failed to understand JID, please supply the JID to the user account you want to delete]]
198 show_usage [[deluser user@host]]
203 show_message [[Please specify a JID, including a host. e.g. alice@example.com]];
207 if not prosody.hosts[host] then
208 show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
209 prosody.hosts[host] = startup.make_host(host); --luacheck: ignore 122
212 if not prosodyctl.user_exists { user = user, host = host } then
213 show_message [[That user does not exist on this server]]
217 local ok, msg = prosodyctl.deluser { user = user, host = host };
219 if ok then return 0; end
221 show_message(error_messages[msg])
225 function commands.start(arg)
226 if arg[1] == "--help" then
227 show_usage([[start]], [[Start Prosody]]);
230 local ok, ret = prosodyctl.isrunning();
232 show_message(error_messages[ret]);
237 --luacheck: ignore 421/ret
238 local ok, ret = prosodyctl.getpid();
240 show_message("Couldn't get running Prosody's PID");
241 show_message(error_messages[ret]);
244 show_message("Prosody is already running with PID %s", ret or "(unknown)");
248 --luacheck: ignore 411/ret
254 until arg[i-1] == nil
257 local ok, ret = prosodyctl.start(prosody.paths.source, lua);
259 local daemonize = configmanager.get("*", "daemonize");
260 if daemonize == nil then
261 daemonize = prosody.installed;
266 local ok, running = prosodyctl.isrunning();
267 if ok and running then
270 show_message("Still waiting...");
271 elseif i >= prosodyctl_timeout then
272 show_message("Prosody is still not running. Please give it some time or check your log files for errors.");
278 show_message("Started");
283 show_message("Failed to start Prosody");
284 show_message(error_messages[ret])
288 function commands.status(arg)
289 if arg[1] == "--help" then
290 show_usage([[status]], [[Reports the running status of Prosody]]);
294 local ok, ret = prosodyctl.isrunning();
296 show_message(error_messages[ret]);
301 --luacheck: ignore 421/ret
302 local ok, ret = prosodyctl.getpid();
304 show_message("Couldn't get running Prosody's PID");
305 show_message(error_messages[ret]);
308 show_message("Prosody is running with PID %s", ret or "(unknown)");
311 show_message("Prosody is not running");
312 if not prosody.switched_user and prosody.current_uid ~= 0 then
314 print(" You will also see this if prosodyctl is not running under");
315 print(" the same user account as Prosody. Try running as root (e.g. ");
316 print(" with 'sudo' in front) to gain access to Prosody's real status.");
322 function commands.stop(arg)
323 if arg[1] == "--help" then
324 show_usage([[stop]], [[Stop a running Prosody server]]);
328 if not prosodyctl.isrunning() then
329 show_message("Prosody is not running");
333 local ok, ret = prosodyctl.stop();
337 local ok, running = prosodyctl.isrunning();
338 if ok and not running then
341 show_message("Still waiting...");
342 elseif i >= prosodyctl_timeout then
343 show_message("Prosody is still running. Please give it some time or check your log files for errors.");
349 show_message("Stopped");
353 show_message(error_messages[ret]);
357 function commands.restart(arg)
358 if arg[1] == "--help" then
359 show_usage([[restart]], [[Restart a running Prosody server]]);
364 return commands.start(arg);
367 function commands.about(arg)
368 if arg[1] == "--help" then
369 show_usage([[about]], [[Show information about this Prosody installation]]);
374 local array = require "util.array";
375 local keys = require "util.iterators".keys;
376 local hg = require"util.mercurial";
377 local relpath = configmanager.resolve_relative_path;
379 print("Prosody "..(prosody.version or "(unknown version)"));
381 print("# Prosody directories");
382 print("Data directory: "..relpath(pwd, prosody.paths.data));
383 print("Config directory: "..relpath(pwd, prosody.paths.config or "."));
384 print("Source directory: "..relpath(pwd, prosody.paths.source or "."));
385 print("Plugin directories:")
386 print(" "..(prosody.paths.plugins:gsub("([^;]+);?", function(path)
387 path = configmanager.resolve_relative_path(pwd, path);
388 local hgid, hgrepo = hg.check_id(path);
389 if not hgid and hgrepo then
390 return path.." - "..hgrepo .."!\n ";
392 -- 010452cfaf53 is the first commit in the prosody-modules repository
393 hgrepo = hgrepo == "010452cfaf53" and "prosody-modules";
394 return path..(hgid and " - "..(hgrepo or "HG").." rev: "..hgid or "")
398 local have_pposix, pposix = pcall(require, "util.pposix");
399 if have_pposix and pposix.uname then
400 print("# Operating system");
401 local uname, err = pposix.uname();
402 print(uname and uname.sysname .. " " .. uname.release or "Unknown POSIX", err or "");
405 print("# Lua environment");
406 print("Lua version: ", _G._VERSION);
408 print("Lua module search paths:");
409 for path in package.path:gmatch("[^;]+") do
413 print("Lua C module search paths:");
414 for path in package.cpath:gmatch("[^;]+") do
418 local luarocks_status = "Not installed"
419 if pcall(require, "luarocks.loader") then
420 luarocks_status = "Installed (2.x+)";
421 if package.loaded["luarocks.cfg"] then
422 luarocks_status = "Installed ("..(package.loaded["luarocks.cfg"].program_version or "2.x+")..")";
424 elseif pcall(require, "luarocks.require") then
425 luarocks_status = "Installed (1.x)";
427 print("LuaRocks: ", luarocks_status);
431 print("Backend: "..require "net.server".get_backend());
433 print("# Lua module versions");
434 local module_versions, longest_name = {}, 8;
435 local luaevent =dependencies.softreq"luaevent";
436 dependencies.softreq"ssl";
437 dependencies.softreq"DBI";
438 for name, module in pairs(package.loaded) do
439 if type(module) == "table" and rawget(module, "_VERSION")
440 and name ~= "_G" and not name:match("%.") then
441 if #name > longest_name then
442 longest_name = #name;
444 module_versions[name] = module._VERSION;
448 module_versions["libevent"] = luaevent.core.libevent_version();
450 local sorted_keys = array.collect(keys(module_versions)):sort();
451 for _, name in ipairs(sorted_keys) do
452 print(name..":"..string.rep(" ", longest_name-#name), module_versions[name]);
457 function commands.reload(arg)
458 if arg[1] == "--help" then
459 show_usage([[reload]], [[Reload Prosody's configuration and re-open log files]]);
463 if not prosodyctl.isrunning() then
464 show_message("Prosody is not running");
468 local ok, ret = prosodyctl.reload();
471 show_message("Prosody log files re-opened and config file reloaded. You may need to reload modules for some changes to take effect.");
475 show_message(error_messages[ret]);
478 -- ejabberdctl compatibility
480 local unpack = table.unpack or unpack; -- luacheck: ignore 113
482 function commands.register(arg)
483 local user, host, password = unpack(arg);
484 if (not (user and host)) or arg[1] == "--help" then
485 if user ~= "--help" then
487 show_message [[No username specified]]
489 show_message [[Please specify which host you want to register the user on]];
492 show_usage("register USER HOST [PASSWORD]", "Register a user on the server, with the given password");
496 password = read_password();
498 show_message [[Unable to register user with no password]];
503 local ok, msg = prosodyctl.adduser { user = user, host = host, password = password };
505 if ok then return 0; end
507 show_message(error_messages[msg])
511 function commands.unregister(arg)
512 local user, host = unpack(arg);
513 if (not (user and host)) or arg[1] == "--help" then
514 if user ~= "--help" then
516 show_message [[No username specified]]
518 show_message [[Please specify which host you want to unregister the user from]];
521 show_usage("unregister USER HOST [PASSWORD]", "Permanently remove a user account from the server");
525 local ok, msg = prosodyctl.deluser { user = user, host = host };
527 if ok then return 0; end
529 show_message(error_messages[msg])
536 local cert_commands = {};
538 -- If a file already exists, ask if the user wants to use it or replace it
539 -- Backups the old file if replaced
540 local function use_existing(filename)
541 local attrs = lfs.attributes(filename);
543 if show_yesno(filename .. " exists, do you want to replace it? [y/n]") then
544 local backup = filename..".bkp~"..os.date("%FT%T", attrs.change);
545 os.rename(filename, backup);
546 show_message(filename.." backed up to "..backup);
548 -- Use the existing file
554 local have_pposix, pposix = pcall(require, "util.pposix");
555 local cert_basedir = prosody.paths.data == "." and "./certs" or prosody.paths.data;
556 if have_pposix and pposix.getuid() == 0 then
557 -- FIXME should be enough to check if this directory is writable
558 local cert_dir = configmanager.get("*", "certificates") or "certs";
559 cert_basedir = configmanager.resolve_relative_path(prosody.paths.config, cert_dir);
562 function cert_commands.config(arg)
563 if #arg >= 1 and arg[1] ~= "--help" then
564 local conf_filename = cert_basedir .. "/" .. arg[1] .. ".cnf";
565 if use_existing(conf_filename) then
566 return nil, conf_filename;
568 local distinguished_name;
569 if arg[#arg]:find("^/") then
570 distinguished_name = table.remove(arg);
572 local conf = openssl.config.new();
573 conf:from_prosody(prosody.hosts, configmanager, arg);
574 if distinguished_name then
576 for k, v in distinguished_name:gmatch("/([^=/]+)=([^/]+)") do
580 conf.distinguished_name = dn;
582 show_message("Please provide details to include in the certificate config file.");
583 show_message("Leave the field empty to use the default value or '.' to exclude the field.")
584 for _, k in ipairs(openssl._DN_order) do
585 local v = conf.distinguished_name[k];
588 if k == "commonName" then
590 elseif k == "emailAddress" then
591 v = "xmpp@" .. arg[1];
592 elseif k == "countryName" then
593 local tld = arg[1]:match"%.([a-z]+)$";
594 if tld and #tld == 2 and tld ~= "uk" then
598 nv = show_prompt(("%s (%s):"):format(k, nv or v));
599 nv = (not nv or nv == "") and v or nv;
600 if nv:find"[\192-\252][\128-\191]+" then
601 conf.req.string_mask = "utf8only"
603 conf.distinguished_name[k] = nv ~= "." and nv or nil;
607 local conf_file, err = io.open(conf_filename, "w");
608 if not conf_file then
609 show_warning("Could not open OpenSSL config file for writing");
613 conf_file:write(conf:serialize());
616 show_message("Config written to " .. conf_filename);
617 return nil, conf_filename;
619 show_usage("cert config HOSTNAME [HOSTNAME+]", "Builds a certificate config file covering the supplied hostname(s)")
623 function cert_commands.key(arg)
624 if #arg >= 1 and arg[1] ~= "--help" then
625 local key_filename = cert_basedir .. "/" .. arg[1] .. ".key";
626 if use_existing(key_filename) then
627 return nil, key_filename;
629 os.remove(key_filename); -- This file, if it exists is unlikely to have write permissions
630 local key_size = tonumber(arg[2] or show_prompt("Choose key size (2048):") or 2048);
631 local old_umask = pposix.umask("0377");
632 if openssl.genrsa{out=key_filename, key_size} then
633 os.execute(("chmod 400 '%s'"):format(key_filename));
634 show_message("Key written to ".. key_filename);
635 pposix.umask(old_umask);
636 return nil, key_filename;
638 show_message("There was a problem, see OpenSSL output");
640 show_usage("cert key HOSTNAME <bits>", "Generates a RSA key named HOSTNAME.key\n "
641 .."Prompts for a key size if none given")
645 function cert_commands.request(arg)
646 if #arg >= 1 and arg[1] ~= "--help" then
647 local req_filename = cert_basedir .. "/" .. arg[1] .. ".req";
648 if use_existing(req_filename) then
649 return nil, req_filename;
651 local _, key_filename = cert_commands.key({arg[1]});
652 local _, conf_filename = cert_commands.config(arg);
653 if openssl.req{new=true, key=key_filename, utf8=true, sha256=true, config=conf_filename, out=req_filename} then
654 show_message("Certificate request written to ".. req_filename);
656 show_message("There was a problem, see OpenSSL output");
659 show_usage("cert request HOSTNAME [HOSTNAME+]", "Generates a certificate request for the supplied hostname(s)")
663 function cert_commands.generate(arg)
664 if #arg >= 1 and arg[1] ~= "--help" then
665 local cert_filename = cert_basedir .. "/" .. arg[1] .. ".crt";
666 if use_existing(cert_filename) then
667 return nil, cert_filename;
669 local _, key_filename = cert_commands.key({arg[1]});
670 local _, conf_filename = cert_commands.config(arg);
671 if key_filename and conf_filename and cert_filename
672 and openssl.req{new=true, x509=true, nodes=true, key=key_filename,
673 days=365, sha256=true, utf8=true, config=conf_filename, out=cert_filename} then
674 show_message("Certificate written to ".. cert_filename);
677 show_message("There was a problem, see OpenSSL output");
680 show_usage("cert generate HOSTNAME [HOSTNAME+]", "Generates a self-signed certificate for the current hostname(s)")
684 local function sh_esc(s)
685 return "'" .. s:gsub("'", "'\\''") .. "'";
688 local function copy(from, to, umask, owner, group)
689 local old_umask = umask and pposix.umask(umask);
690 local attrs = lfs.attributes(to);
691 if attrs then -- Move old file out of the way
692 local backup = to..".bkp~"..os.date("%FT%T", attrs.change);
693 os.rename(to, backup);
695 -- FIXME friendlier error handling, maybe move above backup back?
696 local input = assert(io.open(from));
697 local output = assert(io.open(to, "w"));
698 local data = input:read(2^11);
699 while data and output:write(data) do
700 data = input:read(2^11);
702 assert(input:close());
703 assert(output:close());
704 if not prosody.installed then
705 -- FIXME this is possibly specific to GNU chown
706 os.execute(("chown -c --reference=%s %s"):format(sh_esc(cert_basedir), sh_esc(to)));
707 elseif owner and group then
708 local ok = os.execute(("chown %s:%s %s"):format(sh_esc(owner), sh_esc(group), sh_esc(to)));
709 assert(ok == true or ok == 0, "Failed to change ownership of "..to);
711 if old_umask then pposix.umask(old_umask); end
715 function cert_commands.import(arg)
716 local hostnames = {};
717 -- Move hostname arguments out of arg, the rest should be a list of paths
718 while arg[1] and prosody.hosts[ arg[1] ] do
719 table.insert(hostnames, table.remove(arg, 1));
721 if hostnames[1] == nil then
722 local domains = os.getenv"RENEWED_DOMAINS"; -- Set if invoked via certbot
724 for host in domains:gmatch("%S+") do
725 table.insert(hostnames, host);
728 for host in pairs(prosody.hosts) do
729 if host ~= "*" and configmanager.get(host, "enabled") ~= false then
730 table.insert(hostnames, host);
735 if not arg[1] or arg[1] == "--help" then -- Probably forgot the path
736 show_usage("cert import [HOSTNAME+] /path/to/certs [/other/paths/]+",
737 "Copies certificates to "..cert_basedir);
741 if pposix.getuid() == 0 then -- We need root to change ownership
742 owner = configmanager.get("*", "prosody_user") or "prosody";
743 group = configmanager.get("*", "prosody_group") or owner;
745 local cm = require "core.certmanager";
747 for _, host in ipairs(hostnames) do
748 for _, dir in ipairs(arg) do
749 local paths = cm.find_cert(dir, host);
751 copy(paths.certificate, cert_basedir .. "/" .. host .. ".crt", nil, owner, group);
752 copy(paths.key, cert_basedir .. "/" .. host .. ".key", "0377", owner, group);
753 table.insert(imported, host);
755 -- TODO Say where we looked
756 show_warning("No certificate for host "..host.." found :(");
758 -- TODO Additional checks
759 -- Certificate names matches the hostname
760 -- Private key matches public key in certificate
764 show_message("Imported certificate and key for hosts "..table.concat(imported, ", "));
765 local ok, err = prosodyctl.reload();
766 if not ok and err ~= "not-running" then
767 show_message(error_messages[err]);
770 show_warning("No certificates imported :(");
775 function commands.cert(arg)
776 if #arg >= 1 and arg[1] ~= "--help" then
777 openssl = require "util.openssl";
779 local cert_dir_attrs = lfs.attributes(cert_basedir);
780 if not cert_dir_attrs then
781 show_warning("The directory "..cert_basedir.." does not exist");
782 return 1; -- TODO Should we create it?
784 local uid = pposix.getuid();
785 if uid ~= 0 and uid ~= cert_dir_attrs.uid then
786 show_warning("The directory "..cert_basedir.." is not owned by the current user, won't be able to write files to it");
788 elseif not cert_dir_attrs.permissions then -- COMPAT with LuaFilesystem < 1.6.2 (hey CentOS!)
789 show_message("Unable to check permissions on "..cert_basedir.." (LuaFilesystem 1.6.2+ required)");
790 show_message("Please confirm that Prosody (and only Prosody) can write to this directory)");
791 elseif cert_dir_attrs.permissions:match("^%.w..%-..%-.$") then
792 show_warning("The directory "..cert_basedir.." not only writable by its owner");
795 local subcmd = table.remove(arg, 1);
796 if type(cert_commands[subcmd]) == "function" then
797 if subcmd ~= "import" then -- hostnames are optional for import
799 show_message"You need to supply at least one hostname"
802 if arg[1] ~= "--help" and not prosody.hosts[arg[1]] then
803 show_message(error_messages["no-such-host"]);
807 return cert_commands[subcmd](arg);
808 elseif subcmd == "check" then
809 return commands.check({"certs"});
812 show_usage("cert config|request|generate|key|import", "Helpers for generating X.509 certificates and keys.")
813 for _, cmd in pairs(cert_commands) do
819 function commands.check(arg)
820 if arg[1] == "--help" then
821 show_usage([[check]], [[Perform basic checks on your Prosody installation]]);
824 local what = table.remove(arg, 1);
825 local set = require "util.set";
826 local it = require "util.iterators";
828 local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end
829 local function enabled_hosts() return it.filter(disabled_hosts, pairs(configmanager.getconfig())); end
830 if not (what == nil or what == "disabled" or what == "config" or what == "dns" or what == "certs") then
831 show_warning("Don't know how to check '%s'. Try one of 'config', 'dns', 'certs' or 'disabled'.", what);
834 if not what or what == "disabled" then
835 local disabled_hosts_set = set.new();
836 for host, host_options in it.filter("*", pairs(configmanager.getconfig())) do
837 if host_options.enabled == false then
838 disabled_hosts_set:add(host);
841 if not disabled_hosts_set:empty() then
842 local msg = "Checks will be skipped for these disabled hosts: %s";
843 if what then msg = "These hosts are disabled: %s"; end
844 show_warning(msg, tostring(disabled_hosts_set));
845 if what then return 0; end
849 if not what or what == "config" then
850 print("Checking config...");
851 local deprecated = set.new({
852 "bosh_ports", "disallow_s2s", "no_daemonize", "anonymous_login", "require_encryption",
853 "vcard_compatibility", "cross_domain_bosh", "cross_domain_websocket"
855 local known_global_options = set.new({
856 "pidfile", "log", "plugin_paths", "prosody_user", "prosody_group", "daemonize",
857 "umask", "prosodyctl_timeout", "use_ipv6", "use_libevent", "network_settings",
858 "network_backend", "http_default_host",
859 "statistics_interval", "statistics", "statistics_config",
861 local config = configmanager.getconfig();
862 -- Check that we have any global options (caused by putting a host at the top)
863 if it.count(it.filter("log", pairs(config["*"]))) == 0 then
866 print(" No global options defined. Perhaps you have put a host definition at the top")
867 print(" of the config file? They should be at the bottom, see https://prosody.im/doc/configure#overview");
869 if it.count(enabled_hosts()) == 0 then
872 if it.count(it.filter("*", pairs(config))) == 0 then
873 print(" No hosts are defined, please add at least one VirtualHost section")
874 elseif config["*"]["enabled"] == false then
875 print(" No hosts are enabled. Remove enabled = false from the global section or put enabled = true under at least one VirtualHost section")
877 print(" All hosts are disabled. Remove enabled = false from at least one VirtualHost section")
880 if not config["*"].modules_enabled then
881 print(" No global modules_enabled is set?");
882 local suggested_global_modules;
883 for host, options in enabled_hosts() do --luacheck: ignore 213/host
884 if not options.component_module and options.modules_enabled then
885 suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled));
888 if suggested_global_modules and not suggested_global_modules:empty() then
889 print(" Consider moving these modules into modules_enabled in the global section:")
890 print(" "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end));
895 do -- Check for modules enabled both normally and as components
896 local modules = set.new(config["*"]["modules_enabled"]);
897 for host, options in enabled_hosts() do
898 local component_module = options.component_module;
899 if component_module and modules:contains(component_module) then
900 print((" mod_%s is enabled both in modules_enabled and as Component %q %q"):format(component_module, host, component_module));
901 print(" This means the service is enabled on all VirtualHosts as well as the Component.");
902 print(" Are you sure this what you want? It may cause unexpected behaviour.");
907 -- Check for global options under hosts
908 local global_options = set.new(it.to_array(it.keys(config["*"])));
909 local deprecated_global_options = set.intersection(global_options, deprecated);
910 if not deprecated_global_options:empty() then
912 print(" You have some deprecated options in the global section:");
913 print(" "..tostring(deprecated_global_options))
916 for host, options in it.filter(function (h) return h ~= "*" end, pairs(configmanager.getconfig())) do
917 local host_options = set.new(it.to_array(it.keys(options)));
918 local misplaced_options = set.intersection(host_options, known_global_options);
919 for name in pairs(options) do
920 if name:match("^interfaces?")
921 or name:match("_ports?$") or name:match("_interfaces?$")
922 or (name:match("_ssl$") and not name:match("^[cs]2s_ssl$")) then
923 misplaced_options:add(name);
926 if not misplaced_options:empty() then
929 local n = it.count(misplaced_options);
930 print(" You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be");
931 print(" in the global section of the config file, above any VirtualHost or Component definitions,")
932 print(" see https://prosody.im/doc/configure#overview for more information.")
934 print(" You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", "));
937 for host, options in enabled_hosts() do
938 local host_options = set.new(it.to_array(it.keys(options)));
939 local subdomain = host:match("^[^.]+");
940 if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp"
941 or subdomain == "chat" or subdomain == "im") then
943 print(" Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to");
944 print(" "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host..".");
945 print(" For more information see: https://prosody.im/doc/dns");
948 local all_modules = set.new(config["*"].modules_enabled);
949 local all_options = set.new(it.to_array(it.keys(config["*"])));
950 for host in enabled_hosts() do
951 all_options:include(set.new(it.to_array(it.keys(config[host]))));
952 all_modules:include(set.new(config[host].modules_enabled));
954 for mod in all_modules do
955 if mod:match("^mod_") then
957 print(" Modules in modules_enabled should not have the 'mod_' prefix included.");
958 print(" Change '"..mod.."' to '"..mod:match("^mod_(.*)").."'.");
959 elseif mod:match("^auth_") then
961 print(" Authentication modules should not be added to modules_enabled,");
962 print(" but be specified in the 'authentication' option.");
963 print(" Remove '"..mod.."' from modules_enabled and instead add");
964 print(" authentication = '"..mod:match("^auth_(.*)").."'");
965 print(" For more information see https://prosody.im/doc/authentication");
966 elseif mod:match("^storage_") then
968 print(" storage modules should not be added to modules_enabled,");
969 print(" but be specified in the 'storage' option.");
970 print(" Remove '"..mod.."' from modules_enabled and instead add");
971 print(" storage = '"..mod:match("^storage_(.*)").."'");
972 print(" For more information see https://prosody.im/doc/storage");
975 for host, host_config in pairs(config) do --luacheck: ignore 213/host
976 if type(rawget(host_config, "storage")) == "string" and rawget(host_config, "default_storage") then
978 print(" The 'default_storage' option is not needed if 'storage' is set to a string.");
982 local require_encryption = set.intersection(all_options, set.new({
983 "require_encryption", "c2s_require_encryption", "s2s_require_encryption"
985 local ssl = dependencies.softreq"ssl";
987 if not require_encryption then
989 print(" You require encryption but LuaSec is not available.");
990 print(" Connections will fail.");
993 elseif not ssl.loadcertificate then
994 if all_options:contains("s2s_secure_auth") then
996 print(" You have set s2s_secure_auth but your version of LuaSec does ");
997 print(" not support certificate validation, so all s2s connections will");
1000 elseif all_options:contains("s2s_secure_domains") then
1001 local secure_domains = set.new();
1002 for host in enabled_hosts() do
1003 if config[host].s2s_secure_auth == true then
1004 secure_domains:add("*");
1006 secure_domains:include(set.new(config[host].s2s_secure_domains));
1009 if not secure_domains:empty() then
1011 print(" You have set s2s_secure_domains but your version of LuaSec does ");
1012 print(" not support certificate validation, so s2s connections to/from ");
1013 print(" these domains will fail.");
1017 elseif require_encryption and not all_modules:contains("tls") then
1019 print(" You require encryption but mod_tls is not enabled.");
1020 print(" Connections will fail.");
1026 if not what or what == "dns" then
1027 local dns = require "net.dns";
1028 local idna = require "util.encodings".idna;
1029 local ip = require "util.ip";
1030 local c2s_ports = set.new(configmanager.get("*", "c2s_ports") or {5222});
1031 local s2s_ports = set.new(configmanager.get("*", "s2s_ports") or {5269});
1033 local c2s_srv_required, s2s_srv_required;
1034 if not c2s_ports:contains(5222) then
1035 c2s_srv_required = true;
1037 if not s2s_ports:contains(5269) then
1038 s2s_srv_required = true;
1041 local problem_hosts = set.new();
1043 local external_addresses, internal_addresses = set.new(), set.new();
1045 local fqdn = socket.dns.tohostname(socket.dns.gethostname());
1048 local res = dns.lookup(idna.to_ascii(fqdn), "A");
1050 for _, record in ipairs(res) do
1051 external_addresses:add(record.a);
1056 local res = dns.lookup(idna.to_ascii(fqdn), "AAAA");
1058 for _, record in ipairs(res) do
1059 external_addresses:add(record.aaaa);
1065 local local_addresses = require"util.net".local_addresses() or {};
1067 for addr in it.values(local_addresses) do
1068 if not ip.new_ip(addr).private then
1069 external_addresses:add(addr);
1071 internal_addresses:add(addr);
1075 if external_addresses:empty() then
1077 print(" Failed to determine the external addresses of this server. Checks may be inaccurate.");
1078 c2s_srv_required, s2s_srv_required = true, true;
1081 local v6_supported = not not socket.tcp6;
1083 for jid, host_options in enabled_hosts() do
1084 local all_targets_ok, some_targets_ok = true, false;
1085 local node, host = jid_split(jid);
1087 local modules, component_module = modulemanager.get_modules_for_host(host);
1088 if component_module then
1089 modules:add(component_module);
1092 local is_component = not not host_options.component_module;
1093 print("Checking DNS for "..(is_component and "component" or "host").." "..jid.."...");
1095 print("Only the domain part ("..host..") is used in DNS.")
1097 local target_hosts = set.new();
1098 if modules:contains("c2s") then
1099 local res = dns.lookup("_xmpp-client._tcp."..idna.to_ascii(host)..".", "SRV");
1101 for _, record in ipairs(res) do
1102 target_hosts:add(record.srv.target);
1103 if not c2s_ports:contains(record.srv.port) then
1104 print(" SRV target "..record.srv.target.." contains unknown client port: "..record.srv.port);
1108 if c2s_srv_required then
1109 print(" No _xmpp-client SRV record found for "..host..", but it looks like you need one.");
1110 all_targets_ok = false;
1112 target_hosts:add(host);
1116 if modules:contains("s2s") then
1117 local res = dns.lookup("_xmpp-server._tcp."..idna.to_ascii(host)..".", "SRV");
1119 for _, record in ipairs(res) do
1120 target_hosts:add(record.srv.target);
1121 if not s2s_ports:contains(record.srv.port) then
1122 print(" SRV target "..record.srv.target.." contains unknown server port: "..record.srv.port);
1126 if s2s_srv_required then
1127 print(" No _xmpp-server SRV record found for "..host..", but it looks like you need one.");
1128 all_targets_ok = false;
1130 target_hosts:add(host);
1134 if target_hosts:empty() then
1135 target_hosts:add(host);
1138 if target_hosts:contains("localhost") then
1139 print(" Target 'localhost' cannot be accessed from other servers");
1140 target_hosts:remove("localhost");
1143 if modules:contains("proxy65") then
1144 local proxy65_target = configmanager.get(host, "proxy65_address") or host;
1145 local A, AAAA = dns.lookup(idna.to_ascii(proxy65_target), "A"), dns.lookup(idna.to_ascii(proxy65_target), "AAAA");
1148 table.insert(prob, "A");
1150 if v6_supported and not AAAA then
1151 table.insert(prob, "AAAA");
1154 print(" File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/")
1155 .." record. Create one or set 'proxy65_address' to the correct host/IP.");
1159 for target_host in target_hosts do
1160 local host_ok_v4, host_ok_v6;
1162 local res = dns.lookup(idna.to_ascii(target_host), "A");
1164 for _, record in ipairs(res) do
1165 if external_addresses:contains(record.a) then
1166 some_targets_ok = true;
1168 elseif internal_addresses:contains(record.a) then
1170 some_targets_ok = true;
1171 print(" "..target_host.." A record points to internal address, external connections might fail");
1173 print(" "..target_host.." A record points to unknown address "..record.a);
1174 all_targets_ok = false;
1180 local res = dns.lookup(idna.to_ascii(target_host), "AAAA");
1182 for _, record in ipairs(res) do
1183 if external_addresses:contains(record.aaaa) then
1184 some_targets_ok = true;
1186 elseif internal_addresses:contains(record.aaaa) then
1188 some_targets_ok = true;
1189 print(" "..target_host.." AAAA record points to internal address, external connections might fail");
1191 print(" "..target_host.." AAAA record points to unknown address "..record.aaaa);
1192 all_targets_ok = false;
1198 local bad_protos = {}
1199 if not host_ok_v4 then
1200 table.insert(bad_protos, "IPv4");
1202 if not host_ok_v6 then
1203 table.insert(bad_protos, "IPv6");
1205 if #bad_protos > 0 then
1206 print(" Host "..target_host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")");
1208 if host_ok_v6 and not v6_supported then
1209 print(" Host "..target_host.." has AAAA records, but your version of LuaSocket does not support IPv6.");
1210 print(" Please see https://prosody.im/doc/ipv6 for more information.");
1213 if not all_targets_ok then
1214 print(" "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server.");
1215 if is_component then
1216 print(" DNS records are necessary if you want users on other servers to access this component.");
1218 problem_hosts:add(host);
1222 if not problem_hosts:empty() then
1224 print("For more information about DNS configuration please see https://prosody.im/doc/dns");
1229 if not what or what == "certs" then
1231 print"Checking certificates..."
1232 local x509_verify_identity = require"util.x509".verify_identity;
1233 local create_context = require "core.certmanager".create_context;
1234 local ssl = dependencies.softreq"ssl";
1235 -- local datetime_parse = require"util.datetime".parse_x509;
1236 local load_cert = ssl and ssl.loadcertificate;
1237 -- or ssl.cert_from_pem
1239 print("LuaSec not available, can't perform certificate checks")
1240 if what == "certs" then cert_ok = false end
1241 elseif not load_cert then
1242 print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking");
1245 local function skip_bare_jid_hosts(host)
1246 if jid_split(host) then
1252 for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do
1253 print("Checking certificate for "..host);
1254 -- First, let's find out what certificate this host uses.
1255 local host_ssl_config = configmanager.rawget(host, "ssl")
1256 or configmanager.rawget(host:match("%.(.*)"), "ssl");
1257 local global_ssl_config = configmanager.rawget("*", "ssl");
1258 local ok, err, ssl_config = create_context(host, "server", host_ssl_config, global_ssl_config);
1260 print(" Error: "..err);
1262 elseif not ssl_config.certificate then
1263 print(" No 'certificate' found for "..host)
1265 elseif not ssl_config.key then
1266 print(" No 'key' found for "..host)
1269 local key, err = io.open(ssl_config.key); -- Permissions check only
1271 print(" Could not open "..ssl_config.key..": "..err);
1276 local cert_fh, err = io.open(ssl_config.certificate); -- Load the file.
1278 print(" Could not open "..ssl_config.certificate..": "..err);
1281 print(" Certificate: "..ssl_config.certificate)
1282 local cert = load_cert(cert_fh:read"*a"); cert_fh:close();
1283 if not cert:validat(os.time()) then
1284 print(" Certificate has expired.")
1286 elseif not cert:validat(os.time() + 86400) then
1287 print(" Certificate expires within one day.")
1289 elseif not cert:validat(os.time() + 86400*7) then
1290 print(" Certificate expires within one week.")
1291 elseif not cert:validat(os.time() + 86400*31) then
1292 print(" Certificate expires within one month.")
1294 if configmanager.get(host, "component_module") == nil
1295 and not x509_verify_identity(host, "_xmpp-client", cert) then
1296 print(" Not valid for client connections to "..host..".")
1299 if (not (configmanager.get(host, "anonymous_login")
1300 or configmanager.get(host, "authentication") == "anonymous"))
1301 and not x509_verify_identity(host, "_xmpp-server", cert) then
1302 print(" Not valid for server-to-server connections to "..host..".")
1309 if cert_ok == false then
1311 print("For more information about certificates please see https://prosody.im/doc/certificates");
1317 print("Problems found, see above.");
1319 print("All checks passed, congratulations!");
1321 return ok and 0 or 2;
1324 ---------------------
1326 local async = require "util.async";
1327 local server = require "net.server";
1329 error = function (_, err)
1332 waiting = function ()
1336 local command_runner = async.runner(function ()
1337 if command and command:match("^mod_") then -- Is a command in a module
1338 local module_name = command:match("^mod_(.+)");
1340 local ret, err = modulemanager.load("*", module_name);
1342 show_message("Failed to load module '"..module_name.."': "..err);
1347 local module = modulemanager.get_module("*", module_name);
1349 show_message("Failed to load module '"..module_name.."': Unknown error");
1353 if not modulemanager.module_has_method(module, "command") then
1354 show_message("Fail: mod_"..module_name.." does not support any commands");
1358 local ok, ret = modulemanager.call_module_method(module, "command", arg);
1360 if type(ret) == "number" then
1362 elseif type(ret) == "string" then
1367 show_message("Failed to execute command: "..error_messages[ret]);
1372 if not commands[command] then -- Show help for all commands
1373 function show_usage(usage, desc)
1378 print("prosodyctl - Manage a Prosody server");
1380 print("Usage: "..arg[0].." COMMAND [OPTIONS]");
1382 print("Where COMMAND may be one of:\n");
1384 local hidden_commands = require "util.set".new{ "register", "unregister", "addplugin" };
1385 local commands_order = { "install", "remove", "list", "adduser", "passwd", "deluser", "start", "stop", "restart", "reload",
1390 for _, command_name in ipairs(commands_order) do
1391 local command_func = commands[command_name];
1392 if command_func then
1393 command_func{ "--help" };
1395 done[command_name] = true;
1399 for command_name, command_func in pairs(commands) do
1400 if not done[command_name] and not hidden_commands:contains(command_name) then
1401 command_func{ "--help" };
1403 done[command_name] = true;
1411 os.exit(commands[command](arg));
1414 command_runner:run(true);