diff options
Diffstat (limited to 'util')
-rw-r--r-- | util/argparse.lua | 77 | ||||
-rw-r--r-- | util/prosodyctl/check.lua | 312 | ||||
-rw-r--r-- | util/prosodyctl/shell.lua | 12 | ||||
-rw-r--r-- | util/sql.lua | 2 | ||||
-rw-r--r-- | util/x509.lua | 51 |
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 |