Merge config-updates+check-turn from timber
authorMatthew Wild <mwild1@gmail.com>
Fri, 04 Mar 2022 16:33:41 +0000
changeset 12366 0fd58f54d653
parent 12324 f0be98bab9dd (current diff)
parent 12365 713f8ee9e1b4 (diff)
child 12367 0576d7d625a0
Merge config-updates+check-turn from timber
CHANGES
GNUmakefile
core/certmanager.lua
plugins/mod_s2s.lua
util/prosodyctl/check.lua
--- a/CHANGES	Wed Mar 02 16:12:28 2022 +0000
+++ b/CHANGES	Fri Mar 04 16:33:41 2022 +0000
@@ -33,6 +33,8 @@
 -   Pluggable authorization providers (mod_authz_)
 -   Easy use of Mozilla TLS recommendations presets
 -   Unencrypted HTTP port (5280) restricted to loopback by default
+-   require_encryption options default to 'true' if unspecified
+-   Authentication module defaults to 'internal_hashed' if unspecified
 
 ### HTTP
 
--- a/COPYING	Wed Mar 02 16:12:28 2022 +0000
+++ b/COPYING	Fri Mar 04 16:33:41 2022 +0000
@@ -1,5 +1,12 @@
-Copyright (c) 2008-2011 Matthew Wild
-Copyright (c) 2008-2011 Waqas Hussain
+All source code in this project is released under the below MIT license. Some
+components are not authored by the Prosody maintainers, but such code is
+itself either released under a MIT license or declared public domain.
+
+---
+
+Copyright (C) 2008-2022 Matthew Wild
+Copyright (C) 2008-2020 Waqas Hussain
+Copyright (C) 2010-2022 Kim Alvefur
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -18,3 +25,20 @@
 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+---
