view mod_onions/mod_onions.lua @ 6214:cc5c0f1dc89b

mod_muc_anonymize_moderation_actions: Fix mentioning Virtualhost for MUC, Compatibility section and License. diff --git a/mod_muc_anonymize_moderation_actions/README.md b/mod_muc_anonymize_moderation_actions/README.md --- a/mod_muc_anonymize_moderation_actions/README.md +++ b/mod_muc_anonymize_moderation_actions/README.md @@ -1,8 +1,10 @@ -<!-- -SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/> -SPDX-License-Identifier: AGPL-3.0-only ---> -# mod_muc_anonymize_moderation_actions +--- +labels: +- 'Stage-Alpha' +summary: Anonymize moderator actions for participants +--- + +## Introduction This modules allows to anonymize affiliation and role changes in MUC rooms. @@ -11,13 +13,10 @@ When the feature is enabled, when a mode This is particularly usefull to prevent some revenge when a moderator bans someone. -This module is under AGPL-3.0 license. - -It was tested on Prosody 0.12.x. ## Configuration -Just enable the module on your MUC VirtualHost. +Just enable the module on your MUC Component. The feature will be accessible throught the room configuration form. You can tweak the position of the settings in the MUC configuration form using `anonymize_moderation_actions_form_position`. @@ -26,7 +25,20 @@ This value will be passed as priority fo By default, the field will be between muc#roomconfig_changesubject and muc#roomconfig_moderatedroom (default value is `78`). ``` lua -VirtualHost "muc.example.com" +Component "muc.example.com" "muc" modules_enabled = { "muc_anonymize_moderation_actions" } anonymize_moderation_actions_form_position = 96 ``` + +## Compatibility + + ------ ---------------------- + trunk Works as of 25-05-12 + 0.13 Works + 0.12 Works + ------ ---------------------- + +### License + +SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/> +SPDX-License-Identifier: AGPL-3.0-only
author Menel <menel@snikket.de>
date Mon, 12 May 2025 10:42:26 +0200
parents fa415cd9eeca
children
line wrap: on
line source

local prosody = prosody;
local core_process_stanza = prosody.core_process_stanza;

local addclient = require "net.server".addclient;
local s2s_new_outgoing = require "core.s2smanager".new_outgoing;
local initialize_filters = require "util.filters".initialize;
local st = require "util.stanza";

local portmanager = require "core.portmanager";

local softreq = require "util.dependencies".softreq;

local bit = assert(softreq "bit" or softreq "bit32" or softreq "util.bitcompat", "No bit module found. See https://prosody.im/doc/depends#bitop");

local band = bit.band;
local rshift = bit.rshift;
local lshift = bit.lshift;

local byte = string.byte;
local c = string.char;

module:depends("s2s");

local proxy_ip = module:get_option_string("onions_socks5_host", "127.0.0.1");
local proxy_port = module:get_option_number("onions_socks5_port", 9050);
local forbid_else = module:get_option_boolean("onions_only", false);
local torify_all = module:get_option_boolean("onions_tor_all", false);
local onions_map = module:get_option("onions_map", {});

local sessions = module:shared("sessions");

-- The socks5listener handles connection while still connecting to the proxy,
-- then it hands them over to the normal listener (in mod_s2s)
local socks5listener = { default_port = proxy_port, default_mode = "*a", default_interface = "*" };

