From 76471a0ea9c871b9c092e75255d037347a73f17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BC=D1=8F=D0=BD=20=D0=9C=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= Date: Mon, 28 Nov 2022 14:18:33 -0600 Subject: [PATCH] feat: Modules for implementing visitor nodes. (#12593) * feat: Modules for implementing visitor nodes. Still WIP, uses visitor nodes prosodies where we create the main participants and forward the visitors to watch. Used for huge conferences. * squash: Fix comments. --- resources/prosody-plugins/mod_certs_all.lua | 16 ++ resources/prosody-plugins/mod_fmuc.lua | 138 +++++++++++ .../prosody-plugins/mod_muc_meeting_id.lua | 8 + .../prosody-plugins/mod_xxl_conference.lua | 216 ++++++++++++++++++ 4 files changed, 378 insertions(+) create mode 100644 resources/prosody-plugins/mod_certs_all.lua create mode 100644 resources/prosody-plugins/mod_fmuc.lua create mode 100644 resources/prosody-plugins/mod_xxl_conference.lua diff --git a/resources/prosody-plugins/mod_certs_all.lua b/resources/prosody-plugins/mod_certs_all.lua new file mode 100644 index 000000000..8d716feb6 --- /dev/null +++ b/resources/prosody-plugins/mod_certs_all.lua @@ -0,0 +1,16 @@ +-- validates all certificates, global module +-- Warning: use this only for testing purposes as it will accept all kind of certificates for s2s connections +-- you can use https://modules.prosody.im/mod_s2s_whitelist.html for whitelisting only certain destinations +module:set_global(); + +function attach(event) + local session = event.session; + + session.cert_chain_status = 'valid'; + session.cert_identity_status = 'valid'; + + return true; +end +module:wrap_event('s2s-check-certificate', function (handlers, event_name, event_data) + return attach(event_data); +end); diff --git a/resources/prosody-plugins/mod_fmuc.lua b/resources/prosody-plugins/mod_fmuc.lua new file mode 100644 index 000000000..a867832bf --- /dev/null +++ b/resources/prosody-plugins/mod_fmuc.lua @@ -0,0 +1,138 @@ +--- activate under main muc component +--- Add the following config under the main muc component +--- muc_room_default_presence_broadcast = { +--- visitor = false; +--- participant = true; +--- moderator = true; +--- }; +--- Enable in global modules: 's2s_bidi' +--- Make sure 's2s' is not in modules_disabled +--- TODO: Do we need the /etc/hosts changes? We can drop it for https://modules.prosody.im/mod_s2soutinjection.html +--- In /etc/hosts add: +--- vmmain-ip-address focus.domain.com +--- vmmain-ip-address conference.domain.com +--- vmmain-ip-address domain.com +--- Open port 5269 on the provider side and on the firewall of the machine, so the core node can access this visitor one +local jid = require 'util.jid'; +local st = require 'util.stanza'; + +local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference'); +local main_domain = string.gsub(module.host, muc_domain_prefix..'.', ''); + +local function get_focus_occupant(room) + local focus_occupant = room._data.focus_occupant; + + if focus_occupant then + return focus_occupant; + end + + for _, n_occupant in room:each_occupant() do + if jid.node(n_occupant.jid) == 'focus' then + room._data.focus_occupant = n_occupant; + return n_occupant; + end + end + + return nil; +end + +-- mark all occupants as visitors +module:hook('muc-occupant-pre-join', function (event) + local occupant = event.occupant; + + if jid.host(occupant.bare_jid) == main_domain then + occupant.role = 'visitor'; + end +end, 3); + +-- when occupant is leaving forward presences to jicofo for visitors +-- do not check occupant.role as it maybe already reset +-- if there are no main occupants or no visitors, destroy the room (give 15 seconds of grace period for reconnections) +module:hook('muc-occupant-left', function (event) + local room, occupant = event.room, event.occupant; + local occupant_domain = jid.host(occupant.bare_jid); + + if occupant_domain == main_domain then + local focus_occupant = get_focus_occupant(room); + if not focus_occupant then + module:log('warn', 'No focus found for %s', room.jid); + return; + end + -- Let's forward unavailable presence to the special jicofo + local pr = st.presence({ to = focus_occupant.jid, from = occupant.nick, type = 'unavailable' }) + :tag('x', { xmlns = 'http://jabber.org/protocol/muc#user' }) + :tag('item', { + affiliation = room:get_affiliation(occupant.bare_jid) or 'none'; + role = 'none'; + nick = event.nick; + jid = occupant.bare_jid }):up() + :up(); + room:route_stanza(pr); + end + + if not room.destroying then + if room.xxl_destroy_timer then + room.xxl_destroy_timer:stop(); + end + + room.xxl_destroy_timer = module:add_timer(15, function() + -- let's check are all visitors in the room, if all a visitors - destroy it + -- if all are main-participants also destroy it + local main_count = 0; + local visitors_count = 0; + + for _, o in room:each_occupant() do + -- if there are visitor and main participant there is no point continue + if main_count > 0 and visitors_count > 0 then + return; + end + + if o.role == 'visitor' then + visitors_count = visitors_count + 1; + else + main_count = main_count + 1; + end + end + + if main_count == 0 then + module:log('info', 'Will destroy:%s main_occupants:%s visitors:%s', room.jid, main_count, visitors_count); + room:destroy(nil, 'No main participants.'); + elseif visitors_count == 0 then + module:log('info', 'Will destroy:%s main_occupants:%s visitors:%s', room.jid, main_count, visitors_count); + room:destroy(nil, 'No visitors.'); + end + end); + end +end); + +-- forward visitor presences to jicofo +module:hook('muc-broadcast-presence', function (event) + local occupant = event.occupant; + + ---- we are interested only of visitors presence to send it to jicofo + if occupant.role ~= 'visitor' then + return; + end + + local room = event.room; + local focus_occupant = get_focus_occupant(room); + + if not focus_occupant then + return; + end + + local actor, base_presence, nick, reason, x = event.actor, event.stanza, event.nick, event.reason, event.x; + local actor_nick; + if actor then + actor_nick = jid.resource(room:get_occupant_jid(actor)); + end + + -- create a presence to send it to jicofo, as jicofo is special :) + local full_x = st.clone(x.full or x); + + room:build_item_list(occupant, full_x, false, nick, actor_nick, actor, reason); + local full_p = st.clone(base_presence):add_child(full_x); + full_p.attr.to = focus_occupant.jid; + room:route_to_occupant(focus_occupant, full_p); + return; +end); diff --git a/resources/prosody-plugins/mod_muc_meeting_id.lua b/resources/prosody-plugins/mod_muc_meeting_id.lua index 1d9bc7439..6f6dc14ac 100644 --- a/resources/prosody-plugins/mod_muc_meeting_id.lua +++ b/resources/prosody-plugins/mod_muc_meeting_id.lua @@ -38,3 +38,11 @@ end); module:hook("muc-config-form", function(event) table.insert(event.form, getMeetingIdConfig(event.room)); end, 90-3); + +-- disabled few options for room config, to not mess with visitor logic +module:hook("muc-config-submitted/muc#roomconfig_moderatedroom", function() + return true; +end, 99); +module:hook("muc-config-submitted/muc#roomconfig_presencebroadcast", function() + return true; +end, 99); diff --git a/resources/prosody-plugins/mod_xxl_conference.lua b/resources/prosody-plugins/mod_xxl_conference.lua new file mode 100644 index 000000000..8fa942f2d --- /dev/null +++ b/resources/prosody-plugins/mod_xxl_conference.lua @@ -0,0 +1,216 @@ +--- activate under main vhost +--- In /etc/hosts add: +--- vm1-ip-address visitors1.domain.com +--- vm1-ip-address conference.visitors1.domain.com +--- vm2-ip-address visitors2.domain.com +--- vm2-ip-address conference.visitors2.domain.com +--- TODO: drop the /etc/hosts changes for https://modules.prosody.im/mod_s2soutinjection.html +--- Enable in global modules: 's2s_bidi' and 'certs_all' +--- Make sure 's2s' is not in modules_disabled +--- Open port 5269 on the provider side and on the firewall on the machine (iptables -I INPUT 4 -p tcp -m tcp --dport 5269 -j ACCEPT) +--- TODO: make it work with tenants +local st = require 'util.stanza'; +local jid = require 'util.jid'; +local util = module:require 'util'; +local presence_check_status = util.presence_check_status; + +local um_is_admin = require 'core.usermanager'.is_admin; +local function is_admin(jid) + return um_is_admin(jid, module.host); +end + +local MUC_NS = 'http://jabber.org/protocol/muc'; + +-- get/infer focus component hostname so we can intercept IQ bound for it +local focus_component_host = module:get_option_string('focus_component'); +if not focus_component_host then + local muc_domain_base = module:get_option_string('muc_mapper_domain_base'); + if not muc_domain_base then + module:log('error', 'Could not infer focus domain. Disabling %s', module:get_name()); + return; + end + focus_component_host = 'focus.'..muc_domain_base; +end + +-- required parameter for custom muc component prefix, defaults to 'conference' +local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference'); + +local main_muc_component_config = module:get_option_string('main_muc'); +if main_muc_component_config == nil then + module:log('error', 'xxl rooms not enabled missing main_muc config'); + return ; +end + +-- visitors_nodes = { +-- roomjid1 = { +-- nodes = { +-- ['conference.visitors1.jid'] = 2, // number of main participants, on 0 we clean it +-- ['conference.visitors2.jid'] = 3 +-- } +-- }, +-- roomjid2 = {} +--} +local visitors_nodes = {}; + +--- Intercept conference IQ error from Jicofo. Sends the main participants to the visitor node. +--- Jicofo is connected to the room when sending this error +module:log('info', 'Hook to iq/host'); +module:hook('iq/full', function(event) + local session, stanza = event.origin, event.stanza; + + if stanza.name ~= 'iq' or stanza.attr.type ~= 'error' or stanza.attr.from ~= focus_component_host then + return; -- not IQ from jicofo. Ignore this event. + end + + local error = stanza:get_child('error'); + if error == nil then + return; -- not Conference IQ error. Ignore. + end + + local redirect = error:get_child('redirect', 'urn:ietf:params:xml:ns:xmpp-stanzas'); + local redirect_host = error:get_child_text('url', 'http://jitsi.org/jitmeet'); + + if not redirect or not redirect_host then + return; + end + + -- let's send participants if any from the room to the visitors room + -- TODO fix room name extract, make sure it works wit tenants + local main_room = error:get_child_text('main-room', 'http://jitsi.org/jitmeet'); + + if not main_room then + return; + end + + local room = get_room_from_jid(main_room); + + if room == nil then + return; -- room does not exists. Continue with normal flow + end + + local conference_service = muc_domain_prefix..'.'..redirect_host; + + if visitors_nodes[room.jid] and + visitors_nodes[room.jid].nodes and + visitors_nodes[room.jid].nodes[conference_service] then + -- nothing to do + return; + end + + if visitors_nodes[room.jid] == nil then + visitors_nodes[room.jid] = {}; + end + if visitors_nodes[room.jid].nodes == nil then + visitors_nodes[room.jid].nodes = {}; + end + + local sent_main_participants = 0; + + for _, o in room:each_occupant() do + if not is_admin(o.bare_jid) then + local fmuc_pr = st.clone(o:get_presence()); + local user, _, res = jid.split(o.nick); + fmuc_pr.attr.to = jid.join(user, conference_service , res); + fmuc_pr.attr.from = o.jid; + -- add + fmuc_pr:tag('x', { xmlns = MUC_NS }):up(); + + module:send(fmuc_pr); + + sent_main_participants = sent_main_participants + 1; + end + end + visitors_nodes[room.jid].nodes[conference_service] = sent_main_participants; +end, 900); + +-- takes care when the visitor nodes destroys the room to count the leaving participants from there, and if its really destroyed +-- we clean up, so if we establish again the connection to the same visitor node to send the main participants +module:hook('presence/full', function(event) + local stanza = event.stanza; + local room_name, from_host = jid.split(stanza.attr.from); + if stanza.attr.type == 'unavailable' and from_host ~= main_muc_component_config then + -- TODO tenants??? + local room_jid = jid.join(room_name, main_muc_component_config); -- converts from visitor to main room jid + + local x = stanza:get_child('x', 'http://jabber.org/protocol/muc#user'); + if not presence_check_status(x, '110') then + return; + end + + if visitors_nodes[room_jid] and visitors_nodes[room_jid].nodes + and visitors_nodes[room_jid].nodes[from_host] then + visitors_nodes[room_jid].nodes[from_host] = visitors_nodes[room_jid].nodes[from_host] - 1; + if visitors_nodes[room_jid].nodes[from_host] == 0 then + visitors_nodes[room_jid].nodes[from_host] = nil; + end + end + end +end, 900); + +-- process a host module directly if loaded or hooks to wait for its load +function process_host_module(name, callback) + local function process_host(host) + if host == name then + callback(module:context(host), host); + end + end + + if prosody.hosts[name] == nil then + module:log('debug', 'No host/component found, will wait for it: %s', name) + + -- when a host or component is added + prosody.events.add_handler('host-activated', process_host); + else + process_host(name); + end +end + +process_host_module(main_muc_component_config, function(host_module, host) + -- detects presence change in a main participant and propagate it to the used visitor nodes + host_module:hook('muc-occupant-pre-change', function (event) + local room, stanza, occupant = event.room, event.stanza, event.dest_occupant; + + -- filter focus + if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil then + return; + end + + local vnodes = visitors_nodes[room.jid].nodes; + -- a change in the presence of a main participant we need to update all active visitor nodes + for k in pairs(vnodes) do + local fmuc_pr = st.clone(stanza); + local user, _, res = jid.split(occupant.nick); + fmuc_pr.attr.to = jid.join(user, k, res); + fmuc_pr.attr.from = occupant.jid; + module:send(fmuc_pr); + end + end); + + -- when a main participant leaves inform the visitor nodes + host_module:hook('muc-occupant-left', function (event) + local room, stanza, occupant = event.room, event.stanza, event.occupant; + + if is_admin(occupant.bare_jid) or visitors_nodes[room.jid] == nil or visitors_nodes[room.jid].nodes == nil then + return; + end + + -- we want to update visitor node that a main participant left + if stanza then + local vnodes = visitors_nodes[room.jid].nodes; + for k in pairs(vnodes) do + local fmuc_pr = st.clone(stanza); + local user, _, res = jid.split(occupant.nick); + fmuc_pr.attr.to = jid.join(user, k, res); + fmuc_pr.attr.from = occupant.jid; + module:send(fmuc_pr); + end + else + module:log('warn', 'No unavailable stanza found ... leak participant on visitor'); + end + end); + + -- cleanup cache + host_module:hook('muc-room-destroyed',function(event) + visitors_nodes[event.room.jid] = nil; + end); +end);