File

mod_client_proxy/mod_client_proxy.lua @ 5691:ecfd7aece33b

mod_measure_modules: Report module statuses via OpenMetrics Someone in the chat asked about a health check endpoint, which reminded me of mod_http_status, which provides access to module statuses with full details. After that, this idea came about, which seems natural. As noted in the README, it could be used to monitor that critical modules are in fact loaded correctly. As more modules use the status API, the more useful this module and mod_http_status becomes.
author Kim Alvefur <zash@zash.se>
date Fri, 06 Oct 2023 18:34:39 +0200
parent 3101:a14a573d43ff
line wrap: on
line source

if module:get_host_type() ~= "component" then
	error("proxy_component should be loaded as component", 0);
end

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");

sessions = array{};
local sessions = sessions;

local function handle_target_presence(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(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(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)
		end
		-- FIXME: handle and forward result/error correctly
	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(stanza)
			else
				module:send(st.error_reply(stanza, "cancel", "bad-request"))
				return true
			end
		else
			-- to someone else
			handle_from_target(stanza)
		end
	else
		handle_to_target(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)