local function socks5_connect_sent(conn, data)

	local session = sessions[conn];

	if #data < 5 then
		session.socks5_buffer = data;
		return;
	end

	local request_status = byte(data, 2);

	if not request_status == 0x00 then
		module:log("debug", "Failed to connect to the SOCKS5 proxy. :(");
		session:close(false);
		return;
	end

	module:log("debug", "Successfully connected to SOCKS5 proxy.");

	local response = byte(data, 4);

	if response == 0x01 then
		if #data < 10 then
			-- let's try again when we have enough
			session.socks5_buffer = data;
			return;
		end

		-- this means the server tells us to connect on an IPv4 address
		local ip = string.format("%d.%d.%d.%d", byte(data, 5,8));
		local port = band(byte(data, 9), lshift(byte(data, 10), 8));
		module:log("debug", "Should connect to: %s:%d", ip, port);

		if not (ip == "0.0.0.0" and port == 0) then
			module:log("debug", "The SOCKS5 proxy tells us to connect to a different IP, don't know how. :(");
			session:close(false);
			return;
		end

		-- Now the real s2s listener can take over the connection.
		local listener = portmanager.get_service("s2s").listener;

		module:log("debug", "SOCKS5 done, handing over listening to "..tostring(listener));

		session.socks5_handler = nil;
		session.socks5_buffer = nil;

		local w, log = conn.send, session.log;

		local filter = initialize_filters(session);

		session.version = 1;

		session.sends2s = function (t)
			log("debug", "sending (s2s over socks5): %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
			if t.name then
				t = filter("stanzas/out", t);
			end
			if t then
				t = filter("bytes/out", tostring(t));
				if t then
					return conn:write(tostring(t));
				end
			end
		end

		session.open_stream = function ()
			session.sends2s(st.stanza("stream:stream", {
				xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
				["xmlns:stream"]='http://etherx.jabber.org/streams',
				from=session.from_host, to=session.to_host, version='1.0', ["xml:lang"]='en'}):top_tag());
		end

		conn.setlistener(conn, listener);

		listener.register_outgoing(conn, session);

		listener.onconnect(conn);
	end
end

local function socks5_handshake_sent(conn, data)

	local session = sessions[conn];

	if #data < 2 then
		session.socks5_buffer = data;
		return;
	end

	-- version, method
	local request_status = byte(data, 2);

	module:log("debug", "SOCKS version: "..byte(data, 1));
	module:log("debug", "Response: "..request_status);

	if not request_status == 0x00 then
		module:log("debug", "Failed to connect to the SOCKS5 proxy. :( It seems to require authentication.");
		session:close(false);
		return;
	end

	module:log("debug", "Sending connect message.");

	-- version 5, connect, (reserved), type: domainname, (length, hostname), port
	conn:write(c(5) .. c(1) .. c(0) .. c(3) .. c(#session.socks5_to) .. session.socks5_to);
	conn:write(c(rshift(session.socks5_port, 8)) .. c(band(session.socks5_port, 0xff)));

	session.socks5_handler = socks5_connect_sent;
end

function socks5listener.onconnect(conn)
	module:log("debug", "Connected to SOCKS5 proxy, sending SOCKS5 handshake.");

	-- Socks version 5, 1 method, no auth
	conn:write(c(5) .. c(1) .. c(0));

	sessions[conn].socks5_handler = socks5_handshake_sent;
end

function socks5listener.register_outgoing(conn, session)
	session.direction = "outgoing";
	sessions[conn] = session;
end

function socks5listener.ondisconnect(conn, err)
	sessions[conn]  = nil;
end

function socks5listener.onincoming(conn, data)
	local session = sessions[conn];

	if session.socks5_buffer then
		data = session.socks5_buffer .. data;
	end

	if session.socks5_handler then
		session.socks5_handler(conn, data);
	end
end

local function connect_socks5(host_session, connect_host, connect_port)

	module:log("debug", "Connecting to " .. connect_host .. ":" .. connect_port);

	-- this is not necessarily the same as .to_host (it can be that this is from the onions_map)
	host_session.socks5_to = connect_host;
	host_session.socks5_port = connect_port;

	local conn = addclient(proxy_ip, proxy_port, socks5listener, "*a");

	socks5listener.register_outgoing(conn, host_session);

	host_session.conn = conn;
end

local bouncy_stanzas = { message = true, presence = true, iq = true };
local function bounce_sendq(session, reason)
	local sendq = session.sendq;
	if not sendq then return; end
	session.log("info", "Sending error replies for "..#sendq.." queued stanzas because of failed outgoing connection to "..tostring(session.to_host));
	local dummy = {
		type = "s2sin";
		send = function(s)
			(session.log or log)("error", "Replying to to an s2s error reply, please report this! Traceback: %s", debug.traceback());
		end;
		dummy = true;
	};
	for i, data in ipairs(sendq) do
		local reply = data[2];
		if reply and not(reply.attr.xmlns) and bouncy_stanzas[reply.name] then
			reply.attr.type = "error";
			reply:tag("error", {type = "cancel"})
				:tag("remote-server-not-found", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up();
			if reason then
				reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"})
					:text("Server-to-server connection failed: "..reason):up();
			end
			core_process_stanza(dummy, reply);
		end
		sendq[i] = nil;
	end
	session.sendq = nil;
end
-- Try to intercept anything to *.onion
local function route_to_onion(event)
	local stanza = event.stanza;
	local to_host = event.to_host;
	local onion_host = nil;
	local onion_port = nil;

	if not to_host:find("%.onion$") then
		if onions_map[to_host] then
			if type(onions_map[to_host]) == "string" then
				onion_host = onions_map[to_host];
			else
				onion_host = onions_map[to_host].host;
				onion_port = onions_map[to_host].port;
			end
		elseif forbid_else then
			module:log("debug", event.to_host .. " is not an onion. Blocking it.");
			return false;
		elseif not torify_all then
			return;
		end
	end

	module:log("debug", "Onion routing something to ".. to_host);

	if hosts[event.from_host].s2sout[to_host] then
		return;
	end

	local host_session = s2s_new_outgoing(event.from_host, to_host);

	host_session.bounce_sendq = bounce_sendq;
	host_session.sendq = { {tostring(stanza), stanza.attr and stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };

	hosts[event.from_host].s2sout[to_host] = host_session;

	connect_socks5(host_session, onion_host or to_host, onion_port or 5269);

	return true;
end

module:log("debug", "Onions ready and loaded");

module:hook("route/remote", route_to_onion, 200);

module:hook_global("s2s-check-certificate", function (event)
	local host = event.host;
	if host and host:find("%.onion$") then
		-- This cancels the event chain without reporting any cert
		-- validation results. The connection will typically proceed
		-- to auth using dialback.
		return true;
	end
end);