Changeset

5118:7bce75e74f86

Merge
author Kim Alvefur <zash@zash.se>
date Sun, 18 Dec 2022 15:30:02 +0100
parents 5114:d2a84e6aed2b (diff) 5117:2b94ee74d1f1 (current diff)
children 5119:048e339706ba
files mod_http_muc_log/mod_http_muc_log.lua mod_http_muc_log/static/style.css mod_http_muc_log/static/timestamps.js
diffstat 26 files changed, 421 insertions(+), 146 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_clean_roster/README.md	Sun Dec 18 15:30:02 2022 +0100
@@ -0,0 +1,5 @@
+Removes invalid characters from roster entries.
+
+```bash
+sudo prosodyctl mod_clean_roster
+```
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_clean_roster/mod_clean_roster.lua	Sun Dec 18 15:30:02 2022 +0100
@@ -0,0 +1,60 @@
+local s_find = string.find;
+
+local pctl = require "util.prosodyctl";
+
+local rostermanager = require "core.rostermanager";
+local storagemanager = require "core.storagemanager";
+local usermanager = require "core.usermanager";
+
+-- copypaste from util.stanza
+local function valid_xml_cdata(str, attr)
+	return not s_find(str, attr and "[^\1\9\10\13\20-~\128-\247]" or "[^\9\10\13\20-~\128-\247]");
+end
+
+function module.command(_arg)
+	if select(2, pctl.isrunning()) then
+		pctl.show_warning("Stop Prosody before running this command");
+		return 1;
+	end
+
+	for hostname, host in pairs(prosody.hosts) do
+		if hostname ~= "*" then
+			if host.users.name == "null" then
+				storagemanager.initialize_host(hostname);
+				usermanager.initialize_host(hostname);
+			end
+			local fixes = 0;
+			for username in host.users.users() do
+				local roster = rostermanager.load_roster(username, hostname);
+				local changed = false;
+				for contact, item in pairs(roster) do
+					if contact ~= false then
+						if item.name and not valid_xml_cdata(item.name, false) then
+							item.name = item.name:gsub("[^\9\10\13\20-~\128-\247]", "�");
+							fixes = fixes + 1;
+							changed = true;
+						end
+						local clean_groups = {};
+						for group in pairs(item.groups) do
+							if valid_xml_cdata(group, false) then
+								clean_groups[group] = true;
+							else
+								clean_groups[group:gsub("[^\9\10\13\20-~\128-\247]",  "�")] = true;
+								fixes = fixes + 1;
+								changed = true;
+							end
+						end
+						item.groups = clean_groups;
+					else
+						-- pending entries etc
+					end
+				end
+				if changed then
+					assert(rostermanager.save_roster(username, hostname, roster));
+				end
+			end
+			pctl.show_message("Fixed %d items on host %s", fixes, hostname);
+		end
+	end
+	return 0;
+end
--- a/mod_compat_roles/mod_compat_roles.lua	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_compat_roles/mod_compat_roles.lua	Sun Dec 18 15:30:02 2022 +0100
@@ -28,15 +28,26 @@
 	return get_jid_role_name(username.."@"..host, host);
 end
 
--- permissions[host][permission_name] = permitted_role_name
+-- permissions[host][role_name][permission_name] = is_permitted
 local permissions = {};
 
-local function role_may(role_name, permission)
-	local role_permissions = permissions[role_name];
+local role_inheritance = {
+	["prosody:operator"] = "prosody:admin";
+	["prosody:admin"] = "prosody:user";
+	["prosody:user"] = "prosody:restricted";
+};
+
+local function role_may(host, role_name, permission)
+	local host_roles = permissions[host];
+	if not host_roles then
+		return false;
+	end
+	local role_permissions = host_roles[role_name];
 	if not role_permissions then
 		return false;
 	end
-	return not not permissions[role_name][permission];
+	local next_role = role_inheritance[role_name];
+	return not not permissions[role_name][permission] or (next_role and role_may(host, next_role, permission));
 end
 
 function moduleapi.may(self, action, context)
@@ -56,7 +67,7 @@
 			return false;
 		end
 
-		local permit = role_may(role, action);
+		local permit = role_may(self.host, role, action);
 		if not permit then
 			self:log("debug", "Access denied: JID <%s> may not %s (not permitted by role %s)", context, action, role.name);
 		end
@@ -74,7 +85,7 @@
 			self:log("debug", "Access denied: JID <%s> may not %s (no role found)", actor_jid, action);
 			return false;
 		end
-		local permit = role_may(role_name, action, context);
+		local permit = role_may(self.host, role_name, action, context);
 		if not permit then
 			self:log("debug", "Access denied: JID <%s> may not %s (not permitted by role %s)", actor_jid, action, role_name);
 		end
@@ -83,10 +94,15 @@
 end
 
 function moduleapi.default_permission(self, role_name, permission)
-	local r = permissions[self.host][role_name];
+	local p = permissions[self.host];
+	if not p then
+		p = {};
+		permissions[self.host] = p;
+	end
+	local r = p[role_name];
 	if not r then
 		r = {};
