mirror of
https://github.com/bjc/prosody.git
synced 2025-04-02 12:47:38 +03:00
1843 lines
66 KiB
Lua
1843 lines
66 KiB
Lua
local configmanager = require "prosody.core.configmanager";
|
|
local moduleapi = require "prosody.core.moduleapi";
|
|
local show_usage = require "prosody.util.prosodyctl".show_usage;
|
|
local show_warning = require "prosody.util.prosodyctl".show_warning;
|
|
local is_prosody_running = require "prosody.util.prosodyctl".isrunning;
|
|
local parse_args = require "prosody.util.argparse".parse;
|
|
local dependencies = require "prosody.util.dependencies";
|
|
local socket = require "socket";
|
|
local socket_url = require "socket.url";
|
|
local jid_split = require "prosody.util.jid".prepped_split;
|
|
local modulemanager = require "prosody.core.modulemanager";
|
|
local async = require "prosody.util.async";
|
|
local httputil = require "prosody.util.http";
|
|
local human_units = require "prosody.util.human.units";
|
|
|
|
local function api(host)
|
|
return setmetatable({ name = "prosodyctl.check"; host = host; log = prosody.log }, { __index = moduleapi })
|
|
end
|
|
|
|
local function check_ojn(check_type, target_host)
|
|
local http = require "prosody.net.http"; -- .new({});
|
|
local json = require "prosody.util.json";
|
|
|
|
local response, err = async.wait_for(http.request(
|
|
("https://observe.jabber.network/api/v1/check/%s"):format(httputil.urlencode(check_type)),
|
|
{
|
|
method="POST",
|
|
headers={["Accept"] = "application/json"; ["Content-Type"] = "application/json"},
|
|
body=json.encode({target=target_host}),
|
|
}));
|
|
|
|
if not response then
|
|
return false, err;
|
|
end
|
|
|
|
if response.code ~= 200 then
|
|
return false, ("API replied with non-200 code: %d"):format(response.code);
|
|
end
|
|
|
|
local decoded_body, err = json.decode(response.body);
|
|
if decoded_body == nil then
|
|
return false, ("Failed to parse API JSON: %s"):format(err)
|
|
end
|
|
|
|
local success = decoded_body["success"];
|
|
return success == true, nil;
|
|
end
|
|
|
|
local function check_probe(base_url, probe_module, target)
|
|
local http = require "prosody.net.http"; -- .new({});
|
|
local params = httputil.formencode({ module = probe_module; target = target })
|
|
local response, err = async.wait_for(http.request(base_url .. "?" .. params));
|
|
|
|
if not response then return false, err; end
|
|
|
|
if response.code ~= 200 then return false, ("API replied with non-200 code: %d"):format(response.code); end
|
|
|
|
for line in response.body:gmatch("[^\r\n]+") do
|
|
local probe_success = line:match("^probe_success%s+(%d+)");
|
|
|
|
if probe_success == "1" then
|
|
return true;
|
|
elseif probe_success == "0" then
|
|
return false;
|
|
end
|
|
end
|
|
return false, "Probe endpoint did not return a success status";
|
|
end
|
|
|
|
local function check_turn_service(turn_service, ping_service)
|
|
local ip = require "prosody.util.ip";
|
|
local stun = require "prosody.net.stun";
|
|
|
|
local result = { warnings = {} };
|
|
|
|
-- Create UDP socket for communication with the server
|
|
local sock = assert(require "socket".udp());
|
|
do
|
|
local ok, err = sock:setsockname("*", 0);
|
|
if not ok then
|
|
result.error = "Unable to perform TURN test: setsockname: "..tostring(err);
|
|
return result;
|
|
end
|
|
ok, err = sock:setpeername(turn_service.host, turn_service.port);
|
|
if not ok then
|
|
result.error = "Unable to perform TURN test: setpeername: "..tostring(err);
|
|
return result;
|
|
end
|
|
end
|
|
sock:settimeout(10);
|
|
|
|
-- Helper function to receive a packet
|
|
local function receive_packet()
|
|
local raw_packet, err = sock:receive();
|
|
if not raw_packet then
|
|
return nil, err;
|
|
end
|
|
return stun.new_packet():deserialize(raw_packet);
|
|
end
|
|
|
|
-- Send a "binding" query, i.e. a request for our external IP/port
|
|
local bind_query = stun.new_packet("binding", "request");
|
|
bind_query:add_attribute("software", "prosodyctl check turn");
|
|
sock:send(bind_query:serialize());
|
|
|
|
local bind_result, err = receive_packet();
|
|
if not bind_result then
|
|
result.error = "No STUN response: "..err;
|
|
return result;
|
|
elseif bind_result:is_err_resp() then
|
|
result.error = ("STUN server returned error: %d (%s)"):format(bind_result:get_error());
|
|
return result;
|
|
elseif not bind_result:is_success_resp() then
|
|
result.error = ("Unexpected STUN response: %d (%s)"):format(bind_result:get_type());
|
|
return result;
|
|
end
|
|
|
|
result.external_ip = bind_result:get_xor_mapped_address();
|
|
if not result.external_ip then
|
|
result.error = "STUN server did not return an address";
|
|
return result;
|
|
end
|
|
if ip.new_ip(result.external_ip.address).private then
|
|
table.insert(result.warnings, "STUN returned a private IP! Is the TURN server behind a NAT and misconfigured?");
|
|
end
|
|
|
|
-- Send a TURN "allocate" request. Expected to fail due to auth, but
|
|
-- necessary to obtain a valid realm/nonce from the server.
|
|
local pre_request = stun.new_packet("allocate", "request");
|
|
sock:send(pre_request:serialize());
|
|
|
|
local pre_result, err = receive_packet();
|
|
if not pre_result then
|
|
result.error = "No initial TURN response: "..err;
|
|
return result;
|
|
elseif pre_result:is_success_resp() then
|
|
result.error = "TURN server does not have authentication enabled";
|
|
return result;
|
|
end
|
|
|
|
local realm = pre_result:get_attribute("realm");
|
|
local nonce = pre_result:get_attribute("nonce");
|
|
|
|
if not realm then
|
|
table.insert(result.warnings, "TURN server did not return an authentication realm. Is authentication enabled?");
|
|
end
|
|
if not nonce then
|
|
table.insert(result.warnings, "TURN server did not return a nonce");
|
|
end
|
|
|
|
-- Use the configured secret to obtain temporary user/pass credentials
|
|
local turn_user, turn_pass = stun.get_user_pass_from_secret(turn_service.secret);
|
|
|
|
-- Send a TURN allocate request, will fail if auth is wrong
|
|
local alloc_request = stun.new_packet("allocate", "request");
|
|
alloc_request:add_requested_transport("udp");
|
|
alloc_request:add_attribute("username", turn_user);
|
|
if realm then
|
|
alloc_request:add_attribute("realm", realm);
|
|
end
|
|
if nonce then
|
|
alloc_request:add_attribute("nonce", nonce);
|
|
end
|
|
local key = stun.get_long_term_auth_key(realm or turn_service.host, turn_user, turn_pass);
|
|
alloc_request:add_message_integrity(key);
|
|
sock:send(alloc_request:serialize());
|
|
|
|
-- Check the response
|
|
local alloc_response, err = receive_packet();
|
|
if not alloc_response then
|
|
result.error = "TURN server did not response to allocation request: "..err;
|
|
return result;
|
|
elseif alloc_response:is_err_resp() then
|
|
result.error = ("TURN server failed to create allocation: %d (%s)"):format(alloc_response:get_error());
|
|
return result;
|
|
elseif not alloc_response:is_success_resp() then
|
|
result.error = ("Unexpected TURN response: %d (%s)"):format(alloc_response:get_type());
|
|
return result;
|
|
end
|
|
|
|
result.relayed_addresses = alloc_response:get_xor_relayed_addresses();
|
|
|
|
if not ping_service then
|
|
-- Success! We won't be running the relay test.
|
|
return result;
|
|
end
|
|
|
|
-- Run the relay test - i.e. send a binding request to ping_service
|
|
-- and receive a response.
|
|
|
|
-- Resolve the IP of the ping service
|
|
local ping_host, ping_port = ping_service:match("^([^:]+):(%d+)$");
|
|
if ping_host then
|
|
ping_port = tonumber(ping_port);
|
|
else
|
|
-- Only a hostname specified, use default STUN port
|
|
ping_host, ping_port = ping_service, 3478;
|
|
end
|
|
|
|
if ping_host == turn_service.host then
|
|
result.error = ("Unable to perform ping test: please supply an external STUN server address. See https://prosody.im/doc/turn#prosodyctl-check");
|
|
return result;
|
|
end
|
|
|
|
local ping_service_ip, err = socket.dns.toip(ping_host);
|
|
if not ping_service_ip then
|
|
result.error = "Unable to resolve ping service hostname: "..err;
|
|
return result;
|
|
end
|
|
|
|
-- Ask the TURN server to allow packets from the ping service IP
|
|
local perm_request = stun.new_packet("create-permission");
|
|
perm_request:add_xor_peer_address(ping_service_ip);
|
|
perm_request:add_attribute("username", turn_user);
|
|
if realm then
|
|
perm_request:add_attribute("realm", realm);
|
|
end
|
|
if nonce then
|
|
perm_request:add_attribute("nonce", nonce);
|
|
end
|
|
perm_request:add_message_integrity(key);
|
|
sock:send(perm_request:serialize());
|
|
|
|
local perm_response, err = receive_packet();
|
|
if not perm_response then
|
|
result.error = "No response from TURN server when requesting peer permission: "..err;
|
|
return result;
|
|
elseif perm_response:is_err_resp() then
|
|
result.error = ("TURN permission request failed: %d (%s)"):format(perm_response:get_error());
|
|
return result;
|
|
elseif not perm_response:is_success_resp() then
|
|
result.error = ("Unexpected TURN response: %d (%s)"):format(perm_response:get_type());
|
|
return result;
|
|
end
|
|
|
|
-- Ask the TURN server to relay a STUN binding request to the ping server
|
|
local ping_data = stun.new_packet("binding"):serialize();
|
|
|
|
local ping_request = stun.new_packet("send", "indication");
|
|
ping_request:add_xor_peer_address(ping_service_ip, ping_port);
|
|
ping_request:add_attribute("data", ping_data);
|
|
ping_request:add_attribute("username", turn_user);
|
|
if realm then
|
|
ping_request:add_attribute("realm", realm);
|
|
end
|
|
if nonce then
|
|
ping_request:add_attribute("nonce", nonce);
|
|
end
|
|
ping_request:add_message_integrity(key);
|
|
sock:send(ping_request:serialize());
|
|
|
|
local ping_response, err = receive_packet();
|
|
if not ping_response then
|
|
result.error = "No response from ping server ("..ping_service_ip.."): "..err;
|
|
return result;
|
|
elseif not ping_response:is_indication() or select(2, ping_response:get_method()) ~= "data" then
|
|
result.error = ("Unexpected TURN response: %s %s"):format(select(2, ping_response:get_method()), select(2, ping_response:get_type()));
|
|
return result;
|
|
end
|
|
|
|
local pong_data = ping_response:get_attribute("data");
|
|
if not pong_data then
|
|
result.error = "No data relayed from remote server";
|
|
return result;
|
|
end
|
|
local pong = stun.new_packet():deserialize(pong_data);
|
|
|
|
result.external_ip_pong = pong:get_xor_mapped_address();
|
|
if not result.external_ip_pong then
|
|
result.error = "Ping server did not return an address";
|
|
return result;
|
|
end
|
|
|
|
local relay_address_found, relay_port_matches;
|
|
for _, relayed_address in ipairs(result.relayed_addresses) do
|
|
if relayed_address.address == result.external_ip_pong.address then
|
|
relay_address_found = true;
|
|
relay_port_matches = result.external_ip_pong.port == relayed_address.port;
|
|
end
|
|
end
|
|
if not relay_address_found then
|
|
table.insert(result.warnings, "TURN external IP vs relay address mismatch! Is the TURN server behind a NAT and misconfigured?");
|
|
elseif not relay_port_matches then
|
|
table.insert(result.warnings, "External port does not match reported relay port! This is probably caused by a NAT in front of the TURN server.");
|
|
end
|
|
|
|
--
|
|
|
|
return result;
|
|
end
|
|
|
|
local function skip_bare_jid_hosts(host)
|
|
if jid_split(host) then
|
|
-- See issue #779
|
|
return false;
|
|
end
|
|
return true;
|
|
end
|
|
|
|
local check_opts = {
|
|
short_params = {
|
|
h = "help", v = "verbose";
|
|
};
|
|
value_params = {
|
|
ping = true;
|
|
};
|
|
};
|
|
|
|
local function check(arg)
|
|
if arg[1] == "help" or arg[1] == "--help" then
|
|
show_usage([[check]], [[Perform basic checks on your Prosody installation]]);
|
|
return 1;
|
|
end
|
|
local what = table.remove(arg, 1);
|
|
local opts, opts_err, opts_info = parse_args(arg, check_opts);
|
|
if opts_err == "missing-value" then
|
|
print("Error: Expected a value after '"..opts_info.."'");
|
|
return 1;
|
|
elseif opts_err == "param-not-found" then
|
|
print("Error: Unknown parameter: "..opts_info);
|
|
return 1;
|
|
end
|
|
local array = require "prosody.util.array";
|
|
local set = require "prosody.util.set";
|
|
local it = require "prosody.util.iterators";
|
|
local ok = true;
|
|
local function contains_match(hayset, needle) for member in hayset do if member:find(needle) then return true end end end
|
|
local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end
|
|
local function is_user_host(host, conf) return host ~= "*" and conf.component_module == nil; end
|
|
local function is_component_host(host, conf) return host ~= "*" and conf.component_module ~= nil; end
|
|
local function enabled_hosts() return it.filter(disabled_hosts, it.sorted_pairs(configmanager.getconfig())); end
|
|
local function enabled_user_hosts() return it.filter(is_user_host, it.sorted_pairs(configmanager.getconfig())); end
|
|
local function enabled_components() return it.filter(is_component_host, it.sorted_pairs(configmanager.getconfig())); end
|
|
|
|
local checks = {};
|
|
function checks.disabled()
|
|
local disabled_hosts_set = set.new();
|
|
for host in it.filter("*", pairs(configmanager.getconfig())) do
|
|
if api(host):get_option_boolean("enabled") == false then
|
|
disabled_hosts_set:add(host);
|
|
end
|
|
end
|
|
if not disabled_hosts_set:empty() then
|
|
local msg = "Checks will be skipped for these disabled hosts: %s";
|
|
if what then msg = "These hosts are disabled: %s"; end
|
|
show_warning(msg, tostring(disabled_hosts_set));
|
|
if what then return 0; end
|
|
print""
|
|
end
|
|
end
|
|
function checks.config()
|
|
print("Checking config...");
|
|
|
|
if what == "config" then
|
|
local files = configmanager.files();
|
|
print(" The following configuration files have been loaded:");
|
|
print(" - "..table.concat(files, "\n - "));
|
|
end
|
|
|
|
local obsolete = set.new({ --> remove
|
|
"archive_cleanup_interval",
|
|
"dns_timeout",
|
|
"muc_log_cleanup_interval",
|
|
"s2s_dns_resolvers",
|
|
"setgid",
|
|
"setuid",
|
|
});
|
|
local function instead_use(kind, name, value)
|
|
if kind == "option" then
|
|
if value then
|
|
return string.format("instead, use '%s = %q'", name, value);
|
|
else
|
|
return string.format("instead, use '%s'", name);
|
|
end
|
|
elseif kind == "module" then
|
|
return string.format("instead, add %q to '%s'", name, value or "modules_enabled");
|
|
elseif kind == "community" then
|
|
return string.format("instead, add %q from %s", name, value or "prosody-modules");
|
|
end
|
|
return kind
|
|
end
|
|
local deprecated_replacements = {
|
|
anonymous_login = instead_use("option", "authentication", "anonymous");
|
|
daemonize = "instead, use the --daemonize/-D or --foreground/-F command line flags";
|
|
disallow_s2s = instead_use("module", "s2s", "modules_disabled");
|
|
no_daemonize = "instead, use the --daemonize/-D or --foreground/-F command line flags";
|
|
require_encryption = "instead, use 'c2s_require_encryption' and 's2s_require_encryption'";
|
|
vcard_compatibility = instead_use("community", "mod_compat_vcard");
|
|
use_libevent = instead_use("option", "network_backend", "event");
|
|
whitelist_registration_only = instead_use("option", "allowlist_registration_only");
|
|
registration_whitelist = instead_use("option", "registration_allowlist");
|
|
registration_blacklist = instead_use("option", "registration_blocklist");
|
|
blacklist_on_registration_throttle_overload = instead_use("blocklist_on_registration_throttle_overload");
|
|
cross_domain_bosh = "instead, use 'http_cors_override', see https://prosody.im/doc/http#cross-domain-cors-support";
|
|
cross_domain_websocket = "instead, use 'http_cors_override', see https://prosody.im/doc/http#cross-domain-cors-support";
|
|
};
|
|
-- FIXME all the singular _port and _interface options are supposed to be deprecated too
|
|
local deprecated_ports = { bosh = "http", legacy_ssl = "c2s_direct_tls" };
|
|
local port_suffixes = set.new({ "port", "ports", "interface", "interfaces", "ssl" });
|
|
for port, replacement in pairs(deprecated_ports) do
|
|
for suffix in port_suffixes do
|
|
local rsuffix = (suffix == "port" or suffix == "interface") and suffix.."s" or suffix;
|
|
deprecated_replacements[port.."_"..suffix] = "instead, use '"..replacement.."_"..rsuffix.."'"
|
|
end
|
|
end
|
|
local deprecated = set.new(array.collect(it.keys(deprecated_replacements)));
|
|
local known_global_options = set.new({
|
|
"access_control_allow_credentials",
|
|
"access_control_allow_headers",
|
|
"access_control_allow_methods",
|
|
"access_control_max_age",
|
|
"admin_socket",
|
|
"body_size_limit",
|
|
"bosh_max_inactivity",
|
|
"bosh_max_polling",
|
|
"bosh_max_wait",
|
|
"buffer_size_limit",
|
|
"c2s_close_timeout",
|
|
"c2s_stanza_size_limit",
|
|
"c2s_tcp_keepalives",
|
|
"c2s_timeout",
|
|
"component_stanza_size_limit",
|
|
"component_tcp_keepalives",
|
|
"consider_bosh_secure",
|
|
"consider_websocket_secure",
|
|
"console_banner",
|
|
"console_prettyprint_settings",
|
|
"daemonize",
|
|
"gc",
|
|
"http_default_host",
|
|
"http_errors_always_show",
|
|
"http_errors_default_message",
|
|
"http_errors_detailed",
|
|
"http_errors_messages",
|
|
"http_max_buffer_size",
|
|
"http_max_content_size",
|
|
"installer_plugin_path",
|
|
"limits",
|
|
"limits_resolution",
|
|
"log",
|
|
"multiplex_buffer_size",
|
|
"network_backend",
|
|
"network_default_read_size",
|
|
"network_settings",
|
|
"openmetrics_allow_cidr",
|
|
"openmetrics_allow_ips",
|
|
"pidfile",
|
|
"plugin_paths",
|
|
"plugin_server",
|
|
"prosodyctl_timeout",
|
|
"prosody_group",
|
|
"prosody_user",
|
|
"run_as_root",
|
|
"s2s_close_timeout",
|
|
"s2s_insecure_domains",
|
|
"s2s_require_encryption",
|
|
"s2s_secure_auth",
|
|
"s2s_secure_domains",
|
|
"s2s_stanza_size_limit",
|
|
"s2s_tcp_keepalives",
|
|
"s2s_timeout",
|
|
"statistics",
|
|
"statistics_config",
|
|
"statistics_interval",
|
|
"tcp_keepalives",
|
|
"tls_profile",
|
|
"trusted_proxies",
|
|
"umask",
|
|
"use_dane",
|
|
"use_ipv4",
|
|
"use_ipv6",
|
|
"websocket_frame_buffer_limit",
|
|
"websocket_frame_fragment_limit",
|
|
"websocket_get_response_body",
|
|
"websocket_get_response_text",
|
|
});
|
|
local config = configmanager.getconfig();
|
|
local global = api("*");
|
|
-- Check that we have any global options (caused by putting a host at the top)
|
|
if it.count(it.filter("log", pairs(config["*"]))) == 0 then
|
|
ok = false;
|
|
print("");
|
|
print(" No global options defined. Perhaps you have put a host definition at the top")
|
|
print(" of the config file? They should be at the bottom, see https://prosody.im/doc/configure#overview");
|
|
end
|
|
if it.count(enabled_hosts()) == 0 then
|
|
ok = false;
|
|
print("");
|
|
if it.count(it.filter("*", pairs(config))) == 0 then
|
|
print(" No hosts are defined, please add at least one VirtualHost section")
|
|
elseif config["*"]["enabled"] == false then
|
|
print(" No hosts are enabled. Remove enabled = false from the global section or put enabled = true under at least one VirtualHost section")
|
|
else
|
|
print(" All hosts are disabled. Remove enabled = false from at least one VirtualHost section")
|
|
end
|
|
end
|
|
if not config["*"].modules_enabled then
|
|
print(" No global modules_enabled is set?");
|
|
local suggested_global_modules;
|
|
for host, options in enabled_hosts() do --luacheck: ignore 213/host
|
|
if not options.component_module and options.modules_enabled then
|
|
suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled));
|
|
end
|
|
end
|
|
if suggested_global_modules and not suggested_global_modules:empty() then
|
|
print(" Consider moving these modules into modules_enabled in the global section:")
|
|
print(" "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end));
|
|
end
|
|
print();
|
|
end
|
|
|
|
local function validate_module_list(host, name, modules)
|
|
if modules == nil then
|
|
return -- okay except for global section, checked separately
|
|
end
|
|
local t = type(modules)
|
|
if t ~= "table" then
|
|
print(" The " .. name .. " in the " .. host .. " section should not be a " .. t .. " but a list of strings, e.g.");
|
|
print(" " .. name .. " = { \"name_of_module\", \"another_plugin\", }")
|
|
print()
|
|
ok = false
|
|
return
|
|
end
|
|
for k, v in pairs(modules) do
|
|
if type(k) ~= "number" or type(v) ~= "string" then
|
|
print(" The " .. name .. " in the " .. host .. " section should be a list of strings, e.g.");
|
|
print(" " .. name .. " = { \"name_of_module\", \"another_plugin\", }")
|
|
print(" It should not contain key = value pairs, try putting them outside the {} brackets.");
|
|
ok = false
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
for host, options in enabled_hosts() do
|
|
validate_module_list(host, "modules_enabled", options.modules_enabled);
|
|
validate_module_list(host, "modules_disabled", options.modules_disabled);
|
|
end
|
|
|
|
do -- Check for modules enabled both normally and as components
|
|
local modules = global:get_option_set("modules_enabled");
|
|
for host, options in enabled_hosts() do
|
|
local component_module = options.component_module;
|
|
if component_module and modules:contains(component_module) then
|
|
print((" mod_%s is enabled both in modules_enabled and as Component %q %q"):format(component_module, host, component_module));
|
|
print(" This means the service is enabled on all VirtualHosts as well as the Component.");
|
|
print(" Are you sure this what you want? It may cause unexpected behaviour.");
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Check for global options under hosts
|
|
local global_options = set.new(it.to_array(it.keys(config["*"])));
|
|
local obsolete_global_options = set.intersection(global_options, obsolete);
|
|
if not obsolete_global_options:empty() then
|
|
print("");
|
|
print(" You have some obsolete options you can remove from the global section:");
|
|
print(" "..tostring(obsolete_global_options))
|
|
ok = false;
|
|
end
|
|
local deprecated_global_options = set.intersection(global_options, deprecated);
|
|
if not deprecated_global_options:empty() then
|
|
print("");
|
|
print(" You have some deprecated options in the global section:");
|
|
for option in deprecated_global_options do
|
|
print((" '%s' -- %s"):format(option, deprecated_replacements[option]));
|
|
end
|
|
ok = false;
|
|
end
|
|
for host, options in it.filter(function (h) return h ~= "*" end, pairs(configmanager.getconfig())) do
|
|
local host_options = set.new(it.to_array(it.keys(options)));
|
|
local misplaced_options = set.intersection(host_options, known_global_options);
|
|
for name in pairs(options) do
|
|
if name:match("^interfaces?")
|
|
or name:match("_ports?$") or name:match("_interfaces?$")
|
|
or (name:match("_ssl$") and not name:match("^[cs]2s_ssl$")) then
|
|
misplaced_options:add(name);
|
|
end
|
|
end
|
|
-- FIXME These _could_ be misplaced, but we would have to check where the corresponding module is loaded to be sure
|
|
misplaced_options:exclude(set.new({ "external_service_port", "turn_external_port" }));
|
|
if not misplaced_options:empty() then
|
|
ok = false;
|
|
print("");
|
|
local n = it.count(misplaced_options);
|
|
print(" You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be");
|
|
print(" in the global section of the config file, above any VirtualHost or Component definitions,")
|
|
print(" see https://prosody.im/doc/configure#overview for more information.")
|
|
print("");
|
|
print(" You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", "));
|
|
end
|
|
end
|
|
for host, options in enabled_hosts() do
|
|
local host_options = set.new(it.to_array(it.keys(options)));
|
|
local subdomain = host:match("^[^.]+");
|
|
if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp"
|
|
or subdomain == "chat" or subdomain == "im") then
|
|
print("");
|
|
print(" Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to");
|
|
print(" "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host..".");
|
|
print(" For more information see: https://prosody.im/doc/dns");
|
|
end
|
|
end
|
|
local all_modules = set.new(config["*"].modules_enabled);
|
|
local all_options = set.new(it.to_array(it.keys(config["*"])));
|
|
for host in enabled_hosts() do
|
|
all_options:include(set.new(it.to_array(it.keys(config[host]))));
|
|
all_modules:include(set.new(config[host].modules_enabled));
|
|
end
|
|
for mod in all_modules do
|
|
if mod:match("^mod_") then
|
|
print("");
|
|
print(" Modules in modules_enabled should not have the 'mod_' prefix included.");
|
|
print(" Change '"..mod.."' to '"..mod:match("^mod_(.*)").."'.");
|
|
elseif mod:match("^auth_") then
|
|
print("");
|
|
print(" Authentication modules should not be added to modules_enabled,");
|
|
print(" but be specified in the 'authentication' option.");
|
|
print(" Remove '"..mod.."' from modules_enabled and instead add");
|
|
print(" authentication = '"..mod:match("^auth_(.*)").."'");
|
|
print(" For more information see https://prosody.im/doc/authentication");
|
|
elseif mod:match("^storage_") then
|
|
print("");
|
|
print(" storage modules should not be added to modules_enabled,");
|
|
print(" but be specified in the 'storage' option.");
|
|
print(" Remove '"..mod.."' from modules_enabled and instead add");
|
|
print(" storage = '"..mod:match("^storage_(.*)").."'");
|
|
print(" For more information see https://prosody.im/doc/storage");
|
|
end
|
|
end
|
|
if all_modules:contains("vcard") and all_modules:contains("vcard_legacy") then
|
|
print("");
|
|
print(" Both mod_vcard_legacy and mod_vcard are enabled but they conflict");
|
|
print(" with each other. Remove one.");
|
|
end
|
|
if all_modules:contains("pep") and all_modules:contains("pep_simple") then
|
|
print("");
|
|
print(" Both mod_pep_simple and mod_pep are enabled but they conflict");
|
|
print(" with each other. Remove one.");
|
|
end
|
|
if all_modules:contains("posix") then
|
|
print("");
|
|
print(" mod_posix is loaded in your configuration file, but it has");
|
|
print(" been deprecated. You can safely remove it.");
|
|
end
|
|
|
|
for host, host_config in pairs(config) do --luacheck: ignore 213/host
|
|
if type(rawget(host_config, "storage")) == "string" and rawget(host_config, "default_storage") then
|
|
print("");
|
|
print(" The 'default_storage' option is not needed if 'storage' is set to a string.");
|
|
break;
|
|
end
|
|
end
|
|
|
|
for host, host_config in pairs(config) do --luacheck: ignore 213/host
|
|
if type(rawget(host_config, "storage")) == "string" and rawget(host_config, "default_storage") then
|
|
print("");
|
|
print(" The 'default_storage' option is not needed if 'storage' is set to a string.");
|
|
break;
|
|
end
|
|
end
|
|
|
|
local require_encryption = set.intersection(all_options, set.new({
|
|
"require_encryption", "c2s_require_encryption", "s2s_require_encryption"
|
|
})):empty();
|
|
local ssl = dependencies.softreq"ssl";
|
|
if not ssl then
|
|
if not require_encryption then
|
|
print("");
|
|
print(" You require encryption but LuaSec is not available.");
|
|
print(" Connections will fail.");
|
|
ok = false;
|
|
end
|
|
elseif not ssl.loadcertificate then
|
|
if all_options:contains("s2s_secure_auth") then
|
|
print("");
|
|
print(" You have set s2s_secure_auth but your version of LuaSec does ");
|
|
print(" not support certificate validation, so all s2s connections will");
|
|
print(" fail.");
|
|
ok = false;
|
|
elseif all_options:contains("s2s_secure_domains") then
|
|
local secure_domains = set.new();
|
|
for host in enabled_hosts() do
|
|
if api(host):get_option_boolean("s2s_secure_auth") then
|
|
secure_domains:add("*");
|
|
else
|
|
secure_domains:include(api(host):get_option_set("s2s_secure_domains", {}));
|
|
end
|
|
end
|
|
if not secure_domains:empty() then
|
|
print("");
|
|
print(" You have set s2s_secure_domains but your version of LuaSec does ");
|
|
print(" not support certificate validation, so s2s connections to/from ");
|
|
print(" these domains will fail.");
|
|
ok = false;
|
|
end
|
|
end
|
|
elseif require_encryption and not all_modules:contains("tls") then
|
|
print("");
|
|
print(" You require encryption but mod_tls is not enabled.");
|
|
print(" Connections will fail.");
|
|
ok = false;
|
|
end
|
|
|
|
do
|
|
local registration_enabled_hosts = {};
|
|
for host in enabled_hosts() do
|
|
local host_modules, component = modulemanager.get_modules_for_host(host);
|
|
local hostapi = api(host);
|
|
local allow_registration = hostapi:get_option_boolean("allow_registration", false);
|
|
local mod_register = host_modules:contains("register");
|
|
local mod_register_ibr = host_modules:contains("register_ibr");
|
|
local mod_invites_register = host_modules:contains("invites_register");
|
|
local registration_invite_only = hostapi:get_option_boolean("registration_invite_only", true);
|
|
local is_vhost = not component;
|
|
if is_vhost and (mod_register_ibr or (mod_register and allow_registration))
|
|
and not (mod_invites_register and registration_invite_only) then
|
|
table.insert(registration_enabled_hosts, host);
|
|
end
|
|
end
|
|
if #registration_enabled_hosts > 0 then
|
|
table.sort(registration_enabled_hosts);
|
|
print("");
|
|
print(" Public registration is enabled on:");
|
|
print(" "..table.concat(registration_enabled_hosts, ", "));
|
|
print("");
|
|
print(" If this is intentional, review our guidelines on running a public server");
|
|
print(" at https://prosody.im/doc/public_servers - otherwise, consider switching to");
|
|
print(" invite-based registration, which is more secure.");
|
|
end
|
|
end
|
|
|
|
do
|
|
local orphan_components = {};
|
|
local referenced_components = set.new();
|
|
local enabled_hosts_set = set.new();
|
|
local invalid_disco_items = {};
|
|
for host in it.filter("*", pairs(configmanager.getconfig())) do
|
|
local hostapi = api(host);
|
|
if hostapi:get_option_boolean("enabled", true) then
|
|
enabled_hosts_set:add(host);
|
|
for _, disco_item in ipairs(hostapi:get_option_array("disco_items", {})) do
|
|
if type(disco_item[1]) == "string" then
|
|
referenced_components:add(disco_item[1]);
|
|
else
|
|
invalid_disco_items[host] = true;
|
|
end
|
|
end
|
|
end
|
|
end
|
|
for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do
|
|
local is_component = not not select(2, modulemanager.get_modules_for_host(host));
|
|
if is_component then
|
|
local parent_domain = host:match("^[^.]+%.(.+)$");
|
|
local is_orphan = not (enabled_hosts_set:contains(parent_domain) or referenced_components:contains(host));
|
|
if is_orphan then
|
|
table.insert(orphan_components, host);
|
|
end
|
|
end
|
|
end
|
|
|
|
if next(invalid_disco_items) ~= nil then
|
|
print("");
|
|
print(" Some hosts in your configuration file have an invalid 'disco_items' option.");
|
|
print(" This may cause further errors, such as unreferenced components.");
|
|
print("");
|
|
for host in it.sorted_pairs(invalid_disco_items) do
|
|
print(" - "..host);
|
|
end
|
|
print("");
|
|
end
|
|
|
|
if #orphan_components > 0 then
|
|
table.sort(orphan_components);
|
|
print("");
|
|
print(" Your configuration contains the following unreferenced components:\n");
|
|
print(" "..table.concat(orphan_components, "\n "));
|
|
print("");
|
|
print(" Clients may not be able to discover these services because they are not linked to");
|
|
print(" any VirtualHost. They are automatically linked if they are direct subdomains of a");
|
|
print(" VirtualHost. Alternatively, you can explicitly link them using the disco_items option.");
|
|
print(" For more information see https://prosody.im/doc/modules/mod_disco#items");
|
|
end
|
|
end
|
|
|
|
-- Check hostname validity
|
|
do
|
|
local idna = require "prosody.util.encodings".idna;
|
|
local invalid_hosts = {};
|
|
local alabel_hosts = {};
|
|
for host in it.filter("*", pairs(configmanager.getconfig())) do
|
|
local _, h, _ = jid_split(host);
|
|
if not h or not idna.to_ascii(h) then
|
|
table.insert(invalid_hosts, host);
|
|
else
|
|
for label in h:gmatch("[^%.]+") do
|
|
if label:match("^xn%-%-") then
|
|
table.insert(alabel_hosts, host);
|
|
break;
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if #invalid_hosts > 0 then
|
|
table.sort(invalid_hosts);
|
|
print("");
|
|
print(" Your configuration contains invalid host names:");
|
|
print(" "..table.concat(invalid_hosts, "\n "));
|
|
print("");
|
|
print(" Clients may not be able to log in to these hosts, or you may not be able to");
|
|
print(" communicate with remote servers.");
|
|
print(" Use a valid domain name to correct this issue.");
|
|
end
|
|
|
|
if #alabel_hosts > 0 then
|
|
table.sort(alabel_hosts);
|
|
print("");
|
|
print(" Your configuration contains incorrectly-encoded hostnames:");
|
|
for _, ahost in ipairs(alabel_hosts) do
|
|
print((" '%s' (should be '%s')"):format(ahost, idna.to_unicode(ahost)));
|
|
end
|
|
print("");
|
|
print(" Clients may not be able to log in to these hosts, or you may not be able to");
|
|
print(" communicate with remote servers.");
|
|
print(" To correct this issue, use the Unicode version of the domain in Prosody's config file.");
|
|
end
|
|
|
|
if #invalid_hosts > 0 or #alabel_hosts > 0 then
|
|
print("");
|
|
print(" WARNING: Changing the name of a VirtualHost in Prosody's config file");
|
|
print(" WILL NOT migrate any existing data (user accounts, etc.) to the new name.");
|
|
ok = false;
|
|
end
|
|
end
|
|
|
|
-- Check features
|
|
do
|
|
local missing_features = {};
|
|
for host in enabled_user_hosts() do
|
|
local all_features = checks.features(host, true);
|
|
if not all_features then
|
|
table.insert(missing_features, host);
|
|
end
|
|
end
|
|
if #missing_features > 0 then
|
|
print("");
|
|
print(" Some of your hosts may be missing features due to a lack of configuration.");
|
|
print(" For more details, use the 'prosodyctl check features' command.");
|
|
end
|
|
end
|
|
|
|
print("Done.\n");
|
|
end
|
|
function checks.dns()
|
|
local dns = require "prosody.net.dns";
|
|
pcall(function ()
|
|
local unbound = require"prosody.net.unbound";
|
|
dns = unbound.dns;
|
|
end)
|
|
local idna = require "prosody.util.encodings".idna;
|
|
local ip = require "prosody.util.ip";
|
|
local global = api("*");
|
|
local c2s_ports = global:get_option_set("c2s_ports", {5222});
|
|
local s2s_ports = global:get_option_set("s2s_ports", {5269});
|
|
local c2s_tls_ports = global:get_option_set("c2s_direct_tls_ports", {});
|
|
local s2s_tls_ports = global:get_option_set("s2s_direct_tls_ports", {});
|
|
|
|
local global_enabled = set.new();
|
|
for host in enabled_hosts() do
|
|
global_enabled:include(modulemanager.get_modules_for_host(host));
|
|
end
|
|
if global_enabled:contains("net_multiplex") then
|
|
local multiplex_ports = global:get_option_set("ports", {});
|
|
local multiplex_tls_ports = global:get_option_set("ssl_ports", {});
|
|
if not multiplex_ports:empty() then
|
|
c2s_ports = c2s_ports + multiplex_ports;
|
|
s2s_ports = s2s_ports + multiplex_ports;
|
|
end
|
|
if not multiplex_tls_ports:empty() then
|
|
c2s_tls_ports = c2s_tls_ports + multiplex_tls_ports;
|
|
s2s_tls_ports = s2s_tls_ports + multiplex_tls_ports;
|
|
end
|
|
end
|
|
|
|
local c2s_srv_required, s2s_srv_required, c2s_tls_srv_required, s2s_tls_srv_required;
|
|
if not c2s_ports:contains(5222) then
|
|
c2s_srv_required = true;
|
|
end
|
|
if not s2s_ports:contains(5269) then
|
|
s2s_srv_required = true;
|
|
end
|
|
if not c2s_tls_ports:empty() then
|
|
c2s_tls_srv_required = true;
|
|
end
|
|
if not s2s_tls_ports:empty() then
|
|
s2s_tls_srv_required = true;
|
|
end
|
|
|
|
local problem_hosts = set.new();
|
|
|
|
local external_addresses, internal_addresses = set.new(), set.new();
|
|
|
|
local fqdn = socket.dns.tohostname(socket.dns.gethostname());
|
|
if fqdn then
|
|
local fqdn_a = idna.to_ascii(fqdn);
|
|
if fqdn_a then
|
|
local res = dns.lookup(fqdn_a, "A");
|
|
if res then
|
|
for _, record in ipairs(res) do
|
|
external_addresses:add(record.a);
|
|
end
|
|
end
|
|
end
|
|
if fqdn_a then
|
|
local res = dns.lookup(fqdn_a, "AAAA");
|
|
if res then
|
|
for _, record in ipairs(res) do
|
|
external_addresses:add(record.aaaa);
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local local_addresses = require"prosody.util.net".local_addresses() or {};
|
|
|
|
for addr in it.values(local_addresses) do
|
|
if not ip.new_ip(addr).private then
|
|
external_addresses:add(addr);
|
|
else
|
|
internal_addresses:add(addr);
|
|
end
|
|
end
|
|
|
|
-- Allow admin to specify additional (e.g. undiscoverable) IP addresses in the config
|
|
for _, address in ipairs(global:get_option_array("external_addresses", {})) do
|
|
external_addresses:add(address);
|
|
end
|
|
|
|
if external_addresses:empty() then
|
|
print("");
|
|
print(" Failed to determine the external addresses of this server. Checks may be inaccurate.");
|
|
print(" If you know the correct external addresses you can specify them in the config like:")
|
|
print(" external_addresses = { \"192.0.2.34\", \"2001:db8::abcd:1234\" }")
|
|
c2s_srv_required, s2s_srv_required = true, true;
|
|
end
|
|
|
|
local v6_supported = not not socket.tcp6;
|
|
local use_ipv4 = global:get_option_boolean("use_ipv4", true);
|
|
local use_ipv6 = global:get_option_boolean("use_ipv6", true);
|
|
|
|
local function trim_dns_name(n)
|
|
return (n:gsub("%.$", ""));
|
|
end
|
|
|
|
local unknown_addresses = set.new();
|
|
|
|
local function is_valid_domain(domain)
|
|
return idna.to_ascii(domain) ~= nil;
|
|
end
|
|
|
|
for jid in it.filter(is_valid_domain, enabled_hosts()) do
|
|
local all_targets_ok, some_targets_ok = true, false;
|
|
local node, host = jid_split(jid);
|
|
|
|
local modules, component_module = modulemanager.get_modules_for_host(host);
|
|
if component_module then
|
|
modules:add(component_module);
|
|
end
|
|
|
|
-- TODO Refactor these DNS SRV checks since they are very similar
|
|
-- FIXME Suggest concrete actionable steps to correct issues so that
|
|
-- users don't have to copy-paste the message into the support chat and
|
|
-- ask what to do about it.
|
|
local is_component = not not component_module;
|
|
print("Checking DNS for "..(is_component and "component" or "host").." "..jid.."...");
|
|
if node then
|
|
print("Only the domain part ("..host..") is used in DNS.")
|
|
end
|
|
local target_hosts = set.new();
|
|
if modules:contains("c2s") then
|
|
local res = dns.lookup("_xmpp-client._tcp."..idna.to_ascii(host)..".", "SRV");
|
|
if res and #res > 0 then
|
|
for _, record in ipairs(res) do
|
|
if record.srv.target == "." then -- TODO is this an error if mod_c2s is enabled?
|
|
print(" 'xmpp-client' service disabled by pointing to '.'"); -- FIXME Explain better what this is
|
|
break;
|
|
end
|
|
local target = trim_dns_name(record.srv.target);
|
|
target_hosts:add(target);
|
|
if not c2s_ports:contains(record.srv.port) then
|
|
print(" SRV target "..target.." contains unknown client port: "..record.srv.port);
|
|
end
|
|
end
|
|
else
|
|
if c2s_srv_required then
|
|
print(" No _xmpp-client SRV record found for "..host..", but it looks like you need one.");
|
|
all_targets_ok = false;
|
|
else
|
|
target_hosts:add(host);
|
|
end
|
|
end
|
|
end
|
|
if modules:contains("c2s") then
|
|
local res = dns.lookup("_xmpps-client._tcp."..idna.to_ascii(host)..".", "SRV");
|
|
if res and #res > 0 then
|
|
for _, record in ipairs(res) do
|
|
if record.srv.target == "." then -- TODO is this an error if mod_c2s is enabled?
|
|
print(" 'xmpps-client' service disabled by pointing to '.'"); -- FIXME Explain better what this is
|
|
break;
|
|
end
|
|
local target = trim_dns_name(record.srv.target);
|
|
target_hosts:add(target);
|
|
if not c2s_tls_ports:contains(record.srv.port) then
|
|
print(" SRV target "..target.." contains unknown Direct TLS client port: "..record.srv.port);
|
|
end
|
|
end
|
|
elseif c2s_tls_srv_required then
|
|
print(" No _xmpps-client SRV record found for "..host..", but it looks like you need one.");
|
|
all_targets_ok = false;
|
|
end
|
|
end
|
|
if modules:contains("s2s") then
|
|
local res = dns.lookup("_xmpp-server._tcp."..idna.to_ascii(host)..".", "SRV");
|
|
if res and #res > 0 then
|
|
for _, record in ipairs(res) do
|
|
if record.srv.target == "." then -- TODO Is this an error if mod_s2s is enabled?
|
|
print(" 'xmpp-server' service disabled by pointing to '.'"); -- FIXME Explain better what this is
|
|
break;
|
|
end
|
|
local target = trim_dns_name(record.srv.target);
|
|
target_hosts:add(target);
|
|
if not s2s_ports:contains(record.srv.port) then
|
|
print(" SRV target "..target.." contains unknown server port: "..record.srv.port);
|
|
end
|
|
end
|
|
else
|
|
if s2s_srv_required then
|
|
print(" No _xmpp-server SRV record found for "..host..", but it looks like you need one.");
|
|
all_targets_ok = false;
|
|
else
|
|
target_hosts:add(host);
|
|
end
|
|
end
|
|
end
|
|
if modules:contains("s2s") then
|
|
local res = dns.lookup("_xmpps-server._tcp."..idna.to_ascii(host)..".", "SRV");
|
|
if res and #res > 0 then
|
|
for _, record in ipairs(res) do
|
|
if record.srv.target == "." then -- TODO is this an error if mod_s2s is enabled?
|
|
print(" 'xmpps-server' service disabled by pointing to '.'"); -- FIXME Explain better what this is
|
|
break;
|
|
end
|
|
local target = trim_dns_name(record.srv.target);
|
|
target_hosts:add(target);
|
|
if not s2s_tls_ports:contains(record.srv.port) then
|
|
print(" SRV target "..target.." contains unknown Direct TLS server port: "..record.srv.port);
|
|
end
|
|
end
|
|
elseif s2s_tls_srv_required then
|
|
print(" No _xmpps-server SRV record found for "..host..", but it looks like you need one.");
|
|
all_targets_ok = false;
|
|
end
|
|
end
|
|
if target_hosts:empty() then
|
|
target_hosts:add(host);
|
|
end
|
|
|
|
if target_hosts:contains("localhost") then
|
|
print(" Target 'localhost' cannot be accessed from other servers");
|
|
target_hosts:remove("localhost");
|
|
end
|
|
|
|
local function check_address(target)
|
|
local A, AAAA = dns.lookup(idna.to_ascii(target), "A"), dns.lookup(idna.to_ascii(target), "AAAA");
|
|
local prob = {};
|
|
if use_ipv4 and not (A and #A > 0) then table.insert(prob, "A"); end
|
|
if use_ipv6 and not (AAAA and #AAAA > 0) then table.insert(prob, "AAAA"); end
|
|
return prob;
|
|
end
|
|
|
|
if modules:contains("proxy65") then
|
|
local proxy65_target = api(host):get_option_string("proxy65_address", host);
|
|
if type(proxy65_target) == "string" then
|
|
local prob = check_address(proxy65_target);
|
|
if #prob > 0 then
|
|
print(" File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/")
|
|
.." record. Create one or set 'proxy65_address' to the correct host/IP.");
|
|
end
|
|
else
|
|
print(" proxy65_address for "..host.." should be set to a string, unable to perform DNS check");
|
|
end
|
|
end
|
|
|
|
local known_http_modules = set.new { "bosh"; "http_files"; "http_file_share"; "http_openmetrics"; "websocket" };
|
|
|
|
if modules:contains("http") or not set.intersection(modules, known_http_modules):empty()
|
|
or contains_match(modules, "^http_") or contains_match(modules, "_web$") then
|
|
|
|
local http_host = api(host):get_option_string("http_host", host);
|
|
local http_internal_host = http_host;
|
|
local http_url = api(host):get_option_string("http_external_url");
|
|
if http_url then
|
|
local url_parse = require "socket.url".parse;
|
|
local external_url_parts = url_parse(http_url);
|
|
if external_url_parts then
|
|
http_host = external_url_parts.host;
|
|
else
|
|
print(" The 'http_external_url' setting is not a valid URL");
|
|
end
|
|
end
|
|
|
|
local prob = check_address(http_host);
|
|
if #prob > 1 then
|
|
print(" HTTP service " .. http_host .. " has no " .. table.concat(prob, "/") .. " record. Create one or change "
|
|
.. (http_url and "'http_external_url'" or "'http_host'").." to the correct host.");
|
|
end
|
|
|
|
if http_host ~= http_internal_host then
|
|
print(" Ensure the reverse proxy sets the HTTP Host header to '" .. http_internal_host .. "'");
|
|
end
|
|
end
|
|
|
|
if not use_ipv4 and not use_ipv6 then
|
|
print(" Both IPv6 and IPv4 are disabled, Prosody will not listen on any ports");
|
|
print(" nor be able to connect to any remote servers.");
|
|
all_targets_ok = false;
|
|
end
|
|
|
|
for target_host in target_hosts do
|
|
local host_ok_v4, host_ok_v6;
|
|
do
|
|
local res = dns.lookup(idna.to_ascii(target_host), "A");
|
|
if res then
|
|
for _, record in ipairs(res) do
|
|
if external_addresses:contains(record.a) then
|
|
some_targets_ok = true;
|
|
host_ok_v4 = true;
|
|
elseif internal_addresses:contains(record.a) then
|
|
host_ok_v4 = true;
|
|
some_targets_ok = true;
|
|
print(" "..target_host.." A record points to internal address, external connections might fail");
|
|
else
|
|
print(" "..target_host.." A record points to unknown address "..record.a);
|
|
unknown_addresses:add(record.a);
|
|
all_targets_ok = false;
|
|
end
|
|
end
|
|
end
|
|
end
|
|
do
|
|
local res = dns.lookup(idna.to_ascii(target_host), "AAAA");
|
|
if res then
|
|
for _, record in ipairs(res) do
|
|
if external_addresses:contains(record.aaaa) then
|
|
some_targets_ok = true;
|
|
host_ok_v6 = true;
|
|
elseif internal_addresses:contains(record.aaaa) then
|
|
host_ok_v6 = true;
|
|
some_targets_ok = true;
|
|
print(" "..target_host.." AAAA record points to internal address, external connections might fail");
|
|
else
|
|
print(" "..target_host.." AAAA record points to unknown address "..record.aaaa);
|
|
unknown_addresses:add(record.aaaa);
|
|
all_targets_ok = false;
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if host_ok_v4 and not use_ipv4 then
|
|
print(" Host "..target_host.." does seem to resolve to this server but IPv4 has been disabled");
|
|
all_targets_ok = false;
|
|
end
|
|
|
|
if host_ok_v6 and not use_ipv6 then
|
|
print(" Host "..target_host.." does seem to resolve to this server but IPv6 has been disabled");
|
|
all_targets_ok = false;
|
|
end
|
|
|
|
local bad_protos = {}
|
|
if use_ipv4 and not host_ok_v4 then
|
|
table.insert(bad_protos, "IPv4");
|
|
end
|
|
if use_ipv6 and not host_ok_v6 then
|
|
table.insert(bad_protos, "IPv6");
|
|
end
|
|
if #bad_protos > 0 then
|
|
print(" Host "..target_host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")");
|
|
end
|
|
if host_ok_v6 and not v6_supported then
|
|
print(" Host "..target_host.." has AAAA records, but your version of LuaSocket does not support IPv6.");
|
|
print(" Please see https://prosody.im/doc/ipv6 for more information.");
|
|
elseif host_ok_v6 and not use_ipv6 then
|
|
print(" Host "..target_host.." has AAAA records, but IPv6 is disabled.");
|
|
-- TODO Tell them to drop the AAAA records or enable IPv6?
|
|
print(" Please see https://prosody.im/doc/ipv6 for more information.");
|
|
end
|
|
end
|
|
if not all_targets_ok then
|
|
print(" "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server.");
|
|
if is_component then
|
|
print(" DNS records are necessary if you want users on other servers to access this component.");
|
|
end
|
|
problem_hosts:add(host);
|
|
end
|
|
print("");
|
|
end
|
|
if not problem_hosts:empty() then
|
|
if not unknown_addresses:empty() then
|
|
print("");
|
|
print("Some of your DNS records point to unknown IP addresses. This may be expected if your server");
|
|
print("is behind a NAT or proxy. The unrecognized addresses were:");
|
|
print("");
|
|
print(" Unrecognized: "..tostring(unknown_addresses));
|
|
print("");
|
|
print("The addresses we found on this system are:");
|
|
print("");
|
|
print(" Internal: "..tostring(internal_addresses));
|
|
print(" External: "..tostring(external_addresses));
|
|
print("")
|
|
print("If the list of external external addresses is incorrect you can specify correct addresses in the config:")
|
|
print(" external_addresses = { \"192.0.2.34\", \"2001:db8::abcd:1234\" }")
|
|
end
|
|
print("");
|
|
print("For more information about DNS configuration please see https://prosody.im/doc/dns");
|
|
print("");
|
|
ok = false;
|
|
end
|
|
end
|
|
function checks.certs()
|
|
local cert_ok;
|
|
print"Checking certificates..."
|
|
local x509_verify_identity = require"prosody.util.x509".verify_identity;
|
|
local use_dane = configmanager.get("*", "use_dane");
|
|
local pem2der = require"prosody.util.x509".pem2der;
|
|
local sha256 = require"prosody.util.hashes".sha256;
|
|
local create_context = require "prosody.core.certmanager".create_context;
|
|
local ssl = dependencies.softreq"ssl";
|
|
-- local datetime_parse = require"util.datetime".parse_x509;
|
|
local load_cert = ssl and ssl.loadcertificate;
|
|
-- or ssl.cert_from_pem
|
|
if not ssl then
|
|
print("LuaSec not available, can't perform certificate checks")
|
|
if what == "certs" then cert_ok = false end
|
|
elseif not load_cert then
|
|
print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking");
|
|
cert_ok = false
|
|
else
|
|
for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do
|
|
local modules = modulemanager.get_modules_for_host(host);
|
|
print("Checking certificate for "..host);
|
|
-- First, let's find out what certificate this host uses.
|
|
local host_ssl_config = configmanager.rawget(host, "ssl")
|
|
or configmanager.rawget(host:match("%.(.*)"), "ssl");
|
|
local global_ssl_config = configmanager.rawget("*", "ssl");
|
|
local ctx_ok, err, ssl_config = create_context(host, "server", host_ssl_config, global_ssl_config);
|
|
if not ctx_ok then
|
|
print(" Error: "..err);
|
|
cert_ok = false
|
|
elseif not ssl_config.certificate then
|
|
print(" No 'certificate' found for "..host)
|
|
cert_ok = false
|
|
elseif not ssl_config.key then
|
|
print(" No 'key' found for "..host)
|
|
cert_ok = false
|
|
else
|
|
local key, err = io.open(ssl_config.key); -- Permissions check only
|
|
if not key then
|
|
print(" Could not open "..ssl_config.key..": "..err);
|
|
cert_ok = false
|
|
else
|
|
key:close();
|
|
end
|
|
local cert_fh, err = io.open(ssl_config.certificate); -- Load the file.
|
|
if not cert_fh then
|
|
print(" Could not open "..ssl_config.certificate..": "..err);
|
|
cert_ok = false
|
|
else
|
|
print(" Certificate: "..ssl_config.certificate)
|
|
local cert = load_cert(cert_fh:read"*a"); cert_fh:close();
|
|
if not cert:validat(os.time()) then
|
|
print(" Certificate has expired.")
|
|
cert_ok = false
|
|
elseif not cert:validat(os.time() + 86400) then
|
|
print(" Certificate expires within one day.")
|
|
cert_ok = false
|
|
elseif not cert:validat(os.time() + 86400*7) then
|
|
print(" Certificate expires within one week.")
|
|
elseif not cert:validat(os.time() + 86400*31) then
|
|
print(" Certificate expires within one month.")
|
|
end
|
|
if modules:contains("c2s") and not x509_verify_identity(host, "_xmpp-client", cert) then
|
|
print(" Not valid for client connections to "..host..".")
|
|
cert_ok = false
|
|
end
|
|
local anon = api(host):get_option_string("authentication", "internal_hashed") == "anonymous";
|
|
local anon_s2s = api(host):get_option_boolean("allow_anonymous_s2s", false);
|
|
if modules:contains("s2s") and (anon_s2s or not anon) and not x509_verify_identity(host, "_xmpp-server", cert) then
|
|
print(" Not valid for server-to-server connections to "..host..".")
|
|
cert_ok = false
|
|
end
|
|
|
|
local known_http_modules = set.new { "bosh"; "http_files"; "http_file_share"; "http_openmetrics"; "websocket" };
|
|
local http_loaded = modules:contains("http")
|
|
or not set.intersection(modules, known_http_modules):empty()
|
|
or contains_match(modules, "^http_")
|
|
or contains_match(modules, "_web$");
|
|
|
|
local http_host = api(host):get_option_string("http_host", host);
|
|
if api(host):get_option_string("http_external_url") then
|
|
-- Assumed behind a reverse proxy
|
|
http_loaded = false;
|
|
end
|
|
if http_loaded and not x509_verify_identity(http_host, nil, cert) then
|
|
print(" Not valid for HTTPS connections to "..host..".")
|
|
cert_ok = false
|
|
end
|
|
if use_dane then
|
|
if cert.pubkey then
|
|
print(" DANE: TLSA 3 1 1 "..sha256(pem2der(cert:pubkey()), true))
|
|
elseif cert.pem then
|
|
print(" DANE: TLSA 3 0 1 "..sha256(pem2der(cert:pem()), true))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if cert_ok == false then
|
|
print("")
|
|
print("For more information about certificates please see https://prosody.im/doc/certificates");
|
|
ok = false
|
|
end
|
|
print("")
|
|
end
|
|
-- intentionally not doing this by default
|
|
function checks.connectivity()
|
|
local _, prosody_is_running = is_prosody_running();
|
|
if api("*"):get_option_string("pidfile") and not prosody_is_running then
|
|
print("Prosody does not appear to be running, which is required for this test.");
|
|
print("Start it and then try again.");
|
|
return 1;
|
|
end
|
|
|
|
local checker = "observe.jabber.network";
|
|
local probe_instance;
|
|
local probe_modules = {
|
|
["xmpp-client"] = "c2s_normal_auth";
|
|
["xmpp-server"] = "s2s_normal";
|
|
["xmpps-client"] = nil; -- TODO
|
|
["xmpps-server"] = nil; -- TODO
|
|
};
|
|
local probe_settings = api("*"):get_option_string("connectivity_probe");
|
|
if type(probe_settings) == "string" then
|
|
probe_instance = probe_settings;
|
|
elseif type(probe_settings) == "table" and type(probe_settings.url) == "string" then
|
|
probe_instance = probe_settings.url;
|
|
if type(probe_settings.modules) == "table" then
|
|
probe_modules = probe_settings.modules;
|
|
end
|
|
elseif probe_settings ~= nil then
|
|
print("The 'connectivity_probe' setting not understood.");
|
|
print("Expected an URL or a table with 'url' and 'modules' fields");
|
|
print("See https://prosody.im/doc/prosodyctl#check for more information."); -- FIXME
|
|
return 1;
|
|
end
|
|
|
|
local check_api;
|
|
if probe_instance then
|
|
local parsed_url = socket_url.parse(probe_instance);
|
|
if not parsed_url then
|
|
print(("'connectivity_probe' is not a valid URL: %q"):format(probe_instance));
|
|
print("Set it to the URL of an XMPP Blackbox Exporter instance and try again");
|
|
return 1;
|
|
end
|
|
checker = parsed_url.host;
|
|
|
|
function check_api(protocol, host)
|
|
local target = socket_url.build({scheme="xmpp",path=host});
|
|
local probe_module = probe_modules[protocol];
|
|
if not probe_module then
|
|
return nil, "Checking protocol '"..protocol.."' is currently unsupported";
|
|
end
|
|
return check_probe(probe_instance, probe_module, target);
|
|
end
|
|
else
|
|
check_api = check_ojn;
|
|
end
|
|
|
|
for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do
|
|
local modules, component_module = modulemanager.get_modules_for_host(host);
|
|
if component_module then
|
|
modules:add(component_module)
|
|
end
|
|
|
|
print("Checking external connectivity for "..host.." via "..checker)
|
|
local function check_connectivity(protocol)
|
|
local success, err = check_api(protocol, host);
|
|
if not success and err ~= nil then
|
|
print((" %s: Failed to request check at API: %s"):format(protocol, err))
|
|
elseif success then
|
|
print((" %s: Works"):format(protocol))
|
|
else
|
|
print((" %s: Check service failed to establish (secure) connection"):format(protocol))
|
|
ok = false
|
|
end
|
|
end
|
|
|
|
if modules:contains("c2s") then
|
|
check_connectivity("xmpp-client")
|
|
if not api("*"):get_option_set("c2s_direct_tls_ports", {}):empty() then
|
|
check_connectivity("xmpps-client");
|
|
end
|
|
end
|
|
|
|
if modules:contains("s2s") then
|
|
check_connectivity("xmpp-server")
|
|
if not api("*"):get_option_set("s2s_direct_tls_ports", {}):empty() then
|
|
check_connectivity("xmpps-server");
|
|
end
|
|
end
|
|
|
|
print()
|
|
end
|
|
print("Note: The connectivity check only checks the reachability of the domain.")
|
|
print("Note: It does not ensure that the check actually reaches this specific prosody instance.")
|
|
end
|
|
|
|
function checks.turn()
|
|
local turn_enabled_hosts = {};
|
|
local turn_services = {};
|
|
|
|
for host in enabled_hosts() do
|
|
local has_external_turn = modulemanager.get_modules_for_host(host):contains("turn_external");
|
|
if has_external_turn then
|
|
local hostapi = api(host);
|
|
table.insert(turn_enabled_hosts, host);
|
|
local turn_host = hostapi:get_option_string("turn_external_host", host);
|
|
local turn_port = hostapi:get_option_number("turn_external_port", 3478);
|
|
local turn_secret = hostapi:get_option_string("turn_external_secret");
|
|
if not turn_secret then
|
|
print("Error: Your configuration is missing a turn_external_secret for "..host);
|
|
print("Error: TURN will not be advertised for this host.");
|
|
ok = false;
|
|
else
|
|
local turn_id = ("%s:%d"):format(turn_host, turn_port);
|
|
if turn_services[turn_id] and turn_services[turn_id].secret ~= turn_secret then
|
|
print("Error: Your configuration contains multiple differing secrets");
|
|
print(" for the TURN service at "..turn_id.." - we will only test one.");
|
|
elseif not turn_services[turn_id] then
|
|
turn_services[turn_id] = {
|
|
host = turn_host;
|
|
port = turn_port;
|
|
secret = turn_secret;
|
|
};
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if what == "turn" then
|
|
local count = it.count(pairs(turn_services));
|
|
if count == 0 then
|
|
print("Error: Unable to find any TURN services configured. Enable mod_turn_external!");
|
|
ok = false;
|
|
else
|
|
print("Identified "..tostring(count).." TURN services.");
|
|
print("");
|
|
end
|
|
end
|
|
|
|
for turn_id, turn_service in pairs(turn_services) do
|
|
print("Testing TURN service "..turn_id.."...");
|
|
|
|
local result = check_turn_service(turn_service, opts.ping);
|
|
if #result.warnings > 0 then
|
|
print(("%d warnings:\n"):format(#result.warnings));
|
|
print(" "..table.concat(result.warnings, "\n "));
|
|
print("");
|
|
end
|
|
|
|
if opts.verbose then
|
|
if result.external_ip then
|
|
print(("External IP: %s"):format(result.external_ip.address));
|
|
end
|
|
if result.relayed_addresses then
|
|
for i, relayed_address in ipairs(result.relayed_addresses) do
|
|
print(("Relayed address %d: %s:%d"):format(i, relayed_address.address, relayed_address.port));
|
|
end
|
|
end
|
|
if result.external_ip_pong then
|
|
print(("TURN external address: %s:%d"):format(result.external_ip_pong.address, result.external_ip_pong.port));
|
|
end
|
|
end
|
|
|
|
if result.error then
|
|
print("Error: "..result.error.."\n");
|
|
ok = false;
|
|
else
|
|
print("Success!\n");
|
|
end
|
|
end
|
|
end
|
|
|
|
function checks.features(check_host, quiet)
|
|
if not quiet then
|
|
print("Feature report");
|
|
end
|
|
|
|
local common_subdomains = {
|
|
http_file_share = "share";
|
|
muc = "groups";
|
|
};
|
|
|
|
local recommended_component_modules = {
|
|
muc = { "muc_mam" };
|
|
};
|
|
|
|
local function print_feature_status(feature, host)
|
|
if quiet then return; end
|
|
print("", feature.ok and "OK" or "(!)", feature.name);
|
|
if feature.desc then
|
|
print("", "", feature.desc);
|
|
print("");
|
|
end
|
|
if not feature.ok then
|
|
if feature.lacking_modules then
|
|
table.sort(feature.lacking_modules);
|
|
print("", "", "Suggested modules: ");
|
|
for _, module in ipairs(feature.lacking_modules) do
|
|
print("", "", (" - %s: https://prosody.im/doc/modules/mod_%s"):format(module, module));
|
|
end
|
|
end
|
|
if feature.lacking_components then
|
|
table.sort(feature.lacking_components);
|
|
for _, component_module in ipairs(feature.lacking_components) do
|
|
local subdomain = common_subdomains[component_module];
|
|
local recommended_mods = recommended_component_modules[component_module];
|
|
if subdomain then
|
|
print("", "", "Suggested component:");
|
|
print("");
|
|
print("", "", "", ("-- Documentation: https://prosody.im/doc/modules/mod_%s"):format(component_module));
|
|
print("", "", "", ("Component %q %q"):format(subdomain.."."..host, component_module));
|
|
if recommended_mods then
|
|
print("", "", "", " modules_enabled = {");
|
|
table.sort(recommended_mods);
|
|
for _, mod in ipairs(recommended_mods) do
|
|
print("", "", "", (" %q;"):format(mod));
|
|
end
|
|
print("", "", "", " }");
|
|
end
|
|
else
|
|
print("", "", ("Suggested component: %s"):format(component_module));
|
|
end
|
|
end
|
|
print("");
|
|
print("", "", "If you have already configured any of these components, they may not be");
|
|
print("", "", "linked correctly to "..host..". For more info see https://prosody.im/doc/components");
|
|
end
|
|
if feature.lacking_component_modules then
|
|
table.sort(feature.lacking_component_modules, function (a, b)
|
|
return a.host < b.host;
|
|
end);
|
|
for _, problem in ipairs(feature.lacking_component_modules) do
|
|
local hostapi = api(problem.host);
|
|
local current_modules_enabled = hostapi:get_option_array("modules_enabled", {});
|
|
print("", "", ("Component %q is missing the following modules: %s"):format(problem.host, table.concat(problem.missing_mods)));
|
|
print("");
|
|
print("","", "Add the missing modules to your modules_enabled under the Component, like this:");
|
|
print("");
|
|
print("");
|
|
print("", "", "", ("-- Documentation: https://prosody.im/doc/modules/mod_%s"):format(problem.component_module));
|
|
print("", "", "", ("Component %q %q"):format(problem.host, problem.component_module));
|
|
print("", "", "", (" modules_enabled = {"));
|
|
for _, mod in ipairs(current_modules_enabled) do
|
|
print("", "", "", (" %q;"):format(mod));
|
|
end
|
|
for _, mod in ipairs(problem.missing_mods) do
|
|
print("", "", "", (" %q; -- Add this!"):format(mod));
|
|
end
|
|
print("", "", "", (" }"));
|
|
end
|
|
end
|
|
end
|
|
if feature.meta then
|
|
for k, v in it.sorted_pairs(feature.meta) do
|
|
print("", "", (" - %s: %s"):format(k, v));
|
|
end
|
|
end
|
|
print("");
|
|
end
|
|
|
|
local all_ok = true;
|
|
|
|
local config = configmanager.getconfig();
|
|
|
|
local f, s, v;
|
|
if check_host then
|
|
f, s, v = it.values({ check_host });
|
|
else
|
|
f, s, v = enabled_user_hosts();
|
|
end
|
|
|
|
for host in f, s, v do
|
|
local modules_enabled = set.new(config["*"].modules_enabled);
|
|
modules_enabled:include(set.new(config[host].modules_enabled));
|
|
|
|
-- { [component_module] = { hostname1, hostname2, ... } }
|
|
local host_components = setmetatable({}, { __index = function (t, k) return rawset(t, k, {})[k]; end });
|
|
|
|
do
|
|
local hostapi = api(host);
|
|
|
|
-- Find implicitly linked components
|
|
for other_host in enabled_components() do
|
|
local parent_host = other_host:match("^[^.]+%.(.+)$");
|
|
if parent_host == host then
|
|
local component_module = configmanager.get(other_host, "component_module");
|
|
if component_module then
|
|
table.insert(host_components[component_module], other_host);
|
|
end
|
|
end
|
|
end
|
|
|
|
-- And components linked explicitly
|
|
for _, disco_item in ipairs(hostapi:get_option_array("disco_items", {})) do
|
|
local other_host = disco_item[1];
|
|
if type(other_host) == "string" then
|
|
local component_module = configmanager.get(other_host, "component_module");
|
|
if component_module then
|
|
table.insert(host_components[component_module], other_host);
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local current_feature;
|
|
|
|
local function check_module(suggested, alternate, ...)
|
|
if set.intersection(modules_enabled, set.new({suggested, alternate, ...})):empty() then
|
|
current_feature.lacking_modules = current_feature.lacking_modules or {};
|
|
table.insert(current_feature.lacking_modules, suggested);
|
|
end
|
|
end
|
|
|
|
local function check_component(suggested, alternate, ...)
|
|
local found;
|
|
for _, component_module in ipairs({ suggested, alternate, ... }) do
|
|
found = host_components[component_module][1];
|
|
if found then
|
|
local enabled_component_modules = api(found):get_option_inherited_set("modules_enabled");
|
|
local recommended_mods = recommended_component_modules[component_module];
|
|
if recommended_mods then
|
|
local missing_mods = {};
|
|
for _, mod in ipairs(recommended_mods) do
|
|
if not enabled_component_modules:contains(mod) then
|
|
table.insert(missing_mods, mod);
|
|
end
|
|
end
|
|
if #missing_mods > 0 then
|
|
if not current_feature.lacking_component_modules then
|
|
current_feature.lacking_component_modules = {};
|
|
end
|
|
table.insert(current_feature.lacking_component_modules, {
|
|
host = found;
|
|
component_module = component_module;
|
|
missing_mods = missing_mods;
|
|
});
|
|
end
|
|
end
|
|
break;
|
|
end
|
|
end
|
|
if not found then
|
|
current_feature.lacking_components = current_feature.lacking_components or {};
|
|
table.insert(current_feature.lacking_components, suggested);
|
|
end
|
|
return found;
|
|
end
|
|
|
|
local features = {
|
|
{
|
|
name = "Basic functionality";
|
|
desc = "Support for secure connections, authentication and messaging";
|
|
check = function ()
|
|
check_module("disco");
|
|
check_module("roster");
|
|
check_module("saslauth");
|
|
check_module("tls");
|
|
end;
|
|
};
|
|
{
|
|
name = "Multi-device messaging and data synchronization";
|
|
desc = "Multiple clients connected to the same account stay in sync";
|
|
check = function ()
|
|
check_module("carbons");
|
|
check_module("mam");
|
|
check_module("bookmarks");
|
|
check_module("pep");
|
|
end;
|
|
};
|
|
{
|
|
name = "Mobile optimizations";
|
|
desc = "Help mobile clients reduce battery and data usage";
|
|
check = function ()
|
|
check_module("smacks");
|
|
check_module("csi_simple", "csi_battery_saver");
|
|
end;
|
|
};
|
|
{
|
|
name = "Web connections";
|
|
desc = "Allow connections from browser-based web clients";
|
|
check = function ()
|
|
check_module("bosh");
|
|
check_module("websocket");
|
|
end;
|
|
};
|
|
{
|
|
name = "User profiles";
|
|
desc = "Enable users to publish profile information";
|
|
check = function ()
|
|
check_module("vcard_legacy", "vcard");
|
|
end;
|
|
};
|
|
{
|
|
name = "Blocking";
|
|
desc = "Block communication with chosen entities";
|
|
check = function ()
|
|
check_module("blocklist");
|
|
end;
|
|
};
|
|
{
|
|
name = "Push notifications";
|
|
desc = "Receive notifications on platforms that don't support persistent connections";
|
|
check = function ()
|
|
check_module("cloud_notify");
|
|
end;
|
|
};
|
|
{
|
|
name = "Audio/video calls and P2P";
|
|
desc = "Assist clients in setting up connections between each other";
|
|
check = function ()
|
|
check_module(
|
|
"turn_external",
|
|
"external_services",
|
|
"turncredentials",
|
|
"extdisco"
|
|
);
|
|
end;
|
|
};
|
|
{
|
|
name = "File sharing";
|
|
desc = "Sharing of files to groups and offline users";
|
|
check = function (self)
|
|
local service = check_component("http_file_share", "http_upload", "http_upload_external");
|
|
if service then
|
|
local size_limit;
|
|
if api(service):get_option("component_module") == "http_file_share" then
|
|
size_limit = api(service):get_option_number("http_file_share_size_limit", 10*1024*1024);
|
|
end
|
|
if size_limit then
|
|
self.meta = {
|
|
["Size limit"] = human_units.format(size_limit, "b", "b");
|
|
};
|
|
end
|
|
end
|
|
end;
|
|
};
|
|
{
|
|
name = "Group chats";
|
|
desc = "Create group chats and channels";
|
|
check = function ()
|
|
check_component("muc");
|
|
end;
|
|
};
|
|
};
|
|
|
|
if not quiet then
|
|
print(host);
|
|
end
|
|
|
|
for _, feature in ipairs(features) do
|
|
current_feature = feature;
|
|
feature:check();
|
|
feature.ok = (
|
|
not feature.lacking_modules and
|
|
not feature.lacking_components and
|
|
not feature.lacking_component_modules
|
|
);
|
|
-- For improved presentation, we group the (ok) and (not ok) features
|
|
if feature.ok then
|
|
print_feature_status(feature, host);
|
|
end
|
|
end
|
|
|
|
for _, feature in ipairs(features) do
|
|
if not feature.ok then
|
|
all_ok = false;
|
|
print_feature_status(feature, host);
|
|
end
|
|
end
|
|
|
|
if not quiet then
|
|
print("");
|
|
end
|
|
end
|
|
|
|
return all_ok;
|
|
end
|
|
|
|
if what == nil or what == "all" then
|
|
local ret;
|
|
ret = checks.disabled();
|
|
if ret ~= nil then return ret; end
|
|
ret = checks.config();
|
|
if ret ~= nil then return ret; end
|
|
ret = checks.dns();
|
|
if ret ~= nil then return ret; end
|
|
ret = checks.certs();
|
|
if ret ~= nil then return ret; end
|
|
ret = checks.turn();
|
|
if ret ~= nil then return ret; end
|
|
elseif checks[what] then
|
|
local ret = checks[what]();
|
|
if ret ~= nil then return ret; end
|
|
else
|
|
show_warning("Don't know how to check '%s'. Try one of 'config', 'dns', 'certs', 'disabled', 'turn' or 'connectivity'.", what);
|
|
show_warning("Note: The connectivity check will connect to a remote server.");
|
|
return 1;
|
|
end
|
|
|
|
if not ok then
|
|
print("Problems found, see above.");
|
|
else
|
|
print("All checks passed, congratulations!");
|
|
end
|
|
return ok and 0 or 2;
|
|
end
|
|
|
|
return {
|
|
check = check;
|
|
};
|