aboutsummaryrefslogtreecommitdiffstats
path: root/util
diff options
context:
space:
mode:
Diffstat (limited to 'util')
-rw-r--r--util/argparse.lua77
-rw-r--r--util/prosodyctl/check.lua312
-rw-r--r--util/prosodyctl/shell.lua12
-rw-r--r--util/sql.lua2
-rw-r--r--util/x509.lua51
5 files changed, 364 insertions, 90 deletions
diff --git a/util/argparse.lua b/util/argparse.lua
index 7a55cb0b..3a7d1ba2 100644
--- a/util/argparse.lua
+++ b/util/argparse.lua
@@ -2,6 +2,9 @@ local function parse(arg, config)
local short_params = config and config.short_params or {};
local value_params = config and config.value_params or {};
local array_params = config and config.array_params or {};
+ local kv_params = config and config.kv_params or {};
+ local strict = config and config.strict;
+ local stop_on_positional = not config or config.stop_on_positional ~= false;
local parsed_opts = {};
@@ -15,51 +18,65 @@ local function parse(arg, config)
end
local prefix = raw_param:match("^%-%-?");
- if not prefix then
+ if not prefix and stop_on_positional then
break;
elseif prefix == "--" and raw_param == "--" then
table.remove(arg, 1);
break;
end
- local param = table.remove(arg, 1):sub(#prefix+1);
- if #param == 1 and short_params then
- param = short_params[param];
- end
- if not param then
- return nil, "param-not-found", raw_param;
- end
+ if prefix then
+ local param = table.remove(arg, 1):sub(#prefix+1);
+ if #param == 1 and short_params then
+ param = short_params[param];
+ end
- local param_k, param_v;
- if value_params[param] or array_params[param] then
- param_k, param_v = param, table.remove(arg, 1);
- if not param_v then
- return nil, "missing-value", raw_param;
+ if not param then
+ return nil, "param-not-found", raw_param;
end
- else
- param_k, param_v = param:match("^([^=]+)=(.+)$");
- if not param_k then
- if param:match("^no%-") then
- param_k, param_v = param:sub(4), false;
- else
- param_k, param_v = param, true;
+
+ local uparam = param:match("^[^=]*"):gsub("%-", "_");
+
+ local param_k, param_v;
+ if value_params[uparam] or array_params[uparam] then
+ param_k, param_v = uparam, table.remove(arg, 1);
+ if not param_v then
+ return nil, "missing-value", raw_param;
+ end
+ else
+ param_k, param_v = param:match("^([^=]+)=(.+)$");
+ if not param_k then
+ if param:match("^no%-") then
+ param_k, param_v = param:sub(4), false;
+ else
+ param_k, param_v = param, true;
+ end
+ end
+ param_k = param_k:gsub("%-", "_");
+ if strict and not kv_params[param_k] then
+ return nil, "param-not-found", raw_param;
end
end
- param_k = param_k:gsub("%-", "_");
- end
- if array_params[param] then
- if parsed_opts[param_k] then
- table.insert(parsed_opts[param_k], param_v);
+ if array_params[uparam] then
+ if parsed_opts[param_k] then
+ table.insert(parsed_opts[param_k], param_v);
+ else
+ parsed_opts[param_k] = { param_v };
+ end
else
- parsed_opts[param_k] = { param_v };
+ parsed_opts[param_k] = param_v;
end
- else
- parsed_opts[param_k] = param_v;
+ elseif not stop_on_positional then
+ table.insert(parsed_opts, table.remove(arg, 1));
end
end
- for i = 1, #arg do
- parsed_opts[i] = arg[i];
+
+ if stop_on_positional then
+ for i = 1, #arg do
+ parsed_opts[i] = arg[i];
+ end
end
+
return parsed_opts;
end
diff --git a/util/prosodyctl/check.lua b/util/prosodyctl/check.lua
index ac8cc9c1..4ac7af9e 100644
--- a/util/prosodyctl/check.lua
+++ b/util/prosodyctl/check.lua
@@ -325,7 +325,12 @@ local function check(arg)
local ok = true;
local function contains_match(hayset, needle) for member in hayset do if member:find(needle) then return true end end end
local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end
- local function enabled_hosts() return it.filter(disabled_hosts, pairs(configmanager.getconfig())); end
+ local function is_user_host(host, conf) return host ~= "*" and conf.component_module == nil; end
+ local function is_component_host(host, conf) return host ~= "*" and conf.component_module ~= nil; end
+ local function enabled_hosts() return it.filter(disabled_hosts, it.sorted_pairs(configmanager.getconfig())); end
+ local function enabled_user_hosts() return it.filter(is_user_host, it.sorted_pairs(configmanager.getconfig())); end
+ local function enabled_components() return it.filter(is_component_host, it.sorted_pairs(configmanager.getconfig())); end
+
local checks = {};
function checks.disabled()
local disabled_hosts_set = set.new();
@@ -632,6 +637,12 @@ local function check(arg)
print(" Both mod_pep_simple and mod_pep are enabled but they conflict");
print(" with each other. Remove one.");
end
+ if all_modules:contains("posix") then
+ print("");
+ print(" mod_posix is loaded in your configuration file, but it has");
+ print(" been deprecated. You can safely remove it.");
+ end
+
for host, host_config in pairs(config) do --luacheck: ignore 213/host
if type(rawget(host_config, "storage")) == "string" and rawget(host_config, "default_storage") then
print("");
@@ -790,12 +801,28 @@ local function check(arg)
if #invalid_hosts > 0 or #alabel_hosts > 0 then
print("");
- print("WARNING: Changing the name of a VirtualHost in Prosody's config file");
- print(" WILL NOT migrate any existing data (user accounts, etc.) to the new name.");
+ print(" WARNING: Changing the name of a VirtualHost in Prosody's config file");
+ print(" WILL NOT migrate any existing data (user accounts, etc.) to the new name.");
ok = false;
end
end
+ -- Check features
+ do
+ local missing_features = {};
+ for host in enabled_user_hosts() do
+ local all_features = checks.features(host, true);
+ if not all_features then
+ table.insert(missing_features, host);
+ end
+ end
+ if #missing_features > 0 then
+ print("");
+ print(" Some of your hosts may be missing features due to a lack of configuration.");
+ print(" For more details, use the 'prosodyctl check features' command.");
+ end
+ end
+
print("Done.\n");
end
function checks.dns()
@@ -901,7 +928,11 @@ local function check(arg)
local unknown_addresses = set.new();
- for jid in enabled_hosts() do
+ local function is_valid_domain(domain)
+ return idna.to_ascii(domain) ~= nil;
+ end
+
+ for jid in it.filter(is_valid_domain, enabled_hosts()) do
local all_targets_ok, some_targets_ok = true, false;
local node, host = jid_split(jid);
@@ -1444,6 +1475,279 @@ local function check(arg)
end
end
end
+
+ function checks.features(check_host, quiet)
+ if not quiet then
+ print("Feature report");
+ end
+
+ local common_subdomains = {
+ http_file_share = "share";
+ muc = "groups";
+ };
+
+ local recommended_component_modules = {
+ muc = { "muc_mam" };
+ };
+
+ local function print_feature_status(feature, host)
+ if quiet then return; end
+ print("", feature.ok and "OK" or "(!)", feature.name);
+ if not feature.ok then
+ if feature.lacking_modules then
+ table.sort(feature.lacking_modules);
+ print("", "", "Suggested modules: ");
+ for _, module in ipairs(feature.lacking_modules) do
+ print("", "", (" - %s: https://prosody.im/doc/modules/mod_%s"):format(module, module));
+ end
+ end
+ if feature.lacking_components then
+ table.sort(feature.lacking_components);
+ for _, component_module in ipairs(feature.lacking_components) do
+ local subdomain = common_subdomains[component_module];
+ local recommended_mods = recommended_component_modules[component_module];
+ if subdomain then
+ print("", "", "Suggested component:");
+ print("");
+ print("", "", "", ("-- Documentation: https://prosody.im/doc/modules/mod_%s"):format(component_module));
+ print("", "", "", ("Component %q %q"):format(subdomain.."."..host, component_module));
+ if recommended_mods then
+ print("", "", "", " modules_enabled = {");
+ table.sort(recommended_mods);
+ for _, mod in ipairs(recommended_mods) do
+ print("", "", "", (" %q;"):format(mod));
+ end
+ print("", "", "", " }");
+ end
+ else
+ print("", "", ("Suggested component: %s"):format(component_module));
+ end
+ end
+ print("");
+ print("", "", "If you have already configured any of these components, they may not be");
+ print("", "", "linked correctly to "..host..". For more info see https://prosody.im/doc/components");
+ end
+ if feature.lacking_component_modules then
+ table.sort(feature.lacking_component_modules, function (a, b)
+ return a.host < b.host;
+ end);
+ for _, problem in ipairs(feature.lacking_component_modules) do
+ local hostapi = api(problem.host);
+ local current_modules_enabled = hostapi:get_option_array("modules_enabled", {});
+ print("", "", ("Component %q is missing the following modules: %s"):format(problem.host, table.concat(problem.missing_mods)));
+ print("");
+ print("","", "Add the missing modules to your modules_enabled under the Component, like this:");
+ print("");
+ print("");
+ print("", "", "", ("-- Documentation: https://prosody.im/doc/modules/mod_%s"):format(problem.component_module));
+ print("", "", "", ("Component %q %q"):format(problem.host, problem.component_module));
+ print("", "", "", (" modules_enabled = {"));
+ for _, mod in ipairs(current_modules_enabled) do
+ print("", "", "", (" %q;"):format(mod));
+ end
+ for _, mod in ipairs(problem.missing_mods) do
+ print("", "", "", (" %q; -- Add this!"):format(mod));
+ end
+ print("", "", "", (" }"));
+ end
+ end
+ end
+ print("");
+ end
+
+ local all_ok = true;
+
+ local config = configmanager.getconfig();
+
+ local f, s, v;
+ if check_host then
+ f, s, v = it.values({ check_host });
+ else
+ f, s, v = enabled_user_hosts();
+ end
+
+ for host in f, s, v do
+ local modules_enabled = set.new(config["*"].modules_enabled);
+ modules_enabled:include(set.new(config[host].modules_enabled));
+
+ -- { [component_module] = { hostname1, hostname2, ... } }
+ local host_components = setmetatable({}, { __index = function (t, k) return rawset(t, k, {})[k]; end });
+
+ do
+ local hostapi = api(host);
+
+ -- Find implicitly linked components
+ for other_host in enabled_components() do
+ local parent_host = other_host:match("^[^.]+%.(.+)$");
+ if parent_host == host then
+ local component_module = configmanager.get(other_host, "component_module");
+ if component_module then
+ table.insert(host_components[component_module], other_host);
+ end
+ end
+ end
+
+ -- And components linked explicitly
+ for _, disco_item in ipairs(hostapi:get_option_array("disco_items", {})) do
+ local other_host = disco_item[1];
+ local component_module = configmanager.get(other_host, "component_module");
+ if component_module then
+ table.insert(host_components[component_module], other_host);
+ end
+ end
+ end
+
+ local current_feature;
+
+ local function check_module(suggested, alternate, ...)
+ if set.intersection(modules_enabled, set.new({suggested, alternate, ...})):empty() then
+ current_feature.lacking_modules = current_feature.lacking_modules or {};
+ table.insert(current_feature.lacking_modules, suggested);
+ end
+ end
+
+ local function check_component(suggested, alternate, ...)
+ local found;
+ for _, component_module in ipairs({ suggested, alternate, ... }) do
+ found = host_components[component_module][1];
+ if found then
+ local enabled_component_modules = api(found):get_option_inherited_set("modules_enabled");
+ local recommended_mods = recommended_component_modules[component_module];
+ if recommended_mods then
+ local missing_mods = {};
+ for _, mod in ipairs(recommended_mods) do
+ if not enabled_component_modules:contains(mod) then
+ table.insert(missing_mods, mod);
+ end
+ end
+ if #missing_mods > 0 then
+ if not current_feature.lacking_component_modules then
+ current_feature.lacking_component_modules = {};
+ end
+ table.insert(current_feature.lacking_component_modules, {
+ host = found;
+ component_module = component_module;
+ missing_mods = missing_mods;
+ });
+ end
+ end
+ end
+ end
+ if not found then
+ current_feature.lacking_components = current_feature.lacking_components or {};
+ table.insert(current_feature.lacking_components, suggested);
+ end
+ end
+
+ local features = {
+ {
+ name = "Basic functionality";
+ check = function ()
+ check_module("disco");
+ check_module("roster");
+ check_module("saslauth");
+ check_module("tls");
+ check_module("pep");
+ end;
+ };
+ {
+ name = "Multi-device sync";
+ check = function ()
+ check_module("carbons");
+ check_module("mam");
+ check_module("bookmarks");
+ end;
+ };
+ {
+ name = "Mobile optimizations";
+ check = function ()
+ check_module("smacks");
+ check_module("csi_simple", "csi_battery_saver");
+ end;
+ };
+ {
+ name = "Web connections";
+ check = function ()
+ check_module("bosh");
+ check_module("websocket");
+ end;
+ };
+ {
+ name = "User profiles";
+ check = function ()
+ check_module("vcard_legacy", "vcard");
+ end;
+ };
+ {
+ name = "Blocking";
+ check = function ()
+ check_module("blocklist");
+ end;
+ };
+ {
+ name = "Push notifications";
+ check = function ()
+ check_module("cloud_notify");
+ end;
+ };
+ {
+ name = "Audio/video calls";
+ check = function ()
+ check_module(
+ "turn_external",
+ "external_services",
+ "turncredentials",
+ "extdisco"
+ );
+ end;
+ };
+ {
+ name = "File sharing";
+ check = function ()
+ check_component("http_file_share", "http_upload", "http_upload_external");
+ end;
+ };
+ {
+ name = "Group chats";
+ check = function ()
+ check_component("muc");
+ end;
+ };
+ };
+
+ if not quiet then
+ print(host);
+ end
+
+ for _, feature in ipairs(features) do
+ current_feature = feature;
+ feature.check();
+ feature.ok = (
+ not feature.lacking_modules and
+ not feature.lacking_components and
+ not feature.lacking_component_modules
+ );
+ -- For improved presentation, we group the (ok) and (not ok) features
+ if feature.ok then
+ print_feature_status(feature, host);
+ end
+ end
+
+ for _, feature in ipairs(features) do
+ if not feature.ok then
+ all_ok = false;
+ print_feature_status(feature, host);
+ end
+ end
+
+ if not quiet then
+ print("");
+ end
+ end
+
+ return all_ok;
+ end
+
if what == nil or what == "all" then
local ret;
ret = checks.disabled();
diff --git a/util/prosodyctl/shell.lua b/util/prosodyctl/shell.lua
index a0fbb09c..d72cf294 100644
--- a/util/prosodyctl/shell.lua
+++ b/util/prosodyctl/shell.lua
@@ -87,17 +87,7 @@ local function start(arg) --luacheck: ignore 212/arg
if arg[1] then
if arg[2] then
- local fmt = { "%s"; ":%s("; ")" };
- for i = 3, #arg do
- if arg[i]:sub(1, 1) == ":" then
- table.insert(fmt, i, ")%s(");
- elseif i > 3 and fmt[i - 1]:match("%%q$") then
- table.insert(fmt, i, ", %q");
- else
- table.insert(fmt, i, "%q");
- end
- end
- arg[1] = string.format(table.concat(fmt), table.unpack(arg));
+ arg[1] = ("{"..string.rep("%q", #arg, ", ").."}"):format(table.unpack(arg, 1, #arg));
end
client.events.add_handler("connected", function()
diff --git a/util/sql.lua b/util/sql.lua
index 2f0ec493..06550455 100644
--- a/util/sql.lua
+++ b/util/sql.lua
@@ -84,7 +84,7 @@ function engine:connect()
dbh:autocommit(false); -- don't commit automatically
self.conn = dbh;
self.prepared = {};
- if params.password then
+ if params.driver == "SQLite3" and params.password then
local ok, err = self:execute(("PRAGMA key='%s'"):format(dbh:quote(params.password)));
if not ok then
return ok, err;
diff --git a/util/x509.lua b/util/x509.lua
index 9ecb5b60..6d856be0 100644
--- a/util/x509.lua
+++ b/util/x509.lua
@@ -11,7 +11,8 @@
-- IDN libraries complicate that.
--- [TLS-CERTS] - https://www.rfc-editor.org/rfc/rfc6125.html
+-- [TLS-CERTS] - https://www.rfc-editor.org/rfc/rfc6125.html -- Obsolete
+-- [TLS-IDENT] - https://www.rfc-editor.org/rfc/rfc9525.html
-- [XMPP-CORE] - https://www.rfc-editor.org/rfc/rfc6120.html
-- [SRV-ID] - https://www.rfc-editor.org/rfc/rfc4985.html
-- [IDNA] - https://www.rfc-editor.org/rfc/rfc5890.html
@@ -35,10 +36,8 @@ local oid_subjectaltname = "2.5.29.17"; -- [PKIX] 4.2.1.6
local oid_xmppaddr = "1.3.6.1.5.5.7.8.5"; -- [XMPP-CORE]
local oid_dnssrv = "1.3.6.1.5.5.7.8.7"; -- [SRV-ID]
--- Compare a hostname (possibly international) with asserted names
--- extracted from a certificate.
--- This function follows the rules laid out in
--- sections 6.4.1 and 6.4.2 of [TLS-CERTS]
+-- Compare a hostname (possibly international) with asserted names extracted from a certificate.
+-- This function follows the rules laid out in section 6.3 of [TLS-IDENT]
--
-- A wildcard ("*") all by itself is allowed only as the left-most label
local function compare_dnsname(host, asserted_names)
@@ -159,61 +158,25 @@ local function verify_identity(host, service, cert)
if ext[oid_subjectaltname] then
local sans = ext[oid_subjectaltname];
- -- Per [TLS-CERTS] 6.3, 6.4.4, "a client MUST NOT seek a match for a
- -- reference identifier if the presented identifiers include a DNS-ID
- -- SRV-ID, URI-ID, or any application-specific identifier types"
- local had_supported_altnames = false
-
if sans[oid_xmppaddr] then
- had_supported_altnames = true
if service == "_xmpp-client" or service == "_xmpp-server" then
if compare_xmppaddr(host, sans[oid_xmppaddr]) then return true end
end
end
if sans[oid_dnssrv] then
- had_supported_altnames = true
-- Only check srvNames if the caller specified a service
if service and compare_srvname(host, service, sans[oid_dnssrv]) then return true end
end
if sans["dNSName"] then
- had_supported_altnames = true
if compare_dnsname(host, sans["dNSName"]) then return true end
end
-
- -- We don't need URIs, but [TLS-CERTS] is clear.
- if sans["uniformResourceIdentifier"] then
- had_supported_altnames = true
- end
-
- if had_supported_altnames then return false end
- end
-
- -- Extract a common name from the certificate, and check it as if it were
- -- a dNSName subjectAltName (wildcards may apply for, and receive,
- -- cat treats)
- --
- -- Per [TLS-CERTS] 1.8, a CN-ID is the Common Name from a cert subject
- -- which has one and only one Common Name
- local subject = cert:subject()
- local cn = nil
- for i=1,#subject do
- local dn = subject[i]
- if dn["oid"] == oid_commonname then
- if cn then
- log("info", "Certificate has multiple common names")
- return false
- end
-
- cn = dn["value"];
- end
end
- if cn then
- -- Per [TLS-CERTS] 6.4.4, follow the comparison rules for dNSName SANs.
- return compare_dnsname(host, { cn })
- end
+ -- Per [TLS-IDENT] ignore the Common Name
+ -- The server identity can only be expressed in the subjectAltNames extension;
+ -- it is no longer valid to use the commonName RDN, known as CN-ID in [TLS-CERTS].
-- If all else fails, well, why should we be any different?
return false