diff mod_delegation/mod_delegation.lua @ 1710:b68eed25b880

mod_delegation: iq forwarding to managing entity
author Goffi <goffi@goffi.org>
date Fri, 17 Apr 2015 21:05:08 +0200
parents 59ba224bf145
children 55b9ac807ac9
line wrap: on
line diff
--- a/mod_delegation/mod_delegation.lua	Fri Apr 17 21:02:22 2015 +0200
+++ b/mod_delegation/mod_delegation.lua	Fri Apr 17 21:05:08 2015 +0200
@@ -4,6 +4,10 @@
 -- This module is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 
+-- This module manage namespace delegation, a way to delegate server features
+-- to an external entity/component. Only the admin mode is implemented so far
+
+-- TODO: client mode, managing entity error handling
 
 local jid = require("util/jid")
 local st = require("util/stanza")
@@ -20,8 +24,8 @@
 local connected_cb = delegation_session.connected_cb
 
 local _DELEGATION_NS = 'urn:xmpp:delegation:1'
--- local _FORWARDED_NS = 'urn:xmpp:forward:0'
-
+local _FORWARDED_NS = 'urn:xmpp:forward:0'
+local _ORI_ID_PREFIX = "IQ_RESULT_"
 
 module:log("debug", "Loading namespace delegation module ");
 
@@ -31,14 +35,15 @@
 local ns_delegations = module:get_option("delegations", {})
 
 local jid2ns = {}
-for namespace, config in pairs(ns_delegations) do
+for namespace, ns_data in pairs(ns_delegations) do
 	-- "connected" contain the full jid of connected managing entity
-	config.connected = nil
-	if config.jid then
-		if jid2ns[config.jid] == nil then
-			jid2ns[config.jid] = {}
+	ns_data.connected = nil
+	if ns_data.jid then
+		if jid2ns[ns_data.jid] == nil then
+			jid2ns[ns_data.jid] = {}
 		end
-		jid2ns[config.jid][namespace] = config
+		jid2ns[ns_data.jid][namespace] = ns_data
+		module:log("debug", "Namespace %s is delegated%s to %s", namespace, ns_data.filtering and " (with filtering)" or "", ns_data.jid)
 	else
 		module:log("warn", "Ignoring delegation for %s: no jid specified", tostring(namespace))
 		ns_delegations[namespace] = nil
@@ -57,12 +62,12 @@
 	-- and other to full jids, and several resources are connected.
 	local have_delegation = false
 
-	for namespace, config  in pairs(jid2ns[to_jid]) do
-		if config.connected == to_jid then
+	for namespace, ns_data  in pairs(jid2ns[to_jid]) do
+		if ns_data.connected == to_jid then
 			have_delegation = true
 			message:tag("delegated", {namespace=namespace})
-			if type(config.filtering) == "table" then
-				for _, attribute in pairs(config.filtering) do
+			if type(ns_data.filtering) == "table" then
+				for _, attribute in pairs(ns_data.filtering) do
 					message:tag("attribute", {name=attribute}):up()
 				end
 				message:up()
@@ -79,9 +84,9 @@
 	-- set the "connected" key for all namespace managed by entity_jid
 	-- if the namespace has already a connected entity, ignore the new one
 	local function set_config(jid_)
-		for _, config in pairs(jid2ns[jid_]) do
-			if config.connected == nil then
-				config.connected = entity_jid
+		for _, ns_data in pairs(jid2ns[jid_]) do
+			if ns_data.connected == nil then
+				ns_data.connected = entity_jid
 			end
 		end
 	end
@@ -132,3 +137,110 @@
 connected_cb:add(on_component_connected)
 module:hook('component-authenticated', on_component_auth)
 module:hook('presence/initial', on_presence)