+
+util-src/encodings.c:
+  Parts included from Lua 5.3. Copyright (C) 1994-2015 Lua.org, PUC-Rio.
+
+util-src/signal.c:
+  Copyright (C) 2007  Patrick J. Donnelly (batrick@batbytes.com)
+  See full copyright notice in the source file.
+
+util-src/struct.c:
+  Copyright (C) 2010-2018 Lua.org, PUC-Rio. All rights reserved.
+  See full copyright notice in the source file.
+
+net/dns.lua:
+  public domain 20080404 lua@ztact.com
+
--- a/GNUmakefile	Wed Mar 02 16:12:28 2022 +0000
+++ b/GNUmakefile	Fri Mar 04 16:33:41 2022 +0000
@@ -56,7 +56,7 @@
 	$(INSTALL_DATA) net/resolvers/*.lua $(SOURCE)/net/resolvers
 	$(INSTALL_DATA) net/websocket/*.lua $(SOURCE)/net/websocket
 
-install-util: util/encodings.so util/encodings.so util/pposix.so util/signal.so
+install-util: util/encodings.so util/encodings.so util/pposix.so util/signal.so util/struct.so
 	$(MKDIR) $(SOURCE)
 	$(MKDIR) $(SOURCE)/util
 	$(INSTALL_DATA) util/*.lua $(SOURCE)/util
--- a/core/certmanager.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/core/certmanager.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -6,20 +6,10 @@
 -- COPYING file in the source package for more information.
 --
 
-local softreq = require"util.dependencies".softreq;
-local ssl = softreq"ssl";
-if not ssl then
-	return {
-		create_context = function ()
-			return nil, "LuaSec (required for encryption) was not found";
-		end;
-		reload_ssl_config = function () end;
-	}
-end
-
+local ssl = require "ssl";
 local configmanager = require "core.configmanager";
 local log = require "util.logger".init("certmanager");
-local ssl_context = ssl.context or softreq"ssl.context";
+local ssl_context = ssl.context or require "ssl.context";
 local ssl_newcontext = ssl.newcontext;
 local new_config = require"util.sslconfig".new;
 local stat = require "lfs".attributes;
@@ -48,7 +38,7 @@
 
 local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)");
 local luasec_version = tonumber(luasec_major) * 100 + tonumber(luasec_minor);
-local luasec_has = ssl.config or softreq"ssl.config" or {
+local luasec_has = ssl.config or {
 	algorithms = {
 		ec = luasec_version >= 5;
 	};
--- a/core/usermanager.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/core/usermanager.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -23,7 +23,7 @@
 
 local setmetatable = setmetatable;
 
-local default_provider = "internal_plain";
+local default_provider = "internal_hashed";
 
 local _ENV = nil;
 -- luacheck: std none
--- a/net/server_epoll.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/net/server_epoll.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -68,8 +68,7 @@
 	min_wait = 0.001;
 
 	-- Enable extra noisy debug logging
-	-- TODO disable once considered stable
-	verbose = true;
+	verbose = false;
 
 	-- EXPERIMENTAL
 	-- Whether to kill connections in case of callback errors.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/stun.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -0,0 +1,283 @@
+local base64 = require "util.encodings".base64;
+local hashes = require "util.hashes";
+local net = require "util.net";
+local random = require "util.random";
+local struct = require "util.struct";
+local bit32 = require"util.bitcompat";
+local sxor = require"util.strbitop".sxor;
+
+--- Public helpers
+
+-- Following draft-uberti-behave-turn-rest-00, convert a 'secret' string
+-- into a username/password pair that can be used to auth to a TURN server
+local function get_user_pass_from_secret(secret, ttl, opt_username)
+	ttl = ttl or 86400;
+	local username;
+	if opt_username then
+		username = ("%d:%s"):format(os.time() + ttl, opt_username);
+	else
+		username = ("%d"):format(os.time() + ttl);
+	end
+	local password = base64.encode(hashes.hmac_sha1(secret, username));
+	return username, password, ttl;
+end
+
+-- Following RFC 8489 9.2, convert credentials to a HMAC key for signing
+local function get_long_term_auth_key(realm, username, password)
+	return hashes.md5(username..":"..realm..":"..password);
+end
+
+--- Packet building/parsing
+
+local packet_methods = {};
+local packet_mt = { __index = packet_methods };
+
+local magic_cookie = string.char(0x21, 0x12, 0xA4, 0x42);
+
+local methods = {
+	binding = 0x001;
+	-- TURN
+	allocate = 0x003;
+	refresh = 0x004;
+	send = 0x006;
+	data = 0x007;
+	create_permission = 0x008;
+	channel_bind = 0x009;
+};
+local method_lookup = {};
+for name, value in pairs(methods) do
+	method_lookup[name] = value;
+	method_lookup[value] = name;
+end
+
+local classes = {
+	request = 0;
+	indication = 1;
+	success = 2;
+	error = 3;
+};
+local class_lookup = {};
+for name, value in pairs(classes) do
+	class_lookup[name] = value;
+	class_lookup[value] = name;
+end
+
+local attributes = {
+	["mapped-address"] = 0x0001;
+	["username"] = 0x0006;
+	["message-integrity"] = 0x0008;
+	["error-code"] = 0x0009;
+	["unknown-attributes"] = 0x000A;
+	["realm"] = 0x0014;
+	["nonce"] = 0x0015;
+	["xor-mapped-address"] = 0x0020;
+	["software"] = 0x8022;
+	["alternate-server"] = 0x8023;
+	["fingerprint"] = 0x8028;
+	["message-integrity-sha256"] = 0x001C;
+	["password-algorithm"] = 0x001D;
+	["userhash"] = 0x001E;
+	["password-algorithms"] = 0x8002;
+	["alternate-domains"] = 0x8003;
+
+	-- TURN
+	["requested-transport"] = 0x0019;
+};
+local attribute_lookup = {};
+for name, value in pairs(attributes) do
+	attribute_lookup[name] = value;
+	attribute_lookup[value] = name;
+end
+
+function packet_methods:serialize_header(length)
+	assert(#self.transaction_id == 12, "invalid transaction id length");
+	local header = struct.pack(">I2I2",
+		self.type,
+		length
+	)..magic_cookie..self.transaction_id;
+	return header;
+end
+
+function packet_methods:serialize()
+	local payload = table.concat(self.attributes);
+	return self:serialize_header(#payload)..payload;
+end
+
+function packet_methods:is_request()
+	return bit32.band(self.type, 0x0110) == 0x0000;
+end
+
+function packet_methods:is_indication()
+	return bit32.band(self.type, 0x0110) == 0x0010;
+end
+
+function packet_methods:is_success_resp()
+	return bit32.band(self.type, 0x0110) == 0x0100;
+end
+
+function packet_methods:is_err_resp()
+	return bit32.band(self.type, 0x0110) == 0x0110;
+end
+
+function packet_methods:get_method()
+	local method = bit32.bor(
+		bit32.rshift(bit32.band(self.type, 0x3E00), 2),
+		bit32.rshift(bit32.band(self.type, 0x00E0), 1),
+		bit32.band(self.type, 0x000F)
+	);
+	return method, method_lookup[method];
+end
+
+function packet_methods:get_class()
+	local class = bit32.bor(
+		bit32.rshift(bit32.band(self.type, 0x0100), 7),
+		bit32.rshift(bit32.band(self.type, 0x0010), 4)
+	);
+	return class, class_lookup[class];
+end
+
+function packet_methods:set_type(method, class)
+	if type(method) == "string" then
+		method = assert(method_lookup[method:lower()], "unknown method: "..method);
+	end
+	if type(class) == "string" then
+		class = assert(classes[class], "unknown class: "..class);
+	end
+	self.type = bit32.bor(
+		bit32.lshift(bit32.band(method, 0x1F80), 2),
+		bit32.lshift(bit32.band(method, 0x0070), 1),
+		bit32.band(method, 0x000F),
+		bit32.lshift(bit32.band(class, 0x0002), 7),
+		bit32.lshift(bit32.band(class, 0x0001), 4)
+	);
+end
+
+local function _serialize_attribute(attr_type, value)
+	local len = #value;
+	local padding = string.rep("\0", (4 - len)%4);
+	return struct.pack(">I2I2",
+		attr_type, len
+	)..value..padding;
+end
+
+function packet_methods:add_attribute(attr_type, value)
+	if type(attr_type) == "string" then
+		attr_type = assert(attributes[attr_type], "unknown attribute: "..attr_type);
+	end
+	table.insert(self.attributes, _serialize_attribute(attr_type, value));
+end
+
+function packet_methods:deserialize(bytes)
+	local type, len, cookie = struct.unpack(">I2I2I4", bytes);
+	assert(#bytes == (len + 20), "incorrect packet length");
+	assert(cookie == 0x2112A442, "invalid magic cookie");
+	self.type = type;
+	self.transaction_id = bytes:sub(9, 20);
+	self.attributes = {};
+	local pos = 21;
+	while pos < #bytes do
+		local attr_hdr = bytes:sub(pos, pos+3);
+		assert(#attr_hdr == 4, "packet truncated in attribute header");
+		local attr_type, attr_len = struct.unpack(">I2I2", attr_hdr); --luacheck: ignore 211/attr_type
+		if attr_len == 0 then
+			table.insert(self.attributes, attr_hdr);
+			pos = pos + 20;
+		else
+			local data = bytes:sub(pos + 4, pos + 3 + attr_len);
+			assert(#data == attr_len, "packet truncated in attribute value");
+			table.insert(self.attributes, attr_hdr..data);
+			local n_padding = (4 - attr_len)%4;
+			pos = pos + 4 + attr_len + n_padding;
+		end
+	end
+	return self;
+end
+
+function packet_methods:get_attribute(attr_type)
+	if type(attr_type) == "string" then
+		attr_type = assert(attribute_lookup[attr_type:lower()], "unknown attribute: "..attr_type);
+	end
+	for _, attribute in ipairs(self.attributes) do
+		if struct.unpack(">I2", attribute) == attr_type then
+			return attribute:sub(5);
+		end
+	end
+end
+
+local addr_families = { "IPv4", "IPv6" };
+function packet_methods:get_mapped_address()
+	local data = self:get_attribute("mapped-address");
+	if not data then return; end
+	local family, port = struct.unpack("x>BI2", data);
+	local addr = data:sub(5);
+	return {
+		family = addr_families[family] or "unknown";
+		port = port;
+		address = net.ntop(addr);
+	};
+end
+
+function packet_methods:get_xor_mapped_address()
+	local data = self:get_attribute("xor-mapped-address");
+	if not data then return; end
+	local family, port = struct.unpack("x>BI2", data);
+	local addr = sxor(data:sub(5), magic_cookie..self.transaction_id);
+	return {
+		family = addr_families[family] or "unknown";
+		port = bit32.bxor(port, 0x2112);
+		address = net.ntop(addr);
+		address_raw = data:sub(5);
+	};
+end
+
+function packet_methods:add_message_integrity(key)
+	-- Add attribute with a dummy value so we can artificially increase
+	-- the packet 'length'
+	self:add_attribute("message-integrity", string.rep("\0", 20));
+	-- Get the packet data, minus the message-integrity attribute itself
+	local pkt = self:serialize():sub(1, -25);
+	local hash = hashes.hmac_sha1(key, pkt, false);
+	self.attributes[#self.attributes] = nil;
+	assert(#hash == 20, "invalid hash length");
+	self:add_attribute("message-integrity", hash);
+end
+
+do
+	local transports = {
+		udp = 0x11;
+	};
+	function packet_methods:add_requested_transport(transport)
+		local transport_code = transports[transport];
+		assert(transport_code, "unsupported transport: "..tostring(transport));
+		self:add_attribute("requested-transport", string.char(
+			transport_code, 0x00, 0x00, 0x00
+		));
+	end
+end
+
+function packet_methods:get_error()
+	local err_attr = self:get_attribute("error-code");
+	if not err_attr then
+		return nil;
+	end
+	local number = err_attr:byte(4);
+	local class = bit32.band(0x07, err_attr:byte(3));
+	local msg = err_attr:sub(5);
+	return (class*100)+number, msg;
+end
+
+local function new_packet(method, class)
+	local p = setmetatable({
+		transaction_id = random.bytes(12);
+		length = 0;
+		attributes = {};
+	}, packet_mt);
+	p:set_type(method or "binding", class or "request");
+	return p;
+end
+
+return {
+	new_packet = new_packet;
+	get_user_pass_from_secret = get_user_pass_from_secret;
+	get_long_term_auth_key = get_long_term_auth_key;
+};
--- a/plugins/mod_auth_internal_hashed.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/plugins/mod_auth_internal_hashed.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -14,7 +14,7 @@
 local generate_uuid = require "util.uuid".generate;
 local new_sasl = require "util.sasl".new;
 local hex = require"util.hex";
-local to_hex, from_hex = hex.to, hex.from;
+local to_hex, from_hex = hex.encode, hex.decode;
 local saslprep = require "util.encodings".stringprep.saslprep;
 local secure_equals = require "util.hashes".equals;
 
--- a/plugins/mod_invites_register.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/plugins/mod_invites_register.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -4,7 +4,7 @@
 local rostermanager = require "core.rostermanager";
 
 local require_encryption = module:get_option_boolean("c2s_require_encryption",
-	module:get_option_boolean("require_encryption", false));
+	module:get_option_boolean("require_encryption", true));
 local invite_only = module:get_option_boolean("registration_invite_only", true);
 
 local invites;
--- a/plugins/mod_legacyauth.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/plugins/mod_legacyauth.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -12,7 +12,7 @@
 local t_concat = table.concat;
 
 local secure_auth_only = module:get_option("c2s_require_encryption",
-	module:get_option("require_encryption"))
+	module:get_option("require_encryption", true))
 	or not(module:get_option("allow_unencrypted_plain_auth"));
 
 local sessionmanager = require "core.sessionmanager";
--- a/plugins/mod_register_ibr.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/plugins/mod_register_ibr.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -18,7 +18,7 @@
 
 local additional_fields = module:get_option("additional_registration_fields", {});
 local require_encryption = module:get_option_boolean("c2s_require_encryption",
-	module:get_option_boolean("require_encryption", false));
+	module:get_option_boolean("require_encryption", true));
 
 pcall(function ()
 	module:depends("register_limits");
--- a/plugins/mod_s2s.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/plugins/mod_s2s.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -40,7 +40,7 @@
 local secure_auth = module:get_option_boolean("s2s_secure_auth", false); -- One day...
 local secure_domains, insecure_domains =
 	module:get_option_set("s2s_secure_domains", {})._items, module:get_option_set("s2s_insecure_domains", {})._items;
-local require_encryption = module:get_option_boolean("s2s_require_encryption", false);
+local require_encryption = module:get_option_boolean("s2s_require_encryption", true);
 local stanza_size_limit = module:get_option_number("s2s_stanza_size_limit", 1024*512);
 
 local measure_connections_inbound = module:metric(
--- a/plugins/mod_s2s_bidi.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/plugins/mod_s2s_bidi.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -10,7 +10,7 @@
 local xmlns_bidi_feature = "urn:xmpp:features:bidi"
 local xmlns_bidi = "urn:xmpp:bidi";
 
-local require_encryption = module:get_option_boolean("s2s_require_encryption", false);
+local require_encryption = module:get_option_boolean("s2s_require_encryption", true);
 
 module:hook("s2s-stream-features", function(event)
 	local origin, features = event.origin, event.features;
--- a/plugins/mod_saslauth.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/plugins/mod_saslauth.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -17,7 +17,7 @@
 
 local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler;
 
-local secure_auth_only = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", false));
+local secure_auth_only = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", true));
 local allow_unencrypted_plain_auth = module:get_option_boolean("allow_unencrypted_plain_auth", false)
 local insecure_mechanisms = module:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth and {} or {"PLAIN", "LOGIN"});
 local disabled_mechanisms = module:get_option_set("disable_sasl_mechanisms", { "DIGEST-MD5" });
@@ -309,7 +309,7 @@
 			return;
 		end
 
-		local authmod = module:get_option_string("authentication", "internal_plain");
+		local authmod = module:get_option_string("authentication", "internal_hashed");
 		if available_mechanisms:empty() then
 			log("warn", "No available SASL mechanisms, verify that the configured authentication module '%s' is loaded and configured correctly", authmod);
 			return;
--- a/plugins/mod_storage_xep0227.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/plugins/mod_storage_xep0227.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -68,11 +68,11 @@
 end
 
 local function hex_to_base64(s)
-	return base64.encode(hex.from(s));
+	return base64.encode(hex.decode(s));
 end
 
 local function base64_to_hex(s)
-	return base64.encode(hex.from(s));
+	return base64.encode(hex.decode(s));
 end
 
 local handlers = {};
--- a/plugins/mod_tls.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/plugins/mod_tls.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -10,8 +10,8 @@
 local rawgetopt = require"core.configmanager".rawget;
 local st = require "util.stanza";
 
-local c2s_require_encryption = module:get_option("c2s_require_encryption", module:get_option("require_encryption"));
-local s2s_require_encryption = module:get_option("s2s_require_encryption");
+local c2s_require_encryption = module:get_option("c2s_require_encryption", module:get_option("require_encryption", true));
+local s2s_require_encryption = module:get_option("s2s_require_encryption", true);
 local allow_s2s_tls = module:get_option("s2s_allow_encryption") ~= false;
 local s2s_secure_auth = module:get_option("s2s_secure_auth");
 
--- a/prosody.cfg.lua.dist	Wed Mar 02 16:12:28 2022 +0000
+++ b/prosody.cfg.lua.dist	Fri Mar 04 16:33:41 2022 +0000
@@ -23,67 +23,67 @@
 -- Example: admins = { "user1@example.com", "user2@example.net" }
 admins = { }
 
--- Prosody includes several alternative modules for keeping track of network connections.
--- For more information see: https://prosody.im/doc/network_backend
---network_backend = "epoll"
-
--- Prosody will always look in its source directory for modules, but
--- this option allows you to specify additional locations where Prosody
--- will look for modules first. For community modules, see https://modules.prosody.im/
+-- This option allows you to specify additional locations where Prosody
+-- will search first for modules. For additional modules you can install, see
+-- the community module repository at https://modules.prosody.im/
 --plugin_paths = {}
 
--- Single directory for custom prosody plugins and/or Lua libraries installation
--- This path takes priority over plugin_paths, when prosody is searching for modules
---installer_plugin_path = ""
-
 -- This is the list of modules Prosody will load on startup.
--- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too.
 -- Documentation for bundled modules can be found at: https://prosody.im/doc/modules
 modules_enabled = {
 
 	-- Generally required
+		"disco"; -- Service discovery
 		"roster"; -- Allow users to have a roster. Recommended ;)
 		"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
 		"tls"; -- Add support for secure TLS on c2s/s2s connections
-		"dialback"; -- s2s dialback support
-		"disco"; -- Service discovery
 
 	-- Not essential, but recommended
-		"carbons"; -- Keep multiple clients in sync
-		"pep"; -- Enables users to publish their avatar, mood, activity, playing music and more
-		"private"; -- Private XML storage (for room bookmarks, etc.)
 		"blocklist"; -- Allow users to block communications with other users
+		"bookmarks"; -- Synchronise the list of open rooms between clients
+		"carbons"; -- Keep multiple online clients in sync
+		"dialback"; -- Support for verifying remote servers using DNS
+		"limits"; -- Enable bandwidth limiting for XMPP connections
+		"pep"; -- Allow users to store public and private data in their account
+		"private"; -- Legacy account storage mechanism (XEP-0049)
+		"smacks"; -- Stream management and resumption (XEP-0198)
 		"vcard4"; -- User profiles (stored in PEP)
 		"vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard
-		"limits"; -- Enable bandwidth limiting for XMPP connections
 
 	-- Nice to have
-		"version"; -- Replies to server version requests
-		"uptime"; -- Report how long server has been running
-		"time"; -- Let others know the time here on this server
+		"csi_simple"; -- Simple but effective traffic optimizations for mobile devices
+		"invites"; -- Create and manage invites
+		"invites_adhoc"; -- Allow admins/users to create invitations via their client
+		"invites_register"; -- Allows invited users to create accounts
 		"ping"; -- Replies to XMPP pings with pongs
 		"register"; -- Allow users to register on this server using a client and change passwords
-		--"mam"; -- Store messages in an archive and allow users to access it
-		--"csi_simple"; -- Simple Mobile optimizations
+		"time"; -- Let others know the time here on this server
+		"uptime"; -- Report how long server has been running
+		"version"; -- Replies to server version requests
+		--"mam"; -- Store recent messages to allow multi-device synchronization
+		--"turn_external"; -- Provide external STUN/TURN service for e.g. audio/video calls
 
 	-- Admin interfaces
 		"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
-		--"admin_telnet"; -- Opens telnet console interface on localhost port 5582
+		"admin_shell"; -- Allow secure administration via 'prosodyctl shell'
 
 	-- HTTP modules
 		--"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
+		--"http_openmetrics"; -- for exposing metrics to stats collectors
 		--"websocket"; -- XMPP over WebSockets
-		--"http_files"; -- Serve static files from a directory over HTTP
 
 	-- Other specific functionality
+		--"announce"; -- Send announcement to all online users
 		--"groups"; -- Shared roster support
+		--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
+		--"mimicking"; -- Prevent address spoofing
+		--"motd"; -- Send a message to users when they log in
+		--"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use
+		--"s2s_bidi"; -- Bi-directional server-to-server (XEP-0288)
 		--"server_contact_info"; -- Publish contact information for this service
-		--"announce"; -- Send announcement to all online users
+		--"tombstones"; -- Prevent registration of deleted accounts
+		--"watchregistrations"; -- Alert admins of registrations
 		--"welcome"; -- Welcome users who register accounts
-		--"watchregistrations"; -- Alert admins of registrations
-		--"motd"; -- Send a message to users when they log in
-		--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
-		--"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use
 }
 
 -- These modules are auto-loaded, but should you want
@@ -95,28 +95,17 @@
 	-- "posix"; -- POSIX functionality, sends server to background, etc.
 }
 
--- Disable account creation by default, for security
--- For more information see https://prosody.im/doc/creating_accounts
-allow_registration = false
-
--- Force clients to use encrypted connections? This option will
--- prevent clients from authenticating unless they are using encryption.
-
-c2s_require_encryption = true
 
--- Force servers to use encrypted connections? This option will
--- prevent servers from authenticating unless they are using encryption.
+-- Server-to-server authentication
+-- Require valid certificates for server-to-server connections?
+-- If false, other methods such as dialback (DNS) may be used instead.
 
-s2s_require_encryption = true
-
--- Force certificate authentication for server-to-server connections?
-
-s2s_secure_auth = false
+s2s_secure_auth = true
 
 -- Some servers have invalid or self-signed certificates. You can list
 -- remote domains here that will not be required to authenticate using
--- certificates. They will be authenticated using DNS instead, even
--- when s2s_secure_auth is enabled.
+-- certificates. They will be authenticated using other methods instead,
+-- even when s2s_secure_auth is enabled.
 
 --s2s_insecure_domains = { "insecure.example" }
 
@@ -125,7 +114,10 @@
 
 --s2s_secure_domains = { "jabber.org" }
 
--- Enable rate limits for incoming client and server connections
+
+-- Rate limits
+-- Enable rate limits for incoming client and server connections. These help
+-- protect from excessive resource consumption and denial-of-service attacks.
 
 limits = {
 	c2s = {
@@ -136,11 +128,19 @@
 	};
 }
 
+-- Authentication
 -- Select the authentication backend to use. The 'internal' providers
 -- use Prosody's configured data storage to store the authentication data.
+-- For more information see https://prosody.im/doc/authentication
 
 authentication = "internal_hashed"
 
+-- Many authentication providers, including the default one, allow you to
+-- create user accounts via Prosody's admin interfaces. For details, see the
+-- documentation at https://prosody.im/doc/creating_accounts
+
+
+-- Storage
 -- Select the storage backend to use. By default Prosody uses flat files
 -- in its configured data directory, but it also supports more backends
 -- through modules. An "sql" backend is included by default, but requires
@@ -165,19 +165,36 @@
 -- You can also configure messages to be stored in-memory only. For more
 -- archiving options, see https://prosody.im/doc/modules/mod_mam
 
+
+-- Audio/video call relay (STUN/TURN)
+-- To ensure clients connected to the server can establish connections for
+-- low-latency media streaming (such as audio and video calls), it is
+-- recommended to run a STUN/TURN server for clients to use. If you do this,
+-- specify the details here so clients can discover it.
+-- Find more information at https://prosody.im/doc/turn
+
+-- Specify the address of the TURN service (you may use the same domain as XMPP)
+--turn_external_host = "turn.example.com"
+
+-- This secret must be set to the same value in both Prosody and the TURN server
+--turn_external_secret = "your-secret-turn-access-token"
+
+
 -- Logging configuration
 -- For advanced logging see https://prosody.im/doc/logging
 log = {
 	info = "prosody.log"; -- Change 'info' to 'debug' for verbose logging
 	error = "prosody.err";
 	-- "*syslog"; -- Uncomment this for logging to syslog
-	-- "*console"; -- Log to the console, useful for debugging with daemonize=false
+	-- "*console"; -- Log to the console, useful for debugging when running in the foreground
 }
 
+
 -- Uncomment to enable statistics
 -- For more info see https://prosody.im/doc/statistics
 -- statistics = "internal"
 
+
 -- Certificates
 -- Every virtual host and component needs a certificate so that clients and
 -- servers can securely verify its identity. Prosody will automatically load
@@ -188,17 +205,16 @@
 -- Location of directory to find certificates in (relative to main config file):
 certificates = "certs"
 
--- HTTPS currently only supports a single certificate, specify it here:
---https_certificate = "certs/localhost.crt"
-
 ----------- Virtual hosts -----------
 -- You need to add a VirtualHost entry for each domain you wish Prosody to serve.
 -- Settings under each VirtualHost entry apply *only* to that host.
 
 VirtualHost "localhost"
+-- Prosody requires at least one enabled VirtualHost to function. You can
+-- safely remove or disable 'localhost' once you have added another.
+
 
 --VirtualHost "example.com"
---	certificate = "/path/to/example.crt"
 
 ------ Components ------
 -- You can specify components to add hosts that provide special services,
@@ -210,10 +226,13 @@
 --- Store MUC messages in an archive and allow users to access it
 --modules_enabled = { "muc_mam" }
 
+---Set up a file sharing component
+--Component "share.example.com" "http_file_share"
+
 ---Set up an external component (default component port is 5347)
 --
 -- External components allow adding various services, such as gateways/
--- transports to other networks like ICQ, MSN and Yahoo. For more info
+-- bridges to non-XMPP networks and services. For more info
 -- see: https://prosody.im/doc/components#adding_an_external_component
 --
 --Component "gateway.example.com"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/spec/net_stun_spec.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -0,0 +1,100 @@
+local hex = require "util.hex";
+
+local function parse(pkt_desc)
+	local result = {};
+	for line in pkt_desc:gmatch("([^\n]+)\n") do
+		local b1, b2, b3, b4 = line:match("^%s*(%x%x) (%x%x) (%x%x) (%x%x)%s");
+		if b1 then
+			table.insert(result, b1);
+			table.insert(result, b2);
+			table.insert(result, b3);
+			table.insert(result, b4);
+		end
+	end
+	return hex.decode(table.concat(result));
+end
+
+local sample_packet = parse[[
+      00 01 00 60     Request type and message length
+      21 12 a4 42     Magic cookie
+      78 ad 34 33  }
+      c6 ad 72 c0  }  Transaction ID
+      29 da 41 2e  }
+      00 06 00 12     USERNAME attribute header
+      e3 83 9e e3  }
+      83 88 e3 83  }
+      aa e3 83 83  }  Username value (18 bytes) and padding (2 bytes)
+      e3 82 af e3  }
+      82 b9 00 00  }
+      00 15 00 1c     NONCE attribute header
+      66 2f 2f 34  }
+      39 39 6b 39  }
+      35 34 64 36  }
+      4f 4c 33 34  }  Nonce value
+      6f 4c 39 46  }
+      53 54 76 79  }
+      36 34 73 41  }
+      00 14 00 0b     REALM attribute header
+      65 78 61 6d  }
+      70 6c 65 2e  }  Realm value (11 bytes) and padding (1 byte)
+      6f 72 67 00  }
+      00 08 00 14     MESSAGE-INTEGRITY attribute header
+      f6 70 24 65  }
+      6d d6 4a 3e  }
+      02 b8 e0 71  }  HMAC-SHA1 fingerprint
+      2e 85 c9 a2  }
+      8c a8 96 66  }
+]];
+
+--print(hex.encode(sample_packet))
+print(sample_packet)
+
+describe("net.stun", function ()
+	local stun = require "net.stun";
+
+	it("works", function ()
+		local packet = stun.new_packet();
+		assert.is_string(packet:serialize());
+	end);
+
+	it("can decode the sample packet", function ()
+		local packet = stun.new_packet():deserialize(sample_packet);
+		assert(packet);
+		local method, method_name = packet:get_method();
+		assert.equal(1, method);
+		assert.equal("binding", method_name);
+		assert.equal("example.org", packet:get_attribute("realm"));
+	end);
+
+	it("can generate the sample packet", function ()
+		-- These values, and the sample packet, come from RFC 5769 2.4
+		local username = string.char(
+			-- U+30DE KATAKANA LETTER MA
+			0xE3, 0x83, 0x9E,
+			-- U+30C8 KATAKANA LETTER TO
+			0xE3, 0x83, 0x88,
+			-- U+30EA KATAKANA LETTER RI
+			0xE3, 0x83, 0xAA,
+			-- U+30C3 KATAKANA LETTER SMALL TU
+			0xE3, 0x83, 0x83,
+			-- U+30AF KATAKANA LETTER KU
+			0xE3, 0x82, 0xAF,
+			-- U+30B9 KATAKANA LETTER SU
+			0xE3, 0x82, 0xB9
+		);
+
+		--    Password:  "The<U+00AD>M<U+00AA>tr<U+2168>" and "TheMatrIX" (without
+		--       quotes) respectively before and after SASLprep processing
+		local password = "TheMatrIX";
+		local realm = "example.org";
+
+		local p3 = stun.new_packet("binding", "request");
+		p3.transaction_id = hex.decode("78AD3433C6AD72C029DA412E");
+		p3:add_attribute("username", username);
+		p3:add_attribute("nonce", "f//499k954d6OL34oL9FSTvy64sA");
+		p3:add_attribute("realm", realm);
+		local key = stun.get_long_term_auth_key(realm, username, password);
+		p3:add_message_integrity(key);
+		assert.equal(sample_packet, p3:serialize());
+	end);
+end);
--- a/spec/util_hashes_spec.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/spec/util_hashes_spec.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -10,28 +10,28 @@
 		local S = "salt"
 		local c = 1
 		local DK = "0c60c80f961f0e71f3a9b524af6012062fe037a6";
-		assert.equal(DK, hex.to(hashes.pbkdf2_hmac_sha1(P, S, c)));
+		assert.equal(DK, hex.encode(hashes.pbkdf2_hmac_sha1(P, S, c)));
 	end);
 	it("test vector 2", function ()
 		local P = "password"
 		local S = "salt"
 		local c = 2
 		local DK = "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957";
-		assert.equal(DK, hex.to(hashes.pbkdf2_hmac_sha1(P, S, c)));
+		assert.equal(DK, hex.encode(hashes.pbkdf2_hmac_sha1(P, S, c)));
 	end);
 	it("test vector 3", function ()
 		local P = "password"
 		local S = "salt"
 		local c = 4096
 		local DK = "4b007901b765489abead49d926f721d065a429c1";
-		assert.equal(DK, hex.to(hashes.pbkdf2_hmac_sha1(P, S, c)));
+		assert.equal(DK, hex.encode(hashes.pbkdf2_hmac_sha1(P, S, c)));
 	end);
 	it("test vector 4 #SLOW", function ()
 		local P = "password"
 		local S = "salt"
 		local c = 16777216
 		local DK = "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984";
-		assert.equal(DK, hex.to(hashes.pbkdf2_hmac_sha1(P, S, c)));
+		assert.equal(DK, hex.encode(hashes.pbkdf2_hmac_sha1(P, S, c)));
 	end);
 end);
 
@@ -41,14 +41,14 @@
 		local S = "salt";
 		local c = 1
 		local DK = "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b";
-		assert.equal(DK, hex.to(hashes.pbkdf2_hmac_sha256(P, S, c)));
+		assert.equal(DK, hex.encode(hashes.pbkdf2_hmac_sha256(P, S, c)));
 	end);
 	it("test vector 2", function ()
 		local P = "password";
 		local S = "salt";
 		local c = 2
 		local DK = "ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43";
-		assert.equal(DK, hex.to(hashes.pbkdf2_hmac_sha256(P, S, c)));
+		assert.equal(DK, hex.encode(hashes.pbkdf2_hmac_sha256(P, S, c)));
 	end);
 end);
 
--- a/spec/util_hmac_spec.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/spec/util_hmac_spec.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -7,8 +7,8 @@
 local hex = require "util.hex";
 
 describe("Test case 1", function ()
-	local Key  = hex.from("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
-	local Data = hex.from("4869205468657265");
+	local Key  = hex.decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+	local Data = hex.decode("4869205468657265");
 	describe("HMAC-SHA-256", function ()
 		it("works", function()
 			assert.equal("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", hmac.sha256(Key, Data, true))
@@ -21,8 +21,8 @@
 	end);
 end);
 describe("Test case 2", function ()
-	local Key  = hex.from("4a656665");
-	local Data = hex.from("7768617420646f2079612077616e7420666f72206e6f7468696e673f");
+	local Key  = hex.decode("4a656665");
+	local Data = hex.decode("7768617420646f2079612077616e7420666f72206e6f7468696e673f");
 	describe("HMAC-SHA-256", function ()
 		it("works", function()
 			assert.equal("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", hmac.sha256(Key, Data, true))
@@ -35,8 +35,8 @@
 	end);
 end);
 describe("Test case 3", function ()
-	local Key  = hex.from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
-	local Data = hex.from("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd");
+	local Key  = hex.decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+	local Data = hex.decode("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd");
 	describe("HMAC-SHA-256", function ()
 		it("works", function()
 			assert.equal("773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe", hmac.sha256(Key, Data, true))
@@ -49,8 +49,8 @@
 	end);
 end);
 describe("Test case 4", function ()
-	local Key  = hex.from("0102030405060708090a0b0c0d0e0f10111213141516171819");
-	local Data = hex.from("cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd");
+	local Key  = hex.decode("0102030405060708090a0b0c0d0e0f10111213141516171819");
+	local Data = hex.decode("cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd");
 	describe("HMAC-SHA-256", function ()
 		it("works", function()
 			assert.equal("82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b", hmac.sha256(Key, Data, true))
@@ -63,8 +63,8 @@
 	end);
 end);
 describe("Test case 5", function ()
-	local Key  = hex.from("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c");
-	local Data = hex.from("546573742057697468205472756e636174696f6e");
+	local Key  = hex.decode("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c");
+	local Data = hex.decode("546573742057697468205472756e636174696f6e");
 	describe("HMAC-SHA-256", function ()
 		it("works", function()
 			assert.equal("a3b6167473100ee06e0c796c2955552b", hmac.sha256(Key, Data, true):sub(1,128/4))
@@ -77,8 +77,8 @@
 	end);
 end);
 describe("Test case 6", function ()
-	local Key  = hex.from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
-	local Data = hex.from("54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a65204b6579202d2048617368204b6579204669727374");
+	local Key  = hex.decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+	local Data = hex.decode("54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a65204b6579202d2048617368204b6579204669727374");
 	describe("HMAC-SHA-256", function ()
 		it("works", function()
 			assert.equal("60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54", hmac.sha256(Key, Data, true))
@@ -91,8 +91,8 @@
 	end);
 end);
 describe("Test case 7", function ()
-	local Key  = hex.from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
-	local Data = hex.from("5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e");
+	local Key  = hex.decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+	local Data = hex.decode("5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e");
 	describe("HMAC-SHA-256", function ()
 		it("works", function()
 			assert.equal("9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2", hmac.sha256(Key, Data, true))
--- a/util-src/GNUmakefile	Wed Mar 02 16:12:28 2022 +0000
+++ b/util-src/GNUmakefile	Fri Mar 04 16:33:41 2022 +0000
@@ -7,7 +7,8 @@
 TARGET?=../util/
 
 ALL=encodings.so hashes.so net.so pposix.so signal.so table.so \
-    ringbuffer.so time.so poll.so compat.so strbitop.so
+    ringbuffer.so time.so poll.so compat.so strbitop.so \
+    struct.so
 
 ifdef RANDOM
 ALL+=crand.so
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util-src/struct.c	Fri Mar 04 16:33:41 2022 +0000
@@ -0,0 +1,422 @@
+/*
+** {======================================================
+** Library for packing/unpacking structures.
+** $Id: struct.c,v 1.8 2018/05/16 11:00:23 roberto Exp $
+** See Copyright Notice at the end of this file
+** =======================================================
+*/
+/*
+** Valid formats:
+** > - big endian
+** < - little endian
+** ![num] - alignment
+** x - pading
+** b/B - signed/unsigned byte
+** h/H - signed/unsigned short
+** l/L - signed/unsigned long
+** T   - size_t
+** i/In - signed/unsigned integer with size 'n' (default is size of int)
+** cn - sequence of 'n' chars (from/to a string); when packing, n==0 means
+        the whole string; when unpacking, n==0 means use the previous
+        read number as the string length
+** s - zero-terminated string
+** f - float
+** d - double
+** ' ' - ignored
+*/
+
+
+#include <ctype.h>
+#include <limits.h>
+#include <stddef.h>
+#include <string.h>
+
+
+#include "lua.h"
+#include "lauxlib.h"
+
+
+#if (LUA_VERSION_NUM >= 502)
+
+#define luaL_register(L,n,f)	luaL_newlib(L,f)
+
+#endif
+
+
+/* basic integer type */
+#if !defined(STRUCT_INT)
+#define STRUCT_INT	long
+#endif
+
+typedef STRUCT_INT Inttype;
+
+/* corresponding unsigned version */
+typedef unsigned STRUCT_INT Uinttype;
+
+
+/* maximum size (in bytes) for integral types */
+#define MAXINTSIZE	32
+
+/* is 'x' a power of 2? */
+#define isp2(x)		((x) > 0 && ((x) & ((x) - 1)) == 0)
+
+/* dummy structure to get alignment requirements */
+struct cD {
+  char c;
+  double d;
+};
+
+
+#define PADDING		(sizeof(struct cD) - sizeof(double))
+#define MAXALIGN  	(PADDING > sizeof(int) ? PADDING : sizeof(int))
+
+
+/* endian options */
+#define BIG	0
+#define LITTLE	1
+
+
+static union {
+  int dummy;
+  char endian;
+} const native = {1};
+
+
+typedef struct Header {
+  int endian;
+  int align;
+} Header;
+
+
+static int getnum (const char **fmt, int df) {
+  if (!isdigit(**fmt))  /* no number? */
+    return df;  /* return default value */
+  else {
+    int a = 0;
+    do {
+      a = a*10 + *((*fmt)++) - '0';
+    } while (isdigit(**fmt));
+    return a;
+  }
+}
+
+
+#define defaultoptions(h)	((h)->endian = native.endian, (h)->align = 1)
+
+
+
+static size_t optsize (lua_State *L, char opt, const char **fmt) {
+  switch (opt) {
+    case 'B': case 'b': return sizeof(char);
+    case 'H': case 'h': return sizeof(short);
+    case 'L': case 'l': return sizeof(long);
+    case 'T': return sizeof(size_t);
+    case 'f':  return sizeof(float);
+    case 'd':  return sizeof(double);
+    case 'x': return 1;
+    case 'c': return getnum(fmt, 1);
+    case 'i': case 'I': {
+      int sz = getnum(fmt, sizeof(int));
+      if (sz > MAXINTSIZE)
+        luaL_error(L, "integral size %d is larger than limit of %d",
+                       sz, MAXINTSIZE);
+      return sz;
+    }
+    default: return 0;  /* other cases do not need alignment */
+  }
+}
+
+
+/*
+** return number of bytes needed to align an element of size 'size'
+** at current position 'len'
+*/
+static int gettoalign (size_t len, Header *h, int opt, size_t size) {
+  if (size == 0 || opt == 'c') return 0;
+  if (size > (size_t)h->align)
+    size = h->align;  /* respect max. alignment */
+  return (size - (len & (size - 1))) & (size - 1);
+}
+
+
+/*
+** options to control endianess and alignment
+*/
+static void controloptions (lua_State *L, int opt, const char **fmt,
+                            Header *h) {
+  switch (opt) {
+    case  ' ': return;  /* ignore white spaces */
+    case '>': h->endian = BIG; return;
+    case '<': h->endian = LITTLE; return;
+    case '!': {
+      int a = getnum(fmt, MAXALIGN);
+      if (!isp2(a))
+        luaL_error(L, "alignment %d is not a power of 2", a);
+      h->align = a;
+      return;
+    }
+    default: {
+      const char *msg = lua_pushfstring(L, "invalid format option '%c'", opt);
+      luaL_argerror(L, 1, msg);
+    }
+  }
+}
+
+
+static void putinteger (lua_State *L, luaL_Buffer *b, int arg, int endian,
+                        int size) {
+  lua_Number n = luaL_checknumber(L, arg);
+  Uinttype value;
+  char buff[MAXINTSIZE];
+  if (n < 0)
+    value = (Uinttype)(Inttype)n;
+  else
+    value = (Uinttype)n;
+  if (endian == LITTLE) {
+    int i;
+    for (i = 0; i < size; i++) {
+      buff[i] = (value & 0xff);
+      value >>= 8;
+    }
+  }
+  else {
+    int i;
+    for (i = size - 1; i >= 0; i--) {
+      buff[i] = (value & 0xff);
+      value >>= 8;
+    }
+  }
+  luaL_addlstring(b, buff, size);
+}
+
+
+static void correctbytes (char *b, int size, int endian) {
+  if (endian != native.endian) {
+    int i = 0;
+    while (i < --size) {
+      char temp = b[i];
+      b[i++] = b[size];
+      b[size] = temp;
+    }
+  }
+}
+
+
+static int b_pack (lua_State *L) {
+  luaL_Buffer b;
+  const char *fmt = luaL_checkstring(L, 1);
+  Header h;
+  int arg = 2;
+  size_t totalsize = 0;
+  defaultoptions(&h);
+  lua_pushnil(L);  /* mark to separate arguments from string buffer */
+  luaL_buffinit(L, &b);
+  while (*fmt != '\0') {
+    int opt = *fmt++;
+    size_t size = optsize(L, opt, &fmt);
+    int toalign = gettoalign(totalsize, &h, opt, size);
+    totalsize += toalign;
+    while (toalign-- > 0) luaL_addchar(&b, '\0');
+    switch (opt) {
+      case 'b': case 'B': case 'h': case 'H':
+      case 'l': case 'L': case 'T': case 'i': case 'I': {  /* integer types */
+        putinteger(L, &b, arg++, h.endian, size);
+        break;
+      }
+      case 'x': {
+        luaL_addchar(&b, '\0');
+        break;
+      }
+      case 'f': {
+        float f = (float)luaL_checknumber(L, arg++);
+        correctbytes((char *)&f, size, h.endian);
+        luaL_addlstring(&b, (char *)&f, size);
+        break;
+      }
+      case 'd': {
+        double d = luaL_checknumber(L, arg++);
+        correctbytes((char *)&d, size, h.endian);
+        luaL_addlstring(&b, (char *)&d, size);
+        break;
+      }
+      case 'c': case 's': {
+        size_t l;
+        const char *s = luaL_checklstring(L, arg++, &l);
+        if (size == 0) size = l;
+        luaL_argcheck(L, l >= (size_t)size, arg, "string too short");
+        luaL_addlstring(&b, s, size);
+        if (opt == 's') {
+          luaL_addchar(&b, '\0');  /* add zero at the end */
+          size++;
+        }
+        break;
+      }
+      default: controloptions(L, opt, &fmt, &h);
+    }
+    totalsize += size;
+  }
+  luaL_pushresult(&b);
+  return 1;
+}
+
+
+static lua_Number getinteger (const char *buff, int endian,
+                        int issigned, int size) {
+  Uinttype l = 0;
+  int i;
+  if (endian == BIG) {
+    for (i = 0; i < size; i++) {
+      l <<= 8;
+      l |= (Uinttype)(unsigned char)buff[i];
+    }
+  }
+  else {
+    for (i = size - 1; i >= 0; i--) {
+      l <<= 8;
+      l |= (Uinttype)(unsigned char)buff[i];
+    }
+  }
+  if (!issigned)
+    return (lua_Number)l;
+  else {  /* signed format */
+    Uinttype mask = (Uinttype)(~((Uinttype)0)) << (size*8 - 1);
+    if (l & mask)  /* negative value? */
+      l |= mask;  /* signal extension */
+    return (lua_Number)(Inttype)l;
+  }
+}
+
+
+static int b_unpack (lua_State *L) {
+  Header h;
+  const char *fmt = luaL_checkstring(L, 1);
+  size_t ld;
+  const char *data = luaL_checklstring(L, 2, &ld);
+  size_t pos = (size_t)luaL_optinteger(L, 3, 1) - 1;
+  int n = 0;  /* number of results */
+  luaL_argcheck(L, pos <= ld, 3, "initial position out of string");
+  defaultoptions(&h);
+  while (*fmt) {
+    int opt = *fmt++;
+    size_t size = optsize(L, opt, &fmt);
+    pos += gettoalign(pos, &h, opt, size);
+    luaL_argcheck(L, size <= ld - pos, 2, "data string too short");
+    /* stack space for item + next position */
+    luaL_checkstack(L, 2, "too many results");
+    switch (opt) {
+      case 'b': case 'B': case 'h': case 'H':
+      case 'l': case 'L': case 'T': case 'i':  case 'I': {  /* integer types */
+        int issigned = islower(opt);
+        lua_Number res = getinteger(data+pos, h.endian, issigned, size);
+        lua_pushnumber(L, res); n++;
+        break;
+      }
+      case 'x': {
+        break;
+      }
+      case 'f': {
+        float f;
+        memcpy(&f, data+pos, size);
+        correctbytes((char *)&f, sizeof(f), h.endian);
+        lua_pushnumber(L, f); n++;
+        break;
+      }
+      case 'd': {
+        double d;
+        memcpy(&d, data+pos, size);
+        correctbytes((char *)&d, sizeof(d), h.endian);
+        lua_pushnumber(L, d); n++;
+        break;
+      }
+      case 'c': {
+        if (size == 0) {
+          if (n == 0 || !lua_isnumber(L, -1))
+            luaL_error(L, "format 'c0' needs a previous size");
+          size = lua_tonumber(L, -1);
+          lua_pop(L, 1); n--;
+          luaL_argcheck(L, size <= ld - pos, 2, "data string too short");
+        }
+        lua_pushlstring(L, data+pos, size); n++;
+        break;
+      }
+      case 's': {
+        const char *e = (const char *)memchr(data+pos, '\0', ld - pos);
+        if (e == NULL)
+          luaL_error(L, "unfinished string in data");
+        size = (e - (data+pos)) + 1;
+        lua_pushlstring(L, data+pos, size - 1); n++;
+        break;
+      }
+      default: controloptions(L, opt, &fmt, &h);
+    }
+    pos += size;
+  }
+  lua_pushinteger(L, pos + 1);  /* next position */
+  return n + 1;
+}
+
+
+static int b_size (lua_State *L) {
+  Header h;
+  const char *fmt = luaL_checkstring(L, 1);
+  size_t pos = 0;
+  defaultoptions(&h);
+  while (*fmt) {
+    int opt = *fmt++;
+    size_t size = optsize(L, opt, &fmt);
+    pos += gettoalign(pos, &h, opt, size);
+    if (opt == 's')
+      luaL_argerror(L, 1, "option 's' has no fixed size");
+    else if (opt == 'c' && size == 0)
+      luaL_argerror(L, 1, "option 'c0' has no fixed size");
+    if (!isalnum(opt))
+      controloptions(L, opt, &fmt, &h);
+    pos += size;
+  }
+  lua_pushinteger(L, pos);
+  return 1;
+}
+
+/* }====================================================== */
+
+
+
+static const struct luaL_Reg thislib[] = {
+  {"pack", b_pack},
+  {"unpack", b_unpack},
+  {"size", b_size},
+  {NULL, NULL}
+};
+
+
+LUALIB_API int luaopen_util_struct (lua_State *L);
+
+LUALIB_API int luaopen_util_struct (lua_State *L) {
+  luaL_register(L, "struct", thislib);
+  return 1;
+}
+
+
+/******************************************************************************
+* Copyright (C) 2010-2018 Lua.org, PUC-Rio.  All rights reserved.
+*
+* Permission is hereby granted, free of charge, to any person obtaining
+* a copy of this software and associated documentation files (the
+* "Software"), to deal in the Software without restriction, including
+* without limitation the rights to use, copy, modify, merge, publish,
+* distribute, sublicense, and/or sell copies of the Software, and to
+* permit persons to whom the Software is furnished to do so, subject to
+* the following conditions:
+*
+* The above copyright notice and this permission notice shall be
+* included in all copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+******************************************************************************/
+
--- a/util/bit53.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/util/bit53.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -3,5 +3,7 @@
 	band   = function (a, b) return a & b end;
 	bor    = function (a, b) return a | b end;
 	bxor   = function (a, b) return a ~ b end;
