From eb0714be6faf451f3e42915c5e045964ef4e813c Mon Sep 17 00:00:00 2001 From: Daniel Berteaud Date: Mon, 16 Jun 2025 16:00:13 +0200 Subject: [PATCH] Update to 2025-06-16 16:00 --- roles/consul_bin/defaults/main.yml | 4 +- roles/consul_template/defaults/main.yml | 4 +- roles/docker/defaults/main.yml | 5 + roles/jitsi/defaults/main.yml | 6 +- .../files/prosody/modules/luajwtjitsi.lib.lua | 6 +- .../files/prosody/modules/mod_auth_token.lua | 8 +- .../modules/mod_av_moderation_component.lua | 34 ++- .../prosody/modules/mod_filter_iq_jibri.lua | 60 ++++- .../prosody/modules/mod_filter_iq_rayo.lua | 184 ++++++++------- .../prosody/modules/mod_filter_messages.lua | 42 ++++ .../jitsi/files/prosody/modules/mod_fmuc.lua | 187 ++++++++++++--- .../prosody/modules/mod_jibri_session.lua | 8 +- .../prosody/modules/mod_jitsi_permissions.lua | 201 +++++++++++++++++ .../prosody/modules/mod_jitsi_session.lua | 3 + .../prosody/modules/mod_muc_allowners.lua | 18 +- .../modules/mod_muc_breakout_rooms.lua | 55 ++++- .../files/prosody/modules/mod_muc_flip.lua | 6 +- .../prosody/modules/mod_muc_hide_all.lua | 26 ++- .../modules/mod_muc_kick_participant.lua | 171 ++++++++++++++ .../prosody/modules/mod_muc_lobby_rooms.lua | 4 + .../prosody/modules/mod_muc_meeting_id.lua | 141 ++++++++++-- .../prosody/modules/mod_muc_wait_for_host.lua | 6 +- .../jitsi/files/prosody/modules/mod_polls.lua | 70 +++--- .../prosody/modules/mod_presence_identity.lua | 13 +- .../files/prosody/modules/mod_rate_limit.lua | 14 +- .../modules/mod_room_metadata_component.lua | 200 +++++++++++++---- .../prosody/modules/mod_short_lived_token.lua | 136 +++++++++++ .../modules/mod_speakerstats_component.lua | 17 +- .../modules/mod_system_chat_message.lua | 1 - .../modules/mod_token_verification.lua | 8 +- .../files/prosody/modules/mod_visitors.lua | 63 ++++-- .../modules/mod_visitors_component.lua | 212 ++++++++++++++---- .../files/prosody/modules/token/util.lib.lua | 4 +- .../jitsi/files/prosody/modules/util.lib.lua | 187 +++++++++++++-- roles/jitsi/templates/prosody.cfg.lua.j2 | 1 + roles/jitsi_videobridge/defaults/main.yml | 4 +- .../templates/postgresql-client.repo.j2 | 2 +- 37 files changed, 1718 insertions(+), 393 deletions(-) create mode 100644 roles/jitsi/files/prosody/modules/mod_filter_messages.lua create mode 100644 roles/jitsi/files/prosody/modules/mod_jitsi_permissions.lua create mode 100644 roles/jitsi/files/prosody/modules/mod_muc_kick_participant.lua create mode 100644 roles/jitsi/files/prosody/modules/mod_short_lived_token.lua diff --git a/roles/consul_bin/defaults/main.yml b/roles/consul_bin/defaults/main.yml index 66c0d12..ccc7967 100644 --- a/roles/consul_bin/defaults/main.yml +++ b/roles/consul_bin/defaults/main.yml @@ -1,8 +1,8 @@ --- # Version of consul to deploy -consul_version: 1.21.0 +consul_version: 1.21.1 # URL from where the consul archive will be downloaded consul_archive_url: https://releases.hashicorp.com/consul/{{ consul_version }}/consul_{{ consul_version }}_linux_amd64.zip # Expected sha256 of the archive -consul_archive_sha256: e916e30904eedfa7ee2e2a378b5e8a9a374f2f351e645aa4c0a03adc15dabaec +consul_archive_sha256: cf5b8d429c67d4e3c86e2f52eb3245ee00119a9a389f2af36a77b16b1e1eb27c diff --git a/roles/consul_template/defaults/main.yml b/roles/consul_template/defaults/main.yml index ee180a1..9adae52 100644 --- a/roles/consul_template/defaults/main.yml +++ b/roles/consul_template/defaults/main.yml @@ -1,11 +1,11 @@ --- # Version of consul-template to install -consul_tpl_version: 0.40.0 +consul_tpl_version: 0.41.0 # URL of the archive consul_tpl_archive_url: https://releases.hashicorp.com/consul-template/{{ consul_tpl_version }}/consul-template_{{ consul_tpl_version }}_linux_amd64.zip # Expected sha256 of the archive -consul_tpl_archive_sha256: f73cb36988b9aaccb0ac918df26c854ccd199e60c0df011357405672f3d934bc +consul_tpl_archive_sha256: 64e732cdd75a778ea6a5e16b32792a1effc88963d37e73f0088a115ea790938f # Root dir where consul-template will be installed consul_tpl_root_dir: /opt/consul_template diff --git a/roles/docker/defaults/main.yml b/roles/docker/defaults/main.yml index 54134cf..db49d63 100644 --- a/roles/docker/defaults/main.yml +++ b/roles/docker/defaults/main.yml @@ -7,6 +7,11 @@ docker_base_conf: data-root: /opt/docker log-driver: journald storage-driver: overlay2 + default-ulimits: + nofile: + Hard: 1048576 + Soft: 1048576 + Name: nofile docker_extra_conf: {} # docker_extra_conf: # log-opts: diff --git a/roles/jitsi/defaults/main.yml b/roles/jitsi/defaults/main.yml index 7c22637..bc1848f 100644 --- a/roles/jitsi/defaults/main.yml +++ b/roles/jitsi/defaults/main.yml @@ -9,16 +9,16 @@ jitsi_user: jitsi jitsi_web_src_ip: - 0.0.0.0/0 -jitsi_version: 10133 +jitsi_version: 10314 jitsi_jicofo_archive_url: https://github.com/jitsi/jicofo/archive/refs/tags/stable/jitsi-meet_{{ jitsi_version }}.tar.gz -jitsi_jicofo_archive_sha256: a233b30fbbb41c30cdef0bbfbf971d7dcb3b0ce96a54f16a4bf9e1b7633f31fc +jitsi_jicofo_archive_sha256: 0e081653d525462bfa1358ff6a25b091636792c4ac4a4fbf0b6235951d7cc4ac # Jigasi has no release, nor tags, so use master jitsi_jigasi_archive_url: https://github.com/jitsi/jigasi/archive/refs/heads/master.tar.gz jitsi_meet_archive_url: https://github.com/jitsi/jitsi-meet/archive/refs/tags/stable/jitsi-meet_{{ jitsi_version }}.tar.gz -jitsi_meet_archive_sha256: c1ed6ff9546fe681ac3996b49bee22df420350f406468408acbe58491d95c0d9 +jitsi_meet_archive_sha256: 04770928b232fb794206083f9f4cdfe24112ce8dd57e84c253380afb39ebc5d3 jitsi_excalidraw_version: 2025.2.2 jitsi_excalidraw_archive_url: https://github.com/jitsi/excalidraw-backend/archive/refs/tags/{{ jitsi_excalidraw_version }}.tar.gz diff --git a/roles/jitsi/files/prosody/modules/luajwtjitsi.lib.lua b/roles/jitsi/files/prosody/modules/luajwtjitsi.lib.lua index 990b1e6..d14fea4 100644 --- a/roles/jitsi/files/prosody/modules/luajwtjitsi.lib.lua +++ b/roles/jitsi/files/prosody/modules/luajwtjitsi.lib.lua @@ -226,7 +226,11 @@ function M.verify(token, expectedAlgo, key, acceptedIssuers, acceptedAudiences) if body.exp and os.time() >= body.exp then - return nil, "Not acceptable by exp ("..tostring(os.time()-body.exp)..")" + local extra_msg = ''; + if body.iat then + extra_msg = ", valid for:"..tostring(body.exp-body.iat).." sec"; + end + return nil, "Not acceptable by exp ("..tostring(os.time()-body.exp).." sec since expired"..extra_msg..")" end if body.nbf and os.time() < body.nbf then diff --git a/roles/jitsi/files/prosody/modules/mod_auth_token.lua b/roles/jitsi/files/prosody/modules/mod_auth_token.lua index 562be90..d18e2dc 100644 --- a/roles/jitsi/files/prosody/modules/mod_auth_token.lua +++ b/roles/jitsi/files/prosody/modules/mod_auth_token.lua @@ -48,13 +48,14 @@ function init_session(event) -- After validating auth_token will be cleaned in case of error and few -- other fields will be extracted from the token and set in the session - if query and params.token then + if params and params.token then token = params.token; end end -- in either case set auth_token in the session session.auth_token = token; + session.user_agent_header = request.headers['user_agent']; end module:hook_global("bosh-session", init_session); @@ -101,8 +102,9 @@ function provider.get_sasl_handler(session) local res, error, reason = token_util:process_and_verify_token(session); if res == false then module:log("warn", - "Error verifying token err:%s, reason:%s tenant:%s room:%s", - error, reason, session.jitsi_web_query_prefix, session.jitsi_web_query_room); + "Error verifying token err:%s, reason:%s tenant:%s room:%s user_agent:%s", + error, reason, session.jitsi_web_query_prefix, session.jitsi_web_query_room, + session.user_agent_header); session.auth_token = nil; measure_verify_fail(1); return res, error, reason; diff --git a/roles/jitsi/files/prosody/modules/mod_av_moderation_component.lua b/roles/jitsi/files/prosody/modules/mod_av_moderation_component.lua index efa61a4..fa65670 100644 --- a/roles/jitsi/files/prosody/modules/mod_av_moderation_component.lua +++ b/roles/jitsi/files/prosody/modules/mod_av_moderation_component.lua @@ -4,6 +4,7 @@ local is_healthcheck_room = util.is_healthcheck_room; local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite; local room_jid_match_rewrite = util.room_jid_match_rewrite; local process_host_module = util.process_host_module; +local table_shallow_copy = util.table_shallow_copy; local array = require "util.array"; local json = require 'cjson.safe'; local st = require 'util.stanza'; @@ -47,7 +48,7 @@ function notify_occupants_enable(jid, enable, room, actorJid, mediaType) body_json.type = 'av_moderation'; body_json.enabled = enable; body_json.room = internal_room_jid_match_rewrite(room.jid); - body_json.actor = actorJid; + body_json.actor = internal_room_jid_match_rewrite(actorJid); body_json.mediaType = mediaType; local body_json_str, error = json.encode(body_json); @@ -75,11 +76,20 @@ function notify_whitelist_change(jid, moderators, room, mediaType, removed) local body_json = {}; body_json.type = 'av_moderation'; body_json.room = internal_room_jid_match_rewrite(room.jid); - body_json.whitelists = room.av_moderation; + -- we will be modifying it, so we need a copy + body_json.whitelists = table_shallow_copy(room.av_moderation); if removed then body_json.removed = true; end body_json.mediaType = mediaType; + + -- sanitize, make sure we don't have an empty array as it will encode it as {} not as [] + for _,mediaType in pairs({'audio', 'video'}) do + if body_json.whitelists[mediaType] and #body_json.whitelists[mediaType] == 0 then + body_json.whitelists[mediaType] = nil; + end + end + local moderators_body_json_str, error = json.encode(body_json); if not moderators_body_json_str then @@ -197,6 +207,20 @@ function on_message(event) room.av_moderation_actors = {}; end room.av_moderation[mediaType] = array{}; + + -- We want to set startMuted policy in metadata, in case of new participants are joining to respect + -- it, that will be enforced by jicofo + local startMutedMetadata = room.jitsiMetadata.startMuted or {}; + + -- We want to keep the previous value of startMuted for this mediaType if av moderation is disabled + -- to be able to restore + local av_moderation_startMuted_restore = room.av_moderation_startMuted_restore or {}; + av_moderation_startMuted_restore = startMutedMetadata[mediaType]; + room.av_moderation_startMuted_restore = av_moderation_startMuted_restore; + + startMutedMetadata[mediaType] = true; + room.jitsiMetadata.startMuted = startMutedMetadata; + room.av_moderation_actors[mediaType] = occupant.nick; end else @@ -208,7 +232,11 @@ function on_message(event) room.av_moderation[mediaType] = nil; room.av_moderation_actors[mediaType] = nil; - -- clears room.av_moderation if empty + local startMutedMetadata = room.jitsiMetadata.startMuted or {}; + local av_moderation_startMuted_restore = room.av_moderation_startMuted_restore or {}; + startMutedMetadata[mediaType] = av_moderation_startMuted_restore[mediaType]; + room.jitsiMetadata.startMuted = startMutedMetadata; + local is_empty = true; for key,_ in pairs(room.av_moderation) do if room.av_moderation[key] then diff --git a/roles/jitsi/files/prosody/modules/mod_filter_iq_jibri.lua b/roles/jitsi/files/prosody/modules/mod_filter_iq_jibri.lua index b56cf54..8df669f 100644 --- a/roles/jitsi/files/prosody/modules/mod_filter_iq_jibri.lua +++ b/roles/jitsi/files/prosody/modules/mod_filter_iq_jibri.lua @@ -1,5 +1,27 @@ +-- This module is enabled under the main virtual host +local cache = require 'util.cache'; +local new_throttle = require 'util.throttle'.create; local st = require "util.stanza"; -local is_feature_allowed = module:require "util".is_feature_allowed; +local jid_bare = require "util.jid".bare; +local util = module:require 'util'; +local is_feature_allowed = util.is_feature_allowed; +local get_ip = util.get_ip; +local get_room_from_jid = util.get_room_from_jid; +local room_jid_match_rewrite = util.room_jid_match_rewrite; + +local limit_jibri_reach_ip_attempts; +local limit_jibri_reach_room_attempts; +local rates_per_ip; +local function load_config() + limit_jibri_reach_ip_attempts = module:get_option_number("max_number_ip_attempts_per_minute", 9); + limit_jibri_reach_room_attempts = module:get_option_number("max_number_room_attempts_per_minute", 3); + -- The size of the cache that saves state for IP addresses + cache_size = module:get_option_number("jibri_rate_limit_cache_size", 10000); + + -- Maps an IP address to a util.throttle which keeps the rate of attempts to reach jibri events from that IP. + rates_per_ip = cache.new(cache_size); +end +load_config(); -- filters jibri iq in case of requested from jwt authenticated session that -- has features in the user context, but without feature for recording @@ -10,15 +32,35 @@ module:hook("pre-iq/full", function(event) if jibri then local session = event.origin; local token = session.auth_token; + local room = get_room_from_jid(room_jid_match_rewrite(jid_bare(stanza.attr.to))); + local occupant = room:get_occupant_by_real_jid(stanza.attr.from); + local feature = jibri.attr.recording_mode == 'file' and 'recording' or 'livestreaming'; + local is_allowed = is_feature_allowed( + feature, + session.jitsi_meet_context_features, + session.granted_jitsi_meet_context_features, + occupant.role == 'moderator'); - if jibri.attr.action == 'start' then - if token == nil - or not is_feature_allowed(session.jitsi_meet_context_features, - (jibri.attr.recording_mode == 'file' and 'recording' or 'livestreaming') - ) then - module:log("info", - "Filtering jibri start recording, stanza:%s", tostring(stanza)); - session.send(st.error_reply(stanza, "auth", "forbidden")); + if jibri.attr.action == 'start' or jibri.attr.action == 'stop' then + if not is_allowed then + module:log('info', 'Filtering jibri start recording, stanza:%s', tostring(stanza)); + session.send(st.error_reply(stanza, 'auth', 'forbidden')); + return true; + end + + local ip = get_ip(session); + if not rates_per_ip:get(ip) then + rates_per_ip:set(ip, new_throttle(limit_jibri_reach_ip_attempts, 60)); + end + + if not room.jibri_throttle then + room.jibri_throttle = new_throttle(limit_jibri_reach_room_attempts, 60); + end + + if not rates_per_ip:get(ip):poll(1) or not room.jibri_throttle:poll(1) then + module:log('warn', 'Filtering jibri start recording, ip:%s, room:%s stanza:%s', + ip, room.jid, tostring(stanza)); + session.send(st.error_reply(stanza, 'wait', 'policy-violation')); return true; end end diff --git a/roles/jitsi/files/prosody/modules/mod_filter_iq_rayo.lua b/roles/jitsi/files/prosody/modules/mod_filter_iq_rayo.lua index 86bd0cc..dbeb0a8 100644 --- a/roles/jitsi/files/prosody/modules/mod_filter_iq_rayo.lua +++ b/roles/jitsi/files/prosody/modules/mod_filter_iq_rayo.lua @@ -1,13 +1,14 @@ +-- This module is enabled under the main virtual host local new_throttle = require "util.throttle".create; local st = require "util.stanza"; +local jid = require "util.jid"; -local token_util = module:require "token/util".new(module); local util = module:require 'util'; +local is_admin = util.is_admin; local room_jid_match_rewrite = util.room_jid_match_rewrite; local is_feature_allowed = util.is_feature_allowed; local is_sip_jigasi = util.is_sip_jigasi; local get_room_from_jid = util.get_room_from_jid; -local is_healthcheck_room = util.is_healthcheck_room; local process_host_module = util.process_host_module; local jid_bare = require "util.jid".bare; @@ -22,17 +23,30 @@ if main_muc_component_host == nil then end local main_muc_service; + +-- this is the main virtual host of the main prosody that this vnode serves +local main_domain = module:get_option_string('main_domain'); +-- only the visitor prosody has main_domain setting +local is_visitor_prosody = main_domain ~= nil; + +-- this is the main virtual host of this vnode +local local_domain = module:get_option_string('muc_mapper_domain_base'); + +local parentCtx = module:context(local_domain); +if parentCtx == nil then + log("error", + "Failed to start - unable to get parent context for host: %s", + tostring(local_domain)); + return; +end +local token_util = module:require "token/util".new(parentCtx); + -- no token configuration but required if token_util == nil then module:log("error", "no token configuration but it is required"); return; end -local um_is_admin = require 'core.usermanager'.is_admin; -local function is_admin(jid) - return um_is_admin(jid, module.host); -end - -- The maximum number of simultaneous calls, -- and also the maximum number of new calls per minute that a session is allowed to create. local limit_outgoing_calls; @@ -44,6 +58,8 @@ load_config(); -- Header names to use to push extra data extracted from token, if any local OUT_INITIATOR_USER_ATTR_NAME = "X-outbound-call-initiator-user"; local OUT_INITIATOR_GROUP_ATTR_NAME = "X-outbound-call-initiator-group"; +local OUT_ROOM_NAME_ATTR_NAME = "JvbRoomName"; + local OUTGOING_CALLS_THROTTLE_INTERVAL = 60; -- if max_number_outgoing_calls is enabled it will be -- the max number of outgoing calls a user can try for a minute @@ -59,30 +75,45 @@ module:hook("pre-iq/full", function(event) local token = session.auth_token; -- find header with attr name 'JvbRoomName' and extract its value - local headerName = 'JvbRoomName'; local roomName; - for _, child in ipairs(dial.tags) do - if (child.name == 'header' - and child.attr.name == headerName) then - roomName = child.attr.value; - break; + -- Remove any 'header' element if it already exists, so it cannot be spoofed by a client + dial:maptags(function(tag) + if tag.name == "header" + and (tag.attr.name == OUT_INITIATOR_USER_ATTR_NAME + or tag.attr.name == OUT_INITIATOR_GROUP_ATTR_NAME) then + return nil + elseif tag.name == "header" and tag.attr.name == OUT_ROOM_NAME_ATTR_NAME then + roomName = tag.attr.value; + -- we will remove it as we will add it later, modified + if is_visitor_prosody then + return nil; + end end + return tag + end); + + local room_jid = jid.bare(stanza.attr.to); + local room_real_jid = room_jid_match_rewrite(room_jid); + local room = main_muc_service.get_room_from_jid(room_real_jid); + local is_sender_in_room = room:get_occupant_jid(stanza.attr.from) ~= nil; + + if not room or not is_sender_in_room then + module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza)); + session.send(st.error_reply(stanza, "auth", "forbidden")); + return true; end local feature = dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' or 'outbound-call'; - local is_session_allowed = is_feature_allowed(session.jitsi_meet_context_features, feature); + local is_session_allowed = is_feature_allowed( + feature, + session.jitsi_meet_context_features, + session.granted_jitsi_meet_context_features, + room:get_affiliation(stanza.attr.from) == 'owner'); - -- if current user is not allowed, but was granted moderation by a user - -- that is allowed by its features we want to allow it - local is_granting_session_allowed = false; - if (session.granted_jitsi_meet_context_features) then - is_granting_session_allowed = is_feature_allowed(session.granted_jitsi_meet_context_features, feature); - end - - if (token == nil - or roomName == nil - or not token_util:verify_room(session, room_jid_match_rewrite(roomName)) - or not (is_session_allowed or is_granting_session_allowed)) + if roomName == nil + or roomName ~= room_jid + or (token ~= nil and not token_util:verify_room(session, room_real_jid)) + or not is_session_allowed then module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza)); session.send(st.error_reply(stanza, "auth", "forbidden")); @@ -99,8 +130,8 @@ module:hook("pre-iq/full", function(event) group_id = session.granted_jitsi_meet_context_group_id; end - -- now lets check any limits if configured - if limit_outgoing_calls > 0 then + -- now lets check any limits for outgoing calls if configured + if feature == 'outbound-call' and limit_outgoing_calls > 0 then if not session.dial_out_throttle then -- module:log("debug", "Enabling dial-out throttle session=%s.", session); session.dial_out_throttle = new_throttle(limit_outgoing_calls, OUTGOING_CALLS_THROTTLE_INTERVAL); @@ -119,25 +150,11 @@ module:hook("pre-iq/full", function(event) -- now lets insert token information if any if session and user_id then - -- First remove any 'header' element if it already - -- exists, so it cannot be spoofed by a client - stanza:maptags( - function(tag) - if tag.name == "header" - and (tag.attr.name == OUT_INITIATOR_USER_ATTR_NAME - or tag.attr.name == OUT_INITIATOR_GROUP_ATTR_NAME) then - return nil - end - return tag - end - ) - - local dial = stanza:get_child('dial', 'urn:xmpp:rayo:1'); -- adds initiator user id from token dial:tag("header", { xmlns = "urn:xmpp:rayo:1", name = OUT_INITIATOR_USER_ATTR_NAME, - value = user_id }); + value = tostring(user_id)}); dial:up(); -- Add the initiator group information if it is present @@ -145,13 +162,22 @@ module:hook("pre-iq/full", function(event) dial:tag("header", { xmlns = "urn:xmpp:rayo:1", name = OUT_INITIATOR_GROUP_ATTR_NAME, - value = session.jitsi_meet_context_group }); + value = tostring(session.jitsi_meet_context_group) }); dial:up(); end end + + -- we want to instruct jigasi to enter the main room, so send the correct main room jid + if is_visitor_prosody then + dial:tag("header", { + xmlns = "urn:xmpp:rayo:1", + name = OUT_ROOM_NAME_ATTR_NAME, + value = string.gsub(roomName, local_domain, main_domain) }); + dial:up(); + end end end -end); +end, 1); -- make sure we run before domain mapper --- Finds and returns the number of concurrent outgoing calls for a user -- @param context_user the user id extracted from the token @@ -200,49 +226,10 @@ end module:hook_global('config-reloaded', load_config); -function process_set_affiliation(event) - local actor, affiliation, jid, previous_affiliation, room - = event.actor, event.affiliation, event.jid, event.previous_affiliation, event.room; - local actor_session = sessions[actor]; - - if is_admin(jid) or is_healthcheck_room(room.jid) or not actor or not previous_affiliation - or not actor_session or not actor_session.jitsi_meet_context_features then - return; - end - - local occupant; - for _, o in room:each_occupant() do - if o.bare_jid == jid then - occupant = o; - end - end - - if not occupant then - return; - end - - local occupant_session = sessions[occupant.jid]; - if not occupant_session then - return; - end - - if previous_affiliation == 'none' and affiliation == 'owner' then - occupant_session.granted_jitsi_meet_context_features = actor_session.jitsi_meet_context_features; - occupant_session.granted_jitsi_meet_context_user_id = actor_session.jitsi_meet_context_user["id"]; - occupant_session.granted_jitsi_meet_context_group_id = actor_session.jitsi_meet_context_group; - elseif previous_affiliation == 'owner' and ( affiliation == 'member' or affiliation == 'none' ) then - occupant_session.granted_jitsi_meet_context_features = nil; - occupant_session.granted_jitsi_meet_context_user_id = nil; - occupant_session.granted_jitsi_meet_context_group_id = nil; - end -end - function process_main_muc_loaded(main_muc, host_module) module:log('debug', 'Main muc loaded'); main_muc_service = main_muc; - module:log("info", "Hook to muc events on %s", main_muc_component_host); - host_module:hook("muc-pre-set-affiliation", process_set_affiliation); end process_host_module(main_muc_component_host, function(host_module, host) @@ -259,3 +246,34 @@ process_host_module(main_muc_component_host, function(host_module, host) end); end end); + +-- when recording participants may enable and backend transcriptions +-- it is possible that participant is not moderator, but has the features enabled for +-- transcribing, we need to allow that operation +module:hook('jitsi-metadata-allow-moderation', function (event) + local data, key, occupant, session = event.data, event.key, event.actor, event.session; + + if key == 'recording' and data and data.isTranscribingEnabled ~= nil then + -- if it is recording we want to allow setting in metadata if not moderator but features + -- are present + if session.jitsi_meet_context_features + and occupant.role ~= 'moderator' + and is_feature_allowed('transcription', session.jitsi_meet_context_features) + and is_feature_allowed('recording', session.jitsi_meet_context_features) then + local res = {}; + res.isTranscribingEnabled = data.isTranscribingEnabled; + return res; + elseif not session.jitsi_meet_context_features and occupant.role == 'moderator' then + return data; + else + return nil; + end + end + + if occupant.role == 'moderator' then + return data; + end + + return nil; +end); + diff --git a/roles/jitsi/files/prosody/modules/mod_filter_messages.lua b/roles/jitsi/files/prosody/modules/mod_filter_messages.lua new file mode 100644 index 0000000..8e9716a --- /dev/null +++ b/roles/jitsi/files/prosody/modules/mod_filter_messages.lua @@ -0,0 +1,42 @@ +-- enable under the main muc module +-- a module that will filter group messages based on features (jitsi_meet_context_features) +-- when requested via metadata (permissions.groupChatRestricted) +local util = module:require 'util'; +local get_room_from_jid = util.get_room_from_jid; +local st = require 'util.stanza'; + +local function on_message(event) + local stanza = event.stanza; + local body = stanza:get_child('body'); + local session = event.origin; + + if not body or not session then + -- we ignore messages without body - lobby, polls ... + return; + end + + -- get room name with tenant and find room. + -- this should already been through domain mapper and this should be the real room jid [tenant]name format + local room = get_room_from_jid(stanza.attr.to); + if not room then + module:log('warn', 'No room found found for %s', stanza.attr.to); + return; + end + + if room.jitsiMetadata and room.jitsiMetadata.permissions + and room.jitsiMetadata.permissions.groupChatRestricted + and not is_feature_allowed('send-groupchat', session.jitsi_meet_context_features) then + + local reply = st.error_reply(stanza, 'cancel', 'not-allowed', 'Sending group messages not allowed'); + if session.type == 's2sin' or session.type == 's2sout' then + reply.skipMapping = true; + end + module:send(reply); + + -- let's filter this message + return true; + end +end + +module:hook('message/bare', on_message); -- room messages +module:hook('jitsi-visitor-groupchat-pre-route', on_message); -- visitors messages diff --git a/roles/jitsi/files/prosody/modules/mod_fmuc.lua b/roles/jitsi/files/prosody/modules/mod_fmuc.lua index 496c514..9753c3e 100644 --- a/roles/jitsi/files/prosody/modules/mod_fmuc.lua +++ b/roles/jitsi/files/prosody/modules/mod_fmuc.lua @@ -14,8 +14,11 @@ local jid = require 'util.jid'; local st = require 'util.stanza'; local new_id = require 'util.id'.medium; local filters = require 'util.filters'; +local array = require 'util.array'; +local set = require 'util.set'; local util = module:require 'util'; +local is_admin = util.is_admin; local ends_with = util.ends_with; local is_vpaas = util.is_vpaas; local room_jid_match_rewrite = util.room_jid_match_rewrite; @@ -23,6 +26,12 @@ 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; local presence_check_status = util.presence_check_status; +local respond_iq_result = util.respond_iq_result; + +local PARTICIPANT_PROP_RAISE_HAND = 'jitsi_participant_raisedHand'; +local PARTICIPANT_PROP_REQUEST_TRANSCRIPTION = 'jitsi_participant_requestingTranscription'; +local PARTICIPANT_PROP_TRANSLATION_LANG = 'jitsi_participant_translation_language'; +local TRANSCRIPT_DEFAULT_LANG = module:get_option_string('transcriptions_default_language', 'en'); -- this is the main virtual host of this vnode local local_domain = module:get_option_string('muc_mapper_domain_base'); @@ -43,6 +52,9 @@ local local_muc_domain = muc_domain_prefix..'.'..local_domain; local NICK_NS = 'http://jabber.org/protocol/nick'; +-- in certain cases we consider participants with token as moderators, this is the default behavior which can be turned off +local auto_promoted_with_token = module:get_option_boolean('visitors_auto_promoted_with_token', true); + -- 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'); @@ -52,17 +64,72 @@ local sent_iq_cache = require 'util.cache'.new(200); local sessions = prosody.full_sessions; -local um_is_admin = require 'core.usermanager'.is_admin; -local function is_admin(jid) - return um_is_admin(jid, module.host); +local function send_transcriptions_update(room) + -- let's notify main prosody + local lang_array = array(); + local count = 0; + + for k, v in pairs(room._transcription_languages) do + lang_array:push(v); + count = count + 1; + end + + local iq_id = new_id(); + sent_iq_cache:set(iq_id, socket.gettime()); + module:send(st.iq({ + type = 'set', + 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..'.'..main_domain) }) + :tag('transcription-languages', { + xmlns = 'jitsi:visitors', + langs = lang_array:unique():sort():concat(','), + count = tostring(count) + }):up()); +end + +local function remove_transcription(room, occupant) + local send_update = false; + if room._transcription_languages then + if room._transcription_languages[occupant.jid] then + send_update = true; + end + room._transcription_languages[occupant.jid] = nil; + end + + if send_update then + send_transcriptions_update(room); + end +end + +-- if lang is nil we will remove it from the list +local function add_transcription(room, occupant, lang) + if not room._transcription_languages then + room._transcription_languages = {}; + end + + local old = room._transcription_languages[occupant.jid]; + room._transcription_languages[occupant.jid] = lang or TRANSCRIPT_DEFAULT_LANG; + + if old ~= room._transcription_languages[occupant.jid] then + send_transcriptions_update(room); + end end -- mark all occupants as visitors module:hook('muc-occupant-pre-join', function (event) local occupant, room, origin, stanza = event.occupant, event.room, event.origin, event.stanza; local node, host = jid.split(occupant.bare_jid); + local resource = jid.resource(occupant.nick); - if prosody.hosts[host] and not is_admin(occupant.bare_jid) then + if is_admin(occupant.bare_jid) then + return; + end + + if prosody.hosts[host] then + -- local participants which host is defined in this prosody if room._main_room_lobby_enabled then origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Visitors not allowed while lobby is on!') :tag('no-visitors-lobby', { xmlns = 'jitsi:visitors' })); @@ -70,6 +137,9 @@ module:hook('muc-occupant-pre-join', function (event) else occupant.role = 'visitor'; end + elseif room.moderators_list and room.moderators_list:contains(resource) then + -- remote participants, host is the main prosody + occupant.role = 'moderator'; end end, 3); @@ -89,7 +159,7 @@ module:hook('muc-occupant-pre-leave', function (event) -- to main prosody local pr = occupant:get_presence(); - local raiseHand = pr:get_child_text('jitsi_participant_raisedHand'); + local raiseHand = pr:get_child_text(PARTICIPANT_PROP_RAISE_HAND); -- a promotion detected let's send it to main prosody if raiseHand and #raiseHand > 0 then @@ -111,6 +181,7 @@ module:hook('muc-occupant-pre-leave', function (event) module:send(promotion_request); end + remove_transcription(room, occupant); end, 1); -- rate limit is 0 -- Returns the main participants count and the visitors count @@ -136,6 +207,20 @@ local function cancel_destroy_timer(room) end end +local function destroy_with_conference_ended(room) + -- if the room is being destroyed, ignore + if room.destroying then + return; + end + + cancel_destroy_timer(room); + + local main_count, visitors_count = get_occupant_counts(room); + module:log('info', 'Will destroy:%s main_occupants:%s visitors:%s', room.jid, main_count, visitors_count); + room:destroy(nil, 'Conference ended.'); + return true; +end + -- schedules a new destroy timer which will destroy the room if there are no visitors after the timeout local function schedule_destroy_timer(room) cancel_destroy_timer(room); @@ -165,7 +250,9 @@ module:hook('muc-occupant-left', function (event) if prosody.hosts[occupant_domain] and not is_admin(occupant.bare_jid) then local focus_occupant = get_focus_occupant(room); if not focus_occupant then - module:log('info', 'No focus found for %s', room.jid); + if not room.destroying then + module:log('warn', 'No focus found for %s', room.jid); + end return; end -- Let's forward unavailable presence to the special jicofo @@ -194,10 +281,15 @@ module:hook('muc-occupant-left', function (event) if visitors_count == 0 then schedule_destroy_timer(room); end + + if main_count == 0 then + destroy_with_conference_ended(room); + end end); -- forward visitor presences to jicofo -- detects raise hand in visitors presence, this is request for promotion +-- detects the requested transcription and its language to send updates for it module:hook('muc-broadcast-presence', function (event) local occupant = event.occupant; @@ -227,10 +319,11 @@ module:hook('muc-broadcast-presence', function (event) full_p.attr.to = focus_occupant.jid; room:route_to_occupant(focus_occupant, full_p); - local raiseHand = full_p:get_child_text('jitsi_participant_raisedHand'); + local raiseHand = full_p:get_child_text(PARTICIPANT_PROP_RAISE_HAND); -- a promotion detected let's send it to main prosody if raiseHand then local user_id; + local group_id; local is_moderator; local session = sessions[occupant.jid]; local identity = session and session.jitsi_meet_context_user; @@ -246,14 +339,14 @@ module:hook('muc-broadcast-presence', function (event) -- so we can be auto promoted if identity and identity.id then user_id = session.jitsi_meet_context_user.id; + group_id = session.jitsi_meet_context_group; - if room._data.moderator_id then - if room._data.moderator_id == user_id then + if session.auth_token and auto_promoted_with_token then + if not session.jitsi_meet_tenant_mismatch or session.jitsi_web_query_prefix == '' then + -- non-vpaas and having a token is considered a moderator, and if it is not in '/' tenant + -- the tenant from url and token should match is_moderator = true; end - elseif session.auth_token then - -- non-vpass and having a token is considered a moderator - is_moderator = true; end end end @@ -272,6 +365,7 @@ module:hook('muc-broadcast-presence', function (event) jid = occupant.jid, time = raiseHand, userId = user_id, + groupId = group_id, forcePromote = is_moderator and 'true' or 'false'; }):up(); @@ -283,6 +377,18 @@ module:hook('muc-broadcast-presence', function (event) module:send(promotion_request); end + local requestTranscriptionValue = full_p:get_child_text(PARTICIPANT_PROP_REQUEST_TRANSCRIPTION); + local hasTranscriptionEnabled = room._transcription_languages and room._transcription_languages[occupant.jid]; + + -- detect transcription + if requestTranscriptionValue == 'true' then + local lang = full_p:get_child_text(PARTICIPANT_PROP_TRANSLATION_LANG); + + add_transcription(room, occupant, lang); + elseif hasTranscriptionEnabled then + remove_transcription(room, occupant, nil); + end + return; end); @@ -317,7 +423,6 @@ local function stanza_handler(event) 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 @@ -327,12 +432,7 @@ local function stanza_handler(event) 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 - })); + respond_iq_result(origin, stanza); local req_jid = request_promotion.attr.jid; -- now let's find the occupant and forward the response @@ -469,7 +569,15 @@ module:hook('jicofo-unlock-room', function(e) return true; end); --- handles incoming iq connect stanzas +-- handles incoming iq visitors stanzas +-- connect - sent after sending all main participant's presences +-- disconnect - sent when main room is destroyed or when we receive a 'disconnect-vnode' iq from jicofo +-- update - sent on: +-- * room secret is changed +-- * lobby enabled or disabled +-- * initially before connect to report currently joined moderators +-- * moderator participant joins main room +-- * a participant has been granted moderator rights local function iq_from_main_handler(event) local origin, stanza = event.origin, event.stanza; @@ -500,7 +608,7 @@ local function iq_from_main_handler(event) 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); + module:log('warn', 'No room found %s in iq_from_main_handler for:%s', room_jid, visitors_iq); return; end @@ -523,27 +631,16 @@ local function iq_from_main_handler(event) 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 - })); + respond_iq_result(origin, stanza); if process_disconnect then - cancel_destroy_timer(room); - - local main_count, visitors_count = get_occupant_counts(room); - module:log('info', 'Will destroy:%s main_occupants:%s visitors:%s', room.jid, main_count, visitors_count); - room:destroy(nil, 'Conference ended.'); - return true; + return destroy_with_conference_ended(room); end -- 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); room._data.meetingId = node.attr.meetingId; - room._data.moderator_id = node.attr.moderatorId; local createdTimestamp = node.attr.createdTimestamp; room.created_timestamp = createdTimestamp and tonumber(createdTimestamp) or nil; @@ -553,6 +650,28 @@ local function iq_from_main_handler(event) room._main_room_lobby_enabled = false; end + -- read the moderators list + room.moderators_list = room.moderators_list or set.new(); + local moderators = node:get_child('moderators'); + + if moderators then + for _, child in ipairs(moderators.tags) do + if child.name == 'item' then + room.moderators_list:add(child.attr.epId); + end + end + + -- let's check current occupants roles and promote them if needed + -- we change only main participants which are not moderators, but participant + for _, o in room:each_occupant() do + if not is_admin(o.bare_jid) + and o.role == 'participant' + and room.moderators_list:contains(jid.resource(o.nick)) then + room:set_affiliation(true, o.bare_jid, 'owner'); + end + end + end + if fire_jicofo_unlock then -- everything is connected allow participants to join module:fire_event('jicofo-unlock-room', { room = room; fmuc_fired = true; }); diff --git a/roles/jitsi/files/prosody/modules/mod_jibri_session.lua b/roles/jitsi/files/prosody/modules/mod_jibri_session.lua index 5fb0f58..4830232 100644 --- a/roles/jitsi/files/prosody/modules/mod_jibri_session.lua +++ b/roles/jitsi/files/prosody/modules/mod_jibri_session.lua @@ -44,10 +44,12 @@ local stanza = event.stanza; if session.jitsi_meet_context_user ~= nil then initiator.id = session.jitsi_meet_context_user.id; + else + initiator.id = session.granted_jitsi_meet_context_user_id; end - if session.jitsi_meet_context_group ~= nil then - initiator.group = session.jitsi_meet_context_group; - end + + initiator.group + = session.jitsi_meet_context_group or session.granted_jitsi_meet_context_group_id; app_data.file_recording_metadata.initiator = initiator update_app_data = true; diff --git a/roles/jitsi/files/prosody/modules/mod_jitsi_permissions.lua b/roles/jitsi/files/prosody/modules/mod_jitsi_permissions.lua new file mode 100644 index 0000000..7a761ff --- /dev/null +++ b/roles/jitsi/files/prosody/modules/mod_jitsi_permissions.lua @@ -0,0 +1,201 @@ +-- this is auto loaded by meeting_id +local filters = require 'util.filters'; +local jid = require 'util.jid'; + +local util = module:require 'util'; +local is_admin = util.is_admin; +local get_room_from_jid = util.get_room_from_jid; +local is_healthcheck_room = util.is_healthcheck_room; +local room_jid_match_rewrite = util.room_jid_match_rewrite; +local ends_with = util.ends_with; +local presence_check_status = util.presence_check_status; + +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'); +if not muc_domain_base then + module:log('warn', 'No "muc_domain_base" option set, disabling kick check endpoint.'); + return ; +end + +-- only the visitor prosody has main_domain setting +local is_visitor_prosody = module:get_option_string('main_domain') ~= nil; + +-- load it only on the main muc component as it is loaded by muc_meeting_id which is loaded and for the breakout room muc +if muc_domain_prefix..'.'..muc_domain_base ~= module.host or is_visitor_prosody then + return; +end + +local sessions = prosody.full_sessions; +local default_permissions; + +local function load_config() + default_permissions = module:get_option('jitsi_default_permissions', { + livestreaming = true; + recording = true; + transcription = true; + ['outbound-call'] = true; + ['create-polls'] = true; + ['send-groupchat'] = true; + flip = true; + }); +end +load_config(); + +function process_set_affiliation(event) + local actor, affiliation, jid, previous_affiliation, room + = event.actor, event.affiliation, event.jid, event.previous_affiliation, event.room; + local actor_session = sessions[actor]; + + if is_admin(jid) or is_healthcheck_room(room.jid) or not actor or not previous_affiliation + or not actor_session or not actor_session.jitsi_meet_context_features then + return; + end + + local occupant; + for _, o in room:each_occupant() do + if o.bare_jid == jid then + occupant = o; + end + end + + if not occupant then + return; + end + + local occupant_session = sessions[occupant.jid]; + if not occupant_session then + return; + end + + if previous_affiliation == 'none' and affiliation == 'owner' then + occupant_session.granted_jitsi_meet_context_features = actor_session.jitsi_meet_context_features; + if actor_session.jitsi_meet_context_user then + occupant_session.granted_jitsi_meet_context_user_id = actor_session.jitsi_meet_context_user['id'] + or actor_session.granted_jitsi_meet_context_user_id; + end + occupant_session.granted_jitsi_meet_context_group_id = actor_session.jitsi_meet_context_group + or actor_session.granted_jitsi_meet_context_group_id; + elseif previous_affiliation == 'owner' and ( affiliation == 'member' or affiliation == 'none' ) then + occupant_session.granted_jitsi_meet_context_features = nil; + occupant_session.granted_jitsi_meet_context_user_id = nil; + occupant_session.granted_jitsi_meet_context_group_id = nil; + + -- on revoke + if not session.auth_token then + occupant_session.jitsi_meet_context_features = nil; + end + end +end + +-- Detects when sending self-presence because of role change +-- we can end up here because of the following cases: +-- 1. user joins the room and is granted moderator by another moderator or jicofo +-- 2. Some module changes the role of the user by using set_affiliation method +-- In cases where authentication is 'anonymous', 'jitsi-anonymous', 'internal_hashed', 'internal_plain', 'cyrus' we +-- want to send default permissions all to indicate UI that everything is allowed (to not relay on the UI to check +-- is participant moderator or not), to allow finer control over the permissions. +-- In case the authentication is 'token' based we want to send permissions only if the token of the user does not include +-- features in the user.context. +-- In case of allowners we want to send the permissions, no matter of the authentication method. +-- In case permissions were granted we want to send the granted permissions in all cases except when the user is +-- using token that has features pre-defined (authentication is 'token'). +function filter_stanza(stanza, session) + if not stanza.attr or not stanza.attr.to or stanza.name ~= 'presence' + or stanza.attr.type == 'unavailable' or ends_with(stanza.attr.from, '/focus') then + return stanza; + end + + local bare_to = jid.bare(stanza.attr.to); + if is_admin(bare_to) then + return stanza; + end + + local muc_x = stanza:get_child('x', MUC_NS..'#user'); + if not muc_x or not presence_check_status(muc_x, '110') then + return stanza; + end + + local room = get_room_from_jid(room_jid_match_rewrite(jid.bare(stanza.attr.from))); + + if not room or is_healthcheck_room(room.jid) then + return stanza; + end + + if not room.send_default_permissions_to then + room.send_default_permissions_to = {}; + end + + if not session.force_permissions_update then + if session.auth_token and session.jitsi_meet_context_features then -- token and features are set so skip + room.send_default_permissions_to[bare_to] = nil; + return stanza; + end + + -- we are sending permissions only when becoming a member + local is_moderator = false; + for item in muc_x:childtags('item') do + if item.attr.role == 'moderator' then + is_moderator = true; + break; + end + end + + if not is_moderator then + return stanza; + end + + if not room.send_default_permissions_to[bare_to] then + return stanza; + end + end + + session.force_permissions_update = false; + + local permissions_to_send + = session.jitsi_meet_context_features or session.granted_jitsi_meet_context_features or default_permissions; + + room.send_default_permissions_to[bare_to] = nil; + + if not session.granted_jitsi_meet_context_features and not session.jitsi_meet_context_features then + session.jitsi_meet_context_features = {}; + end + + stanza:tag('permissions', { xmlns='http://jitsi.org/jitmeet' }); + for k, v in pairs(permissions_to_send) do + local val = tostring(v); + stanza:tag('p', { name = k, val = val }):up(); + if session.jitsi_meet_context_features then + session.jitsi_meet_context_features[k] = val; + end + end + stanza:up(); + + return stanza; +end + +-- we need to indicate that we will send permissions if we need to +-- we need to handle granted features and stuff in the pre-set hook so they are unavailable +-- when the self presence is set, so we can update the client, the checks +-- whether the actor is allowed to set the affiliation are done before pre-set hook is fired +module:hook('muc-pre-set-affiliation', function(event) + local jid, room = event.jid, event.room; + + if not room.send_default_permissions_to then + room.send_default_permissions_to = {}; + end + room.send_default_permissions_to[jid] = true; + + process_set_affiliation(event); +end); + +function filter_session(session) + -- domain mapper is filtering on default priority 0 + -- allowners is -1 and we need it after that + filters.add_filter(session, 'stanzas/out', filter_stanza, -2); +end + +-- enable filtering presences +filters.add_filter_hook(filter_session); diff --git a/roles/jitsi/files/prosody/modules/mod_jitsi_session.lua b/roles/jitsi/files/prosody/modules/mod_jitsi_session.lua index 8d281c4..4ea2986 100644 --- a/roles/jitsi/files/prosody/modules/mod_jitsi_session.lua +++ b/roles/jitsi/files/prosody/modules/mod_jitsi_session.lua @@ -3,6 +3,7 @@ module:set_global(); local formdecode = require "util.http".formdecode; +local region_header_name = module:get_option_string('region_header_name', 'x_proxy_region'); -- Extract the following parameters from the URL and set them in the session: -- * previd: for session resumption @@ -24,6 +25,8 @@ function init_session(event) session.jitsi_web_query_room = params.room; session.jitsi_web_query_prefix = params.prefix or ""; end + + session.user_region = request.headers[region_header_name]; end module:hook_global("bosh-session", init_session, 1); diff --git a/roles/jitsi/files/prosody/modules/mod_muc_allowners.lua b/roles/jitsi/files/prosody/modules/mod_muc_allowners.lua index b8df9ff..dbb4572 100644 --- a/roles/jitsi/files/prosody/modules/mod_muc_allowners.lua +++ b/roles/jitsi/files/prosody/modules/mod_muc_allowners.lua @@ -1,13 +1,15 @@ +--- activate under the main muc component local filters = require 'util.filters'; local jid = require "util.jid"; local jid_bare = require "util.jid".bare; local jid_host = require "util.jid".host; local st = require "util.stanza"; -local um_is_admin = require "core.usermanager".is_admin; local util = module:require "util"; +local is_admin = util.is_admin; local is_healthcheck_room = util.is_healthcheck_room; local is_moderated = util.is_moderated; local get_room_from_jid = util.get_room_from_jid; +local room_jid_match_rewrite = util.room_jid_match_rewrite; local presence_check_status = util.presence_check_status; local MUC_NS = 'http://jabber.org/protocol/muc'; @@ -18,15 +20,19 @@ local function load_config() end load_config(); -local function is_admin(_jid) - return um_is_admin(_jid, module.host); -end - -- List of the bare_jids of all occupants that are currently joining (went through pre-join) and will be promoted -- as moderators. As pre-join (where added) and joined event (where removed) happen one after another this list should -- have length of 1 local joining_moderator_participants = {}; +module:hook("muc-room-created", function(event) + local room = event.room; + + if room.jitsiMetadata then + room.jitsiMetadata.allownersEnabled = true; + end +end, -2); -- room_metadata should run before this module on -1 + module:hook("muc-occupant-pre-join", function (event) local room, occupant = event.room, event.occupant; @@ -87,7 +93,7 @@ function filter_stanza(stanza) end -- we want to filter presences only on this host for allowners and skip anything like lobby etc. - local host_from = jid_host(stanza.attr.from); + local host_from = jid_host(room_jid_match_rewrite(stanza.attr.from)); if host_from ~= module.host then return stanza; end diff --git a/roles/jitsi/files/prosody/modules/mod_muc_breakout_rooms.lua b/roles/jitsi/files/prosody/modules/mod_muc_breakout_rooms.lua index c3d2850..537f2cb 100644 --- a/roles/jitsi/files/prosody/modules/mod_muc_breakout_rooms.lua +++ b/roles/jitsi/files/prosody/modules/mod_muc_breakout_rooms.lua @@ -200,8 +200,14 @@ end -- Managing breakout rooms -function create_breakout_room(room_jid, subject) - local main_room, main_room_jid = get_main_room(room_jid); +function create_breakout_room(orig_room, subject) + local main_room, main_room_jid = get_main_room(orig_room.jid); + + if orig_room ~= main_room then + module:log('warn', 'Invalid create breakout room request for %s', orig_room.jid); + return; + end + local breakout_room_jid = uuid_gen() .. '@' .. breakout_rooms_muc_component_config; if not main_room._data.breakout_rooms then @@ -219,13 +225,18 @@ function create_breakout_room(room_jid, subject) broadcast_breakout_rooms(main_room_jid); end -function destroy_breakout_room(room_jid, message) +function destroy_breakout_room(orig_room, room_jid, message) local main_room, main_room_jid = get_main_room(room_jid); if room_jid == main_room_jid then return; end + if orig_room ~= main_room then + module:log('warn', 'Invalid destroy breakout room request for %s', orig_room.jid); + return; + end + local breakout_room = breakout_rooms_muc_service.get_room_from_jid(room_jid); if breakout_room then @@ -244,13 +255,18 @@ function destroy_breakout_room(room_jid, message) end -function rename_breakout_room(room_jid, name) +function rename_breakout_room(orig_room, room_jid, name) local main_room, main_room_jid = get_main_room(room_jid); if room_jid == main_room_jid then return; end + if orig_room ~= main_room then + module:log('warn', 'Invalid rename breakout room request for %s', orig_room.jid); + return; + end + if main_room then if main_room._data.breakout_rooms then main_room._data.breakout_rooms[room_jid] = name; @@ -322,18 +338,25 @@ function on_message(event) end if message.attr.type == JSON_TYPE_ADD_BREAKOUT_ROOM then - create_breakout_room(room.jid, message.attr.subject); + create_breakout_room(room, message.attr.subject); return true; elseif message.attr.type == JSON_TYPE_REMOVE_BREAKOUT_ROOM then - destroy_breakout_room(message.attr.breakoutRoomJid); + destroy_breakout_room(room, message.attr.breakoutRoomJid); return true; elseif message.attr.type == JSON_TYPE_RENAME_BREAKOUT_ROOM then - rename_breakout_room(message.attr.breakoutRoomJid, message.attr.subject); + rename_breakout_room(room, message.attr.breakoutRoomJid, message.attr.subject); return true; elseif message.attr.type == JSON_TYPE_MOVE_TO_ROOM_REQUEST then local participant_jid = message.attr.participantJid; local target_room_jid = message.attr.roomJid; + if not room._data.breakout_rooms or not ( + room._data.breakout_rooms[target_room_jid] or target_room_jid == internal_room_jid_match_rewrite(room.jid)) + then + module:log('warn', 'Invalid breakout room %s for %s', target_room_jid, room.jid); + return false + end + local json_msg, error = json.encode({ type = BREAKOUT_ROOMS_IDENTITY_TYPE, event = JSON_TYPE_MOVE_TO_ROOM_REQUEST, @@ -342,6 +365,7 @@ function on_message(event) if not json_msg then module:log('error', 'skip sending request room:%s error:%s', room.jid, error); + return false end send_json_msg(participant_jid, json_msg) @@ -416,6 +440,16 @@ function exist_occupants_in_rooms(main_room) return false; end +function on_occupant_pre_leave(event) + local room, occupant, session, stanza = event.room, event.occupant, event.origin, event.stanza; + + local main_room = get_main_room(room.jid); + + prosody.events.fire_event('jitsi-breakout-occupant-leaving', { + room = room; main_room = main_room; occupant = occupant; stanza = stanza; session = session; + }); +end + function on_occupant_left(event) local room_jid = event.room.jid; @@ -481,7 +515,7 @@ function on_main_room_destroyed(event) end for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do - destroy_breakout_room(breakout_room_jid, event.reason) + destroy_breakout_room(main_room, breakout_room_jid, event.reason) end end @@ -510,6 +544,7 @@ function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module) host_module:hook('muc-occupant-joined', on_occupant_joined); host_module:hook('muc-occupant-left', on_occupant_left); host_module:hook('muc-room-pre-create', on_breakout_room_pre_create); + host_module:hook('muc-occupant-pre-leave', on_occupant_pre_leave); host_module:hook('muc-disco#info', function (event) local room = event.room; @@ -526,7 +561,7 @@ function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module) name = 'muc#roominfo_breakout_main_room'; label = 'The main room associated with this breakout room'; }); - event.formdata['muc#roominfo_breakout_main_room'] = main_room_jid; + event.formdata['muc#roominfo_breakout_main_room'] = internal_room_jid_match_rewrite(main_room_jid); -- If the main room has a lobby, make it so this breakout room also uses it. if (main_room and main_room._data.lobbyroom and main_room:get_members_only()) then @@ -553,7 +588,7 @@ function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module) table.insert(event.form, { name = 'muc#roominfo_breakout_main_room'; label = 'The main room associated with this breakout room'; - value = main_room_jid; + value = internal_room_jid_match_rewrite(main_room_jid); }); end); diff --git a/roles/jitsi/files/prosody/modules/mod_muc_flip.lua b/roles/jitsi/files/prosody/modules/mod_muc_flip.lua index e59cfaa..0753038 100644 --- a/roles/jitsi/files/prosody/modules/mod_muc_flip.lua +++ b/roles/jitsi/files/prosody/modules/mod_muc_flip.lua @@ -4,9 +4,9 @@ -- Copyright (C) 2023-present 8x8, Inc. local oss_util = module:require "util"; +local is_admin = oss_util.is_admin; local is_healthcheck_room = oss_util.is_healthcheck_room; local process_host_module = oss_util.process_host_module; -local um_is_admin = require "core.usermanager".is_admin; local inspect = require('inspect'); local jid_bare = require "util.jid".bare; local jid = require "util.jid"; @@ -21,10 +21,6 @@ if lobby_muc_component_config == nil then return ; end -local function is_admin(occupant_jid) - return um_is_admin(occupant_jid, module.host); -end - local function remove_flip_tag(stanza) stanza:maptags(function(tag) if tag and tag.name == "flip_device" then diff --git a/roles/jitsi/files/prosody/modules/mod_muc_hide_all.lua b/roles/jitsi/files/prosody/modules/mod_muc_hide_all.lua index 4f59b1c..eea8741 100644 --- a/roles/jitsi/files/prosody/modules/mod_muc_hide_all.lua +++ b/roles/jitsi/files/prosody/modules/mod_muc_hide_all.lua @@ -1,6 +1,30 @@ -- This module makes all MUCs in Prosody unavailable on disco#items query -- Copyright (C) 2023-present 8x8, Inc. +local jid = require 'util.jid'; +local st = require 'util.stanza'; -module:hook("muc-room-pre-create", function(event) +local util = module:require 'util'; +local get_room_from_jid = util.get_room_from_jid; + +module:hook('muc-room-pre-create', function(event) event.room:set_hidden(true); end, -1); + +for _, event_name in pairs { + 'iq-get/bare/http://jabber.org/protocol/disco#info:query'; + 'iq-get/host/http://jabber.org/protocol/disco#info:query'; +} do + module:hook(event_name, function (event) + local origin, stanza = event.origin, event.stanza; + local room_jid = jid.bare(stanza.attr.to); + local room = get_room_from_jid(room_jid); + + if room then + if not room:get_occupant_by_real_jid(stanza.attr.from) then + origin.send(st.error_reply(stanza, 'auth', 'forbidden')); + return true; + end + end + -- prosody will send item-not-found + end, 1) -- make sure we handle it before prosody that uses priority -2 for this +end diff --git a/roles/jitsi/files/prosody/modules/mod_muc_kick_participant.lua b/roles/jitsi/files/prosody/modules/mod_muc_kick_participant.lua new file mode 100644 index 0000000..11b0bbb --- /dev/null +++ b/roles/jitsi/files/prosody/modules/mod_muc_kick_participant.lua @@ -0,0 +1,171 @@ +-- http endpoint to kick participants, access is based on provided jwt token +-- the correct jigasi we fined based on the display name and the number provided +-- Copyright (C) 2023-present 8x8, Inc. + +local util = module:require "util"; +local async_handler_wrapper = util.async_handler_wrapper; +local is_sip_jigasi = util.is_sip_jigasi; +local starts_with = util.starts_with; +local formdecode = require "util.http".formdecode; +local urlencode = require "util.http".urlencode; +local jid = require "util.jid"; +local json = require 'cjson.safe'; + +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"); +if not muc_domain_base then + module:log("warn", "No 'muc_domain_base' option set, disabling kick check endpoint."); + return ; +end + +local json_content_type = "application/json"; + +local token_util = module:require "token/util".new(module); + +local asapKeyServer = module:get_option_string('prosody_password_public_key_repo_url', ''); +if asapKeyServer == '' then + module:log('warn', 'No "prosody_password_public_key_repo_url" option set, disabling kick endpoint.'); + return ; +end + +token_util:set_asap_key_server(asapKeyServer); + +--- Verifies the token +-- @param token the token we received +-- @param room_address the full room address jid +-- @return true if values are ok or false otherwise +function verify_token(token, room_address) + + if token == nil then + module:log("warn", "no token provided for %s", room_address); + return false; + end + + local session = {}; + session.auth_token = token; + local verified, reason, msg = token_util:process_and_verify_token(session); + if not verified then + module:log("warn", "not a valid token %s %s for %s", tostring(reason), tostring(msg), room_address); + return false; + end + + return true; +end + + +-- Validates the request by checking for required url param room and +-- validates the token provided with the request +-- @param request - The request to validate. +-- @return [error_code, room] +local function validate_and_get_room(request) + if not request.url.query then + module:log("warn", "No query"); + return 400, nil; + end + + local params = formdecode(request.url.query); + local room_name = urlencode(params.room) or ""; + local subdomain = urlencode(params.prefix) or ""; + + if not room_name then + module:log("warn", "Missing room param for %s", room_name); + return 400, nil; + end + + local room_address = jid.join(room_name, muc_domain_prefix.."."..muc_domain_base); + + if subdomain and subdomain ~= "" then + room_address = "["..subdomain.."]"..room_address; + end + + -- verify access + local token = request.headers["authorization"] + + if token and starts_with(token,'Bearer ') then + token = token:sub(8,#token) + end + + if not verify_token(token, room_address) then + return 403, nil; + end + + local room = get_room_from_jid(room_address); + + if not room then + module:log("warn", "No room found for %s", room_address); + return 404, nil; + else + return 200, room; + end +end + +function handle_kick_participant (event) + local request = event.request; + if request.headers.content_type ~= json_content_type + or (not request.body or #request.body == 0) then + module:log("warn", "Wrong content type: %s", request.headers.content_type); + return { status_code = 400; } + end + + local params, error = json.decode(request.body); + if not params then + module:log("warn", "Missing params error:%s", error); + return { status_code = 400; } + end + + local number = params["number"]; + local participantId = params["participantId"]; + + if (not number and not participantId) or (number and participantId) then + module:log("warn", "Invalid parameters: exactly one of 'number' or 'participantId' must be provided."); + return { status_code = 400; }; + end + + local error_code, room = validate_and_get_room(request); + + if error_code and error_code ~= 200 then + module:log("error", "Error validating %s", error_code); + return { error_code = 400; } + end + + if not room then + return { status_code = 404; } + end + + for _, occupant in room:each_occupant() do + local pr = occupant:get_presence(); + + if is_participant_match(pr, number, participantId) then + room:set_role(true, occupant.nick, nil); + module:log('info', 'Occupant kicked %s from %s', occupant.nick, room.jid); + return { status_code = 200; } + end + end + + -- not found participant to kick + return { status_code = 404; }; +end + +function is_participant_match(pr, number, participantId) + if number then + local displayName = pr:get_child_text('nick', 'http://jabber.org/protocol/nick'); + return is_sip_jigasi(pr) and displayName and starts_with(displayName, number); + elseif participantId then + local from = pr.attr.from; + local _, _, from_resource = jid.split(from); + if from_resource then + return from_resource == participantId; + end + end + return false; +end + +module:log("info","Adding http handler for /kick-participant on %s", module.host); +module:depends("http"); +module:provides("http", { + default_path = "/"; + route = { + ["PUT kick-participant"] = function (event) return async_handler_wrapper(event, handle_kick_participant) end; + }; +}); diff --git a/roles/jitsi/files/prosody/modules/mod_muc_lobby_rooms.lua b/roles/jitsi/files/prosody/modules/mod_muc_lobby_rooms.lua index 92d95e1..285e5ce 100644 --- a/roles/jitsi/files/prosody/modules/mod_muc_lobby_rooms.lua +++ b/roles/jitsi/files/prosody/modules/mod_muc_lobby_rooms.lua @@ -193,6 +193,10 @@ function filter_stanza(stanza) end end + if not from_real_jid then + return nil; + end + local is_from_moderator = lobby_room:get_affiliation(from_real_jid) == 'owner'; if is_to_moderator or is_from_moderator then diff --git a/roles/jitsi/files/prosody/modules/mod_muc_meeting_id.lua b/roles/jitsi/files/prosody/modules/mod_muc_meeting_id.lua index 00eecac..50e3d97 100644 --- a/roles/jitsi/files/prosody/modules/mod_muc_meeting_id.lua +++ b/roles/jitsi/files/prosody/modules/mod_muc_meeting_id.lua @@ -1,21 +1,29 @@ +local jid = require 'util.jid'; +local json = require 'cjson.safe'; local queue = require "util.queue"; local uuid_gen = require "util.uuid".generate; local main_util = module:require "util"; +local is_admin = main_util.is_admin; local ends_with = main_util.ends_with; +local get_room_from_jid = main_util.get_room_from_jid; local is_healthcheck_room = main_util.is_healthcheck_room; local internal_room_jid_match_rewrite = main_util.internal_room_jid_match_rewrite; local presence_check_status = main_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 extract_subdomain = main_util.extract_subdomain; local QUEUE_MAX_SIZE = 500; --- Module that generates a unique meetingId, attaches it to the room --- and adds it to all disco info form data (when room is queried or in the --- initial room owner config) +module:depends("jitsi_permissions"); + +-- Common module for all logic that can be loaded under the conference muc component. +-- +-- This module: +-- a) Generates a unique meetingId, attaches it to the room and adds it to all disco info form data +-- (when room is queried or in the initial room owner config). +-- b) Updates user region (obtain it from the incoming http headers) in the occupant's presence on pre-join. +-- c) Avoids any participant joining the room in the interval between creating the room and jicofo entering the room. +-- d) Removes any nick that maybe set to messages being sent to the room. +-- e) Fires event for received endpoint messages (optimization to decode them once). -- Hook to assign meetingId for new rooms module:hook("muc-room-created", function(event) @@ -75,17 +83,35 @@ module:hook('muc-broadcast-presence', function (event) end end); ---- Avoids any participant joining the room in the interval between creating the room ---- and jicofo entering the room -module:hook('muc-occupant-pre-join', function (event) - local room, stanza = event.room, event.stanza; - - -- we skip processing only if jicofo_lock is set to false - if room._data.jicofo_lock == false or is_healthcheck_room(room.jid) then +local function process_region(session, stanza) + if not session.user_region then + return; + end + + local region = stanza:get_child_text('jitsi_participant_region'); + if region then + return; + end + + stanza:tag('jitsi_participant_region'):text(session.user_region):up(); +end + +--- Avoids any participant joining the room in the interval between creating the room +--- and jicofo entering the room +module:hook('muc-occupant-pre-join', function (event) + local occupant, room, stanza = event.occupant, event.room, event.stanza; + + local is_health_room = is_healthcheck_room(room.jid); + -- check for region + if not is_admin(occupant.bare_jid) and not is_health_room then + process_region(event.origin, stanza); + end + + -- we skip processing only if jicofo_lock is set to false + if room._data.jicofo_lock == false or is_health_room then return; end - local occupant = event.occupant; if ends_with(occupant.nick, '/focus') then module:fire_event('jicofo-unlock-room', { room = room; }); else @@ -131,3 +157,86 @@ module:hook('jicofo-unlock-room', handle_jicofo_unlock); module:hook("muc-occupant-groupchat", function(event) event.stanza:remove_children('nick', 'http://jabber.org/protocol/nick'); end, 45); -- prosody check is prio 50, we want to run after it + +module:hook('message/bare', function(event) + local stanza = event.stanza; + + if stanza.attr.type ~= 'groupchat' then + return nil; + end + + -- we are interested in all messages without a body + local body = stanza:get_child('body') + if body then + return; + end + + local room = get_room_from_jid(stanza.attr.to); + if not room then + module:log('warn', 'No room found found for %s', stanza.attr.to); + return; + end + + local occupant_jid = stanza.attr.from; + local occupant = room:get_occupant_by_real_jid(occupant_jid); + if not occupant then + module:log("error", "Occupant sending msg %s was not found in room %s", occupant_jid, room.jid) + return; + end + + local json_message = stanza:get_child_text('json-message', 'http://jitsi.org/jitmeet'); + if not json_message then + return; + end + + -- TODO: add optimization by moving type and certain fields like is_interim as attribute on 'json-message' + -- using string find is roughly 70x faster than json decode for checking the value + if string.find(json_message, '"is_interim":true', 1, true) then + return; + end + + local msg_obj, error = json.decode(json_message); + + if error then + module:log('error', 'Error decoding data error:%s Sender: %s to:%s', error, stanza.attr.from, stanza.attr.to); + return true; + end + + if msg_obj.transcript ~= nil then + local transcription = msg_obj; + + -- in case of the string matching optimization above failed + if transcription.is_interim then + return; + end + + -- TODO what if we have multiple alternative transcriptions not just 1 + local text_message = transcription.transcript[1].text; + --do not send empty messages + if text_message == '' then + return; + end + + local user_id = transcription.participant.id; + local who = room:get_occupant_by_nick(jid.bare(room.jid)..'/'..user_id); + + transcription.jid = who and who.jid; + transcription.session_id = room._data.meetingId; + + local tenant, conference_name, id = extract_subdomain(jid.node(room.jid)); + if tenant then + transcription.fqn = tenant..'/'..conference_name; + else + transcription.fqn = conference_name; + end + transcription.customer_id = id; + + return module:fire_event('jitsi-transcript-received', { + room = room, occupant = occupant, transcription = transcription, stanza = stanza }); + end + + return module:fire_event('jitsi-endpoint-message-received', { + room = room, occupant = occupant, message = msg_obj, + origin = event.origin, + stanza = stanza, raw_message = json_message }); +end); diff --git a/roles/jitsi/files/prosody/modules/mod_muc_wait_for_host.lua b/roles/jitsi/files/prosody/modules/mod_muc_wait_for_host.lua index 042bf9e..b368109 100644 --- a/roles/jitsi/files/prosody/modules/mod_muc_wait_for_host.lua +++ b/roles/jitsi/files/prosody/modules/mod_muc_wait_for_host.lua @@ -6,9 +6,9 @@ -- the guest domain which is anonymous. -- The module has the option to set participants to moderators when connected via token/when they are authenticated -- This module depends on mod_persistent_lobby. -local um_is_admin = require 'core.usermanager'.is_admin; local jid = require 'util.jid'; local util = module:require "util"; +local is_admin = util.is_admin; local is_healthcheck_room = util.is_healthcheck_room; local is_moderated = util.is_moderated; local process_host_module = util.process_host_module; @@ -43,10 +43,6 @@ if not disable_auto_owners then end, 2); end -local function is_admin(jid) - return um_is_admin(jid, module.host); -end - -- if not authenticated user is trying to join the room we enable lobby in it -- and wait for the moderator to join module:hook('muc-occupant-pre-join', function (event) diff --git a/roles/jitsi/files/prosody/modules/mod_polls.lua b/roles/jitsi/files/prosody/modules/mod_polls.lua index 5494a6f..be23144 100644 --- a/roles/jitsi/files/prosody/modules/mod_polls.lua +++ b/roles/jitsi/files/prosody/modules/mod_polls.lua @@ -11,26 +11,8 @@ local muc = module:depends("muc"); local NS_NICK = 'http://jabber.org/protocol/nick'; local is_healthcheck_room = util.is_healthcheck_room; --- Checks if the given stanza contains a JSON message, --- and that the message type pertains to the polls feature. --- If yes, returns the parsed message. Otherwise, returns nil. -local function get_poll_message(stanza) - if stanza.attr.type ~= "groupchat" then - return nil; - end - local json_data = stanza:get_child_text("json-message", "http://jitsi.org/jitmeet"); - if json_data == nil then - return nil; - end - local data, error = json.decode(json_data); - if not data or (data.type ~= "new-poll" and data.type ~= "answer-poll") then - if error then - module:log('error', 'Error decoding data error:%s', error); - end - return nil; - end - return data; -end +local POLLS_LIMIT = 128; +local POLL_PAYLOAD_LIMIT = 1024; -- Logs a warning and returns true if a room does not -- have poll data associated with it. @@ -72,6 +54,7 @@ module:hook("muc-room-created", function(event) room.polls = { by_id = {}; order = {}; + count = 0; }; end); @@ -79,27 +62,46 @@ end); -- by listening to "new-poll" and "answer-poll" messages, -- and updating the room poll data accordingly. -- This mirrors the client-side poll update logic. -module:hook("message/bare", function(event) - local data = get_poll_message(event.stanza); - if data == nil then return end +module:hook('jitsi-endpoint-message-received', function(event) + local data, error, occupant, room, origin, stanza + = event.message, event.error, event.occupant, event.room, event.origin, event.stanza; - local room = muc.get_room_from_jid(event.stanza.attr.to); + if not data or (data.type ~= "new-poll" and data.type ~= "answer-poll") then + return; + end + + if string.len(event.raw_message) >= POLL_PAYLOAD_LIMIT then + module:log('error', 'Poll payload too large, discarding. Sender: %s to:%s', stanza.attr.from, stanza.attr.to); + return true; + end if data.type == "new-poll" then if check_polls(room) then return end - local occupant_jid = event.stanza.attr.from; - local occupant = room:get_occupant_by_real_jid(occupant_jid); - if not occupant then - module:log("error", "Occupant %s was not found in room %s", occupant_jid, room.jid) - return - end local poll_creator = get_occupant_details(occupant) if not poll_creator then module:log("error", "Cannot retrieve poll creator id and name for %s from %s", occupant.jid, room.jid) return end + if room.polls.count >= POLLS_LIMIT then + module:log("error", "Too many polls created in %s", room.jid) + return true; + end + + if room.polls.by_id[data.pollId] ~= nil then + module:log("error", "Poll already exists: %s", data.pollId); + origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Poll already exists')); + return true; + end + + if room.jitsiMetadata and room.jitsiMetadata.permissions + and room.jitsiMetadata.permissions.pollCreationRestricted + and not is_feature_allowed('create-polls', origin.jitsi_meet_context_features) then + origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Creation of polls not allowed for user')); + return true; + end + local answers = {} local compact_answers = {} for i, name in ipairs(data.answers) do @@ -117,6 +119,7 @@ module:hook("message/bare", function(event) room.polls.by_id[data.pollId] = poll table.insert(room.polls.order, poll) + room.polls.count = room.polls.count + 1; local pollData = { event = event, @@ -130,16 +133,9 @@ module:hook("message/bare", function(event) } } module:fire_event("poll-created", pollData); - elseif data.type == "answer-poll" then if check_polls(room) then return end - local occupant_jid = event.stanza.attr.from; - local occupant = room:get_occupant_by_real_jid(occupant_jid); - if not occupant then - module:log("error", "Occupant %s does not exists for room %s", occupant_jid, room.jid) - return - end local poll = room.polls.by_id[data.pollId]; if poll == nil then module:log("warn", "answering inexistent poll"); diff --git a/roles/jitsi/files/prosody/modules/mod_presence_identity.lua b/roles/jitsi/files/prosody/modules/mod_presence_identity.lua index 4db397e..44d8634 100644 --- a/roles/jitsi/files/prosody/modules/mod_presence_identity.lua +++ b/roles/jitsi/files/prosody/modules/mod_presence_identity.lua @@ -5,16 +5,13 @@ local update_presence_identity = module:require "util".update_presence_identity; -- values are set in the session, then insert them into the presence messages -- for that session. function on_message(event) - if event and event["stanza"] then - if event.origin and event.origin.jitsi_meet_context_user then - + local stanza, session = event.stanza, event.origin; + if stanza and session then update_presence_identity( - event.stanza, - event.origin.jitsi_meet_context_user, - event.origin.jitsi_meet_context_group + stanza, + session.jitsi_meet_context_user, + session.jitsi_meet_context_group ); - - end end end diff --git a/roles/jitsi/files/prosody/modules/mod_rate_limit.lua b/roles/jitsi/files/prosody/modules/mod_rate_limit.lua index 7d07d53..c0256de 100644 --- a/roles/jitsi/files/prosody/modules/mod_rate_limit.lua +++ b/roles/jitsi/files/prosody/modules/mod_rate_limit.lua @@ -14,6 +14,7 @@ local ip_util = require "util.ip"; local new_ip = ip_util.new_ip; local match_ip = ip_util.match; local parse_cidr = ip_util.parse_cidr; +local get_ip = module:require "util".get_ip; local config = {}; local limits_resolution = 1; @@ -76,14 +77,6 @@ local function is_whitelisted_host(h) return config.whitelist_hosts:contains(h); end --- Discover real remote IP of a session --- Note: http_server.get_request_from_conn() was added in Prosody 0.12.3, --- this code provides backwards compatibility with older versions -local get_request_from_conn = http_server.get_request_from_conn or function (conn) - local response = conn and conn._http_open_response; - return response and response.request or nil; -end; - -- Add an IP to the set of limied IPs local function limit_ip(ip) module:log("info", "Limiting %s due to login/join rate exceeded.", ip); @@ -192,9 +185,8 @@ local function filter_hook(session) return; end - local request = get_request_from_conn(session.conn); - local ip = request and request.ip or session.ip; - module:log("debug", "New session from %s", ip); + local ip = get_ip(session); + module:log("debug", "New session from %s", ip); if is_whitelisted(ip) or is_whitelisted_host(session.host) then return; end diff --git a/roles/jitsi/files/prosody/modules/mod_room_metadata_component.lua b/roles/jitsi/files/prosody/modules/mod_room_metadata_component.lua index 9459172..a026bc8 100644 --- a/roles/jitsi/files/prosody/modules/mod_room_metadata_component.lua +++ b/roles/jitsi/files/prosody/modules/mod_room_metadata_component.lua @@ -10,25 +10,36 @@ -- Component "metadata.jitmeet.example.com" "room_metadata_component" -- muc_component = "conference.jitmeet.example.com" -- breakout_rooms_component = "breakout.jitmeet.example.com" - +local filters = require 'util.filters'; local jid_node = require 'util.jid'.node; local json = require 'cjson.safe'; local st = require 'util.stanza'; +local jid = require 'util.jid'; local util = module:require 'util'; +local is_admin = util.is_admin; local is_healthcheck_room = util.is_healthcheck_room; local get_room_from_jid = util.get_room_from_jid; local room_jid_match_rewrite = util.room_jid_match_rewrite; local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite; local process_host_module = util.process_host_module; +local table_shallow_copy = util.table_shallow_copy; +local table_add = util.table_add; +local MUC_NS = 'http://jabber.org/protocol/muc'; local COMPONENT_IDENTITY_TYPE = 'room_metadata'; local FORM_KEY = 'muc#roominfo_jitsimetadata'; local muc_component_host = module:get_option_string('muc_component'); if muc_component_host == nil then - module:log("error", "No muc_component specified. No muc to operate on!"); + module:log('error', 'No muc_component specified. No muc to operate on!'); + return; +end + +local muc_domain_base = module:get_option_string('muc_mapper_domain_base'); +if not muc_domain_base then + module:log('warn', 'No muc_domain_base option set.'); return; end @@ -40,10 +51,13 @@ local main_muc_module; -- Utility functions -function getMetadataJSON(room) +-- Returns json string with the metadata for the room. +-- @param room The room object. +-- @param metadata Optional metadata to use instead of the room's jitsiMetadata. +function getMetadataJSON(room, metadata) local res, error = json.encode({ type = COMPONENT_IDENTITY_TYPE, - metadata = room.jitsiMetadata or {} + metadata = metadata or room.jitsiMetadata or {} }); if not res then @@ -53,28 +67,52 @@ function getMetadataJSON(room) return res; end --- Putting the information on the config form / disco-info allows us to save --- an extra message to users who join later. -function getFormData(room) - return { - name = FORM_KEY; - type = 'text-multi'; - label = 'Room metadata'; - value = getMetadataJSON(room); - }; -end - function broadcastMetadata(room) local json_msg = getMetadataJSON(room); + if not json_msg then + return; + end + for _, occupant in room:each_occupant() do - send_json_msg(occupant.jid, internal_room_jid_match_rewrite(room.jid), json_msg) + send_metadata(occupant, room, json_msg) end end -function send_json_msg(to_jid, room_jid, json_msg) - local stanza = st.message({ from = module.host; to = to_jid; }) - :tag('json-message', { xmlns = 'http://jitsi.org/jitmeet', room = room_jid }):text(json_msg):up(); +function send_metadata(occupant, room, json_msg) + if not json_msg or is_admin(occupant.bare_jid) then + local metadata_to_send = room.jitsiMetadata or {}; + + -- we want to send the main meeting participants only to jicofo + if is_admin(occupant.bare_jid) then + local participants = {}; + + if room._data.mainMeetingParticipants then + table_add(participants, room._data.mainMeetingParticipants); + end + + if room._data.moderator_id then + table.insert(participants, room._data.moderator_id); + end + + if room._data.moderators then + table_add(participants, room._data.moderators); + end + + if #participants > 0 then + metadata_to_send = table_shallow_copy(metadata_to_send); + metadata_to_send.mainMeetingParticipants = participants; + end + end + + json_msg = getMetadataJSON(room, metadata_to_send); + end + + local stanza = st.message({ from = module.host; to = occupant.jid; }) + :tag('json-message', { + xmlns = 'http://jitsi.org/jitmeet', + room = internal_room_jid_match_rewrite(room.jid) + }):text(json_msg):up(); module:send(stanza); end @@ -84,12 +122,14 @@ function room_created(event) local room = event.room; if is_healthcheck_room(room.jid) then - return ; + return; end if not room.jitsiMetadata then room.jitsiMetadata = {}; end + + room.sent_initial_metadata = {}; end function on_message(event) @@ -129,11 +169,6 @@ function on_message(event) return false; end - if occupant.role ~= 'moderator' then - module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid); - return false; - end - local jsonData, error = json.decode(messageText); if jsonData == nil then -- invalid JSON module:log("error", "Invalid JSON message: %s error:%s", messageText, error); @@ -145,6 +180,19 @@ function on_message(event) return false; end + if occupant.role ~= 'moderator' then + -- will return a non nil filtered data to use, if it is nil, it is not allowed + local res = module:context(muc_domain_base):fire_event('jitsi-metadata-allow-moderation', + { room = room; actor = occupant; key = jsonData.key ; data = jsonData.data; session = session; }); + + if not res then + module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid); + return false; + end + + jsonData.data = res; + end + room.jitsiMetadata[jsonData.key] = jsonData.data; broadcastMetadata(room); @@ -169,21 +217,39 @@ function process_main_muc_loaded(main_muc, host_module) host_module:hook("muc-room-created", room_created, -1); - host_module:hook('muc-disco#info', function (event) - local room = event.room; - - table.insert(event.form, getFormData(room)); - end); - - host_module:hook("muc-config-form", function(event) - local room = event.room; - - table.insert(event.form, getFormData(room)); - end); -- The room metadata was updated internally (from another module). host_module:hook("room-metadata-changed", function(event) broadcastMetadata(event.room); end); + + -- TODO: Once clients update to read/write metadata for startMuted policy we can drop this + -- this is to convert presence settings from old clients to metadata + host_module:hook('muc-broadcast-presence', function (event) + local actor, occupant, room, stanza, x = event.actor, event.occupant, event.room, event.stanza, event.x; + + if is_healthcheck_room(room.jid) or occupant.role ~= 'moderator' then + return; + end + + local startMuted = stanza:get_child('startmuted', 'http://jitsi.org/jitmeet/start-muted'); + + if not startMuted then + return; + end + + if not room.jitsiMetadata then + room.jitsiMetadata = {}; + end + + local startMutedMetadata = room.jitsiMetadata.startMuted or {}; + + startMutedMetadata.audio = startMuted.attr.audio == 'true'; + startMutedMetadata.video = startMuted.attr.video == 'true'; + + room.jitsiMetadata.startMuted = startMutedMetadata; + + host_module:fire_event('room-metadata-changed', { room = room; }); + end); end -- process or waits to process the main muc component @@ -208,18 +274,6 @@ function process_breakout_muc_loaded(breakout_muc, host_module) module:log("info", "Hook to muc events on %s", breakout_rooms_component_host); host_module:hook("muc-room-created", room_created, -1); - - host_module:hook('muc-disco#info', function (event) - local room = event.room; - - table.insert(event.form, getFormData(room)); - end); - - host_module:hook("muc-config-form", function(event) - local room = event.room; - - table.insert(event.form, getFormData(room)); - end); end if breakout_rooms_component_host then @@ -238,3 +292,53 @@ if breakout_rooms_component_host then end end); end + +-- Send a message update for metadata before sending the first self presence +function filter_stanza(stanza, session) + if not stanza.attr or not stanza.attr.to or stanza.name ~= 'presence' + or stanza.attr.type == 'unavailable' or ends_with(stanza.attr.from, '/focus') then + return stanza; + end + + local bare_to = jid.bare(stanza.attr.to); + local muc_x = stanza:get_child('x', MUC_NS..'#user'); + if not muc_x or not presence_check_status(muc_x, '110') then + return stanza; + end + + local room = get_room_from_jid(room_jid_match_rewrite(jid.bare(stanza.attr.from))); + + if not room or not room.sent_initial_metadata or is_healthcheck_room(room.jid) then + return stanza; + end + + if room.sent_initial_metadata[bare_to] then + return stanza; + end + + local occupant; + for _, o in room:each_occupant() do + if o.bare_jid == bare_to then + occupant = o; + end + end + + if not occupant then + module:log('warn', 'No occupant %s found for %s', bare_to, room.jid); + return stanza; + end + + room.sent_initial_metadata[bare_to] = true; + + send_metadata(occupant, room); + + return stanza; +end +function filter_session(session) + -- domain mapper is filtering on default priority 0 + -- allowners is -1 and we need it after that, permissions is -2 + filters.add_filter(session, 'stanzas/out', filter_stanza, -3); +end + +-- enable filtering presences +filters.add_filter_hook(filter_session); diff --git a/roles/jitsi/files/prosody/modules/mod_short_lived_token.lua b/roles/jitsi/files/prosody/modules/mod_short_lived_token.lua new file mode 100644 index 0000000..bc40ed4 --- /dev/null +++ b/roles/jitsi/files/prosody/modules/mod_short_lived_token.lua @@ -0,0 +1,136 @@ +-- to be enabled under the main virtual host with all required settings +-- short_lived_token = { +-- issuer = 'myissuer'; +-- accepted_audiences = { 'file-sharing' }; +-- key_path = '/etc/prosody/short_lived_token.key'; +-- key_id = 'my_kid'; +-- ttl_seconds = 30; +-- }; +-- The key in key_path can be generated via: openssl genrsa -out $PRIVATE_KEY_PATH 2048 +-- And you can get the public key from it, which can be used ot verify those tokens via: +-- openssl rsa -in $PRIVATE_KEY_PATH -pubout -out $PUBLIC_KEY_PATH + +local jid = require 'util.jid'; +local st = require 'util.stanza'; +local jwt = module:require 'luajwtjitsi'; + +local util = module:require 'util'; +local is_vpaas = util.is_vpaas; +local process_host_module = util.process_host_module; +local table_find = util.table_find; +local create_throttle = require 'prosody.util.throttle'.create; + +local SERVICE_TYPE = 'short-lived-token'; +local options = module:get_option('short_lived_token'); + +if not (options.issuer and options.accepted_audiences + and options.key_path and options.key_id and options.ttl_seconds) then + module:log('error', 'Missing required options for short_lived_token'); + return; +end + +local f = io.open(options.key_path, 'r'); +if f then + options.key = f:read('*all'); + f:close(); +end + +local accepted_requests = {}; +for _, host in pairs(options.accepted_audiences) do + accepted_requests[string.format('%s:%s:0', SERVICE_TYPE, host)] = host; +end + +local server_region_name = module:get_option_string('region_name'); + +local main_muc_component_host = module:get_option_string('main_muc'); +if main_muc_component_host == nil then + module:log('error', 'main_muc not configured. Cannot proceed.'); + return; +end +local main_muc_service; + +function generateToken(session, audience, room, occupant) + local t = os.time(); + local exp = t + options.ttl_seconds; + local presence = occupant:get_presence(session.full_jid); + local _, _, id = extract_subdomain(jid.node(room.jid)); + + local payload = { + iss = options.issuer, + aud = audience, + nbf = t, + exp = exp, + sub = session.jitsi_web_query_prefix or module.host, + context = { + group = session.jitsi_meet_context_group or session.granted_jitsi_meet_context_group, + user = session.jitsi_meet_context_user or { + id = session.full_jid, + name = presence:get_child_text('nick', 'http://jabber.org/protocol/nick'), + email = presence:get_child_text("email") or nil, + nick = jid.resource(occupant.nick) + }, + features = session.jitsi_meet_context_features or session.granted_jitsi_meet_context_features + }, + room = session.jitsi_web_query_room, + meeting_id = room._data.meetingId, + granted_from = session.granted_jitsi_meet_context_user_id, + customer_id = id or session.jitsi_meet_context_group or session.granted_jitsi_meet_context_group, + backend_region = server_region_name, + user_region = session.user_region + }; + + local alg = 'RS256'; + local token, err = jwt.encode(payload, options.key, alg, { kid = options.key_id }); + if not err then + return token + else + module:log('error', 'Error generating token: %s', err); + return '' + end +end + +module:hook('external_service/credentials', function (event) + local requested_credentials, services, session, stanza + = event.requested_credentials, event.services, event.origin, event.stanza; + local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix); + + if not room then + session.send(st.error_reply(stanza, 'cancel', 'not-allowed')); + return; + end + + local occupant = room:get_occupant_by_real_jid(session.full_jid); + if not occupant then + session.send(st.error_reply(stanza, 'cancel', 'not-allowed')); + return; + end + + for request in requested_credentials do + local host = accepted_requests[request]; + if host then + services:push({ + type = SERVICE_TYPE; + host = host; + username = 'token'; + password = generateToken(session, host, room, occupant); + expires = os.time() + options.ttl_seconds; + restricted = true; + }); + end + end + +end); + +process_host_module(main_muc_component_host, function(host_module, host) + local muc_module = prosody.hosts[host].modules.muc; + + if muc_module then + main_muc_service = muc_module; + else + prosody.hosts[host].events.add_handler('module-loaded', function(event) + if (event.module == 'muc') then + main_muc_service = prosody.hosts[host].modules.muc; + end + end); + end +end); diff --git a/roles/jitsi/files/prosody/modules/mod_speakerstats_component.lua b/roles/jitsi/files/prosody/modules/mod_speakerstats_component.lua index 3487b17..2e5e1c1 100644 --- a/roles/jitsi/files/prosody/modules/mod_speakerstats_component.lua +++ b/roles/jitsi/files/prosody/modules/mod_speakerstats_component.lua @@ -1,13 +1,15 @@ local util = module:require "util"; +local is_admin = util.is_admin; local get_room_from_jid = util.get_room_from_jid; local room_jid_match_rewrite = util.room_jid_match_rewrite; +local is_jibri = util.is_jibri; local is_healthcheck_room = util.is_healthcheck_room; local process_host_module = util.process_host_module; +local is_transcriber_jigasi = util.is_transcriber_jigasi; local jid_resource = require "util.jid".resource; local st = require "util.stanza"; local socket = require "socket"; local json = require 'cjson.safe'; -local um_is_admin = require "core.usermanager".is_admin; local jid_split = require 'util.jid'.split; -- we use async to detect Prosody 0.10 and earlier @@ -30,10 +32,6 @@ module:log("info", "Starting speakerstats for %s", muc_component_host); local main_muc_service; -local function is_admin(jid) - return um_is_admin(jid, module.host); -end - -- Searches all rooms in the main muc component that holds a breakout room -- caches it if found so we don't search it again -- we should not cache objects in _data as this is being serialized when calling room:save() @@ -221,14 +219,15 @@ end -- Create SpeakerStats object for the joined user function occupant_joined(event) - local occupant, room = event.occupant, event.room; + local occupant, room, stanza = event.occupant, event.room, event.stanza; - if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then + if is_healthcheck_room(room.jid) + or is_admin(occupant.bare_jid) + or is_transcriber_jigasi(stanza) + or is_jibri(occupant) then return; end - local occupant = event.occupant; - local nick = jid_resource(occupant.nick); if room.speakerStats then diff --git a/roles/jitsi/files/prosody/modules/mod_system_chat_message.lua b/roles/jitsi/files/prosody/modules/mod_system_chat_message.lua index 79c71e1..ddb3723 100644 --- a/roles/jitsi/files/prosody/modules/mod_system_chat_message.lua +++ b/roles/jitsi/files/prosody/modules/mod_system_chat_message.lua @@ -15,7 +15,6 @@ local get_room_from_jid = util.get_room_from_jid; local st = require "util.stanza"; local json = require "cjson.safe"; -local muc_domain_base = module:get_option_string("muc_mapper_domain_base"); local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", ""); if asapKeyServer then diff --git a/roles/jitsi/files/prosody/modules/mod_token_verification.lua b/roles/jitsi/files/prosody/modules/mod_token_verification.lua index a888dc0..a4b979b 100644 --- a/roles/jitsi/files/prosody/modules/mod_token_verification.lua +++ b/roles/jitsi/files/prosody/modules/mod_token_verification.lua @@ -4,19 +4,17 @@ local log = module._log; local host = module.host; local st = require "util.stanza"; -local um_is_admin = require "core.usermanager".is_admin; local jid_split = require 'util.jid'.split; local jid_bare = require 'util.jid'.bare; +local util = module:require 'util'; +local is_admin = util.is_admin; + local DEBUG = false; local measure_success = module:measure('success', 'counter'); local measure_fail = module:measure('fail', 'counter'); -local function is_admin(jid) - return um_is_admin(jid, host); -end - local parentHostName = string.gmatch(tostring(host), "%w+.(%w.+)")(); if parentHostName == nil then module:log("error", "Failed to start - unable to get parent hostname"); diff --git a/roles/jitsi/files/prosody/modules/mod_visitors.lua b/roles/jitsi/files/prosody/modules/mod_visitors.lua index 8070100..b0719e0 100644 --- a/roles/jitsi/files/prosody/modules/mod_visitors.lua +++ b/roles/jitsi/files/prosody/modules/mod_visitors.lua @@ -12,13 +12,10 @@ local st = require 'util.stanza'; local jid = require 'util.jid'; local new_id = require 'util.id'.medium; local util = module:require 'util'; +local is_admin = util.is_admin; local presence_check_status = util.presence_check_status; local process_host_module = util.process_host_module; - -local um_is_admin = require 'core.usermanager'.is_admin; -local function is_admin(jid) - return um_is_admin(jid, module.host); -end +local is_transcriber_jigasi = util.is_transcriber_jigasi; local MUC_NS = 'http://jabber.org/protocol/muc'; @@ -57,7 +54,7 @@ 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({ + local visitors_iq = st.iq({ type = 'set', to = conference_service, from = module.host, @@ -68,11 +65,24 @@ local function send_visitors_iq(conference_service, room, type) password = type ~= 'disconnect' and room:get_password() or '', lobby = room._data.lobbyroom and 'true' or 'false', meetingId = room._data.meetingId, - moderatorId = room._data.moderator_id, -- can be used from external modules to set single moderator for meetings createdTimestamp = room.created_timestamp and tostring(room.created_timestamp) or nil - }):up(); + }); - module:send(connect_done); + if type == 'update' then + visitors_iq:tag('moderators', { xmlns = 'jitsi:visitors' }); + + for _, o in room:each_occupant() do + if not is_admin(o.bare_jid) and o.role == 'moderator' then + visitors_iq:tag('item', { epId = jid.resource(o.nick) }):up(); + end + end + + visitors_iq:up(); + end + + visitors_iq:up(); + + module:send(visitors_iq); end -- an event received from visitors component, which receives iqs from jicofo @@ -96,6 +106,9 @@ local function connect_vnode(event) local sent_main_participants = 0; + -- send update initially so we can report the moderators that will join + send_visitors_iq(conference_service, room, 'update'); + for _, o in room:each_occupant() do if not is_admin(o.bare_jid) then local fmuc_pr = st.clone(o:get_presence()); @@ -185,7 +198,7 @@ process_host_module(main_muc_component_config, function(host_module, host) -- filter focus and configured domains (used for jibri and transcribers) if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil - or ignore_list:contains(jid.host(occupant.bare_jid)) then + or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then return; end @@ -206,7 +219,7 @@ process_host_module(main_muc_component_config, function(host_module, host) -- ignore configured domains (jibri and transcribers) if is_admin(occupant.bare_jid) or visitors_nodes[room.jid] == nil or visitors_nodes[room.jid].nodes == nil - or ignore_list:contains(jid.host(occupant.bare_jid)) then + or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then return; end @@ -249,7 +262,7 @@ process_host_module(main_muc_component_config, function(host_module, host) -- filter focus, ignore configured domains (jibri and transcribers) if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil - or ignore_list:contains(jid.host(occupant.bare_jid)) then + or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then return; end @@ -257,6 +270,10 @@ process_host_module(main_muc_component_config, function(host_module, host) local user, _, res = jid.split(occupant.nick); -- a main participant we need to update all active visitor nodes for k in pairs(vnodes) do + if occupant.role == 'moderator' then + -- first send that the participant is a moderator + send_visitors_iq(k, room, 'update'); + end local fmuc_pr = st.clone(stanza); fmuc_pr.attr.to = jid.join(user, k, res); fmuc_pr.attr.from = occupant.jid; @@ -268,7 +285,7 @@ process_host_module(main_muc_component_config, function(host_module, host) local room, stanza, occupant = event.room, event.stanza, event.occupant; -- filter sending messages from transcribers/jibris to visitors - if not visitors_nodes[room.jid] or ignore_list:contains(jid.host(occupant.bare_jid)) then + if not visitors_nodes[room.jid] then return; end @@ -296,6 +313,11 @@ process_host_module(main_muc_component_config, function(host_module, host) return; end + if host_module:fire_event('jitsi-visitor-groupchat-pre-route', event) then + -- message filtered + return; + end + -- a message from visitor occupant of known visitor node stanza.attr.from = to; for _, o in room:each_occupant() do @@ -331,6 +353,21 @@ process_host_module(main_muc_component_config, function(host_module, host) end end end, -100); -- we want to run last in order to check is the status code 104 + + host_module:hook('muc-set-affiliation', function (event) + if event.actor and not is_admin(event.actor) and event.affiliation == 'owner' then + local room = event.room; + + if not visitors_nodes[room.jid] then + return; + end + -- 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, -2); end); module:hook('jitsi-lobby-enabled', function(event) diff --git a/roles/jitsi/files/prosody/modules/mod_visitors_component.lua b/roles/jitsi/files/prosody/modules/mod_visitors_component.lua index 0b33644..deba8bf 100644 --- a/roles/jitsi/files/prosody/modules/mod_visitors_component.lua +++ b/roles/jitsi/files/prosody/modules/mod_visitors_component.lua @@ -1,9 +1,11 @@ module:log('info', 'Starting visitors_component at %s', module.host); +local array = require "util.array"; local http = require 'net.http'; local jid = require 'util.jid'; local st = require 'util.stanza'; local util = module:require 'util'; +local is_admin = util.is_admin; local is_healthcheck_room = util.is_healthcheck_room; local is_sip_jigasi = util.is_sip_jigasi; local room_jid_match_rewrite = util.room_jid_match_rewrite; @@ -11,11 +13,13 @@ 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 internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite; +local table_find = util.table_find; local is_vpaas = util.is_vpaas; local is_sip_jibri_join = util.is_sip_jibri_join; local process_host_module = util.process_host_module; +local respond_iq_result = util.respond_iq_result; +local split_string = util.split_string; local new_id = require 'util.id'.medium; -local um_is_admin = require 'core.usermanager'.is_admin; local json = require 'cjson.safe'; local inspect = require 'inspect'; @@ -47,10 +51,6 @@ local http_headers = { ["Accept"] = "application/json" }; -local function is_admin(jid) - return um_is_admin(jid, module.host); -end - -- This is a map to keep data for room and the jids that were allowed to join after visitor mode is enabled -- automatically allowed or allowed by a moderator local visitors_promotion_map = {}; @@ -63,17 +63,6 @@ local visitors_promotion_requests = {}; local cache = require 'util.cache'; local sent_iq_cache = cache.new(200); --- send iq result that the iq was received and will be processed -local function respond_iq_result(origin, stanza) - -- respond with successful receiving the iq - origin.send(st.iq({ - type = 'result'; - from = stanza.attr.to; - to = stanza.attr.from; - id = stanza.attr.id - })); -end - -- Sends a json-message to the destination jid -- @param to_jid the destination jid -- @param json_message the message content to send @@ -83,13 +72,33 @@ function send_json_message(to_jid, json_message) module:send(stanza); end -local function request_promotion_received(room, from_jid, from_vnode, nick, time, user_id, force_promote) +local function request_promotion_received(room, from_jid, from_vnode, nick, time, user_id, group_id, force_promote_requested) -- if visitors is enabled for the room if visitors_promotion_map[room.jid] then + local force_promote = auto_allow_promotion; + if not force_promote and force_promote_requested == 'true' then + -- Let's do the force_promote checks if requested + -- if it is vpaas meeting we trust the moderator computation from visitor node (value of force_promote_requested) + -- if it is not vpaas we need to check further settings only if they exist + if is_vpaas(room) or (not room._data.moderator_id and not room._data.moderators) + -- _data.moderator_id can be used from external modules to set single moderator for a meeting + -- or a whole group of moderators + or (room._data.moderator_id + and room._data.moderator_id == user_id or room._data.moderator_id == group_id) + + -- all moderators are allowed to auto promote, the fact that user_id and force_promote_requested are set + -- means that the user has token and is moderator on visitor node side + or room._data.allModerators + + -- can be used by external modules to set multiple moderator ids (table of values) + or table_find(room._data.moderators, user_id) + then + force_promote = true; + end + end + -- only for raise hand, ignore lowering the hand - if time and time > 0 and ( - auto_allow_promotion - or force_promote == 'true') then + if time and time > 0 and force_promote then -- we are in auto-allow mode, let's reply with accept -- we store where the request is coming from so we can send back the response local username = new_id():lower(); @@ -179,7 +188,7 @@ local function connect_vnode_received(room, vnode) room._connected_vnodes = cache.new(16); -- we up to 16 vnodes for this prosody end - room._connected_vnodes:set(vnode..'.meet.jitsi', 'connected'); + room._connected_vnodes:set(vnode..'.meet.jitsi', {}); end local function disconnect_vnode_received(room, vnode) @@ -194,6 +203,44 @@ local function disconnect_vnode_received(room, vnode) end end +-- returns the accumulated data for visitors nodes, count all visitors requesting transcriptions +-- and accumulated languages requested +-- @returns count, languages +function get_visitors_languages(room) + if not room._connected_vnodes then + return; + end + + local count = 0; + local languages = array(); + + -- iterate over visitor nodes we are connected to and accumulate data if we have it + for k, v in room._connected_vnodes:items() do + if v.count then + count = count + v.count; + end + if v.langs then + for k in pairs(v.langs) do + local val = v.langs[k] + if not languages[val] then + languages:push(val); + end + end + end + end + return count, languages:sort():concat(','); +end + +local function get_visitors_room_metadata(room) + if not room.jitsiMetadata then + room.jitsiMetadata = {}; + end + if not room.jitsiMetadata.visitors then + room.jitsiMetadata.visitors = {}; + end + return room.jitsiMetadata.visitors; +end + -- listens for iq request for promotion and forward it to moderators in the meeting for approval -- or auto-allow it if such the config is set enabling it local function stanza_handler(event) @@ -229,15 +276,20 @@ local function stanza_handler(event) if not room then -- this maybe as we receive the iq from jicofo after the room is already destroyed module:log('debug', 'No room found %s', room_jid); - return; + return true; + end + + local from_vnode; + if room._connected_vnodes then + from_vnode = room._connected_vnodes:get(stanza.attr.from); end local processed; -- promotion request is coming from visitors and is a set and is over the s2s connection local request_promotion = visitors_iq:get_child('promotion-request'); if request_promotion then - if not (room._connected_vnodes and room._connected_vnodes:get(stanza.attr.from)) then - module:log('warn', 'Received forged promotion-request: %s %s %s', stanza, inspect(room._connected_vnodes), room._connected_vnodes:get(stanza.attr.from)); + if not from_vnode then + module:log('warn', 'Received forged request_promotion message: %s %s',stanza, inspect(room._connected_vnodes)); return true; -- stop processing end @@ -249,6 +301,7 @@ local function stanza_handler(event) display_name, tonumber(request_promotion.attr.time), request_promotion.attr.userId, + request_promotion.attr.groupId, request_promotion.attr.forcePromote ); end @@ -266,6 +319,40 @@ local function stanza_handler(event) end end + -- request to update metadata service for jigasi languages + local transcription_languages = visitors_iq:get_child('transcription-languages'); + + if transcription_languages + and (transcription_languages.attr.langs or transcription_languages.attr.count) then + if not from_vnode then + module:log('warn', 'Received forged transcription_languages message: %s %s',stanza, inspect(room._connected_vnodes)); + return true; -- stop processing + end + + local metadata = get_visitors_room_metadata(room); + + -- we keep the split by languages array to optimize accumulating languages + from_vnode.langs = split_string(transcription_languages.attr.langs, ','); + from_vnode.count = transcription_languages.attr.count; + + local count, languages = get_visitors_languages(room); + + if metadata.transcribingLanguages ~= languages then + metadata.transcribingLanguages = languages; + processed = true; + end + + if metadata.transcribingCount ~= count then + metadata.transcribingCount = count; + processed = true; + end + + if processed then + module:context(muc_domain_prefix..'.'..muc_domain_base) + :fire_event('room-metadata-changed', { room = room; }); + end + end + if not processed then module:log('warn', 'Unknown iq received for %s: %s', module.host, stanza); end @@ -275,6 +362,11 @@ local function stanza_handler(event) end local function process_promotion_response(room, id, approved) + if not approved then + module:log('debug', 'promotion not approved %s, %s', room.jid, id); + return; + end + -- lets reply to participant that requested promotion local username = new_id():lower(); visitors_promotion_map[room.jid][username] = { @@ -312,7 +404,9 @@ local function go_live(room) return; end - if not (room.jitsiMetadata and room.jitsiMetadata.visitors and room.jitsiMetadata.visitors.live) then + -- if missing we assume room is live, only skip if it is marked explicitly as false + if room.jitsiMetadata and room.jitsiMetadata.visitors + and room.jitsiMetadata.visitors.live ~= nil and room.jitsiMetadata.visitors.live == false then return; end @@ -387,10 +481,23 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul end if visitors_promotion_map[room.jid] then + local in_ignore_list = ignore_list:contains(jid.host(stanza.attr.from)); + -- now let's check for jid if visitors_promotion_map[room.jid][jid.node(stanza.attr.from)] -- promotion was approved - or ignore_list:contains(jid.host(stanza.attr.from)) then -- jibri or other domains to ignore + or in_ignore_list then -- jibri or other domains to ignore -- allow join + if not in_ignore_list then + -- let's update metadata + local metadata = get_visitors_room_metadata(room); + if not metadata.promoted then + metadata.promoted = {}; + end + metadata.promoted[jid.resource(occupant.nick)] = true; + module:context(muc_domain_prefix..'.'..muc_domain_base) + :fire_event('room-metadata-changed', { room = room; }); + end + return; end module:log('error', 'Visitor needs to be allowed by a moderator %s', stanza.attr.from); @@ -446,17 +553,11 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul end end end); - host_module:hook("message/bare", function(event) - local stanza = event.stanza; - if stanza.attr.type ~= "groupchat" then - return; - end - local json_data = stanza:get_child_text("json-message", "http://jitsi.org/jitmeet"); - if json_data == nil then - return; - end - local data, error = json.decode(json_data); + host_module:hook('jitsi-endpoint-message-received', function(event) + local data, error, occupant, room, stanza + = event.message, event.error, event.occupant, event.room, event.stanza; + if not data or data.type ~= 'visitors' or (data.action ~= "promotion-response" and data.action ~= "demote-request") then if error then @@ -465,17 +566,9 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul return; end - local room = get_room_from_jid(event.stanza.attr.to); - - local occupant_jid = event.stanza.attr.from; - local occupant = room:get_occupant_by_real_jid(occupant_jid); - if not occupant then - module:log("error", "Occupant %s was not found in room %s", occupant_jid, room.jid) - return - end if occupant.role ~= 'moderator' then module:log('error', 'Occupant %s sending response message but not moderator in room %s', - occupant_jid, room.jid); + occupant.jid, room.jid); return false; end @@ -501,7 +594,6 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul end end end - else if data.id then process_promotion_response(room, data.id, data.approved and 'true' or 'false'); @@ -515,6 +607,7 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul return true; -- halt processing, but return true that we handled it end); + if visitors_queue_service then host_module:hook('muc-room-created', function (event) local room = event.room; @@ -535,7 +628,13 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul go_live(event.room); end); host_module:hook('muc-occupant-joined', function (event) - go_live(event.room); + local room = event.room; + + if is_healthcheck_room(room.jid) then + return; + end + + go_live(room); end); end @@ -571,3 +670,22 @@ prosody.events.add_handler('pre-jitsi-authentication', function(session) return session.customusername; end end); + +-- when occupant is leaving breakout to join the main room and visitors are enabled +-- make sure we will allow that participant to join as it is already part of the main room +function handle_occupant_leaving_breakout(event) + local main_room, occupant, stanza = event.main_room, event.occupant, event.stanza; + local presence_status = stanza:get_child_text('status'); + + if presence_status ~= 'switch_room' or not visitors_promotion_map[main_room.jid] then + return; + end + + local node = jid.node(occupant.bare_jid); + + visitors_promotion_map[main_room.jid][node] = { + from = 'none'; + jid = occupant.bare_jid; + }; +end +module:hook_global('jitsi-breakout-occupant-leaving', handle_occupant_leaving_breakout); diff --git a/roles/jitsi/files/prosody/modules/token/util.lib.lua b/roles/jitsi/files/prosody/modules/token/util.lib.lua index 2668815..739d985 100644 --- a/roles/jitsi/files/prosody/modules/token/util.lib.lua +++ b/roles/jitsi/files/prosody/modules/token/util.lib.lua @@ -80,7 +80,7 @@ function Util.new(module) These setups relay on configuration 'muc_domain_base' which holds the main domain and we use it to subtract subdomains from the virtual addresses. - The following confgurations are for multidomain setups and domain name + The following configurations are for multidomain setups and domain name verification: --]] @@ -225,7 +225,7 @@ function Util:get_public_key(keyId) self.cache:set(keyId, content); else if code == nil then - -- this is timout after nr_retries retries + -- this is timeout after nr_retries retries module:log('warn', 'Timeout retrieving %s from %s', keyId, keyurl); end end diff --git a/roles/jitsi/files/prosody/modules/util.lib.lua b/roles/jitsi/files/prosody/modules/util.lib.lua index d51f5ae..32fde74 100644 --- a/roles/jitsi/files/prosody/modules/util.lib.lua +++ b/roles/jitsi/files/prosody/modules/util.lib.lua @@ -1,7 +1,15 @@ +local http_server = require "net.http.server"; local jid = require "util.jid"; +local st = require 'util.stanza'; local timer = require "util.timer"; local http = require "net.http"; local cache = require "util.cache"; +local array = require "util.array"; +local is_set = require 'util.set'.is_set; +local usermanager = require 'core.usermanager'; + +local config_global_admin_jids = module:context('*'):get_option_set('admins', {}) / jid.prep; +local config_admin_jids = module:get_option_inherited_set('admins', {}) / jid.prep; local http_timeout = 30; local have_async, async = pcall(require, "util.async"); @@ -28,6 +36,8 @@ local roomless_iqs = {}; local OUTBOUND_SIP_JIBRI_PREFIXES = { 'outbound-sip-jibri@', 'sipjibriouta@', 'sipjibrioutb@' }; local INBOUND_SIP_JIBRI_PREFIXES = { 'inbound-sip-jibri@', 'sipjibriina@', 'sipjibriina@' }; +local RECORDER_PREFIXES = module:get_option_inherited_set('recorder_prefixes', { 'recorder@recorder.', 'jibria@recorder.', 'jibrib@recorder.' }); +local TRANSCRIBER_PREFIXES = module:get_option_inherited_set('transcriber_prefixes', { 'transcriber@recorder.', 'transcribera@recorder.', 'transcriberb@recorder.' }); local split_subdomain_cache = cache.new(1000); local extract_subdomain_cache = cache.new(1000); @@ -122,11 +132,7 @@ function get_room_from_jid(room_jid) local component = hosts[host]; if component then local muc = component.modules.muc - if muc and rawget(muc,"rooms") then - -- We're running 0.9.x or 0.10 (old MUC API) - return muc.rooms[room_jid]; - elseif muc and rawget(muc,"get_room_from_jid") then - -- We're running >0.10 (new MUC API) + if muc then return muc.get_room_from_jid(room_jid); else return @@ -202,8 +208,7 @@ end -- @param creator_group the group of the user who created the user which -- presence we are updating (this is the poltergeist case, where a user creates -- a poltergeist), optional. -function update_presence_identity( - stanza, user, group, creator_user, creator_group) +function update_presence_identity(stanza, user, group, creator_user, creator_group) -- First remove any 'identity' element if it already -- exists, so it cannot be spoofed by a client @@ -216,7 +221,11 @@ function update_presence_identity( end return tag end - ) + ); + + if not user then + return; + end stanza:tag("identity"):tag("user"); for k, v in pairs(user) do @@ -250,28 +259,36 @@ end -- Utility function to check whether feature is present and enabled. Allow -- a feature if there are features present in the session(coming from -- the token) and the value of the feature is true. --- If features is not present in the token we skip feature detection and allow --- everything. -function is_feature_allowed(features, ft) - if (features == nil or features[ft] == "true" or features[ft] == true) then - return true; +-- If features are missing but we have granted_features check that +-- if features are missing from the token we check whether it is moderator +function is_feature_allowed(ft, features, granted_features, is_moderator) + if features then + return features[ft] == "true" or features[ft] == true; + elseif granted_features then + return granted_features[ft] == "true" or granted_features[ft] == true; else - return false; + return is_moderator; end end --- Extracts the subdomain and room name from internal jid node [foo]room1 --- @return subdomain(optional, if extracted or nil), the room name +-- @return subdomain(optional, if extracted or nil), the room name, the customer_id in case of vpaas function extract_subdomain(room_node) local ret = extract_subdomain_cache:get(room_node); if ret then - return ret.subdomain, ret.room; + return ret.subdomain, ret.room, ret.customer_id; end local subdomain, room_name = room_node:match("^%[([^%]]+)%](.+)$"); - local cache_value = {subdomain=subdomain, room=room_name}; + + if not subdomain then + room_name = room_node; + end + + local _, customer_id = subdomain and subdomain:match("^(vpaas%-magic%-cookie%-)(.*)$") or nil, nil; + local cache_value = { subdomain=subdomain, room=room_name, customer_id=customer_id }; extract_subdomain_cache:set(room_node, cache_value); - return subdomain, room_name; + return subdomain, room_name, customer_id; end function starts_with(str, start) @@ -282,19 +299,33 @@ function starts_with(str, start) end function starts_with_one_of(str, prefixes) - if not str then + if not str or not prefixes then return false; end - for i=1,#prefixes do - if starts_with(str, prefixes[i]) then - return prefixes[i]; + + if is_set(prefixes) then + -- set is a table with keys and value of true + for k, _ in prefixes:items() do + if starts_with(str, k) then + return k; + end + end + else + for _, v in pairs(prefixes) do + if starts_with(str, v) then + return v; + end end end + return false end - function ends_with(str, ending) + if not str then + return false; + end + return ending == "" or str:sub(-#ending) == ending end @@ -468,9 +499,38 @@ end -- Returns the initiator extension if the stanza is coming from a sip jigasi function is_sip_jigasi(stanza) + if not stanza then + return false; + end + return stanza:get_child('initiator', 'http://jitsi.org/protocol/jigasi'); end +-- This requires presence stanza being passed +function is_transcriber_jigasi(stanza) + if not stanza then + return false; + end + + local features = stanza:get_child('features'); + if not features then + return false; + end + + for i = 1, #features do + local feature = features[i]; + if feature.attr and feature.attr.var and feature.attr.var == 'http://jitsi.org/protocol/transcriber' then + return true; + end + end + + return false; +end + +function is_transcriber(jid) + return starts_with_one_of(jid, TRANSCRIBER_PREFIXES); +end + function get_sip_jibri_email_prefix(email) if not email then return nil; @@ -508,6 +568,10 @@ function is_sip_jibri_join(stanza) return false end +function is_jibri(occupant) + return starts_with_one_of(type(occupant) == "string" and occupant or occupant.jid, RECORDER_PREFIXES) +end + -- 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) @@ -535,30 +599,107 @@ function table_shallow_copy(t) return t2 end +local function table_find(tab, val) + if not tab then + return nil + end + + for i, v in ipairs(tab) do + if v == val then + return i + end + end + return nil +end + +-- Adds second table values to the first table +local function table_add(t1, t2) + for _,v in ipairs(t2) do + table.insert(t1, v); + end +end + +-- Splits a string using delimiter +function split_string(str, delimiter) + str = str .. delimiter; + local result = array(); + for w in str:gmatch("(.-)" .. delimiter) do + result:push(w); + end + + return result; +end + +-- send iq result that the iq was received and will be processed +function respond_iq_result(origin, stanza) + -- respond with successful receiving the iq + origin.send(st.iq({ + type = 'result'; + from = stanza.attr.to; + to = stanza.attr.from; + id = stanza.attr.id + })); +end + +-- Note: http_server.get_request_from_conn() was added in Prosody 0.12.3, +-- this code provides backwards compatibility with older versions +local get_request_from_conn = http_server.get_request_from_conn or function (conn) + local response = conn and conn._http_open_response; + return response and response.request or nil; +end; + +-- Discover real remote IP of a session +function get_ip(session) + local request = get_request_from_conn(session.conn); + return request and request.ip or session.ip; +end + +-- Checks whether the provided jid is in the list of admins +-- we are not using the new permissions and roles api as we have few global modules which need to be +-- refactored into host modules, as that api needs to be executed in host context +local function is_admin(_jid) + local bare_jid = jid.bare(_jid); + + if config_global_admin_jids:contains(bare_jid) or config_admin_jids:contains(bare_jid) then + return true; + end + return false; +end + return { OUTBOUND_SIP_JIBRI_PREFIXES = OUTBOUND_SIP_JIBRI_PREFIXES; INBOUND_SIP_JIBRI_PREFIXES = INBOUND_SIP_JIBRI_PREFIXES; + RECORDER_PREFIXES = RECORDER_PREFIXES; extract_subdomain = extract_subdomain; + is_admin = is_admin; is_feature_allowed = is_feature_allowed; + is_jibri = is_jibri; is_healthcheck_room = is_healthcheck_room; is_moderated = is_moderated; is_sip_jibri_join = is_sip_jibri_join; is_sip_jigasi = is_sip_jigasi; + is_transcriber = is_transcriber; + is_transcriber_jigasi = is_transcriber_jigasi; is_vpaas = is_vpaas; get_focus_occupant = get_focus_occupant; + get_ip = get_ip; get_room_from_jid = get_room_from_jid; get_room_by_name_and_subdomain = get_room_by_name_and_subdomain; get_sip_jibri_email_prefix = get_sip_jibri_email_prefix; async_handler_wrapper = async_handler_wrapper; presence_check_status = presence_check_status; process_host_module = process_host_module; + respond_iq_result = respond_iq_result; room_jid_match_rewrite = room_jid_match_rewrite; room_jid_split_subdomain = room_jid_split_subdomain; internal_room_jid_match_rewrite = internal_room_jid_match_rewrite; update_presence_identity = update_presence_identity; http_get_with_retry = http_get_with_retry; ends_with = ends_with; + split_string = split_string; starts_with = starts_with; starts_with_one_of = starts_with_one_of; + table_add = table_add; table_shallow_copy = table_shallow_copy; + table_find = table_find; }; diff --git a/roles/jitsi/templates/prosody.cfg.lua.j2 b/roles/jitsi/templates/prosody.cfg.lua.j2 index 5ee1523..97c51c7 100644 --- a/roles/jitsi/templates/prosody.cfg.lua.j2 +++ b/roles/jitsi/templates/prosody.cfg.lua.j2 @@ -3,6 +3,7 @@ plugin_paths = { "{{ jitsi_root_dir }}/prosody/modules" } muc_mapper_domain_base = "{{ jitsi_domain }}"; admins = { "{{ jitsi_jicofo_xmpp_user }}@{{ jitsi_auth_domain }}" }; +component_admins_as_room_owners = true; http_default_host = "{{ jitsi_domain }}"; -- Enable use of native prosody 0.11 support for epoll over select diff --git a/roles/jitsi_videobridge/defaults/main.yml b/roles/jitsi_videobridge/defaults/main.yml index 355166a..bfa01f3 100644 --- a/roles/jitsi_videobridge/defaults/main.yml +++ b/roles/jitsi_videobridge/defaults/main.yml @@ -3,9 +3,9 @@ jitsi_root_dir: /opt/jitsi jitsi_user: jitsi -jitsi_videobridge_version: "{{ jitsi_version | default('10133') }}" +jitsi_videobridge_version: "{{ jitsi_version | default('10314') }}" jitsi_videobridge_archive_url: https://github.com/jitsi/jitsi-videobridge/archive/refs/tags/stable/jitsi-meet_{{ jitsi_videobridge_version }}.tar.gz -jitsi_videobridge_archive_sha256: 79e7d50c3eb8c2528830b32ca8428235c02f6049f5601c4c0e7bfb772d980ec6 +jitsi_videobridge_archive_sha256: 55ffb62de63c7280b4e2a6c25045da9c6f3e07c20f28d1b697510231ae56f8bf jitsi_videobridge_rtp_port: 10000 jitsi_videobridge_src_ip: diff --git a/roles/repo_base/templates/postgresql-client.repo.j2 b/roles/repo_base/templates/postgresql-client.repo.j2 index 8fd5f8f..a87deea 100644 --- a/roles/repo_base/templates/postgresql-client.repo.j2 +++ b/roles/repo_base/templates/postgresql-client.repo.j2 @@ -1,5 +1,5 @@ [postgresql-client] -baseurl = https://download.postgresql.org/pub/repos/yum/{{ repo_pg_client_version }}/redhat/rhel-{{ ansible_distribution_version }}-$basearch +baseurl = https://download.postgresql.org/pub/repos/yum/{{ repo_pg_client_version }}/redhat/rhel-{{ ansible_distribution_major_version }}-$basearch gpgcheck = 1 gpgkey = https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-RHEL name = PostgreSQL Client