changeset 3098:a81456a13797

mod_client_proxy: a Jabber Address Translation implementation
author Jonas Wielicki <jonas@wielicki.name>
date Sun, 03 Jun 2018 13:55:10 +0200
parents c7f4e3987ed0
children c88f91626e07
files mod_auth_ldap/mod_auth_ldap.lua mod_client_proxy/README.markdown mod_client_proxy/mod_client_proxy.lua mod_muc_limits/mod_muc_limits.lua
diffstat 4 files changed, 306 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/mod_auth_ldap/mod_auth_ldap.lua	Sun Jun 03 01:34:23 2018 +0200
+++ b/mod_auth_ldap/mod_auth_ldap.lua	Sun Jun 03 13:55:10 2018 +0200
@@ -35,9 +35,18 @@
 	local success, iterator, invariant, initial = pcall(ld[method], ld, ...);
 	if not success then ld = nil; return nil, iterator, "search"; end
 
+	module:log("debug", "success = %s, invariant = %s",
+		tostring(success),
+		tostring(invariant));
+
 	local success, dn, attr = pcall(iterator, invariant, initial);
+	module:log("debug", "success = %s, dn = %s, attr = %s",
+		tostring(success),
+		tostring(dn),
+		tostring(attr));
 	if not success then ld = nil; return success, dn, "iter"; end
 
