comparison 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
comparison
equal deleted inserted replaced
6519:01d8bfbfc435 6520:7eb114975cec
1 -- Copyright (C) 2010 Florian Zeitz
2 --
3 -- This file is MIT/X11 licensed. Please see the
4 -- COPYING file in the source package for more information.
5 --
6
7 -- <session xmlns="http://prosody.im/streams/c2s" jid="alice@example.com/brussels">
8 -- <encrypted/>
9 -- <compressed/>
10 -- </session>
11
12 -- <session xmlns="http://prosody.im/streams/s2s" jid="example.com">
13 -- <encrypted>
14 -- <valid/> / <invalid/>
15 -- </encrypted>
16 -- <compressed/>
17 -- <in/> / <out/>
18 -- </session>
19
20 module:default_permission("prosody:operator", ":web-operator")
21
22 local st = require "util.stanza";
23 local uuid_generate = require "util.uuid".generate;
24 local pubsub = require "util.pubsub";
25 local jid_bare = require "util.jid".bare;
26
27 local hosts = prosody.hosts;
28 local incoming_s2s = prosody.incoming_s2s;
29
30 module:set_global();
31
32 local service = {};
33
34 local xmlns_adminsub = "http://prosody.im/adminsub";
35 local xmlns_c2s_session = "http://prosody.im/streams/c2s";
36 local xmlns_s2s_session = "http://prosody.im/streams/s2s";
37
38 local idmap = {};
39
40 local function add_client(session, host)
41 local name = session.full_jid;
42 local id = idmap[name];
43 if not id then
44 id = uuid_generate();
45 idmap[name] = id;
46 end
47 local item = st.stanza("item", { id = id }):tag("session", {xmlns = xmlns_c2s_session, jid = name}):up();
48 if session.secure then
49 local encrypted = item:tag("encrypted");
50 local sock = session.conn and session.conn.socket and session.conn:socket()
51 local info = sock and sock.info and sock:info();
52 for k, v in pairs(info or {}) do
53 encrypted:tag("info", { name = k }):text(tostring(v)):up();
54 end
55 end
56 if session.compressed then
57 item:tag("compressed"):up();
58 end
59 service[host]:publish(xmlns_c2s_session, host, id, item);
60 module:log("debug", "Added client %s", name);
61 end
62
63 local function del_client(session, host)
64 local name = session.full_jid;
65 local id = idmap[name];
66 if id then
67 local notifier = st.stanza("retract", { id = id });
68 service[host]:retract(xmlns_c2s_session, host, id, notifier);
69 end
70 end
71
72 local function add_host(session, type, host)
73 local name = (type == "out" and session.to_host) or (type == "in" and session.from_host);
74 local id = idmap[name.."_"..type];
75 if not id then
76 id = uuid_generate();
77 idmap[name.."_"..type] = id;
78 end
79 local item = st.stanza("item", { id = id }):tag("session", {xmlns = xmlns_s2s_session, jid = name})
80 :tag(type):up();
81 if session.secure then
82 local encrypted = item:tag("encrypted");
83
84 local sock = session.conn and session.conn.socket and session.conn:socket()
85 local info = sock and sock.info and sock:info();
86 for k, v in pairs(info or {}) do
87 encrypted:tag("info", { name = k }):text(tostring(v)):up();
88 end
89
90 if session.cert_identity_status == "valid" then
91 encrypted:tag("valid");
92 else
93 encrypted:tag("invalid");
94 end
95 end
96 if session.compressed then
97 item:tag("compressed"):up();
98 end
99 service[host]:publish(xmlns_s2s_session, host, id, item);
100 module:log("debug", "Added host %s s2s%s", name, type);
101 end
102
103 local function del_host(session, type, host)
104 local name = (type == "out" and session.to_host) or (type == "in" and session.from_host);
105 local id = idmap[name.."_"..type];
106 if id then
107 local notifier = st.stanza("retract", { id = id });
108 service[host]:retract(xmlns_s2s_session, host, id, notifier);
109 end
110 end
111
112 function module.add_host(module)
113 -- Dependencies
114 module:depends("bosh");
115 module:depends("admin_adhoc");
116 module:depends("http");
117
118 local http_files = require "net.http.files";
119 local serve_file = http_files.serve {
120 path = module:get_directory() .. "/www_files";
121 };
122
123 -- Setup HTTP server
124 module:provides("http", {
125 title = "Admin Interface";
126 name = "admin";
127 route = {
128 ["GET"] = function(event)
129 event.response.headers.location = event.request.path .. "/";
130 return 301;
131 end;
132 ["GET /*"] = serve_file;
133 }
134 });
135
136 -- Setup adminsub service
137 local function simple_broadcast(kind, node, jids, item)
138 if item then
139 item = st.clone(item);
140 item.attr.xmlns = nil; -- Clear the pubsub namespace
141 end
142 local message = st.message({ from = module.host, type = "headline" })
143 :tag("event", { xmlns = xmlns_adminsub .. "#event" })
144 :tag(kind, { node = node })
145 :add_child(item);
146 for jid in pairs(jids) do
147 module:log("debug", "Sending notification to %s", jid);
148 message.attr.to = jid;
149 module:send(message);
150 end
151 end
152
153 service[module.host] = pubsub.new({
154 broadcaster = simple_broadcast;
155 normalize_jid = jid_bare;
156 -- what was this for? prosody:operator has full rights everywhere, this shoud be fine?
157 -- get_affiliation = function(jid) return get_affiliation(jid, module.host) end;
158 get_affiliation = function() return "owner" end;
159 capabilities = {
160 member = {
161 create = false;
162 publish = false;
163 retract = false;
164 get_nodes = true;
165
166 subscribe = true;
167 unsubscribe = true;
168 get_subscription = true;
169 get_subscriptions = true;
170 get_items = true;
171
172 subscribe_other = false;
173 unsubscribe_other = false;
174 get_subscription_other = false;
175 get_subscriptions_other = false;
176
177 be_subscribed = true;
178 be_unsubscribed = true;
179
180 set_affiliation = false;
181 };
182
183 owner = {
184 create = true;
185 publish = true;
186 retract = true;
187 get_nodes = true;
188
189 subscribe = true;
190 unsubscribe = true;
191 get_subscription = true;
192 get_subscriptions = true;
193 get_items = true;
194
195 subscribe_other = true;
196 unsubscribe_other = true;
197 get_subscription_other = true;
198 get_subscriptions_other = true;
199
200 be_subscribed = true;
201 be_unsubscribed = true;
202
203 set_affiliation = true;
204 };
205 };
206 });
207
208 -- Create node for s2s sessions
209 local ok, err = service[module.host]:create(xmlns_s2s_session, true);
210 if not ok then
211 module:log("warn", "Could not create node %s: %s", xmlns_s2s_session, err);
212 else
213 service[module.host]:set_affiliation(xmlns_s2s_session, true, module.host, "owner")
214 end
215
216 -- Add outgoing s2s sessions
217 for _, session in pairs(hosts[module.host].s2sout) do
218 if session.type ~= "s2sout_unauthed" then
219 add_host(session, "out", module.host);
220 end
221 end
222
223 -- Add incoming s2s sessions
224 for session in pairs(incoming_s2s) do
225 if session.to_host == module.host then
226 add_host(session, "in", module.host);
227 end
228 end
229
230 -- Create node for c2s sessions
231 ok, err = service[module.host]:create(xmlns_c2s_session, true);
232 if not ok then
233 module:log("warn", "Could not create node %s: %s", xmlns_c2s_session, tostring(err));
234 else
235 service[module.host]:set_affiliation(xmlns_c2s_session, true, module.host, "owner")
236 end
237
238 -- Add c2s sessions
239 for _, user in pairs(hosts[module.host].sessions or {}) do
240 for _, session in pairs(user.sessions or {}) do
241 add_client(session, module.host);
242 end
243 end
244
245 -- Register adminsub handler
246 module:hook("iq/host/http://prosody.im/adminsub:adminsub", function(event)
247 -- luacheck: ignore 431/ok
248 local origin, stanza = event.origin, event.stanza;
249 local adminsub = stanza.tags[1];
250 local action = adminsub.tags[1];
251 local reply;
252 if action.name == "subscribe" then
253 local ok, ret = service[module.host]:add_subscription(action.attr.node, stanza.attr.from, stanza.attr.from);
254 if ok then
255 reply = st.reply(stanza)
256 :tag("adminsub", { xmlns = xmlns_adminsub });
257 else
258 reply = st.error_reply(stanza, "cancel", ret);
259 end
260 elseif action.name == "unsubscribe" then
261 local ok, ret = service[module.host]:remove_subscription(action.attr.node, stanza.attr.from, stanza.attr.from);
262 if ok then
263 reply = st.reply(stanza)
264 :tag("adminsub", { xmlns = xmlns_adminsub });
265 else
266 reply = st.error_reply(stanza, "cancel", ret);
267 end
268 elseif action.name == "items" then
269 local node = action.attr.node;
270 local ok, ret = service[module.host]:get_items(node, stanza.attr.from);
271 if not ok then
272 origin.send(st.error_reply(stanza, "cancel", ret));
273 return true;
274 end
275
276 local data = st.stanza("items", { node = node });
277 for _, entry in pairs(ret) do
278 data:add_child(entry);
279 end
280 if data then
281 reply = st.reply(stanza)
282 :tag("adminsub", { xmlns = xmlns_adminsub })
283 :add_child(data);
284 else
285 reply = st.error_reply(stanza, "cancel", "item-not-found");
286 end
287 elseif action.name == "adminfor" then
288 local data = st.stanza("adminfor");
289 for host_name in pairs(hosts) do
290 -- new Permission API:
291 if module:context(host_name):may(":web-operator", stanza.attr.from) then
292 data:tag("item"):text(host_name):up();
293 end
294 end
295 reply = st.reply(stanza)
296 :tag("adminsub", { xmlns = xmlns_adminsub })
297 :add_child(data);
298 else
299 reply = st.error_reply(stanza, "feature-not-implemented");
300 end
301 origin.send(reply);
302 return true;
303 end);
304
305 -- Add/remove c2s sessions
306 module:hook("resource-bind", function(event)
307 add_client(event.session, module.host);
308 end);
309
310 module:hook("resource-unbind", function(event)
311 del_client(event.session, module.host);
312 service[module.host]:remove_subscription(xmlns_c2s_session, module.host, event.session.full_jid);
313 service[module.host]:remove_subscription(xmlns_s2s_session, module.host, event.session.full_jid);
314 end);
315
316 -- Add/remove s2s sessions
317 module:hook("s2sout-established", function(event)
318 add_host(event.session, "out", module.host);
319 end);
320
321 module:hook("s2sin-established", function(event)
322 add_host(event.session, "in", module.host);
323 end);
324
325 module:hook("s2sout-destroyed", function(event)
326 del_host(event.session, "out", module.host);
327 end);
328
329 module:hook("s2sin-destroyed", function(event)
330 del_host(event.session, "in", module.host);
331 end);
332 end