+
+
+--> delegated namespaces hook <--
+
+local function managing_ent_result(event)
+	-- this function manage iq results from the managing entity
+	-- it do a couple of security check before sending the
+	-- result to the managed entity
+	local session, stanza = event.origin, event.stanza
+	if stanza.attr.to ~= module.host then
+		module:log("warn", 'forwarded stanza result has "to" attribute not addressed to current host, id conflict ?')
+		return
+	end
+	module:unhook("iq-result/host/"..stanza.attr.id, managing_ent_result)
+
+	-- lot of checks to do...
+	local delegation = stanza.tags[1]
+	if #stanza ~= 1 or delegation.name ~= "delegation" or
+		delegation.attr.xmlns ~= _DELEGATION_NS then
+		session.send(st.error_reply(stanza, 'modify', 'not-acceptable'))
+		return true
+	end
+
+	local forwarded = delegation.tags[1]
+	if #delegation ~= 1 or forwarded.name ~= "forwarded" or
+		forwarded.attr.xmlns ~= _FORWARDED_NS then
+		session.send(st.error_reply(stanza, 'modify', 'not-acceptable'))
+		return true
+	end
+
+	local iq = forwarded.tags[1]
+	if #forwarded ~= 1 or iq.name ~= "iq" or #iq ~= 1 then
+		session.send(st.error_reply(stanza, 'modify', 'not-acceptable'))
+		return true
+	end
+
+	local namespace = iq.tags[1].xmlns
+	local ns_data = ns_delegations[namespace]
+	local original = ns_data[_ORI_ID_PREFIX..stanza.attr.id]
+
+	if stanza.attr.from ~= ns_data.connected or iq.attr.type ~= "result" or
+		iq.attr.id ~= original.attr.id or iq.attr.to ~= original.attr.from then
+		session.send(st.error_reply(stanza, 'auth', 'forbidden'))
+		module:send(st.error_reply(original, 'cancel', 'service-unavailable'))
+		return true
+	end
+
+	-- at this point eveything is checked,
+	-- and we (hopefully) can send the the result safely
+	module:send(iq)
+end
+
+local function forward_iq(stanza, ns_data)
+	local to_jid = ns_data.connected
+	local iq_stanza  = st.iq({ from=module.host, to=to_jid, type="set" })
+		:tag("delegation", { xmlns=_DELEGATION_NS })
+		:tag("forwarded", { xmlns=_FORWARDED_NS })
+		:add_child(stanza)
+	local iq_id = iq_stanza.attr.id
+	-- we save the original stanza to check the managing entity result
+	ns_data[_ORI_ID_PREFIX..iq_id] = stanza
+	module:log("debug", "stanza forwarded to "..to_jid..": "..tostring(iq_stanza))
+	module:hook("iq-result/host/"..iq_id, managing_ent_result)
+	module:send(iq_stanza)
+end
+
+local function iq_hook(event)
+	-- general hook for all the iq which forward delegated ones
+	-- and continue normal behaviour else. If a namespace is
+	-- delegated but managing entity is offline, a service-unavailable
+	-- error will be sent, as requested by the XEP
+	local session, stanza = event.origin, event.stanza
+	if #stanza == 1 and stanza.attr.type == 'get' or stanza.attr.type == 'set' then
+		local namespace = stanza.tags[1].attr.xmlns
+		local ns_data = ns_delegations[namespace]
+
+		if ns_data then
+			module:log("debug", "Namespace %s is delegated", namespace)
+			if ns_data.filtering then
+				local first_child = stanza.tags[1]
+				for _, attribute in ns_data.filtering do
+					-- if any filtered attribute if not present,
+					-- we must continue the normal bahaviour
+					if not first_child.attr[attribute] then
+						module:log("debug", "Filtered attribute %s not present, doing normal workflow", attribute)
+						return;
+					end
+				end
+			end
+			if not ns_data.connected then
+				module:log("warn", "No connected entity to manage "..namespace)
+				session.send(st.error_reply(stanza, 'cancel', 'service-unavailable'))
+			else
+				local managing_entity = ns_data.connected
+				module:log("debug", "Entity %s is managing %s", managing_entity, namespace)
+				forward_iq(stanza, ns_data)
+			end
+			return true
+		else
+			-- we have no delegation, we continue normal behaviour
+			return
+		end
+	end
+end
+
+module:hook("iq/self", iq_hook, 2^32)
+module:hook("iq/host", iq_hook, 2^32)