+	rshift = function (a, n) return a >> n end;
+	lshift = function (a, n) return a << n end;
 };
 
--- a/util/dependencies.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/util/dependencies.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -85,7 +85,7 @@
 				{ "Debian/Ubuntu", "sudo apt install lua-sec" };
 				{ "luarocks", "luarocks install luasec" };
 				{ "Source", "https://github.com/brunoos/luasec" };
-			}, "SSL/TLS support will not be available", err);
+			}, nil, err);
 	end
 
 	local bit, err = softreq"util.bitcompat";
--- a/util/dns.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/util/dns.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -13,7 +13,7 @@
 local s_sub = string.sub;
 
 local iana_data = require "util.dnsregistry";
-local tohex = require "util.hex".to;
+local tohex = require "util.hex".encode;
 local inet_ntop = require "util.net".ntop;
 
 -- Simplified versions of Waqas DNS parsers
--- a/util/hex.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/util/hex.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -23,4 +23,8 @@
 	return (s_gsub(s_lower(s), "%X*(%x%x)%X*", hex_to_char));
 end
 
-return { to = to, from = from }
+return {
+	encode = to, decode = from;
+	-- COMPAT w/pre-0.12:
+	to = to, from = from;
+};
--- a/util/ip.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/util/ip.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -67,7 +67,7 @@
 end
 
 function ip_methods.bits(ip)
