Comparison

util/prosodyctl/check.lua @ 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
child 10932:ea4a7619058f
comparison
equal deleted inserted replaced
10870:3f1889608f3e 10871:e5dee71d0ebb
1 local configmanager = require "core.configmanager";
2 local show_usage = require "util.prosodyctl".show_usage;
3 local show_warning = require "util.prosodyctl".show_warning;
4 local dependencies = require "util.dependencies";
5 local socket = require "socket";
6 local jid_split = require "util.jid".prepped_split;
7 local modulemanager = require "core.modulemanager";
8
9 local function check(arg)
10 if arg[1] == "--help" then
11 show_usage([[check]], [[Perform basic checks on your Prosody installation]]);
12 return 1;
13 end
14 local what = table.remove(arg, 1);
15 local set = require "util.set";
16 local it = require "util.iterators";
17 local ok = true;
18 local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end
19 local function enabled_hosts() return it.filter(disabled_hosts, pairs(configmanager.getconfig())); end
20 if not (what == nil or what == "disabled" or what == "config" or what == "dns" or what == "certs") then
21 show_warning("Don't know how to check '%s'. Try one of 'config', 'dns', 'certs' or 'disabled'.", what);
22 return 1;
23 end
24 if not what or what == "disabled" then
25 local disabled_hosts_set = set.new();
26 for host, host_options in it.filter("*", pairs(configmanager.getconfig())) do
27 if host_options.enabled == false then
28 disabled_hosts_set:add(host);
29 end
30 end
31 if not disabled_hosts_set:empty() then
32 local msg = "Checks will be skipped for these disabled hosts: %s";
33 if what then msg = "These hosts are disabled: %s"; end
34 show_warning(msg, tostring(disabled_hosts_set));
35 if what then return 0; end
36 print""
37 end
38 end
39 if not what or what == "config" then
40 print("Checking config...");
41 local deprecated = set.new({
42 "bosh_ports", "disallow_s2s", "no_daemonize", "anonymous_login", "require_encryption",
43 "vcard_compatibility", "cross_domain_bosh", "cross_domain_websocket", "daemonize",
44 });
45 local known_global_options = set.new({
46 "pidfile", "log", "plugin_paths", "prosody_user", "prosody_group", "daemonize",
47 "umask", "prosodyctl_timeout", "use_ipv6", "use_libevent", "network_settings",
48 "network_backend", "http_default_host",
49 "statistics_interval", "statistics", "statistics_config",
50 });
51 local config = configmanager.getconfig();
52 -- Check that we have any global options (caused by putting a host at the top)
53 if it.count(it.filter("log", pairs(config["*"]))) == 0 then
54 ok = false;
55 print("");
56 print(" No global options defined. Perhaps you have put a host definition at the top")
57 print(" of the config file? They should be at the bottom, see https://prosody.im/doc/configure#overview");
58 end
59 if it.count(enabled_hosts()) == 0 then
60 ok = false;
61 print("");
62 if it.count(it.filter("*", pairs(config))) == 0 then
63 print(" No hosts are defined, please add at least one VirtualHost section")
64 elseif config["*"]["enabled"] == false then
65 print(" No hosts are enabled. Remove enabled = false from the global section or put enabled = true under at least one VirtualHost section")
66 else
67 print(" All hosts are disabled. Remove enabled = false from at least one VirtualHost section")
68 end
69 end
70 if not config["*"].modules_enabled then
71 print(" No global modules_enabled is set?");
72 local suggested_global_modules;
73 for host, options in enabled_hosts() do --luacheck: ignore 213/host
74 if not options.component_module and options.modules_enabled then
75 suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled));
76 end
77 end
78 if suggested_global_modules and not suggested_global_modules:empty() then
79 print(" Consider moving these modules into modules_enabled in the global section:")
80 print(" "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end));
81 end
82 print();
83 end
84
85 do -- Check for modules enabled both normally and as components
86 local modules = set.new(config["*"]["modules_enabled"]);
87 for host, options in enabled_hosts() do
88 local component_module = options.component_module;
89 if component_module and modules:contains(component_module) then
90 print((" mod_%s is enabled both in modules_enabled and as Component %q %q"):format(component_module, host, component_module));
91 print(" This means the service is enabled on all VirtualHosts as well as the Component.");
92 print(" Are you sure this what you want? It may cause unexpected behaviour.");
93 end
94 end
95 end
96
97 -- Check for global options under hosts
98 local global_options = set.new(it.to_array(it.keys(config["*"])));
99 local deprecated_global_options = set.intersection(global_options, deprecated);
100 if not deprecated_global_options:empty() then
101 print("");
102 print(" You have some deprecated options in the global section:");
103 print(" "..tostring(deprecated_global_options))
104 ok = false;
105 end
106 for host, options in it.filter(function (h) return h ~= "*" end, pairs(configmanager.getconfig())) do
107 local host_options = set.new(it.to_array(it.keys(options)));
108 local misplaced_options = set.intersection(host_options, known_global_options);
109 for name in pairs(options) do
110 if name:match("^interfaces?")
111 or name:match("_ports?$") or name:match("_interfaces?$")
112 or (name:match("_ssl$") and not name:match("^[cs]2s_ssl$")) then
113 misplaced_options:add(name);
114 end
115 end
116 if not misplaced_options:empty() then
117 ok = false;
118 print("");
119 local n = it.count(misplaced_options);
120 print(" You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be");
121 print(" in the global section of the config file, above any VirtualHost or Component definitions,")
122 print(" see https://prosody.im/doc/configure#overview for more information.")
123 print("");
124 print(" You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", "));
125 end
126 end
127 for host, options in enabled_hosts() do
128 local host_options = set.new(it.to_array(it.keys(options)));
129 local subdomain = host:match("^[^.]+");
130 if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp"
131 or subdomain == "chat" or subdomain == "im") then
132 print("");
133 print(" Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to");
134 print(" "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host..".");
135 print(" For more information see: https://prosody.im/doc/dns");
136 end
137 end
138 local all_modules = set.new(config["*"].modules_enabled);
139 local all_options = set.new(it.to_array(it.keys(config["*"])));
140 for host in enabled_hosts() do
141 all_options:include(set.new(it.to_array(it.keys(config[host]))));
142 all_modules:include(set.new(config[host].modules_enabled));
143 end
144 for mod in all_modules do
145 if mod:match("^mod_") then
146 print("");
147 print(" Modules in modules_enabled should not have the 'mod_' prefix included.");
148 print(" Change '"..mod.."' to '"..mod:match("^mod_(.*)").."'.");
149 elseif mod:match("^auth_") then
150 print("");
151 print(" Authentication modules should not be added to modules_enabled,");
152 print(" but be specified in the 'authentication' option.");
153 print(" Remove '"..mod.."' from modules_enabled and instead add");
154 print(" authentication = '"..mod:match("^auth_(.*)").."'");
155 print(" For more information see https://prosody.im/doc/authentication");
156 elseif mod:match("^storage_") then
157 print("");
158 print(" storage modules should not be added to modules_enabled,");
159 print(" but be specified in the 'storage' option.");
160 print(" Remove '"..mod.."' from modules_enabled and instead add");
161 print(" storage = '"..mod:match("^storage_(.*)").."'");
162 print(" For more information see https://prosody.im/doc/storage");
163 end
164 end
165 if all_modules:contains("vcard") and all_modules:contains("vcard_legacy") then
166 print("");
167 print(" Both mod_vcard_legacy and mod_vcard are enabled but they conflict");
168 print(" with each other. Remove one.");
169 end
170 if all_modules:contains("pep") and all_modules:contains("pep_simple") then
171 print("");
172 print(" Both mod_pep_simple and mod_pep are enabled but they conflict");
173 print(" with each other. Remove one.");
174 end
175 for host, host_config in pairs(config) do --luacheck: ignore 213/host
176 if type(rawget(host_config, "storage")) == "string" and rawget(host_config, "default_storage") then
177 print("");
178 print(" The 'default_storage' option is not needed if 'storage' is set to a string.");
179 break;
180 end
181 end
182 local require_encryption = set.intersection(all_options, set.new({
183 "require_encryption", "c2s_require_encryption", "s2s_require_encryption"
184 })):empty();
185 local ssl = dependencies.softreq"ssl";
186 if not ssl then
187 if not require_encryption then
188 print("");
189 print(" You require encryption but LuaSec is not available.");
190 print(" Connections will fail.");
191 ok = false;
192 end
193 elseif not ssl.loadcertificate then
194 if all_options:contains("s2s_secure_auth") then
195 print("");
196 print(" You have set s2s_secure_auth but your version of LuaSec does ");
197 print(" not support certificate validation, so all s2s connections will");
198 print(" fail.");
199 ok = false;
200 elseif all_options:contains("s2s_secure_domains") then
201 local secure_domains = set.new();
202 for host in enabled_hosts() do
203 if config[host].s2s_secure_auth == true then
204 secure_domains:add("*");
205 else
206 secure_domains:include(set.new(config[host].s2s_secure_domains));
207 end
208 end
209 if not secure_domains:empty() then
210 print("");
211 print(" You have set s2s_secure_domains but your version of LuaSec does ");
212 print(" not support certificate validation, so s2s connections to/from ");
213 print(" these domains will fail.");
214 ok = false;
215 end
216 end
217 elseif require_encryption and not all_modules:contains("tls") then
218 print("");
219 print(" You require encryption but mod_tls is not enabled.");
220 print(" Connections will fail.");
221 ok = false;
222 end
223
224 print("Done.\n");
225 end
226 if not what or what == "dns" then
227 local dns = require "net.dns";
228 local idna = require "util.encodings".idna;
229 local ip = require "util.ip";
230 local c2s_ports = set.new(configmanager.get("*", "c2s_ports") or {5222});
231 local s2s_ports = set.new(configmanager.get("*", "s2s_ports") or {5269});
232
233 local c2s_srv_required, s2s_srv_required;
234 if not c2s_ports:contains(5222) then
235 c2s_srv_required = true;
236 end
237 if not s2s_ports:contains(5269) then
238 s2s_srv_required = true;
239 end
240
241 local problem_hosts = set.new();
242
243 local external_addresses, internal_addresses = set.new(), set.new();
244
245 local fqdn = socket.dns.tohostname(socket.dns.gethostname());
246 if fqdn then
247 do
248 local res = dns.lookup(idna.to_ascii(fqdn), "A");
249 if res then
250 for _, record in ipairs(res) do
251 external_addresses:add(record.a);
252 end
253 end
254 end
255 do
256 local res = dns.lookup(idna.to_ascii(fqdn), "AAAA");
257 if res then
258 for _, record in ipairs(res) do
259 external_addresses:add(record.aaaa);
260 end
261 end
262 end
263 end
264
265 local local_addresses = require"util.net".local_addresses() or {};
266
267 for addr in it.values(local_addresses) do
268 if not ip.new_ip(addr).private then
269 external_addresses:add(addr);
270 else
271 internal_addresses:add(addr);
272 end
273 end
274
275 if external_addresses:empty() then
276 print("");
277 print(" Failed to determine the external addresses of this server. Checks may be inaccurate.");
278 c2s_srv_required, s2s_srv_required = true, true;
279 end
280
281 local v6_supported = not not socket.tcp6;
282
283 for jid, host_options in enabled_hosts() do
284 local all_targets_ok, some_targets_ok = true, false;
285 local node, host = jid_split(jid);
286
287 local modules, component_module = modulemanager.get_modules_for_host(host);
288 if component_module then
289 modules:add(component_module);
290 end
291
292 local is_component = not not host_options.component_module;
293 print("Checking DNS for "..(is_component and "component" or "host").." "..jid.."...");
294 if node then
295 print("Only the domain part ("..host..") is used in DNS.")
296 end
297 local target_hosts = set.new();
298 if modules:contains("c2s") then
299 local res = dns.lookup("_xmpp-client._tcp."..idna.to_ascii(host)..".", "SRV");
300 if res then
301 for _, record in ipairs(res) do
302 target_hosts:add(record.srv.target);
303 if not c2s_ports:contains(record.srv.port) then
304 print(" SRV target "..record.srv.target.." contains unknown client port: "..record.srv.port);
305 end
306 end
307 else
308 if c2s_srv_required then
309 print(" No _xmpp-client SRV record found for "..host..", but it looks like you need one.");
310 all_targets_ok = false;
311 else
312 target_hosts:add(host);
313 end
314 end
315 end
316 if modules:contains("s2s") then
317 local res = dns.lookup("_xmpp-server._tcp."..idna.to_ascii(host)..".", "SRV");
318 if res then
319 for _, record in ipairs(res) do
320 target_hosts:add(record.srv.target);
321 if not s2s_ports:contains(record.srv.port) then
322 print(" SRV target "..record.srv.target.." contains unknown server port: "..record.srv.port);
323 end
324 end
325 else
326 if s2s_srv_required then
327 print(" No _xmpp-server SRV record found for "..host..", but it looks like you need one.");
328 all_targets_ok = false;
329 else
330 target_hosts:add(host);
331 end
332 end
333 end
334 if target_hosts:empty() then
335 target_hosts:add(host);
336 end
337
338 if target_hosts:contains("localhost") then
339 print(" Target 'localhost' cannot be accessed from other servers");
340 target_hosts:remove("localhost");
341 end
342
343 if modules:contains("proxy65") then
344 local proxy65_target = configmanager.get(host, "proxy65_address") or host;
345 if type(proxy65_target) == "string" then
346 local A, AAAA = dns.lookup(idna.to_ascii(proxy65_target), "A"), dns.lookup(idna.to_ascii(proxy65_target), "AAAA");
347 local prob = {};
348 if not A then
349 table.insert(prob, "A");
350 end
351 if v6_supported and not AAAA then
352 table.insert(prob, "AAAA");
353 end
354 if #prob > 0 then
355 print(" File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/")
356 .." record. Create one or set 'proxy65_address' to the correct host/IP.");
357 end
358 else
359 print(" proxy65_address for "..host.." should be set to a string, unable to perform DNS check");
360 end
361 end
362
363 for target_host in target_hosts do
364 local host_ok_v4, host_ok_v6;
365 do
366 local res = dns.lookup(idna.to_ascii(target_host), "A");
367 if res then
368 for _, record in ipairs(res) do
369 if external_addresses:contains(record.a) then
370 some_targets_ok = true;
371 host_ok_v4 = true;
372 elseif internal_addresses:contains(record.a) then
373 host_ok_v4 = true;
374 some_targets_ok = true;
375 print(" "..target_host.." A record points to internal address, external connections might fail");
376 else
377 print(" "..target_host.." A record points to unknown address "..record.a);
378 all_targets_ok = false;
379 end
380 end
381 end
382 end
383 do
384 local res = dns.lookup(idna.to_ascii(target_host), "AAAA");
385 if res then
386 for _, record in ipairs(res) do
387 if external_addresses:contains(record.aaaa) then
388 some_targets_ok = true;
389 host_ok_v6 = true;
390 elseif internal_addresses:contains(record.aaaa) then
391 host_ok_v6 = true;
392 some_targets_ok = true;
393 print(" "..target_host.." AAAA record points to internal address, external connections might fail");
394 else
395 print(" "..target_host.." AAAA record points to unknown address "..record.aaaa);
396 all_targets_ok = false;
397 end
398 end
399 end
400 end
401
402 local bad_protos = {}
403 if not host_ok_v4 then
404 table.insert(bad_protos, "IPv4");
405 end
406 if not host_ok_v6 then
407 table.insert(bad_protos, "IPv6");
408 end
409 if #bad_protos > 0 then
410 print(" Host "..target_host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")");
411 end
412 if host_ok_v6 and not v6_supported then
413 print(" Host "..target_host.." has AAAA records, but your version of LuaSocket does not support IPv6.");
414 print(" Please see https://prosody.im/doc/ipv6 for more information.");
415 end
416 end
417 if not all_targets_ok then
418 print(" "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server.");
419 if is_component then
420 print(" DNS records are necessary if you want users on other servers to access this component.");
421 end
422 problem_hosts:add(host);
423 end
424 print("");
425 end
426 if not problem_hosts:empty() then
427 print("");
428 print("For more information about DNS configuration please see https://prosody.im/doc/dns");
429 print("");
430 ok = false;
431 end
432 end
433 if not what or what == "certs" then
434 local cert_ok;
435 print"Checking certificates..."
436 local x509_verify_identity = require"util.x509".verify_identity;
437 local create_context = require "core.certmanager".create_context;
438 local ssl = dependencies.softreq"ssl";
439 -- local datetime_parse = require"util.datetime".parse_x509;
440 local load_cert = ssl and ssl.loadcertificate;
441 -- or ssl.cert_from_pem
442 if not ssl then
443 print("LuaSec not available, can't perform certificate checks")
444 if what == "certs" then cert_ok = false end
445 elseif not load_cert then
446 print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking");
447 cert_ok = false
448 else
449 local function skip_bare_jid_hosts(host)
450 if jid_split(host) then
451 -- See issue #779
452 return false;
453 end
454 return true;
455 end
456 for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do
457 print("Checking certificate for "..host);
458 -- First, let's find out what certificate this host uses.
459 local host_ssl_config = configmanager.rawget(host, "ssl")
460 or configmanager.rawget(host:match("%.(.*)"), "ssl");
461 local global_ssl_config = configmanager.rawget("*", "ssl");
462 local ok, err, ssl_config = create_context(host, "server", host_ssl_config, global_ssl_config);
463 if not ok then
464 print(" Error: "..err);
465 cert_ok = false
466 elseif not ssl_config.certificate then
467 print(" No 'certificate' found for "..host)
468 cert_ok = false
469 elseif not ssl_config.key then
470 print(" No 'key' found for "..host)
471 cert_ok = false
472 else
473 local key, err = io.open(ssl_config.key); -- Permissions check only
474 if not key then
475 print(" Could not open "..ssl_config.key..": "..err);
476 cert_ok = false
477 else
478 key:close();
479 end
480 local cert_fh, err = io.open(ssl_config.certificate); -- Load the file.
481 if not cert_fh then
482 print(" Could not open "..ssl_config.certificate..": "..err);
483 cert_ok = false
484 else
485 print(" Certificate: "..ssl_config.certificate)
486 local cert = load_cert(cert_fh:read"*a"); cert_fh:close();
487 if not cert:validat(os.time()) then
488 print(" Certificate has expired.")
489 cert_ok = false
490 elseif not cert:validat(os.time() + 86400) then
491 print(" Certificate expires within one day.")
492 cert_ok = false
493 elseif not cert:validat(os.time() + 86400*7) then
494 print(" Certificate expires within one week.")
495 elseif not cert:validat(os.time() + 86400*31) then
496 print(" Certificate expires within one month.")
497 end
498 if configmanager.get(host, "component_module") == nil
499 and not x509_verify_identity(host, "_xmpp-client", cert) then
500 print(" Not valid for client connections to "..host..".")
501 cert_ok = false
502 end
503 if (not (configmanager.get(host, "anonymous_login")
504 or configmanager.get(host, "authentication") == "anonymous"))
505 and not x509_verify_identity(host, "_xmpp-server", cert) then
506 print(" Not valid for server-to-server connections to "..host..".")
507 cert_ok = false
508 end
509 end
510 end
511 end
512 end
513 if cert_ok == false then
514 print("")
515 print("For more information about certificates please see https://prosody.im/doc/certificates");
516 ok = false
517 end
518 print("")
519 end
520 if not ok then
521 print("Problems found, see above.");
522 else
523 print("All checks passed, congratulations!");
524 end
525 return ok and 0 or 2;
526 end
527
528 return {
529 check = check;
530 };