+
 	return dn, attr, "return";
 end
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_client_proxy/README.markdown	Sun Jun 03 13:55:10 2018 +0200
@@ -0,0 +1,68 @@
+---
+labels:
+- 'Stage-Alpha'
+summary: 'Proxy multiple client resources behind a single component'
+...
+
+What it does
+============
+
+This module must be used as a component. For example:
+
+    Component "proxy.domain.example" "client_proxy"
+        target_address = "some-user@some-domain.example"
+
+All IQ requests against the proxy host (in the above example:
+proxy.domain.example) are sent to a random resource of the target address (in
+the above example: some-user@some-domain.example). The entity behind the
+target address is called the "implementing client".
+
+The IQ requests are JAT-ed (JAT: Jabber Address Translation) so that when the
+implementing client answers the IQ request, it is sent back to the component,
+which reverts the translation and routes the reply back to the user.
+
+Let us assume that user@some-domain.exmaple sends a request. The
+proxy.domain.example component has the client_proxy module loaded and proxies to
+some-user@some-domain.example. some-user@some-domain.example has two resources,
+/a and /b.
+
+    user -> component:
+        <iq type='get' id='1234' to='proxy.domain.example' from='user@some-domain.example/abc'>
+    component -> implementing client:
+        <iq type='get' id='1234' to='some-user@some-domain.example/a' from='proxy.domain.example/encoded-from'>
+    implementing client -> component:
+        <iq type='result' id='1234' to='proxy.domain.example/encoded-from' from='some-user@some-domain.example/a'>
+    component -> user:
+        <iq type='result' id='1234' to='user@some-domain.example/abc' from='proxy.domain.example'>
+
+The encoded-from resource used in the exchange between the proxy component
+and the implementing client is an implementation-defined string which allows
+the proxy component to revert the JAT.
+
+
+Use cases
+=========
+
+* Implementation of services within clients instead of components, thus making
+  use of the more advanced authentication features.
+* General evilness
+
+
+Configuration
+=============
+
+To use this module, it needs to be loaded on a component:
+
+    Component "proxy.yourdomain.example" "client_proxy"
+        target_address = "implementation@yourdomain.example"
+
+It will then send a subscription request to implementation@yourdomain.example
+which MUST be accepted: this is required so that the component can detect the
+resources to which IQ requests can be dispatched.
+
+
+Limitations
+===========
+
+* It does not handle presence or message stanzas.
+* It does not allow the implementing client to initiate IQ requests
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_client_proxy/mod_client_proxy.lua	Sun Jun 03 13:55:10 2018 +0200
@@ -0,0 +1,208 @@
+if module:get_host_type() ~= "component" then
+	error("proxy_component should be loaded as component", 0);
+end
+
+local comp_host = module:get_host();
+local comp_name = module:get_option_string("name", "Proxy Component");
+
+local jid_split = require "util.jid".split;
+local jid_bare = require "util.jid".bare;
+local jid_prep = require "util.jid".prep;
+local st = require "util.stanza";
+local array = require "util.array";
+
+local target_address = module:get_option_string("target_address");
+
+local hosts = prosody.hosts;
+
+sessions = array{};
+local sessions = sessions;
+
+local function handle_target_presence(origin, stanza)
+	local type = stanza.attr.type;
+	module:log("debug", "received presence from destination: %s", type)
+	local _, _, resource = jid_split(stanza.attr.from);
+	if type == "error" then
+		-- drop all known sessions
+		for k in pairs(sessions) do
+			sessions[k] = nil
+		end
+		module:log(
+			"debug",
+			"received error presence, dropping all target sessions",
+			resource
+		)
+	elseif type == "unavailable" then
+		for k in pairs(sessions) do
+			if sessions[k] == resource then
+				sessions[k] = nil
+				module:log(
+					"debug",
+					"dropped target session: %s",
+					resource
+				)
+				break
+			end
+		end
+	elseif not type then
+		-- available
+		local found = false;
+		for k in pairs(sessions) do
+			if sessions[k] == resource then
+				found = true;
+				break
+			end
+		end
+		if not found then
+			module:log(
+				"debug",
+				"registered new target session: %s",
+				resource
+			)
+			sessions:push(resource)
+		end
+	end
+end
+
+local function handle_from_target(origin, stanza)
+	local type = stanza.attr.type
+	module:log(
+		"debug",
+		"non-presence stanza from target: name = %s, type = %s",
+		stanza.name,
+		type
+	)
+	if stanza.name == "iq" then
+		if type == "error" or type == "result" then
+			-- de-NAT message
+			local _, _, denatted_to_unprepped = jid_split(stanza.attr.to);
+			local denatted_to = jid_prep(denatted_to_unprepped);
+			if not denatted_to then
+				module:log(
+					"debug",
+					"cannot de-NAT stanza, invalid to: %s",
+					denatted_to_unprepped
+				)
+				return
+			end
+			local denatted_from = module:get_host();
+
+			module:log(
+				"debug",
+				"de-NAT-ed stanza: from: %s -> %s, to: %s -> %s",
+				stanza.attr.from,
+				denatted_from,
+				stanza.attr.to,
+				denatted_to
+			)
+
+			stanza.attr.from = denatted_from
+			stanza.attr.to = denatted_to
+
+			module:send(stanza)
+		else
+			-- FIXME: we don’t support NATing outbund requests atm.
+			module:send(st.error_reply(stanza, "cancel", "feature-not-implemented"))
+		end
+	elseif stanza.name == "message" then
+		-- not implemented yet, we need a way to ensure that routing doesn’t
+		-- break
+		module:send(st.error_reply(stanza, "cancel", "feature-not-implemented"))
+	end
+end
+
+local function handle_to_target(origin, stanza)
+	local type = stanza.attr.type;
+	module:log(
+		"debug",
+		"stanza to target: name = %s, type = %s",
+		stanza.name, type
+	)
+	if stanza.name == "presence" then
+		if type ~= "error" then
+			module:send(st.error_reply(stanza, "cancel", "bad-request"))
+			return
+		end
+	elseif stanza.name == "iq" then
+		if type == "get" or type == "set" then
+			if #sessions == 0 then
+				-- no sessions available to send to
+				module:log("debug", "no sessions to send to!")
+				module:send(st.error_reply(stanza, "cancel", "service-unavailable"))
+				return
+			end
+
+			-- find a target session
+			local target_session = sessions:random()
+			local target = target_address .. "/" .. target_session
+
+			-- encode sender JID in resource
+			local natted_from = module:get_host() .. "/" .. stanza.attr.from;
+
+			module:log(
+				"debug",
+				"NAT-ed stanza: from: %s -> %s, to: %s -> %s",
+				stanza.attr.from,
+				natted_from,
+				stanza.attr.to,
+				target
+			)
+
+			stanza.attr.from = natted_from
+			stanza.attr.to = target
+
+			module:send(stanza)
+		else
+			-- FIXME: handle and forward result/error correctly
+		end
+	elseif stanza.name == "message" then
+		-- not implemented yet, we need a way to ensure that routing doesn’t
+		-- break
+		module:send(st.error_reply(stanza, "cancel", "feature-not-implemented"))
+	end
+end
+
+local function stanza_handler(event)
+	local origin, stanza = event.origin, event.stanza
+	module:log("debug", "received stanza from %s session", origin.type)
+
+	local bare_from = jid_bare(stanza.attr.from);
+	local _, _, to = jid_split(stanza.attr.to);
+	if bare_from == target_address then
+		-- from our target, to whom?
+		if not to then
+			-- directly to component
+			if stanza.name == "presence" then
+				handle_target_presence(origin, stanza)
+			else
+				module:send(st.error_reply(stanza, "cancel", "bad-request"))
+				return true
+			end
+		else
+			-- to someone else
+			handle_from_target(origin, stanza)
+		end
+	else
+		handle_to_target(origin, stanza)
+	end
+	return true
+end
+
+module:hook("iq/bare", stanza_handler, -1);
+module:hook("message/bare", stanza_handler, -1);
+module:hook("presence/bare", stanza_handler, -1);
+module:hook("iq/full", stanza_handler, -1);
+module:hook("message/full", stanza_handler, -1);
+module:hook("presence/full", stanza_handler, -1);
+module:hook("iq/host", stanza_handler, -1);
+module:hook("message/host", stanza_handler, -1);
+module:hook("presence/host", stanza_handler, -1);
+
+module:log("debug", "loaded proxy on %s", module:get_host())
+
+subscription_request = st.presence({
+	type = "subscribe",
+	to = target_address,
+	from = module:get_host()}
+)
+module:send(subscription_request)
--- a/mod_muc_limits/mod_muc_limits.lua	Sun Jun 03 01:34:23 2018 +0200
+++ b/mod_muc_limits/mod_muc_limits.lua	Sun Jun 03 13:55:10 2018 +0200
@@ -25,6 +25,18 @@
 	dropped_jids = nil;
 end
 
