initial commit

This commit is contained in:
Antoine Ouvrard
2020-11-23 10:28:32 +01:00
commit d3277d6563
283 changed files with 78127 additions and 0 deletions
@@ -0,0 +1,58 @@
-- invite will perform the trigger for external call invites.
-- This trigger is left unimplemented. The implementation is expected
-- to be specific to the deployment.
local function invite(stanza, url, call_id)
module:log(
"warn",
"A module has been configured that triggers external events."
)
module:log("warn", "Implement this lib to trigger external events.")
end
-- cancel will perform the trigger for external call cancellation.
-- This trigger is left unimplemented. The implementation is expected
-- to be specific to the deployment.
local function cancel(stanza, url, reason, call_id)
module:log(
"warn",
"A module has been configured that triggers external events."
)
module:log("warn", "Implement this lib to trigger external events.")
end
-- missed will perform the trigger for external call missed notification.
-- This trigger is left unimplemented. The implementation is expected
-- to be specific to the deployment.
local function missed(stanza, call_id)
module:log(
"warn",
"A module has been configured that triggers external events."
)
module:log("warn", "Implement this lib to trigger external events.")
end
-- Event that speaker stats for a conference are available
-- this is a table where key is the jid and the value is a table:
--{
-- totalDominantSpeakerTime
-- nick
-- displayName
--}
-- This trigger is left unimplemented. The implementation is expected
-- to be specific to the deployment.
local function speaker_stats(room, speakerStats)
module:log(
"warn",
"A module has been configured that triggers external events."
)
module:log("warn", "Implement this lib to trigger external events.")
end
local ext_events = {
missed = missed,
invite = invite,
cancel = cancel,
speaker_stats = speaker_stats
}
return ext_events
@@ -0,0 +1,147 @@
-- Token authentication
-- Copyright (C) 2015 Atlassian
local formdecode = require "util.http".formdecode;
local generate_uuid = require "util.uuid".generate;
local new_sasl = require "util.sasl".new;
local sasl = require "util.sasl";
local token_util = module:require "token/util".new(module);
local sessions = prosody.full_sessions;
-- no token configuration
if token_util == nil then
return;
end
-- define auth provider
local provider = {};
local host = module.host;
-- Extract 'token' param from URL when session is created
function init_session(event)
local session, request = event.session, event.request;
local query = request.url.query;
if query ~= nil then
local params = formdecode(query);
-- The following fields are filled in the session, by extracting them
-- from the query and no validation is beeing done.
-- 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
session.auth_token = query and params.token or nil;
-- previd is used together with https://modules.prosody.im/mod_smacks.html
-- the param is used to find resumed session and re-use anonymous(random) user id
-- (see get_username_from_token)
session.previd = query and params.previd or nil;
-- The room name and optional prefix from the web query
session.jitsi_web_query_room = params.room;
session.jitsi_web_query_prefix = params.prefix or "";
-- Deprecated, you should use jitsi_web_query_room and jitsi_web_query_prefix
session.jitsi_bosh_query_room = session.jitsi_web_query_room;
session.jitsi_bosh_query_prefix = session.jitsi_web_query_prefix;
end
end
module:hook_global("bosh-session", init_session);
module:hook_global("websocket-session", init_session);
function provider.test_password(username, password)
return nil, "Password based auth not supported";
end
function provider.get_password(username)
return nil;
end
function provider.set_password(username, password)
return nil, "Set password not supported";
end
function provider.user_exists(username)
return nil;
end
function provider.create_user(username, password)
return nil;
end
function provider.delete_user(username)
return nil;
end
function provider.get_sasl_handler(session)
local function get_username_from_token(self, message)
-- retrieve custom public key from server and save it on the session
local pre_event_result = prosody.events.fire_event("pre-jitsi-authentication-fetch-key", session);
if pre_event_result ~= nil and pre_event_result.res == false then
log("warn",
"Error verifying token on pre authentication stage:%s, reason:%s", pre_event_result.error, pre_event_result.reason);
session.auth_token = nil;
return pre_event_result.res, pre_event_result.error, pre_event_result.reason;
end
local res, error, reason = token_util:process_and_verify_token(session);
if res == false then
log("warn",
"Error verifying token err:%s, reason:%s", error, reason);
session.auth_token = nil;
return res, error, reason;
end
local customUsername
= prosody.events.fire_event("pre-jitsi-authentication", session);
if (customUsername) then
self.username = customUsername;
elseif (session.previd ~= nil) then
for _, session1 in pairs(sessions) do
if (session1.resumption_token == session.previd) then
self.username = session1.username;
break;
end
end
else
self.username = message;
end
local post_event_result = prosody.events.fire_event("post-jitsi-authentication", session);
if post_event_result ~= nil and post_event_result.res == false then
log("warn",
"Error verifying token on post authentication stage :%s, reason:%s", post_event_result.error, post_event_result.reason);
session.auth_token = nil;
return post_event_result.res, post_event_result.error, post_event_result.reason;
end
return res;
end
return new_sasl(host, { anonymous = get_username_from_token });
end
module:provides("auth", provider);
local function anonymous(self, message)
local username = generate_uuid();
-- This calls the handler created in 'provider.get_sasl_handler(session)'
local result, err, msg = self.profile.anonymous(self, username, self.realm);
if result == true then
if (self.username == nil) then
self.username = username;
end
return "success";
else
return "failure", err, msg;
end
end
sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
@@ -0,0 +1,5 @@
local conference_duration_component
= module:get_option_string(
"conference_duration_component", "conferenceduration"..module.host);
module:add_identity("component", "conference_duration", conference_duration_component);
@@ -0,0 +1,66 @@
local st = require "util.stanza";
local socket = require "socket";
local json = require "util.json";
local ext_events = module:require "ext_events";
local it = require "util.iterators";
-- we use async to detect Prosody 0.10 and earlier
local have_async = pcall(require, "util.async");
if not have_async then
module:log("warn", "conference duration will not work with Prosody version 0.10 or less.");
return;
end
local muc_component_host = module:get_option_string("muc_component");
if muc_component_host == nil then
log("error", "No muc_component specified. No muc to operate on!");
return;
end
log("info", "Starting conference duration timer for %s", muc_component_host);
function occupant_joined(event)
local room = event.room;
local occupant = event.occupant;
local participant_count = it.count(room:each_occupant());
if participant_count > 1 then
if room.created_timestamp == nil then
room.created_timestamp = os.time() * 1000; -- Lua provides UTC time in seconds, so convert to milliseconds
end
local body_json = {};
body_json.type = 'conference_duration';
body_json.created_timestamp = room.created_timestamp;
local stanza = st.message({
from = module.host;
to = occupant.jid;
})
:tag("json-message", {xmlns='http://jitsi.org/jitmeet'})
:text(json.encode(body_json)):up();
room:route_stanza(stanza);
end
end
-- executed on every host added internally in prosody, including components
function process_host(host)
if host == muc_component_host then -- the conference muc component
module:log("info", "Hook to muc events on %s", host);
local muc_module = module:context(host)
muc_module:hook("muc-occupant-joined", occupant_joined, -1);
end
end
if prosody.hosts[muc_component_host] == nil then
module:log("info", "No muc component found, will listen for it: %s", muc_component_host);
-- when a host or component is added
prosody.events.add_handler("host-activated", process_host);
else
process_host(muc_component_host);
end
@@ -0,0 +1,51 @@
local st = require "util.stanza";
local is_feature_allowed = module:require "util".is_feature_allowed;
local token_util = module:require "token/util".new(module);
local accepted_rayo_iq_token_issuers = module:get_option_array("accepted_rayo_iq_token_issuers");
-- filters jibri iq in case of requested from jwt authenticated session that
-- has features in the user context, but without feature for recording
module:hook("pre-iq/full", function(event)
local stanza = event.stanza;
if stanza.name == "iq" then
local jibri = stanza:get_child('jibri', 'http://jitsi.org/protocol/jibri');
if jibri then
local session = event.origin;
local token = session.auth_token;
if jibri.attr.action == 'start' then
local errorReason;
if accepted_rayo_iq_token_issuers then
local iq_token = jibri.attr.token;
if iq_token then
local session = {};
session.auth_token = iq_token;
local verified, reason = token_util:process_and_verify_token(
session, accepted_rayo_iq_token_issuers);
if verified then
return nil; -- this will proceed with dispatching the stanza
end
errorReason = reason;
else
errorReason = 'No recording token provided';
end
module:log("warn", "not a valid token %s", tostring(errorReason));
session.send(st.error_reply(stanza, "auth", "forbidden"));
return true;
end
if token == nil
or not is_feature_allowed(session,
(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"));
return true;
end
end
end
end
end);
@@ -0,0 +1,180 @@
local new_throttle = require "util.throttle".create;
local st = require "util.stanza";
local token_util = module:require "token/util".new(module);
local room_jid_match_rewrite = module:require "util".room_jid_match_rewrite;
local is_feature_allowed = module:require "util".is_feature_allowed;
-- no token configuration but required
if token_util == nil then
log("error", "no token configuration but it is required");
return;
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;
local function load_config()
limit_outgoing_calls = module:get_option_number("max_number_outgoing_calls", -1);
end
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 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
-- filters rayo iq in case of requested from not jwt authenticated sessions
-- or if the session has features in user context and it doesn't mention
-- feature "outbound-call" to be enabled
module:hook("pre-iq/full", function(event)
local stanza = event.stanza;
if stanza.name == "iq" then
local dial = stanza:get_child('dial', 'urn:xmpp:rayo:1');
if dial then
local session = event.origin;
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;
end
end
if token == nil
or roomName == nil
or not token_util:verify_room(session, room_jid_match_rewrite(roomName))
or not is_feature_allowed(session,
(dial.attr.to == 'jitsi_meet_transcribe' and 'transcription'
or 'outbound-call'))
then
module:log("warn",
"Filtering stanza dial, stanza:%s", tostring(stanza));
session.send(st.error_reply(stanza, "auth", "forbidden"));
return true;
end
-- now lets check any limits if configured
if 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);
end
if not session.dial_out_throttle:poll(1) -- we first check the throttle so we can mark one incoming dial for the balance
or get_concurrent_outgoing_count(session.jitsi_meet_context_user["id"], session.jitsi_meet_context_group)
>= limit_outgoing_calls
then
module:log("warn",
"Filtering stanza dial, stanza:%s, outgoing calls limit reached", tostring(stanza));
session.send(st.error_reply(stanza, "cancel", "resource-constraint"));
return true;
end
end
-- now lets insert token information if any
if session and session.jitsi_meet_context_user 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 = session.jitsi_meet_context_user["id"] });
dial:up();
-- Add the initiator group information if it is present
if session.jitsi_meet_context_group then
dial:tag("header", {
xmlns = "urn:xmpp:rayo:1",
name = OUT_INITIATOR_GROUP_ATTR_NAME,
value = session.jitsi_meet_context_group });
dial:up();
end
end
end
end
end);
--- Finds and returns the number of concurrent outgoing calls for a user
-- @param context_user the user id extracted from the token
-- @param context_group the group id extracted from the token
-- @return returns the count of concurrent calls
function get_concurrent_outgoing_count(context_user, context_group)
local count = 0;
for _, host in pairs(hosts) do
local component = host;
if component then
local muc = component.modules.muc
local rooms = nil;
if muc and rawget(muc,"rooms") then
-- We're running 0.9.x or 0.10 (old MUC API)
return muc.rooms;
elseif muc and rawget(muc,"live_rooms") then
-- We're running >=0.11 (new MUC API)
rooms = muc.live_rooms();
elseif muc and rawget(muc,"each_room") then
-- We're running trunk<0.11 (each_room is later [DEPRECATED])
rooms = muc.each_room(true);
end
-- now lets iterate over rooms and occupants and search for
-- call initiated by the user
if rooms then
for room in rooms do
for _, occupant in room:each_occupant() do
for _, presence in occupant:each_session() do
local initiator = presence:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
local found_user = false;
local found_group = false;
if initiator then
initiator:maptags(function (tag)
if tag.name == "header"
and tag.attr.name == OUT_INITIATOR_USER_ATTR_NAME then
found_user = tag.attr.value == context_user;
elseif tag.name == "header"
and tag.attr.name == OUT_INITIATOR_GROUP_ATTR_NAME then
found_group = tag.attr.value == context_group;
end
return tag;
end );
-- if found a jigasi participant initiated by the concurrent
-- participant, count it
if found_user
and (context_group == nil or found_group) then
count = count + 1;
end
end
end
end
end
end
end
end
return count;
end
module:hook_global('config-reloaded', load_config);
@@ -0,0 +1,5 @@
local jibri_queue_component
= module:get_option_string(
"jibri_queue_component", "jibriqueue"..module.host);
module:add_identity("component", "jibri-queue", jibri_queue_component);
@@ -0,0 +1,559 @@
local st = require "util.stanza";
local jid = require "util.jid";
local http = require "net.http";
local json = require "cjson";
local inspect = require('inspect');
local socket = require "socket";
local uuid_gen = require "util.uuid".generate;
local jwt = require "luajwtjitsi";
local it = require "util.iterators";
local neturl = require "net.url";
local parse = neturl.parseQuery;
local get_room_from_jid = module:require "util".get_room_from_jid;
local room_jid_match_rewrite = module:require "util".room_jid_match_rewrite;
local is_healthcheck_room = module:require "util".is_healthcheck_room;
local room_jid_split_subdomain = module:require "util".room_jid_split_subdomain;
local internal_room_jid_match_rewrite = module:require "util".internal_room_jid_match_rewrite;
local async_handler_wrapper = module:require "util".async_handler_wrapper;
-- this basically strips the domain from the conference.domain address
local parentHostName = string.gmatch(tostring(module.host), "%w+.(%w.+)")();
if parentHostName == nil then
log("error", "Failed to start - unable to get parent hostname");
return;
end
local parentCtx = module:context(parentHostName);
if parentCtx == nil then
log("error",
"Failed to start - unable to get parent context for host: %s",
tostring(parentHostName));
return;
end
local token_util = module:require "token/util".new(parentCtx);
local ASAPKeyServer;
local ASAPKeyPath;
local ASAPKeyId;
local ASAPIssuer;
local ASAPAudience;
local ASAPAcceptedIssuers;
local ASAPAcceptedAudiences;
local ASAPTTL;
local ASAPTTL_THRESHOLD;
local ASAPKey;
local JibriRegion;
local disableTokenVerification;
local muc_component_host;
local external_api_url;
local jwtKeyCacheSize;
local jwtKeyCache;
local function load_config()
ASAPKeyServer = module:get_option_string("asap_key_server");
if ASAPKeyServer then
module:log("debug", "ASAP Public Key URL %s", ASAPKeyServer);
token_util:set_asap_key_server(ASAPKeyServer);
end
ASAPKeyPath
= module:get_option_string("asap_key_path", '/etc/prosody/certs/asap.key');
ASAPKeyId
= module:get_option_string("asap_key_id", 'jitsi');
ASAPIssuer
= module:get_option_string("asap_issuer", 'jitsi');
ASAPAudience
= module:get_option_string("asap_audience", 'jibri-queue');
ASAPAcceptedIssuers
= module:get_option_array('asap_accepted_issuers',{'jibri-queue'});
module:log("debug", "ASAP Accepted Issuers %s", ASAPAcceptedIssuers);
token_util:set_asap_accepted_issuers(ASAPAcceptedIssuers);
ASAPAcceptedAudiences
= module:get_option_array('asap_accepted_audiences',{'*'});
module:log("debug", "ASAP Accepted Audiences %s", ASAPAcceptedAudiences);
token_util:set_asap_accepted_audiences(ASAPAcceptedAudiences);
-- do not require room to be set on tokens for jibri queue
token_util:set_asap_require_room_claim(false);
ASAPTTL
= module:get_option_number("asap_ttl", 3600);
ASAPTTL_THRESHOLD
= module:get_option_number("asap_ttl_threshold", 600);
queueServiceURL
= module:get_option_string("jibri_queue_url");
JibriRegion
= module:get_option_string("jibri_region", 'default');
-- option to enable/disable token verifications
disableTokenVerification
= module:get_option_boolean("disable_jibri_queue_token_verification", false);
muc_component_host
= module:get_option_string("muc_component");
external_api_url = module:get_option_string("external_api_url",tostring(parentHostName));
module:log("debug", "External advertised API URL", external_api_url);
-- TODO: Figure out a less arbitrary default cache size.
jwtKeyCacheSize
= module:get_option_number("jwt_pubkey_cache_size", 128);
jwtKeyCache = require"util.cache".new(jwtKeyCacheSize);
if queueServiceURL == nil then
log("error", "No jibri_queue_url specified. No service to contact!");
return;
end
if muc_component_host == nil then
log("error", "No muc_component specified. No muc to operate on for jibri queue!");
return;
end
-- Read ASAP key once on module startup
local f = io.open(ASAPKeyPath, "r");
if f then
ASAPKey = f:read("*all");
f:close();
if not ASAPKey then
module:log("warn", "No ASAP Key read from %s, disabling jibri queue component plugin", ASAPKeyPath);
return
end
else
module:log("warn", "Error reading ASAP Key %s, disabling jibri queue component plugin", ASAPKeyPath);
return
end
return true;
end
local function reload_config()
module:log("info", "Reloading configuration for jibri queue component");
local config_success = load_config();
-- clear ASAP public key cache on config reload
token_util:clear_asap_cache();
if not config_success then
log("error", "Unsuccessful reconfiguration, jibri queue component may misbehave");
end
end
local config_success = load_config();
if not config_success then
log("error", "Unsuccessful configuration step, jibri queue component disabled")
return;
end
local http_headers = {
["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")",
["Content-Type"] = "application/json"
};
-- we use async to detect Prosody 0.10 and earlier
local have_async = pcall(require, "util.async");
if not have_async then
module:log("warn", "conference duration will not work with Prosody version 0.10 or less.");
return;
end
log("info", "Starting jibri queue handling for %s", muc_component_host);
local function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
local function generateToken(audience)
audience = audience or ASAPAudience
local t = os.time()
local err
local exp_key = 'asap_exp.'..audience
local token_key = 'asap_token.'..audience
local exp = jwtKeyCache:get(exp_key)
local token = jwtKeyCache:get(token_key)
--if we find a token and it isn't too far from expiry, then use it
if token ~= nil and exp ~= nil then
exp = tonumber(exp)
if (exp - t) > ASAPTTL_THRESHOLD then
return token
end
end
--expiry is the current time plus TTL
exp = t + ASAPTTL
local payload = {
iss = ASAPIssuer,
aud = audience,
nbf = t,
exp = exp,
}
-- encode
local alg = "RS256"
token, err = jwt.encode(payload, ASAPKey, alg, {kid = ASAPKeyId})
if not err then
token = 'Bearer '..token
jwtKeyCache:set(exp_key,exp)
jwtKeyCache:set(token_key,token)
return token
else
return ''
end
end
local function sendIq(participant,action,requestId,time,position,token)
local iqId = uuid_gen();
local from = module:get_host();
local outStanza = st.iq({type = 'set', from = from, to = participant, id = iqId}):tag("jibri-queue",
{ xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId, action = action });
if token then
outStanza:tag("token"):text(token):up()
end
if time then
outStanza:tag("time"):text(tostring(time)):up()
end
if position then
outStanza:tag("position"):text(tostring(position)):up()
end
module:send(outStanza);
end
local function cb(content_, code_, response_, request_)
if code_ == 200 or code_ == 204 then
module:log("debug", "URL Callback: Code %s, Content %s, Request (host %s, path %s, body %s), Response: %s",
code_, content_, request_.host, request_.path, inspect(request_.body), inspect(response_));
else
module:log("warn", "URL Callback non successful: Code %s, Content %s, Request (%s), Response: %s",
code_, content_, inspect(request_), inspect(response_));
end
end
local function sendEvent(type,room_address,participant,requestId,replyIq,replyError)
local event_ts = round(socket.gettime()*1000);
local node, host, resource, target_subdomain = room_jid_split_subdomain(room_address);
local room_param = '';
if target_subdomain then
room_param = target_subdomain..'/'..node;
else
room_param = node;
end
local out_event = {
["conference"] = room_address,
["roomParam"] = room_param,
["eventType"] = type,
["participant"] = participant,
["externalApiUrl"] = external_api_url.."/jibriqueue/update",
["requestId"] = requestId,
["region"] = JibriRegion,
}
module:log("debug","Sending event %s",inspect(out_event));
local headers = http_headers or {}
headers['Authorization'] = generateToken()
module:log("debug","Sending headers %s",inspect(headers));
local requestURL = queueServiceURL.."/job/recording"
if type=="LeaveQueue" then
requestURL = requestURL .."/cancel"
end
local request = http.request(requestURL, {
headers = headers,
method = "POST",
body = json.encode(out_event)
}, function (content_, code_, response_, request_)
if code_ == 200 or code_ == 204 then
module:log("debug", "URL Callback: Code %s, Content %s, Request (host %s, path %s, body %s), Response: %s",
code_, content_, request_.host, request_.path, inspect(request_.body), inspect(response_));
if (replyIq) then
module:log("debug", "sending reply IQ %s",inspect(replyIq));
module:send(replyIq);
end
else
module:log("warn", "URL Callback non successful: Code %s, Content %s, Request (%s), Response: %s",
code_, content_, inspect(request_), inspect(response_));
if (replyError) then
module:log("warn", "sending reply error IQ %s",inspect(replyError));
module:send(replyError);
end
end
end);
end
function clearRoomQueueByOccupant(room, occupant)
room.jibriQueue[occupant.jid] = nil;
end
function addRoomQueueByOccupant(room, occupant, requestId)
room.jibriQueue[occupant.jid] = requestId;
end
-- receives iq from client currently connected to the room
function on_iq(event)
local requestId;
-- Check the type of the incoming stanza to avoid loops:
if event.stanza.attr.type == "error" then
return; -- We do not want to reply to these, so leave.
end
if event.stanza.attr.to == module:get_host() then
if event.stanza.attr.type == "set" then
local reply = st.reply(event.stanza);
local replyError = st.error_reply(event.stanza,'cancel','internal-server-error',"Queue Server Error");
local jibriQueue
= event.stanza:get_child('jibri-queue', 'http://jitsi.org/protocol/jibri-queue');
if jibriQueue then
module:log("debug", "Received Jibri Queue Request: %s ",inspect(jibriQueue));
local roomAddress = jibriQueue.attr.room;
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
if not room then
module:log("warn", "No room found %s", roomAddress);
return false;
end
local from = event.stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(from);
if not occupant then
module:log("warn", "No occupant %s found for %s", from, roomAddress);
return false;
end
local action = jibriQueue.attr.action;
if action == 'join' then
-- join action, so send event out
requestId = uuid_gen();
module:log("debug","Received join queue request for jid %s occupant %s requestId %s",roomAddress,occupant.jid,requestId);
-- now handle new jibri queue message
addRoomQueueByOccupant(room, occupant, requestId);
reply:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
replyError:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
module:log("debug","Sending JoinQueue event for jid %s occupant %s reply %s",roomAddress,occupant.jid,inspect(reply));
sendEvent('JoinQueue',roomAddress,occupant.jid,requestId,reply,replyError);
end
if action == 'leave' then
requestId = jibriQueue.attr.requestId;
module:log("debug","Received leave queue request for jid %s occupant %s requestId %s",roomAddress,occupant.jid,requestId);
-- TODO: check that requestId is the same as cached value
clearRoomQueueByOccupant(room, occupant);
reply:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
replyError:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
module:log("debug","Sending LeaveQueue event for jid %s occupant %s reply %s",roomAddress,occupant.jid,inspect(reply));
sendEvent('LeaveQueue',roomAddress,occupant.jid,requestId,reply,replyError);
end
else
module:log("warn","Jibri Queue Stanza missing child %s",inspect(event.stanza))
end
end
end
return true
end
-- create recorder queue cache for the room
function room_created(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
room.jibriQueue = {};
end
-- Conference ended, clear all queue cache jids
function room_destroyed(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
for jid, x in pairs(room.jibriQueue) do
if x then
sendEvent('LeaveQueue',internal_room_jid_match_rewrite(room.jid),jid,x);
end
end
end
-- Occupant left remove it from the queue if it joined the queue
function occupant_leaving(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
local occupant = event.occupant;
local requestId = room.jibriQueue[occupant.jid];
-- check if user has cached queue request
if requestId then
-- remove occupant from queue cache, signal backend
room.jibriQueue[occupant.jid] = nil;
sendEvent('LeaveQueue',internal_room_jid_match_rewrite(room.jid),occupant.jid,requestId);
end
end
module:hook("iq/host", on_iq);
-- executed on every host added internally in prosody, including components
function process_host(host)
if host == muc_component_host then -- the conference muc component
module:log("debug","Hook to muc events on %s", host);
local muc_module = module:context(host);
muc_module:hook("muc-room-created", room_created, -1);
-- muc_module:hook("muc-occupant-joined", occupant_joined, -1);
muc_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
muc_module:hook("muc-room-destroyed", room_destroyed, -1);
end
end
if prosody.hosts[muc_component_host] == nil then
module:log("debug","No muc component found, will listen for it: %s", muc_component_host)
-- when a host or component is added
prosody.events.add_handler("host-activated", process_host);
else
process_host(muc_component_host);
end
module:log("info", "Loading jibri_queue_component");
--- Verifies room name, domain name with the values in the token
-- @param token the token we received
-- @param room_name the room name
-- @param group name of the group (optional)
-- @param session the session to use for storing token specific fields
-- @return true if values are ok or false otherwise
function verify_token(token, room_jid, session)
if disableTokenVerification then
return true;
end
-- if not disableTokenVerification and we do not have token
-- stop here, cause the main virtual host can have guest access enabled
-- (allowEmptyToken = true) and we will allow access to rooms info without
-- a token
if token == nil then
log("warn", "no token provided");
return false;
end
session.auth_token = token;
local verified, reason, message = token_util:process_and_verify_token(session);
if not verified then
log("warn", "not a valid token %s: %s", tostring(reason), tostring(message));
log("debug", "invalid token %s", token);
return false;
end
return true;
end
--- Handles request for updating jibri queue status
-- @param event the http event, holds the request query
-- @return GET response, containing a json with response details
function handle_update_jibri_queue(event)
local body = json.decode(event.request.body);
module:log("debug","Update Jibri Queue Event Received: %s",inspect(body));
local token = event.request.headers["authorization"];
if not token then
token = ''
else
local prefixStart, prefixEnd = token:find("Bearer ");
if prefixStart ~= 1 then
module:log("error", "REST event: Invalid authorization header format. The header must start with the string 'Bearer '");
return { status_code = 403; };
end
token = token:sub(prefixEnd + 1);
end
local user_jid = body["participant"];
local roomAddress = body["conference"];
local userJWT = body["token"];
local action = body["action"];
local time = body["time"];
local position = body["position"];
local requestId = body["requestId"];
if not action then
if userJWT then
action = 'token';
else
action = 'info';
end
end
local room_jid = room_jid_match_rewrite(roomAddress);
if not verify_token(token, room_jid, {}) then
log("error", "REST event: Invalid token for room %s to route action %s for requestId %s", roomAddress, action, requestId);
return { status_code = 403; };
end
local room = get_room_from_jid(room_jid);
if (not room) then
log("error", "REST event: no room found %s to route action %s for requestId %s", roomAddress, action, requestId);
return { status_code = 404; };
end
local occupant = room:get_occupant_by_real_jid(user_jid);
if not occupant then
log("warn", "REST event: No occupant %s found for %s to route action %s for requestId %s", user_jid, roomAddress, action, requestId);
return { status_code = 404; };
end
if not room.jibriQueue[occupant.jid] then
log("warn", "REST event: No queue request found for occupant %s in conference %s to route action %s for requestId %s",occupant.jid,room.jid, action, requestId)
return { status_code = 404; };
end
if not requestId then
requestId = room.jibriQueue[occupant.jid];
end
if action == 'token' and userJWT then
log("debug", "REST event: Token received for occupant %s in conference %s requestId %s, clearing room queue");
clearRoomQueueByOccupant(room, occupant);
end
log("debug", "REST event: Sending update for occupant %s in conference %s to route action %s for requestId %s",occupant.jid,room.jid, action, requestId);
sendIq(occupant.jid,action,requestId,time,position,userJWT);
return { status_code = 200; };
end
module:depends("http");
module:provides("http", {
default_path = "/";
name = "jibriqueue";
route = {
["POST /jibriqueue/update"] = function (event) return async_handler_wrapper(event,handle_update_jibri_queue) end;
};
});
module:hook_global('config-reloaded', reload_config);
@@ -0,0 +1,82 @@
local jid = require "util.jid";
local um_is_admin = require "core.usermanager".is_admin;
local is_healthcheck_room = module:require "util".is_healthcheck_room;
local moderated_subdomains;
local moderated_rooms;
local function load_config()
moderated_subdomains = module:get_option_set("allowners_moderated_subdomains", {})
moderated_rooms = module:get_option_set("allowners_moderated_rooms", {})
end
load_config();
local function is_admin(jid)
return um_is_admin(jid, module.host);
end
-- Checks whether the jid is moderated, the room name is in moderated_rooms
-- or if the subdomain is in the moderated_subdomains
-- @return returns on of the:
-- -> false
-- -> true, room_name, subdomain
-- -> true, room_name, nil (if no subdomain is used for the room)
local function is_moderated(room_jid)
local room_node = jid.node(room_jid);
-- parses bare room address, for multidomain expected format is:
-- [subdomain]roomName@conference.domain
local target_subdomain, target_room_name = room_node:match("^%[([^%]]+)%](.+)$");
if target_subdomain then
if moderated_subdomains:contains(target_subdomain) then
return true, target_room_name, target_subdomain;
end
elseif moderated_rooms:contains(room_node) then
return true, room_node, nil;
end
return false;
end
module:hook("muc-occupant-joined", function (event)
local room, occupant = event.room, event.occupant;
if is_healthcheck_room(room.jid) or is_admin(occupant.jid) then
return;
end
local moderated, room_name, subdomain = is_moderated(room.jid);
if moderated then
local session = event.origin;
local token = session.auth_token;
if not token then
module:log('debug', 'skip allowners for non-auth user subdomain:%s room_name:%s', subdomain, room_name);
return;
end
if not (room_name == session.jitsi_meet_room) then
module:log('debug', 'skip allowners for auth user and non matching room name: %s, jwt room name: %s', room_name, session.jitsi_meet_room);
return;
end
if not (subdomain == session.jitsi_meet_context_group) then
module:log('debug', 'skip allowners for auth user and non matching room subdomain: %s, jwt subdomain: %s', subdomain, session.jitsi_meet_context_group);
return;
end
end
room:set_affiliation(true, occupant.bare_jid, "owner");
end, 2);
module:hook("muc-occupant-left", function (event)
local room, occupant = event.room, event.occupant;
if is_healthcheck_room(room.jid) then
return;
end
room:set_affiliation(true, occupant.bare_jid, nil);
end, 2);
module:hook_global('config-reloaded', load_config);
+118
View File
@@ -0,0 +1,118 @@
local ext_events = module:require "ext_events"
local jid = require "util.jid"
-- Options and configuration
local poltergeist_component = module:get_option_string(
"poltergeist_component",
module.host
);
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, unable to send call events."
);
return
end
-- Status strings that trigger call events.
local calling_status = "calling"
local busy_status = "busy"
local rejected_status = "rejected"
local connected_status = "connected"
local expired_status = "expired"
-- url_from_room_jid will determine the url for a conference
-- provided a room jid. It is required that muc domain mapping
-- is enabled and configured. There are two url formats that are supported.
-- The following urls are examples of the supported formats.
-- https://meet.jit.si/jitsi/ProductiveMeeting
-- https://meet.jit.si/MoreProductiveMeeting
-- The urls are derived from portions of the room jid.
local function url_from_room_jid(room_jid)
local node, _, _ = jid.split(room_jid)
if not node then return nil end
local target_subdomain, target_node = node:match("^%[([^%]]+)%](.+)$")
if not(target_node or target_subdomain) then
return "https://"..muc_domain_base.."/"..node
else
return "https://"..muc_domain_base.."/"..target_subdomain.."/"..target_node
end
end
-- Listening for all muc presences stanza events. If a presence stanza is from
-- a poltergeist then it will be further processed to determine if a call
-- event should be triggered. Call events are triggered by status strings
-- the status strings supported are:
-- -------------------------
-- Status | Event Type
-- _________________________
-- "calling" | INVITE
-- "busy" | CANCEL
-- "rejected" | CANCEL
-- "connected" | CANCEL
module:hook(
"muc-broadcast-presence",
function (event)
-- Detect if the presence is for a poltergeist or not.
if not (jid.bare(event.occupant.jid) == poltergeist_component) then
return
end
-- A presence stanza is needed in order to trigger any calls.
if not event.stanza then
return
end
local call_id = event.stanza:get_child_text("call_id")
if not call_id then
module:log("info", "A call id was not provided in the status.")
return
end
local invite = function()
local url = assert(url_from_room_jid(event.stanza.attr.from))
ext_events.invite(event.stanza, url, call_id)
end
local cancel = function()
local url = assert(url_from_room_jid(event.stanza.attr.from))
local status = event.stanza:get_child_text("status")
ext_events.cancel(event.stanza, url, string.lower(status), call_id)
end
-- If for any reason call_cancel is set to true then a cancel
-- is sent regardless of the rest of the presence info.
local should_cancel = event.stanza:get_child_text("call_cancel")
if should_cancel == "true" then
cancel()
return
end
local missed = function()
cancel()
ext_events.missed(event.stanza, call_id)
end
-- All other call flow actions will require a status.
if event.stanza:get_child_text("status") == nil then
return
end
local switch = function(status)
case = {
[calling_status] = function() invite() end,
[busy_status] = function() cancel() end,
[rejected_status] = function() missed() end,
[expired_status] = function() missed() end,
[connected_status] = function() cancel() end
}
if case[status] then case[status]() end
end
switch(event.stanza:get_child_text("status"))
end,
-101
);
@@ -0,0 +1,161 @@
-- Maps MUC JIDs like room1@muc.foo.example.com to JIDs like [foo]room1@muc.example.com
-- Must be loaded on the client host in Prosody
-- It is recommended to set muc_mapper_domain_base to the main domain being served (example.com)
local jid = require "util.jid";
local filters = require "util.filters";
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_mapper_domain_base' option set, disabling muc_mapper plugin inactive");
return
end
-- The "real" MUC domain that we are proxying to
local muc_domain = module:get_option_string("muc_mapper_domain", muc_domain_prefix.."."..muc_domain_base);
local escaped_muc_domain_base = muc_domain_base:gsub("%p", "%%%1");
local escaped_muc_domain_prefix = muc_domain_prefix:gsub("%p", "%%%1");
-- The pattern used to extract the target subdomain (e.g. extract 'foo' from 'foo.muc.example.com')
local target_subdomain_pattern = "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
-- table to store all incoming iqs without roomname in it, like discoinfo to the muc compoent
local roomless_iqs = {};
if not muc_domain then
module:log("warn", "No 'muc_mapper_domain' option set, disabling muc_mapper plugin inactive");
return
end
-- Utility function to check and convert a room JID from virtual room1@muc.foo.example.com to real [foo]room1@muc.example.com
local function match_rewrite_to_jid(room_jid, stanza)
local node, host, resource = jid.split(room_jid);
local target_subdomain = host and host:match(target_subdomain_pattern);
if not target_subdomain then
module:log("debug", "No need to rewrite out 'to' %s", room_jid);
return room_jid;
end
-- Ok, rewrite room_jid address to new format
local new_node, new_host, new_resource;
if node then
new_node, new_host, new_resource = "["..target_subdomain.."]"..node, muc_domain, resource;
else
module:log("debug", "No room name provided so rewriting only host 'to' %s", room_jid);
new_host, new_resource = muc_domain, resource;
if (stanza.attr and stanza.attr.id) then
roomless_iqs[stanza.attr.id] = stanza.attr.to;
end
end
room_jid = jid.join(new_node, new_host, new_resource);
module:log("debug", "Rewrote to %s", room_jid);
return room_jid
end
-- Utility function to check and convert a room JID from real [foo]room1@muc.example.com to virtual room1@muc.foo.example.com
local function match_rewrite_from_jid(room_jid, stanza)
local node, host, resource = jid.split(room_jid);
if host ~= muc_domain or not node then
module:log("debug", "No need to rewrite %s (not from the MUC host) %s, %s", room_jid, stanza.attr.id, roomless_iqs[stanza.attr.id]);
if (stanza.attr and stanza.attr.id and roomless_iqs[stanza.attr.id]) then
local result = roomless_iqs[stanza.attr.id];
roomless_iqs[stanza.attr.id] = nil;
return result;
end
return room_jid;
end
local target_subdomain, target_node = node:match("^%[([^%]]+)%](.+)$");
if not (target_node and target_subdomain) then
module:log("debug", "Not rewriting... unexpected node format: %s", node);
return room_jid;
end
-- Ok, rewrite room_jid address to pretty format
local new_node, new_host, new_resource = target_node, muc_domain_prefix..".".. target_subdomain.."."..muc_domain_base, resource;
room_jid = jid.join(new_node, new_host, new_resource);
module:log("debug", "Rewrote to %s", room_jid);
return room_jid
end
-- We must filter stanzas in order to hook in to all incoming and outgoing messaging which skips the stanza routers
function filter_stanza(stanza)
if stanza.name == "message" or stanza.name == "iq" or stanza.name == "presence" then
module:log("debug", "Filtering stanza type %s to %s from %s",stanza.name,stanza.attr.to,stanza.attr.from);
if stanza.name == "iq" then
local conf = stanza:get_child('conference')
if conf then
module:log("debug", "Filtering stanza conference %s to %s from %s",conf.attr.room,stanza.attr.to,stanza.attr.from);
conf.attr.room = match_rewrite_to_jid(conf.attr.room, stanza)
end
end
if stanza.attr.to then
stanza.attr.to = match_rewrite_to_jid(stanza.attr.to, stanza)
end
if stanza.attr.from then
stanza.attr.from = match_rewrite_from_jid(stanza.attr.from, stanza)
end
end
return stanza;
end
function filter_session(session)
module:log("warn", "Session filters applied");
-- filters.add_filter(session, "stanzas/in", filter_stanza_in);
filters.add_filter(session, "stanzas/out", filter_stanza);
end
function module.load()
if module.reloading then
module:log("debug", "Reloading MUC mapper!");
else
module:log("debug", "First load of MUC mapper!");
end
filters.add_filter_hook(filter_session);
end
function module.unload()
filters.remove_filter_hook(filter_session);
end
local function outgoing_stanza_rewriter(event)
local stanza = event.stanza;
if stanza.attr.to then
stanza.attr.to = match_rewrite_to_jid(stanza.attr.to, stanza)
end
end
local function incoming_stanza_rewriter(event)
local stanza = event.stanza;
if stanza.attr.from then
stanza.attr.from = match_rewrite_from_jid(stanza.attr.from, stanza)
end
end
-- The stanza rewriters helper functions are attached for all stanza router hooks
local function hook_all_stanzas(handler, host_module, event_prefix)
for _, stanza_type in ipairs({ "message", "presence", "iq" }) do
for _, jid_type in ipairs({ "host", "bare", "full" }) do
host_module:hook((event_prefix or "")..stanza_type.."/"..jid_type, handler);
end
end
end
function add_host(host)
module:log("info", "Loading mod_muc_domain_mapper for host %s!", host);
local host_module = module:context(host);
hook_all_stanzas(incoming_stanza_rewriter, host_module);
hook_all_stanzas(outgoing_stanza_rewriter, host_module, "pre-");
end
prosody.events.add_handler("host-activated", add_host);
for host in pairs(prosody.hosts) do
add_host(host);
end
@@ -0,0 +1,425 @@
-- This module added under the main virtual host domain
-- It needs a lobby muc component
--
-- VirtualHost "jitmeet.example.com"
-- modules_enabled = {
-- "muc_lobby_rooms"
-- }
-- lobby_muc = "lobby.jitmeet.example.com"
-- main_muc = "conference.jitmeet.example.com"
--
-- Component "lobby.jitmeet.example.com" "muc"
-- storage = "memory"
-- muc_room_cache_size = 1000
-- restrict_room_creation = true
-- muc_room_locking = false
-- muc_room_default_public_jids = true
--
-- we use async to detect Prosody 0.10 and earlier
local have_async = pcall(require, 'util.async');
if not have_async then
module:log('warn', 'Lobby rooms will not work with Prosody version 0.10 or less.');
return;
end
local formdecode = require "util.http".formdecode;
local jid_split = require 'util.jid'.split;
local jid_bare = require 'util.jid'.bare;
local json = require 'util.json';
local filters = require 'util.filters';
local st = require 'util.stanza';
local MUC_NS = 'http://jabber.org/protocol/muc';
local DISCO_INFO_NS = 'http://jabber.org/protocol/disco#info';
local DISPLAY_NAME_REQUIRED_FEATURE = 'http://jitsi.org/protocol/lobbyrooms#displayname_required';
local LOBBY_IDENTITY_TYPE = 'lobbyrooms';
local NOTIFY_JSON_MESSAGE_TYPE = 'lobby-notify';
local NOTIFY_LOBBY_ENABLED = 'LOBBY-ENABLED';
local NOTIFY_LOBBY_ACCESS_GRANTED = 'LOBBY-ACCESS-GRANTED';
local NOTIFY_LOBBY_ACCESS_DENIED = 'LOBBY-ACCESS-DENIED';
local is_healthcheck_room = module:require 'util'.is_healthcheck_room;
local main_muc_component_config = module:get_option_string('main_muc');
if main_muc_component_config == nil then
module:log('error', 'lobby not enabled missing main_muc config');
return ;
end
local lobby_muc_component_config = module:get_option_string('lobby_muc');
if lobby_muc_component_config == nil then
module:log('error', 'lobby not enabled missing lobby_muc config');
return ;
end
local whitelist;
local check_display_name_required;
local function load_config()
whitelist = module:get_option_set('muc_lobby_whitelist', {});
check_display_name_required
= module:get_option_boolean('muc_lobby_check_display_name_required', true);
end
load_config();
local lobby_muc_service;
local main_muc_service;
-- Checks whether there is status in the <x node
function check_status(muc_x, status)
if not muc_x then
return false;
end
for statusNode in muc_x:childtags('status') do
if statusNode.attr.code == status then
return true;
end
end
return false;
end
function broadcast_json_msg(room, from, json_msg)
json_msg.type = NOTIFY_JSON_MESSAGE_TYPE;
local occupant = room:get_occupant_by_real_jid(from);
if occupant then
room:broadcast_message(
st.message({ type = 'groupchat', from = occupant.nick })
:tag('json-message', {xmlns='http://jitsi.org/jitmeet'})
:text(json.encode(json_msg)):up());
end
end
-- Sends a json message notifying for lobby enabled/disable
-- the message from is the actor that did the operation
function notify_lobby_enabled(room, actor, value)
broadcast_json_msg(room, actor, {
event = NOTIFY_LOBBY_ENABLED,
value = value
});
end
-- Sends a json message notifying that the jid was granted/denied access in lobby
-- the message from is the actor that did the operation
function notify_lobby_access(room, actor, jid, display_name, granted)
local notify_json = {
value = jid,
name = display_name
};
if granted then
notify_json.event = NOTIFY_LOBBY_ACCESS_GRANTED;
else
notify_json.event = NOTIFY_LOBBY_ACCESS_DENIED;
end
broadcast_json_msg(room, actor, notify_json);
end
function filter_stanza(stanza)
if not stanza.attr or not stanza.attr.from or not main_muc_service then
return stanza;
end
-- Allow self-presence (code=110)
local node, from_domain = jid_split(stanza.attr.from);
if from_domain == lobby_muc_component_config then
if stanza.name == 'presence' then
local muc_x = stanza:get_child('x', MUC_NS..'#user');
if check_status(muc_x, '110') then
return stanza;
end
-- check is an owner, only owners can receive the presence
local room = main_muc_service.get_room_from_jid(jid_bare(node .. '@' .. main_muc_component_config));
if not room or room.get_affiliation(room, stanza.attr.to) == 'owner' then
return stanza;
end
return nil;
elseif stanza.name == 'iq' and stanza:get_child('query', DISCO_INFO_NS) then
-- allow disco info from the lobby component
return stanza;
end
return nil;
else
return stanza;
end
end
function filter_session(session)
if session.host and session.host == module.host then
-- domain mapper is filtering on default priority 0, and we need it after that
filters.add_filter(session, 'stanzas/out', filter_stanza, -1);
end
end
function attach_lobby_room(room)
local node = jid_split(room.jid);
local lobby_room_jid = node .. '@' .. lobby_muc_component_config;
if not lobby_muc_service.get_room_from_jid(lobby_room_jid) then
local new_room = lobby_muc_service.create_room(lobby_room_jid);
-- set persistent the lobby room to avoid it to be destroyed
-- there are cases like when selecting new moderator after the current one leaves
-- which can leave the room with no occupants and it will be destroyed and we want to
-- avoid lobby destroy while it is enabled
new_room:set_persistent(true);
module:log("debug","Lobby room jid = %s created",lobby_room_jid);
new_room.main_room = room;
room._data.lobbyroom = new_room;
room:save(true);
return true
end
return false
end
-- destroys lobby room for the supplied main room
function destroy_lobby_room(room, newjid, message)
if not message then
message = 'Lobby room closed.';
end
if room and room._data.lobbyroom then
room._data.lobbyroom:set_persistent(false);
room._data.lobbyroom:destroy(newjid, message);
room._data.lobbyroom = nil;
end
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)
if host == name then
callback(module:context(host), host);
end
end
if prosody.hosts[name] == nil then
module:log('debug', 'No host/component found, will wait for it: %s', name)
-- when a host or component is added
prosody.events.add_handler('host-activated', process_host);
else
process_host(name);
end
end
-- operates on already loaded lobby muc module
function process_lobby_muc_loaded(lobby_muc, host_module)
module:log('debug', 'Lobby muc loaded');
lobby_muc_service = lobby_muc;
-- enable filtering presences in the lobby muc rooms
filters.add_filter_hook(filter_session);
-- Advertise lobbyrooms support on main domain so client can pick up the address and use it
module:add_identity('component', LOBBY_IDENTITY_TYPE, lobby_muc_component_config);
-- Tag the disco#info response with a feature that display name is required
-- when the conference name from the web request has a lobby enabled.
host_module:hook('host-disco-info-node', function (event)
local session, reply, node = event.origin, event.reply, event.node;
if node == LOBBY_IDENTITY_TYPE
and session.jitsi_web_query_room
and main_muc_service
and check_display_name_required then
local room = main_muc_service.get_room_from_jid(
jid_bare(session.jitsi_web_query_room .. '@' .. main_muc_component_config));
if room and room._data.lobbyroom then
reply:tag('feature', { var = DISPLAY_NAME_REQUIRED_FEATURE }):up();
end
end
event.exists = true;
end);
local room_mt = lobby_muc_service.room_mt;
-- we base affiliations (roles) in lobby muc component to be based on the roles in the main muc
room_mt.get_affiliation = function(room, jid)
if not room.main_room then
module:log('error', 'No main room(%s) for %s!', room.jid, jid);
return 'none';
end
-- moderators in main room are moderators here
local role = room.main_room.get_affiliation(room.main_room, jid);
if role then
return role;
end
return 'none';
end
-- listens for kicks in lobby room, 307 is the status for kick according to xep-0045
host_module:hook('muc-broadcast-presence', function (event)
local actor, occupant, room, x = event.actor, event.occupant, event.room, event.x;
if check_status(x, '307') then
local display_name = occupant:get_presence():get_child_text(
'nick', 'http://jabber.org/protocol/nick');
-- we need to notify in the main room
notify_lobby_access(room.main_room, actor, occupant.nick, display_name, false);
end
end);
end
-- process or waits to process the lobby muc component
process_host_module(lobby_muc_component_config, function(host_module, host)
-- lobby muc component created
module:log('info', 'Lobby component loaded %s', host);
local muc_module = prosody.hosts[host].modules.muc;
if muc_module then
process_lobby_muc_loaded(muc_module, host_module);
else
module:log('debug', 'Will wait for muc to be available');
prosody.hosts[host].events.add_handler('module-loaded', function(event)
if (event.module == 'muc') then
process_lobby_muc_loaded(prosody.hosts[host].modules.muc, host_module);
end
end);
end
end);
-- process or waits to process the main muc component
process_host_module(main_muc_component_config, function(host_module, host)
main_muc_service = prosody.hosts[host].modules.muc;
-- hooks when lobby is enabled to create its room, only done here or by admin
host_module:hook('muc-config-submitted', function(event)
local actor, room = event.actor, event.room;
local actor_node = jid_split(actor);
if actor_node == 'focus' then
return;
end
local members_only = event.fields['muc#roomconfig_membersonly'] and true or nil;
if members_only then
local lobby_created = attach_lobby_room(room);
if lobby_created then
event.status_codes['104'] = true;
notify_lobby_enabled(room, actor, true);
end
elseif room._data.lobbyroom then
destroy_lobby_room(room, room.jid);
notify_lobby_enabled(room, actor, false);
end
end);
host_module:hook('muc-room-destroyed',function(event)
local room = event.room;
if room._data.lobbyroom then
destroy_lobby_room(room, nil);
end
end);
host_module:hook('muc-disco#info', function (event)
local room = event.room;
if (room._data.lobbyroom and room:get_members_only()) then
table.insert(event.form, {
name = 'muc#roominfo_lobbyroom';
label = 'Lobby room jid';
value = '';
});
event.formdata['muc#roominfo_lobbyroom'] = room._data.lobbyroom.jid;
end
end);
host_module:hook('muc-occupant-pre-join', function (event)
local room, stanza = event.room, event.stanza;
if is_healthcheck_room(room.jid) or not room:get_members_only() then
return;
end
local join = stanza:get_child('x', MUC_NS);
if not join then
return;
end
local invitee = event.stanza.attr.from;
local invitee_bare_jid = jid_bare(invitee);
local _, invitee_domain = jid_split(invitee);
local whitelistJoin = false;
-- whitelist participants
if whitelist:contains(invitee_domain) or whitelist:contains(invitee_bare_jid) then
whitelistJoin = true;
end
local password = join:get_child_text('password', MUC_NS);
if password and room:get_password() and password == room:get_password() then
whitelistJoin = true;
end
if whitelistJoin then
local affiliation = room:get_affiliation(invitee);
if not affiliation or affiliation == 0 then
event.occupant.role = 'participant';
room:set_affiliation(true, invitee_bare_jid, 'member');
room:save();
return;
end
end
-- we want to add the custom lobbyroom field to fill in the lobby room jid
local invitee = event.stanza.attr.from;
local affiliation = room:get_affiliation(invitee);
if not affiliation or affiliation == 'none' then
local reply = st.error_reply(stanza, 'auth', 'registration-required'):up();
reply.tags[1].attr.code = '407';
reply:tag('x', {xmlns = MUC_NS}):up();
reply:tag('lobbyroom'):text(room._data.lobbyroom.jid);
event.origin.send(reply:tag('x', {xmlns = MUC_NS}));
return true;
end
end, -4); -- the default hook on members_only module is on -5
-- listens for invites for participants to join the main room
host_module:hook('muc-invite', function(event)
local room, stanza = event.room, event.stanza;
local invitee = stanza.attr.to;
local from = stanza:get_child('x', 'http://jabber.org/protocol/muc#user')
:get_child('invite').attr.from;
if room._data.lobbyroom then
local occupant = room._data.lobbyroom:get_occupant_by_real_jid(invitee);
if occupant then
local display_name = occupant:get_presence():get_child_text(
'nick', 'http://jabber.org/protocol/nick');
notify_lobby_access(room, from, occupant.nick, display_name, true);
end
end
end);
end);
-- Extract 'room' param from URL when session is created
function update_session(event)
local session = event.session;
if session.jitsi_web_query_room then
-- no need for an update
return;
end
local query = event.request.url.query;
if query ~= nil then
local params = formdecode(query);
-- The room name and optional prefix from the web query
session.jitsi_web_query_room = params.room;
session.jitsi_web_query_prefix = params.prefix or '';
end
end
function handle_create_lobby(event)
local room = event.room;
room:set_members_only(true);
module:log("info","Set room jid = %s as members only",room.jid);
attach_lobby_room(room)
end
function handle_destroy_lobby(event)
destroy_lobby_room(event.room, event.newjid, event.message);
end
module:hook_global('bosh-session', update_session);
module:hook_global('websocket-session', update_session);
module:hook_global('config-reloaded', load_config);
module:hook_global('create-lobby-room', handle_create_lobby);
module:hook_global('destroy-lobby-room', handle_destroy_lobby);
@@ -0,0 +1,66 @@
-- MUC Max Occupants
-- Configuring muc_max_occupants will set a limit of the maximum number
-- of participants that will be able to join in a room.
-- Participants in muc_access_whitelist will not be counted for the
-- max occupants value (values are jids like recorder@jitsi.meeet.example.com).
-- This module is configured under the muc component that is used for jitsi-meet
local split_jid = require "util.jid".split;
local st = require "util.stanza";
local it = require "util.iterators";
local whitelist = module:get_option_set("muc_access_whitelist");
local MAX_OCCUPANTS = module:get_option_number("muc_max_occupants", -1);
local function count_keys(t)
return it.count(it.keys(t));
end
local function check_for_max_occupants(event)
local room, origin, stanza = event.room, event.origin, event.stanza;
local actor = stanza.attr.from;
local user, domain, res = split_jid(stanza.attr.from);
--no user object means no way to check for max occupants
if user == nil then
return
end
-- If we're a whitelisted user joining the room, don't bother checking the max
-- occupants.
if whitelist and whitelist:contains(domain) or whitelist:contains(user..'@'..domain) then
return;
end
if room and not room._jid_nick[stanza.attr.from] then
local count = count_keys(room._occupants);
local slots = MAX_OCCUPANTS;
-- If there is no whitelist, just check the count.
if not whitelist and count >= MAX_OCCUPANTS then
module:log("info", "Attempt to enter a maxed out MUC");
origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
return true;
end
-- TODO: Are Prosody hooks atomic, or is this a race condition?
-- For each person in the room that's not on the whitelist, subtract one
-- from the count.
for _, occupant in room:each_occupant() do
user, domain, res = split_jid(occupant.bare_jid);
if not whitelist:contains(domain) and not whitelist:contains(user..'@'..domain) then
slots = slots - 1
end
end
-- If the room is full (<0 slots left), error out.
if slots <= 0 then
module:log("info", "Attempt to enter a maxed out MUC");
origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
return true;
end
end
end
if MAX_OCCUPANTS > 0 then
module:hook("muc-occupant-pre-join", check_for_max_occupants, 10);
end
@@ -0,0 +1,40 @@
local uuid_gen = require "util.uuid".generate;
local is_healthcheck_room = module:require "util".is_healthcheck_room;
-- 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)
-- Hook to assign meetingId for new rooms
module:hook("muc-room-created", function(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
room._data.meetingId = uuid_gen();
module:log("debug", "Created meetingId:%s for %s",
room._data.meetingId, room.jid);
end);
-- Returns the meeting config Id form data.
function getMeetingIdConfig(room)
return {
name = "muc#roominfo_meetingId";
type = "text-single";
label = "The meeting unique id.";
value = room._data.meetingId or "";
};
end
-- add meeting Id to the disco info requests to the room
module:hook("muc-disco#info", function(event)
table.insert(event.form, getMeetingIdConfig(event.room));
end);
-- add the meeting Id in the default config we return to jicofo
module:hook("muc-config-form", function(event)
table.insert(event.form, getMeetingIdConfig(event.room));
end, 90-3);
@@ -0,0 +1,332 @@
local bare = require "util.jid".bare;
local get_room_from_jid = module:require "util".get_room_from_jid;
local jid = require "util.jid";
local neturl = require "net.url";
local parse = neturl.parseQuery;
local poltergeist = module:require "poltergeist";
local have_async = pcall(require, "util.async");
if not have_async then
module:log("error", "requires a version of Prosody with util.async");
return;
end
local async_handler_wrapper = module:require "util".async_handler_wrapper;
-- Options
local poltergeist_component
= module:get_option_string("poltergeist_component", module.host);
-- this basically strips the domain from the conference.domain address
local parentHostName = string.gmatch(tostring(module.host), "%w+.(%w.+)")();
if parentHostName == nil then
log("error", "Failed to start - unable to get parent hostname");
return;
end
local parentCtx = module:context(parentHostName);
if parentCtx == nil then
log("error",
"Failed to start - unable to get parent context for host: %s",
tostring(parentHostName));
return;
end
local token_util = module:require "token/util".new(parentCtx);
-- option to enable/disable token verifications
local disableTokenVerification
= module:get_option_boolean("disable_polergeist_token_verification", false);
-- poltergaist management functions
-- Returns the room if available, work and in multidomain mode
-- @param room_name the name of the room
-- @param group name of the group (optional)
-- @return returns room if found or nil
function get_room(room_name, group)
local room_address = jid.join(room_name, module:get_host());
-- if there is a group we are in multidomain mode and that group is not
-- our parent host
if group and group ~= "" and group ~= parentHostName then
room_address = "["..group.."]"..room_address;
end
return get_room_from_jid(room_address);
end
--- Verifies room name, domain name with the values in the token
-- @param token the token we received
-- @param room_name the room name
-- @param group name of the group (optional)
-- @param session the session to use for storing token specific fields
-- @return true if values are ok or false otherwise
function verify_token(token, room_name, group, session)
if disableTokenVerification then
return true;
end
-- if not disableTokenVerification and we do not have token
-- stop here, cause the main virtual host can have guest access enabled
-- (allowEmptyToken = true) and we will allow access to rooms info without
-- a token
if token == nil then
log("warn", "no token provided");
return false;
end
session.auth_token = token;
local verified, reason = token_util:process_and_verify_token(session);
if not verified then
log("warn", "not a valid token %s", tostring(reason));
return false;
end
local room_address = jid.join(room_name, module:get_host());
-- if there is a group we are in multidomain mode and that group is not
-- our parent host
if group and group ~= "" and group ~= parentHostName then
room_address = "["..group.."]"..room_address;
end
if not token_util:verify_room(session, room_address) then
log("warn", "Token %s not allowed to join: %s",
tostring(token), tostring(room_address));
return false;
end
return true;
end
-- Event handlers
-- if we found that a session for a user with id has a poltergiest already
-- created, retrieve its jid and return it to the authentication
-- so we can reuse it and we that real user will replace the poltergiest
prosody.events.add_handler("pre-jitsi-authentication", function(session)
if (session.jitsi_meet_context_user) then
local room = get_room(
session.jitsi_web_query_room,
session.jitsi_web_query_prefix);
if (not room) then
return nil;
end
local username = poltergeist.get_username(
room,
session.jitsi_meet_context_user["id"]
);
if (not username) then
return nil;
end
log("debug", "Found predefined username %s", username);
-- let's find the room and if the poltergeist occupant is there
-- lets remove him before the real participant joins
-- when we see the unavailable presence to go out the server
-- we will mark it with ignore tag
local nick = poltergeist.create_nick(username);
if (poltergeist.occupies(room, nick)) then
module:log("info", "swapping poltergeist for user: %s/%s", room, nick)
-- notify that user connected using the poltergeist
poltergeist.update(room, nick, "connected");
poltergeist.remove(room, nick, true);
end
return username;
end
return nil;
end);
--- Note: mod_muc and some of its sub-modules add event handlers between 0 and -100,
--- e.g. to check for banned users, etc.. Hence adding these handlers at priority -100.
module:hook("muc-decline", function (event)
poltergeist.remove(event.room, bare(event.stanza.attr.from), false);
end, -100);
-- before sending the presence for a poltergeist leaving add ignore tag
-- as poltergeist is leaving just before the real user joins and in the client
-- we ignore this presence to avoid leaving/joining experience and the real
-- user will reuse all currently created UI components for the same nick
module:hook("muc-broadcast-presence", function (event)
if (bare(event.occupant.jid) == poltergeist_component) then
if(event.stanza.attr.type == "unavailable"
and poltergeist.should_ignore(event.occupant.nick)) then
event.stanza:tag(
"ignore", { xmlns = "http://jitsi.org/jitmeet/" }):up();
poltergeist.reset_ignored(event.occupant.nick);
end
end
end, -100);
-- cleanup room table after room is destroyed
module:hook(
"muc-room-destroyed",
function(event)
poltergeist.remove_room(event.room);
end
);
--- Handles request for creating/managing poltergeists
-- @param event the http event, holds the request query
-- @return GET response, containing a json with response details
function handle_create_poltergeist (event)
if (not event.request.url.query) then
return { status_code = 400; };
end
local params = parse(event.request.url.query);
local user_id = params["user"];
local room_name = params["room"];
local group = params["group"];
local name = params["name"];
local avatar = params["avatar"];
local status = params["status"];
local conversation = params["conversation"];
local session = {};
if not verify_token(params["token"], room_name, group, session) then
return { status_code = 403; };
end
-- If the provided room conference doesn't exist then we
-- can't add a poltergeist to it.
local room = get_room(room_name, group);
if (not room) then
log("error", "no room found %s", room_name);
return { status_code = 404; };
end
-- If the poltergiest is already in the conference then it will
-- be in our username store and another can't be added.
local username = poltergeist.get_username(room, user_id);
if (username ~=nil and
poltergeist.occupies(room, poltergeist.create_nick(username))) then
log("warn",
"poltergeist for username:%s already in the room:%s",
username,
room_name
);
return { status_code = 202; };
end
local context = {
user = {
id = user_id;
};
group = group;
creator_user = session.jitsi_meet_context_user;
creator_group = session.jitsi_meet_context_group;
};
if avatar ~= nil then
context.user.avatar = avatar
end
local resources = {};
if conversation ~= nil then
resources["conversation"] = conversation
end
poltergeist.add_to_muc(room, user_id, name, avatar, context, status, resources)
return { status_code = 200; };
end
--- Handles request for updating poltergeists status
-- @param event the http event, holds the request query
-- @return GET response, containing a json with response details
function handle_update_poltergeist (event)
if (not event.request.url.query) then
return { status_code = 400; };
end
local params = parse(event.request.url.query);
local user_id = params["user"];
local room_name = params["room"];
local group = params["group"];
local status = params["status"];
local call_id = params["callid"];
local call_cancel = false
if params["callcancel"] == "true" then
call_cancel = true;
end
if not verify_token(params["token"], room_name, group, {}) then
return { status_code = 403; };
end
local room = get_room(room_name, group);
if (not room) then
log("error", "no room found %s", room_name);
return { status_code = 404; };
end
local username = poltergeist.get_username(room, user_id);
if (not username) then
return { status_code = 404; };
end
local call_details = {
["cancel"] = call_cancel;
["id"] = call_id;
};
local nick = poltergeist.create_nick(username);
if (not poltergeist.occupies(room, nick)) then
return { status_code = 404; };
end
poltergeist.update(room, nick, status, call_details);
return { status_code = 200; };
end
--- Handles remove poltergeists
-- @param event the http event, holds the request query
-- @return GET response, containing a json with response details
function handle_remove_poltergeist (event)
if (not event.request.url.query) then
return { status_code = 400; };
end
local params = parse(event.request.url.query);
local user_id = params["user"];
local room_name = params["room"];
local group = params["group"];
if not verify_token(params["token"], room_name, group, {}) then
return { status_code = 403; };
end
local room = get_room(room_name, group);
if (not room) then
log("error", "no room found %s", room_name);
return { status_code = 404; };
end
local username = poltergeist.get_username(room, user_id);
if (not username) then
return { status_code = 404; };
end
local nick = poltergeist.create_nick(username);
if (not poltergeist.occupies(room, nick)) then
return { status_code = 404; };
end
poltergeist.remove(room, nick, false);
return { status_code = 200; };
end
log("info", "Loading poltergeist service");
module:depends("http");
module:provides("http", {
default_path = "/";
name = "poltergeist";
route = {
["GET /poltergeist/create"] = function (event) return async_handler_wrapper(event,handle_create_poltergeist) end;
["GET /poltergeist/update"] = function (event) return async_handler_wrapper(event,handle_update_poltergeist) end;
["GET /poltergeist/remove"] = function (event) return async_handler_wrapper(event,handle_remove_poltergeist) end;
};
});
+197
View File
@@ -0,0 +1,197 @@
-- Prosody IM
-- Copyright (C) 2017 Atlassian
--
local jid = require "util.jid";
local it = require "util.iterators";
local json = require "util.json";
local iterators = require "util.iterators";
local array = require"util.array";
local have_async = pcall(require, "util.async");
if not have_async then
module:log("error", "requires a version of Prosody with util.async");
return;
end
local async_handler_wrapper = module:require "util".async_handler_wrapper;
local tostring = tostring;
local neturl = require "net.url";
local parse = neturl.parseQuery;
-- option to enable/disable room API token verifications
local enableTokenVerification
= module:get_option_boolean("enable_roomsize_token_verification", false);
local token_util = module:require "token/util".new(module);
local get_room_from_jid = module:require "util".get_room_from_jid;
-- no token configuration but required
if token_util == nil and enableTokenVerification then
log("error", "no token configuration but it is required");
return;
end
-- required parameter for custom muc component prefix,
-- defaults to "conference"
local muc_domain_prefix
= module:get_option_string("muc_mapper_domain_prefix", "conference");
--- Verifies room name, domain name with the values in 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 not enableTokenVerification then
return true;
end
-- if enableTokenVerification is enabled and we do not have token
-- stop here, cause the main virtual host can have guest access enabled
-- (allowEmptyToken = true) and we will allow access to rooms info without
-- a token
if token == nil then
log("warn", "no token provided");
return false;
end
local session = {};
session.auth_token = token;
local verified, reason = token_util:process_and_verify_token(session);
if not verified then
log("warn", "not a valid token %s", tostring(reason));
return false;
end
if not token_util:verify_room(session, room_address) then
log("warn", "Token %s not allowed to join: %s",
tostring(token), tostring(room_address));
return false;
end
return true;
end
--- Handles request for retrieving the room size
-- @param event the http event, holds the request query
-- @return GET response, containing a json with participants count,
-- tha value is without counting the focus.
function handle_get_room_size(event)
if (not event.request.url.query) then
return { status_code = 400; };
end
local params = parse(event.request.url.query);
local room_name = params["room"];
local domain_name = params["domain"];
local subdomain = params["subdomain"];
local room_address
= jid.join(room_name, muc_domain_prefix.."."..domain_name);
if subdomain and subdomain ~= "" then
room_address = "["..subdomain.."]"..room_address;
end
if not verify_token(params["token"], room_address) then
return { status_code = 403; };
end
local room = get_room_from_jid(room_address);
local participant_count = 0;
log("debug", "Querying room %s", tostring(room_address));
if room then
local occupants = room._occupants;
if occupants then
participant_count = iterators.count(room:each_occupant());
end
log("debug",
"there are %s occupants in room", tostring(participant_count));
else
log("debug", "no such room exists");
return { status_code = 404; };
end
if participant_count > 1 then
participant_count = participant_count - 1;
end
return { status_code = 200; body = [[{"participants":]]..participant_count..[[}]] };
end
--- Handles request for retrieving the room participants details
-- @param event the http event, holds the request query
-- @return GET response, containing a json with participants details
function handle_get_room (event)
if (not event.request.url.query) then
return { status_code = 400; };
end
local params = parse(event.request.url.query);
local room_name = params["room"];
local domain_name = params["domain"];
local subdomain = params["subdomain"];
local room_address
= jid.join(room_name, muc_domain_prefix.."."..domain_name);
if subdomain and subdomain ~= "" then
room_address = "["..subdomain.."]"..room_address;
end
if not verify_token(params["token"], room_address) then
return { status_code = 403; };
end
local room = get_room_from_jid(room_address);
local participant_count = 0;
local occupants_json = array();
log("debug", "Querying room %s", tostring(room_address));
if room then
local occupants = room._occupants;
if occupants then
participant_count = iterators.count(room:each_occupant());
for _, occupant in room:each_occupant() do
-- filter focus as we keep it as hidden participant
if string.sub(occupant.nick,-string.len("/focus"))~="/focus" then
for _, pr in occupant:each_session() do
local nick = pr:get_child_text("nick", "http://jabber.org/protocol/nick") or "";
local email = pr:get_child_text("email") or "";
occupants_json:push({
jid = tostring(occupant.nick),
email = tostring(email),
display_name = tostring(nick)});
end
end
end
end
log("debug",
"there are %s occupants in room", tostring(participant_count));
else
log("debug", "no such room exists");
return { status_code = 404; };
end
if participant_count > 1 then
participant_count = participant_count - 1;
end
return { status_code = 200; body = json.encode(occupants_json); };
end;
function module.load()
module:depends("http");
module:provides("http", {
default_path = "/";
route = {
["GET room-size"] = function (event) return async_handler_wrapper(event,handle_get_room_size) end;
["GET sessions"] = function () return tostring(it.count(it.keys(prosody.full_sessions))); end;
["GET room"] = function (event) return async_handler_wrapper(event,handle_get_room) end;
};
});
end
@@ -0,0 +1,31 @@
--This module performs features checking when a transcription is requested.
--If the transcription feature is not allowed, the tag indicating that a
--transcription is being requested will be stripped from the presence stanza.
--The module must be enabled under the muc component.
local is_feature_allowed = module:require "util".is_feature_allowed;
module:log("info", "Loading mod_muc_transcription_filter!");
local filtered_tag_name = "jitsi_participant_requestingTranscription";
function filter_transcription_tag(event)
local stanza = event.stanza;
local session = event.origin;
if stanza and stanza.name == "presence" then
if not is_feature_allowed(session,'transcription') then
stanza:maptags(function(tag)
if tag and tag.name == filtered_tag_name then
module:log("info", "Removing %s tag from presence stanza!", filtered_tag_name);
return nil;
else
return tag;
end
end)
end
end
end
module:hook("presence/bare", filter_transcription_tag);
module:hook("presence/full", filter_transcription_tag);
module:hook("presence/host", filter_transcription_tag);
module:log("info", "Loaded mod_muc_transcription_filter!");
@@ -0,0 +1,21 @@
local st = require "util.stanza";
-- A component which we use to receive all stanzas for the created poltergeists
-- replays with error if an iq is sent
function no_action()
return true;
end
function error_reply(event)
module:send(st.error_reply(event.stanza, "cancel", "service-unavailable"));
return true;
end
module:hook("presence/host", no_action);
module:hook("message/host", no_action);
module:hook("presence/full", no_action);
module:hook("message/full", no_action);
module:hook("iq/host", error_reply);
module:hook("iq/full", error_reply);
module:hook("iq/bare", error_reply);
@@ -0,0 +1,22 @@
local stanza = require "util.stanza";
local update_presence_identity = module:require "util".update_presence_identity;
-- For all received presence messages, if the jitsi_meet_context_(user|group)
-- 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
update_presence_identity(
event.stanza,
event.origin.jitsi_meet_context_user,
event.origin.jitsi_meet_context_group
);
end
end
end
module:hook("pre-presence/bare", on_message);
module:hook("pre-presence/full", on_message);
+646
View File
@@ -0,0 +1,646 @@
-- XEP-0198: Stream Management for Prosody IM
--
-- Copyright (C) 2010-2015 Matthew Wild
-- Copyright (C) 2010 Waqas Hussain
-- Copyright (C) 2012-2015 Kim Alvefur
-- Copyright (C) 2012 Thijs Alkemade
-- Copyright (C) 2014 Florian Zeitz
-- Copyright (C) 2016-2020 Thilo Molitor
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local st = require "util.stanza";
local dep = require "util.dependencies";
local cache = dep.softreq("util.cache"); -- only available in prosody 0.10+
local uuid_generate = require "util.uuid".generate;
local jid = require "util.jid";
local t_insert, t_remove = table.insert, table.remove;
local math_min = math.min;
local math_max = math.max;
local os_time = os.time;
local tonumber, tostring = tonumber, tostring;
local add_filter = require "util.filters".add_filter;
local timer = require "util.timer";
local datetime = require "util.datetime";
local xmlns_sm2 = "urn:xmpp:sm:2";
local xmlns_sm3 = "urn:xmpp:sm:3";
local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas";
local xmlns_delay = "urn:xmpp:delay";
local sm2_attr = { xmlns = xmlns_sm2 };
local sm3_attr = { xmlns = xmlns_sm3 };
local resume_timeout = module:get_option_number("smacks_hibernation_time", 300);
local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", false);
local s2s_resend = module:get_option_boolean("smacks_s2s_resend", false);
local max_unacked_stanzas = module:get_option_number("smacks_max_unacked_stanzas", 0);
local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 60);
local max_hibernated_sessions = module:get_option_number("smacks_max_hibernated_sessions", 10);
local max_old_sessions = module:get_option_number("smacks_max_old_sessions", 10);
local core_process_stanza = prosody.core_process_stanza;
local sessionmanager = require"core.sessionmanager";
assert(max_hibernated_sessions > 0, "smacks_max_hibernated_sessions must be greater than 0");
assert(max_old_sessions > 0, "smacks_max_old_sessions must be greater than 0");
local c2s_sessions = module:shared("/*/c2s/sessions");
local function init_session_cache(max_entries, evict_callback)
-- old prosody version < 0.10 (no limiting at all!)
if not cache then
local store = {};
return {
get = function(user, key)
if not user then return nil; end
if not key then return nil; end
return store[key];
end;
set = function(user, key, value)
if not user then return nil; end
if not key then return nil; end
store[key] = value;
end;
};
end
-- use per user limited cache for prosody >= 0.10
local stores = {};
return {
get = function(user, key)
if not user then return nil; end
if not key then return nil; end
if not stores[user] then
stores[user] = cache.new(max_entries, evict_callback);
end
return stores[user]:get(key);
end;
set = function(user, key, value)
if not user then return nil; end
if not key then return nil; end
if not stores[user] then stores[user] = cache.new(max_entries, evict_callback); end
stores[user]:set(key, value);
-- remove empty caches completely
if not stores[user]:count() then stores[user] = nil; end
end;
};
end
local old_session_registry = init_session_cache(max_old_sessions, nil);
local session_registry = init_session_cache(max_hibernated_sessions, function(resumption_token, session)
if session.destroyed then return true; end -- destroyed session can always be removed from cache
session.log("warn", "User has too much hibernated sessions, removing oldest session (token: %s)", resumption_token);
-- store old session's h values on force delete
-- save only actual h value and username/host (for security)
old_session_registry.set(session.username, resumption_token, {
h = session.handled_stanza_count,
username = session.username,
host = session.host
});
return true; -- allow session to be removed from full cache to make room for new one
end);
local function stoppable_timer(delay, callback)
local stopped = false;
local timer = module:add_timer(delay, function (t)
if stopped then return; end
return callback(t);
end);
if timer and timer.stop then return timer; end -- new prosody api includes stop() function
return {
stop = function(self) stopped = true end;
timer;
};
end
local function delayed_ack_function(session)
-- fire event only if configured to do so and our session is not already hibernated or destroyed
if delayed_ack_timeout > 0 and session.awaiting_ack
and not session.hibernating and not session.destroyed then
session.log("debug", "Firing event 'smacks-ack-delayed', queue = %d",
session.outgoing_stanza_queue and #session.outgoing_stanza_queue or 0);
module:fire_event("smacks-ack-delayed", {origin = session, queue = session.outgoing_stanza_queue});
end
session.delayed_ack_timer = nil;
end
local function can_do_smacks(session, advertise_only)
if session.smacks then return false, "unexpected-request", "Stream management is already enabled"; end
local session_type = session.type;
if session.username then
if not(advertise_only) and not(session.resource) then -- Fail unless we're only advertising sm
return false, "unexpected-request", "Client must bind a resource before enabling stream management";
end
return true;
elseif s2s_smacks and (session_type == "s2sin" or session_type == "s2sout") then
return true;
end
return false, "service-unavailable", "Stream management is not available for this stream";
end
module:hook("stream-features",
function (event)
if can_do_smacks(event.origin, true) then
event.features:tag("sm", sm2_attr):tag("optional"):up():up();
event.features:tag("sm", sm3_attr):tag("optional"):up():up();
end
end);
module:hook("s2s-stream-features",
function (event)
if can_do_smacks(event.origin, true) then
event.features:tag("sm", sm2_attr):tag("optional"):up():up();
event.features:tag("sm", sm3_attr):tag("optional"):up():up();
end
end);
local function request_ack_if_needed(session, force, reason)
local queue = session.outgoing_stanza_queue;
local expected_h = session.last_acknowledged_stanza + #queue;
-- session.log("debug", "*** SMACKS(1) ***: awaiting_ack=%s, hibernating=%s", tostring(session.awaiting_ack), tostring(session.hibernating));
if session.awaiting_ack == nil and not session.hibernating then
-- this check of last_requested_h prevents ack-loops if missbehaving clients report wrong
-- stanza counts. it is set when an <r> is really sent (e.g. inside timer), preventing any
-- further requests until a higher h-value would be expected.
-- session.log("debug", "*** SMACKS(2) ***: #queue=%s, max_unacked_stanzas=%s, expected_h=%s, last_requested_h=%s", tostring(#queue), tostring(max_unacked_stanzas), tostring(expected_h), tostring(session.last_requested_h));
if (#queue > max_unacked_stanzas and expected_h ~= session.last_requested_h) or force then
session.log("debug", "Queuing <r> (in a moment) from %s - #queue=%d", reason, #queue);
session.awaiting_ack = false;
session.awaiting_ack_timer = stoppable_timer(1e-06, function ()
-- session.log("debug", "*** SMACKS(3) ***: awaiting_ack=%s, hibernating=%s", tostring(session.awaiting_ack), tostring(session.hibernating));
-- only request ack if needed and our session is not already hibernated or destroyed
if not session.awaiting_ack and not session.hibernating and not session.destroyed then
session.log("debug", "Sending <r> (inside timer, before send) from %s - #queue=%d", reason, #queue);
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }))
session.awaiting_ack = true;
-- expected_h could be lower than this expression e.g. more stanzas added to the queue meanwhile)
session.last_requested_h = session.last_acknowledged_stanza + #queue;
session.log("debug", "Sending <r> (inside timer, after send) from %s - #queue=%d", reason, #queue);
if not session.delayed_ack_timer then
session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function()
delayed_ack_function(session);
end);
end
end
end);
end
end
-- Trigger "smacks-ack-delayed"-event if we added new (ackable) stanzas to the outgoing queue
-- and there isn't already a timer for this event running.
-- If we wouldn't do this, stanzas added to the queue after the first "smacks-ack-delayed"-event
-- would not trigger this event (again).
if #queue > max_unacked_stanzas and session.awaiting_ack and session.delayed_ack_timer == nil then
session.log("debug", "Calling delayed_ack_function directly (still waiting for ack)");
delayed_ack_function(session);
end
end
local function outgoing_stanza_filter(stanza, session)
local is_stanza = stanza.attr and not stanza.attr.xmlns and not stanza.name:find":";
if is_stanza and not stanza._cached then -- Stanza in default stream namespace
local queue = session.outgoing_stanza_queue;
local cached_stanza = st.clone(stanza);
cached_stanza._cached = true;
if cached_stanza and cached_stanza.name ~= "iq" and cached_stanza:get_child("delay", xmlns_delay) == nil then
cached_stanza = cached_stanza:tag("delay", {
xmlns = xmlns_delay,
from = jid.bare(session.full_jid or session.host),
stamp = datetime.datetime()
});
end
queue[#queue+1] = cached_stanza;
if session.hibernating then
session.log("debug", "hibernating, stanza queued");
module:fire_event("smacks-hibernation-stanza-queued", {origin = session, queue = queue, stanza = cached_stanza});
return nil;
end
request_ack_if_needed(session, false, "outgoing_stanza_filter");
end
return stanza;
end
local function count_incoming_stanzas(stanza, session)
if not stanza.attr.xmlns then
session.handled_stanza_count = session.handled_stanza_count + 1;
session.log("debug", "Handled %d incoming stanzas", session.handled_stanza_count);
end
return stanza;
end
local function wrap_session_out(session, resume)
if not resume then
session.outgoing_stanza_queue = {};
session.last_acknowledged_stanza = 0;
end
add_filter(session, "stanzas/out", outgoing_stanza_filter, -999);
local session_close = session.close;
function session.close(...)
if session.resumption_token then
session_registry.set(session.username, session.resumption_token, nil);
old_session_registry.set(session.username, session.resumption_token, nil);
session.resumption_token = nil;
end
-- send out last ack as per revision 1.5.2 of XEP-0198
if session.smacks and session.conn then
(session.sends2s or session.send)(st.stanza("a", { xmlns = session.smacks, h = string.format("%d", session.handled_stanza_count) }));
end
return session_close(...);
end
return session;
end
local function wrap_session_in(session, resume)
if not resume then
session.handled_stanza_count = 0;
end
add_filter(session, "stanzas/in", count_incoming_stanzas, 999);
return session;
end
local function wrap_session(session, resume)
wrap_session_out(session, resume);
wrap_session_in(session, resume);
return session;
end
function handle_enable(session, stanza, xmlns_sm)
local ok, err, err_text = can_do_smacks(session);
if not ok then
session.log("warn", "Failed to enable smacks: %s", err_text); -- TODO: XEP doesn't say we can send error text, should it?
(session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):tag(err, { xmlns = xmlns_errors}));
return true;
end
module:log("debug", "Enabling stream management");
session.smacks = xmlns_sm;
wrap_session(session, false);
local resume_token;
local resume = stanza.attr.resume;
if resume == "true" or resume == "1" then
resume_token = uuid_generate();
session_registry.set(session.username, resume_token, session);
session.resumption_token = resume_token;
end
(session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm, id = resume_token, resume = resume, max = tostring(resume_timeout) }));
return true;
end
module:hook_stanza(xmlns_sm2, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm2); end, 100);
module:hook_stanza(xmlns_sm3, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm3); end, 100);
module:hook_stanza("http://etherx.jabber.org/streams", "features",
function (session, stanza)
stoppable_timer(1e-6, function ()
if can_do_smacks(session) then
if stanza:get_child("sm", xmlns_sm3) then
session.sends2s(st.stanza("enable", sm3_attr));
session.smacks = xmlns_sm3;
elseif stanza:get_child("sm", xmlns_sm2) then
session.sends2s(st.stanza("enable", sm2_attr));
session.smacks = xmlns_sm2;
else
return;
end
wrap_session_out(session, false);
end
end);
end);
function handle_enabled(session, stanza, xmlns_sm)
module:log("debug", "Enabling stream management");
session.smacks = xmlns_sm;
wrap_session_in(session, false);
-- FIXME Resume?
return true;
end
module:hook_stanza(xmlns_sm2, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm2); end, 100);
module:hook_stanza(xmlns_sm3, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm3); end, 100);
function handle_r(origin, stanza, xmlns_sm)
if not origin.smacks then
module:log("debug", "Received ack request from non-smack-enabled session");
return;
end
module:log("debug", "Received ack request, acking for %d", origin.handled_stanza_count);
-- Reply with <a>
(origin.sends2s or origin.send)(st.stanza("a", { xmlns = xmlns_sm, h = string.format("%d", origin.handled_stanza_count) }));
-- piggyback our own ack request if needed (see request_ack_if_needed() for explanation of last_requested_h)
local expected_h = origin.last_acknowledged_stanza + #origin.outgoing_stanza_queue;
if #origin.outgoing_stanza_queue > 0 and expected_h ~= origin.last_requested_h then
request_ack_if_needed(origin, true, "piggybacked by handle_r");
end
return true;
end
module:hook_stanza(xmlns_sm2, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm2); end);
module:hook_stanza(xmlns_sm3, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm3); end);
function handle_a(origin, stanza)
if not origin.smacks then return; end
origin.awaiting_ack = nil;
if origin.awaiting_ack_timer then
origin.awaiting_ack_timer:stop();
end
if origin.delayed_ack_timer then
origin.delayed_ack_timer:stop();
origin.delayed_ack_timer = nil;
end
-- Remove handled stanzas from outgoing_stanza_queue
-- origin.log("debug", "ACK: h=%s, last=%s", stanza.attr.h or "", origin.last_acknowledged_stanza or "");
local h = tonumber(stanza.attr.h);
if not h then
origin:close{ condition = "invalid-xml"; text = "Missing or invalid 'h' attribute"; };
return;
end
local handled_stanza_count = h-origin.last_acknowledged_stanza;
local queue = origin.outgoing_stanza_queue;
if handled_stanza_count > #queue then
origin.log("warn", "The client says it handled %d new stanzas, but we only sent %d :)",
handled_stanza_count, #queue);
origin.log("debug", "Client h: %d, our h: %d", tonumber(stanza.attr.h), origin.last_acknowledged_stanza);
for i=1,#queue do
origin.log("debug", "Q item %d: %s", i, tostring(queue[i]));
end
end
for i=1,math_min(handled_stanza_count,#queue) do
local handled_stanza = t_remove(origin.outgoing_stanza_queue, 1);
module:fire_event("delivery/success", { session = origin, stanza = handled_stanza });
end
origin.log("debug", "#queue = %d", #queue);
origin.last_acknowledged_stanza = origin.last_acknowledged_stanza + handled_stanza_count;
request_ack_if_needed(origin, false, "handle_a")
return true;
end
module:hook_stanza(xmlns_sm2, "a", handle_a);
module:hook_stanza(xmlns_sm3, "a", handle_a);
--TODO: Optimise... incoming stanzas should be handled by a per-session
-- function that has a counter as an upvalue (no table indexing for increments,
-- and won't slow non-198 sessions). We can also then remove the .handled flag
-- on stanzas
local function handle_unacked_stanzas(session)
local queue = session.outgoing_stanza_queue;
local error_attr = { type = "cancel" };
if #queue > 0 then
session.outgoing_stanza_queue = {};
for i=1,#queue do
if not module:fire_event("delivery/failure", { session = session, stanza = queue[i] }) then
local reply = st.reply(queue[i]);
if reply.attr.to ~= session.full_jid then
reply.attr.type = "error";
reply:tag("error", error_attr)
:tag("recipient-unavailable", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"});
core_process_stanza(session, reply);
end
end
end
end
end
-- don't send delivery errors for messages which will be delivered by mam later on
module:hook("delivery/failure", function(event)
local session, stanza = event.session, event.stanza;
-- Only deal with authenticated (c2s) sessions
if session.username then
if stanza.name == "message" and stanza.attr.xmlns == nil and
( stanza.attr.type == "chat" or ( stanza.attr.type or "normal" ) == "normal" ) then
-- do nothing here for normal messages and don't send out "message delivery errors",
-- because messages are already in MAM at this point (no need to frighten users)
if session.mam_requested and stanza._was_archived then
return true; -- stanza handled, don't send an error
end
-- store message in offline store, if this client does not use mam *and* was the last client online
local sessions = prosody.hosts[module.host].sessions[session.username] and
prosody.hosts[module.host].sessions[session.username].sessions or nil;
if sessions and next(sessions) == session.resource and next(sessions, session.resource) == nil then
module:fire_event("message/offline/handle", { origin = session, stanza = stanza } );
return true; -- stanza handled, don't send an error
end
end
end
end);
-- mark stanzas as archived --> this will allow us to send back errors for stanzas not archived
-- because the user configured the server to do so ("no-archive"-setting for one special contact for example)
module:hook("archive-message-added", function(event)
local session, stanza, for_user, stanza_id = event.origin, event.stanza, event.for_user, event.id;
if session then session.log("debug", "Marking stanza as archived, archive_id: %s, stanza: %s", tostring(stanza_id), tostring(stanza:top_tag())); end
if not session then module:log("debug", "Marking stanza as archived in unknown session, archive_id: %s, stanza: %s", tostring(stanza_id), tostring(stanza:top_tag())); end
stanza._was_archived = true;
end);
module:hook("pre-resource-unbind", function (event)
local session, err = event.session, event.error;
if session.smacks then
if not session.resumption_token then
local queue = session.outgoing_stanza_queue;
if #queue > 0 then
session.log("debug", "Destroying session with %d unacked stanzas", #queue);
handle_unacked_stanzas(session);
end
else
session.log("debug", "mod_smacks hibernating session for up to %d seconds", resume_timeout);
local hibernate_time = os_time(); -- Track the time we went into hibernation
session.hibernating = hibernate_time;
local resumption_token = session.resumption_token;
module:fire_event("smacks-hibernation-start", {origin = session, queue = session.outgoing_stanza_queue});
timer.add_task(resume_timeout, function ()
session.log("debug", "mod_smacks hibernation timeout reached...");
-- We need to check the current resumption token for this resource
-- matches the smacks session this timer is for in case it changed
-- (for example, the client may have bound a new resource and
-- started a new smacks session, or not be using smacks)
local curr_session = full_sessions[session.full_jid];
if session.destroyed then
session.log("debug", "The session has already been destroyed");
elseif curr_session and curr_session.resumption_token == resumption_token
-- Check the hibernate time still matches what we think it is,
-- otherwise the session resumed and re-hibernated.
and session.hibernating == hibernate_time then
-- wait longer if the timeout isn't reached because push was enabled for this session
-- session.first_hibernated_push is the starting point for hibernation timeouts of those push enabled clients
-- wait for an additional resume_timeout seconds if no push occured since hibernation at all
local current_time = os_time();
local timeout_start = math_max(session.hibernating, session.first_hibernated_push or session.hibernating);
if session.push_identifier ~= nil and not session.first_hibernated_push then
session.log("debug", "No push happened since hibernation started, hibernating session for up to %d extra seconds", resume_timeout);
return resume_timeout;
end
if current_time-timeout_start < resume_timeout and session.push_identifier ~= nil then
session.log("debug", "A push happened since hibernation started, hibernating session for up to %d extra seconds", current_time-timeout_start);
return current_time-timeout_start; -- time left to wait
end
session.log("debug", "Destroying session for hibernating too long");
session_registry.set(session.username, session.resumption_token, nil);
-- save only actual h value and username/host (for security)
old_session_registry.set(session.username, session.resumption_token, {
h = session.handled_stanza_count,
username = session.username,
host = session.host
});
session.resumption_token = nil;
sessionmanager.destroy_session(session);
else
session.log("debug", "Session resumed before hibernation timeout, all is well")
end
end);
return true; -- Postpone destruction for now
end
end
end);
local function handle_s2s_destroyed(event)
local session = event.session;
local queue = session.outgoing_stanza_queue;
if queue and #queue > 0 then
session.log("warn", "Destroying session with %d unacked stanzas", #queue);
if s2s_resend then
for i = 1, #queue do
module:send(queue[i]);
end
session.outgoing_stanza_queue = nil;
else
handle_unacked_stanzas(session);
end
end
end
module:hook("s2sout-destroyed", handle_s2s_destroyed);
module:hook("s2sin-destroyed", handle_s2s_destroyed);
local function get_session_id(session)
return session.id or (tostring(session):match("[a-f0-9]+$"));
end
function handle_resume(session, stanza, xmlns_sm)
if session.full_jid then
session.log("warn", "Tried to resume after resource binding");
session.send(st.stanza("failed", { xmlns = xmlns_sm })
:tag("unexpected-request", { xmlns = xmlns_errors })
);
return true;
end
local id = stanza.attr.previd;
local original_session = session_registry.get(session.username, id);
if not original_session then
session.log("debug", "Tried to resume non-existent session with id %s", id);
local old_session = old_session_registry.get(session.username, id);
if old_session and session.username == old_session.username
and session.host == old_session.host
and old_session.h then
session.send(st.stanza("failed", { xmlns = xmlns_sm, h = string.format("%d", old_session.h) })
:tag("item-not-found", { xmlns = xmlns_errors })
);
else
session.send(st.stanza("failed", { xmlns = xmlns_sm })
:tag("item-not-found", { xmlns = xmlns_errors })
);
end;
elseif session.username == original_session.username
and session.host == original_session.host then
session.log("debug", "mod_smacks resuming existing session %s...", get_session_id(original_session));
original_session.log("debug", "mod_smacks session resumed from %s...", get_session_id(session));
-- TODO: All this should move to sessionmanager (e.g. session:replace(new_session))
if original_session.conn then
original_session.log("debug", "mod_smacks closing an old connection for this session");
local conn = original_session.conn;
c2s_sessions[conn] = nil;
conn:close();
end
local migrated_session_log = session.log;
original_session.ip = session.ip;
original_session.conn = session.conn;
original_session.send = session.send;
original_session.close = session.close;
original_session.filter = session.filter;
original_session.filter.session = original_session;
original_session.filters = session.filters;
original_session.stream = session.stream;
original_session.secure = session.secure;
original_session.hibernating = nil;
session.log = original_session.log;
session.type = original_session.type;
wrap_session(original_session, true);
-- Inform xmppstream of the new session (passed to its callbacks)
original_session.stream:set_session(original_session);
-- Similar for connlisteners
c2s_sessions[session.conn] = original_session;
original_session.send(st.stanza("resumed", { xmlns = xmlns_sm,
h = string.format("%d", original_session.handled_stanza_count), previd = id }));
-- Fake an <a> with the h of the <resume/> from the client
original_session:dispatch_stanza(st.stanza("a", { xmlns = xmlns_sm,
h = stanza.attr.h }));
-- Ok, we need to re-send any stanzas that the client didn't see
-- ...they are what is now left in the outgoing stanza queue
-- We have to use the send of "session" because we don't want to add our resent stanzas
-- to the outgoing queue again
local queue = original_session.outgoing_stanza_queue;
session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", #queue);
for i=1,#queue do
session.send(queue[i]);
end
session.log("debug", "all stanzas resent, now disabling send() in this migrated session, #queue = %d", #queue);
function session.send(stanza)
migrated_session_log("error", "Tried to send stanza on old session migrated by smacks resume (maybe there is a bug?): %s", tostring(stanza));
return false;
end
module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue});
request_ack_if_needed(original_session, true, "handle_resume");
else
module:log("warn", "Client %s@%s[%s] tried to resume stream for %s@%s[%s]",
session.username or "?", session.host or "?", session.type,
original_session.username or "?", original_session.host or "?", original_session.type);
session.send(st.stanza("failed", { xmlns = xmlns_sm })
:tag("not-authorized", { xmlns = xmlns_errors }));
end
return true;
end
module:hook_stanza(xmlns_sm2, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm2); end);
module:hook_stanza(xmlns_sm3, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm3); end);
local function handle_read_timeout(event)
local session = event.session;
if session.smacks then
if session.awaiting_ack then
if session.awaiting_ack_timer then
session.awaiting_ack_timer:stop();
end
if session.delayed_ack_timer then
session.delayed_ack_timer:stop();
session.delayed_ack_timer = nil;
end
return false; -- Kick the session
end
session.log("debug", "Sending <r> (read timeout)");
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }));
session.awaiting_ack = true;
if not session.delayed_ack_timer then
session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function()
delayed_ack_function(session);
end);
end
return true;
end
end
module:hook("s2s-read-timeout", handle_read_timeout);
module:hook("c2s-read-timeout", handle_read_timeout);
@@ -0,0 +1,7 @@
local speakerstats_component
= module:get_option_string(
"speakerstats_component", "speakerstats"..module.host);
-- Advertise speaker stats so client can pick up the address and start sending
-- dominant speaker events
module:add_identity("component", "speakerstats", speakerstats_component);
@@ -0,0 +1,237 @@
local get_room_from_jid = module:require "util".get_room_from_jid;
local room_jid_match_rewrite = module:require "util".room_jid_match_rewrite;
local is_healthcheck_room = module:require "util".is_healthcheck_room;
local jid_resource = require "util.jid".resource;
local ext_events = module:require "ext_events"
local st = require "util.stanza";
local socket = require "socket";
local json = require "util.json";
-- we use async to detect Prosody 0.10 and earlier
local have_async = pcall(require, "util.async");
if not have_async then
module:log("warn", "speaker stats will not work with Prosody version 0.10 or less.");
return;
end
local muc_component_host = module:get_option_string("muc_component");
if muc_component_host == nil then
log("error", "No muc_component specified. No muc to operate on!");
return;
end
log("info", "Starting speakerstats for %s", muc_component_host);
-- receives messages from client currently connected to the room
-- clients indicates their own dominant speaker events
function on_message(event)
-- Check the type of the incoming stanza to avoid loops:
if event.stanza.attr.type == "error" then
return; -- We do not want to reply to these, so leave.
end
local speakerStats
= event.stanza:get_child('speakerstats', 'http://jitsi.org/jitmeet');
if speakerStats then
local roomAddress = speakerStats.attr.room;
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
if not room then
log("warn", "No room found %s", roomAddress);
return false;
end
local roomSpeakerStats = room.speakerStats;
local from = event.stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(from);
if not occupant then
log("warn", "No occupant %s found for %s", from, roomAddress);
return false;
end
local newDominantSpeaker = roomSpeakerStats[occupant.jid];
local oldDominantSpeakerId = roomSpeakerStats['dominantSpeakerId'];
if oldDominantSpeakerId then
local oldDominantSpeaker = roomSpeakerStats[oldDominantSpeakerId];
if oldDominantSpeaker then
oldDominantSpeaker:setDominantSpeaker(false);
end
end
if newDominantSpeaker then
newDominantSpeaker:setDominantSpeaker(true);
end
room.speakerStats['dominantSpeakerId'] = occupant.jid;
end
return true
end
--- Start SpeakerStats implementation
local SpeakerStats = {};
SpeakerStats.__index = SpeakerStats;
function new_SpeakerStats(nick, context_user)
return setmetatable({
totalDominantSpeakerTime = 0;
_dominantSpeakerStart = 0;
nick = nick;
context_user = context_user;
displayName = nil;
}, SpeakerStats);
end
-- Changes the dominantSpeaker data for current occupant
-- saves start time if it is new dominat speaker
-- or calculates and accumulates time of speaking
function SpeakerStats:setDominantSpeaker(isNowDominantSpeaker)
log("debug",
"set isDominant %s for %s", tostring(isNowDominantSpeaker), self.nick);
if not self:isDominantSpeaker() and isNowDominantSpeaker then
self._dominantSpeakerStart = socket.gettime()*1000;
elseif self:isDominantSpeaker() and not isNowDominantSpeaker then
local now = socket.gettime()*1000;
local timeElapsed = math.floor(now - self._dominantSpeakerStart);
self.totalDominantSpeakerTime
= self.totalDominantSpeakerTime + timeElapsed;
self._dominantSpeakerStart = 0;
end
end
-- Returns true if the tracked user is currently a dominant speaker.
function SpeakerStats:isDominantSpeaker()
return self._dominantSpeakerStart > 0;
end
--- End SpeakerStats
-- create speakerStats for the room
function room_created(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
room.speakerStats = {};
end
-- Create SpeakerStats object for the joined user
function occupant_joined(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
local occupant = event.occupant;
local nick = jid_resource(occupant.nick);
if room.speakerStats then
-- lets send the current speaker stats to that user, so he can update
-- its local stats
if next(room.speakerStats) ~= nil then
local users_json = {};
for jid, values in pairs(room.speakerStats) do
-- skip reporting those without a nick('dominantSpeakerId')
-- and skip focus if sneaked into the table
if values.nick ~= nil and values.nick ~= 'focus' then
local resultSpeakerStats = {};
local totalDominantSpeakerTime
= values.totalDominantSpeakerTime;
-- before sending we need to calculate current dominant speaker
-- state
if values:isDominantSpeaker() then
local timeElapsed = math.floor(
socket.gettime()*1000 - values._dominantSpeakerStart);
totalDominantSpeakerTime = totalDominantSpeakerTime
+ timeElapsed;
end
resultSpeakerStats.displayName = values.displayName;
resultSpeakerStats.totalDominantSpeakerTime
= totalDominantSpeakerTime;
users_json[values.nick] = resultSpeakerStats;
end
end
local body_json = {};
body_json.type = 'speakerstats';
body_json.users = users_json;
local stanza = st.message({
from = module.host;
to = occupant.jid; })
:tag("json-message", {xmlns='http://jitsi.org/jitmeet'})
:text(json.encode(body_json)):up();
room:route_stanza(stanza);
end
local context_user = event.origin and event.origin.jitsi_meet_context_user or nil;
room.speakerStats[occupant.jid] = new_SpeakerStats(nick, context_user);
end
end
-- Occupant left set its dominant speaker to false and update the store the
-- display name
function occupant_leaving(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
local occupant = event.occupant;
local speakerStatsForOccupant = room.speakerStats[occupant.jid];
if speakerStatsForOccupant then
speakerStatsForOccupant:setDominantSpeaker(false);
-- set display name
local displayName = occupant:get_presence():get_child_text(
'nick', 'http://jabber.org/protocol/nick');
speakerStatsForOccupant.displayName = displayName;
end
end
-- Conference ended, send speaker stats
function room_destroyed(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
ext_events.speaker_stats(room, room.speakerStats);
end
module:hook("message/host", on_message);
-- executed on every host added internally in prosody, including components
function process_host(host)
if host == muc_component_host then -- the conference muc component
module:log("info","Hook to muc events on %s", host);
local muc_module = module:context(host);
muc_module:hook("muc-room-created", room_created, -1);
muc_module:hook("muc-occupant-joined", occupant_joined, -1);
muc_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
muc_module:hook("muc-room-destroyed", room_destroyed, -1);
end
end
if prosody.hosts[muc_component_host] == nil then
module:log("info","No muc component found, will listen for it: %s", muc_component_host)
-- when a host or component is added
prosody.events.add_handler("host-activated", process_host);
else
process_host(muc_component_host);
end
@@ -0,0 +1,113 @@
-- Token authentication
-- Copyright (C) 2015 Atlassian
local log = module._log;
local host = module.host;
local st = require "util.stanza";
local is_admin = require "core.usermanager".is_admin;
local parentHostName = string.gmatch(tostring(host), "%w+.(%w.+)")();
if parentHostName == nil then
log("error", "Failed to start - unable to get parent hostname");
return;
end
local parentCtx = module:context(parentHostName);
if parentCtx == nil then
log("error",
"Failed to start - unable to get parent context for host: %s",
tostring(parentHostName));
return;
end
local token_util = module:require "token/util".new(parentCtx);
-- no token configuration
if token_util == nil then
return;
end
log("debug",
"%s - starting MUC token verifier app_id: %s app_secret: %s allow empty: %s",
tostring(host), tostring(token_util.appId), tostring(token_util.appSecret),
tostring(token_util.allowEmptyToken));
-- option to disable room modification (sending muc config form) for guest that do not provide token
local require_token_for_moderation;
local function load_config()
require_token_for_moderation = module:get_option_boolean("token_verification_require_token_for_moderation");
end
load_config();
-- verify user and whether he is allowed to join a room based on the token information
local function verify_user(session, stanza)
log("debug", "Session token: %s, session room: %s",
tostring(session.auth_token),
tostring(session.jitsi_meet_room));
-- token not required for admin users
local user_jid = stanza.attr.from;
if is_admin(user_jid) then
log("debug", "Token not required from admin user: %s", user_jid);
return true;
end
log("debug",
"Will verify token for user: %s, room: %s ", user_jid, stanza.attr.to);
if not token_util:verify_room(session, stanza.attr.to) then
log("error", "Token %s not allowed to join: %s",
tostring(session.auth_token), tostring(stanza.attr.to));
session.send(
st.error_reply(
stanza, "cancel", "not-allowed", "Room and token mismatched"));
return false; -- we need to just return non nil
end
log("debug",
"allowed: %s to enter/create room: %s", user_jid, stanza.attr.to);
return true;
end
module:hook("muc-room-pre-create", function(event)
local origin, stanza = event.origin, event.stanza;
log("debug", "pre create: %s %s", tostring(origin), tostring(stanza));
if not verify_user(origin, stanza) then
return true; -- Returning any value other than nil will halt processing of the event
end
end);
module:hook("muc-occupant-pre-join", function(event)
local origin, room, stanza = event.origin, event.room, event.stanza;
log("debug", "pre join: %s %s", tostring(room), tostring(stanza));
if not verify_user(origin, stanza) then
return true; -- Returning any value other than nil will halt processing of the event
end
end);
for event_name, method in pairs {
-- Normal room interactions
["iq-set/bare/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ;
-- Host room
["iq-set/host/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ;
} do
module:hook(event_name, function (event)
local session, stanza = event.origin, event.stanza;
-- if we do not require token we pass it through(default behaviour)
-- or the request is coming from admin (focus)
if not require_token_for_moderation or is_admin(stanza.attr.from) then
return;
end
-- jitsi_meet_room is set after the token had been verified
if not session.auth_token or not session.jitsi_meet_room then
session.send(
st.error_reply(
stanza, "cancel", "not-allowed", "Room modification disabled for guests"));
return true;
end
end, -1); -- the default prosody hook is on -2
end
module:hook_global('config-reloaded', load_config);
@@ -0,0 +1,80 @@
-- XEP-0215 implementation for time-limited turn credentials
-- Copyright (C) 2012-2014 Philipp Hancke
-- This file is MIT/X11 licensed.
--turncredentials_secret = "keepthissecret";
--turncredentials = {
-- { type = "stun", host = "8.8.8.8" },
-- { type = "turn", host = "8.8.8.8", port = "3478" },
-- { type = "turn", host = "8.8.8.8", port = "80", transport = "tcp" }
--}
-- for stun servers, host is required, port defaults to 3478
-- for turn servers, host is required, port defaults to tcp,
-- transport defaults to udp
-- hosts can be a list of server names / ips for random
-- choice loadbalancing
local st = require "util.stanza";
local hmac_sha1 = require "util.hashes".hmac_sha1;
local base64 = require "util.encodings".base64;
local os_time = os.time;
local secret = module:get_option_string("turncredentials_secret");
local ttl = module:get_option_number("turncredentials_ttl", 86400);
local hosts = module:get_option("turncredentials") or {};
if not (secret) then
module:log("error", "turncredentials not configured");
return;
end
module:add_feature("urn:xmpp:extdisco:1");
function random(arr)
local index = math.random(1, #arr);
return arr[index];
end
module:hook_global("config-reloaded", function()
module:log("debug", "config-reloaded")
secret = module:get_option_string("turncredentials_secret");
ttl = module:get_option_number("turncredentials_ttl", 86400);
hosts = module:get_option("turncredentials") or {};
end);
module:hook("iq-get/host/urn:xmpp:extdisco:1:services", function(event)
local origin, stanza = event.origin, event.stanza;
if origin.type ~= "c2s" then
return;
end
local now = os_time() + ttl;
local userpart = tostring(now);
local nonce = base64.encode(hmac_sha1(secret, tostring(userpart), false));
local reply = st.reply(stanza):tag("services", {xmlns = "urn:xmpp:extdisco:1"})
for idx, item in pairs(hosts) do
if item.type == "stun" or item.type == "stuns" then
-- stun items need host and port (defaults to 3478)
reply:tag("service",
{ type = item.type, host = item.host, port = tostring(item.port) or "3478" }
):up();
elseif item.type == "turn" or item.type == "turns" then
local turn = {}
-- turn items need host, port (defaults to 3478),
-- transport (defaults to udp)
-- username, password, ttl
turn.type = item.type;
turn.port = tostring(item.port);
turn.transport = item.transport;
turn.username = userpart;
turn.password = nonce;
turn.ttl = tostring(ttl);
if item.hosts then
turn.host = random(item.hosts)
else
turn.host = item.host
end
reply:tag("service", turn):up();
end
end
origin.send(reply);
return true;
end);
@@ -0,0 +1,19 @@
# HG changeset patch
# User Matthew Wild <mwild1@gmail.com>
# Date 1579882890 0
# Node ID 37936c72846d77bb4b23c4987ccc9dc8805fe67c
# Parent b9a054ad38e72c0480534c06a7b4397c048d122a
mod_websocket: Fire event on session creation (thanks Aaron van Meerten)
diff -r b9a054ad38e7 -r 37936c72846d plugins/mod_websocket.lua
--- a/plugins/mod_websocket.lua Thu Jan 23 21:59:13 2020 +0000
+++ b/plugins/mod_websocket.lua Fri Jan 24 16:21:30 2020 +0000
@@ -305,6 +305,8 @@
response.headers.sec_webSocket_accept = base64(sha1(request.headers.sec_websocket_key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
response.headers.sec_webSocket_protocol = "xmpp";
+ module:fire_event("websocket-session", { session = session, request = request });
+
session.log("debug", "Sending WebSocket handshake");
return "";
@@ -0,0 +1,101 @@
--- mod_websocket.lua
+++ mod_websocket.lua
@@ -163,34 +163,34 @@ function handle_request(event)
return 403;
end
- local function websocket_close(code, message)
+ local function websocket_close(conn, code, message)
conn:write(build_close(code, message));
conn:close();
end
local dataBuffer;
- local function handle_frame(frame)
+ local function handle_frame(conn, frame)
local opcode = frame.opcode;
local length = frame.length;
module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
-- Error cases
if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
- websocket_close(1002, "Reserved bits not zero");
+ websocket_close(conn, 1002, "Reserved bits not zero");
return false;
end
if opcode == 0x8 then -- close frame
if length == 1 then
- websocket_close(1002, "Close frame with payload, but too short for status code");
+ websocket_close(conn, 1002, "Close frame with payload, but too short for status code");
return false;
elseif length >= 2 then
local status_code = parse_close(frame.data)
if status_code < 1000 then
- websocket_close(1002, "Closed with invalid status code");
+ websocket_close(conn, 1002, "Closed with invalid status code");
return false;
elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
- websocket_close(1002, "Closed with reserved status code");
+ websocket_close(conn, 1002, "Closed with reserved status code");
return false;
end
end
@@ -198,28 +198,28 @@ function handle_request(event)
if opcode >= 0x8 then
if length > 125 then -- Control frame with too much payload
- websocket_close(1002, "Payload too large");
+ websocket_close(conn, 1002, "Payload too large");
return false;
end
if not frame.FIN then -- Fragmented control frame
- websocket_close(1002, "Fragmented control frame");
+ websocket_close(conn, 1002, "Fragmented control frame");
return false;
end
end
if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then
- websocket_close(1002, "Reserved opcode");
+ websocket_close(conn, 1002, "Reserved opcode");
return false;
end
if opcode == 0x0 and not dataBuffer then
- websocket_close(1002, "Unexpected continuation frame");
+ websocket_close(conn, 1002, "Unexpected continuation frame");
return false;
end
if (opcode == 0x1 or opcode == 0x2) and dataBuffer then
- websocket_close(1002, "Continuation frame expected");
+ websocket_close(conn, 1002, "Continuation frame expected");
return false;
end
@@ -229,11 +229,11 @@ function handle_request(event)
elseif opcode == 0x1 then -- Text frame
dataBuffer = {frame.data};
elseif opcode == 0x2 then -- Binary frame
- websocket_close(1003, "Only text frames are supported");
+ websocket_close(conn, 1003, "Only text frames are supported");
return;
elseif opcode == 0x8 then -- Close request
- websocket_close(1000, "Goodbye");
- return;
+ websocket_close(conn, 1000, "Goodbye");
+ return "";
elseif opcode == 0x9 then -- Ping frame
frame.opcode = 0xA;
conn:write(build_frame(frame));
@@ -276,7 +276,7 @@ function handle_request(event)
while frame do
frameBuffer = frameBuffer:sub(length + 1);
- local result = handle_frame(frame);
+ local result = handle_frame(session.conn, frame);
if not result then return; end
cache[#cache+1] = filter_open_close(result);
frame, length = parse_frame(frameBuffer);
@@ -0,0 +1,21 @@
--- muc.lib.lua 2016-10-26 18:26:53.432377291 +0000
+++ muc.lib.lua 2016-10-26 18:41:40.754426072 +0000
@@ -1256,15 +1256,16 @@
if actor == true then
actor = nil -- So we can pass it safely to 'publicise_occupant_status' below
else
+ local actor_affiliation = self:get_affiliation(actor);
+
-- Can't do anything to other owners or admins
local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
- if occupant_affiliation == "owner" or occupant_affiliation == "admin" then
+ if (occupant_affiliation == "owner" and actor_affiliation ~= "owner") or (occupant_affiliation == "admin" and actor_affiliation ~= "admin" and actor_affiliation ~= "owner") then
return nil, "cancel", "not-allowed";
end
-- If you are trying to give or take moderator role you need to be an owner or admin
if occupant.role == "moderator" or role == "moderator" then
- local actor_affiliation = self:get_affiliation(actor);
if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then
return nil, "cancel", "not-allowed";
end
@@ -0,0 +1,397 @@
local inspect = require("inspect")
local jid = require("util.jid")
local stanza = require("util.stanza")
local timer = require("util.timer")
local update_presence_identity = module:require("util").update_presence_identity
local uuid = require("util.uuid")
local component = module:get_option_string(
"poltergeist_component",
module.host
)
local expiration_timeout = module:get_option_string(
"poltergeist_leave_timeout",
30 -- defaults to 30 seconds
)
local MUC_NS = "http://jabber.org/protocol/muc"
--------------------------------------------------------------------------------
-- Utility functions for commonly used poltergeist codes.
--------------------------------------------------------------------------------
-- Creates a nick for a poltergeist.
-- @param username is the unique username of the poltergeist
-- @return a nick to use for xmpp
local function create_nick(username)
return string.sub(username, 0,8)
end
-- Returns the last presence of the occupant.
-- @param room the room instance where to check for occupant
-- @param nick the nick of the occupant
-- @return presence stanza of the occupant
function get_presence(room, nick)
local occupant_jid = room:get_occupant_jid(component.."/"..nick)
if occupant_jid then
return room:get_occupant_by_nick(occupant_jid):get_presence();
end
return nil;
end
-- Checks for existance of a poltergeist occupant in a room.
-- @param room the room instance where to check for the occupant
-- @param nick the nick of the occupant
-- @return true if occupant is found, false otherwise
function occupies(room, nick)
-- Find out if we have a poltergeist occupant in the room for this JID
return not not room:get_occupant_jid(component.."/"..nick);
end
--------------------------------------------------------------------------------
-- Username storage for poltergeist.
--
-- Every poltergeist will have a username stored in a table underneath
-- the room name that they are currently active in. The username can
-- be retrieved given a room and a user_id. The username is removed from
-- a room by providing the room and the nick.
--
-- A table with a single entry looks like:
-- {
-- ["[hug]hostilewerewolvesthinkslightly"] = {
-- ["655363:52148a3e-b5fb-4cfc-8fbd-f55e793cf657"] = "ed7757d6-d88d-4e6a-8e24-aca2adc31348",
-- ed7757d6 = "655363:52148a3e-b5fb-4cfc-8fbd-f55e793cf657"
-- }
-- }
--------------------------------------------------------------------------------
-- state is the table where poltergeist usernames and call resources are stored
-- for a given xmpp muc.
local state = module:shared("state")
-- Adds a poltergeist to the store.
-- @param room is the room the poltergeist is being added to
-- @param user_id is the user_id of the user the poltergeist represents
-- @param username is the unique id of the poltergeist itself
local function store_username(room, user_id, username)
local room_name = jid.node(room.jid)
if not state[room_name] then
state[room_name] = {}
end
state[room_name][user_id] = username
state[room_name][create_nick(username)] = user_id
end
-- Retrieves a poltergeist username from the store if one exists.
-- @param room is the room to check for the poltergeist in the store
-- @param user_id is the user id of the user the poltergeist represents
local function get_username(room, user_id)
local room_name = jid.node(room.jid)
if not state[room_name] then
return nil
end
return state[room_name][user_id]
end
local function get_username_from_nick(room_name, nick)
if not state[room_name] then
return nil
end
local user_id = state[room_name][nick]
return state[room_name][user_id]
end
-- Removes the username from the store.
-- @param room is the room the poltergeist is being removed from
-- @param nick is the nick of the muc occupant
local function remove_username(room, nick)
local room_name = jid.node(room.jid)
if not state[room_name] then
return
end
local user_id = state[room_name][nick]
state[room_name][user_id] = nil
state[room_name][nick] = nil
end
-- Removes all poltergeists in the store for the provided room.
-- @param room is the room all poltergiest will be removed from
local function remove_room(room)
local room_name = jid.node(room.jid)
if state[room_name] then
state[room_name] = nil
end
end
-- Adds a resource that is associated with a a call in a room. There
-- is only one resource for each type.
-- @param room is the room the call and poltergeist is in.
-- @param call_id is the unique id for the call.
-- @param resource_type is type of resource being added.
-- @param resource_id is the id of the resource being added.
local function add_call_resource(room, call_id, resource_type, resource_id)
local room_name = jid.node(room.jid)
if not state[room_name] then
state[room_name] = {}
end
if not state[room_name][call_id] then
state[room_name][call_id] = {}
end
state[room_name][call_id][resource_type] = resource_id
end
--------------------------------------------------------------------------------
-- State for toggling the tagging of presence stanzas with ignored tag.
--
-- A poltergeist with it's full room/nick set to ignore will have a jitsi ignore
-- tag applied to all presence stanza's broadcasted. The following funcitons
-- assisst in managing this state.
--------------------------------------------------------------------------------
local presence_ignored = {}
-- Sets the nick to ignored state.
-- @param room_nick full room/nick jid
local function set_ignored(room_nick)
presence_ignored[room_nick] = true
end
-- Resets the nick out of ignored state.
-- @param room_nick full room/nick jid
local function reset_ignored(room_nick)
presence_ignored[room_nick] = nil
end
-- Determines whether or not the leave presence should be tagged with ignored.
-- @param room_nick full room/nick jid
local function should_ignore(room_nick)
if presence_ignored[room_nick] == nil then
return false
end
return presence_ignored[room_nick]
end
--------------------------------------------------------------------------------
-- Poltergeist control functions for adding, updating and removing poltergeist.
--------------------------------------------------------------------------------
-- Updates the status tags and call flow tags of an existing poltergeist
-- presence.
-- @param presence_stanza is the actual presence stanza for a poltergeist.
-- @param status is the new status to be updated in the stanza.
-- @param call_details is a table of call flow signal information.
function update_presence_tags(presence_stanza, status, call_details)
local call_cancel = false
local call_id = nil
-- Extract optional call flow signal information.
if call_details then
call_id = call_details["id"]
if call_details["cancel"] then
call_cancel = call_details["cancel"]
end
end
presence_stanza:maptags(function (tag)
if tag.name == "status" then
if call_cancel then
-- If call cancel is set then the status should not be changed.
return tag
end
return stanza.stanza("status"):text(status)
elseif tag.name == "call_id" then
if call_id then
return stanza.stanza("call_id"):text(call_id)
else
-- If no call id is provided the re-use the existing id.
return tag
end
elseif tag.name == "call_cancel" then
if call_cancel then
return stanza.stanza("call_cancel"):text("true")
else
return stanza.stanza("call_cancel"):text("false")
end
end
return tag
end)
return presence_stanza
end
-- Updates the presence status of a poltergeist.
-- @param room is the room the poltergeist has occupied
-- @param nick is the xmpp nick of the poltergeist occupant
-- @param status is the status string to set in the presence
-- @param call_details is a table of call flow control details
local function update(room, nick, status, call_details)
local original_presence = get_presence(room, nick)
if not original_presence then
module:log("info", "update issued for a non-existing poltergeist")
return
end
-- update occupant presence with appropriate to and from
-- so we can send it again
update_presence = stanza.clone(original_presence)
update_presence.attr.to = room.jid.."/"..nick
update_presence.attr.from = component.."/"..nick
update_presence = update_presence_tags(update_presence, status, call_details)
module:log("info", "updating poltergeist: %s/%s - %s", room, nick, status)
room:handle_normal_presence(
prosody.hosts[component],
update_presence
)
end
-- Removes the poltergeist from the room.
-- @param room is the room the poltergeist has occupied
-- @param nick is the xmpp nick of the poltergeist occupant
-- @param ignore toggles if the leave subsequent leave presence should be tagged
local function remove(room, nick, ignore)
local original_presence = get_presence(room, nick);
if not original_presence then
module:log("info", "attempted to remove a poltergeist with no presence")
return
end
local leave_presence = stanza.clone(original_presence)
leave_presence.attr.to = room.jid.."/"..nick
leave_presence.attr.from = component.."/"..nick
leave_presence.attr.type = "unavailable"
if (ignore) then
set_ignored(room.jid.."/"..nick)
end
remove_username(room, nick)
module:log("info", "removing poltergeist: %s/%s", room, nick)
room:handle_normal_presence(
prosody.hosts[component],
leave_presence
)
end
-- Adds a poltergeist to a muc/room.
-- @param room is the room the poltergeist will occupy
-- @param is the id of the user the poltergeist represents
-- @param display_name is the display name to use for the poltergeist
-- @param avatar is the avatar link used for the poltergeist display
-- @param context is the session context of the user making the request
-- @param status is the presence status string to use
-- @param resources is a table of resource types and resource ids to correlate.
local function add_to_muc(room, user_id, display_name, avatar, context, status, resources)
local username = uuid.generate()
local presence_stanza = original_presence(
room,
username,
display_name,
avatar,
context,
status
)
module:log("info", "adding poltergeist: %s/%s", room, create_nick(username))
store_username(room, user_id, username)
for k, v in pairs(resources) do
add_call_resource(room, username, k, v)
end
room:handle_first_presence(
prosody.hosts[component],
presence_stanza
)
local remove_delay = 5
local expiration = expiration_timeout - remove_delay;
local nick = create_nick(username)
timer.add_task(
expiration,
function ()
update(room, nick, "expired")
timer.add_task(
remove_delay,
function ()
if occupies(room, nick) then
remove(room, nick, false)
end
end
)
end
)
end
-- Generates an original presence for a new poltergeist
-- @param room is the room the poltergeist will occupy
-- @param username is the unique name for the poltergeist
-- @param display_name is the display name to use for the poltergeist
-- @param avatar is the avatar link used for the poltergeist display
-- @param context is the session context of the user making the request
-- @param status is the presence status string to use
-- @return a presence stanza that can be used to add the poltergeist to the muc
function original_presence(room, username, display_name, avatar, context, status)
local nick = create_nick(username)
local p = stanza.presence({
to = room.jid.."/"..nick,
from = component.."/"..nick,
}):tag("x", { xmlns = MUC_NS }):up();
p:tag("bot", { type = "poltergeist" }):up();
p:tag("call_cancel"):text(nil):up();
p:tag("call_id"):text(username):up();
if status then
p:tag("status"):text(status):up();
else
p:tag("status"):text(nil):up();
end
if display_name then
p:tag(
"nick",
{ xmlns = "http://jabber.org/protocol/nick" }):text(display_name):up();
end
if avatar then
p:tag("avatar-url"):text(avatar):up();
end
-- If the room has a password set, let the poltergeist enter using it
local room_password = room:get_password();
if room_password then
local join = p:get_child("x", MUC_NS);
join:tag("password", { xmlns = MUC_NS }):text(room_password);
end
update_presence_identity(
p,
context.user,
context.group,
context.creator_user,
context.creator_group
)
return p
end
return {
get_username = get_username,
get_username_from_nick = get_username_from_nick,
occupies = occupies,
remove_room = remove_room,
reset_ignored = reset_ignored,
should_ignore = should_ignore,
create_nick = create_nick,
add_to_muc = add_to_muc,
update = update,
remove = remove
}
@@ -0,0 +1,405 @@
-- Token authentication
-- Copyright (C) 2015 Atlassian
local basexx = require "basexx";
local have_async, async = pcall(require, "util.async");
local hex = require "util.hex";
local jwt = require "luajwtjitsi";
local jid = require "util.jid";
local json_safe = require "cjson.safe";
local path = require "util.paths";
local sha256 = require "util.hashes".sha256;
local http_get_with_retry = module:require "util".http_get_with_retry;
local nr_retries = 3;
-- TODO: Figure out a less arbitrary default cache size.
local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128);
local Util = {}
Util.__index = Util
--- Constructs util class for token verifications.
-- Constructor that uses the passed module to extract all the
-- needed configurations.
-- If confuguration is missing returns nil
-- @param module the module in which options to check for configs.
-- @return the new instance or nil
function Util.new(module)
local self = setmetatable({}, Util)
self.appId = module:get_option_string("app_id");
self.appSecret = module:get_option_string("app_secret");
self.asapKeyServer = module:get_option_string("asap_key_server");
self.allowEmptyToken = module:get_option_boolean("allow_empty_token");
self.cache = require"util.cache".new(cacheSize);
--[[
Multidomain can be supported in some deployments. In these deployments
there is a virtual conference muc, which address contains the subdomain
to use. Those deployments are accessible
by URL https://domain/subdomain.
Then the address of the room will be:
roomName@conference.subdomain.domain. This is like a virtual address
where there is only one muc configured by default with address:
conference.domain and the actual presentation of the room in that muc
component is [subdomain]roomName@conference.domain.
These setups relay on configuration 'muc_domain_base' which holds
the main domain and we use it to substract subdomains from the
virtual addresses.
The following confgurations are for multidomain setups and domain name
verification:
--]]
-- optional parameter for custom muc component prefix,
-- defaults to "conference"
self.muc_domain_prefix = module:get_option_string(
"muc_mapper_domain_prefix", "conference");
-- domain base, which is the main domain used in the deployment,
-- the main VirtualHost for the deployment
self.muc_domain_base = module:get_option_string("muc_mapper_domain_base");
-- The "real" MUC domain that we are proxying to
if self.muc_domain_base then
self.muc_domain = module:get_option_string(
"muc_mapper_domain",
self.muc_domain_prefix.."."..self.muc_domain_base);
end
-- whether domain name verification is enabled, by default it is disabled
self.enableDomainVerification = module:get_option_boolean(
"enable_domain_verification", false);
if self.allowEmptyToken == true then
module:log("warn", "WARNING - empty tokens allowed");
end
if self.appId == nil then
module:log("error", "'app_id' must not be empty");
return nil;
end
if self.appSecret == nil and self.asapKeyServer == nil then
module:log("error", "'app_secret' or 'asap_key_server' must be specified");
return nil;
end
--array of accepted issuers: by default only includes our appId
self.acceptedIssuers = module:get_option_array('asap_accepted_issuers',{self.appId})
--array of accepted audiences: by default only includes our appId
self.acceptedAudiences = module:get_option_array('asap_accepted_audiences',{'*'})
self.requireRoomClaim = module:get_option_boolean('asap_require_room_claim', true);
if self.asapKeyServer and not have_async then
module:log("error", "requires a version of Prosody with util.async");
return nil;
end
return self
end
function Util:set_asap_key_server(asapKeyServer)
self.asapKeyServer = asapKeyServer;
end
function Util:set_asap_accepted_issuers(acceptedIssuers)
self.acceptedIssuers = acceptedIssuers;
end
function Util:set_asap_accepted_audiences(acceptedAudiences)
self.acceptedAudiences = acceptedAudiences;
end
function Util:set_asap_require_room_claim(checkRoom)
self.requireRoomClaim = checkRoom;
end
function Util:clear_asap_cache()
self.cache = require"util.cache".new(cacheSize);
end
--- Returns the public key by keyID
-- @param keyId the key ID to request
-- @return the public key (the content of requested resource) or nil
function Util:get_public_key(keyId)
local content = self.cache:get(keyId);
if content == nil then
-- If the key is not found in the cache.
module:log("debug", "Cache miss for key: "..keyId);
local keyurl = path.join(self.asapKeyServer, hex.to(sha256(keyId))..'.pem');
module:log("debug", "Fetching public key from: "..keyurl);
content = http_get_with_retry(keyurl, nr_retries);
if content ~= nil then
self.cache:set(keyId, content);
end
return content;
else
-- If the key is in the cache, use it.
module:log("debug", "Cache hit for key: "..keyId);
return content;
end
end
--- Verifies issuer part of token
-- @param 'iss' claim from the token to verify
-- @param 'acceptedIssuers' list of issuers to check
-- @return nil and error string or true for accepted claim
function Util:verify_issuer(issClaim, acceptedIssuers)
if not acceptedIssuers then
acceptedIssuers = self.acceptedIssuers
end
module:log("debug","verify_issuer claim: %s against accepted: %s",issClaim, acceptedIssuers);
for i, iss in ipairs(acceptedIssuers) do
if issClaim == iss then
--claim matches an accepted issuer so return success
return true;
end
end
--if issClaim not found in acceptedIssuers, fail claim
return nil, "Invalid issuer ('iss' claim)";
end
--- Verifies audience part of token
-- @param 'aud' claim from the token to verify
-- @return nil and error string or true for accepted claim
function Util:verify_audience(audClaim)
module:log("debug","verify_audience claim: %s against accepted: %s",audClaim, self.acceptedAudiences);
for i, aud in ipairs(self.acceptedAudiences) do
if aud == '*' then
--* indicates to accept any audience in the claims so return success
return true;
end
if audClaim == aud then
--claim matches an accepted audience so return success
return true;
end
end
--if issClaim not found in acceptedIssuers, fail claim
return nil, "Invalid audience ('aud' claim)";
end
--- Verifies token
-- @param token the token to verify
-- @param secret the secret to use to verify token
-- @param acceptedIssuers the list of accepted issuers to check
-- @return nil and error or the extracted claims from the token
function Util:verify_token(token, secret, acceptedIssuers)
local claims, err = jwt.decode(token, secret, true);
if claims == nil then
return nil, err;
end
local alg = claims["alg"];
if alg ~= nil and (alg == "none" or alg == "") then
return nil, "'alg' claim must not be empty";
end
local issClaim = claims["iss"];
if issClaim == nil then
return nil, "'iss' claim is missing";
end
--check the issuer against the accepted list
local issCheck, issCheckErr = self:verify_issuer(issClaim, acceptedIssuers);
if issCheck == nil then
return nil, issCheckErr;
end
if self.requireRoomClaim then
local roomClaim = claims["room"];
if roomClaim == nil then
return nil, "'room' claim is missing";
end
end
local audClaim = claims["aud"];
if audClaim == nil then
return nil, "'aud' claim is missing";
end
--check the audience against the accepted list
local audCheck, audCheckErr = self:verify_audience(audClaim);
if audCheck == nil then
return nil, audCheckErr;
end
return claims;
end
--- Verifies token and process needed values to be stored in the session.
-- Token is obtained from session.auth_token.
-- Stores in session the following values:
-- session.jitsi_meet_room - the room name value from the token
-- session.jitsi_meet_domain - the domain name value from the token
-- session.jitsi_meet_context_user - the user details from the token
-- session.jitsi_meet_context_group - the group value from the token
-- session.jitsi_meet_context_features - the features value from the token
-- @param session the current session
-- @param acceptedIssuers optional list of accepted issuers to check
-- @return false and error
function Util:process_and_verify_token(session, acceptedIssuers)
if not acceptedIssuers then
acceptedIssuers = self.acceptedIssuers;
end
if session.auth_token == nil then
if self.allowEmptyToken then
return true;
else
return false, "not-allowed", "token required";
end
end
local pubKey;
if session.public_key then
module:log("debug","Public key was found on the session");
pubKey = session.public_key;
elseif self.asapKeyServer and session.auth_token ~= nil then
local dotFirst = session.auth_token:find("%.");
if not dotFirst then return nil, "Invalid token" end
local header, err = json_safe.decode(basexx.from_url64(session.auth_token:sub(1,dotFirst-1)));
if err then
return false, "not-allowed", "bad token format";
end
local kid = header["kid"];
if kid == nil then
return false, "not-allowed", "'kid' claim is missing";
end
pubKey = self:get_public_key(kid);
if pubKey == nil then
return false, "not-allowed", "could not obtain public key";
end
end
-- now verify the whole token
local claims, msg;
if self.asapKeyServer then
claims, msg = self:verify_token(session.auth_token, pubKey, acceptedIssuers);
else
claims, msg = self:verify_token(session.auth_token, self.appSecret, acceptedIssuers);
end
if claims ~= nil then
-- Binds room name to the session which is later checked on MUC join
session.jitsi_meet_room = claims["room"];
-- Binds domain name to the session
session.jitsi_meet_domain = claims["sub"];
-- Binds the user details to the session if available
if claims["context"] ~= nil then
if claims["context"]["user"] ~= nil then
session.jitsi_meet_context_user = claims["context"]["user"];
end
if claims["context"]["group"] ~= nil then
-- Binds any group details to the session
session.jitsi_meet_context_group = claims["context"]["group"];
end
if claims["context"]["features"] ~= nil then
-- Binds any features details to the session
session.jitsi_meet_context_features = claims["context"]["features"];
end
end
return true;
else
return false, "not-allowed", msg;
end
end
--- Verifies room name and domain if necesarry.
-- Checks configs and if necessary checks the room name extracted from
-- room_address against the one saved in the session when token was verified.
-- Also verifies domain name from token against the domain in the room_address,
-- if enableDomainVerification is enabled.
-- @param session the current session
-- @param room_address the whole room address as received
-- @return returns true in case room was verified or there is no need to verify
-- it and returns false in case verification was processed
-- and was not successful
function Util:verify_room(session, room_address)
if self.allowEmptyToken and session.auth_token == nil then
module:log(
"debug",
"Skipped room token verification - empty tokens are allowed");
return true;
end
-- extract room name using all chars, except the not allowed ones
local room,_,_ = jid.split(room_address);
if room == nil then
log("error",
"Unable to get name of the MUC room ? to: %s", room_address);
return true;
end
local auth_room = session.jitsi_meet_room;
if not self.enableDomainVerification then
-- if auth_room is missing, this means user is anonymous (no token for
-- its domain) we let it through, jicofo is verifying creation domain
if auth_room and room ~= string.lower(auth_room) and auth_room ~= '*' then
return false;
end
return true;
end
local room_address_to_verify = jid.bare(room_address);
local room_node = jid.node(room_address);
-- parses bare room address, for multidomain expected format is:
-- [subdomain]roomName@conference.domain
local target_subdomain, target_room = room_node:match("^%[([^%]]+)%](.+)$");
-- if we have '*' as room name in token, this means all rooms are allowed
-- so we will use the actual name of the room when constructing strings
-- to verify subdomains and domains to simplify checks
local room_to_check;
if auth_room == '*' then
-- authorized for accessing any room assign to room_to_check the actual
-- room name
if target_room ~= nil then
-- we are in multidomain mode and we were able to extract room name
room_to_check = target_room;
else
-- no target_room, room_address_to_verify does not contain subdomain
-- so we get just the node which is the room name
room_to_check = room_node;
end
else
-- no wildcard, so check room against authorized room in token
room_to_check = auth_room;
end
local auth_domain = session.jitsi_meet_domain;
local subdomain_to_check;
if target_subdomain then
if auth_domain == '*' then
-- check for wildcard in JWT claim, allow access if found
subdomain_to_check = target_subdomain;
else
-- no wildcard in JWT claim, so check subdomain against sub in token
subdomain_to_check = auth_domain;
end
-- from this point we depend on muc_domain_base,
-- deny access if option is missing
if not self.muc_domain_base then
module:log("warn", "No 'muc_domain_base' option set, denying access!");
return false;
end
return room_address_to_verify == jid.join(
"["..string.lower(subdomain_to_check).."]"..string.lower(room_to_check), self.muc_domain);
else
if auth_domain == '*' then
-- check for wildcard in JWT claim, allow access if found
subdomain_to_check = self.muc_domain;
else
-- no wildcard in JWT claim, so check subdomain against sub in token
subdomain_to_check = self.muc_domain_prefix.."."..auth_domain;
end
-- we do not have a domain part (multidomain is not enabled)
-- verify with info from the token
return room_address_to_verify == jid.join(
string.lower(room_to_check), string.lower(subdomain_to_check));
end
end
return Util;
+295
View File
@@ -0,0 +1,295 @@
local jid = require "util.jid";
local timer = require "util.timer";
local http = require "net.http";
local http_timeout = 30;
local have_async, async = pcall(require, "util.async");
local http_headers = {
["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")"
};
local muc_domain_prefix
= module:get_option_string("muc_mapper_domain_prefix", "conference");
-- defaults to module.host, the module that uses the utility
local muc_domain_base
= module:get_option_string("muc_mapper_domain_base", module.host);
-- The "real" MUC domain that we are proxying to
local muc_domain = module:get_option_string(
"muc_mapper_domain", muc_domain_prefix.."."..muc_domain_base);
local escaped_muc_domain_base = muc_domain_base:gsub("%p", "%%%1");
local escaped_muc_domain_prefix = muc_domain_prefix:gsub("%p", "%%%1");
-- The pattern used to extract the target subdomain
-- (e.g. extract 'foo' from 'foo.muc.example.com')
local target_subdomain_pattern
= "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
-- Utility function to split room JID to include room name and subdomain
local function room_jid_split_subdomain(room_jid)
local node, host, resource = jid.split(room_jid);
local target_subdomain = host and host:match(target_subdomain_pattern);
return node, host, resource, target_subdomain
end
--- Utility function to check and convert a room JID from
-- virtual room1@muc.foo.example.com to real [foo]room1@muc.example.com
-- @param room_jid the room jid to match and rewrite if needed
-- @return returns room jid [foo]room1@muc.example.com when it has subdomain
-- otherwise room1@muc.example.com(the room_jid value untouched)
local function room_jid_match_rewrite(room_jid)
local node, host, resource, target_subdomain = room_jid_split_subdomain(room_jid);
if not target_subdomain then
module:log("debug", "No need to rewrite out 'to' %s", room_jid);
return room_jid;
end
-- Ok, rewrite room_jid address to new format
local new_node, new_host, new_resource
= "["..target_subdomain.."]"..node, muc_domain, resource;
room_jid = jid.join(new_node, new_host, new_resource);
module:log("debug", "Rewrote to %s", room_jid);
return room_jid
end
local function internal_room_jid_match_rewrite(room_jid)
local node, host, resource = jid.split(room_jid);
if host ~= muc_domain or not node then
module:log("debug", "No need to rewrite %s (not from the MUC host)", room_jid);
return room_jid;
end
local target_subdomain, target_node = node:match("^%[([^%]]+)%](.+)$");
if not (target_node and target_subdomain) then
module:log("debug", "Not rewriting... unexpected node format: %s", node);
return room_jid;
end
-- Ok, rewrite room_jid address to pretty format
local new_node, new_host, new_resource = target_node, muc_domain_prefix..".".. target_subdomain.."."..muc_domain_base, resource;
room_jid = jid.join(new_node, new_host, new_resource);
module:log("debug", "Rewrote to %s", room_jid);
return room_jid
end
--- Finds and returns room by its jid
-- @param room_jid the room jid to search in the muc component
-- @return returns room if found or nil
function get_room_from_jid(room_jid)
local _, host = jid.split(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)
return muc.get_room_from_jid(room_jid);
else
return
end
end
end
function async_handler_wrapper(event, handler)
if not have_async then
module:log("error", "requires a version of Prosody with util.async");
return nil;
end
local runner = async.runner;
-- Grab a local response so that we can send the http response when
-- the handler is done.
local response = event.response;
local async_func = runner(
function (event)
local result = handler(event)
-- If there is a status code in the result from the
-- wrapped handler then add it to the response.
if tonumber(result.status_code) ~= nil then
response.status_code = result.status_code
end
-- If there are headers in the result from the
-- wrapped handler then add them to the response.
if result.headers ~= nil then
response.headers = result.headers
end
-- Send the response to the waiting http client with
-- or without the body from the wrapped handler.
if result.body ~= nil then
response:send(result.body)
else
response:send();
end
end
)
async_func:run(event)
-- return true to keep the client http connection open.
return true;
end
--- Updates presence stanza, by adding identity node
-- @param stanza the presence stanza
-- @param user the user to which presence we are updating identity
-- @param group the group of the user to which presence we are updating identity
-- @param creator_user the user who created the user which presence we
-- are updating (this is the poltergeist case, where a user creates
-- a poltergeist), optional.
-- @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)
-- First remove any 'identity' element if it already
-- exists, so it cannot be spoofed by a client
stanza:maptags(
function(tag)
for k, v in pairs(tag) do
if k == "name" and v == "identity" then
return nil
end
end
return tag
end
)
module:log("debug",
"Presence after previous identity stripped: %s", tostring(stanza));
stanza:tag("identity"):tag("user");
for k, v in pairs(user) do
stanza:tag(k):text(v):up();
end
stanza:up();
-- Add the group information if it is present
if group then
stanza:tag("group"):text(group):up();
end
-- Add the creator user information if it is present
if creator_user then
stanza:tag("creator_user");
for k, v in pairs(creator_user) do
stanza:tag(k):text(v):up();
end
stanza:up();
-- Add the creator group information if it is present
if creator_group then
stanza:tag("creator_group"):text(creator_group):up();
end
stanza:up();
end
module:log("debug",
"Presence with identity inserted %s", tostring(stanza))
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(session, feature)
if (session.jitsi_meet_context_features == nil
or session.jitsi_meet_context_features[feature] == "true") then
return true;
else
return false;
end
end
function starts_with(str, start)
return str:sub(1, #start) == start
end
-- healthcheck rooms in jicofo starts with a string '__jicofo-health-check'
function is_healthcheck_room(room_jid)
if starts_with(room_jid, "__jicofo-health-check") then
return true;
end
return false;
end
-- Utility function to make an http get request and
-- retry @param retry number of times
-- @param url endpoint to be called
-- @param retry nr of retries, if retry is
-- nil there will be no retries
-- @returns result of the http call or nil if
-- the external call failed after the last retry
function http_get_with_retry(url, retry)
local content, code;
local timeout_occurred;
local wait, done = async.waiter();
local function cb(content_, code_, response_, request_)
if timeout_occurred == nil then
code = code_;
if code == 200 or code == 204 then
module:log("debug", "External call was successful, content %s", content_);
content = content_
else
module:log("warn", "Error on public key request: Code %s, Content %s",
code_, content_);
end
done();
else
module:log("warn", "External call reply delivered after timeout from: %s", url);
end
end
local function call_http()
return http.request(url, {
headers = http_headers or {},
method = "GET"
}, cb);
end
local request = call_http();
local function cancel()
-- TODO: This check is racey. Not likely to be a problem, but we should
-- still stick a mutex on content / code at some point.
if code == nil then
timeout_occurred = true;
module:log("warn", "Timeout %s seconds making the external call to: %s", http_timeout, url);
-- no longer present in prosody 0.11, so check before calling
if http.destroy_request ~= nil then
http.destroy_request(request);
end
if retry == nil then
module:log("debug", "External call failed and retry policy is not set");
done();
elseif retry ~= nil and retry < 1 then
module:log("debug", "External call failed after retry")
done();
else
module:log("debug", "External call failed, retry nr %s", retry)
retry = retry - 1;
request = call_http()
return http_timeout;
end
end
end
timer.add_task(http_timeout, cancel);
wait();
return content;
end
return {
is_feature_allowed = is_feature_allowed;
is_healthcheck_room = is_healthcheck_room;
get_room_from_jid = get_room_from_jid;
async_handler_wrapper = async_handler_wrapper;
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;
};