-	return hex.to(ip.packed):upper():gsub(".", hex2bits);
+	return hex.encode(ip.packed):upper():gsub(".", hex2bits);
 end
 
 function ip_methods.bits_full(ip)
--- a/util/prosodyctl/check.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/util/prosodyctl/check.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -60,6 +60,108 @@
 	return false, "Probe endpoint did not return a success status";
 end
 
+local function check_turn_service(turn_service)
+	local stun = require "net.stun";
+
+	-- Create UDP socket for communication with the server
+	local sock = assert(require "socket".udp());
+	sock:setsockname("*", 0);
+	sock:setpeername(turn_service.host, turn_service.port);
+	sock:settimeout(10);
+
+	-- Helper function to receive a packet
+	local function receive_packet()
+		local raw_packet, err = sock:receive();
+		if not raw_packet then
+			return nil, err;
+		end
+		return stun.new_packet():deserialize(raw_packet);
+	end
+
+	local result = { warnings = {} };
+
+	-- Send a "binding" query, i.e. a request for our external IP/port
+	local bind_query = stun.new_packet("binding", "request");
+	bind_query:add_attribute("software", "prosodyctl check turn");
+	sock:send(bind_query:serialize());
+
+	local bind_result, err = receive_packet();
+	if not bind_result then
+		result.error = "No STUN response: "..err;
+		return result;
+	elseif bind_result:is_err_resp() then
+		result.error = ("STUN server returned error: %d (%s)"):format(bind_result:get_error());
+		return result;
+	elseif not bind_result:is_success_resp() then
+		result.error = ("Unexpected STUN response: %d (%s)"):format(bind_result:get_type());
+		return result;
+	end
+
+	result.external_ip = bind_result:get_xor_mapped_address();
+	if not result.external_ip then
+		result.error = "STUN server did not return an address";
+		return result;
+	end
+
+	-- Send a TURN "allocate" request. Expected to fail due to auth, but
+	-- necessary to obtain a valid realm/nonce from the server.
+	local pre_request = stun.new_packet("allocate", "request");
+	sock:send(pre_request:serialize());
+
+	local pre_result, err = receive_packet();
+	if not pre_result then
+		result.error = "No initial TURN response: "..err;
+		return result;
+	elseif pre_result:is_success_resp() then
+		result.error = "TURN server does not have authentication enabled";
+		return result;
+	end
+
+	local realm = pre_result:get_attribute("realm");
+	local nonce = pre_result:get_attribute("nonce");
+
+	if not realm then
+		table.insert(result.warnings, "TURN server did not return an authentication realm");
+	end
+	if not nonce then
+		table.insert(result.warnings, "TURN server did not return a nonce");
+	end
+
+	-- Use the configured secret to obtain temporary user/pass credentials
+	local turn_user, turn_pass = stun.get_user_pass_from_secret(turn_service.secret);
+
+	-- Send a TURN allocate request, will fail if auth is wrong
+	local alloc_request = stun.new_packet("allocate", "request");
+	alloc_request:add_requested_transport("udp");
+	alloc_request:add_attribute("username", turn_user);
+	if realm then
+		alloc_request:add_attribute("realm", realm);
+	end
+	if nonce then
+		alloc_request:add_attribute("nonce", nonce);
+	end
+	local key = stun.get_long_term_auth_key(realm or turn_service.host, turn_user, turn_pass);
+	alloc_request:add_message_integrity(key);
+	sock:send(alloc_request:serialize());
+
+	-- Check the response
+	local alloc_response, err = receive_packet();
+	if not alloc_response then
+		result.error = "TURN server did not response to allocation request: "..err;
+		return;
+	elseif alloc_response:is_err_resp() then
+		result.error = ("TURN allocation failed: %d (%s)"):format(alloc_response:get_error());
+		return result;
+	elseif not alloc_response:is_success_resp() then
+		result.error = ("Unexpected TURN response: %d (%s)"):format(alloc_response:get_type());
+		return result;
+	end
+
+	-- No errors? Ok!
+
+	return result;
+end
+
 local function skip_bare_jid_hosts(host)
 	if jid_split(host) then
 		-- See issue #779
