Software / code / prosody
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 |