+local function get_non_outcast_affiliations(room)
+	local nmembers = 0;
+	-- this is an evil hack, we probably should not access _affiliations
+	-- directly ...
+	for _, aff in pairs(room._affiliations) do
+		if aff ~= "outcast" then
+			nmembers = nmembers + 1;
+		end
+	end
+	return nmembers;
+end
+
 local function handle_stanza(event)
 	local origin, stanza = event.origin, event.stanza;
 	if stanza.name == "presence" and stanza.attr.type == "unavailable" then -- Don't limit room leaving
@@ -49,7 +61,15 @@
 		throttle = new_throttle(period*burst, burst);
 		room.throttle = throttle;
 	end
-	if not throttle:poll(1) then
+
+	local cost = 1;
+	-- we scale the cost by the inverse of the square root of the number of
+	-- members; this should effectively raise the limit by a factor of
+	-- sqrt(nmembers)
+	local nmembers = math.max(get_non_outcast_affiliations(room), 1);
+	cost = cost / math.sqrt(nmembers);
+
+	if not throttle:poll(cost) then
 		module:log("debug", "Dropping stanza for %s@%s from %s, over rate limit", dest_room, dest_host, from_jid);
 		if not dropped_jids then
 			dropped_jids = { [from_jid] = true, from_jid };