view mod_remote_roster/mod_remote_roster.lua @ 6319:04c3273cb81f

mod_auth_cyrus: Add empty 'profile' table to SASL handler objects This is for compatibility with Prosody's built-in util.sasl objects. A SASL profile table usually includes methods supported by the backend, which can be used by SASL mechanism handlers to perform operations (such as testing the password). It also optionally contains a 'cb' field with channel binding method handlers. The Cyrus backend doesn't support channel binding, and doesn't have the same concept of auth backend methods (it handles all that internally, and Prosody has no insight or control over it). Thus, we create an empty profile which informs Prosody that the SASL handler does not support any of the auth or channel binding methods. Some features will not work, but they didn't work anyway. This just makes it explicit. This fixes a traceback in mod_sasl2_fast, which expected SASL handlers to always contain a 'profile' field.
author Matthew Wild <mwild1@gmail.com>
date Thu, 04 Sep 2025 10:14:46 +0100
parents ad6e9b0fd15b
children
line wrap: on
line source

--
-- mod_remote_roster
--
-- This is an experimental implementation of http://jkaluza.fedorapeople.org/remote-roster.html
--

local st = require "util.stanza";
local jid_split = require "util.jid".split;
local jid_prep = require "util.jid".prep;
local t_concat = table.concat;
local tonumber = tonumber;
local pairs, ipairs = pairs, ipairs;
local hosts = hosts;

local load_roster = require "core.rostermanager".load_roster;
local save_roster = require "core.rostermanager".save_roster;
local rm_remove_from_roster = require "core.rostermanager".remove_from_roster;
local rm_add_to_roster = require "core.rostermanager".add_to_roster;
local rm_roster_push = require "core.rostermanager".roster_push;
local user_exists = require "core.usermanager".user_exists;
local add_task = require "util.timer".add_task;
local new_id = require "util.id".short;

module:hook("iq-get/bare/jabber:iq:roster:query", function(event)
	local origin, stanza = event.origin, event.stanza;
	
	if origin.type == "component" and stanza.attr.from == origin.host then
		local node, host = jid_split(stanza.attr.to);
		local roster = load_roster(node, host);
		
		local reply = st.reply(stanza):query("jabber:iq:roster");
		for jid, item in pairs(roster) do
			if jid ~= "pending" and jid then
				local node, host = jid_split(jid);
				if host == origin.host then -- only include contacts which are on this component
					reply:tag("item", {
						jid = jid,
						subscription = item.subscription,
						ask = item.ask,
						name = item.name,
					});
					for group in pairs(item.groups) do
						reply:tag("group"):text(group):up();
					end
					reply:up(); -- move out from item
				end
			end
		end
		origin.send(reply);
		--origin.interested = true; -- resource is interested in roster updates
		return true;
	end
end);

module:hook("iq-set/bare/jabber:iq:roster:query", function(event)
	local session, stanza = event.origin, event.stanza;
	
	if not(session.type == "component" and stanza.attr.from == session.host) then return; end
	local from_node, from_host = jid_split(stanza.attr.to);
	if not(user_exists(from_node, from_host)) then return; end
	local roster = load_roster(from_node, from_host);
	if not(roster) then return; end

	local query = stanza.tags[1];
	if #query.tags == 1 and query.tags[1].name == "item"
			and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid
			-- Protection against overwriting roster.pending, until we move it
			and query.tags[1].attr.jid ~= "pending" then
		local item = query.tags[1];
		local jid = jid_prep(item.attr.jid);
		local node, host, resource = jid_split(jid);
		if not resource and host == session.host then
			if jid ~= stanza.attr.to then -- not self-jid
				if item.attr.subscription == "remove" then
					local r_item = roster[jid];
					if r_item then
						local to_bare = node and (node.."@"..host) or host; -- bare JID
						--if r_item.subscription == "both" or r_item.subscription == "from" or (roster.pending and roster.pending[jid]) then
						--	module:send(st.presence({type="unsubscribed", from=stanza.attr.to, to=to_bare}));
						--end
						--if r_item.subscription == "both" or r_item.subscription == "to" or r_item.ask then
						--	module:send(st.presence({type="unsubscribe", from=stanza.attr.to, to=to_bare}));
						--end
						roster[jid] = nil;
						if save_roster(from_node, from_host, roster) then
							session.send(st.reply(stanza));
							rm_roster_push(from_node, from_host, jid);
						else
							roster[jid] = item;
							session.send(st.error_reply(stanza, "wait", "internal-server-error", "Unable to save roster"));
						end
					else
						session.send(st.error_reply(stanza, "modify", "item-not-found"));
					end
				else
					local subscription = item.attr.subscription;
					if subscription ~= "both" and subscription ~= "to" and subscription ~= "from" and subscription ~= "none" then -- TODO error on invalid
						subscription = roster[jid] and roster[jid].subscription or "none";
					end
					local r_item = {name = item.attr.name, groups = {}};
					if r_item.name == "" then r_item.name = nil; end
					r_item.subscription = subscription;
					if subscription ~= "both" and subscription ~= "to" then
						r_item.ask = roster[jid] and roster[jid].ask;
					end
					for _, child in ipairs(item) do
						if child.name == "group" then
							local text = t_concat(child);
							if text and text ~= "" then
								r_item.groups[text] = true;
							end
						end
					end
					local olditem = roster[jid];
					roster[jid] = r_item;
					if save_roster(from_node, from_host, roster) then -- Ok, send success
						session.send(st.reply(stanza));
						-- and push change to all resources
						rm_roster_push(from_node, from_host, jid);
					else -- Adding to roster failed
						roster[jid] = olditem;
						session.send(st.error_reply(stanza, "wait", "internal-server-error", "Unable to save roster"));
					end
				end
			else -- Trying to add self to roster
				session.send(st.error_reply(stanza, "cancel", "not-allowed"));
			end
		else -- Invalid JID added to roster
			session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error?
		end
	else -- Roster set didn't include a single item, or its name wasn't  'item'
		session.send(st.error_reply(stanza, "modify", "bad-request"));
	end
	return true;
end);

function component_roster_push(node, host, jid)
	local roster = load_roster(node, host);
	if roster then
		local item = roster[jid];
		local contact_node, contact_host = jid_split(jid);
		local stanza = st.iq({ type="set", from=node.."@"..host, to=contact_host, id = new_id() }):query("jabber:iq:roster");
		if item then
			stanza:tag("item", { jid = jid, subscription = item.subscription, name = item.name, ask = item.ask });
			for group in pairs(item.groups) do
				stanza:tag("group"):text(group):up();
			end
		else
			stanza:tag("item", {jid = jid, subscription = "remove"});
		end
		stanza:up(); -- move out from item
		stanza:up(); -- move out from stanza
		module:send(stanza);
	end
end

module:hook("iq-set/bare/jabber:iq:roster:query", function(event)
	local origin, stanza = event.origin, event.stanza;
	local query = stanza.tags[1];
	local item = query.tags[1];
	local contact_jid = item and item.name == "item" and item.attr.jid ~= "pending" and item.attr.jid;
	if contact_jid then
		local contact_node, contact_host = jid_split(contact_jid);
		if hosts[contact_host] and hosts[contact_host].type == "component" then
			local node, host = jid_split(stanza.attr.to or origin.full_jid);
			add_task(0, function()
				component_roster_push(node, host, contact_jid);
			end);
		end
	end
end, 100);