diff options
28 files changed, 584 insertions, 71 deletions
diff --git a/.luacheckrc b/.luacheckrc index 09225d01..1a392dab 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -131,6 +131,11 @@ files["spec/"] = { std = "+busted"; globals = { "randomize" }; } +files["spec/tls"] = { + -- luacheck complains about the config files here, + -- but we don't really care about them + only = {}; +} files["prosody.cfg.lua"] = { ignore = { "131" }; globals = { diff --git a/GNUmakefile b/GNUmakefile index ec51c893..e47b258f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -122,6 +122,9 @@ integration-test-%: all $(RUNWITH) prosodyctl --config ./spec/scansion/prosody.cfg.lua stop \ exit $$R +integration-test-tls: all + cd ./spec/tls && ./run.sh + coverage: -rm -- luacov.* $(BUSTED) --lua=$(RUNWITH) -c diff --git a/core/certmanager.lua b/core/certmanager.lua index 3acddf73..b13d57b3 100644 --- a/core/certmanager.lua +++ b/core/certmanager.lua @@ -116,14 +116,17 @@ local function index_certs(dir, files_by_name, depth_limit) else log("debug", "Skipping expired certificate: %s", full); end + else + log("debug", "Skipping non-certificate (based on contents): %s", full); end f:close(); elseif err then - log("debug", "Failed to open file for indexing: %s", full); + log("debug", "Skipping file due to error: %s", err); end + else + log("debug", "Skipping non-certificate (based on filename): %s", full); end end - log("debug", "Certificate index in %s: %q", dir, files_by_name); -- | hostname | filename | service | return files_by_name; end diff --git a/core/configmanager.lua b/core/configmanager.lua index 6c6b670b..fc475b6b 100644 --- a/core/configmanager.lua +++ b/core/configmanager.lua @@ -227,7 +227,17 @@ do host = env.__currenthost or "*"; option_name = k; }, config_option_proxy_mt); + elseif val == nil then + t_insert( + warnings, + ("%s: %d: unrecognized value: %s (you may be missing quotes around it)"):format( + config_file, + get_line_number(config_file), + k + ) + ); end + return val; end diff --git a/core/modulemanager.lua b/core/modulemanager.lua index 7295ba25..4d144a86 100644 --- a/core/modulemanager.lua +++ b/core/modulemanager.lua @@ -54,50 +54,58 @@ local _G = _G; local _ENV = nil; -- luacheck: std none -local loader = pluginloader.init({ - load_filter_cb = function (path, content) - local metadata = {}; - for line in content:gmatch("([^\r\n]+)\r?\n") do - local key, value = line:match("^%-%-%% *([%w_]+): *(.+)$"); - if key then - value = value:gsub("%s+$", ""); - metadata[key] = value; - end +local function plugin_load_filter_cb(path, content) + local metadata = {}; + for line in content:gmatch("([^\r\n]+)\r?\n") do + local key, value = line:match("^%-%-%% *([%w_]+): *(.+)$"); + if key then + value = value:gsub("%s+$", ""); + metadata[key] = value; end + end - if metadata.lua then - local supported = false; - for supported_lua_version in metadata.lua:gmatch("[^, ]+") do - if supported_lua_version == lua_version then - supported = true; - break; - end + if metadata.lua then + local supported = false; + for supported_lua_version in metadata.lua:gmatch("[^, ]+") do + if supported_lua_version == lua_version then + supported = true; + break; end - if not supported then + end + if not supported then + if prosody.process_type ~= "prosodyctl" then log("warn", "Not loading module, we have Lua %s but the module requires one of (%s): %s", lua_version, metadata.lua, path); - return; -- Don't load this module end + return nil, "incompatible with Lua "..lua_version; -- Don't load this module end + end - if metadata.conflicts then - local conflicts_features = set.new(array.collect(metadata.conflicts:gmatch("[^, ]+"))); - local conflicted_features = set.intersection(conflicts_features, core_features); - if not conflicted_features:empty() then - log("warn", "Not loading module, due to conflicting features '%s': %s", conflicted_features, path); - return; -- Don't load this module + if metadata.conflicts then + local conflicts_features = set.new(array.collect(metadata.conflicts:gmatch("[^, ]+"))); + local conflicted_features = set.intersection(conflicts_features, core_features); + if not conflicted_features:empty() then + if prosody.process_type ~= "prosodyctl" then + log("warn", "Not loading module, due to conflict with built-in features '%s': %s", conflicted_features, path); end + return nil, "conflict with built-in feature"; -- Don't load this module end - if metadata.requires then - local required_features = set.new(array.collect(metadata.requires:gmatch("[^, ]+"))); - local missing_features = required_features - core_features; - if not missing_features:empty() then + end + if metadata.requires then + local required_features = set.new(array.collect(metadata.requires:gmatch("[^, ]+"))); + local missing_features = required_features - core_features; + if not missing_features:empty() then + if prosody.process_type ~= "prosodyctl" then log("warn", "Not loading module, due to missing features '%s': %s", missing_features, path); - return; -- Don't load this module end + return nil, "Prosody version missing required feature"; -- Don't load this module end + end - return path, content, metadata; - end; + return path, content, metadata; +end; + +local loader = pluginloader.init({ + load_filter_cb = plugin_load_filter_cb; }); local load_modules_for_host, load, unload, reload, get_module, get_items; diff --git a/core/portmanager.lua b/core/portmanager.lua index 88bd7b61..3b9b8d67 100644 --- a/core/portmanager.lua +++ b/core/portmanager.lua @@ -253,12 +253,14 @@ local function add_sni_host(host, service) -- TODO should this be some generic thing? e.g. in the service definition alternate_host = config.get(host, "http_host"); end - local ssl, err, cfg = certmanager.create_context(alternate_host or host, "server", prefix_ssl_config, active_service.tls_cfg); + local autocert = certmanager.find_host_cert(alternate_host or host); + local ssl, err, cfg = certmanager.create_context(alternate_host or host, "server", prefix_ssl_config, autocert, active_service.tls_cfg); if not ssl then log("error", "Error creating TLS context for SNI host %s: %s", host, err); else + log("debug", "Using certificate %s for %s (%s) on %s (%s)", cfg.certificate, service or name, name, alternate_host or host, host) local ok, err = active_service.server:sslctx():set_sni_host( - host, + alternate_host or host, cfg.certificate, cfg.key ); diff --git a/net/server_epoll.lua b/net/server_epoll.lua index 44ab4f69..ca5a950c 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -772,7 +772,7 @@ function interface:starttls(tls_ctx) self.onreadable = interface.inittls; self:set(true, true); self:setreadtimeout(false); - self:setwritetimeout(cfg.ssl_handshake_timeout); + self:setwritetimeout(self._connected and cfg.ssl_handshake_timeout or cfg.connect_timeout); self:debug("Prepared to start TLS"); end end diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index c2b921b4..de345484 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -139,6 +139,8 @@ Built-in roles are: prosody:admin - Host administrator prosody:operator - Server administrator +To view roles and policies, see the commands in 'help role'. + Roles can be assigned using the user management commands (see 'help user'). ]]; @@ -2401,6 +2403,121 @@ function def_env.debug:async(runner_id) return true, ("%d runners pending"):format(c); end +describe_command [[debug:cert_index([path]) - Show Prosody's view of a directory of certs]] +function def_env.debug:cert_index(path) + local print = self.session.print; + local cm = require "core.certmanager"; + + path = path or module:get_option("certificates", "certs"); + + local sink = logger.add_simple_sink(function (source, level, message) + if source == "certmanager" then + if level == "debug" or level == "info" then + level = "II"; + elseif level == "warn" or level == "error" then + level = "EE"; + end + self.session.print(level..": "..message); + end + end); + + print("II: Scanning "..path.."..."); + + local index = {}; + cm.index_certs(path, index) + + if not logger.remove_sink(sink) then + module:log("warn", "Unable to remove log sink"); + end + + local c, max_domain = 0, 8; + for domain in pairs(index) do + if #domain > max_domain then + max_domain = #domain; + end + end + + print(""); + + local row = format_table({ + { title = "Domain", width = max_domain }; + { title = "Certificate", width = "100%" }; + { title = "Service", width = 5 }; + }, self.session.width); + print(row()); + print(("-"):rep(self.session.width or 80)); + for domain, certs in it.sorted_pairs(index) do + for cert_file, services in it.sorted_pairs(certs) do + for service in it.sorted_pairs(services) do + c = c + 1; + print(row({ domain, cert_file, service })); + end + end + end + + print(""); + + return true, ("Showing %d certificates in %s"):format(c, path); +end + +def_env.role = new_section("Role and access management"); + +describe_command [[role:list(host) - List known roles]] +function def_env.role:list(host) + if not host then + return nil, "Specify which host to list roles for"; + end + local role_list = {}; + for _, role in it.sorted_pairs(um.get_all_roles(host)) do + table.insert(role_list, role); + end + table.sort(role_list, function (a, b) + if a.priority ~= b.priority then + return (a.priority or 0) > (b.priority or 0); + end + return a.name < b.name; + end); + for _, role in ipairs(role_list) do + self.session.print(role.name); + end + return true, ("Showing %d roles on %s"):format(#role_list, host); +end + +describe_command [[role:show(host, role_name) - Show information about a role]] +function def_env.role:show(host, role_name) + if not host or not role_name then + return nil, "Specify the host and role to show"; + end + + local print = self.session.print; + local role = um.get_role_by_name(role_name, host); + + if not role then + return nil, ("Unable to find role %s on host %s"):format(role_name, host); + end + + local inherits = {}; + for _, inherited_role in ipairs(role.inherits or {}) do + table.insert(inherits, inherited_role.name); + end + + local permissions = {}; + for permission, is_allowed in role:policies() do + permissions[permission] = is_allowed and "allowed" or "denied"; + end + + print("Name: ", role.name); + print("Inherits:", table.concat(inherits, ", ")); + print("Policies:"); + local c = 0; + for permission, policy in it.sorted_pairs(permissions) do + c = c + 1; + print(" ["..(policy == "allowed" and "+" or " ").."] " .. permission); + end + print(""); + return true, ("Showing role %s with %d policies"):format(role.name, c); +end + def_env.stats = new_section("Commands to show internal statistics"); local short_units = { diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index f683d90c..1282f617 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -298,7 +298,11 @@ function add_default_permission(role_name, action, policy) end function get_role_by_name(role_name) - return assert(role_registry[role_name], role_name); + local role = role_registry[role_name]; + if not role then + return error("Unknown role: "..role_name); + end + return role, role_name; end function get_all_roles() diff --git a/plugins/mod_http.lua b/plugins/mod_http.lua index c13a2363..b5d0084c 100644 --- a/plugins/mod_http.lua +++ b/plugins/mod_http.lua @@ -331,7 +331,7 @@ local trusted_proxies = module:get_option_set("trusted_proxies", { "127.0.0.1", --- deal with [ipv6]:port / ip:port format local function normal_ip(ip) - return ip:match("^%[([%x:]*)%]") or ip:match("^([%d.]+)") or ip; + return ip:match("^%[([%x:]*)%]") or ip:match("^%d+%.%d+%.%d+%.%d+") or ip; end local function is_trusted_proxy(ip) @@ -339,7 +339,8 @@ local function is_trusted_proxy(ip) if trusted_proxies[ip] then return true; end - local parsed_ip = new_ip(ip) + local parsed_ip, err = new_ip(ip); + if not parsed_ip then return nil, err; end for trusted_proxy in trusted_proxies do if match_ip(parsed_ip, parse_cidr(trusted_proxy)) then return true; @@ -357,10 +358,14 @@ local function get_forwarded_connection_info(request) --> ip:string, secure:bool request.forwarded = forwarded; for i = #forwarded, 1, -1 do local proxy = forwarded[i] - if is_trusted_proxy(ip) then + local trusted, err = is_trusted_proxy(ip); + if trusted then ip = normal_ip(proxy["for"]); secure = secure and proxy.proto == "https"; else + if err then + request.log("warn", "Could not parse forwarded connection details: %s"); + end break end end @@ -387,7 +392,10 @@ function get_forwarded_connection_info(request) --> ip:string, secure:boolean -- Case d) If all IPs are in trusted proxies, something went obviously wrong and the logic never overwrites `ip`, leaving it at the original request IP. forwarded_for = forwarded_for..", "..ip; for forwarded_ip in forwarded_for:gmatch("[^%s,]+") do - if not is_trusted_proxy(forwarded_ip) then + local trusted, err = is_trusted_proxy(forwarded_ip); + if err then + request.log("warn", "Could not parse forwarded connection details: %s"); + elseif not trusted then ip = forwarded_ip; end end diff --git a/plugins/mod_invites_register.lua b/plugins/mod_invites_register.lua index d9274ce4..76b644c7 100644 --- a/plugins/mod_invites_register.lua +++ b/plugins/mod_invites_register.lua @@ -101,8 +101,20 @@ module:hook("user-registering", function (event) -- for this module to do... return; end - if validated_invite and validated_invite.additional_data and validated_invite.additional_data.allow_reset then - event.allow_reset = validated_invite.additional_data.allow_reset; + if validated_invite then + local username = validated_invite.username; + if username and username ~= event.username then + event.allowed = false; + event.reason = "The chosen username is not valid with this invitation"; + end + local reset_username = validated_invite.additional_data and validated_invite.additional_data.allow_reset; + if reset_username then + if reset_username ~= event.username then + event.allowed = false; + event.reason = "Incorrect username for password reset"; + end + event.allow_reset = reset_username; + end end end); diff --git a/plugins/mod_storage_internal.lua b/plugins/mod_storage_internal.lua index 1332ae75..d5ef7112 100644 --- a/plugins/mod_storage_internal.lua +++ b/plugins/mod_storage_internal.lua @@ -205,12 +205,11 @@ function archive:find(username, query) return query.start - when; end); i = wi - 1; - else - iter = it.filter(function(item) - local when = item.when or datetime.parse(item.attr.stamp); - return when >= query.start; - end, iter); end + iter = it.filter(function(item) + local when = item.when or datetime.parse(item.attr.stamp); + return when >= query.start; + end, iter); end if query["end"] then if query.reverse then @@ -221,12 +220,11 @@ function archive:find(username, query) if wi then i = wi + 1; end - else - iter = it.filter(function(item) - local when = item.when or datetime.parse(item.attr.stamp); - return when <= query["end"]; - end, iter); end + iter = it.filter(function(item) + local when = item.when or datetime.parse(item.attr.stamp); + return when <= query["end"]; + end, iter); end if query.after then local found = false; diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua index f053f729..6d9af68a 100644 --- a/plugins/mod_storage_sql.lua +++ b/plugins/mod_storage_sql.lua @@ -13,20 +13,24 @@ local t_concat = table.concat; local have_dbisql, dbisql = pcall(require, "prosody.util.sql"); local have_sqlite, sqlite = pcall(require, "prosody.util.sqlite3"); -if not have_dbisql then - module:log("debug", "Could not load LuaDBI: %s", dbisql) - dbisql = nil; -end -if not have_sqlite then - module:log("debug", "Could not load LuaSQLite3: %s", sqlite) - sqlite = nil; -end if not (have_dbisql or have_sqlite) then module:log("error", "LuaDBI or LuaSQLite3 are required for using SQL databases but neither are installed"); module:log("error", "Please install at least one of LuaDBI and LuaSQLite3. See https://prosody.im/doc/depends"); + module:log("debug", "Could not load LuaDBI: %s", dbisql); + module:log("debug", "Could not load LuaSQLite3: %s", sqlite); error("No SQL library available") end +local function get_sql_lib(driver) + if driver == "SQLite3" and have_sqlite then + return sqlite; + elseif have_dbisql then + return dbisql; + else + error(dbisql); + end +end + local noop = function() end local unpack = table.unpack; local function iterator(result) @@ -42,11 +46,11 @@ end local function has_upsert(engine) if engine.params.driver == "SQLite3" then -- SQLite3 >= 3.24.0 - return engine.sqlite_version and (engine.sqlite_version[2] or 0) >= 24; + return engine.sqlite_version and (engine.sqlite_version[2] or 0) >= 24 and engine.has_upsert_index; elseif engine.params.driver == "PostgreSQL" then -- PostgreSQL >= 9.5 -- Versions without support have long since reached end of life. - return true; + return engine.has_upsert_index; end -- We don't support UPSERT on MySQL/MariaDB, they seem to have a completely different syntax, uncertaint from which versions. return false @@ -757,7 +761,7 @@ end local function create_table(engine) -- luacheck: ignore 431/engine - local sql = engine.params.driver == "SQLite3" and sqlite or dbisql; + local sql = get_sql_lib(engine.params.driver); local Table, Column, Index = sql.Table, sql.Column, sql.Index; local ProsodyTable = Table { @@ -798,7 +802,7 @@ end local function upgrade_table(engine, params, apply_changes) -- luacheck: ignore 431/engine local changes = false; if params.driver == "MySQL" then - local sql = dbisql; + local sql = get_sql_lib("MySQL"); local success,err = engine:transaction(function() do local result = assert(engine:execute("SHOW COLUMNS FROM \"prosody\" WHERE \"Field\"='value' and \"Type\"='text'")); @@ -879,7 +883,7 @@ local function upgrade_table(engine, params, apply_changes) -- luacheck: ignore indices[row[1]] = true; end elseif params.driver == "PostgreSQL" then - for row in engine:select [[SELECT "indexname" FROM "pg_indexes" WHERE "tablename"='prosody' AND "indexname"='prosody_index';]] do + for row in engine:select [[SELECT "indexname" FROM "pg_indexes" WHERE "tablename"='prosody';]] do indices[row[1]] = true; end end @@ -893,6 +897,12 @@ local function upgrade_table(engine, params, apply_changes) -- luacheck: ignore return false; end end + if not indices["prosody_unique_index"] then + module:log("warn", "Index \"prosody_unique_index\" does not exist, performance may be worse than normal!"); + engine.has_upsert_index = false; + else + engine.has_upsert_index = true; + end end return changes; end @@ -920,7 +930,7 @@ end function module.load() local engines = module:shared("/*/sql/connections"); local params = normalize_params(module:get_option("sql", default_params)); - local sql = params.driver == "SQLite3" and sqlite or dbisql; + local sql = get_sql_lib(params.driver); local db_uri = sql.db2uri(params); engine = engines[db_uri]; if not engine then @@ -1012,7 +1022,7 @@ function module.command(arg) local uris = {}; for host in pairs(prosody.hosts) do -- luacheck: ignore 431/host local params = normalize_params(config.get(host, "sql") or default_params); - local sql = engine.params.driver == "SQLite3" and sqlite or dbisql; + local sql = get_sql_lib(engine.params.driver); uris[sql.db2uri(params)] = params; end print("We will check and upgrade the following databases:\n"); @@ -1028,7 +1038,7 @@ function module.command(arg) -- Upgrade each one for _, params in pairs(uris) do print("Checking "..params.database.."..."); - local sql = params.driver == "SQLite3" and sqlite or dbisql; + local sql = get_sql_lib(params.driver); engine = sql:create_engine(params); upgrade_table(engine, params, true); end @@ -1040,3 +1050,32 @@ function module.command(arg) print("","upgrade - Perform database upgrade"); end end + +module:add_item("shell-command", { + section = "sql"; + section_desc = "SQL management commands"; + name = "create"; + desc = "Create the tables and indices used by Prosody (again)"; + args = { { name = "host"; type = "string" } }; + host_selector = "host"; + handler = function(shell, _host) + local logger = require "prosody.util.logger"; + local writing = false; + local sink = logger.add_simple_sink(function (source, _level, message) + local print = shell.session.print; + if writing or source ~= "sql" then return; end + writing = true; + print(message); + writing = false; + end); + + local debug_enabled = engine._debug; + engine:debug(true); + create_table(engine); + engine:debug(debug_enabled); + + if not logger.remove_sink(sink) then + module:log("warn", "Unable to remove log sink"); + end + end; +}) diff --git a/spec/core_storagemanager_spec.lua b/spec/core_storagemanager_spec.lua index 32a8d6f0..a6857d0c 100644 --- a/spec/core_storagemanager_spec.lua +++ b/spec/core_storagemanager_spec.lua @@ -436,6 +436,44 @@ describe("storagemanager", function () assert.equal(#test_data - 3, count); end); + it("by time (start before first item)", function () + -- luacheck: ignore 211/err + local data, err = archive:find("user", { + ["start"] = test_time-5; + }); + assert.truthy(data); + local count = 0; + for id, item, when in data do + count = count + 1; + assert.truthy(id); + assert(st.is_stanza(item)); + assert.equal("test", item.name); + assert.equal("urn:example:foo", item.attr.xmlns); + assert.equal(2, #item.tags); + assert(when >= test_time-5, ("%d >= %d"):format(when, test_time-5)); + end + assert.equal(#test_data, count); + end); + + it("by time (start after last item)", function () + -- luacheck: ignore 211/err + local data, err = archive:find("user", { + ["start"] = test_time+5; + }); + assert.truthy(data); + local count = 0; + for id, item, when in data do + count = count + 1; + assert.truthy(id); + assert(st.is_stanza(item)); + assert.equal("test", item.name); + assert.equal("urn:example:foo", item.attr.xmlns); + assert.equal(2, #item.tags); + assert(when >= test_time+5, ("%d >= %d"):format(when, test_time+5)); + end + assert.equal(0, count); + end); + it("by time (start+end)", function () -- luacheck: ignore 211/err local data, err = archive:find("user", { diff --git a/spec/tls/README b/spec/tls/README new file mode 100644 index 00000000..58201728 --- /dev/null +++ b/spec/tls/README @@ -0,0 +1,11 @@ +These tests check that SSL/TLS configuration is working as expected. + +Just run ./run.sh in this directory (or from the top level, +`make integration-test-tls`. + +Known issues: + - The tests do not thorougly clean up after themselves (certs, logs, etc.). + This is partly intentional, so they can be inspected in case of failures. + - Certs are regenerated every time. Could be smarter about this. But it also + helps to guard against incorrect Prosody instances running and hogging the + ports, etc. diff --git a/spec/tls/config1/assert.sh b/spec/tls/config1/assert.sh new file mode 100755 index 00000000..f7d41c26 --- /dev/null +++ b/spec/tls/config1/assert.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +#set -x + +. ../lib.sh + +expect_cert "certs/example.com.crt" "localhost:5222" "example.com" "xmpp" +expect_cert "certs/share.example.com.crt" "localhost:5281" "share.example.com" "tls" + +exit "$failures" diff --git a/spec/tls/config1/prepare.sh b/spec/tls/config1/prepare.sh new file mode 100755 index 00000000..a8ec2822 --- /dev/null +++ b/spec/tls/config1/prepare.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +certs="./certs" + +for domain in {,share.}example.com; do + openssl req -x509 \ + -newkey rsa:4096 \ + -keyout "${certs}/${domain}.key" \ + -out "${certs}/${domain}.crt" \ + -sha256 \ + -days 365 \ + -nodes \ + -subj "/CN=${domain}" 2>/dev/null; +done diff --git a/spec/tls/config1/prosody.cfg.lua b/spec/tls/config1/prosody.cfg.lua new file mode 100644 index 00000000..9e94de58 --- /dev/null +++ b/spec/tls/config1/prosody.cfg.lua @@ -0,0 +1,6 @@ +Include "prosody-default.cfg.lua" + +VirtualHost "example.com" + enabled = true + +Component "share.example.com" "http_file_share" diff --git a/spec/tls/config2/assert.sh b/spec/tls/config2/assert.sh new file mode 100755 index 00000000..d1af0f51 --- /dev/null +++ b/spec/tls/config2/assert.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +#set -x + +. ../lib.sh + +expect_cert "certs/xmpp.example.com.crt" "localhost:5281" "xmpp.example.com" "tls" +expect_cert "certs/example.com.crt" "localhost:5222" "example.com" "xmpp" + +exit "$failures" diff --git a/spec/tls/config2/prepare.sh b/spec/tls/config2/prepare.sh new file mode 100755 index 00000000..1d67af4e --- /dev/null +++ b/spec/tls/config2/prepare.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +certs="./certs" + +for domain in {,xmpp.}example.com; do + openssl req -x509 \ + -newkey rsa:4096 \ + -keyout "${certs}/${domain}.key" \ + -out "${certs}/${domain}.crt" \ + -sha256 \ + -days 365 \ + -nodes \ + -subj "/CN=${domain}" 2>/dev/null; +done diff --git a/spec/tls/config2/prosody.cfg.lua b/spec/tls/config2/prosody.cfg.lua new file mode 100644 index 00000000..a5728516 --- /dev/null +++ b/spec/tls/config2/prosody.cfg.lua @@ -0,0 +1,6 @@ +Include "prosody-default.cfg.lua" + +VirtualHost "example.com" + enabled = true + modules_enabled = { "http" } + http_host = "xmpp.example.com" diff --git a/spec/tls/config3/assert.sh b/spec/tls/config3/assert.sh new file mode 100755 index 00000000..e36f7fb1 --- /dev/null +++ b/spec/tls/config3/assert.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +#set -x + +. ../lib.sh + +expect_cert "certs/xmpp.example.com.crt" "localhost:5281" "xmpp.example.com" "tls" +expect_cert "certs/example.com.crt" "localhost:5222" "example.com" "xmpp" +expect_cert "certs/example.com.crt" "localhost:5223" "example.com" "xmpps" + +# Weirdly configured host, just to test manual override behaviour +expect_cert "certs/example.com.crt" "localhost:5222" "example.net" "xmpp" +expect_cert "certs/example.com.crt" "localhost:5222" "example.net" "xmpp" +expect_cert "certs/example.com.crt" "localhost:5223" "example.net" "tls" +expect_cert "certs/example.com.crt" "localhost:5281" "example.net" "tls" + +# Three domains using a single cert with SANs +expect_cert "certs/example.org.crt" "localhost:5222" "example.org" "xmpp" +expect_cert "certs/example.org.crt" "localhost:5223" "example.org" "xmpps" +expect_cert "certs/example.org.crt" "localhost:5269" "example.org" "xmpp-server" +expect_cert "certs/example.org.crt" "localhost:5269" "share.example.org" "xmpp-server" +expect_cert "certs/example.org.crt" "localhost:5269" "groups.example.org" "xmpp-server" +expect_cert "certs/example.org.crt" "localhost:5281" "share.example.org" "tls" + +exit "$failures" diff --git a/spec/tls/config3/prepare.sh b/spec/tls/config3/prepare.sh new file mode 100755 index 00000000..89269d73 --- /dev/null +++ b/spec/tls/config3/prepare.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +certs="./certs" + +for domain in {,xmpp.}example.com example.net; do + openssl req -x509 \ + -newkey rsa:4096 \ + -keyout "${certs}/${domain}.key" \ + -out "${certs}/${domain}.crt" \ + -sha256 \ + -days 365 \ + -nodes \ + -quiet \ + -subj "/CN=${domain}" 2>/dev/null; +done + +for domain in example.org; do + openssl req -x509 \ + -newkey rsa:4096 \ + -keyout "${certs}/${domain}.key" \ + -out "${certs}/${domain}.crt" \ + -sha256 \ + -days 365 \ + -nodes \ + -subj "/CN=${domain}" \ + -addext "subjectAltName = DNS:${domain}, DNS:groups.${domain}, DNS:share.${domain}" \ + 2>/dev/null; +done diff --git a/spec/tls/config3/prosody.cfg.lua b/spec/tls/config3/prosody.cfg.lua new file mode 100644 index 00000000..a92dbfa8 --- /dev/null +++ b/spec/tls/config3/prosody.cfg.lua @@ -0,0 +1,28 @@ +Include "prosody-default.cfg.lua" + +c2s_direct_tls_ports = { 5223 } + +VirtualHost "example.com" + enabled = true + modules_enabled = { "http" } + http_host = "xmpp.example.com" + +VirtualHost "example.net" + ssl = { + certificate = "certs/example.com.crt"; + key = "certs/example.com.key"; + } + + https_ssl = { + certificate = "certs/example.com.crt"; + key = "certs/example.com.key"; + } + + c2s_direct_tls_ssl = { + certificate = "certs/example.com.crt"; + key = "certs/example.com.key"; + } + +VirtualHost "example.org" +Component "share.example.org" "http_file_share" +Component "groups.example.org" "muc" diff --git a/spec/tls/lib.sh b/spec/tls/lib.sh new file mode 100644 index 00000000..d072802a --- /dev/null +++ b/spec/tls/lib.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +test_name="$(basename "$PWD")" +export failures=0 + +get_net_cert () { + address="${1?}" + sni="${2?}" + proto="${3?}" + local flags=() + case "$proto" in + "xmpp") flags=(-starttls xmpp -name "$sni");; + "xmpps") flags=(-alpn xmpp-client);; + "xmpp-server") flags=(-starttls xmpp-server -name "$sni");; + "xmpps-server") flags=(-alpn xmpp-server);; + "tls") ;; + *) printf "EE: Unknown protocol: %s\n" "$proto" >&2; exit 1;; + esac + openssl s_client -connect "$address" -servername "$sni" "${flags[@]}" 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' +} + +get_file_cert () { + fn="${1?}" + sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' "$fn" +} + +expect_cert () { + fn="${1?}" + address="${2?}" + sni="${3?}" + proto="${4?}" + net_cert="$(get_net_cert "$address" "$sni" "$proto")" + file_cert="$(get_file_cert "$fn")" + if [[ "$file_cert" != "$net_cert" ]]; then + echo "---" + echo "NOT OK: $test_name: Expected $fn on $address (SNI $sni)" + echo "Received:" + openssl x509 -in <(echo "$net_cert") -text + echo "---" + failures=1; + return 1; + fi + echo "OK: $test_name: $fn observed on $address (SNI $sni)" + return 0; +} diff --git a/spec/tls/run.sh b/spec/tls/run.sh new file mode 100755 index 00000000..8bceddb2 --- /dev/null +++ b/spec/tls/run.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +export LUA_PATH="../../../?.lua;;" +export LUA_CPATH="../../../?.so;;" + +any_failed=0 + +for config in config*; do + echo "# Preparing $config" + pushd "$config"; + cp ../../../prosody.cfg.lua.dist ./prosody-default.cfg.lua + echo 'VirtualHost "*" {pidfile = "prosody.pid";log={debug="prosody.log"}}' >> ./prosody-default.cfg.lua + ln -s ../../../plugins plugins + mkdir -p certs data + ./prepare.sh + ../../../prosody -D + sleep 1; + echo "# Testing $config" + ./assert.sh + status=$? + ../../../prosodyctl stop + rm plugins #prosody-default.cfg.lua + popd + if [[ "$status" != "0" ]]; then + echo -n "NOT "; + any_failed=1 + fi + echo "OK: $config"; +done + +if [[ "$any_failed" != "0" ]]; then + echo "NOT OK: One or more TLS tests failed"; + exit 1; +fi + +echo "OK: All TLS tests passed"; +exit 0; diff --git a/util/pluginloader.lua b/util/pluginloader.lua index 634bd6f8..4d05ea8d 100644 --- a/util/pluginloader.lua +++ b/util/pluginloader.lua @@ -25,6 +25,7 @@ local pluginloader_mt = { __index = pluginloader_methods }; function pluginloader_methods:load_file(names) local file, err, path; local load_filter_cb = self._options.load_filter_cb; + local last_filter_path, last_filter_err; for i=1,#plugin_dir do for j=1,#names do path = plugin_dir[i]..names[j]; @@ -36,12 +37,18 @@ function pluginloader_methods:load_file(names) if load_filter_cb then path, content, metadata = load_filter_cb(path, content); end - if content and path then + if path and content then return content, path, metadata; + else + last_filter_path = plugin_dir[i]..names[j]; + last_filter_err = content or "skipped"; end end end end + if last_filter_err then + return nil, err..(" (%s skipped because of %s)"):format(last_filter_path, last_filter_err); + end return file, err; end diff --git a/util/prosodyctl/check.lua b/util/prosodyctl/check.lua index 75ff5da4..9d3c3cb9 100644 --- a/util/prosodyctl/check.lua +++ b/util/prosodyctl/check.lua @@ -643,6 +643,31 @@ local function check(arg) print(" mod_posix is loaded in your configuration file, but it has"); print(" been deprecated. You can safely remove it."); end + if all_modules:contains("admin_telnet") then + print(""); + print(" mod_admin_telnet is being replaced by mod_admin_shell (prosodyctl shell)."); + print(" To update and ensure all commands are available, simply change \"admin_telnet\" to \"admin_shell\""); + print(" in your modules_enabled list."); + end + + local load_failures = {}; + for mod_name in all_modules do + local mod, err = modulemanager.loader:load_resource(mod_name, nil); + if not mod then + load_failures[mod_name] = err; + end + end + + if next(load_failures) ~= nil then + print(""); + print(" The following modules failed to load:"); + print(""); + for mod_name, err in it.sorted_pairs(load_failures) do + print((" mod_%s: %s"):format(mod_name, err)); + end + print("") + print(" Check for typos and remove any obsolete/incompatible modules from your config."); + 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 @@ -1313,7 +1338,7 @@ local function check(arg) http_loaded = false; end if http_loaded and not x509_verify_identity(http_host, nil, cert) then - print(" Not valid for HTTPS connections to "..host..".") + print(" Not valid for HTTPS connections to "..http_host..".") cert_ok = false end if use_dane then |