Comparison

prosodyctl @ 11120:b2331f3dfeea

Merge 0.11->trunk
author Matthew Wild <mwild1@gmail.com>
date Wed, 30 Sep 2020 09:50:33 +0100
parent 11006:8ac958938e0f
child 11133:624eafed3343
comparison
equal deleted inserted replaced
11119:68df52bf08d5 11120:b2331f3dfeea
8 -- 8 --
9 9
10 -- prosodyctl - command-line controller for Prosody XMPP server 10 -- prosodyctl - command-line controller for Prosody XMPP server
11 11
12 -- Will be modified by configure script if run -- 12 -- Will be modified by configure script if run --
13
14 CFG_SOURCEDIR=CFG_SOURCEDIR or os.getenv("PROSODY_SRCDIR"); 13 CFG_SOURCEDIR=CFG_SOURCEDIR or os.getenv("PROSODY_SRCDIR");
15 CFG_CONFIGDIR=CFG_CONFIGDIR or os.getenv("PROSODY_CFGDIR"); 14 CFG_CONFIGDIR=CFG_CONFIGDIR or os.getenv("PROSODY_CFGDIR");
16 CFG_PLUGINDIR=CFG_PLUGINDIR or os.getenv("PROSODY_PLUGINDIR"); 15 CFG_PLUGINDIR=CFG_PLUGINDIR or os.getenv("PROSODY_PLUGINDIR");
17 CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR"); 16 CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR");
18 17
47 46
48 local startup = require "util.startup"; 47 local startup = require "util.startup";
49 startup.prosodyctl(); 48 startup.prosodyctl();
50 49
51 ----------- 50 -----------
52
53 local error_messages = setmetatable({
54 ["invalid-username"] = "The given username is invalid in a Jabber ID";
55 ["invalid-hostname"] = "The given hostname is invalid";
56 ["no-password"] = "No password was supplied";
57 ["no-such-user"] = "The given user does not exist on the server";
58 ["no-such-host"] = "The given hostname does not exist in the config";
59 ["unable-to-save-data"] = "Unable to store, perhaps you don't have permission?";
60 ["no-pidfile"] = "There is no 'pidfile' option in the configuration file, see https://prosody.im/doc/prosodyctl#pidfile for help";
61 ["invalid-pidfile"] = "The 'pidfile' option in the configuration file is not a string, see https://prosody.im/doc/prosodyctl#pidfile for help";
62 ["no-posix"] = "The mod_posix module is not enabled in the Prosody config file, see https://prosody.im/doc/prosodyctl for more info";
63 ["no-such-method"] = "This module has no commands";
64 ["not-running"] = "Prosody is not running";
65 }, { __index = function (_,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end });
66 51
67 local configmanager = require "core.configmanager"; 52 local configmanager = require "core.configmanager";
68 local modulemanager = require "core.modulemanager" 53 local modulemanager = require "core.modulemanager"
69 local prosodyctl = require "util.prosodyctl" 54 local prosodyctl = require "util.prosodyctl"
70 local socket = require "socket" 55 local socket = require "socket"
71 local dependencies = require "util.dependencies"; 56 local dependencies = require "util.dependencies";
72 local lfs = dependencies.softreq "lfs"; 57 local lfs = dependencies.softreq "lfs";
73 58
74 ----------------------- 59 -----------------------
75 60
61 local human_io = require "util.human.io";
62
76 local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning; 63 local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning;
77 local show_usage = prosodyctl.show_usage; 64 local show_usage = prosodyctl.show_usage;
78 local show_yesno = prosodyctl.show_yesno; 65 local read_password = human_io.read_password;
79 local show_prompt = prosodyctl.show_prompt; 66 local call_luarocks = prosodyctl.call_luarocks;
80 local read_password = prosodyctl.read_password; 67 local error_messages = prosodyctl.error_messages;
81 68
82 local jid_split = require "util.jid".prepped_split; 69 local jid_split = require "util.jid".prepped_split;
83 70
84 local prosodyctl_timeout = (configmanager.get("*", "prosodyctl_timeout") or 5) * 2; 71 local prosodyctl_timeout = (configmanager.get("*", "prosodyctl_timeout") or 5) * 2;
85 ----------------------- 72 -----------------------
86 local commands = {}; 73 local commands = {};
87 local command = arg[1]; 74 local command = table.remove(arg, 1);
75
76 function commands.install(arg)
77 if arg[1] == "--help" then
78 show_usage([[install]], [[Installs a prosody/luarocks plugin]]);
79 return 1;
80 end
81 call_luarocks(arg[1], "install")
82 end
83
84 function commands.remove(arg)
85 if arg[1] == "--help" then
86 show_usage([[remove]], [[Removes a module installed in the working directory's plugins folder]]);
87 return 1;
88 end
89 call_luarocks(arg[1], "remove")
90 end
91
92 function commands.list(arg)
93 if arg[1] == "--help" then
94 show_usage([[list]], [[Shows installed rocks]]);
95 return 1;
96 end
97 call_luarocks(arg[1], "list")
98 end
88 99
89 function commands.adduser(arg) 100 function commands.adduser(arg)
90 if not arg[1] or arg[1] == "--help" then 101 if not arg[1] or arg[1] == "--help" then
91 show_usage([[adduser JID]], [[Create the specified user account in Prosody]]); 102 show_usage([[adduser JID]], [[Create the specified user account in Prosody]]);
92 return 1; 103 return 1;
119 130
120 local ok, msg = prosodyctl.adduser { user = user, host = host, password = password }; 131 local ok, msg = prosodyctl.adduser { user = user, host = host, password = password };
121 132
122 if ok then return 0; end 133 if ok then return 0; end
123 134
124 show_message(msg) 135 show_message(error_messages[msg])
125 return 1; 136 return 1;
126 end 137 end
127 138
128 function commands.passwd(arg) 139 function commands.passwd(arg)
129 if not arg[1] or arg[1] == "--help" then 140 if not arg[1] or arg[1] == "--help" then
236 show_message("Prosody is already running with PID %s", ret or "(unknown)"); 247 show_message("Prosody is already running with PID %s", ret or "(unknown)");
237 return 1; 248 return 1;
238 end 249 end
239 250
240 --luacheck: ignore 411/ret 251 --luacheck: ignore 411/ret
241 local ok, ret = prosodyctl.start(prosody.paths.source); 252 local lua;
253 do
254 local i = 0;
255 repeat
256 i = i - 1;
257 until arg[i-1] == nil
258 lua = arg[i];
259 end
260 local ok, ret = prosodyctl.start(prosody.paths.source, lua);
242 if ok then 261 if ok then
243 local daemonize = configmanager.get("*", "daemonize"); 262 local daemonize = configmanager.get("*", "daemonize");
244 if daemonize == nil then 263 if daemonize == nil then
245 daemonize = prosody.installed; 264 daemonize = prosody.installed;
246 end 265 end
357 show_usage([[about]], [[Show information about this Prosody installation]]); 376 show_usage([[about]], [[Show information about this Prosody installation]]);
358 return 1; 377 return 1;
359 end 378 end
360 379
361 local pwd = "."; 380 local pwd = ".";
362 local array = require "util.array"; 381 local sorted_pairs = require "util.iterators".sorted_pairs;
363 local keys = require "util.iterators".keys;
364 local hg = require"util.mercurial"; 382 local hg = require"util.mercurial";
365 local relpath = configmanager.resolve_relative_path; 383 local relpath = configmanager.resolve_relative_path;
366 384
367 print("Prosody "..(prosody.version or "(unknown version)")); 385 print("Prosody "..(prosody.version or "(unknown version)"));
368 print(""); 386 print("");
381 hgrepo = hgrepo == "010452cfaf53" and "prosody-modules"; 399 hgrepo = hgrepo == "010452cfaf53" and "prosody-modules";
382 return path..(hgid and " - "..(hgrepo or "HG").." rev: "..hgid or "") 400 return path..(hgid and " - "..(hgrepo or "HG").." rev: "..hgid or "")
383 .."\n "; 401 .."\n ";
384 end))); 402 end)));
385 print(""); 403 print("");
404 local have_pposix, pposix = pcall(require, "util.pposix");
405 if have_pposix and pposix.uname then
406 print("# Operating system");
407 local uname, err = pposix.uname();
408 print(uname and uname.sysname .. " " .. uname.release or "Unknown POSIX", err or "");
409 print("");
410 end
386 print("# Lua environment"); 411 print("# Lua environment");
387 print("Lua version: ", _G._VERSION); 412 print("Lua version: ", _G._VERSION);
388 print(""); 413 print("");
389 print("Lua module search paths:"); 414 print("Lua module search paths:");
390 for path in package.path:gmatch("[^;]+") do 415 for path in package.path:gmatch("[^;]+") do
411 print(""); 436 print("");
412 print("Backend: "..require "net.server".get_backend()); 437 print("Backend: "..require "net.server".get_backend());
413 print(""); 438 print("");
414 print("# Lua module versions"); 439 print("# Lua module versions");
415 local module_versions, longest_name = {}, 8; 440 local module_versions, longest_name = {}, 8;
416 local luaevent =dependencies.softreq"luaevent"; 441 local library_versions = {};
417 dependencies.softreq"ssl"; 442 dependencies.softreq"ssl";
418 dependencies.softreq"DBI"; 443 dependencies.softreq"DBI";
444 local friendly_names = {
445 DBI = "LuaDBI";
446 lfs = "LuaFileSystem";
447 lunbound = "luaunbound";
448 lxp = "LuaExpat";
449 socket = "LuaSocket";
450 ssl = "LuaSec";
451 };
452 local lunbound = dependencies.softreq"lunbound";
419 for name, module in pairs(package.loaded) do 453 for name, module in pairs(package.loaded) do
420 if type(module) == "table" and rawget(module, "_VERSION") 454 if type(module) == "table" and rawget(module, "_VERSION")
421 and name ~= "_G" and not name:match("%.") then 455 and name ~= "_G" and not name:match("%.") then
456 name = friendly_names[name] or name;
422 if #name > longest_name then 457 if #name > longest_name then
423 longest_name = #name; 458 longest_name = #name;
424 end 459 end
425 module_versions[name] = module._VERSION; 460 local mod_version = module._VERSION;
426 end 461 if tostring(mod_version):sub(1, #name+1) == name .. " " then
427 end 462 mod_version = mod_version:sub(#name+2);
428 if luaevent then 463 end
429 module_versions["libevent"] = luaevent.core.libevent_version(); 464 module_versions[name] = mod_version;
430 end 465 end
431 local sorted_keys = array.collect(keys(module_versions)):sort(); 466 end
432 for _, name in ipairs(sorted_keys) do 467 if lunbound then
433 print(name..":"..string.rep(" ", longest_name-#name), module_versions[name]); 468 if not module_versions["luaunbound"] then
469 module_versions["luaunbound"] = "0.5 (?)";
470 end
471 library_versions["libunbound"] = lunbound._LIBVER;
472 end
473 for name, version in sorted_pairs(module_versions) do
474 print(name..":"..string.rep(" ", longest_name-#name), version);
475 end
476 print("");
477 print("# library versions");
478 if require "net.server".event_base then
479 library_versions["libevent"] = require"luaevent".core.libevent_version();
480 end
481 for name, version in sorted_pairs(library_versions) do
482 print(name..":"..string.rep(" ", longest_name-#name), version);
434 end 483 end
435 print(""); 484 print("");
436 end 485 end
437 486
438 function commands.reload(arg) 487 function commands.reload(arg)
509 558
510 if ok then return 0; end 559 if ok then return 0; end
511 560
512 show_message(error_messages[msg]) 561 show_message(error_messages[msg])
513 return 1; 562 return 1;
514 end
515
516 local openssl;
517
518 local cert_commands = {};
519
520 -- If a file already exists, ask if the user wants to use it or replace it
521 -- Backups the old file if replaced
522 local function use_existing(filename)
523 local attrs = lfs.attributes(filename);
524 if attrs then
525 if show_yesno(filename .. " exists, do you want to replace it? [y/n]") then
526 local backup = filename..".bkp~"..os.date("%FT%T", attrs.change);
527 os.rename(filename, backup);
528 show_message(filename.." backed up to "..backup);
529 else
530 -- Use the existing file
531 return true;
532 end
533 end
534 end
535
536 local have_pposix, pposix = pcall(require, "util.pposix");
537 local cert_basedir = prosody.paths.data == "." and "./certs" or prosody.paths.data;
538 if have_pposix and pposix.getuid() == 0 then
539 -- FIXME should be enough to check if this directory is writable
540 local cert_dir = configmanager.get("*", "certificates") or "certs";
541 cert_basedir = configmanager.resolve_relative_path(prosody.paths.config, cert_dir);
542 end
543
544 function cert_commands.config(arg)
545 if #arg >= 1 and arg[1] ~= "--help" then
546 local conf_filename = cert_basedir .. "/" .. arg[1] .. ".cnf";
547 if use_existing(conf_filename) then
548 return nil, conf_filename;
549 end
550 local distinguished_name;
551 if arg[#arg]:find("^/") then
552 distinguished_name = table.remove(arg);
553 end
554 local conf = openssl.config.new();
555 conf:from_prosody(prosody.hosts, configmanager, arg);
556 if distinguished_name then
557 local dn = {};
558 for k, v in distinguished_name:gmatch("/([^=/]+)=([^/]+)") do
559 table.insert(dn, k);
560 dn[k] = v;
561 end
562 conf.distinguished_name = dn;
563 else
564 show_message("Please provide details to include in the certificate config file.");
565 show_message("Leave the field empty to use the default value or '.' to exclude the field.")
566 for _, k in ipairs(openssl._DN_order) do
567 local v = conf.distinguished_name[k];
568 if v then
569 local nv = nil;
570 if k == "commonName" then
571 v = arg[1]
572 elseif k == "emailAddress" then
573 v = "xmpp@" .. arg[1];
574 elseif k == "countryName" then
575 local tld = arg[1]:match"%.([a-z]+)$";
576 if tld and #tld == 2 and tld ~= "uk" then
577 v = tld:upper();
578 end
579 end
580 nv = show_prompt(("%s (%s):"):format(k, nv or v));
581 nv = (not nv or nv == "") and v or nv;
582 if nv:find"[\192-\252][\128-\191]+" then
583 conf.req.string_mask = "utf8only"
584 end
585 conf.distinguished_name[k] = nv ~= "." and nv or nil;
586 end
587 end
588 end
589 local conf_file, err = io.open(conf_filename, "w");
590 if not conf_file then
591 show_warning("Could not open OpenSSL config file for writing");
592 show_warning(err);
593 os.exit(1);
594 end
595 conf_file:write(conf:serialize());
596 conf_file:close();
597 print("");
598 show_message("Config written to " .. conf_filename);
599 return nil, conf_filename;
600 else
601 show_usage("cert config HOSTNAME [HOSTNAME+]", "Builds a certificate config file covering the supplied hostname(s)")
602 end
603 end
604
605 function cert_commands.key(arg)
606 if #arg >= 1 and arg[1] ~= "--help" then
607 local key_filename = cert_basedir .. "/" .. arg[1] .. ".key";
608 if use_existing(key_filename) then
609 return nil, key_filename;
610 end
611 os.remove(key_filename); -- This file, if it exists is unlikely to have write permissions
612 local key_size = tonumber(arg[2] or show_prompt("Choose key size (2048):") or 2048);
613 local old_umask = pposix.umask("0377");
614 if openssl.genrsa{out=key_filename, key_size} then
615 os.execute(("chmod 400 '%s'"):format(key_filename));
616 show_message("Key written to ".. key_filename);
617 pposix.umask(old_umask);
618 return nil, key_filename;
619 end
620 show_message("There was a problem, see OpenSSL output");
621 else
622 show_usage("cert key HOSTNAME <bits>", "Generates a RSA key named HOSTNAME.key\n "
623 .."Prompts for a key size if none given")
624 end
625 end
626
627 function cert_commands.request(arg)
628 if #arg >= 1 and arg[1] ~= "--help" then
629 local req_filename = cert_basedir .. "/" .. arg[1] .. ".req";
630 if use_existing(req_filename) then
631 return nil, req_filename;
632 end
633 local _, key_filename = cert_commands.key({arg[1]});
634 local _, conf_filename = cert_commands.config(arg);
635 if openssl.req{new=true, key=key_filename, utf8=true, sha256=true, config=conf_filename, out=req_filename} then
636 show_message("Certificate request written to ".. req_filename);
637 else
638 show_message("There was a problem, see OpenSSL output");
639 end
640 else
641 show_usage("cert request HOSTNAME [HOSTNAME+]", "Generates a certificate request for the supplied hostname(s)")
642 end
643 end
644
645 function cert_commands.generate(arg)
646 if #arg >= 1 and arg[1] ~= "--help" then
647 local cert_filename = cert_basedir .. "/" .. arg[1] .. ".crt";
648 if use_existing(cert_filename) then
649 return nil, cert_filename;
650 end
651 local _, key_filename = cert_commands.key({arg[1]});
652 local _, conf_filename = cert_commands.config(arg);
653 if key_filename and conf_filename and cert_filename
654 and openssl.req{new=true, x509=true, nodes=true, key=key_filename,
655 days=365, sha256=true, utf8=true, config=conf_filename, out=cert_filename} then
656 show_message("Certificate written to ".. cert_filename);
657 print();
658 else
659 show_message("There was a problem, see OpenSSL output");
660 end
661 else
662 show_usage("cert generate HOSTNAME [HOSTNAME+]", "Generates a self-signed certificate for the current hostname(s)")
663 end
664 end
665
666 local function sh_esc(s)
667 return "'" .. s:gsub("'", "'\\''") .. "'";
668 end
669
670 local function copy(from, to, umask, owner, group)
671 local old_umask = umask and pposix.umask(umask);
672 local attrs = lfs.attributes(to);
673 if attrs then -- Move old file out of the way
674 local backup = to..".bkp~"..os.date("%FT%T", attrs.change);
675 os.rename(to, backup);
676 end
677 -- FIXME friendlier error handling, maybe move above backup back?
678 local input = assert(io.open(from));
679 local output = assert(io.open(to, "w"));
680 local data = input:read(2^11);
681 while data and output:write(data) do
682 data = input:read(2^11);
683 end
684 assert(input:close());
685 assert(output:close());
686 if not prosody.installed then
687 -- FIXME this is possibly specific to GNU chown
688 os.execute(("chown -c --reference=%s %s"):format(sh_esc(cert_basedir), sh_esc(to)));
689 elseif owner and group then
690 local ok = os.execute(("chown %s:%s %s"):format(sh_esc(owner), sh_esc(group), sh_esc(to)));
691 assert(ok == true or ok == 0, "Failed to change ownership of "..to);
692 end
693 if old_umask then pposix.umask(old_umask); end
694 return true;
695 end
696
697 function cert_commands.import(arg)
698 local hostnames = {};
699 -- Move hostname arguments out of arg, the rest should be a list of paths
700 while arg[1] and prosody.hosts[ arg[1] ] do
701 table.insert(hostnames, table.remove(arg, 1));
702 end
703 if hostnames[1] == nil then
704 local domains = os.getenv"RENEWED_DOMAINS"; -- Set if invoked via certbot
705 if domains then
706 for host in domains:gmatch("%S+") do
707 table.insert(hostnames, host);
708 end
709 else
710 for host in pairs(prosody.hosts) do
711 if host ~= "*" and configmanager.get(host, "enabled") ~= false then
712 table.insert(hostnames, host);
713 end
714 end
715 end
716 end
717 if not arg[1] or arg[1] == "--help" then -- Probably forgot the path
718 show_usage("cert import [HOSTNAME+] /path/to/certs [/other/paths/]+",
719 "Copies certificates to "..cert_basedir);
720 return 1;
721 end
722 local owner, group;
723 if pposix.getuid() == 0 then -- We need root to change ownership
724 owner = configmanager.get("*", "prosody_user") or "prosody";
725 group = configmanager.get("*", "prosody_group") or owner;
726 end
727 local cm = require "core.certmanager";
728 local imported = {};
729 for _, host in ipairs(hostnames) do
730 for _, dir in ipairs(arg) do
731 local paths = cm.find_cert(dir, host);
732 if paths then
733 copy(paths.certificate, cert_basedir .. "/" .. host .. ".crt", nil, owner, group);
734 copy(paths.key, cert_basedir .. "/" .. host .. ".key", "0377", owner, group);
735 table.insert(imported, host);
736 else
737 -- TODO Say where we looked
738 show_warning("No certificate for host "..host.." found :(");
739 end
740 -- TODO Additional checks
741 -- Certificate names matches the hostname
742 -- Private key matches public key in certificate
743 end
744 end
745 if imported[1] then
746 show_message("Imported certificate and key for hosts "..table.concat(imported, ", "));
747 local ok, err = prosodyctl.reload();
748 if not ok and err ~= "not-running" then
749 show_message(error_messages[err]);
750 end
751 else
752 show_warning("No certificates imported :(");
753 return 1;
754 end
755 end
756
757 function commands.cert(arg)
758 if #arg >= 1 and arg[1] ~= "--help" then
759 openssl = require "util.openssl";
760 lfs = require "lfs";
761 local cert_dir_attrs = lfs.attributes(cert_basedir);
762 if not cert_dir_attrs then
763 show_warning("The directory "..cert_basedir.." does not exist");
764 return 1; -- TODO Should we create it?
765 end
766 local uid = pposix.getuid();
767 if uid ~= 0 and uid ~= cert_dir_attrs.uid then
768 show_warning("The directory "..cert_basedir.." is not owned by the current user, won't be able to write files to it");
769 return 1;
770 elseif not cert_dir_attrs.permissions then -- COMPAT with LuaFilesystem < 1.6.2 (hey CentOS!)
771 show_message("Unable to check permissions on "..cert_basedir.." (LuaFilesystem 1.6.2+ required)");
772 show_message("Please confirm that Prosody (and only Prosody) can write to this directory)");
773 elseif cert_dir_attrs.permissions:match("^%.w..%-..%-.$") then
774 show_warning("The directory "..cert_basedir.." not only writable by its owner");
775 return 1;
776 end
777 local subcmd = table.remove(arg, 1);
778 if type(cert_commands[subcmd]) == "function" then
779 if subcmd ~= "import" then -- hostnames are optional for import
780 if not arg[1] then
781 show_message"You need to supply at least one hostname"
782 arg = { "--help" };
783 end
784 if arg[1] ~= "--help" and not prosody.hosts[arg[1]] then
785 show_message(error_messages["no-such-host"]);
786 return 1;
787 end
788 end
789 return cert_commands[subcmd](arg);
790 elseif subcmd == "check" then
791 return commands.check({"certs"});
792 end
793 end
794 show_usage("cert config|request|generate|key|import", "Helpers for generating X.509 certificates and keys.")
795 for _, cmd in pairs(cert_commands) do
796 print()
797 cmd{ "--help" }
798 end
799 end
800
801 function commands.check(arg)
802 if arg[1] == "--help" then
803 show_usage([[check]], [[Perform basic checks on your Prosody installation]]);
804 return 1;
805 end
806 local what = table.remove(arg, 1);
807 local set = require "util.set";
808 local it = require "util.iterators";
809 local ok = true;
810 local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end
811 local function enabled_hosts() return it.filter(disabled_hosts, pairs(configmanager.getconfig())); end
812 if not (what == nil or what == "disabled" or what == "config" or what == "dns" or what == "certs") then
813 show_warning("Don't know how to check '%s'. Try one of 'config', 'dns', 'certs' or 'disabled'.", what);
814 return 1;
815 end
816 if not what or what == "disabled" then
817 local disabled_hosts_set = set.new();
818 for host, host_options in it.filter("*", pairs(configmanager.getconfig())) do
819 if host_options.enabled == false then
820 disabled_hosts_set:add(host);
821 end
822 end
823 if not disabled_hosts_set:empty() then
824 local msg = "Checks will be skipped for these disabled hosts: %s";
825 if what then msg = "These hosts are disabled: %s"; end
826 show_warning(msg, tostring(disabled_hosts_set));
827 if what then return 0; end
828 print""
829 end
830 end
831 if not what or what == "config" then
832 print("Checking config...");
833 local deprecated = set.new({
834 "bosh_ports", "disallow_s2s", "no_daemonize", "anonymous_login", "require_encryption",
835 "vcard_compatibility",
836 });
837 local known_global_options = set.new({
838 "pidfile", "log", "plugin_paths", "prosody_user", "prosody_group", "daemonize",
839 "umask", "prosodyctl_timeout", "use_ipv6", "use_libevent", "network_settings",
840 "network_backend", "http_default_host",
841 "statistics_interval", "statistics", "statistics_config",
842 });
843 local config = configmanager.getconfig();
844 -- Check that we have any global options (caused by putting a host at the top)
845 if it.count(it.filter("log", pairs(config["*"]))) == 0 then
846 ok = false;
847 print("");
848 print(" No global options defined. Perhaps you have put a host definition at the top")
849 print(" of the config file? They should be at the bottom, see https://prosody.im/doc/configure#overview");
850 end
851 if it.count(enabled_hosts()) == 0 then
852 ok = false;
853 print("");
854 if it.count(it.filter("*", pairs(config))) == 0 then
855 print(" No hosts are defined, please add at least one VirtualHost section")
856 elseif config["*"]["enabled"] == false then
857 print(" No hosts are enabled. Remove enabled = false from the global section or put enabled = true under at least one VirtualHost section")
858 else
859 print(" All hosts are disabled. Remove enabled = false from at least one VirtualHost section")
860 end
861 end
862 if not config["*"].modules_enabled then
863 print(" No global modules_enabled is set?");
864 local suggested_global_modules;
865 for host, options in enabled_hosts() do --luacheck: ignore 213/host
866 if not options.component_module and options.modules_enabled then
867 suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled));
868 end
869 end
870 if suggested_global_modules and not suggested_global_modules:empty() then
871 print(" Consider moving these modules into modules_enabled in the global section:")
872 print(" "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end));
873 end
874 print();
875 end
876
877 do -- Check for modules enabled both normally and as components
878 local modules = set.new(config["*"]["modules_enabled"]);
879 for host, options in enabled_hosts() do
880 local component_module = options.component_module;
881 if component_module and modules:contains(component_module) then
882 print((" mod_%s is enabled both in modules_enabled and as Component %q %q"):format(component_module, host, component_module));
883 print(" This means the service is enabled on all VirtualHosts as well as the Component.");
884 print(" Are you sure this what you want? It may cause unexpected behaviour.");
885 end
886 end
887 end
888
889 -- Check for global options under hosts
890 local global_options = set.new(it.to_array(it.keys(config["*"])));
891 local deprecated_global_options = set.intersection(global_options, deprecated);
892 if not deprecated_global_options:empty() then
893 print("");
894 print(" You have some deprecated options in the global section:");
895 print(" "..tostring(deprecated_global_options))
896 ok = false;
897 end
898 for host, options in it.filter(function (h) return h ~= "*" end, pairs(configmanager.getconfig())) do
899 local host_options = set.new(it.to_array(it.keys(options)));
900 local misplaced_options = set.intersection(host_options, known_global_options);
901 for name in pairs(options) do
902 if name:match("^interfaces?")
903 or name:match("_ports?$") or name:match("_interfaces?$")
904 or (name:match("_ssl$") and not name:match("^[cs]2s_ssl$")) then
905 misplaced_options:add(name);
906 end
907 end
908 if not misplaced_options:empty() then
909 ok = false;
910 print("");
911 local n = it.count(misplaced_options);
912 print(" You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be");
913 print(" in the global section of the config file, above any VirtualHost or Component definitions,")
914 print(" see https://prosody.im/doc/configure#overview for more information.")
915 print("");
916 print(" You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", "));
917 end
918 end
919 for host, options in enabled_hosts() do
920 local host_options = set.new(it.to_array(it.keys(options)));
921 local subdomain = host:match("^[^.]+");
922 if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp"
923 or subdomain == "chat" or subdomain == "im") then
924 print("");
925 print(" Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to");
926 print(" "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host..".");
927 print(" For more information see: https://prosody.im/doc/dns");
928 end
929 end
930 local all_modules = set.new(config["*"].modules_enabled);
931 local all_options = set.new(it.to_array(it.keys(config["*"])));
932 for host in enabled_hosts() do
933 all_options:include(set.new(it.to_array(it.keys(config[host]))));
934 all_modules:include(set.new(config[host].modules_enabled));
935 end
936 for mod in all_modules do
937 if mod:match("^mod_") then
938 print("");
939 print(" Modules in modules_enabled should not have the 'mod_' prefix included.");
940 print(" Change '"..mod.."' to '"..mod:match("^mod_(.*)").."'.");
941 elseif mod:match("^auth_") then
942 print("");
943 print(" Authentication modules should not be added to modules_enabled,");
944 print(" but be specified in the 'authentication' option.");
945 print(" Remove '"..mod.."' from modules_enabled and instead add");
946 print(" authentication = '"..mod:match("^auth_(.*)").."'");
947 print(" For more information see https://prosody.im/doc/authentication");
948 elseif mod:match("^storage_") then
949 print("");
950 print(" storage modules should not be added to modules_enabled,");
951 print(" but be specified in the 'storage' option.");
952 print(" Remove '"..mod.."' from modules_enabled and instead add");
953 print(" storage = '"..mod:match("^storage_(.*)").."'");
954 print(" For more information see https://prosody.im/doc/storage");
955 end
956 end
957 if all_modules:contains("vcard") and all_modules:contains("vcard_legacy") then
958 print("");
959 print(" Both mod_vcard_legacy and mod_vcard are enabled but they conflict");
960 print(" with each other. Remove one.");
961 end
962 if all_modules:contains("pep") and all_modules:contains("pep_simple") then
963 print("");
964 print(" Both mod_pep_simple and mod_pep are enabled but they conflict");
965 print(" with each other. Remove one.");
966 end
967 for host, host_config in pairs(config) do --luacheck: ignore 213/host
968 if type(rawget(host_config, "storage")) == "string" and rawget(host_config, "default_storage") then
969 print("");
970 print(" The 'default_storage' option is not needed if 'storage' is set to a string.");
971 break;
972 end
973 end
974 local require_encryption = set.intersection(all_options, set.new({
975 "require_encryption", "c2s_require_encryption", "s2s_require_encryption"
976 })):empty();
977 local ssl = dependencies.softreq"ssl";
978 if not ssl then
979 if not require_encryption then
980 print("");
981 print(" You require encryption but LuaSec is not available.");
982 print(" Connections will fail.");
983 ok = false;
984 end
985 elseif not ssl.loadcertificate then
986 if all_options:contains("s2s_secure_auth") then
987 print("");
988 print(" You have set s2s_secure_auth but your version of LuaSec does ");
989 print(" not support certificate validation, so all s2s connections will");
990 print(" fail.");
991 ok = false;
992 elseif all_options:contains("s2s_secure_domains") then
993 local secure_domains = set.new();
994 for host in enabled_hosts() do
995 if config[host].s2s_secure_auth == true then
996 secure_domains:add("*");
997 else
998 secure_domains:include(set.new(config[host].s2s_secure_domains));
999 end
1000 end
1001 if not secure_domains:empty() then
1002 print("");
1003 print(" You have set s2s_secure_domains but your version of LuaSec does ");
1004 print(" not support certificate validation, so s2s connections to/from ");
1005 print(" these domains will fail.");
1006 ok = false;
1007 end
1008 end
1009 elseif require_encryption and not all_modules:contains("tls") then
1010 print("");
1011 print(" You require encryption but mod_tls is not enabled.");
1012 print(" Connections will fail.");
1013 ok = false;
1014 end
1015
1016 print("Done.\n");
1017 end
1018 if not what or what == "dns" then
1019 local dns = require "net.dns";
1020 local idna = require "util.encodings".idna;
1021 local ip = require "util.ip";
1022 local c2s_ports = set.new(configmanager.get("*", "c2s_ports") or {5222});
1023 local s2s_ports = set.new(configmanager.get("*", "s2s_ports") or {5269});
1024
1025 local c2s_srv_required, s2s_srv_required;
1026 if not c2s_ports:contains(5222) then
1027 c2s_srv_required = true;
1028 end
1029 if not s2s_ports:contains(5269) then
1030 s2s_srv_required = true;
1031 end
1032
1033 local problem_hosts = set.new();
1034
1035 local external_addresses, internal_addresses = set.new(), set.new();
1036
1037 local fqdn = socket.dns.tohostname(socket.dns.gethostname());
1038 if fqdn then
1039 do
1040 local res = dns.lookup(idna.to_ascii(fqdn), "A");
1041 if res then
1042 for _, record in ipairs(res) do
1043 external_addresses:add(record.a);
1044 end
1045 end
1046 end
1047 do
1048 local res = dns.lookup(idna.to_ascii(fqdn), "AAAA");
1049 if res then
1050 for _, record in ipairs(res) do
1051 external_addresses:add(record.aaaa);
1052 end
1053 end
1054 end
1055 end
1056
1057 local local_addresses = require"util.net".local_addresses() or {};
1058
1059 for addr in it.values(local_addresses) do
1060 if not ip.new_ip(addr).private then
1061 external_addresses:add(addr);
1062 else
1063 internal_addresses:add(addr);
1064 end
1065 end
1066
1067 if external_addresses:empty() then
1068 print("");
1069 print(" Failed to determine the external addresses of this server. Checks may be inaccurate.");
1070 c2s_srv_required, s2s_srv_required = true, true;
1071 end
1072
1073 local v6_supported = not not socket.tcp6;
1074
1075 for jid, host_options in enabled_hosts() do
1076 local all_targets_ok, some_targets_ok = true, false;
1077 local node, host = jid_split(jid);
1078
1079 local modules, component_module = modulemanager.get_modules_for_host(host);
1080 if component_module then
1081 modules:add(component_module);
1082 end
1083
1084 local is_component = not not host_options.component_module;
1085 print("Checking DNS for "..(is_component and "component" or "host").." "..jid.."...");
1086 if node then
1087 print("Only the domain part ("..host..") is used in DNS.")
1088 end
1089 local target_hosts = set.new();
1090 if modules:contains("c2s") then
1091 local res = dns.lookup("_xmpp-client._tcp."..idna.to_ascii(host)..".", "SRV");
1092 if res then
1093 for _, record in ipairs(res) do
1094 target_hosts:add(record.srv.target);
1095 if not c2s_ports:contains(record.srv.port) then
1096 print(" SRV target "..record.srv.target.." contains unknown client port: "..record.srv.port);
1097 end
1098 end
1099 else
1100 if c2s_srv_required then
1101 print(" No _xmpp-client SRV record found for "..host..", but it looks like you need one.");
1102 all_targets_ok = false;
1103 else
1104 target_hosts:add(host);
1105 end
1106 end
1107 end
1108 if modules:contains("s2s") then
1109 local res = dns.lookup("_xmpp-server._tcp."..idna.to_ascii(host)..".", "SRV");
1110 if res then
1111 for _, record in ipairs(res) do
1112 target_hosts:add(record.srv.target);
1113 if not s2s_ports:contains(record.srv.port) then
1114 print(" SRV target "..record.srv.target.." contains unknown server port: "..record.srv.port);
1115 end
1116 end
1117 else
1118 if s2s_srv_required then
1119 print(" No _xmpp-server SRV record found for "..host..", but it looks like you need one.");
1120 all_targets_ok = false;
1121 else
1122 target_hosts:add(host);
1123 end
1124 end
1125 end
1126 if target_hosts:empty() then
1127 target_hosts:add(host);
1128 end
1129
1130 if target_hosts:contains("localhost") then
1131 print(" Target 'localhost' cannot be accessed from other servers");
1132 target_hosts:remove("localhost");
1133 end
1134
1135 if modules:contains("proxy65") then
1136 local proxy65_target = configmanager.get(host, "proxy65_address") or host;
1137 if type(proxy65_target) == "string" then
1138 local A, AAAA = dns.lookup(idna.to_ascii(proxy65_target), "A"), dns.lookup(idna.to_ascii(proxy65_target), "AAAA");
1139 local prob = {};
1140 if not A then
1141 table.insert(prob, "A");
1142 end
1143 if v6_supported and not AAAA then
1144 table.insert(prob, "AAAA");
1145 end
1146 if #prob > 0 then
1147 print(" File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/")
1148 .." record. Create one or set 'proxy65_address' to the correct host/IP.");
1149 end
1150 else
1151 print(" proxy65_address for "..host.." should be set to a string, unable to perform DNS check");
1152 end
1153 end
1154
1155 for target_host in target_hosts do
1156 local host_ok_v4, host_ok_v6;
1157 do
1158 local res = dns.lookup(idna.to_ascii(target_host), "A");
1159 if res then
1160 for _, record in ipairs(res) do
1161 if external_addresses:contains(record.a) then
1162 some_targets_ok = true;
1163 host_ok_v4 = true;
1164 elseif internal_addresses:contains(record.a) then
1165 host_ok_v4 = true;
1166 some_targets_ok = true;
1167 print(" "..target_host.." A record points to internal address, external connections might fail");
1168 else
1169 print(" "..target_host.." A record points to unknown address "..record.a);
1170 all_targets_ok = false;
1171 end
1172 end
1173 end
1174 end
1175 do
1176 local res = dns.lookup(idna.to_ascii(target_host), "AAAA");
1177 if res then
1178 for _, record in ipairs(res) do
1179 if external_addresses:contains(record.aaaa) then
1180 some_targets_ok = true;
1181 host_ok_v6 = true;
1182 elseif internal_addresses:contains(record.aaaa) then
1183 host_ok_v6 = true;
1184 some_targets_ok = true;
1185 print(" "..target_host.." AAAA record points to internal address, external connections might fail");
1186 else
1187 print(" "..target_host.." AAAA record points to unknown address "..record.aaaa);
1188 all_targets_ok = false;
1189 end
1190 end
1191 end
1192 end
1193
1194 local bad_protos = {}
1195 if not host_ok_v4 then
1196 table.insert(bad_protos, "IPv4");
1197 end
1198 if not host_ok_v6 then
1199 table.insert(bad_protos, "IPv6");
1200 end
1201 if #bad_protos > 0 then
1202 print(" Host "..target_host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")");
1203 end
1204 if host_ok_v6 and not v6_supported then
1205 print(" Host "..target_host.." has AAAA records, but your version of LuaSocket does not support IPv6.");
1206 print(" Please see https://prosody.im/doc/ipv6 for more information.");
1207 end
1208 end
1209 if not all_targets_ok then
1210 print(" "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server.");
1211 if is_component then
1212 print(" DNS records are necessary if you want users on other servers to access this component.");
1213 end
1214 problem_hosts:add(host);
1215 end
1216 print("");
1217 end
1218 if not problem_hosts:empty() then
1219 print("");
1220 print("For more information about DNS configuration please see https://prosody.im/doc/dns");
1221 print("");
1222 ok = false;
1223 end
1224 end
1225 if not what or what == "certs" then
1226 local cert_ok;
1227 print"Checking certificates..."
1228 local x509_verify_identity = require"util.x509".verify_identity;
1229 local create_context = require "core.certmanager".create_context;
1230 local ssl = dependencies.softreq"ssl";
1231 -- local datetime_parse = require"util.datetime".parse_x509;
1232 local load_cert = ssl and ssl.loadcertificate;
1233 -- or ssl.cert_from_pem
1234 if not ssl then
1235 print("LuaSec not available, can't perform certificate checks")
1236 if what == "certs" then cert_ok = false end
1237 elseif not load_cert then
1238 print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking");
1239 cert_ok = false
1240 else
1241 local function skip_bare_jid_hosts(host)
1242 if jid_split(host) then
1243 -- See issue #779
1244 return false;
1245 end
1246 return true;
1247 end
1248 for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do
1249 print("Checking certificate for "..host);
1250 -- First, let's find out what certificate this host uses.
1251 local host_ssl_config = configmanager.rawget(host, "ssl")
1252 or configmanager.rawget(host:match("%.(.*)"), "ssl");
1253 local global_ssl_config = configmanager.rawget("*", "ssl");
1254 local ok, err, ssl_config = create_context(host, "server", host_ssl_config, global_ssl_config);
1255 if not ok then
1256 print(" Error: "..err);
1257 cert_ok = false
1258 elseif not ssl_config.certificate then
1259 print(" No 'certificate' found for "..host)
1260 cert_ok = false
1261 elseif not ssl_config.key then
1262 print(" No 'key' found for "..host)
1263 cert_ok = false
1264 else
1265 local key, err = io.open(ssl_config.key); -- Permissions check only
1266 if not key then
1267 print(" Could not open "..ssl_config.key..": "..err);
1268 cert_ok = false
1269 else
1270 key:close();
1271 end
1272 local cert_fh, err = io.open(ssl_config.certificate); -- Load the file.
1273 if not cert_fh then
1274 print(" Could not open "..ssl_config.certificate..": "..err);
1275 cert_ok = false
1276 else
1277 print(" Certificate: "..ssl_config.certificate)
1278 local cert = load_cert(cert_fh:read"*a"); cert_fh:close();
1279 if not cert:validat(os.time()) then
1280 print(" Certificate has expired.")
1281 cert_ok = false
1282 elseif not cert:validat(os.time() + 86400) then
1283 print(" Certificate expires within one day.")
1284 cert_ok = false
1285 elseif not cert:validat(os.time() + 86400*7) then
1286 print(" Certificate expires within one week.")
1287 elseif not cert:validat(os.time() + 86400*31) then
1288 print(" Certificate expires within one month.")
1289 end
1290 if configmanager.get(host, "component_module") == nil
1291 and not x509_verify_identity(host, "_xmpp-client", cert) then
1292 print(" Not valid for client connections to "..host..".")
1293 cert_ok = false
1294 end
1295 if (not (configmanager.get(host, "anonymous_login")
1296 or configmanager.get(host, "authentication") == "anonymous"))
1297 and not x509_verify_identity(host, "_xmpp-server", cert) then
1298 print(" Not valid for server-to-server connections to "..host..".")
1299 cert_ok = false
1300 end
1301 end
1302 end
1303 end
1304 end
1305 if cert_ok == false then
1306 print("")
1307 print("For more information about certificates please see https://prosody.im/doc/certificates");
1308 ok = false
1309 end
1310 print("")
1311 end
1312 if not ok then
1313 print("Problems found, see above.");
1314 else
1315 print("All checks passed, congratulations!");
1316 end
1317 return ok and 0 or 2;
1318 end 563 end
1319 564
1320 --------------------- 565 ---------------------
1321 566
1322 local async = require "util.async"; 567 local async = require "util.async";
1338 show_message("Failed to load module '"..module_name.."': "..err); 583 show_message("Failed to load module '"..module_name.."': "..err);
1339 os.exit(1); 584 os.exit(1);
1340 end 585 end
1341 end 586 end
1342 587
1343 table.remove(arg, 1);
1344
1345 local module = modulemanager.get_module("*", module_name); 588 local module = modulemanager.get_module("*", module_name);
1346 if not module then 589 if not module then
1347 show_message("Failed to load module '"..module_name.."': Unknown error"); 590 show_message("Failed to load module '"..module_name.."': Unknown error");
1348 os.exit(1); 591 os.exit(1);
1349 end 592 end
1365 show_message("Failed to execute command: "..error_messages[ret]); 608 show_message("Failed to execute command: "..error_messages[ret]);
1366 os.exit(1); -- :( 609 os.exit(1); -- :(
1367 end 610 end
1368 end 611 end
1369 612
613 if command and not commands[command] then
614 local ok, command_module = pcall(require, "util.prosodyctl."..command);
615 if ok and command_module[command] then
616 commands[command] = command_module[command];
617 end
618 end
619
1370 if not commands[command] then -- Show help for all commands 620 if not commands[command] then -- Show help for all commands
1371 function show_usage(usage, desc) 621 function show_usage(usage, desc)
1372 print(" "..usage); 622 print(" "..usage);
1373 print(" "..desc); 623 print(" "..desc);
1374 end 624 end
1378 print("Usage: "..arg[0].." COMMAND [OPTIONS]"); 628 print("Usage: "..arg[0].." COMMAND [OPTIONS]");
1379 print(""); 629 print("");
1380 print("Where COMMAND may be one of:\n"); 630 print("Where COMMAND may be one of:\n");
1381 631
1382 local hidden_commands = require "util.set".new{ "register", "unregister", "addplugin" }; 632 local hidden_commands = require "util.set".new{ "register", "unregister", "addplugin" };
1383 local commands_order = { "adduser", "passwd", "deluser", "start", "stop", "restart", "reload", "about" }; 633 local commands_order = { "install", "remove", "list", "adduser", "passwd", "deluser", "start", "stop", "restart", "reload",
634 "about" };
1384 635
1385 local done = {}; 636 local done = {};
1386 637
1387 for _, command_name in ipairs(commands_order) do 638 for _, command_name in ipairs(commands_order) do
1388 local command_func = commands[command_name]; 639 local command_func = commands[command_name];
1403 654
1404 655
1405 os.exit(0); 656 os.exit(0);
1406 end 657 end
1407 658
1408 os.exit(commands[command]({ select(2, unpack(arg)) })); 659 os.exit(commands[command](arg));
1409 end, watchers); 660 end, watchers);
1410 661
1411 command_runner:run(true); 662 command_runner:run(true);