Comparison

prosodyctl @ 10871:e5dee71d0ebb

prosodyctl+util.prosodyctl.*: Start breaking up the ever-growing prosodyctl
author Matthew Wild <mwild1@gmail.com>
date Tue, 02 Jun 2020 08:01:21 +0100
parent 10858:efa49d484560
child 10905:709255e332d8
comparison
equal deleted inserted replaced
10870:3f1889608f3e 10871:e5dee71d0ebb
47 local startup = require "util.startup"; 47 local startup = require "util.startup";
48 startup.prosodyctl(); 48 startup.prosodyctl();
49 49
50 ----------- 50 -----------
51 51
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 });
65
66 local configmanager = require "core.configmanager"; 52 local configmanager = require "core.configmanager";
67 local modulemanager = require "core.modulemanager" 53 local modulemanager = require "core.modulemanager"
68 local prosodyctl = require "util.prosodyctl" 54 local prosodyctl = require "util.prosodyctl"
69 local socket = require "socket" 55 local socket = require "socket"
70 local dependencies = require "util.dependencies"; 56 local dependencies = require "util.dependencies";
71 local lfs = dependencies.softreq "lfs"; 57 local lfs = dependencies.softreq "lfs";
72 58
73 ----------------------- 59 -----------------------
74 60
61 local human_io = require "util.human.io";
62
75 local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning; 63 local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning;
76 local show_usage = prosodyctl.show_usage; 64 local show_usage = prosodyctl.show_usage;
77 local show_yesno = prosodyctl.show_yesno; 65 local read_password = human_io.read_password;
78 local show_prompt = prosodyctl.show_prompt;
79 local read_password = prosodyctl.read_password;
80 local call_luarocks = prosodyctl.call_luarocks; 66 local call_luarocks = prosodyctl.call_luarocks;
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 -----------------------
548 535
549 if ok then return 0; end 536 if ok then return 0; end
550 537
551 show_message(error_messages[msg]) 538 show_message(error_messages[msg])
552 return 1; 539 return 1;
553 end
554
555 local openssl;
556
557 local cert_commands = {};
558
559 -- If a file already exists, ask if the user wants to use it or replace it
560 -- Backups the old file if replaced
561 local function use_existing(filename)
562 local attrs = lfs.attributes(filename);
563 if attrs then
564 if show_yesno(filename .. " exists, do you want to replace it? [y/n]") then
565 local backup = filename..".bkp~"..os.date("%FT%T", attrs.change);
566 os.rename(filename, backup);
567 show_message(filename.." backed up to "..backup);
568 else
569 -- Use the existing file
570 return true;
571 end
572 end
573 end
574
575 local have_pposix, pposix = pcall(require, "util.pposix");
576 local cert_basedir = prosody.paths.data == "." and "./certs" or prosody.paths.data;
577 if have_pposix and pposix.getuid() == 0 then
578 -- FIXME should be enough to check if this directory is writable
579 local cert_dir = configmanager.get("*", "certificates") or "certs";
580 cert_basedir = configmanager.resolve_relative_path(prosody.paths.config, cert_dir);
581 end
582
583 function cert_commands.config(arg)
584 if #arg >= 1 and arg[1] ~= "--help" then
585 local conf_filename = cert_basedir .. "/" .. arg[1] .. ".cnf";
586 if use_existing(conf_filename) then
587 return nil, conf_filename;
588 end
589 local distinguished_name;
590 if arg[#arg]:find("^/") then
591 distinguished_name = table.remove(arg);
592 end
593 local conf = openssl.config.new();
594 conf:from_prosody(prosody.hosts, configmanager, arg);
595 if distinguished_name then
596 local dn = {};
597 for k, v in distinguished_name:gmatch("/([^=/]+)=([^/]+)") do
598 table.insert(dn, k);
599 dn[k] = v;
600 end
601 conf.distinguished_name = dn;
602 else
603 show_message("Please provide details to include in the certificate config file.");
604 show_message("Leave the field empty to use the default value or '.' to exclude the field.")
605 for _, k in ipairs(openssl._DN_order) do
606 local v = conf.distinguished_name[k];
607 if v then
608 local nv = nil;
609 if k == "commonName" then
610 v = arg[1]
611 elseif k == "emailAddress" then
612 v = "xmpp@" .. arg[1];
613 elseif k == "countryName" then
614 local tld = arg[1]:match"%.([a-z]+)$";
615 if tld and #tld == 2 and tld ~= "uk" then
616 v = tld:upper();
617 end
618 end
619 nv = show_prompt(("%s (%s):"):format(k, nv or v));
620 nv = (not nv or nv == "") and v or nv;
621 if nv:find"[\192-\252][\128-\191]+" then
622 conf.req.string_mask = "utf8only"
623 end
624 conf.distinguished_name[k] = nv ~= "." and nv or nil;
625 end
626 end
627 end
628 local conf_file, err = io.open(conf_filename, "w");
629 if not conf_file then
630 show_warning("Could not open OpenSSL config file for writing");
631 show_warning(err);
632 os.exit(1);
633 end
634 conf_file:write(conf:serialize());
635 conf_file:close();
636 print("");
637 show_message("Config written to " .. conf_filename);
638 return nil, conf_filename;
639 else
640 show_usage("cert config HOSTNAME [HOSTNAME+]", "Builds a certificate config file covering the supplied hostname(s)")
641 end
642 end
643
644 function cert_commands.key(arg)
645 if #arg >= 1 and arg[1] ~= "--help" then
646 local key_filename = cert_basedir .. "/" .. arg[1] .. ".key";
647 if use_existing(key_filename) then
648 return nil, key_filename;
649 end
650 os.remove(key_filename); -- This file, if it exists is unlikely to have write permissions
651 local key_size = tonumber(arg[2] or show_prompt("Choose key size (2048):") or 2048);
652 local old_umask = pposix.umask("0377");
653 if openssl.genrsa{out=key_filename, key_size} then
654 os.execute(("chmod 400 '%s'"):format(key_filename));
655 show_message("Key written to ".. key_filename);
656 pposix.umask(old_umask);
657 return nil, key_filename;
658 end
659 show_message("There was a problem, see OpenSSL output");
660 else
661 show_usage("cert key HOSTNAME <bits>", "Generates a RSA key named HOSTNAME.key\n "
662 .."Prompts for a key size if none given")
663 end
664 end
665
666 function cert_commands.request(arg)
667 if #arg >= 1 and arg[1] ~= "--help" then
668 local req_filename = cert_basedir .. "/" .. arg[1] .. ".req";
669 if use_existing(req_filename) then
670 return nil, req_filename;
671 end
672 local _, key_filename = cert_commands.key({arg[1]});
673 local _, conf_filename = cert_commands.config(arg);
674 if openssl.req{new=true, key=key_filename, utf8=true, sha256=true, config=conf_filename, out=req_filename} then
675 show_message("Certificate request written to ".. req_filename);
676 else
677 show_message("There was a problem, see OpenSSL output");
678 end
679 else
680 show_usage("cert request HOSTNAME [HOSTNAME+]", "Generates a certificate request for the supplied hostname(s)")
681 end
682 end
683
684 function cert_commands.generate(arg)
685 if #arg >= 1 and arg[1] ~= "--help" then
686 local cert_filename = cert_basedir .. "/" .. arg[1] .. ".crt";
687 if use_existing(cert_filename) then
688 return nil, cert_filename;
689 end
690 local _, key_filename = cert_commands.key({arg[1]});
691 local _, conf_filename = cert_commands.config(arg);
692 if key_filename and conf_filename and cert_filename
693 and openssl.req{new=true, x509=true, nodes=true, key=key_filename,
694 days=365, sha256=true, utf8=true, config=conf_filename, out=cert_filename} then
695 show_message("Certificate written to ".. cert_filename);
696 print();
697 else
698 show_message("There was a problem, see OpenSSL output");
699 end
700 else
701 show_usage("cert generate HOSTNAME [HOSTNAME+]", "Generates a self-signed certificate for the current hostname(s)")
702 end
703 end
704
705 local function sh_esc(s)
706 return "'" .. s:gsub("'", "'\\''") .. "'";
707 end
708
709 local function copy(from, to, umask, owner, group)
710 local old_umask = umask and pposix.umask(umask);
711 local attrs = lfs.attributes(to);
712 if attrs then -- Move old file out of the way
713 local backup = to..".bkp~"..os.date("%FT%T", attrs.change);
714 os.rename(to, backup);
715 end
716 -- FIXME friendlier error handling, maybe move above backup back?
717 local input = assert(io.open(from));
718 local output = assert(io.open(to, "w"));
719 local data = input:read(2^11);
720 while data and output:write(data) do
721 data = input:read(2^11);
722 end
723 assert(input:close());
724 assert(output:close());
725 if not prosody.installed then
726 -- FIXME this is possibly specific to GNU chown
727 os.execute(("chown -c --reference=%s %s"):format(sh_esc(cert_basedir), sh_esc(to)));
728 elseif owner and group then
729 local ok = os.execute(("chown %s:%s %s"):format(sh_esc(owner), sh_esc(group), sh_esc(to)));
730 assert(ok == true or ok == 0, "Failed to change ownership of "..to);
731 end
732 if old_umask then pposix.umask(old_umask); end
733 return true;
734 end
735
736 function cert_commands.import(arg)
737 local hostnames = {};
738 -- Move hostname arguments out of arg, the rest should be a list of paths
739 while arg[1] and prosody.hosts[ arg[1] ] do
740 table.insert(hostnames, table.remove(arg, 1));
741 end
742 if hostnames[1] == nil then
743 local domains = os.getenv"RENEWED_DOMAINS"; -- Set if invoked via certbot
744 if domains then
745 for host in domains:gmatch("%S+") do
746 table.insert(hostnames, host);
747 end
748 else
749 for host in pairs(prosody.hosts) do
750 if host ~= "*" and configmanager.get(host, "enabled") ~= false then
751 table.insert(hostnames, host);
752 end
753 end
754 end
755 end
756 if not arg[1] or arg[1] == "--help" then -- Probably forgot the path
757 show_usage("cert import [HOSTNAME+] /path/to/certs [/other/paths/]+",
758 "Copies certificates to "..cert_basedir);
759 return 1;
760 end
761 local owner, group;
762 if pposix.getuid() == 0 then -- We need root to change ownership
763 owner = configmanager.get("*", "prosody_user") or "prosody";
764 group = configmanager.get("*", "prosody_group") or owner;
765 end
766 local cm = require "core.certmanager";
767 local imported = {};
768 for _, host in ipairs(hostnames) do
769 for _, dir in ipairs(arg) do
770 local paths = cm.find_cert(dir, host);
771 if paths then
772 copy(paths.certificate, cert_basedir .. "/" .. host .. ".crt", nil, owner, group);
773 copy(paths.key, cert_basedir .. "/" .. host .. ".key", "0377", owner, group);
774 table.insert(imported, host);
775 else
776 -- TODO Say where we looked
777 show_warning("No certificate for host "..host.." found :(");
778 end
779 -- TODO Additional checks
780 -- Certificate names matches the hostname
781 -- Private key matches public key in certificate
782 end
783 end
784 if imported[1] then
785 show_message("Imported certificate and key for hosts "..table.concat(imported, ", "));
786 local ok, err = prosodyctl.reload();
787 if not ok and err ~= "not-running" then
788 show_message(error_messages[err]);
789 end
790 else
791 show_warning("No certificates imported :(");
792 return 1;
793 end
794 end
795
796 function commands.cert(arg)
797 if #arg >= 1 and arg[1] ~= "--help" then
798 openssl = require "util.openssl";
799 lfs = require "lfs";
800 local cert_dir_attrs = lfs.attributes(cert_basedir);
801 if not cert_dir_attrs then
802 show_warning("The directory "..cert_basedir.." does not exist");
803 return 1; -- TODO Should we create it?
804 end
805 local uid = pposix.getuid();
806 if uid ~= 0 and uid ~= cert_dir_attrs.uid then
807 show_warning("The directory "..cert_basedir.." is not owned by the current user, won't be able to write files to it");
808 return 1;
809 elseif not cert_dir_attrs.permissions then -- COMPAT with LuaFilesystem < 1.6.2 (hey CentOS!)
810 show_message("Unable to check permissions on "..cert_basedir.." (LuaFilesystem 1.6.2+ required)");
811 show_message("Please confirm that Prosody (and only Prosody) can write to this directory)");
812 elseif cert_dir_attrs.permissions:match("^%.w..%-..%-.$") then
813 show_warning("The directory "..cert_basedir.." not only writable by its owner");
814 return 1;
815 end
816 local subcmd = table.remove(arg, 1);
817 if type(cert_commands[subcmd]) == "function" then
818 if subcmd ~= "import" then -- hostnames are optional for import
819 if not arg[1] then
820 show_message"You need to supply at least one hostname"
821 arg = { "--help" };
822 end
823 if arg[1] ~= "--help" and not prosody.hosts[arg[1]] then
824 show_message(error_messages["no-such-host"]);
825 return 1;
826 end
827 end
828 return cert_commands[subcmd](arg);
829 elseif subcmd == "check" then
830 return commands.check({"certs"});
831 end
832 end
833 show_usage("cert config|request|generate|key|import", "Helpers for generating X.509 certificates and keys.")
834 for _, cmd in pairs(cert_commands) do
835 print()
836 cmd{ "--help" }
837 end
838 end
839
840 function commands.check(arg)
841 if arg[1] == "--help" then
842 show_usage([[check]], [[Perform basic checks on your Prosody installation]]);
843 return 1;
844 end
845 local what = table.remove(arg, 1);
846 local set = require "util.set";
847 local it = require "util.iterators";
848 local ok = true;
849 local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end
850 local function enabled_hosts() return it.filter(disabled_hosts, pairs(configmanager.getconfig())); end
851 if not (what == nil or what == "disabled" or what == "config" or what == "dns" or what == "certs") then
852 show_warning("Don't know how to check '%s'. Try one of 'config', 'dns', 'certs' or 'disabled'.", what);
853 return 1;
854 end
855 if not what or what == "disabled" then
856 local disabled_hosts_set = set.new();
857 for host, host_options in it.filter("*", pairs(configmanager.getconfig())) do
858 if host_options.enabled == false then
859 disabled_hosts_set:add(host);
860 end
861 end
862 if not disabled_hosts_set:empty() then
863 local msg = "Checks will be skipped for these disabled hosts: %s";
864 if what then msg = "These hosts are disabled: %s"; end
865 show_warning(msg, tostring(disabled_hosts_set));
866 if what then return 0; end
867 print""
868 end
869 end
870 if not what or what == "config" then
871 print("Checking config...");
872 local deprecated = set.new({
873 "bosh_ports", "disallow_s2s", "no_daemonize", "anonymous_login", "require_encryption",
874 "vcard_compatibility", "cross_domain_bosh", "cross_domain_websocket", "daemonize",
875 });
876 local known_global_options = set.new({
877 "pidfile", "log", "plugin_paths", "prosody_user", "prosody_group", "daemonize",
878 "umask", "prosodyctl_timeout", "use_ipv6", "use_libevent", "network_settings",
879 "network_backend", "http_default_host",
880 "statistics_interval", "statistics", "statistics_config",
881 });
882 local config = configmanager.getconfig();
883 -- Check that we have any global options (caused by putting a host at the top)
884 if it.count(it.filter("log", pairs(config["*"]))) == 0 then
885 ok = false;
886 print("");
887 print(" No global options defined. Perhaps you have put a host definition at the top")
888 print(" of the config file? They should be at the bottom, see https://prosody.im/doc/configure#overview");
889 end
890 if it.count(enabled_hosts()) == 0 then
891 ok = false;
892 print("");
893 if it.count(it.filter("*", pairs(config))) == 0 then
894 print(" No hosts are defined, please add at least one VirtualHost section")
895 elseif config["*"]["enabled"] == false then
896 print(" No hosts are enabled. Remove enabled = false from the global section or put enabled = true under at least one VirtualHost section")
897 else
898 print(" All hosts are disabled. Remove enabled = false from at least one VirtualHost section")
899 end
900 end
901 if not config["*"].modules_enabled then
902 print(" No global modules_enabled is set?");
903 local suggested_global_modules;
904 for host, options in enabled_hosts() do --luacheck: ignore 213/host
905 if not options.component_module and options.modules_enabled then
906 suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled));
907 end
908 end
909 if suggested_global_modules and not suggested_global_modules:empty() then
910 print(" Consider moving these modules into modules_enabled in the global section:")
911 print(" "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end));
912 end
913 print();
914 end
915
916 do -- Check for modules enabled both normally and as components
917 local modules = set.new(config["*"]["modules_enabled"]);
918 for host, options in enabled_hosts() do
919 local component_module = options.component_module;
920 if component_module and modules:contains(component_module) then
921 print((" mod_%s is enabled both in modules_enabled and as Component %q %q"):format(component_module, host, component_module));
922 print(" This means the service is enabled on all VirtualHosts as well as the Component.");
923 print(" Are you sure this what you want? It may cause unexpected behaviour.");
924 end
925 end
926 end
927
928 -- Check for global options under hosts
929 local global_options = set.new(it.to_array(it.keys(config["*"])));
930 local deprecated_global_options = set.intersection(global_options, deprecated);
931 if not deprecated_global_options:empty() then
932 print("");
933 print(" You have some deprecated options in the global section:");
934 print(" "..tostring(deprecated_global_options))
935 ok = false;
936 end
937 for host, options in it.filter(function (h) return h ~= "*" end, pairs(configmanager.getconfig())) do
938 local host_options = set.new(it.to_array(it.keys(options)));
939 local misplaced_options = set.intersection(host_options, known_global_options);
940 for name in pairs(options) do
941 if name:match("^interfaces?")
942 or name:match("_ports?$") or name:match("_interfaces?$")
943 or (name:match("_ssl$") and not name:match("^[cs]2s_ssl$")) then
944 misplaced_options:add(name);
945 end
946 end
947 if not misplaced_options:empty() then
948 ok = false;
949 print("");
950 local n = it.count(misplaced_options);
951 print(" You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be");
952 print(" in the global section of the config file, above any VirtualHost or Component definitions,")
953 print(" see https://prosody.im/doc/configure#overview for more information.")
954 print("");
955 print(" You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", "));
956 end
957 end
958 for host, options in enabled_hosts() do
959 local host_options = set.new(it.to_array(it.keys(options)));
960 local subdomain = host:match("^[^.]+");
961 if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp"
962 or subdomain == "chat" or subdomain == "im") then
963 print("");
964 print(" Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to");
965 print(" "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host..".");
966 print(" For more information see: https://prosody.im/doc/dns");
967 end
968 end
969 local all_modules = set.new(config["*"].modules_enabled);
970 local all_options = set.new(it.to_array(it.keys(config["*"])));
971 for host in enabled_hosts() do
972 all_options:include(set.new(it.to_array(it.keys(config[host]))));
973 all_modules:include(set.new(config[host].modules_enabled));
974 end
975 for mod in all_modules do
976 if mod:match("^mod_") then
977 print("");
978 print(" Modules in modules_enabled should not have the 'mod_' prefix included.");
979 print(" Change '"..mod.."' to '"..mod:match("^mod_(.*)").."'.");
980 elseif mod:match("^auth_") then
981 print("");
982 print(" Authentication modules should not be added to modules_enabled,");
983 print(" but be specified in the 'authentication' option.");
984 print(" Remove '"..mod.."' from modules_enabled and instead add");
985 print(" authentication = '"..mod:match("^auth_(.*)").."'");
986 print(" For more information see https://prosody.im/doc/authentication");
987 elseif mod:match("^storage_") then
988 print("");
989 print(" storage modules should not be added to modules_enabled,");
990 print(" but be specified in the 'storage' option.");
991 print(" Remove '"..mod.."' from modules_enabled and instead add");
992 print(" storage = '"..mod:match("^storage_(.*)").."'");
993 print(" For more information see https://prosody.im/doc/storage");
994 end
995 end
996 if all_modules:contains("vcard") and all_modules:contains("vcard_legacy") then
997 print("");
998 print(" Both mod_vcard_legacy and mod_vcard are enabled but they conflict");
999 print(" with each other. Remove one.");
1000 end
1001 if all_modules:contains("pep") and all_modules:contains("pep_simple") then
1002 print("");
1003 print(" Both mod_pep_simple and mod_pep are enabled but they conflict");
1004 print(" with each other. Remove one.");
1005 end
1006 for host, host_config in pairs(config) do --luacheck: ignore 213/host
1007 if type(rawget(host_config, "storage")) == "string" and rawget(host_config, "default_storage") then
1008 print("");
1009 print(" The 'default_storage' option is not needed if 'storage' is set to a string.");
1010 break;
1011 end
1012 end
1013 local require_encryption = set.intersection(all_options, set.new({
1014 "require_encryption", "c2s_require_encryption", "s2s_require_encryption"
1015 })):empty();
1016 local ssl = dependencies.softreq"ssl";
1017 if not ssl then
1018 if not require_encryption then
1019 print("");
1020 print(" You require encryption but LuaSec is not available.");
1021 print(" Connections will fail.");
1022 ok = false;
1023 end
1024 elseif not ssl.loadcertificate then
1025 if all_options:contains("s2s_secure_auth") then
1026 print("");
1027 print(" You have set s2s_secure_auth but your version of LuaSec does ");
1028 print(" not support certificate validation, so all s2s connections will");
1029 print(" fail.");
1030 ok = false;
1031 elseif all_options:contains("s2s_secure_domains") then
1032 local secure_domains = set.new();
1033 for host in enabled_hosts() do
1034 if config[host].s2s_secure_auth == true then
1035 secure_domains:add("*");
1036 else
1037 secure_domains:include(set.new(config[host].s2s_secure_domains));
1038 end
1039 end
1040 if not secure_domains:empty() then
1041 print("");
1042 print(" You have set s2s_secure_domains but your version of LuaSec does ");
1043 print(" not support certificate validation, so s2s connections to/from ");
1044 print(" these domains will fail.");
1045 ok = false;
1046 end
1047 end
1048 elseif require_encryption and not all_modules:contains("tls") then
1049 print("");
1050 print(" You require encryption but mod_tls is not enabled.");
1051 print(" Connections will fail.");
1052 ok = false;
1053 end
1054
1055 print("Done.\n");
1056 end
1057 if not what or what == "dns" then
1058 local dns = require "net.dns";
1059 local idna = require "util.encodings".idna;
1060 local ip = require "util.ip";
1061 local c2s_ports = set.new(configmanager.get("*", "c2s_ports") or {5222});
1062 local s2s_ports = set.new(configmanager.get("*", "s2s_ports") or {5269});
1063
1064 local c2s_srv_required, s2s_srv_required;
1065 if not c2s_ports:contains(5222) then
1066 c2s_srv_required = true;
1067 end
1068 if not s2s_ports:contains(5269) then
1069 s2s_srv_required = true;
1070 end
1071
1072 local problem_hosts = set.new();
1073
1074 local external_addresses, internal_addresses = set.new(), set.new();
1075
1076 local fqdn = socket.dns.tohostname(socket.dns.gethostname());
1077 if fqdn then
1078 do
1079 local res = dns.lookup(idna.to_ascii(fqdn), "A");
1080 if res then
1081 for _, record in ipairs(res) do
1082 external_addresses:add(record.a);
1083 end
1084 end
1085 end
1086 do
1087 local res = dns.lookup(idna.to_ascii(fqdn), "AAAA");
1088 if res then
1089 for _, record in ipairs(res) do
1090 external_addresses:add(record.aaaa);
1091 end
1092 end
1093 end
1094 end
1095
1096 local local_addresses = require"util.net".local_addresses() or {};
1097
1098 for addr in it.values(local_addresses) do
1099 if not ip.new_ip(addr).private then
1100 external_addresses:add(addr);
1101 else
1102 internal_addresses:add(addr);
1103 end
1104 end
1105
1106 if external_addresses:empty() then
1107 print("");
1108 print(" Failed to determine the external addresses of this server. Checks may be inaccurate.");
1109 c2s_srv_required, s2s_srv_required = true, true;
1110 end
1111
1112 local v6_supported = not not socket.tcp6;
1113
1114 for jid, host_options in enabled_hosts() do
1115 local all_targets_ok, some_targets_ok = true, false;
1116 local node, host = jid_split(jid);
1117
1118 local modules, component_module = modulemanager.get_modules_for_host(host);
1119 if component_module then
1120 modules:add(component_module);
1121 end
1122
1123 local is_component = not not host_options.component_module;
1124 print("Checking DNS for "..(is_component and "component" or "host").." "..jid.."...");
1125 if node then
1126 print("Only the domain part ("..host..") is used in DNS.")
1127 end
1128 local target_hosts = set.new();
1129 if modules:contains("c2s") then
1130 local res = dns.lookup("_xmpp-client._tcp."..idna.to_ascii(host)..".", "SRV");
1131 if res then
1132 for _, record in ipairs(res) do
1133 target_hosts:add(record.srv.target);
1134 if not c2s_ports:contains(record.srv.port) then
1135 print(" SRV target "..record.srv.target.." contains unknown client port: "..record.srv.port);
1136 end
1137 end
1138 else
1139 if c2s_srv_required then
1140 print(" No _xmpp-client SRV record found for "..host..", but it looks like you need one.");
1141 all_targets_ok = false;
1142 else
1143 target_hosts:add(host);
1144 end
1145 end
1146 end
1147 if modules:contains("s2s") then
1148 local res = dns.lookup("_xmpp-server._tcp."..idna.to_ascii(host)..".", "SRV");
1149 if res then
1150 for _, record in ipairs(res) do
1151 target_hosts:add(record.srv.target);
1152 if not s2s_ports:contains(record.srv.port) then
1153 print(" SRV target "..record.srv.target.." contains unknown server port: "..record.srv.port);
1154 end
1155 end
1156 else
1157 if s2s_srv_required then
1158 print(" No _xmpp-server SRV record found for "..host..", but it looks like you need one.");
1159 all_targets_ok = false;
1160 else
1161 target_hosts:add(host);
1162 end
1163 end
1164 end
1165 if target_hosts:empty() then
1166 target_hosts:add(host);
1167 end
1168
1169 if target_hosts:contains("localhost") then
1170 print(" Target 'localhost' cannot be accessed from other servers");
1171 target_hosts:remove("localhost");
1172 end
1173
1174 if modules:contains("proxy65") then
1175 local proxy65_target = configmanager.get(host, "proxy65_address") or host;
1176 if type(proxy65_target) == "string" then
1177 local A, AAAA = dns.lookup(idna.to_ascii(proxy65_target), "A"), dns.lookup(idna.to_ascii(proxy65_target), "AAAA");
1178 local prob = {};
1179 if not A then
1180 table.insert(prob, "A");
1181 end
1182 if v6_supported and not AAAA then
1183 table.insert(prob, "AAAA");
1184 end
1185 if #prob > 0 then
1186 print(" File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/")
1187 .." record. Create one or set 'proxy65_address' to the correct host/IP.");
1188 end
1189 else
1190 print(" proxy65_address for "..host.." should be set to a string, unable to perform DNS check");
1191 end
1192 end
1193
1194 for target_host in target_hosts do
1195 local host_ok_v4, host_ok_v6;
1196 do
1197 local res = dns.lookup(idna.to_ascii(target_host), "A");
1198 if res then
1199 for _, record in ipairs(res) do
1200 if external_addresses:contains(record.a) then
1201 some_targets_ok = true;
1202 host_ok_v4 = true;
1203 elseif internal_addresses:contains(record.a) then
1204 host_ok_v4 = true;
1205 some_targets_ok = true;
1206 print(" "..target_host.." A record points to internal address, external connections might fail");
1207 else
1208 print(" "..target_host.." A record points to unknown address "..record.a);
1209 all_targets_ok = false;
1210 end
1211 end
1212 end
1213 end
1214 do
1215 local res = dns.lookup(idna.to_ascii(target_host), "AAAA");
1216 if res then
1217 for _, record in ipairs(res) do
1218 if external_addresses:contains(record.aaaa) then
1219 some_targets_ok = true;
1220 host_ok_v6 = true;
1221 elseif internal_addresses:contains(record.aaaa) then
1222 host_ok_v6 = true;
1223 some_targets_ok = true;
1224 print(" "..target_host.." AAAA record points to internal address, external connections might fail");
1225 else
1226 print(" "..target_host.." AAAA record points to unknown address "..record.aaaa);
1227 all_targets_ok = false;
1228 end
1229 end
1230 end
1231 end
1232
1233 local bad_protos = {}
1234 if not host_ok_v4 then
1235 table.insert(bad_protos, "IPv4");
1236 end
1237 if not host_ok_v6 then
1238 table.insert(bad_protos, "IPv6");
1239 end
1240 if #bad_protos > 0 then
1241 print(" Host "..target_host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")");
1242 end
1243 if host_ok_v6 and not v6_supported then
1244 print(" Host "..target_host.." has AAAA records, but your version of LuaSocket does not support IPv6.");
1245 print(" Please see https://prosody.im/doc/ipv6 for more information.");
1246 end
1247 end
1248 if not all_targets_ok then
1249 print(" "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server.");
1250 if is_component then
1251 print(" DNS records are necessary if you want users on other servers to access this component.");
1252 end
1253 problem_hosts:add(host);
1254 end
1255 print("");
1256 end
1257 if not problem_hosts:empty() then
1258 print("");
1259 print("For more information about DNS configuration please see https://prosody.im/doc/dns");
1260 print("");
1261 ok = false;
1262 end
1263 end
1264 if not what or what == "certs" then
1265 local cert_ok;
1266 print"Checking certificates..."
1267 local x509_verify_identity = require"util.x509".verify_identity;
1268 local create_context = require "core.certmanager".create_context;
1269 local ssl = dependencies.softreq"ssl";
1270 -- local datetime_parse = require"util.datetime".parse_x509;
1271 local load_cert = ssl and ssl.loadcertificate;
1272 -- or ssl.cert_from_pem
1273 if not ssl then
1274 print("LuaSec not available, can't perform certificate checks")
1275 if what == "certs" then cert_ok = false end
1276 elseif not load_cert then
1277 print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking");
1278 cert_ok = false
1279 else
1280 local function skip_bare_jid_hosts(host)
1281 if jid_split(host) then
1282 -- See issue #779
1283 return false;
1284 end
1285 return true;
1286 end
1287 for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do
1288 print("Checking certificate for "..host);
1289 -- First, let's find out what certificate this host uses.
1290 local host_ssl_config = configmanager.rawget(host, "ssl")
1291 or configmanager.rawget(host:match("%.(.*)"), "ssl");
1292 local global_ssl_config = configmanager.rawget("*", "ssl");
1293 local ok, err, ssl_config = create_context(host, "server", host_ssl_config, global_ssl_config);
1294 if not ok then
1295 print(" Error: "..err);
1296 cert_ok = false
1297 elseif not ssl_config.certificate then
1298 print(" No 'certificate' found for "..host)
1299 cert_ok = false
1300 elseif not ssl_config.key then
1301 print(" No 'key' found for "..host)
1302 cert_ok = false
1303 else
1304 local key, err = io.open(ssl_config.key); -- Permissions check only
1305 if not key then
1306 print(" Could not open "..ssl_config.key..": "..err);
1307 cert_ok = false
1308 else
1309 key:close();
1310 end
1311 local cert_fh, err = io.open(ssl_config.certificate); -- Load the file.
1312 if not cert_fh then
1313 print(" Could not open "..ssl_config.certificate..": "..err);
1314 cert_ok = false
1315 else
1316 print(" Certificate: "..ssl_config.certificate)
1317 local cert = load_cert(cert_fh:read"*a"); cert_fh:close();
1318 if not cert:validat(os.time()) then
1319 print(" Certificate has expired.")
1320 cert_ok = false
1321 elseif not cert:validat(os.time() + 86400) then
1322 print(" Certificate expires within one day.")
1323 cert_ok = false
1324 elseif not cert:validat(os.time() + 86400*7) then
1325 print(" Certificate expires within one week.")
1326 elseif not cert:validat(os.time() + 86400*31) then
1327 print(" Certificate expires within one month.")
1328 end
1329 if configmanager.get(host, "component_module") == nil
1330 and not x509_verify_identity(host, "_xmpp-client", cert) then
1331 print(" Not valid for client connections to "..host..".")
1332 cert_ok = false
1333 end
1334 if (not (configmanager.get(host, "anonymous_login")
1335 or configmanager.get(host, "authentication") == "anonymous"))
1336 and not x509_verify_identity(host, "_xmpp-server", cert) then
1337 print(" Not valid for server-to-server connections to "..host..".")
1338 cert_ok = false
1339 end
1340 end
1341 end
1342 end
1343 end
1344 if cert_ok == false then
1345 print("")
1346 print("For more information about certificates please see https://prosody.im/doc/certificates");
1347 ok = false
1348 end
1349 print("")
1350 end
1351 if not ok then
1352 print("Problems found, see above.");
1353 else
1354 print("All checks passed, congratulations!");
1355 end
1356 return ok and 0 or 2;
1357 end
1358
1359 function commands.shell(arg)
1360 require "util.prosodyctl.shell".start(arg);
1361 end 540 end
1362 541
1363 --------------------- 542 ---------------------
1364 543
1365 local async = require "util.async"; 544 local async = require "util.async";
1406 show_message("Failed to execute command: "..error_messages[ret]); 585 show_message("Failed to execute command: "..error_messages[ret]);
1407 os.exit(1); -- :( 586 os.exit(1); -- :(
1408 end 587 end
1409 end 588 end
1410 589
590 if not commands[command] then
591 local ok, command_module = pcall(require, "util.prosodyctl."..command);
592 if ok and command_module[command] then
593 commands[command] = command_module[command];
594 end
595 end
596
1411 if not commands[command] then -- Show help for all commands 597 if not commands[command] then -- Show help for all commands
1412 function show_usage(usage, desc) 598 function show_usage(usage, desc)
1413 print(" "..usage); 599 print(" "..usage);
1414 print(" "..desc); 600 print(" "..desc);
1415 end 601 end