Update to 2025-06-16 16:00

This commit is contained in:
Daniel Berteaud 2025-06-16 16:00:13 +02:00
parent ffe9caded5
commit eb0714be6f
37 changed files with 1718 additions and 393 deletions

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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);
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);

View File

@ -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

View File

@ -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; });

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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;
};
});

View File

@ -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

View File

@ -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);

View File

@ -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)

View File

@ -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");

View File

@ -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

View File

@ -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,8 +185,7 @@ local function filter_hook(session)
return;
end
local request = get_request_from_conn(session.conn);
local ip = request and request.ip or session.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;

View File

@ -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
@ -90,6 +128,8 @@ function room_created(event)
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);

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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");

View File

@ -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)

View File

@ -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);

View File

@ -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

View File

@ -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;
};

View File

@ -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

View File

@ -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:

View File

@ -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