-		permissions[self.host][role_name] = r;
+		p[role_name] = r;
 	end
 	r[permission] = true;
 end
--- a/mod_http_muc_log/README.markdown	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_http_muc_log/README.markdown	Sun Dec 18 15:30:02 2022 +0100
@@ -6,6 +6,7 @@
   build:
     copy_directories:
       - res
+      - static
 ...
 
 Introduction
--- a/mod_http_muc_log/mod_http_muc_log.lua	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_http_muc_log/mod_http_muc_log.lua	Sun Dec 18 15:30:02 2022 +0100
@@ -40,6 +40,8 @@
 	end
 end
 
+local resources = module:get_option_path(module.name .. "_resources", "static");
+
 -- local base_url = module:http_url() .. '/'; -- TODO: Generate links in a smart way
 local get_link do
 	local link, path = { path = '/' }, { "", "", is_directory = true };
@@ -248,6 +250,7 @@
 	response.headers.content_type = "text/html; charset=utf-8";
 	local room_obj = get_room(room);
 	return render(template, {
+		static = "../@static";
 		room = room_obj._data;
 		jid = room_obj.jid;
 		jid_node = jid_split(room_obj.jid);
@@ -467,6 +470,7 @@
 	response.headers.content_type = "text/html; charset=utf-8";
 	local room_obj = get_room(room);
 	return render(template, {
+		static = "../@static";
 		date = date;
 		room = room_obj._data;
 		jid = room_obj.jid;
@@ -517,6 +521,7 @@
 
 	response.headers.content_type = "text/html; charset=utf-8";
 	return render(template, {
+		static = "./@static";
 		title = module:get_option_string("name", "Prosody Chatrooms");
 		jid = module.host;
 		hide_presence = hide_presence(request);
@@ -526,6 +531,20 @@
 	});
 end
 
+local serve_static
+do
+	if prosody.process_type == "prosody" then
+		-- Prosody >= 0.12
+		local http_files = require "net.http.files";
+		serve = http_files.serve;
+	else
+		-- Prosody <= 0.11
+		serve = module:depends "http_files".serve;
+	end
+	local mime_map = module:shared("/*/http_files/mime").types or { css = "text/css"; js = "application/javascript" };
+	serve_static = serve({ path = resources; mime_map = mime_map });
+end
+
 module:provides("http", {
 	title = module:get_option_string("name", "Chatroom logs");
 	route = {
@@ -535,6 +554,10 @@
 		-- thus:
 		-- GET /room --> years_page (via logs_page)
 		-- GET /room/yyyy-mm-dd --> logs_page (for real)
+
+		["GET /@static/*"] = serve_static;
+		-- There are not many ASCII characters that are safe to use in URLs but not
+		-- valid in JID localparts, '@' seemed the only option.
 	};
 });
 
--- a/mod_http_muc_log/res/http_muc_log.html	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_http_muc_log/res/http_muc_log.html	Sun Dec 18 15:30:02 2022 +0100
@@ -5,59 +5,7 @@
 <meta name="viewport" content="width=device-width, initial-scale=1">
 {date&<meta name="dcterms.date" content="{date}">}
 <title>{title?{room.name?{jid_node}}{date& - {date}}}</title>
-<style>
-:link,:visited{color:#3465a4;text-decoration:none;}
-:link:hover,:visited:hover{color:#6197df;}
-body{background-color:#eeeeec;margin:1ex 0;padding-bottom:3em;font-family:Arial,Helvetica,sans-serif;}
-ul,ol{padding:0;}
-li{list-style:none;}
-hr{visibility:hidden;clear:both;}
-br{clear:both;}
-header,footer{margin:1ex 1em;}
-footer{font-size:smaller;color:#babdb6;}
-nav{font-size:large;margin:1ex 1ex;clear:both;line-height:1.5em;}
-footer nav .up{display:none;}
-@media screen and (min-width: 460px) {
-nav {font-size:x-large;margin:1ex 1em;}
-}
-nav a{padding:1ex}
-nav li,nav dt{margin:1ex}
-nav .up{font-size:smaller;display:block;clear:both;}
-nav .up::before{content:"↑ ";}
-nav .prev{float:left;}
-nav .next{float:right;}
-nav .next::after{content:" →";}
-nav .prev::before{content:"← ";}
-nav .last::after{content:" ⇥";}
-nav :empty::after,nav :empty::before{content:""}
-table{display:inline-block; margin:1ex 1em;vertical-align:top;}
-th{font-size:x-small}
-td{text-align:right;color:#bababa}
-td > a, td > span{padding:0.4em}
-.content{background-color:white;padding:1em;list-style-position:inside;}
-.time{float:right;font-size:small;opacity:0.2;}
-li:hover .time{opacity:1;}
-.description{font-size:smaller;}
-.body{white-space:pre-line;}
-.body::before,.body::after{content:"";}
-.presence .verb{font-style:normal;color:#30c030;}
-.unavailable .verb{color:#c03030;}
-.button{display:inline-block}
-.button>a{color:white;background-color:orange;border-radius:4px}
-.reaction{font-size:smaller;outline:1px solid silver;border-radius:2px}
-form{text-align:right}
-li.edited{display:none}
-li:target{outline:1px gray dotted;display:inherit}
-figure img{max-height:9em;max-width:16em}
-@media (prefers-color-scheme: dark) {
-html{color:#eee}
-body{background-color:#161616}
-.content{background-color:#1c1c1c}
-footer{color:#444}
-td{color:#444}
-.button>a{background-color:#282828}
-}
-</style>
+<link rel="stylesheet" type="text/css" href="{static}/style.css">
 </head>
 <body>
 <header>
@@ -110,11 +58,11 @@
 </div>
 
 <ol class="chat-logs">{lines#
-<li {item.lang&lang="{item.lang}"} class="{item.st_name} {item.st_type?} {item.edited&edited}" id="{item.archive_id}">
-<a class="time" href="#{item.archive_id}"><time id="{item.time}" datetime="{item.datetime}">{item.time}</time></a>
+<li class="{item.st_name} {item.st_type?} {item.edited&edited}" id="{item.archive_id}">
 <b class="nick">{item.nick}</b>
 <em class="verb">{item.verb?}</em>
-<q class="body">{item.edited&<del>}{item.body?}{item.edited&</del> <a href="#{item.edited}" title="jump to corrected version">&#9998;</a>}{item.edit& <a href="#{item.edit}" title="jump to previous version">&#9999;</a>}{item.reply& <a href="#{item.reply}" title="jump to message responded to">&#8634;</a>}</q>
+<a class="time" href="#{item.archive_id}"><time id="{item.time}" datetime="{item.datetime}">{item.time}</time></a>
+<p {item.lang&lang="{item.lang}"} class="body">{item.edited&<del>}{item.body?}{item.edited&</del> <a href="#{item.edited}" title="jump to corrected version">&#9998;</a>}{item.edit& <a href="#{item.edit}" title="jump to previous version">&#9999;</a>}{item.reply& <a href="#{item.reply}" title="jump to message responded to">&#8634;</a>}</p>
 {item.reactions%<span class="reaction">{idx} {item}</span>}
 {item.oob.url&<figure><a rel="nofollow" href="{item.oob.url?}"><img alt="{item.oob.desc?}" src="{item.oob.url?}"/></a><figcaption>{item.oob.desc?}</figcaption></figure>}
 </li>}
@@ -130,25 +78,6 @@
 <br>
 <div class="powered-by">Prosody</div>
 </footer>
-<script>
-/*
-* Local timestamps
-*/
-(function () {
-var timeTags = document.getElementsByTagName("time");
-var i = 0, tag, date;
-while(timeTags[i]) {
-tag = timeTags[i++];
-if(date = tag.getAttribute("datetime")) {
-date = new Date(date);
-tag.textContent = date.toLocaleTimeString(navigator.language);
-tag.setAttribute("title", date.toString());
-}
-}
-document.forms[0].elements.p.addEventListener("change", function() {
-document.forms[0].submit();
-});
-})();
-</script>
+<script defer type="application/javascript" src="{static}/timestamps.js"></script>
 </body>
 </html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_http_muc_log/static/style.css	Sun Dec 18 15:30:02 2022 +0100
@@ -0,0 +1,81 @@
+@charset "UTF-8";
+/* Style for mod_http_muc_log */
+:link, :visited { color: #3465a4; text-decoration: none; }
+
+:link:hover, :link:hover, :visited:hover, :visited:hover { color: #6197df; }
+
+body { background-color: #eeeeec; margin: 1ex 0; padding-bottom: 3em; font-family: Arial,Helvetica,sans-serif; }
+
+ul, ol { padding: 0; }
+
+li { list-style: none; }
+
+li.edited { display: none; }
+
+li:target { outline: 1px gray dotted; display: inherit; }
+
+hr { visibility: hidden; clear: both; }
+
+br { clear: both; }
+
+header, footer { margin: 1ex 1em; }
+
+footer { font-size: smaller; color: #babdb6; }
+
+footer nav .up { display: none; }
+
+nav { font-size: large; margin: 1ex 1ex; clear: both; line-height: 1.5em; }
+
+nav a { padding: 1ex; }
+
+nav li, nav dt { margin: 1ex; }
+
+nav .up { font-size: smaller; display: block; clear: both; }
+
+nav .up::before { content: "↑ "; }
+
+nav .prev { float: left; }
+
+nav .prev::before { content: "← "; }
+
+nav .next { float: right; }
+
+nav .next::after { content: " →"; }
+
+nav .last::after { content: " ⇥"; }
+
+nav :empty::after, nav :empty::before { content: ""; }
+
+@media screen and (min-width: 460px) { nav { font-size: x-large; margin: 1ex 1em; } }
+
+table { display: inline-block; margin: 1ex 1em; vertical-align: top; }
+
+th { font-size: x-small; }
+
+td { text-align: right; color: #bababa; }
+
+td > a, td > span { padding: 0.4em; }
+
+.content { background-color: white; padding: 1em; list-style-position: inside; }
+
+.time { margin-left: 1em; font-size: small; }
+
+.description { font-size: smaller; }
+
+.body { white-space: pre-line; margin: 1pt 0 1ex; }
+
+.presence .verb { font-style: normal; color: #30c030; }
+
+.unavailable .verb { color: #c03030; }
+
+.button { display: inline-block; }
+
+.button > a { color: white; background-color: orange; border-radius: 4px; }
+
+.reaction { font-size: smaller; outline: 1px solid silver; border-radius: 2px; }
+
+form { text-align: right; }
+
+figure img { max-height: 9em; max-width: 16em; }
+
+@media (prefers-color-scheme: dark) { html { color: #eee; } body { background-color: #161616; } .content { background-color: #1c1c1c; } footer { color: #444; } td { color: #444; } .button > a { background-color: #282828; } }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_http_muc_log/static/timestamps.js	Sun Dec 18 15:30:02 2022 +0100
@@ -0,0 +1,21 @@
+/*
+* Local timestamps
+*/
+(function () {
+var timeTags = document.getElementsByTagName("time");
+var i = 0, tag, date;
+while(timeTags[i]) {
+tag = timeTags[i++];
+if(date = tag.getAttribute("datetime")) {
+date = new Date(date);
+tag.textContent = date.toLocaleTimeString(navigator.language);
+tag.setAttribute("title", date.toString());
+}
+}
+if(document.forms.length>0){
+document.forms[0].elements.p.addEventListener("change", function() {
+document.forms[0].submit();
+});
+}
+})();
+
--- a/mod_isolate_host/mod_isolate_host.lua	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_isolate_host/mod_isolate_host.lua	Sun Dec 18 15:30:02 2022 +0100
@@ -39,7 +39,7 @@
 function check_user_isolated(event)
 	local session = event.session;
 	local bare_jid = jid_bare(session.full_jid);
-	if module:may("xmpp:federate") or except_users:contains(bare_jid) then
+	if module:may("xmpp:federate", event) or except_users:contains(bare_jid) then
 		session.no_host_isolation = true;
 	end
 	module:log("debug", "%s is %sisolated", session.full_jid or "[?]", session.no_host_isolation and "" or "not ");
--- a/mod_password_reset/README.markdown	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_password_reset/README.markdown	Sun Dec 18 15:30:02 2022 +0100
@@ -2,6 +2,10 @@
 labels:
 - 'Stage-Alpha'
 summary: 'Enables users to reset their password via a link'
+rockspec:
+  build:
+    copy_directories:
+    - password_reset
 ...
 
 Introduction
--- a/mod_pubsub_feeds/README.markdown	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_pubsub_feeds/README.markdown	Sun Dec 18 15:30:02 2022 +0100
@@ -1,5 +1,9 @@
 ---
 summary: Subscribe to Atom and RSS feeds over pubsub
+rockspec:
+  build:
+    modules:
+      pubsub_feeds.feeds: feeds.lib.lua
 ---
 
 # Introduction
--- a/mod_pubsub_mqtt/mod_pubsub_mqtt.lua	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_pubsub_mqtt/mod_pubsub_mqtt.lua	Sun Dec 18 15:30:02 2022 +0100
@@ -1,14 +1,52 @@
 module:set_global();
 
 local mqtt = module:require "mqtt";
+local id = require "util.id";
 local st = require "util.stanza";
 
+local function tostring_content(item)
+	return tostring(item[1]);
+end
+
+local data_translators = setmetatable({
+	utf8 = {
+		from_item = function (item)
+			return item:find("{https://prosody.im/protocol/data}data#");
+		end;
+		to_item = function (payload)
+			return st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub", id = id.medium() })
+				:text_tag("data", payload, { xmlns = "https://prosody.im/protocol/data" })
+		end;
+	};
+	json = {
+		from_item = function (item)
+			return item:find("{urn:xmpp:json:0}json#");
+		end;
+		to_item = function (payload)
+			return st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub", id = id.medium() })
+				:text_tag("json", payload, { xmlns = "urn:xmpp:json:0" });
+		end;
+	};
+	atom_title = {
+		from_item = function (item)
+			return item:find("{http://www.w3.org/2005/Atom}entry/title#");
+		end;
+		to_item = function (payload)
+			return st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub", id = id.medium() })
+				:tag("entry", { xmlns = "http://www.w3.org/2005/Atom" })
+					:text_tag("title", payload, { type = "text" });
+		end;
+	};
+}, {
+	__index = function () return { from_item = tostring }; end;
+});
+
 local pubsub_services = {};
 local pubsub_subscribers = {};
 local packet_handlers = {};
 
 function handle_packet(session, packet)
-	module:log("warn", "MQTT packet received! Length: %d", packet.length);
+	module:log("debug", "MQTT packet received! Length: %d", packet.length);
 	for k,v in pairs(packet) do
 		module:log("debug", "MQTT %s: %s", tostring(k), tostring(v));
 	end
@@ -32,18 +70,26 @@
 end
 
 function packet_handlers.publish(session, packet)
-	module:log("warn", "PUBLISH to %s", packet.topic);
-	local host, node = packet.topic:match("^([^/]+)/(.+)$");
+	module:log("info", "PUBLISH to %s", packet.topic);
+	local host, payload_type, node = packet.topic:match("^([^/]+)/([^/]+)/(.+)$");
+	if not host then
+		module:log("warn", "Invalid topic format - expected: HOST/TYPE/NODE");
+		return;
+	end
 	local pubsub = pubsub_services[host];
 	if not pubsub then
 		module:log("warn", "Unable to locate host/node: %s", packet.topic);
 		return;
 	end
-	local id = "mqtt";
-	local ok, err = pubsub:publish(node, true, id,
-		st.stanza("data", { xmlns = "https://prosody.im/protocol/data" })
-			:text(packet.data)
-	);
+
+	local payload_translator = data_translators[payload_type];
+	if not payload_translator or not payload_translator.to_item then
+		module:log("warn", "Unsupported payload type '%s' on topic '%s'", payload_type, packet.topic);
+		return;
+	end
+
+	local payload_item = payload_translator.to_item(packet.data);
+	local ok, err = pubsub:publish(node, true, payload_item.attr.id, payload_item);
 	if not ok then
 		module:log("warn", "Error publishing MQTT data: %s", tostring(err));
 	end
@@ -51,8 +97,12 @@
 
 function packet_handlers.subscribe(session, packet)
 	for _, topic in ipairs(packet.topics) do
-		module:log("warn", "SUBSCRIBE to %s", topic);
-		local host, node = topic:match("^([^/]+)/(.+)$");
+		module:log("info", "SUBSCRIBE to %s", topic);
+		local host, payload_type, node = topic:match("^([^/]+)/([^/]+)/(.+)$");
+		if not host then
+			module:log("warn", "Invalid topic format - expected: HOST/TYPE/NODE");
+			return;
+		end
 		local pubsub = pubsub_subscribers[host];
 		if not pubsub then
 			module:log("warn", "Unable to locate host/node: %s", topic);
@@ -63,8 +113,8 @@
 			node_subs = {};
 			pubsub[node] = node_subs;
 		end
-		session.subscriptions[topic] = true;
-		node_subs[session] = true;
+		session.subscriptions[topic] = payload_type;
+		node_subs[session] = payload_type;
 	end
 
 end
@@ -116,17 +166,6 @@
 	listener = mqtt_listener;
 });
 
-local function tostring_content(item)
-	return tostring(item[1]);
-end
-
-local data_translators = setmetatable({
-	["data https://prosody.im/protocol/data"] = tostring_content;
-	["json urn:xmpp:json:0"] = tostring_content;
-}, {
-	__index = function () return tostring; end;
-});
-
 function module.add_host(module)
 	local pubsub_module = hosts[module.host].modules.pubsub
 	if pubsub_module then
@@ -137,16 +176,22 @@
 		pubsub_subscribers[module.host] = subscribers;
 		local function handle_publish(event)
 			-- Build MQTT packet
-			local packet = mqtt.serialize_packet{
-				type = "publish";
-				id = "\000\000";
-				topic = module.host.."/"..event.node;
-				data = data_translators[tostring(event.item.name).." "..tostring(event.item.attr.xmlns)](event.item);
-			};
+			local packet_types = setmetatable({}, {
+				__index = function (self, payload_type)
+					local packet = mqtt.serialize_packet{
+						type = "publish";
+						id = "\000\000";
+						topic = module.host.."/"..payload_type.."/"..event.node;
+						data = data_translators[payload_type].from_item(event.item) or "";
+					};
+					rawset(self, packet);
+					return packet;
+				end;
+			});
 			-- Broadcast to subscribers
-			module:log("debug", "Broadcasting PUBLISH to subscribers of %s/%s", module.host, event.node);
-			for session in pairs(subscribers[event.node] or {}) do
-				session.conn:write(packet);
+			module:log("debug", "Broadcasting PUBLISH to subscribers of %s/*/%s", module.host, event.node);
+			for session, payload_type in pairs(subscribers[event.node] or {}) do
+				session.conn:write(packet_types[payload_type]);
 				module:log("debug", "Sent to %s", tostring(session));
 			end
 		end
--- a/mod_pubsub_mqtt/mqtt.lib.lua	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_pubsub_mqtt/mqtt.lib.lua	Sun Dec 18 15:30:02 2022 +0100
@@ -54,7 +54,7 @@
 	until bit.band(digit, 0x80) == 0;
 	packet.length = length;
 	if packet.type == "connect" then
-		if self:read_string() ~= "MQIsdp" then
+		if self:read_string() ~= "MQTT" then
 			module:log("warn", "Unexpected packet signature!");
 			packet.type = nil; -- Invalid packet
 		else
--- a/mod_rest/mod_rest.lua	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_rest/mod_rest.lua	Sun Dec 18 15:30:02 2022 +0100
@@ -461,9 +461,7 @@
 		};
 	});
 
--- Forward stanzas from XMPP to HTTP and return any reply
-local rest_url = module:get_option_string("rest_callback_url", nil);
-if rest_url then
+function new_webhook(rest_url, send_type)
 	local function get_url() return rest_url; end
 	if rest_url:find("%b{}") then
 		local httputil = require "util.http";
@@ -473,7 +471,6 @@
 			return render_url(rest_url, { kind = stanza.name, type = at.type, to = at.to, from = at.from });
 		end
 	end
-	local send_type = module:get_option_string("rest_callback_content_type", "application/xmpp+xml");
 	if send_type == "json" then
 		send_type = "application/json";
 	end
@@ -500,7 +497,7 @@
 
 	local function handle_stanza(event)
 		local stanza, origin = event.stanza, event.origin;
-		local reply_allowed = stanza.attr.type ~= "error";
+		local reply_allowed = stanza.attr.type ~= "error" and stanza.attr.type ~= "result";
 		local reply_needed = reply_allowed and stanza.name == "iq";
 		local receipt;
 
@@ -601,6 +598,16 @@
 		return true;
 	end
 
+	return handle_stanza;
+end
+
+-- Forward stanzas from XMPP to HTTP and return any reply
+local rest_url = module:get_option_string("rest_callback_url", nil);
+if rest_url then
+	local send_type = module:get_option_string("rest_callback_content_type", "application/xmpp+xml");
+
+	local handle_stanza = new_webhook(rest_url, send_type);
+
 	local send_kinds = module:get_option_set("rest_callback_stanzas", { "message", "presence", "iq" });
 
 	local event_presets = {
--- a/mod_rest/res/openapi.yaml	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_rest/res/openapi.yaml	Sun Dec 18 15:30:02 2022 +0100
@@ -1096,6 +1096,7 @@
       description: Message ID of a message that has been displayed
       xml:
         namespace: urn:xmpp:chat-markers:0
+        x_single_attribute: id
 
     idle_since:
       type: string
--- a/mod_rest/res/schema-xmpp.json	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_rest/res/schema-xmpp.json	Sun Dec 18 15:30:02 2022 +0100
@@ -733,7 +733,8 @@
                "title" : "XEP-0333: Chat Markers",
                "type" : "string",
                "xml" : {
-                  "namespace" : "urn:xmpp:chat-markers:0"
+                  "namespace" : "urn:xmpp:chat-markers:0",
+                  "x_single_attribute" : "id"
                }
             },
             "encryption" : {
--- a/mod_s2soutinjection/mod_s2soutinjection.lua	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_s2soutinjection/mod_s2soutinjection.lua	Sun Dec 18 15:30:02 2022 +0100
@@ -1,9 +1,7 @@
 local st = require"util.stanza";
-local new_ip = require"util.ip".new_ip;
 local new_outgoing = require"core.s2smanager".new_outgoing;
 local bounce_sendq = module:depends"s2s".route_to_new_session.bounce_sendq;
 local initialize_filters = require "util.filters".initialize;
-local st = require "util.stanza";
 
 local portmanager = require "core.portmanager";
 
@@ -17,7 +15,7 @@
 
 -- The proxy_listener handles connection while still connecting to the proxy,
 -- then it hands them over to the normal listener (in mod_s2s)
-local proxy_listener = { default_port = port, default_mode = "*a", default_interface = "*" };
+local proxy_listener = { default_port = nil, default_mode = "*a", default_interface = "*" };
 
 function proxy_listener.onconnect(conn)
 	local session = sessions[conn];
@@ -25,7 +23,7 @@
 	-- Now the real s2s listener can take over the connection.
 	local listener = portmanager.get_service("s2s").listener;
 
-	local w, log = conn.send, session.log;
+	local log = session.log;
 
 	local filter = initialize_filters(session);
 
@@ -71,13 +69,13 @@
 	local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza;
 	local inject = injected and injected[to_host];
 	if not inject then return end
-	log("debug", "opening a new outgoing connection for this stanza");
+	module:log("debug", "opening a new outgoing connection for this stanza");
 	local host_session = new_outgoing(from_host, to_host);
 
 	-- Store in buffer
 	host_session.bounce_sendq = bounce_sendq;
 	host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };
-	log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name));
+	host_session.log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name));
 
 	local host, port = inject[1] or inject, tonumber(inject[2]) or 5269;
 
--- a/mod_sasl2/README.md	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_sasl2/README.md	Sun Dec 18 15:30:02 2022 +0100
@@ -1,10 +1,18 @@
 ---
 labels:
-- Stage-Alpha
+- Stage-Beta
 summary: "XEP-0388: Extensible SASL Profile"
 ---
 
-Experimental implementation of [XEP-0388: Extensible SASL Profile]
+Implementation of [XEP-0388: Extensible SASL Profile]. **Note: At the time of
+writing (Nov 2022) the version of the XEP implemented by this module is still
+working its way through the XSF standards process. See [PR #1214](https://github.com/xsf/xeps/pull/1214)
+for the current status.**
+
+## Configuration
+
+This module honours the same configuration options as Prosody's existing
+[mod_saslauth](https://prosody.im/doc/modules/mod_saslauth).
 
 ## Developers
 
--- a/mod_sasl2/mod_sasl2.lua	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_sasl2/mod_sasl2.lua	Sun Dec 18 15:30:02 2022 +0100
@@ -18,6 +18,7 @@
 
 local xmlns_sasl2 = "urn:xmpp:sasl:2";
 
+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" });
@@ -44,6 +45,9 @@
 	if origin.type ~= "c2s_unauthed" then
 		log("debug", "Already authenticated");
 		return
+	elseif secure_auth_only and not origin.secure then
+		log("debug", "Not offering authentication on insecure connection");
+		return;
 	end
 
 	local sasl_handler = usermanager_get_sasl_handler(host, origin)
@@ -187,6 +191,9 @@
 end
 
 module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth)
+	if secure_auth_only and not session.secure then
+		return handle_status(session, "failure", "encryption-required");
+	end
 	local sasl_handler = session.sasl_handler;
 	if not sasl_handler then
 		sasl_handler = usermanager_get_sasl_handler(host, session);
--- a/mod_sasl2_bind2/README.md	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_sasl2_bind2/README.md	Sun Dec 18 15:30:02 2022 +0100
@@ -1,7 +1,16 @@
 ---
 labels:
-- Stage-Alpha
+- Stage-Beta
 summary: "Bind 2 integration with SASL2"
+rockspec:
+  dependencies:
+  - mod_sasl2
 ---
 
-Add support for inlining Bind 2.0 into the SASL2 process. Experimental WIP.
+Add support for [XEP-0386: Bind 2], which is a new method for clients to bind
+resources and establish sessions in XMPP, using SASL2. **Note: At the time of
+writing (November 2022), this plugin implements a version of XEP-0386 that is
+still working its way through the XSF standards process. See [PR #1217](https://github.com/xsf/xeps/pull/1217)
+for more information and current status.**
+
+This module depends on [mod_sasl2]. It exposes no configuration options.
--- a/mod_sasl2_bind2/mod_sasl2_bind2.lua	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_sasl2_bind2/mod_sasl2_bind2.lua	Sun Dec 18 15:30:02 2022 +0100
@@ -8,6 +8,8 @@
 local xmlns_bind2 = "urn:xmpp:bind:0";
 local xmlns_sasl2 = "urn:xmpp:sasl:2";
 
+module:depends("sasl2");
+
 -- Advertise what we can do
 
 module:hook("advertise-sasl-features", function(event)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_sasl2_fast/README.md	Sun Dec 18 15:30:02 2022 +0100
@@ -0,0 +1,34 @@
+---
+labels:
+- Stage-Beta
+summary: "Fast Authentication Streamlining Tokens"
+rockspec:
+  dependencies:
+  - mod_sasl2
+---
+
+This module implements a mechanism via which clients can exchange a password
+for a secure token, improving security and streamlining future reconnections.
+
+At the time of writing, the XEP that describes the FAST protocol is still
+working its way through the XSF standards process. You can [view the FAST XEP
+proposal here](https://xmpp.org/extensions/inbox/xep-fast.html).
+
+This module depends on [mod_sasl2].
+
+## Configuration
+
+| Name                      | Description                                            | Default               |
+|---------------------------|--------------------------------------------------------|-----------------------|
+| sasl2_fast_token_ttl      | Default token expiry (seconds)                         | `86400*21` (21 days)  |
+| sasl2_fast_token_min_ttl  | Time before tokens are eligible for rotation (seconds) | `86400` (1 day)       |
+
+The `sasl2_fast_token_ttl` option determines the length of time a client can
+remain disconnected before being "logged out" and needing to authenticate with
+a password. Clients must perform at least one FAST authentication within this
+period to remain active.
+
+The `sasl2_fast_token_min_ttl` option defines how long before a token will be
+rotated by the server. By default a token is rotated if it is older than 24
+hours. This value should be less than `sasl2_fast_token_ttl` to prevent
+clients being logged out unexpectedly.
--- a/mod_sasl2_fast/mod_sasl2_fast.lua	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_sasl2_fast/mod_sasl2_fast.lua	Sun Dec 18 15:30:02 2022 +0100
@@ -6,6 +6,8 @@
 local now = require "util.time".now;
 local hash = require "util.hashes";
 
+module:depends("sasl2");
+
 -- Tokens expire after 21 days by default
 local fast_token_ttl = module:get_option_number("sasl2_fast_token_ttl", 86400*21);
 -- Tokens are automatically rotated daily
@@ -47,6 +49,7 @@
 				if hash.equals(expected_hash, token_hash) then
 					local current_time = now();
 					if token.expires_at < current_time then
+						log("debug", "Token found, but it has expired (%ds ago). Cleaning up...", current_time - token.expires_at);
 						token_store:set(username, key, nil);
 						return nil, "credentials-expired";
 					end
@@ -61,9 +64,10 @@
 					if invalidate then
 						token_store:set(username, key, nil);
 					elseif current_time - token.issued_at > fast_token_min_ttl then
+						log("debug", "FAST token due for rotation (age: %d)", current_time - token.issued_at);
 						rotation_needed = true;
 					end
-					return true, username, hmac_f(token.secret, "Responder"..cb_data), token, rotation_needed;
+					return true, username, hmac_f(token.secret, "Responder"..cb_data), rotation_needed;
 				end
 			end
 			if not tried_current_token then
@@ -98,6 +102,8 @@
 	end
 	local sasl_handler = get_sasl_handler(username);
 	if not sasl_handler then return; end
+	sasl_handler.profile.cb = session.sasl_handler.profile.cb;
+	sasl_handler.userdata = session.sasl_handler.userdata;
 	session.fast_sasl_handler = sasl_handler;
 	local fast = st.stanza("fast", { xmlns = xmlns_fast });
 	for mech in pairs(sasl_handler:mechanisms()) do
@@ -150,7 +156,7 @@
 	local token_request = session.fast_token_request;
 	local client_id = session.client_id;
 	local sasl_handler = session.sasl_handler;
-	if token_request or sasl_handler.fast and sasl_handler.rotation_needed then
+	if token_request or (sasl_handler.fast and sasl_handler.rotation_needed) then
 		if not client_id then
 			session.log("warn", "FAST token requested, but missing client id");
 			return;
@@ -173,23 +179,24 @@
 local function new_ht_mechanism(mechanism_name, backend_profile_name, cb_name)
 	return function (sasl_handler, message)
 		local backend = sasl_handler.profile[backend_profile_name];
-		local username, token_hash = message:match("^([^%z]+)%z(.+)$");
-		if not username then
+		local authc_username, token_hash = message:match("^([^%z]+)%z(.+)$");
+		if not authc_username then
 			return "failure", "malformed-request";
 		end
 		local cb_data = cb_name and sasl_handler.profile.cb[cb_name](sasl_handler) or "";
-		local ok, status, response, rotation_needed = backend(
+		local ok, authz_username, response, rotation_needed = backend(
 			mechanism_name,
-			username,
+			authc_username,
 			sasl_handler.client_id,
 			token_hash,
 			cb_data,
 			sasl_handler.invalidate
 		);
 		if not ok then
-			return "failure", status or "not-authorized";
+			-- authz_username is error condition
+			return "failure", authz_username or "not-authorized";
 		end
-		sasl_handler.username = status;
+		sasl_handler.username = authz_username;
 		sasl_handler.rotation_needed = rotation_needed;
 		return "success", response;
 	end
@@ -201,10 +208,10 @@
 		backend_profile_name,
 		cb_name
 	),
-	{ cb_name });
+	cb_name and { cb_name } or nil);
 end
 
 register_ht_mechanism("HT-SHA-256-NONE", "ht_sha_256", nil);
 register_ht_mechanism("HT-SHA-256-UNIQ", "ht_sha_256", "tls-unique");
-register_ht_mechanism("HT-SHA-256-ENDP", "ht_sha_256", "tls-endpoint");
+register_ht_mechanism("HT-SHA-256-ENDP", "ht_sha_256", "tls-server-end-point");
 register_ht_mechanism("HT-SHA-256-EXPR", "ht_sha_256", "tls-exporter");
--- a/mod_sasl2_sm/README.md	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_sasl2_sm/README.md	Sun Dec 18 15:30:02 2022 +0100
@@ -1,7 +1,17 @@
 ---
 labels:
-- Stage-Alpha
+- Stage-Beta
 summary: "XEP-0198 integration with SASL2"
+rockspec:
+  dependencies:
+  - mod_sasl2
 ---
 
 Add support for inlining stream management negotiation into the SASL2 process.
+
+**Note: At the time of writing (November 2022), this module implements a
+version of XEP-0198 that is still working its way through the XSF standards
+process. For more information and current status, see [PR #1215](https://github.com/xsf/xeps/pull/1215).**
+
+This module depends on [mod_sasl2] and [mod_sasl2_bind2]. It exposes no
+configuration options.
--- a/mod_sasl2_sm/mod_sasl2_sm.lua	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_sasl2_sm/mod_sasl2_sm.lua	Sun Dec 18 15:30:02 2022 +0100
@@ -5,6 +5,8 @@
 local xmlns_sasl2 = "urn:xmpp:sasl:2";
 local xmlns_sm = "urn:xmpp:sm:3";
 
+module:depends("sasl2");
+
 -- Advertise what we can do
 
 module:hook("advertise-sasl-features", function (event)
--- a/mod_vjud/README.markdown	Sat Dec 17 14:13:06 2022 +0100
+++ b/mod_vjud/README.markdown	Sun Dec 18 15:30:02 2022 +0100
@@ -40,7 +40,7 @@
 
   Option       Default    Description
   ------------ ---------- --------------------------------
-  vjud\_mode   "opt-in"   Defines how the module behaves
+  vjud\_mode   "opt-in"   Choose how users are listed in the directory ("opt-in" or "all")
 
 Compatibility
 =============