initial commit
This commit is contained in:
@@ -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);
|
||||
@@ -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;
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user