From e3e5f1fbfa13a3360d52baab5a4d3a95118b62ff 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, 1 May 2023 17:16:16 -0500 Subject: [PATCH] feat(visitors): Handles locked rooms for visitors. (#13296) * feat(visitors): Handles locked rooms for visitors. * squash: Handle locked room password on promotion. * squash: quotes. * squash: Renames main_domain to local_domain. * squash: Renames fmuc_main_domain to main_domain. Adds required config to point to the main virtual host of the main prosody. There are cases when the first visitor tries to join and there are not main participants as they are in the queue waiting for the vnode connect message and we cannot get dynamically the main domain. * squash: Fix check for main_domain config. --- .../prosody.cfg.lua.visitor.template | 1 + resources/prosody-plugins/mod_fmuc.lua | 140 ++++++++++++++---- resources/prosody-plugins/mod_visitors.lua | 63 +++++++- .../mod_visitors_component.lua | 19 ++- 4 files changed, 186 insertions(+), 37 deletions(-) diff --git a/resources/extra-large-conference/prosody.cfg.lua.visitor.template b/resources/extra-large-conference/prosody.cfg.lua.visitor.template index 263464326e..4b667558bc 100644 --- a/resources/extra-large-conference/prosody.cfg.lua.visitor.template +++ b/resources/extra-large-conference/prosody.cfg.lua.visitor.template @@ -55,6 +55,7 @@ external_services = { }; muc_mapper_domain_base = 'vX.meet.jitsi'; +main_domain = 'jitmeet.example.com'; -- https://prosody.im/doc/modules/mod_smacks smacks_max_unacked_stanzas = 5; diff --git a/resources/prosody-plugins/mod_fmuc.lua b/resources/prosody-plugins/mod_fmuc.lua index 71a92dcab4..48fbfe650c 100644 --- a/resources/prosody-plugins/mod_fmuc.lua +++ b/resources/prosody-plugins/mod_fmuc.lua @@ -20,18 +20,28 @@ local get_room_from_jid = util.get_room_from_jid; local get_focus_occupant = util.get_focus_occupant; local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite; +-- this is the main virtual host of this vnode +local local_domain = module:get_option_string('muc_mapper_domain_base'); +if not local_domain then + module:log('warn', "No 'muc_mapper_domain_base' option set, disabling fmuc plugin"); + return; +end + +-- this is the main virtual host of the main prosody that this vnode serves +local main_domain = module:get_option_string('main_domain'); +if not main_domain then + module:log('warn', "No 'main_domain' option set, disabling fmuc plugin"); + return; +end + local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference'); -local main_domain = string.gsub(module.host, muc_domain_prefix..'.', ''); local NICK_NS = 'http://jabber.org/protocol/nick'; -- we send stats for the total number of rooms, total number of participants and total number of visitors -local measure_rooms = module:measure("vnode-rooms", "amount"); -local measure_participants = module:measure("vnode-participants", "amount"); -local measure_visitors = module:measure("vnode-visitors", "amount"); - --- This is the domain of the main prosody that is federating with us; -local fmuc_main_domain; +local measure_rooms = module:measure('vnode-rooms', 'amount'); +local measure_participants = module:measure('vnode-participants', 'amount'); +local measure_visitors = module:measure('vnode-visitors', 'amount'); local sent_iq_cache = require 'util.cache'.new(200); @@ -45,12 +55,8 @@ module:hook('muc-occupant-pre-join', function (event) local occupant, session = event.occupant, event.origin; local node, host = jid.split(occupant.bare_jid); - if host == main_domain then + if host == local_domain then occupant.role = 'visitor'; - elseif not fmuc_main_domain then - if node ~= 'focus' then - fmuc_main_domain = host; - end end end, 3); @@ -103,7 +109,7 @@ 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 + if occupant_domain == local_domain then local focus_occupant = get_focus_occupant(room); if not focus_occupant then module:log('warn', 'No focus found for %s', room.jid); @@ -181,11 +187,11 @@ module:hook('muc-broadcast-presence', function (event) sent_iq_cache:set(iq_id, socket.gettime()); local promotion_request = st.iq({ type = 'set', - to = 'visitors.'..fmuc_main_domain, - from = main_domain, + to = 'visitors.'..main_domain, + from = local_domain, id = iq_id }) :tag('visitors', { xmlns = 'jitsi:visitors', - room = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..fmuc_main_domain) }) + room = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain) }) :tag('promotion-request', { xmlns = 'jitsi:visitors', jid = occupant.jid }):up(); local nick_element = occupant:get_presence():get_child('nick', NICK_NS); @@ -221,8 +227,8 @@ local function stanza_handler(event) return; end - if stanza.attr.from ~= 'visitors.'..fmuc_main_domain then - module:log('warn', 'not from visitors component, ignore! %s %s', stanza.attr.from, stanza); + if stanza.attr.from ~= 'visitors.'..main_domain then + module:log('warn', 'not from visitors component, ignore! %s', stanza); return true; end @@ -241,7 +247,7 @@ local function stanza_handler(event) -- respond with successful receiving the iq origin.send(st.iq({ - type = "result"; + type = 'result'; from = stanza.attr.to; to = stanza.attr.from; id = stanza.attr.id @@ -276,12 +282,12 @@ function process_host_module(name, callback) process_host(name); end end -process_host_module(main_domain, function(host_module, host) +process_host_module(local_domain, function(host_module, host) host_module:hook('iq/host', stanza_handler, 10); end); -- only live chat is supported for visitors -module:hook("muc-occupant-groupchat", function(event) +module:hook('muc-occupant-groupchat', function(event) local occupant, room, stanza = event.occupant, event.room, event.stanza; local from = stanza.attr.from; local occupant_host = jid.host(occupant.bare_jid); @@ -289,7 +295,7 @@ module:hook("muc-occupant-groupchat", function(event) -- if there is no occupant this is a message from main, probably coming from other vnode if occupant then -- we manage nick only for visitors - if occupant_host ~= fmuc_main_domain then + if occupant_host ~= main_domain then -- add to message stanza display name for the visitor -- remove existing nick to avoid forgery stanza:remove_children('nick', NICK_NS); @@ -310,15 +316,15 @@ module:hook("muc-occupant-groupchat", function(event) -- let's send it to main chat and rest of visitors here for _, o in room:each_occupant() do -- filter remote occupants - if jid.host(o.bare_jid) == main_domain then + if jid.host(o.bare_jid) == local_domain then room:route_to_occupant(o, stanza) end end -- send to main participants only messages from local occupants (skip from remote vnodes) - if occupant and occupant_host ~= fmuc_main_domain then + if occupant and occupant_host ~= main_domain then local main_message = st.clone(stanza); - main_message.attr.to = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..fmuc_main_domain); + main_message.attr.to = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain); module:send(main_message); end stanza.attr.from = from; -- something prosody does internally @@ -328,20 +334,20 @@ end, 55); -- prosody check for visitor's chat is prio 50, we want to override it module:hook('muc-private-message', function(event) -- private messaging is forbidden - event.origin.send(st.error_reply(event.stanza, "auth", "forbidden", - "Private messaging is disabled on visitor nodes")); + event.origin.send(st.error_reply(event.stanza, 'auth', 'forbidden', + 'Private messaging is disabled on visitor nodes')); return true; end, 10); -- we calculate the stats on the configured interval (60 seconds by default) -module:hook_global("stats-update", function () +module:hook_global('stats-update', function () local participants_count, rooms_count, visitors_count = 0, 0, 0; -- iterate over all rooms for room in prosody.hosts[module.host].modules.muc.each_room() do rooms_count = rooms_count + 1; for _, o in room:each_occupant() do - if jid.host(o.bare_jid) == main_domain then + if jid.host(o.bare_jid) == local_domain then visitors_count = visitors_count + 1; else participants_count = participants_count + 1; @@ -356,4 +362,80 @@ module:hook_global("stats-update", function () measure_participants(participants_count); end); +-- we skip it till the main participants are added from the main prosody +module:hook('jicofo-unlock-room', function(e) + -- we do not block events we fired + if e.fmuc_fired then + return; + end + return true; +end); + +-- handles incoming iq connect stanzas +local function iq_from_main_handler(event) + local origin, stanza = event.origin, event.stanza; + + if stanza.name ~= 'iq' then + return; + end + + if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then + sent_iq_cache:set(stanza.attr.id, nil); + return true; + end + + if stanza.attr.type ~= 'set' then + return; + end + + local visitors_iq = event.stanza:get_child('visitors', 'jitsi:visitors'); + if not visitors_iq then + return; + end + + if stanza.attr.from ~= main_domain then + module:log('warn', 'not from main prosody, ignore! %s', stanza); + return true; + end + + local room_jid = visitors_iq.attr.room; + local room = get_room_from_jid(room_jid_match_rewrite(room_jid)); + + if not room then + module:log('warn', 'No room found %s', room_jid); + return; + end + + local node = visitors_iq:get_child('connect'); + local fire_jicofo_unlock = true; + + if not node then + node = visitors_iq:get_child('update'); + fire_jicofo_unlock = false; + end + + if not node then + return; + end + + -- respond with successful receiving the iq + origin.send(st.iq({ + type = 'result'; + from = stanza.attr.to; + to = stanza.attr.from; + id = stanza.attr.id + })); + + -- if there is password supplied use it + -- if this is update it will either set or remove the password + room:set_password(node.attr.password); + + if fire_jicofo_unlock then + -- everything is connected allow participants to join + module:fire_event('jicofo-unlock-room', { room = room; fmuc_fired = true; }); + end + + return true; +end +module:hook('iq/host', iq_from_main_handler, 10); diff --git a/resources/prosody-plugins/mod_visitors.lua b/resources/prosody-plugins/mod_visitors.lua index 9d5449c870..78e2f72ed3 100644 --- a/resources/prosody-plugins/mod_visitors.lua +++ b/resources/prosody-plugins/mod_visitors.lua @@ -10,6 +10,7 @@ --- NOTE: Make sure all communication between prosodies is using the real jids ([foo]room1@muc.example.com) local st = require 'util.stanza'; local jid = require 'util.jid'; +local new_id = require 'util.id'.medium; local util = module:require 'util'; local presence_check_status = util.presence_check_status; @@ -36,6 +37,8 @@ local ignore_list = module:get_option_set('visitors_ignore_list', {}); -- Advertise the component for discovery via disco#items module:add_identity('component', 'visitors', 'visitors.'..module.host); +local sent_iq_cache = require 'util.cache'.new(200); + -- visitors_nodes = { -- roomjid1 = { -- nodes = { @@ -47,6 +50,24 @@ module:add_identity('component', 'visitors', 'visitors.'..module.host); --} local visitors_nodes = {}; +-- sends connect or update iq +-- @parameter type - Type of iq to send 'connect' or 'update' +local function send_visitors_iq(conference_service, room, type) + -- send iq informing the vnode that the connect is done and it will allow visitors to join + local iq_id = new_id(); + sent_iq_cache:set(iq_id, socket.gettime()); + local connect_done = st.iq({ + type = 'set', + to = conference_service, + from = module.host, + id = iq_id }) + :tag('visitors', { xmlns = 'jitsi:visitors', + room = jid.join(jid.node(room.jid), conference_service) }) + :tag(type, { xmlns = 'jitsi:visitors', password = room:get_password() or '' }):up(); + + module:send(connect_done); +end + -- an event received from visitors component, which receives iqs from jicofo local function connect_vnode(event) local room, vnode = event.room, event.vnode; @@ -75,7 +96,15 @@ local function connect_vnode(event) 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(); + fmuc_pr:tag('x', { xmlns = MUC_NS }); + + -- if there is a password on the main room let's add the password for the vnode join + -- as we will set the password to the vnode room and we will need it + local pass = room:get_password(); + if pass and pass ~= '' then + fmuc_pr:tag('password'):text(pass); + end + fmuc_pr:up(); module:send(fmuc_pr); @@ -83,9 +112,26 @@ local function connect_vnode(event) end end visitors_nodes[room.jid].nodes[conference_service] = sent_main_participants; + + send_visitors_iq(conference_service, room, 'connect'); end module:hook('jitsi-connect-vnode', connect_vnode); +-- listens for responses to the iq sent for connecting vnode +local function stanza_handler(event) + local origin, stanza = event.origin, event.stanza; + + if stanza.name ~= 'iq' then + return; + end + + if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then + sent_iq_cache:set(stanza.attr.id, nil); + return true; + end +end +module:hook('iq/host', stanza_handler, 10); + -- an event received from visitors component, which receives iqs from jicofo local function disconnect_vnode(event) local room, vnode = event.room, event.vnode; @@ -211,7 +257,7 @@ process_host_module(main_muc_component_config, function(host_module, host) end end); -- forwards messages from main participants to vnodes - host_module:hook("muc-occupant-groupchat", function(event) + host_module:hook('muc-occupant-groupchat', function(event) local room, stanza, occupant = event.room, event.stanza, event.occupant; -- filter sending messages from transcribers/jibris to visitors @@ -231,7 +277,7 @@ process_host_module(main_muc_component_config, function(host_module, host) end); -- receiving messages from visitor nodes and forward them to local main participants -- and forward them to the rest of visitor nodes - host_module:hook("muc-occupant-groupchat", function(event) + host_module:hook('muc-occupant-groupchat', function(event) local occupant, room, stanza = event.occupant, event.room, event.stanza; local to = stanza.attr.to; local from = stanza.attr.from; @@ -260,4 +306,15 @@ process_host_module(main_muc_component_config, function(host_module, host) return true; end, 55); -- prosody check for unknown participant chat is prio 50, we want to override it + + host_module:hook('muc-config-submitted/muc#roomconfig_roomsecret', function(event) + if event.status_codes['104'] then + local room = event.room; + -- we need to update all vnodes + local vnodes = visitors_nodes[room.jid].nodes; + for conference_service in pairs(vnodes) do + send_visitors_iq(conference_service, room, 'update'); + end + end +end, -100); -- we want to run last in order to check is the status code 104 end); diff --git a/resources/prosody-plugins/mod_visitors_component.lua b/resources/prosody-plugins/mod_visitors_component.lua index 90849b3ebd..42c86b1b79 100644 --- a/resources/prosody-plugins/mod_visitors_component.lua +++ b/resources/prosody-plugins/mod_visitors_component.lua @@ -8,7 +8,9 @@ local get_room_from_jid = util.get_room_from_jid; local get_focus_occupant = util.get_focus_occupant; local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain; local new_id = require 'util.id'.medium; -local um_is_admin = require "core.usermanager".is_admin; +local um_is_admin = require 'core.usermanager'.is_admin; + +local MUC_NS = 'http://jabber.org/protocol/muc'; local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference'); local muc_domain_base = module:get_option_string('muc_mapper_domain_base'); @@ -17,7 +19,7 @@ if not muc_domain_base then return; end -local auto_allow_promotion = module:get_option_boolean("auto_allow_visitor_promotion", false); +local auto_allow_promotion = module:get_option_boolean('auto_allow_visitor_promotion', false); local function is_admin(jid) return um_is_admin(jid, module.host); @@ -33,7 +35,7 @@ local sent_iq_cache = require 'util.cache'.new(200); local function respond_iq_result(origin, stanza) -- respond with successful receiving the iq origin.send(st.iq({ - type = "result"; + type = 'result'; from = stanza.attr.to; to = stanza.attr.from; id = stanza.attr.id @@ -158,7 +160,7 @@ local function stanza_handler(event) return processed; end -module:hook("iq/host", stanza_handler, 10); +module:hook('iq/host', stanza_handler, 10); --process a host module directly if loaded or hooks to wait for its load function process_host_module(name, callback) @@ -184,6 +186,13 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul host_module:hook('muc-occupant-pre-join', function (event) local room, stanza, origin = event.room, event.stanza, event.origin; + -- visitors were already in the room one way or another they have access + -- skip password challenge + local join = stanza:get_child('x', MUC_NS); + if join and room:get_password() then + join:tag('password', { xmlns = MUC_NS }):text(room:get_password()); + end + -- we skip any checks when auto-allow is enabled if auto_allow_promotion then return; @@ -208,7 +217,7 @@ end); -- enable only in case of auto-allow is enabled if auto_allow_promotion then - prosody.events.add_handler("pre-jitsi-authentication", function(session) + prosody.events.add_handler('pre-jitsi-authentication', function(session) if not session.customusername or not session.jitsi_web_query_room then return nil; end