Software /
code /
prosody
Comparison
prosodyctl @ 11120:b2331f3dfeea
Merge 0.11->trunk
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Wed, 30 Sep 2020 09:50:33 +0100 |
parent | 11006:8ac958938e0f |
child | 11133:624eafed3343 |
comparison
equal
deleted
inserted
replaced
11119:68df52bf08d5 | 11120:b2331f3dfeea |
---|---|
8 -- | 8 -- |
9 | 9 |
10 -- prosodyctl - command-line controller for Prosody XMPP server | 10 -- prosodyctl - command-line controller for Prosody XMPP server |
11 | 11 |
12 -- Will be modified by configure script if run -- | 12 -- Will be modified by configure script if run -- |
13 | |
14 CFG_SOURCEDIR=CFG_SOURCEDIR or os.getenv("PROSODY_SRCDIR"); | 13 CFG_SOURCEDIR=CFG_SOURCEDIR or os.getenv("PROSODY_SRCDIR"); |
15 CFG_CONFIGDIR=CFG_CONFIGDIR or os.getenv("PROSODY_CFGDIR"); | 14 CFG_CONFIGDIR=CFG_CONFIGDIR or os.getenv("PROSODY_CFGDIR"); |
16 CFG_PLUGINDIR=CFG_PLUGINDIR or os.getenv("PROSODY_PLUGINDIR"); | 15 CFG_PLUGINDIR=CFG_PLUGINDIR or os.getenv("PROSODY_PLUGINDIR"); |
17 CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR"); | 16 CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR"); |
18 | 17 |
47 | 46 |
48 local startup = require "util.startup"; | 47 local startup = require "util.startup"; |
49 startup.prosodyctl(); | 48 startup.prosodyctl(); |
50 | 49 |
51 ----------- | 50 ----------- |
52 | |
53 local error_messages = setmetatable({ | |
54 ["invalid-username"] = "The given username is invalid in a Jabber ID"; | |
55 ["invalid-hostname"] = "The given hostname is invalid"; | |
56 ["no-password"] = "No password was supplied"; | |
57 ["no-such-user"] = "The given user does not exist on the server"; | |
58 ["no-such-host"] = "The given hostname does not exist in the config"; | |
59 ["unable-to-save-data"] = "Unable to store, perhaps you don't have permission?"; | |
60 ["no-pidfile"] = "There is no 'pidfile' option in the configuration file, see https://prosody.im/doc/prosodyctl#pidfile for help"; | |
61 ["invalid-pidfile"] = "The 'pidfile' option in the configuration file is not a string, see https://prosody.im/doc/prosodyctl#pidfile for help"; | |
62 ["no-posix"] = "The mod_posix module is not enabled in the Prosody config file, see https://prosody.im/doc/prosodyctl for more info"; | |
63 ["no-such-method"] = "This module has no commands"; | |
64 ["not-running"] = "Prosody is not running"; | |
65 }, { __index = function (_,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end }); | |
66 | 51 |
67 local configmanager = require "core.configmanager"; | 52 local configmanager = require "core.configmanager"; |
68 local modulemanager = require "core.modulemanager" | 53 local modulemanager = require "core.modulemanager" |
69 local prosodyctl = require "util.prosodyctl" | 54 local prosodyctl = require "util.prosodyctl" |
70 local socket = require "socket" | 55 local socket = require "socket" |
71 local dependencies = require "util.dependencies"; | 56 local dependencies = require "util.dependencies"; |
72 local lfs = dependencies.softreq "lfs"; | 57 local lfs = dependencies.softreq "lfs"; |
73 | 58 |
74 ----------------------- | 59 ----------------------- |
75 | 60 |
61 local human_io = require "util.human.io"; | |
62 | |
76 local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning; | 63 local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning; |
77 local show_usage = prosodyctl.show_usage; | 64 local show_usage = prosodyctl.show_usage; |
78 local show_yesno = prosodyctl.show_yesno; | 65 local read_password = human_io.read_password; |
79 local show_prompt = prosodyctl.show_prompt; | 66 local call_luarocks = prosodyctl.call_luarocks; |
80 local read_password = prosodyctl.read_password; | 67 local error_messages = prosodyctl.error_messages; |
81 | 68 |
82 local jid_split = require "util.jid".prepped_split; | 69 local jid_split = require "util.jid".prepped_split; |
83 | 70 |
84 local prosodyctl_timeout = (configmanager.get("*", "prosodyctl_timeout") or 5) * 2; | 71 local prosodyctl_timeout = (configmanager.get("*", "prosodyctl_timeout") or 5) * 2; |
85 ----------------------- | 72 ----------------------- |
86 local commands = {}; | 73 local commands = {}; |
87 local command = arg[1]; | 74 local command = table.remove(arg, 1); |
75 | |
76 function commands.install(arg) | |
77 if arg[1] == "--help" then | |
78 show_usage([[install]], [[Installs a prosody/luarocks plugin]]); | |
79 return 1; | |
80 end | |
81 call_luarocks(arg[1], "install") | |
82 end | |
83 | |
84 function commands.remove(arg) | |
85 if arg[1] == "--help" then | |
86 show_usage([[remove]], [[Removes a module installed in the working directory's plugins folder]]); | |
87 return 1; | |
88 end | |
89 call_luarocks(arg[1], "remove") | |
90 end | |
91 | |
92 function commands.list(arg) | |
93 if arg[1] == "--help" then | |
94 show_usage([[list]], [[Shows installed rocks]]); | |
95 return 1; | |
96 end | |
97 call_luarocks(arg[1], "list") | |
98 end | |
88 | 99 |
89 function commands.adduser(arg) | 100 function commands.adduser(arg) |
90 if not arg[1] or arg[1] == "--help" then | 101 if not arg[1] or arg[1] == "--help" then |
91 show_usage([[adduser JID]], [[Create the specified user account in Prosody]]); | 102 show_usage([[adduser JID]], [[Create the specified user account in Prosody]]); |
92 return 1; | 103 return 1; |
119 | 130 |
120 local ok, msg = prosodyctl.adduser { user = user, host = host, password = password }; | 131 local ok, msg = prosodyctl.adduser { user = user, host = host, password = password }; |
121 | 132 |
122 if ok then return 0; end | 133 if ok then return 0; end |
123 | 134 |
124 show_message(msg) | 135 show_message(error_messages[msg]) |
125 return 1; | 136 return 1; |
126 end | 137 end |
127 | 138 |
128 function commands.passwd(arg) | 139 function commands.passwd(arg) |
129 if not arg[1] or arg[1] == "--help" then | 140 if not arg[1] or arg[1] == "--help" then |
236 show_message("Prosody is already running with PID %s", ret or "(unknown)"); | 247 show_message("Prosody is already running with PID %s", ret or "(unknown)"); |
237 return 1; | 248 return 1; |
238 end | 249 end |
239 | 250 |
240 --luacheck: ignore 411/ret | 251 --luacheck: ignore 411/ret |
241 local ok, ret = prosodyctl.start(prosody.paths.source); | 252 local lua; |
253 do | |
254 local i = 0; | |
255 repeat | |
256 i = i - 1; | |
257 until arg[i-1] == nil | |
258 lua = arg[i]; | |
259 end | |
260 local ok, ret = prosodyctl.start(prosody.paths.source, lua); | |
242 if ok then | 261 if ok then |
243 local daemonize = configmanager.get("*", "daemonize"); | 262 local daemonize = configmanager.get("*", "daemonize"); |
244 if daemonize == nil then | 263 if daemonize == nil then |
245 daemonize = prosody.installed; | 264 daemonize = prosody.installed; |
246 end | 265 end |
357 show_usage([[about]], [[Show information about this Prosody installation]]); | 376 show_usage([[about]], [[Show information about this Prosody installation]]); |
358 return 1; | 377 return 1; |
359 end | 378 end |
360 | 379 |
361 local pwd = "."; | 380 local pwd = "."; |
362 local array = require "util.array"; | 381 local sorted_pairs = require "util.iterators".sorted_pairs; |
363 local keys = require "util.iterators".keys; | |
364 local hg = require"util.mercurial"; | 382 local hg = require"util.mercurial"; |
365 local relpath = configmanager.resolve_relative_path; | 383 local relpath = configmanager.resolve_relative_path; |
366 | 384 |
367 print("Prosody "..(prosody.version or "(unknown version)")); | 385 print("Prosody "..(prosody.version or "(unknown version)")); |
368 print(""); | 386 print(""); |
381 hgrepo = hgrepo == "010452cfaf53" and "prosody-modules"; | 399 hgrepo = hgrepo == "010452cfaf53" and "prosody-modules"; |
382 return path..(hgid and " - "..(hgrepo or "HG").." rev: "..hgid or "") | 400 return path..(hgid and " - "..(hgrepo or "HG").." rev: "..hgid or "") |
383 .."\n "; | 401 .."\n "; |
384 end))); | 402 end))); |
385 print(""); | 403 print(""); |
404 local have_pposix, pposix = pcall(require, "util.pposix"); | |
405 if have_pposix and pposix.uname then | |
406 print("# Operating system"); | |
407 local uname, err = pposix.uname(); | |
408 print(uname and uname.sysname .. " " .. uname.release or "Unknown POSIX", err or ""); | |
409 print(""); | |
410 end | |
386 print("# Lua environment"); | 411 print("# Lua environment"); |
387 print("Lua version: ", _G._VERSION); | 412 print("Lua version: ", _G._VERSION); |
388 print(""); | 413 print(""); |
389 print("Lua module search paths:"); | 414 print("Lua module search paths:"); |
390 for path in package.path:gmatch("[^;]+") do | 415 for path in package.path:gmatch("[^;]+") do |
411 print(""); | 436 print(""); |
412 print("Backend: "..require "net.server".get_backend()); | 437 print("Backend: "..require "net.server".get_backend()); |
413 print(""); | 438 print(""); |
414 print("# Lua module versions"); | 439 print("# Lua module versions"); |
415 local module_versions, longest_name = {}, 8; | 440 local module_versions, longest_name = {}, 8; |
416 local luaevent =dependencies.softreq"luaevent"; | 441 local library_versions = {}; |
417 dependencies.softreq"ssl"; | 442 dependencies.softreq"ssl"; |
418 dependencies.softreq"DBI"; | 443 dependencies.softreq"DBI"; |
444 local friendly_names = { | |
445 DBI = "LuaDBI"; | |
446 lfs = "LuaFileSystem"; | |
447 lunbound = "luaunbound"; | |
448 lxp = "LuaExpat"; | |
449 socket = "LuaSocket"; | |
450 ssl = "LuaSec"; | |
451 }; | |
452 local lunbound = dependencies.softreq"lunbound"; | |
419 for name, module in pairs(package.loaded) do | 453 for name, module in pairs(package.loaded) do |
420 if type(module) == "table" and rawget(module, "_VERSION") | 454 if type(module) == "table" and rawget(module, "_VERSION") |
421 and name ~= "_G" and not name:match("%.") then | 455 and name ~= "_G" and not name:match("%.") then |
456 name = friendly_names[name] or name; | |
422 if #name > longest_name then | 457 if #name > longest_name then |
423 longest_name = #name; | 458 longest_name = #name; |
424 end | 459 end |
425 module_versions[name] = module._VERSION; | 460 local mod_version = module._VERSION; |
426 end | 461 if tostring(mod_version):sub(1, #name+1) == name .. " " then |
427 end | 462 mod_version = mod_version:sub(#name+2); |
428 if luaevent then | 463 end |
429 module_versions["libevent"] = luaevent.core.libevent_version(); | 464 module_versions[name] = mod_version; |
430 end | 465 end |
431 local sorted_keys = array.collect(keys(module_versions)):sort(); | 466 end |
432 for _, name in ipairs(sorted_keys) do | 467 if lunbound then |
433 print(name..":"..string.rep(" ", longest_name-#name), module_versions[name]); | 468 if not module_versions["luaunbound"] then |
469 module_versions["luaunbound"] = "0.5 (?)"; | |
470 end | |
471 library_versions["libunbound"] = lunbound._LIBVER; | |
472 end | |
473 for name, version in sorted_pairs(module_versions) do | |
474 print(name..":"..string.rep(" ", longest_name-#name), version); | |
475 end | |
476 print(""); | |
477 print("# library versions"); | |
478 if require "net.server".event_base then | |
479 library_versions["libevent"] = require"luaevent".core.libevent_version(); | |
480 end | |
481 for name, version in sorted_pairs(library_versions) do | |
482 print(name..":"..string.rep(" ", longest_name-#name), version); | |
434 end | 483 end |
435 print(""); | 484 print(""); |
436 end | 485 end |
437 | 486 |
438 function commands.reload(arg) | 487 function commands.reload(arg) |
509 | 558 |
510 if ok then return 0; end | 559 if ok then return 0; end |
511 | 560 |
512 show_message(error_messages[msg]) | 561 show_message(error_messages[msg]) |
513 return 1; | 562 return 1; |
514 end | |
515 | |
516 local openssl; | |
517 | |
518 local cert_commands = {}; | |
519 | |
520 -- If a file already exists, ask if the user wants to use it or replace it | |
521 -- Backups the old file if replaced | |
522 local function use_existing(filename) | |
523 local attrs = lfs.attributes(filename); | |
524 if attrs then | |
525 if show_yesno(filename .. " exists, do you want to replace it? [y/n]") then | |
526 local backup = filename..".bkp~"..os.date("%FT%T", attrs.change); | |
527 os.rename(filename, backup); | |
528 show_message(filename.." backed up to "..backup); | |
529 else | |
530 -- Use the existing file | |
531 return true; | |
532 end | |
533 end | |
534 end | |
535 | |
536 local have_pposix, pposix = pcall(require, "util.pposix"); | |
537 local cert_basedir = prosody.paths.data == "." and "./certs" or prosody.paths.data; | |
538 if have_pposix and pposix.getuid() == 0 then | |
539 -- FIXME should be enough to check if this directory is writable | |
540 local cert_dir = configmanager.get("*", "certificates") or "certs"; | |
541 cert_basedir = configmanager.resolve_relative_path(prosody.paths.config, cert_dir); | |
542 end | |
543 | |
544 function cert_commands.config(arg) | |
545 if #arg >= 1 and arg[1] ~= "--help" then | |
546 local conf_filename = cert_basedir .. "/" .. arg[1] .. ".cnf"; | |
547 if use_existing(conf_filename) then | |
548 return nil, conf_filename; | |
549 end | |
550 local distinguished_name; | |
551 if arg[#arg]:find("^/") then | |
552 distinguished_name = table.remove(arg); | |
553 end | |
554 local conf = openssl.config.new(); | |
555 conf:from_prosody(prosody.hosts, configmanager, arg); | |
556 if distinguished_name then | |
557 local dn = {}; | |
558 for k, v in distinguished_name:gmatch("/([^=/]+)=([^/]+)") do | |
559 table.insert(dn, k); | |
560 dn[k] = v; | |
561 end | |
562 conf.distinguished_name = dn; | |
563 else | |
564 show_message("Please provide details to include in the certificate config file."); | |
565 show_message("Leave the field empty to use the default value or '.' to exclude the field.") | |
566 for _, k in ipairs(openssl._DN_order) do | |
567 local v = conf.distinguished_name[k]; | |
568 if v then | |
569 local nv = nil; | |
570 if k == "commonName" then | |
571 v = arg[1] | |
572 elseif k == "emailAddress" then | |
573 v = "xmpp@" .. arg[1]; | |
574 elseif k == "countryName" then | |
575 local tld = arg[1]:match"%.([a-z]+)$"; | |
576 if tld and #tld == 2 and tld ~= "uk" then | |
577 v = tld:upper(); | |
578 end | |
579 end | |
580 nv = show_prompt(("%s (%s):"):format(k, nv or v)); | |
581 nv = (not nv or nv == "") and v or nv; | |
582 if nv:find"[\192-\252][\128-\191]+" then | |
583 conf.req.string_mask = "utf8only" | |
584 end | |
585 conf.distinguished_name[k] = nv ~= "." and nv or nil; | |
586 end | |
587 end | |
588 end | |
589 local conf_file, err = io.open(conf_filename, "w"); | |
590 if not conf_file then | |
591 show_warning("Could not open OpenSSL config file for writing"); | |
592 show_warning(err); | |
593 os.exit(1); | |
594 end | |
595 conf_file:write(conf:serialize()); | |
596 conf_file:close(); | |
597 print(""); | |
598 show_message("Config written to " .. conf_filename); | |
599 return nil, conf_filename; | |
600 else | |
601 show_usage("cert config HOSTNAME [HOSTNAME+]", "Builds a certificate config file covering the supplied hostname(s)") | |
602 end | |
603 end | |
604 | |
605 function cert_commands.key(arg) | |
606 if #arg >= 1 and arg[1] ~= "--help" then | |
607 local key_filename = cert_basedir .. "/" .. arg[1] .. ".key"; | |
608 if use_existing(key_filename) then | |
609 return nil, key_filename; | |
610 end | |
611 os.remove(key_filename); -- This file, if it exists is unlikely to have write permissions | |
612 local key_size = tonumber(arg[2] or show_prompt("Choose key size (2048):") or 2048); | |
613 local old_umask = pposix.umask("0377"); | |
614 if openssl.genrsa{out=key_filename, key_size} then | |
615 os.execute(("chmod 400 '%s'"):format(key_filename)); | |
616 show_message("Key written to ".. key_filename); | |
617 pposix.umask(old_umask); | |
618 return nil, key_filename; | |
619 end | |
620 show_message("There was a problem, see OpenSSL output"); | |
621 else | |
622 show_usage("cert key HOSTNAME <bits>", "Generates a RSA key named HOSTNAME.key\n " | |
623 .."Prompts for a key size if none given") | |
624 end | |
625 end | |
626 | |
627 function cert_commands.request(arg) | |
628 if #arg >= 1 and arg[1] ~= "--help" then | |
629 local req_filename = cert_basedir .. "/" .. arg[1] .. ".req"; | |
630 if use_existing(req_filename) then | |
631 return nil, req_filename; | |
632 end | |
633 local _, key_filename = cert_commands.key({arg[1]}); | |
634 local _, conf_filename = cert_commands.config(arg); | |
635 if openssl.req{new=true, key=key_filename, utf8=true, sha256=true, config=conf_filename, out=req_filename} then | |
636 show_message("Certificate request written to ".. req_filename); | |
637 else | |
638 show_message("There was a problem, see OpenSSL output"); | |
639 end | |
640 else | |
641 show_usage("cert request HOSTNAME [HOSTNAME+]", "Generates a certificate request for the supplied hostname(s)") | |
642 end | |
643 end | |
644 | |
645 function cert_commands.generate(arg) | |
646 if #arg >= 1 and arg[1] ~= "--help" then | |
647 local cert_filename = cert_basedir .. "/" .. arg[1] .. ".crt"; | |
648 if use_existing(cert_filename) then | |
649 return nil, cert_filename; | |
650 end | |
651 local _, key_filename = cert_commands.key({arg[1]}); | |
652 local _, conf_filename = cert_commands.config(arg); | |
653 if key_filename and conf_filename and cert_filename | |
654 and openssl.req{new=true, x509=true, nodes=true, key=key_filename, | |
655 days=365, sha256=true, utf8=true, config=conf_filename, out=cert_filename} then | |
656 show_message("Certificate written to ".. cert_filename); | |
657 print(); | |
658 else | |
659 show_message("There was a problem, see OpenSSL output"); | |
660 end | |
661 else | |
662 show_usage("cert generate HOSTNAME [HOSTNAME+]", "Generates a self-signed certificate for the current hostname(s)") | |
663 end | |
664 end | |
665 | |
666 local function sh_esc(s) | |
667 return "'" .. s:gsub("'", "'\\''") .. "'"; | |
668 end | |
669 | |
670 local function copy(from, to, umask, owner, group) | |
671 local old_umask = umask and pposix.umask(umask); | |
672 local attrs = lfs.attributes(to); | |
673 if attrs then -- Move old file out of the way | |
674 local backup = to..".bkp~"..os.date("%FT%T", attrs.change); | |
675 os.rename(to, backup); | |
676 end | |
677 -- FIXME friendlier error handling, maybe move above backup back? | |
678 local input = assert(io.open(from)); | |
679 local output = assert(io.open(to, "w")); | |
680 local data = input:read(2^11); | |
681 while data and output:write(data) do | |
682 data = input:read(2^11); | |
683 end | |
684 assert(input:close()); | |
685 assert(output:close()); | |
686 if not prosody.installed then | |
687 -- FIXME this is possibly specific to GNU chown | |
688 os.execute(("chown -c --reference=%s %s"):format(sh_esc(cert_basedir), sh_esc(to))); | |
689 elseif owner and group then | |
690 local ok = os.execute(("chown %s:%s %s"):format(sh_esc(owner), sh_esc(group), sh_esc(to))); | |
691 assert(ok == true or ok == 0, "Failed to change ownership of "..to); | |
692 end | |
693 if old_umask then pposix.umask(old_umask); end | |
694 return true; | |
695 end | |
696 | |
697 function cert_commands.import(arg) | |
698 local hostnames = {}; | |
699 -- Move hostname arguments out of arg, the rest should be a list of paths | |
700 while arg[1] and prosody.hosts[ arg[1] ] do | |
701 table.insert(hostnames, table.remove(arg, 1)); | |
702 end | |
703 if hostnames[1] == nil then | |
704 local domains = os.getenv"RENEWED_DOMAINS"; -- Set if invoked via certbot | |
705 if domains then | |
706 for host in domains:gmatch("%S+") do | |
707 table.insert(hostnames, host); | |
708 end | |
709 else | |
710 for host in pairs(prosody.hosts) do | |
711 if host ~= "*" and configmanager.get(host, "enabled") ~= false then | |
712 table.insert(hostnames, host); | |
713 end | |
714 end | |
715 end | |
716 end | |
717 if not arg[1] or arg[1] == "--help" then -- Probably forgot the path | |
718 show_usage("cert import [HOSTNAME+] /path/to/certs [/other/paths/]+", | |
719 "Copies certificates to "..cert_basedir); | |
720 return 1; | |
721 end | |
722 local owner, group; | |
723 if pposix.getuid() == 0 then -- We need root to change ownership | |
724 owner = configmanager.get("*", "prosody_user") or "prosody"; | |
725 group = configmanager.get("*", "prosody_group") or owner; | |
726 end | |
727 local cm = require "core.certmanager"; | |
728 local imported = {}; | |
729 for _, host in ipairs(hostnames) do | |
730 for _, dir in ipairs(arg) do | |
731 local paths = cm.find_cert(dir, host); | |
732 if paths then | |
733 copy(paths.certificate, cert_basedir .. "/" .. host .. ".crt", nil, owner, group); | |
734 copy(paths.key, cert_basedir .. "/" .. host .. ".key", "0377", owner, group); | |
735 table.insert(imported, host); | |
736 else | |
737 -- TODO Say where we looked | |
738 show_warning("No certificate for host "..host.." found :("); | |
739 end | |
740 -- TODO Additional checks | |
741 -- Certificate names matches the hostname | |
742 -- Private key matches public key in certificate | |
743 end | |
744 end | |
745 if imported[1] then | |
746 show_message("Imported certificate and key for hosts "..table.concat(imported, ", ")); | |
747 local ok, err = prosodyctl.reload(); | |
748 if not ok and err ~= "not-running" then | |
749 show_message(error_messages[err]); | |
750 end | |
751 else | |
752 show_warning("No certificates imported :("); | |
753 return 1; | |
754 end | |
755 end | |
756 | |
757 function commands.cert(arg) | |
758 if #arg >= 1 and arg[1] ~= "--help" then | |
759 openssl = require "util.openssl"; | |
760 lfs = require "lfs"; | |
761 local cert_dir_attrs = lfs.attributes(cert_basedir); | |
762 if not cert_dir_attrs then | |
763 show_warning("The directory "..cert_basedir.." does not exist"); | |
764 return 1; -- TODO Should we create it? | |
765 end | |
766 local uid = pposix.getuid(); | |
767 if uid ~= 0 and uid ~= cert_dir_attrs.uid then | |
768 show_warning("The directory "..cert_basedir.." is not owned by the current user, won't be able to write files to it"); | |
769 return 1; | |
770 elseif not cert_dir_attrs.permissions then -- COMPAT with LuaFilesystem < 1.6.2 (hey CentOS!) | |
771 show_message("Unable to check permissions on "..cert_basedir.." (LuaFilesystem 1.6.2+ required)"); | |
772 show_message("Please confirm that Prosody (and only Prosody) can write to this directory)"); | |
773 elseif cert_dir_attrs.permissions:match("^%.w..%-..%-.$") then | |
774 show_warning("The directory "..cert_basedir.." not only writable by its owner"); | |
775 return 1; | |
776 end | |
777 local subcmd = table.remove(arg, 1); | |
778 if type(cert_commands[subcmd]) == "function" then | |
779 if subcmd ~= "import" then -- hostnames are optional for import | |
780 if not arg[1] then | |
781 show_message"You need to supply at least one hostname" | |
782 arg = { "--help" }; | |
783 end | |
784 if arg[1] ~= "--help" and not prosody.hosts[arg[1]] then | |
785 show_message(error_messages["no-such-host"]); | |
786 return 1; | |
787 end | |
788 end | |
789 return cert_commands[subcmd](arg); | |
790 elseif subcmd == "check" then | |
791 return commands.check({"certs"}); | |
792 end | |
793 end | |
794 show_usage("cert config|request|generate|key|import", "Helpers for generating X.509 certificates and keys.") | |
795 for _, cmd in pairs(cert_commands) do | |
796 print() | |
797 cmd{ "--help" } | |
798 end | |
799 end | |
800 | |
801 function commands.check(arg) | |
802 if arg[1] == "--help" then | |
803 show_usage([[check]], [[Perform basic checks on your Prosody installation]]); | |
804 return 1; | |
805 end | |
806 local what = table.remove(arg, 1); | |
807 local set = require "util.set"; | |
808 local it = require "util.iterators"; | |
809 local ok = true; | |
810 local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end | |
811 local function enabled_hosts() return it.filter(disabled_hosts, pairs(configmanager.getconfig())); end | |
812 if not (what == nil or what == "disabled" or what == "config" or what == "dns" or what == "certs") then | |
813 show_warning("Don't know how to check '%s'. Try one of 'config', 'dns', 'certs' or 'disabled'.", what); | |
814 return 1; | |
815 end | |
816 if not what or what == "disabled" then | |
817 local disabled_hosts_set = set.new(); | |
818 for host, host_options in it.filter("*", pairs(configmanager.getconfig())) do | |
819 if host_options.enabled == false then | |
820 disabled_hosts_set:add(host); | |
821 end | |
822 end | |
823 if not disabled_hosts_set:empty() then | |
824 local msg = "Checks will be skipped for these disabled hosts: %s"; | |
825 if what then msg = "These hosts are disabled: %s"; end | |
826 show_warning(msg, tostring(disabled_hosts_set)); | |
827 if what then return 0; end | |
828 print"" | |
829 end | |
830 end | |
831 if not what or what == "config" then | |
832 print("Checking config..."); | |
833 local deprecated = set.new({ | |
834 "bosh_ports", "disallow_s2s", "no_daemonize", "anonymous_login", "require_encryption", | |
835 "vcard_compatibility", | |
836 }); | |
837 local known_global_options = set.new({ | |
838 "pidfile", "log", "plugin_paths", "prosody_user", "prosody_group", "daemonize", | |
839 "umask", "prosodyctl_timeout", "use_ipv6", "use_libevent", "network_settings", | |
840 "network_backend", "http_default_host", | |
841 "statistics_interval", "statistics", "statistics_config", | |
842 }); | |
843 local config = configmanager.getconfig(); | |
844 -- Check that we have any global options (caused by putting a host at the top) | |
845 if it.count(it.filter("log", pairs(config["*"]))) == 0 then | |
846 ok = false; | |
847 print(""); | |
848 print(" No global options defined. Perhaps you have put a host definition at the top") | |
849 print(" of the config file? They should be at the bottom, see https://prosody.im/doc/configure#overview"); | |
850 end | |
851 if it.count(enabled_hosts()) == 0 then | |
852 ok = false; | |
853 print(""); | |
854 if it.count(it.filter("*", pairs(config))) == 0 then | |
855 print(" No hosts are defined, please add at least one VirtualHost section") | |
856 elseif config["*"]["enabled"] == false then | |
857 print(" No hosts are enabled. Remove enabled = false from the global section or put enabled = true under at least one VirtualHost section") | |
858 else | |
859 print(" All hosts are disabled. Remove enabled = false from at least one VirtualHost section") | |
860 end | |
861 end | |
862 if not config["*"].modules_enabled then | |
863 print(" No global modules_enabled is set?"); | |
864 local suggested_global_modules; | |
865 for host, options in enabled_hosts() do --luacheck: ignore 213/host | |
866 if not options.component_module and options.modules_enabled then | |
867 suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled)); | |
868 end | |
869 end | |
870 if suggested_global_modules and not suggested_global_modules:empty() then | |
871 print(" Consider moving these modules into modules_enabled in the global section:") | |
872 print(" "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end)); | |
873 end | |
874 print(); | |
875 end | |
876 | |
877 do -- Check for modules enabled both normally and as components | |
878 local modules = set.new(config["*"]["modules_enabled"]); | |
879 for host, options in enabled_hosts() do | |
880 local component_module = options.component_module; | |
881 if component_module and modules:contains(component_module) then | |
882 print((" mod_%s is enabled both in modules_enabled and as Component %q %q"):format(component_module, host, component_module)); | |
883 print(" This means the service is enabled on all VirtualHosts as well as the Component."); | |
884 print(" Are you sure this what you want? It may cause unexpected behaviour."); | |
885 end | |
886 end | |
887 end | |
888 | |
889 -- Check for global options under hosts | |
890 local global_options = set.new(it.to_array(it.keys(config["*"]))); | |
891 local deprecated_global_options = set.intersection(global_options, deprecated); | |
892 if not deprecated_global_options:empty() then | |
893 print(""); | |
894 print(" You have some deprecated options in the global section:"); | |
895 print(" "..tostring(deprecated_global_options)) | |
896 ok = false; | |
897 end | |
898 for host, options in it.filter(function (h) return h ~= "*" end, pairs(configmanager.getconfig())) do | |
899 local host_options = set.new(it.to_array(it.keys(options))); | |
900 local misplaced_options = set.intersection(host_options, known_global_options); | |
901 for name in pairs(options) do | |
902 if name:match("^interfaces?") | |
903 or name:match("_ports?$") or name:match("_interfaces?$") | |
904 or (name:match("_ssl$") and not name:match("^[cs]2s_ssl$")) then | |
905 misplaced_options:add(name); | |
906 end | |
907 end | |
908 if not misplaced_options:empty() then | |
909 ok = false; | |
910 print(""); | |
911 local n = it.count(misplaced_options); | |
912 print(" You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be"); | |
913 print(" in the global section of the config file, above any VirtualHost or Component definitions,") | |
914 print(" see https://prosody.im/doc/configure#overview for more information.") | |
915 print(""); | |
916 print(" You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", ")); | |
917 end | |
918 end | |
919 for host, options in enabled_hosts() do | |
920 local host_options = set.new(it.to_array(it.keys(options))); | |
921 local subdomain = host:match("^[^.]+"); | |
922 if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp" | |
923 or subdomain == "chat" or subdomain == "im") then | |
924 print(""); | |
925 print(" Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to"); | |
926 print(" "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host.."."); | |
927 print(" For more information see: https://prosody.im/doc/dns"); | |
928 end | |
929 end | |
930 local all_modules = set.new(config["*"].modules_enabled); | |
931 local all_options = set.new(it.to_array(it.keys(config["*"]))); | |
932 for host in enabled_hosts() do | |
933 all_options:include(set.new(it.to_array(it.keys(config[host])))); | |
934 all_modules:include(set.new(config[host].modules_enabled)); | |
935 end | |
936 for mod in all_modules do | |
937 if mod:match("^mod_") then | |
938 print(""); | |
939 print(" Modules in modules_enabled should not have the 'mod_' prefix included."); | |
940 print(" Change '"..mod.."' to '"..mod:match("^mod_(.*)").."'."); | |
941 elseif mod:match("^auth_") then | |
942 print(""); | |
943 print(" Authentication modules should not be added to modules_enabled,"); | |
944 print(" but be specified in the 'authentication' option."); | |
945 print(" Remove '"..mod.."' from modules_enabled and instead add"); | |
946 print(" authentication = '"..mod:match("^auth_(.*)").."'"); | |
947 print(" For more information see https://prosody.im/doc/authentication"); | |
948 elseif mod:match("^storage_") then | |
949 print(""); | |
950 print(" storage modules should not be added to modules_enabled,"); | |
951 print(" but be specified in the 'storage' option."); | |
952 print(" Remove '"..mod.."' from modules_enabled and instead add"); | |
953 print(" storage = '"..mod:match("^storage_(.*)").."'"); | |
954 print(" For more information see https://prosody.im/doc/storage"); | |
955 end | |
956 end | |
957 if all_modules:contains("vcard") and all_modules:contains("vcard_legacy") then | |
958 print(""); | |
959 print(" Both mod_vcard_legacy and mod_vcard are enabled but they conflict"); | |
960 print(" with each other. Remove one."); | |
961 end | |
962 if all_modules:contains("pep") and all_modules:contains("pep_simple") then | |
963 print(""); | |
964 print(" Both mod_pep_simple and mod_pep are enabled but they conflict"); | |
965 print(" with each other. Remove one."); | |
966 end | |
967 for host, host_config in pairs(config) do --luacheck: ignore 213/host | |
968 if type(rawget(host_config, "storage")) == "string" and rawget(host_config, "default_storage") then | |
969 print(""); | |
970 print(" The 'default_storage' option is not needed if 'storage' is set to a string."); | |
971 break; | |
972 end | |
973 end | |
974 local require_encryption = set.intersection(all_options, set.new({ | |
975 "require_encryption", "c2s_require_encryption", "s2s_require_encryption" | |
976 })):empty(); | |
977 local ssl = dependencies.softreq"ssl"; | |
978 if not ssl then | |
979 if not require_encryption then | |
980 print(""); | |
981 print(" You require encryption but LuaSec is not available."); | |
982 print(" Connections will fail."); | |
983 ok = false; | |
984 end | |
985 elseif not ssl.loadcertificate then | |
986 if all_options:contains("s2s_secure_auth") then | |
987 print(""); | |
988 print(" You have set s2s_secure_auth but your version of LuaSec does "); | |
989 print(" not support certificate validation, so all s2s connections will"); | |
990 print(" fail."); | |
991 ok = false; | |
992 elseif all_options:contains("s2s_secure_domains") then | |
993 local secure_domains = set.new(); | |
994 for host in enabled_hosts() do | |
995 if config[host].s2s_secure_auth == true then | |
996 secure_domains:add("*"); | |
997 else | |
998 secure_domains:include(set.new(config[host].s2s_secure_domains)); | |
999 end | |
1000 end | |
1001 if not secure_domains:empty() then | |
1002 print(""); | |
1003 print(" You have set s2s_secure_domains but your version of LuaSec does "); | |
1004 print(" not support certificate validation, so s2s connections to/from "); | |
1005 print(" these domains will fail."); | |
1006 ok = false; | |
1007 end | |
1008 end | |
1009 elseif require_encryption and not all_modules:contains("tls") then | |
1010 print(""); | |
1011 print(" You require encryption but mod_tls is not enabled."); | |
1012 print(" Connections will fail."); | |
1013 ok = false; | |
1014 end | |
1015 | |
1016 print("Done.\n"); | |
1017 end | |
1018 if not what or what == "dns" then | |
1019 local dns = require "net.dns"; | |
1020 local idna = require "util.encodings".idna; | |
1021 local ip = require "util.ip"; | |
1022 local c2s_ports = set.new(configmanager.get("*", "c2s_ports") or {5222}); | |
1023 local s2s_ports = set.new(configmanager.get("*", "s2s_ports") or {5269}); | |
1024 | |
1025 local c2s_srv_required, s2s_srv_required; | |
1026 if not c2s_ports:contains(5222) then | |
1027 c2s_srv_required = true; | |
1028 end | |
1029 if not s2s_ports:contains(5269) then | |
1030 s2s_srv_required = true; | |
1031 end | |
1032 | |
1033 local problem_hosts = set.new(); | |
1034 | |
1035 local external_addresses, internal_addresses = set.new(), set.new(); | |
1036 | |
1037 local fqdn = socket.dns.tohostname(socket.dns.gethostname()); | |
1038 if fqdn then | |
1039 do | |
1040 local res = dns.lookup(idna.to_ascii(fqdn), "A"); | |
1041 if res then | |
1042 for _, record in ipairs(res) do | |
1043 external_addresses:add(record.a); | |
1044 end | |
1045 end | |
1046 end | |
1047 do | |
1048 local res = dns.lookup(idna.to_ascii(fqdn), "AAAA"); | |
1049 if res then | |
1050 for _, record in ipairs(res) do | |
1051 external_addresses:add(record.aaaa); | |
1052 end | |
1053 end | |
1054 end | |
1055 end | |
1056 | |
1057 local local_addresses = require"util.net".local_addresses() or {}; | |
1058 | |
1059 for addr in it.values(local_addresses) do | |
1060 if not ip.new_ip(addr).private then | |
1061 external_addresses:add(addr); | |
1062 else | |
1063 internal_addresses:add(addr); | |
1064 end | |
1065 end | |
1066 | |
1067 if external_addresses:empty() then | |
1068 print(""); | |
1069 print(" Failed to determine the external addresses of this server. Checks may be inaccurate."); | |
1070 c2s_srv_required, s2s_srv_required = true, true; | |
1071 end | |
1072 | |
1073 local v6_supported = not not socket.tcp6; | |
1074 | |
1075 for jid, host_options in enabled_hosts() do | |
1076 local all_targets_ok, some_targets_ok = true, false; | |
1077 local node, host = jid_split(jid); | |
1078 | |
1079 local modules, component_module = modulemanager.get_modules_for_host(host); | |
1080 if component_module then | |
1081 modules:add(component_module); | |
1082 end | |
1083 | |
1084 local is_component = not not host_options.component_module; | |
1085 print("Checking DNS for "..(is_component and "component" or "host").." "..jid.."..."); | |
1086 if node then | |
1087 print("Only the domain part ("..host..") is used in DNS.") | |
1088 end | |
1089 local target_hosts = set.new(); | |
1090 if modules:contains("c2s") then | |
1091 local res = dns.lookup("_xmpp-client._tcp."..idna.to_ascii(host)..".", "SRV"); | |
1092 if res then | |
1093 for _, record in ipairs(res) do | |
1094 target_hosts:add(record.srv.target); | |
1095 if not c2s_ports:contains(record.srv.port) then | |
1096 print(" SRV target "..record.srv.target.." contains unknown client port: "..record.srv.port); | |
1097 end | |
1098 end | |
1099 else | |
1100 if c2s_srv_required then | |
1101 print(" No _xmpp-client SRV record found for "..host..", but it looks like you need one."); | |
1102 all_targets_ok = false; | |
1103 else | |
1104 target_hosts:add(host); | |
1105 end | |
1106 end | |
1107 end | |
1108 if modules:contains("s2s") then | |
1109 local res = dns.lookup("_xmpp-server._tcp."..idna.to_ascii(host)..".", "SRV"); | |
1110 if res then | |
1111 for _, record in ipairs(res) do | |
1112 target_hosts:add(record.srv.target); | |
1113 if not s2s_ports:contains(record.srv.port) then | |
1114 print(" SRV target "..record.srv.target.." contains unknown server port: "..record.srv.port); | |
1115 end | |
1116 end | |
1117 else | |
1118 if s2s_srv_required then | |
1119 print(" No _xmpp-server SRV record found for "..host..", but it looks like you need one."); | |
1120 all_targets_ok = false; | |
1121 else | |
1122 target_hosts:add(host); | |
1123 end | |
1124 end | |
1125 end | |
1126 if target_hosts:empty() then | |
1127 target_hosts:add(host); | |
1128 end | |
1129 | |
1130 if target_hosts:contains("localhost") then | |
1131 print(" Target 'localhost' cannot be accessed from other servers"); | |
1132 target_hosts:remove("localhost"); | |
1133 end | |
1134 | |
1135 if modules:contains("proxy65") then | |
1136 local proxy65_target = configmanager.get(host, "proxy65_address") or host; | |
1137 if type(proxy65_target) == "string" then | |
1138 local A, AAAA = dns.lookup(idna.to_ascii(proxy65_target), "A"), dns.lookup(idna.to_ascii(proxy65_target), "AAAA"); | |
1139 local prob = {}; | |
1140 if not A then | |
1141 table.insert(prob, "A"); | |
1142 end | |
1143 if v6_supported and not AAAA then | |
1144 table.insert(prob, "AAAA"); | |
1145 end | |
1146 if #prob > 0 then | |
1147 print(" File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/") | |
1148 .." record. Create one or set 'proxy65_address' to the correct host/IP."); | |
1149 end | |
1150 else | |
1151 print(" proxy65_address for "..host.." should be set to a string, unable to perform DNS check"); | |
1152 end | |
1153 end | |
1154 | |
1155 for target_host in target_hosts do | |
1156 local host_ok_v4, host_ok_v6; | |
1157 do | |
1158 local res = dns.lookup(idna.to_ascii(target_host), "A"); | |
1159 if res then | |
1160 for _, record in ipairs(res) do | |
1161 if external_addresses:contains(record.a) then | |
1162 some_targets_ok = true; | |
1163 host_ok_v4 = true; | |
1164 elseif internal_addresses:contains(record.a) then | |
1165 host_ok_v4 = true; | |
1166 some_targets_ok = true; | |
1167 print(" "..target_host.." A record points to internal address, external connections might fail"); | |
1168 else | |
1169 print(" "..target_host.." A record points to unknown address "..record.a); | |
1170 all_targets_ok = false; | |
1171 end | |
1172 end | |
1173 end | |
1174 end | |
1175 do | |
1176 local res = dns.lookup(idna.to_ascii(target_host), "AAAA"); | |
1177 if res then | |
1178 for _, record in ipairs(res) do | |
1179 if external_addresses:contains(record.aaaa) then | |
1180 some_targets_ok = true; | |
1181 host_ok_v6 = true; | |
1182 elseif internal_addresses:contains(record.aaaa) then | |
1183 host_ok_v6 = true; | |
1184 some_targets_ok = true; | |
1185 print(" "..target_host.." AAAA record points to internal address, external connections might fail"); | |
1186 else | |
1187 print(" "..target_host.." AAAA record points to unknown address "..record.aaaa); | |
1188 all_targets_ok = false; | |
1189 end | |
1190 end | |
1191 end | |
1192 end | |
1193 | |
1194 local bad_protos = {} | |
1195 if not host_ok_v4 then | |
1196 table.insert(bad_protos, "IPv4"); | |
1197 end | |
1198 if not host_ok_v6 then | |
1199 table.insert(bad_protos, "IPv6"); | |
1200 end | |
1201 if #bad_protos > 0 then | |
1202 print(" Host "..target_host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")"); | |
1203 end | |
1204 if host_ok_v6 and not v6_supported then | |
1205 print(" Host "..target_host.." has AAAA records, but your version of LuaSocket does not support IPv6."); | |
1206 print(" Please see https://prosody.im/doc/ipv6 for more information."); | |
1207 end | |
1208 end | |
1209 if not all_targets_ok then | |
1210 print(" "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server."); | |
1211 if is_component then | |
1212 print(" DNS records are necessary if you want users on other servers to access this component."); | |
1213 end | |
1214 problem_hosts:add(host); | |
1215 end | |
1216 print(""); | |
1217 end | |
1218 if not problem_hosts:empty() then | |
1219 print(""); | |
1220 print("For more information about DNS configuration please see https://prosody.im/doc/dns"); | |
1221 print(""); | |
1222 ok = false; | |
1223 end | |
1224 end | |
1225 if not what or what == "certs" then | |
1226 local cert_ok; | |
1227 print"Checking certificates..." | |
1228 local x509_verify_identity = require"util.x509".verify_identity; | |
1229 local create_context = require "core.certmanager".create_context; | |
1230 local ssl = dependencies.softreq"ssl"; | |
1231 -- local datetime_parse = require"util.datetime".parse_x509; | |
1232 local load_cert = ssl and ssl.loadcertificate; | |
1233 -- or ssl.cert_from_pem | |
1234 if not ssl then | |
1235 print("LuaSec not available, can't perform certificate checks") | |
1236 if what == "certs" then cert_ok = false end | |
1237 elseif not load_cert then | |
1238 print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking"); | |
1239 cert_ok = false | |
1240 else | |
1241 local function skip_bare_jid_hosts(host) | |
1242 if jid_split(host) then | |
1243 -- See issue #779 | |
1244 return false; | |
1245 end | |
1246 return true; | |
1247 end | |
1248 for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do | |
1249 print("Checking certificate for "..host); | |
1250 -- First, let's find out what certificate this host uses. | |
1251 local host_ssl_config = configmanager.rawget(host, "ssl") | |
1252 or configmanager.rawget(host:match("%.(.*)"), "ssl"); | |
1253 local global_ssl_config = configmanager.rawget("*", "ssl"); | |
1254 local ok, err, ssl_config = create_context(host, "server", host_ssl_config, global_ssl_config); | |
1255 if not ok then | |
1256 print(" Error: "..err); | |
1257 cert_ok = false | |
1258 elseif not ssl_config.certificate then | |
1259 print(" No 'certificate' found for "..host) | |
1260 cert_ok = false | |
1261 elseif not ssl_config.key then | |
1262 print(" No 'key' found for "..host) | |
1263 cert_ok = false | |
1264 else | |
1265 local key, err = io.open(ssl_config.key); -- Permissions check only | |
1266 if not key then | |
1267 print(" Could not open "..ssl_config.key..": "..err); | |
1268 cert_ok = false | |
1269 else | |
1270 key:close(); | |
1271 end | |
1272 local cert_fh, err = io.open(ssl_config.certificate); -- Load the file. | |
1273 if not cert_fh then | |
1274 print(" Could not open "..ssl_config.certificate..": "..err); | |
1275 cert_ok = false | |
1276 else | |
1277 print(" Certificate: "..ssl_config.certificate) | |
1278 local cert = load_cert(cert_fh:read"*a"); cert_fh:close(); | |
1279 if not cert:validat(os.time()) then | |
1280 print(" Certificate has expired.") | |
1281 cert_ok = false | |
1282 elseif not cert:validat(os.time() + 86400) then | |
1283 print(" Certificate expires within one day.") | |
1284 cert_ok = false | |
1285 elseif not cert:validat(os.time() + 86400*7) then | |
1286 print(" Certificate expires within one week.") | |
1287 elseif not cert:validat(os.time() + 86400*31) then | |
1288 print(" Certificate expires within one month.") | |
1289 end | |
1290 if configmanager.get(host, "component_module") == nil | |
1291 and not x509_verify_identity(host, "_xmpp-client", cert) then | |
1292 print(" Not valid for client connections to "..host..".") | |
1293 cert_ok = false | |
1294 end | |
1295 if (not (configmanager.get(host, "anonymous_login") | |
1296 or configmanager.get(host, "authentication") == "anonymous")) | |
1297 and not x509_verify_identity(host, "_xmpp-server", cert) then | |
1298 print(" Not valid for server-to-server connections to "..host..".") | |
1299 cert_ok = false | |
1300 end | |
1301 end | |
1302 end | |
1303 end | |
1304 end | |
1305 if cert_ok == false then | |
1306 print("") | |
1307 print("For more information about certificates please see https://prosody.im/doc/certificates"); | |
1308 ok = false | |
1309 end | |
1310 print("") | |
1311 end | |
1312 if not ok then | |
1313 print("Problems found, see above."); | |
1314 else | |
1315 print("All checks passed, congratulations!"); | |
1316 end | |
1317 return ok and 0 or 2; | |
1318 end | 563 end |
1319 | 564 |
1320 --------------------- | 565 --------------------- |
1321 | 566 |
1322 local async = require "util.async"; | 567 local async = require "util.async"; |
1338 show_message("Failed to load module '"..module_name.."': "..err); | 583 show_message("Failed to load module '"..module_name.."': "..err); |
1339 os.exit(1); | 584 os.exit(1); |
1340 end | 585 end |
1341 end | 586 end |
1342 | 587 |
1343 table.remove(arg, 1); | |
1344 | |
1345 local module = modulemanager.get_module("*", module_name); | 588 local module = modulemanager.get_module("*", module_name); |
1346 if not module then | 589 if not module then |
1347 show_message("Failed to load module '"..module_name.."': Unknown error"); | 590 show_message("Failed to load module '"..module_name.."': Unknown error"); |
1348 os.exit(1); | 591 os.exit(1); |
1349 end | 592 end |
1365 show_message("Failed to execute command: "..error_messages[ret]); | 608 show_message("Failed to execute command: "..error_messages[ret]); |
1366 os.exit(1); -- :( | 609 os.exit(1); -- :( |
1367 end | 610 end |
1368 end | 611 end |
1369 | 612 |
613 if command and not commands[command] then | |
614 local ok, command_module = pcall(require, "util.prosodyctl."..command); | |
615 if ok and command_module[command] then | |
616 commands[command] = command_module[command]; | |
617 end | |
618 end | |
619 | |
1370 if not commands[command] then -- Show help for all commands | 620 if not commands[command] then -- Show help for all commands |
1371 function show_usage(usage, desc) | 621 function show_usage(usage, desc) |
1372 print(" "..usage); | 622 print(" "..usage); |
1373 print(" "..desc); | 623 print(" "..desc); |
1374 end | 624 end |
1378 print("Usage: "..arg[0].." COMMAND [OPTIONS]"); | 628 print("Usage: "..arg[0].." COMMAND [OPTIONS]"); |
1379 print(""); | 629 print(""); |
1380 print("Where COMMAND may be one of:\n"); | 630 print("Where COMMAND may be one of:\n"); |
1381 | 631 |
1382 local hidden_commands = require "util.set".new{ "register", "unregister", "addplugin" }; | 632 local hidden_commands = require "util.set".new{ "register", "unregister", "addplugin" }; |
1383 local commands_order = { "adduser", "passwd", "deluser", "start", "stop", "restart", "reload", "about" }; | 633 local commands_order = { "install", "remove", "list", "adduser", "passwd", "deluser", "start", "stop", "restart", "reload", |
634 "about" }; | |
1384 | 635 |
1385 local done = {}; | 636 local done = {}; |
1386 | 637 |
1387 for _, command_name in ipairs(commands_order) do | 638 for _, command_name in ipairs(commands_order) do |
1388 local command_func = commands[command_name]; | 639 local command_func = commands[command_name]; |
1403 | 654 |
1404 | 655 |
1405 os.exit(0); | 656 os.exit(0); |
1406 end | 657 end |
1407 | 658 |
1408 os.exit(commands[command]({ select(2, unpack(arg)) })); | 659 os.exit(commands[command](arg)); |
1409 end, watchers); | 660 end, watchers); |
1410 | 661 |
1411 command_runner:run(true); | 662 command_runner:run(true); |