diff mod_admin_web2/admin_web2/mod_admin_web2.lua @ 6520:7eb114975cec

mod_admin_web2: Fork of admin_web for prosody 13.0. It is a fork because it changes access rights and might be insecure. Mark as alpha.
author Menel <menel@snikket.de>
date Fri, 24 Apr 2026 12:40:13 +0200
parents mod_admin_web/admin_web/mod_admin_web.lua@963c16e417d6
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_admin_web2/admin_web2/mod_admin_web2.lua	Fri Apr 24 12:40:13 2026 +0200
@@ -0,0 +1,332 @@
+-- Copyright (C) 2010 Florian Zeitz
+--
+-- This file is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+-- <session xmlns="http://prosody.im/streams/c2s" jid="alice@example.com/brussels">
+--   <encrypted/>
+--   <compressed/>
+-- </session>
+
+-- <session xmlns="http://prosody.im/streams/s2s" jid="example.com">
+--   <encrypted>
+--     <valid/> / <invalid/>
+--   </encrypted>
+--   <compressed/>
+--   <in/> / <out/>
+-- </session>
+
+module:default_permission("prosody:operator", ":web-operator")
+
+local st = require "util.stanza";
+local uuid_generate = require "util.uuid".generate;
+local pubsub = require "util.pubsub";
+local jid_bare = require "util.jid".bare;
+
+local hosts = prosody.hosts;
+local incoming_s2s = prosody.incoming_s2s;
+
+module:set_global();
+
+local service = {};
+
+local xmlns_adminsub = "http://prosody.im/adminsub";
+local xmlns_c2s_session = "http://prosody.im/streams/c2s";
+local xmlns_s2s_session = "http://prosody.im/streams/s2s";
+
+local idmap = {};
+
+local function add_client(session, host)
+	local name = session.full_jid;
+	local id = idmap[name];
+	if not id then
+		id = uuid_generate();
+		idmap[name] = id;
+	end
+	local item = st.stanza("item", { id = id }):tag("session", {xmlns = xmlns_c2s_session, jid = name}):up();
+	if session.secure then
+		local encrypted = item:tag("encrypted");
+		local sock = session.conn and session.conn.socket and session.conn:socket()
+		local info = sock and sock.info and sock:info();
+		for k, v in pairs(info or {}) do
+			encrypted:tag("info", { name = k }):text(tostring(v)):up();
+		end
+	end
+	if session.compressed then
+		item:tag("compressed"):up();
+	end
+	service[host]:publish(xmlns_c2s_session, host, id, item);
+	module:log("debug", "Added client %s", name);
+end
+
+local function del_client(session, host)
+	local name = session.full_jid;
+	local id = idmap[name];
+	if id then
+		local notifier = st.stanza("retract", { id = id });
+		service[host]:retract(xmlns_c2s_session, host, id, notifier);
+	end
+end
+
+local function add_host(session, type, host)
+	local name = (type == "out" and session.to_host) or (type == "in" and session.from_host);
+	local id = idmap[name.."_"..type];
+	if not id then
+		id = uuid_generate();
+		idmap[name.."_"..type] = id;
+	end
+	local item = st.stanza("item", { id = id }):tag("session", {xmlns = xmlns_s2s_session, jid = name})
+		:tag(type):up();
+	if session.secure then
+		local encrypted = item:tag("encrypted");
+
+		local sock = session.conn and session.conn.socket and session.conn:socket()
+		local info = sock and sock.info and sock:info();
+		for k, v in pairs(info or {}) do
+			encrypted:tag("info", { name = k }):text(tostring(v)):up();
+		end
+
+		if session.cert_identity_status == "valid" then
+			encrypted:tag("valid");
+		else
+			encrypted:tag("invalid");
+		end
+	end
+	if session.compressed then
+		item:tag("compressed"):up();
+	end
+	service[host]:publish(xmlns_s2s_session, host, id, item);
+	module:log("debug", "Added host %s s2s%s", name, type);
+end
+
+local function del_host(session, type, host)
+	local name = (type == "out" and session.to_host) or (type == "in" and session.from_host);
+	local id = idmap[name.."_"..type];
+	if id then
+		local notifier = st.stanza("retract", { id = id });
+		service[host]:retract(xmlns_s2s_session, host, id, notifier);
+	end
+end
+
+function module.add_host(module)
+	-- Dependencies
+	module:depends("bosh");
+	module:depends("admin_adhoc");
+	module:depends("http");
+
+	local http_files = require "net.http.files";
+	local serve_file = http_files.serve {
+		path = module:get_directory() .. "/www_files";
+	};
+
+	-- Setup HTTP server
+	module:provides("http", {
+		title = "Admin Interface";
+		name = "admin";
+		route = {
+			["GET"] = function(event)
+				event.response.headers.location = event.request.path .. "/";
+				return 301;
+			end;
+			["GET /*"] = serve_file;
+		}
+	});
+
+	-- Setup adminsub service
+	local function simple_broadcast(kind, node, jids, item)
+		if item then
+			item = st.clone(item);
+			item.attr.xmlns = nil; -- Clear the pubsub namespace
+		end
+		local message = st.message({ from = module.host, type = "headline" })
+			:tag("event", { xmlns = xmlns_adminsub .. "#event" })
+				:tag(kind, { node = node })
+					:add_child(item);
+		for jid in pairs(jids) do
+			module:log("debug", "Sending notification to %s", jid);
+			message.attr.to = jid;
+			module:send(message);
+		end
+	end
+
+	service[module.host] = pubsub.new({
+		broadcaster = simple_broadcast;
+		normalize_jid = jid_bare;
+-- what was this for? prosody:operator has full rights everywhere, this shoud be fine?
+--		get_affiliation = function(jid) return get_affiliation(jid, module.host) end;
+		get_affiliation = function() return "owner" end;
+		capabilities = {
+			member = {
+				create = false;
+				publish = false;
+				retract = false;
+				get_nodes = true;
+
+				subscribe = true;
+				unsubscribe = true;
+				get_subscription = true;
+				get_subscriptions = true;
+				get_items = true;
+
+				subscribe_other = false;
+				unsubscribe_other = false;
+				get_subscription_other = false;
+				get_subscriptions_other = false;
+
+				be_subscribed = true;
+				be_unsubscribed = true;
+
+				set_affiliation = false;
+			};
+
+			owner = {
+				create = true;
+				publish = true;
+				retract = true;
+				get_nodes = true;
+
+				subscribe = true;
+				unsubscribe = true;
+				get_subscription = true;
+				get_subscriptions = true;
+				get_items = true;
+
+				subscribe_other = true;
+				unsubscribe_other = true;
+				get_subscription_other = true;
+				get_subscriptions_other = true;
+
+				be_subscribed = true;
+				be_unsubscribed = true;
+
+				set_affiliation = true;
+			};
+		};
+	});
+
+	-- Create node for s2s sessions
+	local ok, err = service[module.host]:create(xmlns_s2s_session, true);
+	if not ok then
+		module:log("warn", "Could not create node %s: %s", xmlns_s2s_session, err);
+	else
+		service[module.host]:set_affiliation(xmlns_s2s_session, true, module.host, "owner")
+	end
+
+	-- Add outgoing s2s sessions
+	for _, session in pairs(hosts[module.host].s2sout) do
+		if session.type ~= "s2sout_unauthed" then
+			add_host(session, "out", module.host);
+		end
+	end
+
+	-- Add incoming s2s sessions
+	for session in pairs(incoming_s2s) do
+		if session.to_host == module.host then
+			add_host(session, "in", module.host);
+		end
+	end
+
+	-- Create node for c2s sessions
+	ok, err = service[module.host]:create(xmlns_c2s_session, true);
+	if not ok then
+		module:log("warn", "Could not create node %s: %s", xmlns_c2s_session, tostring(err));
+	else
+		service[module.host]:set_affiliation(xmlns_c2s_session, true, module.host, "owner")
+	end
+
+	-- Add c2s sessions
+	for _, user in pairs(hosts[module.host].sessions or {}) do
+		for _, session in pairs(user.sessions or {}) do
+			add_client(session, module.host);
+		end
+	end
+
+	-- Register adminsub handler
+	module:hook("iq/host/http://prosody.im/adminsub:adminsub", function(event)
+		-- luacheck: ignore 431/ok
+		local origin, stanza = event.origin, event.stanza;
+		local adminsub = stanza.tags[1];
+		local action = adminsub.tags[1];
+		local reply;
+		if action.name == "subscribe" then
+			local ok, ret = service[module.host]:add_subscription(action.attr.node, stanza.attr.from, stanza.attr.from);
+			if ok then
+				reply = st.reply(stanza)
+					:tag("adminsub", { xmlns = xmlns_adminsub });
+			else
+				reply = st.error_reply(stanza, "cancel", ret);
+			end
+		elseif action.name == "unsubscribe" then
+			local ok, ret = service[module.host]:remove_subscription(action.attr.node, stanza.attr.from, stanza.attr.from);
+			if ok then
+				reply = st.reply(stanza)
+					:tag("adminsub", { xmlns = xmlns_adminsub });
+			else
+				reply = st.error_reply(stanza, "cancel", ret);
+			end
+		elseif action.name == "items" then
+			local node = action.attr.node;
+			local ok, ret = service[module.host]:get_items(node, stanza.attr.from);
+			if not ok then
+				origin.send(st.error_reply(stanza, "cancel", ret));
+				return true;
+			end
+
+			local data = st.stanza("items", { node = node });
+			for _, entry in pairs(ret) do
+				data:add_child(entry);
+			end
+			if data then
+				reply = st.reply(stanza)
+					:tag("adminsub", { xmlns = xmlns_adminsub })
+						:add_child(data);
+			else
+				reply = st.error_reply(stanza, "cancel", "item-not-found");
+			end
+		elseif action.name == "adminfor" then
+			local data = st.stanza("adminfor");
+			for host_name in pairs(hosts) do
+				-- new Permission API:
+				if module:context(host_name):may(":web-operator", stanza.attr.from) then
+					data:tag("item"):text(host_name):up();
+				end
+			end
+			reply = st.reply(stanza)
+				:tag("adminsub", { xmlns = xmlns_adminsub })
+					:add_child(data);
+		else
+			reply = st.error_reply(stanza, "feature-not-implemented");
+		end
+		origin.send(reply);
+		return true;
+	end);
+
+	-- Add/remove c2s sessions
+	module:hook("resource-bind", function(event)
+		add_client(event.session, module.host);
+	end);
+
+	module:hook("resource-unbind", function(event)
+		del_client(event.session, module.host);
+		service[module.host]:remove_subscription(xmlns_c2s_session, module.host, event.session.full_jid);
+		service[module.host]:remove_subscription(xmlns_s2s_session, module.host, event.session.full_jid);
+	end);
+
+	-- Add/remove s2s sessions
+	module:hook("s2sout-established", function(event)
+		add_host(event.session, "out", module.host);
+	end);
+
+	module:hook("s2sin-established", function(event)
+		add_host(event.session, "in", module.host);
+	end);
+
+	module:hook("s2sout-destroyed", function(event)
+		del_host(event.session, "out", module.host);
+	end);
+
+	module:hook("s2sin-destroyed", function(event)
+		del_host(event.session, "in", module.host);
+	end);
+end