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