@@ -80,8 +182,8 @@
 	local ok = true;
 	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
-	if not (what == nil or what == "disabled" or what == "config" or what == "dns" or what == "certs" or what == "connectivity") then
-		show_warning("Don't know how to check '%s'. Try one of 'config', 'dns', 'certs', 'disabled' or 'connectivity'.", what);
+	if not (what == nil or what == "disabled" or what == "config" or what == "dns" or what == "certs" or what == "connectivity" or what == "turn") then
+		show_warning("Don't know how to check '%s'. Try one of 'config', 'dns', 'certs', 'disabled', 'turn' or 'connectivity'.", what);
 		show_warning("Note: The connectivity check will connect to a remote server.");
 		return 1;
 	end
@@ -1004,6 +1106,65 @@
 		print("Note: The connectivity check only checks the reachability of the domain.")
 		print("Note: It does not ensure that the check actually reaches this specific prosody instance.")
 	end
+
+	if what == "turn" then
+		local turn_enabled_hosts = {};
+		local turn_services = {};
+
+		for host in enabled_hosts() do
+			local has_external_turn = modulemanager.get_modules_for_host(host):contains("turn_external");
+			if has_external_turn then
+				table.insert(turn_enabled_hosts, host);
+				local turn_host = configmanager.get(host, "turn_external_host") or host;
+				local turn_port = configmanager.get(host, "turn_external_port") or 3478;
+				local turn_secret = configmanager.get(host, "turn_external_secret");
+				if not turn_secret then
+					print("Error: Your configuration is missing a turn_external_secret for "..host);
+					print("Error: TURN will not be advertised for this host.");
+					ok = false;
+				else
+					local turn_id = ("%s:%d"):format(turn_host, turn_port);
+					if turn_services[turn_id] and turn_services[turn_id].secret ~= turn_secret then
+						print("Error: Your configuration contains multiple differing secrets");
+						print("       for the TURN service at "..turn_id.." - we will only test one.");
+					elseif not turn_services[turn_id] then
+						turn_services[turn_id] = {
+							host = turn_host;
+							port = turn_port;
+							secret = turn_secret;
+						};
+					end
+				end
+			end
+		end
+
+		if what == "turn" then
+			local count = it.count(pairs(turn_services));
+			if count == 0 then
+				print("Error: Unable to find any TURN services configured. Enable mod_turn_external!");
+			else
+				print("Identified "..tostring(count).." TURN services.");
+				print("");
+			end
+		end
+
+		for turn_id, turn_service in pairs(turn_services) do
+			print("Testing "..turn_id.."...");
+
+			local result = check_turn_service(turn_service);
+			if #result.warnings > 0 then
+				print(("%d warnings:\n\n    "):format(#result.warnings));
+				print(table.concat(result.warnings, "\n    "));
+			end
+			if result.error then
+				print("Error: "..result.error.."\n");
+				ok = false;
+			else
+				print("Success!\n");
+			end
+		end
+	end
+
 	if not ok then
 		print("Problems found, see above.");
 	else
--- a/util/uuid.lua	Wed Mar 02 16:12:28 2022 +0000
+++ b/util/uuid.lua	Fri Mar 04 16:33:41 2022 +0000
@@ -8,7 +8,7 @@
 
 local random = require "util.random";
 local random_bytes = random.bytes;
-local hex = require "util.hex".to;
+local hex = require "util.hex".encode;
 local m_ceil = math.ceil;
 
 local function get_nibbles(n)